logic_block: Support deprecated tick.

logic_block: Support deprecated tick.

--- engine/logic_block.coffee
+++ engine/logic_block.coffee
@@ -3,11 +3,17 @@
 class LogicBlock
     constructor: (@scene, @settings={})->
         scene = @scene
+        if not scene
+            throw 'No scene given to ' + @constructor.name
         @context = context = scene.context
         @init?()
         if @pre_draw_tick?
             scene.pre_draw_callbacks.push (scene)=> @pre_draw_tick.bind(@)(context.main_loop.frame_duration)

+        if @tick? #TODO: Delete it in future versions.
+            console.warn '"tick" is deprecated in LogicBlock, please use "pre_draw_tick" or "post_draw_tick" instead.'
+            scene.pre_draw_callbacks.push (scene)=> @tick.bind(@)(context.main_loop.frame_duration)
+
         if @post_draw_tick?
             scene.pre_draw_callbacks.push (scene)=> @post_draw_tick.bind(@)(context.main_loop.frame_duration)


refactor backendless views to look at file extension .html or .htm

--- src/scripts/backend/viewsBackend.coffee
+++ src/scripts/backend/viewsBackend.coffee
@@ -1,5 +1,5 @@
 class Run
 	constructor: ($log, $httpBackend) ->
-		$httpBackend.whenGET(/^\/views\//).passThrough()
+		$httpBackend.whenGET(/^.*\.(html|htm)$/).passThrough()

 angular.module('app').run ['$log', '$httpBackend', Run]


code tidy

code tidy

--- src/scripts/directives/d3DotGrid.coffee
+++ src/scripts/directives/d3DotGrid.coffee
@@ -121,12 +121,6 @@
 				columns = theGrid.selectAll("g")
 					.data(data)

-				###
-
-				columns = @container.selectAll("g")
-					.data(data)
-
-				###
 				columns.enter()
 					.append("g")


Update labeling and relocate AK and HI

Update labeling and relocate AK and HI

--- site/app.coffee
+++ site/app.coffee
@@ -31,7 +31,7 @@
       indexes.push(i)
 for i in indexes
   [lat, lon] = geodata.points[i]
-  geodata.points[i] = [lat+20, lon+20]
+  geodata.points[i] = [lat+5, lon+50]

 indexes = []
 for poly in geodata.locations['AK'].paths
@@ -41,7 +41,7 @@
 [minX, minY, maxX, maxY] = [Number.MAX_VALUE, Number.MAX_VALUE, Number.MIN_VALUE, Number.MIN_VALUE]
 for i in indexes
   [lat, lon] = geodata.points[i]
-  [newlat, newlon] = [lat-10, lon-62]
+  [newlat, newlon] = [lat-35, lon-45]
   geodata.points[i] = [newlat, newlon]
   [minX, minY, maxX, maxY] = [Math.min(minX,newlon), Math.min(minY,newlat), Math.max(maxX,newlon), Math.max(maxY,newlat)]
 [centerX, centerY] = [(minX+maxX)/2, (minY+maxY)/2]
@@ -195,10 +195,16 @@
     @canvasMap = $('#canvas_map')
     @canvasMap.mousemove((e) =>
       loc = @hitTest(e.offsetX, e.offsetY)
-      if loc?
+      if loc? and loc != 'AK' and loc != 'HI'
         $('#canvas_map').css('cursor','pointer')
+        @renderMap()
+        ctx = @canvasMap[0].getContext('2d')
+        ctx.font = 12 + 'px sans-serif'
+        ctx.fillStyle = '#eee'
+        ctx.fillText(loc, e.offsetX, e.offsetY)
       else
-        $('#canvas_map').css('cursor','auto'))
+        $('#canvas_map').css('cursor','auto')
+        @renderMap())
     @canvasChart = $('#canvas_chart')
     $(window).resize(() => @resizeCanvas())
     @pointerInput = new PointerInput(@canvasMap, @)
@@ -310,7 +316,6 @@
     }
     @ecef2ortho = get_ecef2ortho(@dlat, @dlon, @zoom, w, h)
     for loc in @locations
-      [cX, cY] = @locCenterOnMap(loc)
       for poly in geodata.locations[loc].paths
         ctx.beginPath()
         ln = 0
@@ -321,9 +326,13 @@
         ctx.fillStyle = @colors?[loc] ? '#ccc'
         ctx.fill()
         ctx.stroke()
-      ctx.font = 24 * Math.min(1, w / 3000) + 'px sans-serif'
-      ctx.fillStyle = 'Cyan'
-      ctx.fillText(loc, cX, cY)
+    # Draw AK and HI seperately
+    [cX, cY] = @locCenterOnMap('AK')
+    ctx.font = 12 + 'px sans-serif'
+    ctx.fillStyle = '#eee'
+    ctx.fillText('AK', cX, cY)
+    [cX, cY] = @locCenterOnMap('HI')
+    ctx.fillText('HI', cX, cY)
     return 0

   renderChart: () ->


#910 - skip logic styling

#910 - skip logic styling

--- jsapp/xlform_model_view/view.rowDetail.SkipLogic.coffee
+++ jsapp/xlform_model_view/view.rowDetail.SkipLogic.coffee
@@ -140,6 +140,12 @@

       @

+    attach_to: (target) ->
+      target.find('.skiplogic__expressionselect').remove()
+      super(target)
+
+      @$el.select2({ minimumResultsForSearch: -1 })
+
     constructor: (@operators) ->
       super()

@@ -148,10 +154,18 @@
     className: 'skiplogic__responseval'
     fill_value: (value) ->

+    attach_to: (target) ->
+      target.find('.skiplogic__responseval').remove()
+      super(target)
+
   class viewRowDetailSkipLogic.SkipLogicTextResponse extends viewRowDetailSkipLogic.Base
     render: () ->
       @setElement('<input placeholder="response value" class="skiplogic__responseval" type="text" />')
       @
+
+    attach_to: (target) ->
+      target.find('.skiplogic__responseval').remove()
+      super(target)

   class viewRowDetailSkipLogic.SkipLogicValidatingTextResponseView extends viewRowDetailSkipLogic.SkipLogicTextResponse
     render: () ->
@@ -170,6 +184,10 @@
     fill_value: (value) ->
       @$('input').val(value)

+    attach_to: (target) ->
+      target.find('.skiplogic__responseval').remove()
+      super(target)
+

   class viewRowDetailSkipLogic.SkipLogicDropDownResponse extends viewRowDetailSkipLogic.Base
     tagName: 'select'
@@ -185,6 +203,12 @@

       @

+    attach_to: (target) ->
+      target.find('.skiplogic__responseval').remove()
+      super(target)
+
+      @$el.select2({ minimumResultsForSearch: -1 })
+
     constructor: (@responses) ->
       super()

@@ -233,10 +257,10 @@
       @presenter.change_response @$response_value.val()

     change_operator: (@operator_picker_view) ->
-      @operator_picker_view.render()
-      @$operator_picker.replaceWith(@operator_picker_view.el)
-
-      @$operator_picker = @$('.skiplogic__expressionselect')
+      @operator_picker_view.render().attach_to(@$el)
+
+
+      @$operator_picker = @$('select.skiplogic__expressionselect')
       @bind_operator_picker()

     change_response: (@response_value_view) ->
@@ -244,9 +268,9 @@
       if @$('.skiplogic__responseval-wrapper').length > 0
         @$('.skiplogic__responseval-wrapper').replaceWith(@response_value_view.el)
       else
-        @$response_value.replaceWith(@response_value_view.el)
-
-      @$response_value = @$('.skiplogic__responseval')
+        @response_value_view.attach_to(@$el)
+
+      @$response_value = @$('select.skiplogic__responseval')

       @bind_response_value()


ImpacWidgetsSvc.refreshAll now calls show on widget with refreshCache set true

--- src/services/widgets/widgets.svc.coffee
+++ src/services/widgets/widgets.svc.coffee
@@ -127,7 +127,7 @@
           widgets = config.currentDashboard.widgets
           _.forEach(widgets, (w) ->
             w.isLoading ||= true
-            _self.show(w).then(
+            _self.show(w, true).then(
               (updatedWidget) ->
                 w.isLoading = false
               (errorResponse) ->


Explicitly set isSearchable:false for set attributes

--- src/spec/integration/testhelpers.coffee
+++ src/spec/integration/testhelpers.coffee
@@ -33,26 +33,26 @@
     { type: { name: 'reference', referenceTypeId: 'review' }, name: "attr-ref-review-#{lowerConstraint}", label: { en: "Attribute REFERENCE-REVIEW #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
     { type: { name: 'reference', referenceTypeId: 'comment' }, name: "attr-ref-comment-#{lowerConstraint}", label: { en: "Attribute REFERENCE-COMMENT #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
     { type: { name: 'reference', referenceTypeId: 'key-value-document' }, name: "attr-ref-key-value-#{lowerConstraint}", label: { en: "Attribute REFERENCE-KEY-VALUE-DOCUMENT #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
-    { type: { name: 'set', elementType: { name: 'text' } }, name: "attr-set-text-#{lowerConstraint}", label: { en: "Attribute SET-TEXT #{constraint}" }, attributeConstraint: constraint, isRequired: false, inputHint: 'SingleLine' }
-    { type: { name: 'set', elementType: { name: 'ltext' } }, name: "attr-set-ltext-#{lowerConstraint}", label: { en: "Attribute SET-LTEXT #{constraint}" }, attributeConstraint: constraint, isRequired: false, inputHint: 'SingleLine' }
-    { type: { name: 'set', elementType: { name: 'enum', values: [{ key: 'enum1', label: 'Enum1' }, { key: 'enum2', label: 'Enum2' }] } }, name: "attr-set-enum-#{lowerConstraint}", label: { en: "Attribute SET-ENUM #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'lenum', values: [{ key: 'lenum1', label: { en: 'Enum1' } }, { key: 'lenum2', label: { en: 'Enum2' } }] } }, name: "attr-set-lenum-#{lowerConstraint}", label: { en: "Attribute SET-LENUM #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'number' } }, name: "attr-set-number-#{lowerConstraint}", label: { en: "Attribute SET-NUMBER #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'boolean' } }, name: "attr-set-boolean-#{lowerConstraint}", label: { en: "Attribute SET-BOOLEAN #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'money' } }, name: "attr-set-money-#{lowerConstraint}", label: { en: "Attribute SET-MONEY #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'date' } }, name: "attr-set-date-#{lowerConstraint}", label: { en: "Attribute SET-DATE #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'time' } }, name: "attr-set-time-#{lowerConstraint}", label: { en: "Attribute SET-TIME #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'datetime' } }, name: "attr-set-datetime-#{lowerConstraint}", label: { en: "Attribute SET-DATETIME #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'product' } }, name: "attr-set-ref-product-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-PRODUCT #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'product-type' } }, name: "attr-set-ref-product-type-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-PRODUCT-TYPE #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'channel' } }, name: "attr-set-ref-channel-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-CHANNEL #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'state' } }, name: "attr-set-ref-state-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-STATE #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'zone' } }, name: "attr-set-ref-zone-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-ZONE #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'shipping-method' } }, name: "attr-set-ref-shipping-method-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-SHIPPING-METHOD #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'category' } }, name: "attr-set-ref-category-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-CATEGORY #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'review' } }, name: "attr-set-ref-review-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-REVIEW #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'comment' } }, name: "attr-set-ref-comment-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-COMMENT #{constraint}" }, attributeConstraint: constraint, isRequired: false }
-    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'key-value-document' } }, name: "attr-set-ref-key-value-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-KEY-VALUE-DOCUMENT #{constraint}" }, attributeConstraint: constraint, isRequired: false }
+    { type: { name: 'set', elementType: { name: 'text' } }, name: "attr-set-text-#{lowerConstraint}", label: { en: "Attribute SET-TEXT #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false, inputHint: 'SingleLine' }
+    { type: { name: 'set', elementType: { name: 'ltext' } }, name: "attr-set-ltext-#{lowerConstraint}", label: { en: "Attribute SET-LTEXT #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false, inputHint: 'SingleLine' }
+    { type: { name: 'set', elementType: { name: 'enum', values: [{ key: 'enum1', label: 'Enum1' }, { key: 'enum2', label: 'Enum2' }] } }, name: "attr-set-enum-#{lowerConstraint}", label: { en: "Attribute SET-ENUM #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'lenum', values: [{ key: 'lenum1', label: { en: 'Enum1' } }, { key: 'lenum2', label: { en: 'Enum2' } }] } }, name: "attr-set-lenum-#{lowerConstraint}", label: { en: "Attribute SET-LENUM #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'number' } }, name: "attr-set-number-#{lowerConstraint}", label: { en: "Attribute SET-NUMBER #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'boolean' } }, name: "attr-set-boolean-#{lowerConstraint}", label: { en: "Attribute SET-BOOLEAN #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'money' } }, name: "attr-set-money-#{lowerConstraint}", label: { en: "Attribute SET-MONEY #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'date' } }, name: "attr-set-date-#{lowerConstraint}", label: { en: "Attribute SET-DATE #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'time' } }, name: "attr-set-time-#{lowerConstraint}", label: { en: "Attribute SET-TIME #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'datetime' } }, name: "attr-set-datetime-#{lowerConstraint}", label: { en: "Attribute SET-DATETIME #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'product' } }, name: "attr-set-ref-product-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-PRODUCT #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'product-type' } }, name: "attr-set-ref-product-type-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-PRODUCT-TYPE #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'channel' } }, name: "attr-set-ref-channel-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-CHANNEL #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'state' } }, name: "attr-set-ref-state-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-STATE #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'zone' } }, name: "attr-set-ref-zone-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-ZONE #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'shipping-method' } }, name: "attr-set-ref-shipping-method-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-SHIPPING-METHOD #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'category' } }, name: "attr-set-ref-category-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-CATEGORY #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'review' } }, name: "attr-set-ref-review-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-REVIEW #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'comment' } }, name: "attr-set-ref-comment-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-COMMENT #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
+    { type: { name: 'set', elementType: { name: 'reference', referenceTypeId: 'key-value-document' } }, name: "attr-set-ref-key-value-#{lowerConstraint}", label: { en: "Attribute SET-REFERENCE-KEY-VALUE-DOCUMENT #{constraint}" }, attributeConstraint: constraint, isRequired: false, isSearchable: false }
   ]

 exports.mockProductType = ->


acually serve json on request

acually serve json on request

--- src/steroids/servers/BuildServer.coffee
+++ src/steroids/servers/BuildServer.coffee
@@ -80,6 +80,8 @@

       config.archives.push zipObject

+      res.json config
+
     @app.get "/appgyver/zips/project.zip", (req, res)->
       res.sendFile Paths.temporaryZip


add validation test

add validation test

--- test-integration/reading-publish.coffee
+++ test-integration/reading-publish.coffee
@@ -122,7 +122,34 @@
     @waitAnd(css: '.calendar-container')

-  @__it 'Publishes a Reading with opensAt to tomorrow and deletes', ->
+  @__it 'Shows Validation Error when saving a blank Reading', ->
+    @timeout 2 * 60 * 1000
+
+    title = "Test: #{@freshId()}"
+
+    @loginDev(TEACHER_USERNAME)
+
+    # Go to the bio dashboard
+    @waitClick(css: '[data-category="biology"]')
+
+    @waitClick(css: '.add-assignment .dropdown-toggle')
+    @waitClick(linkText: 'Add Reading')
+
+    @editReading
+      action: 'SAVE'
+
+    @waitAnd(css: '.assignment-name.has-error .required-hint')
+    .isDisplayed().then (isDisplayed) ->
+      expect(isDisplayed).to.be.true
+
+    @waitAnd(css: '.-assignment-due-date .form-control.empty ~ .required-hint')
+    .isDisplayed().then (isDisplayed) ->
+      expect(isDisplayed).to.be.true
+
+    @waitAnd(css: '.readings-required').isDisplayed().then (isDisplayed) ->
+      expect(isDisplayed).to.be.true
+
+
     @timeout 10 * 60 * 1000 # ~4min to publish a draft (plus mathjax CDN)

     title = "Test: #{@freshId()}"


Fix #41 - be less insulting if server didn't like the file ;-)

--- src/scripts/controllers/dapiControllers.coffee
+++ src/scripts/controllers/dapiControllers.coffee
@@ -325,7 +325,7 @@
       .error((data, status, headers) =>
         @uploading = false
         if status == 406
-          @add_error('The server found that flight boring...')
+          @add_error('No location data found in that file...')
         else
           @add_error('Upload failed: ' + data)
       )


list all files in the current dir:log to console by type

--- src/controllers/index.coffee
+++ src/controllers/index.coffee
@@ -50,7 +50,9 @@
     read_stream.on 'open', ->
       exec "ls -la", (error, stdout, stderr) ->
         sys.puts stdout
-        console.log stdout
+        console.log 'Error: ' + error
+        console.log 'Stdout: ' + stdout
+        console.log 'Stderr: ' + stderr
       #console.log read_stream
       #form_data.append 'photo', read_stream
       #console.log form_data.getHeaders()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine_game_state.coffee
+++ src/engine_game_state.coffee
@@ -50,7 +50,7 @@
         return self
       else
         I.currentState.cameras()
-
+    fadeIn:
     flash: (options={}) ->
       self.camera(options.camera).flash(options)
     objects: ->


Write jquery.turbolinks code

Write jquery.turbolinks code

--- src/jquery.turbolinks.coffee
+++ src/jquery.turbolinks.coffee
@@ -9,3 +9,15 @@
 ###

 $ = require?('jquery') || window.jQuery
+
+callbacks = []
+
+ready = ->
+  callback() for callback in callbacks
+
+$(ready)
+
+$.fn.ready = (callback) ->
+  callbacks.push(callback)
+
+$(document).on('page:change', ready)


Rework tip documentation and organization.

Methods are grouped and more obvious. Lessen and make obvious
method-specific comments.

Simplify some methods syntax-wise where possible. Cleanup some
mistakenly 'exposed' API.

--- src/js/jquery.hlf.tip.coffee
+++ src/js/jquery.hlf.tip.coffee
@@ -48,33 +48,34 @@
     factory jQuery, _, jQuery.hlf
 )(@, ($, _, hlf) ->

+  # It takes some more boilerplate to write the plugins. Any of this additional
+  # support API is put into a plugin specific namespace under `$.hlf`.
   hlf.tip =
-    # It takes some boilerplate to write the plugins. To namespace when
-    # extending any jQuery API, we use custom `toString` helpers. Plugin
-    # namespaces (under the root`$.hlf`) each have a `debug` flag for granular
-    # logging.
+    # To toggle debug logging for all instances of a plguin, use the `debug` flag.
     debug: off
+    # To namespace when extending any jQuery API, we use custom `toString` helpers.
     toString: _.memoize (context) ->
       switch context
         when 'event'  then '.hlf.tip'
         when 'data'   then 'hlf-tip'
         when 'class'  then 'js-tips'
         else 'hlf.tip'
+    # The plugin's default options is available as reference for values like sizes.

     # ❧

     # Tip Options
     # -----------
-    # NOTE: The plugin instance gets extended with the options.
+    #- NOTE: The plugin instance gets extended with the options.
     defaults: do (pre = 'js-tip-') ->

       # - `$viewport` is the element in which the tip must fit into. It is _not_
       #   the context, which stores the tip instance and by convention contains
       #   the triggers.
       $viewport: $ 'body'
-      # - `triggerContent` can be the name of the trigger element's attribute or a
-      #   function that provides custom content when given the trigger element.
-      triggerContent: null
+      # - `autoDirection` automatically changes the direction so the tip can
+      #   better fit inside the viewport.
+      autoDirection: on
       # - `cursorHeight` is the browser's cursor height. We need to know this to
       #   properly offset the tip to avoid cases of cursor-tip-stem overlap.
       cursorHeight: 12
@@ -84,9 +85,6 @@
       defaultDirection: ['bottom', 'right']
       # - `safeToggle` prevents orphan tips, since timers are sometimes unreliable.
       safeToggle: on
-      # - `autoDirection` automatically changes the direction so the tip can
-      #   better fit inside the viewport.
-      autoDirection: on
       # - `tipTemplate` should return interpolated html when given the
       #   additional container class list. Its context is the plugin instance.
       tipTemplate: (containerClass) ->
@@ -99,8 +97,13 @@
           </div>
         </div>
         """
-      # NOTE: The majority of presentation state logic is in the plugin
-      # stylesheet. We update the presentation state by using `classNames`.
+      # - `triggerContent` can be the name of the trigger element's attribute or a
+      #   function that provides custom content when given the trigger element.
+      triggerContent: null
+      # NOTE: For these tip plugins, the majority of presentation state logic is
+      # in the plugin stylesheet. We update the presentation state by using
+      # namespaced `classNames` generated in a closure.
+      #
       # - `classNames.stem` - Empty string to remove the stem.
       # - `classNames.follow` - Empty string to disable cursor following.
       classNames: do ->
@@ -135,13 +138,19 @@

     # SnapTip Options
     # ---------------
-    # These options extend the tip options.
+    #- NOTE: The plugin instance gets extended with the options.
     defaults: do (pre = 'js-snap-tip-') ->
+      # These options extend the tip options.
       $.extend (deep = yes), {}, hlf.tip.defaults,
-        # - `snap.toXAxis` is the switch for snapping along x-axis. Off by default.
-        # - `snap.toYAxis` is the switch for snapping along y-axis. Off by default.
-        # - `snap.toTrigger` is the switch snapping to trigger that builds on top of
-        #   axis-snapping. On by default.
+        # NOTE: For each snapping option, the plugin adds its class to the tip
+        # if the option is on.
+        #
+        # - `snap.toXAxis` is the switch for snapping along x-axis and only
+        #   tracking along y-axis. Off by default.
+        # - `snap.toYAxis` is the switch for snapping along y-axis and only
+        #   tracking along x-axis. Off by default.
+        # - `snap.toTrigger` is the switch for snapping to trigger built on
+        #   axis-snapping. On by default.
         snap:
           toTrigger: on
           toXAxis: off
@@ -154,7 +163,6 @@
             toYAxis:   'y-side'
             toTrigger: 'trigger'
           (classNames.snap[key] = "#{pre}#{value}") for own key, value of dictionary
-          # Update our tip class.
           classNames.tip = 'js-tip js-snap-tip'
           classNames

@@ -165,13 +173,13 @@
   # Read on to learn about implementation details.
   class Tip

-    # ❦ `constructor` keeps `$triggers` and `$context` as properties. `options`
+    # 𝒇 `constructor` keeps `$triggers` and `$context` as properties. `options`
     # is further normalized.
     constructor: (@$triggers, options, @$context) ->
       for own name, animation of options.animations when name isnt 'base'
         _.defaults animation, options.animations.base

-    # ❦ `init` offloads non-trivial setup to other subroutines
+    # 𝒇 `init` offloads non-trivial setup to other subroutines
     init: ->
       _.bindAll @, '_onTriggerMouseMove', '_setBounds'
       # Initialize tip element.
@@ -199,19 +207,9 @@
       @_processTriggers()
       @_bindTriggers()

-    _setTip: ($tip) => @$tip = @$el = $tip
-
-    # Process `$triggers` and setup content, event, and positioning aspects.
-    _processTriggers: ($triggers) ->
-      $triggers ?= @$triggers
-      $triggers.each (i, el) =>
-        $trigger = $ el
-        return no unless $trigger.length
-        $trigger.addClass @classNames.trigger
-        @_saveTriggerContent $trigger
-        @_updateDirectionByTrigger $trigger
-
-    # `_defaultHtml` provides a basic html structure for tip content. It can be
+    # ### Accessors
+
+    # 𝒇 `_defaultHtml` provides a basic html structure for tip content. It can be
     # customized via the `tipTemplate` external option, or by subclasses using
     # the `htmlOnRender` hook.
     _defaultHtml: ->
@@ -223,10 +221,147 @@
       containerClass = $.trim [@classNames.tip, @classNames.follow, directionClass].join ' '
       html = @tipTemplate containerClass

-    # `_saveTriggerContent` comes with a very simple base implementation that's
+    # 𝒇 `_isDirection` is a helper to deduce if `$tip` currently has the given
+    # `directionComponent`. The tip is considered to have the same direction as
+    # the given `$trigger` if it has the classes or if there is no trigger or
+    # saved direction value and the directionComponent is part of
+    # `defaultDirection`. Note that this latter check is placed last for
+    # performance savings.
+    _isDirection: (directionComponent, $trigger) ->
+      @$tip.hasClass(@classNames[directionComponent]) or (
+        (not $trigger? or not $trigger.data(@attr('direction'))) and
+        _.include(@defaultDirection, directionComponent)
+      )
+
+    # 𝒇 `_setState` is a simple setter that returns false if state doesn't change.
+    _setState: (state) ->
+      return no if state is @_state
+      @_state = state
+      @debugLog @_state
+
+    # 𝒇 `_setTip` aliases the conventional `$el` property to `$tip` for clarity.
+    _setTip: ($tip) => @$tip = @$el = $tip
+
+    # 𝒇 `_sizeForTrigger` does a stealth render via `_wrapStealthRender` to find tip
+    # size. It will return saved data if possible before doing a measure. The
+    # measures, used by `_updateDirectionByTrigger`, are stored on the trigger
+    # as namespaced, `width` and `height` jQuery data values. If on,
+    # `contentOnly` will factor in content padding into the size value for the
+    # current size.
+    _sizeForTrigger: ($trigger, contentOnly=no) ->
+      size =
+        width:  $trigger.data 'width'
+        height: $trigger.data 'height'
+
+      $content = @selectByClass('content')
+      if not (size.width? and size.height?)
+        $content.text $trigger.data @attr('content')
+        wrapped = @_wrapStealthRender ->
+          $trigger.data 'width',  (size.width = @$tip.outerWidth())
+          $trigger.data 'height', (size.height = @$tip.outerHeight())
+        wrapped()
+
+      if contentOnly is yes
+        padding = $content.css('padding').split(' ')
+        [top, right, bottom, left] = (parseInt side, 10 for side in padding)
+        bottom ?= top
+        left ?= right
+        size.width -= left + right
+        size.height -= top + bottom + @selectByClass('stem').height() # TODO: This isn't always true.
+
+      size
+
+    # 𝒇 `_stemSize` does a stealth render via `_wrapStealthRender` to find stem
+    # size. The stem layout styles will add offset to the tip content based on
+    # the tip direction. Knowing the size helps operations like overall tip
+    # positioning.
+    _stemSize: ->
+      key = @attr 'stem-size'
+      size = @$tip.data key
+      return size if size?
+
+      $content = @selectByClass 'content'
+      wrapped = @_wrapStealthRender =>
+        for direction, offset in $content.position()
+          if offset > 0
+            size = Math.abs offset
+            @$tip.data key, size
+        0
+      return wrapped()
+
+    # ### Appearance
+
+    # 𝒇 `wakeByTrigger` is the main toggler and a `_state` updater. It takes an
+    # `onWake` callback, which is usually to update position. The toggling and
+    # main changes only happen if the delay is passed. It will return a bool for
+    # success.
+    wakeByTrigger: ($trigger, event, onWake) ->
+      # - Store current trigger info.
+      triggerChanged = not $trigger.is @_$currentTrigger
+      if triggerChanged
+        @_inflateByTrigger $trigger
+        @_$currentTrigger = $trigger
+      # - Go directly to the position updating if no toggling is needed.
+      if @_state is 'awake'
+        @_positionToTrigger $trigger, event
+        @onShow triggerChanged, event
+        if onWake? then onWake()
+        @debugLog 'quick update'
+        return yes
+      if event? then @debugLog event.type
+      # - Don't toggle if awake or waking, or if event isn't `truemouseenter`.
+      return no if @_state in ['awake', 'waking']
+      # - Get delay and initial duration.
+      delay = @animations.show.delay
+      duration = @animations.show.duration
+      # - Our `wake` subroutine runs the timed-out logic, which includes the fade
+      #   transition. The latter is also affected by `safeToggle`. The `onShow`
+      #   and `afterShow` hook methods are also run.
+      wake = =>
+        @_positionToTrigger $trigger, event
+        @onShow triggerChanged, event
+        @$tip.stop().fadeIn duration, =>
+          if triggerChanged
+            onWake() if onWake?
+          if @safeToggle is on then @$tip.siblings(@classNames.tip).fadeOut()
+          @afterShow triggerChanged, event
+          @_setState 'awake'
+      # - Wake up depending on current state. If we are in the middle of
+      #   sleeping, stop sleeping by updating `_sleepCountdown` and wake up
+      #   sooner.
+      if @_state is 'sleeping'
+        @debugLog 'clear sleep'
+        clearTimeout @_sleepCountdown
+        duration = 0
+        wake()
+      # - Start the normal wakeup and update `_wakeCountdown`.
+      else if event? and event.type is 'truemouseenter'
+        triggerChanged = yes
+        @_setState 'waking'
+        @_wakeCountdown = setTimeout wake, delay
+      yes
+
+    # 𝒇 `sleepByTrigger` is a much simpler toggler compared to its counterpart
+    # `wakeByTrigger`. It also updates `_state` and returns a bool for success.
+    # As long as the tip isn't truly visible, or sleeping is redundant, it bails.
+    sleepByTrigger: ($trigger) ->
+      return no if @_state in ['asleep', 'sleeping']
+      @_setState 'sleeping'
+      clearTimeout @_wakeCountdown
+      @_sleepCountdown = setTimeout =>
+        @onHide()
+        @$tip.stop().fadeOut @animations.hide.duration, =>
+          @_setState 'asleep'
+          @afterHide()
+      , @animations.hide.delay
+      yes
+
+    # ### Content
+
+    # 𝒇 `_saveTriggerContent` comes with a very simple base implementation that
     # supports the common `title` and `alt` meta content for an element. Support
     # is also provided for the `triggerContent` option. We take that content and
-    # store it into a `content` jQuery data value on the trigger.
+    # store it into a namespaced `content` jQuery data value on the trigger.
     _saveTriggerContent: ($trigger) ->
       content = null
       attr = null
@@ -246,11 +381,35 @@
       if content?
         $trigger.data @attr('content'), content

-    # `_bindContext` uses MutationObserver. If `doLiveUpdate` is inferred to be
+    # ### Events
+
+    # 𝒇 `_bind` adds event handlers to `$tip` mostly, so state can be updated such
+    # that the handlers on `_$currentTrigger` make an exception. So that cursor
+    # leaving the trigger for the tip doesn't cause the tip to dismiss.
+    #
+    # Additionally, track viewport `_bounds` at a reasonable rate, so that
+    # `_updateDirectionByTrigger` can work properly.
+    _bind: ->
+      @$tip.on
+        mouseenter: (event) =>
+          @debugLog 'enter tip'
+          if @_$currentTrigger?
+            @_$currentTrigger.data @attr('is-active'), yes
+            @wakeByTrigger @_$currentTrigger
+        mouseleave: (event) =>
+          @debugLog 'leave tip'
+          if @_$currentTrigger?
+            @_$currentTrigger.data @attr('is-active'), no
+            @sleepByTrigger @_$currentTrigger
+      if @autoDirection is on
+        $(window).resize _.debounce @_setBounds, 300
+
+    # 𝒇 `_bindContext` uses MutationObserver. If `doLiveUpdate` is inferred to be
     # true, process triggers added in the future. Make sure to ignore mutations
     # related to the tip.
     _bindContext: ->
       return false unless window.MutationObserver?
+
       selector = @$triggers.selector
       @_mutationObserver = new MutationObserver (mutations) =>
         for mutation in mutations
@@ -264,14 +423,15 @@
         childList: yes
         subtree: yes

-    # `_bindTriggers` links each trigger to the tip for: 1) possible appearance
-    # changes during mouseenter, mouseleave (uses special events) and 2)
-    # following on mousemove only if `doFollow` is on. Also note for our
-    # `onMouseMove` handler, it's throttled by `requestAnimationFrame` when
-    # available, otherwise manually at hopefully 60fps.
+    # 𝒇 `_bindTriggers` links each trigger to the tip for:
+    # 1. Possible appearance changes during mouseenter, mouseleave (uses special
+    #    events).
+    # 2. Following on mousemove only if `doFollow` is on.
+    #
+    # Also note for our `onMouseMove` handler, it's throttled by `requestAnimationFrame`
+    # when available, otherwise manually at hopefully 60fps.
     _bindTriggers: ->
       selector = ".#{@classNames.trigger}"
-      # Base bindings.
       @$context.on [
           @evt('truemouseenter')
           @evt('truemouseleave')
@@ -285,7 +445,7 @@
             when 'truemouseleave' then @sleepByTrigger $(event.target)
             else @debugLog 'unknown event type', event.type
           event.stopPropagation()
-      # Follow binding.
+
       if @doFollow is on
         if window.requestAnimationFrame?
           onMouseMove = (event) =>
@@ -295,28 +455,88 @@
           onMouseMove = _.throttle @_onTriggerMouseMove, 16
         @$context.on 'mousemove', selector, onMouseMove

-    # `_bind` adds event handlers to `$tip`, mostly so state can be updated such
-    # that the handlers on `_$currentTrigger` make an exception. The desired
-    # behavior is the cursor leaving the trigger for the tip doesn't cause the
-    # tip to dismiss.
-    _bind: ->
+    # ### Positioning
+
+    # 𝒇 `_onTriggerMouseMove` is actually the main tip toggling handler. To
+    # explain, first we take into account of child elements triggering the mouse
+    # event by deducing the event's actual `$trigger` element. Then we
+    # `wakeByTrigger` if needed.
+    _onTriggerMouseMove: (event) ->
+      return no if not event.pageX?
+
+      $trigger =
+        if ($trigger = $(event.currentTarget)) and $trigger.hasClass @classNames.trigger then $trigger
+        else $trigger.closest(@classNames.trigger)
+      return no if not $trigger.length
+
+      @wakeByTrigger $trigger, event
+
+    # 𝒇 `_positionToTrigger` will properly update the tip offset per
+    # `offsetOnTriggerMouseMove` and `_isDirection`. Also note that `_stemSize`
+    # gets factored in.
+    _positionToTrigger: ($trigger, mouseEvent, cursorHeight=@cursorHeight) ->
+      return no if not mouseEvent?
+
+      offset =
+        top: mouseEvent.pageY
+        left: mouseEvent.pageX
+      offset = @offsetOnTriggerMouseMove(mouseEvent, offset, $trigger) or offset
+
+      if @_isDirection('top', $trigger)
+        offset.top -= @$tip.outerHeight() + @_stemSize()
+      else if @_isDirection('bottom', $trigger)
+        offset.top += @_stemSize() + cursorHeight
+
+      if @_isDirection('left',  $trigger)
+        tipWidth = @$tip.outerWidth()
+        triggerWidth = $trigger.outerWidth()
+        offset.left -= tipWidth
+        #- If direction changed due to tip being wider than trigger.
+        if tipWidth > triggerWidth
+          offset.left += triggerWidth
+
+      @$tip.css offset
+
+    # 𝒇 `_setBounds` updates `_bounds` per `$viewport`'s inner bounds, and those
+    # measures get used by `_updateDirectionByTrigger`.
+    _setBounds: ->
+      $viewport = if @$viewport.is('body') then $(window) else @$viewport
+      @_bounds =
+        top:    $.css @$viewport[0], 'padding-top', yes
+        left:   $.css @$viewport[0], 'padding-left', yes
+        bottom: $viewport.innerHeight()
+        right:  $viewport.innerWidth()
+
+    # ### Rendering
+
+    # 𝒇 `_inflateByTrigger` will reset and update `$tip` and its content element for the given trigger, so
+    # that it is ready to present, ie. it is 'inflated'. If the `resize` animation
+    # is desired, we need to also specify the content element's dimensions for
+    # respective transitions to take effect.
+    _inflateByTrigger: ($trigger) ->
+      $content = @selectByClass 'content'
+      $content.text $trigger.data @attr('content')
+
+      #- NOTE: A transition style is in place, so this causes animation.
+      if @animations.resize.enabled
+        contentSize = @_sizeForTrigger $trigger, (contentOnly = yes)
+        $content
+          .width contentSize.width
+          .height contentSize.height
+
+      compoundDirection =
+        if $trigger.data(@attr('direction')) then $trigger.data(@attr('direction')).split(' ')
+        else @defaultDirection
+      @debugLog 'update direction class', compoundDirection
       @$tip
-        .on 'mouseenter', (event) =>
-          @debugLog 'enter tip'
-          if @_$currentTrigger?
-            @_$currentTrigger.data @attr('is-active'), yes
-            @wakeByTrigger @_$currentTrigger
-        .on 'mouseleave', (event) =>
-          @debugLog 'leave tip'
-          if @_$currentTrigger?
-            @_$currentTrigger.data @attr('is-active'), no
-            @sleepByTrigger @_$currentTrigger
-      # Additionally, track viewport `_bounds` at a reasonable rate, so that
-      # `_updateDirectionByTrigger` can work properly.
-      if @autoDirection is on
-        $(window).resize _.debounce @_setBounds, 300
-
-    # `_render` comes with a base implementation that fills in and attaches
+        .removeClass _.chain(@classNames).pick('top', 'bottom', 'right', 'left').values().join(' ')
+        .addClass $.trim(
+          _.reduce compoundDirection, (classListMemo, directionComponent) =>
+            "#{classListMemo} #{@classNames[directionComponent]}"
+          , ''
+        )
+
+    # 𝒇 `_render` comes with a base implementation that fills in and attaches
     # `$tip` to the DOM, specifically at the beginning of `$viewport`. It uses
     # the result of `htmlOnRender` and falls back to that of `_defaultHtml`.
     # Render also sets up any animations per the `shouldAnimate` option.
@@ -325,99 +545,32 @@
       html = @htmlOnRender()
       if not (html? and html.length) then html = @_defaultHtml()
       $tip = $(html).addClass @classNames.follow
-      # Animation setup.
+
       transitionStyle = []
       if @animations.resize.enabled
         duration = @animations.resize.duration / 1000.0 + 's'
         easing = @animations.resize.easing
         transitionStyle.push "width #{duration} #{easing}", "height #{duration} #{easing}"
       transitionStyle = transitionStyle.join(',')
-      # /Animation setup.
+
       @_setTip $tip
       @selectByClass('content').css 'transition', transitionStyle
       @$tip.prependTo @$viewport

-    # `_inflateByTrigger` will reset and update `$tip` for the given trigger, so
-    # that it is ready to present, i.e. it is 'inflated'. Mostly it's just the
-    # content element and class list that get updated. If the `resize` animation
-    # is desired, we need to also specify the content element's dimensions for
-    # respective transitions to take effect.
-    _inflateByTrigger: ($trigger) ->
-      compoundDirection = if $trigger.data(@attr('direction')) then $trigger.data(@attr('direction')).split(' ') else @defaultDirection
-      @debugLog 'update direction class', compoundDirection
-      $content = @selectByClass 'content'
-      $content.text $trigger.data @attr('content')
-      if @animations.resize.enabled
-        contentSize = @sizeForTrigger $trigger, (contentOnly = yes)
-        $content
-          .width contentSize.width
-          .height contentSize.height
-      @$tip
-        .removeClass [
-          @classNames.top
-          @classNames.bottom
-          @classNames.right
-          @classNames.left
-        ].join ' '
-        .addClass $.trim(
-          _.reduce compoundDirection, (classListMemo, directionComponent) =>
-            "#{classListMemo} #{@classNames[directionComponent]}"
-          , ''
-        )
-
-    # `_onTriggerMouseMove` is actually the main tip toggling handler. To
-    # explain, first we take into account of child elements triggering the mouse
-    # event by deducing the event's actual `$trigger` element. Then we
-    # `wakeByTrigger` if needed.
-    _onTriggerMouseMove: (event) ->
-      return no if not event.pageX?
-      $trigger = if (
-        ($trigger = $(event.currentTarget)) and
-        $trigger.hasClass @classNames.trigger
-      ) then $trigger else $trigger.closest(@classNames.trigger)
-      return no if not $trigger.length
-      @wakeByTrigger $trigger, event
-
-    # `_positionToTrigger` will properly update the tip offset per
-    # `offsetOnTriggerMouseMove` and `isDirection`. Also note that `stemSize`
-    # gets factored in.
-    _positionToTrigger: ($trigger, mouseEvent, cursorHeight=@cursorHeight) ->
-      return no if not mouseEvent?
-      offset =
-        top: mouseEvent.pageY
-        left: mouseEvent.pageX
-      offset = @offsetOnTriggerMouseMove(mouseEvent, offset, $trigger) or offset
-      if @isDirection('top', $trigger)
-        offset.top -= @$tip.outerHeight() + @stemSize()
-      else if @isDirection('bottom', $trigger)
-        offset.top += @stemSize() + cursorHeight
-      if @isDirection('left',  $trigger)
-        tipWidth = @$tip.outerWidth()
-        triggerWidth = $trigger.outerWidth()
-        offset.left -= tipWidth
-        # If direction changed due to tip being wider than trigger.
-        if tipWidth > triggerWidth
-          offset.left += triggerWidth
-      @$tip.css offset
-
-    # `stemSize` does a stealth render via `_wrapStealthRender` to find stem
-    # `size. The stem layout styles will add offset to the tip content based on
-    # `the tip direction. Knowing the size helps operations like overall tip
-    # positioning.
-    stemSize: ->
-      key = @attr 'stem-size'
-      size = @$tip.data key
-      return size if size?
-      $content = @selectByClass 'content'
-      wrapped = @_wrapStealthRender =>
-        for direction, offset in $content.position()
-          if offset > 0
-            size = Math.abs offset
-            @$tip.data key, size
-        0
-      return wrapped()
-
-    # `_updateDirectionByTrigger` is the main provider of auto-direction
+    # ### Subroutines
+
+    # 𝒇 `_processTriggers` does just that and sets up content, event, and
+    # positioning aspects.
+    _processTriggers: ($triggers) ->
+      $triggers ?= @$triggers
+      $triggers.each (i, el) =>
+        $trigger = $ el
+        return no unless $trigger.length
+        $trigger.addClass @classNames.trigger
+        @_saveTriggerContent $trigger
+        @_updateDirectionByTrigger $trigger
+
+    # 𝒇 `_updateDirectionByTrigger` is the main provider of auto-direction
     # support. Given the `$viewport`'s `_bounds`, it changes to the best
     # direction as needed. The current `direction` is stored as jQuery data with
     # trigger.
@@ -426,7 +579,7 @@
       triggerPosition = $trigger.position()
       triggerWidth    = $trigger.outerWidth()
       triggerHeight   = $trigger.outerHeight()
-      tipSize         = @sizeForTrigger $trigger
+      tipSize         = @_sizeForTrigger $trigger
       newDirection    = _.clone @defaultDirection
       @debugLog { triggerPosition, triggerWidth, triggerHeight, tipSize }
       for component in @defaultDirection
@@ -446,51 +599,7 @@
             when 'left'   then newDirection[1] = 'right'
           $trigger.data @attr('direction'), newDirection.join ' '

-    # `_setBounds` updates `_bounds` per `$viewport`'s inner bounds, and those
-    # measures get used by `_updateDirectionByTrigger`.
-    _setBounds: ->
-      $viewport = if @$viewport.is('body') then $(window) else @$viewport
-      @_bounds =
-        top:    $.css @$viewport[0], 'padding-top', yes
-        left:   $.css @$viewport[0], 'padding-left', yes
-        bottom: $viewport.innerHeight()
-        right:  $viewport.innerWidth()
-
-    _setState: (state) ->
-      return no if state is @_state
-      @_state = state
-      @debugLog @_state
-
-    # `sizeForTrigger` does a stealth render via `_wrapStealthRender` to find tip
-    # size. It will return saved data if possible before doing a measure. The
-    # measures, used by `_updateDirectionByTrigger`, are stored on the trigger
-    # as namespaced, `width` and `height` jQuery data values. If on,
-    # `contentOnly` will factor in content padding into the size value for the
-    # current size.
-    sizeForTrigger: ($trigger, contentOnly=no) ->
-      # Short on existing data.
-      size =
-        width:  $trigger.data 'width'
-        height: $trigger.data 'height'
-      # Get size.
-      $content = @selectByClass('content')
-      if not (size.width? and size.height?)
-        $content.text $trigger.data @attr('content')
-        wrapped = @_wrapStealthRender ->
-          $trigger.data 'width',  (size.width = @$tip.outerWidth())
-          $trigger.data 'height', (size.height = @$tip.outerHeight())
-        wrapped()
-      # Get content size.
-      if contentOnly is yes
-        padding = $content.css('padding').split(' ')
-        [top, right, bottom, left] = (parseInt side, 10 for side in padding)
-        bottom ?= top
-        left ?= right
-        size.width -= left + right
-        size.height -= top + bottom + @selectByClass('stem').height() # TODO: This isn't always true.
-      size
-
-    # `_wrapStealthRender` is a helper mostly for size detection on tips and
+    # 𝒇 `_wrapStealthRender` is a helper mostly for size detection on tips and
     # triggers. Without stealth rendering the elements by temporarily un-hiding
     # and making invisible, we can't do `getComputedStyle` on them.
     _wrapStealthRender: (func) ->
@@ -505,86 +614,10 @@
           visibility: 'visible'
         return result

-    # `isDirection` is a helper to deduce if `$tip` currently has the given
-    # `directionComponent`. The tip is considered to have the same direction as
-    # the given `$trigger` if it has the classes or if there is no trigger or
-    # saved direction value and the directionComponent is part of
-    # `defaultDirection`. Note that this latter check is placed last for
-    # performance savings.
-    isDirection: (directionComponent, $trigger) ->
-      @$tip.hasClass(@classNames[directionComponent]) or (
-        (not $trigger? or not $trigger.data(@attr('direction'))) and
-        _.include(@defaultDirection, directionComponent)
-      )
-
-    # `wakeByTrigger` is the main toggler and a `_state` updater. It takes an
-    # `onWake` callback, which is usually to update position. The toggling and
-    # main changes only happen if the delay is passed. It will return a bool for
-    # success.
-    wakeByTrigger: ($trigger, event, onWake) ->
-      # Store current trigger info.
-      triggerChanged = not $trigger.is @_$currentTrigger
-      if triggerChanged
-        @_inflateByTrigger $trigger
-        @_$currentTrigger = $trigger
-      # Go directly to the position updating if no toggling is needed.
-      if @_state is 'awake'
-        @_positionToTrigger $trigger, event
-        @onShow triggerChanged, event
-        if onWake? then onWake()
-        @debugLog 'quick update'
-        return yes
-      if event? then @debugLog event.type
-      # Don't toggle if awake or waking, or if event isn't `truemouseenter`.
-      return no if @_state in ['awake', 'waking']
-      # Get delay and initial duration.
-      delay = @animations.show.delay
-      duration = @animations.show.duration
-      # Our `wake` subroutine runs the timed-out logic, which includes the fade
-      # transition. The latter is also affected by `safeToggle`. The `onShow`
-      # and `afterShow` hook methods are also run.
-      wake = =>
-        @_positionToTrigger $trigger, event
-        @onShow triggerChanged, event
-        @$tip.stop().fadeIn duration, =>
-          if triggerChanged
-            onWake() if onWake?
-          if @safeToggle is on then @$tip.siblings(@classNames.tip).fadeOut()
-          @afterShow triggerChanged, event
-          @_setState 'awake'
-      # Wake up depending on current state.
-      # If we are in the middle of sleeping, stop sleeping by updating
-      # `_sleepCountdown` and wake up sooner.
-      if @_state is 'sleeping'
-        @debugLog 'clear sleep'
-        clearTimeout @_sleepCountdown
-        duration = 0
-        wake()
-      # Start the normal wakeup and update `_wakeCountdown`.
-      else if event? and event.type is 'truemouseenter'
-        triggerChanged = yes
-        @_setState 'waking'
-        @_wakeCountdown = setTimeout wake, delay
-      yes
-
-    # `sleepByTrigger` is a much simpler toggler compared to its counterpart
-    # `wakeByTrigger`. It also updates `_state` and returns a bool for success.
-    # As long as the tip isn't truly visible, sleep is unneeded.
-    sleepByTrigger: ($trigger) ->
-      # Don't toggle if asleep or sleeping.
-      return no if @_state in ['asleep', 'sleeping']
-      @_setState 'sleeping'
-      clearTimeout @_wakeCountdown
-      @_sleepCountdown = setTimeout =>
-        @onHide()
-        @$tip.stop().fadeOut @animations.hide.duration, =>
-          @_setState 'asleep'
-          @afterHide()
-      , @animations.hide.delay
-      yes
-
-    # These methods are hooks for custom functionality from subclasses. Some are
-    # set to no-ops becase they are given no arguments.
+    # ### Delegation to Subclass
+
+    # These methods are hooks for custom functionality from subclasses. (Some are
+    # set to no-ops becase they are given no arguments.)
     onShow: (triggerChanged, event) -> undefined
     onHide: $.noop
     afterShow: (triggerChanged, event) -> undefined
@@ -592,65 +625,72 @@
     htmlOnRender: $.noop
     offsetOnTriggerMouseMove: (event, offset, $trigger) -> no

-  # SnapTip API
-  # -----------
+  # SnapTip Implementation
+  # ----------------------
   # With such a complete base API, extending it with an implementation with
   # snapping becomes almost trivial.
   class SnapTip extends Tip

-    # Continue setting up `$tip` and other properties.
+    # 𝒇 `init` continues setting up `$tip` and other properties.
     init: ->
       super()
-      # Infer `snap.toTrigger`.
+      # - Infer `snap.toTrigger`.
       if @snap.toTrigger is off
         @snap.toTrigger = @snap.toXAxis is on or @snap.toYAxis is on
-      # `_offsetStart` stores the original offset, which is used for snapping.
+      # - `_offsetStart` stores the original offset, which is used for snapping.
       @_offsetStart = null
-      # Add snapping config as classes.
+      # - Add snapping config as classes.
       @$tip.addClass(@classNames.snap[key]) for own key, active of @snap when active

-    # `_moveToTrigger` is the main positioner. The `baseOffset` given is expected
-    # to be the trigger offset.
+    # ### Events
+
+    # 𝒇 `_bindTriggers` extend its super to get initial position for snapping.
+    # This is only for snapping without snapping to the trigger, which is only
+    # what's currently supported. See `afterShow` hook.
+    _bindTriggers: ->
+      super()
+      selector = ".#{@classNames.trigger}"
+      #- Modify base binding.
+      @$context.on @evt('truemouseleave'), selector, { selector },
+        (event) => @_offsetStart = null
+
+
+    # ### Positioning
+
+    # 𝒇 `_moveToTrigger` is the main positioner. The `baseOffset` given is
+    # expected to be the trigger offset.
     _moveToTrigger: ($trigger, baseOffset) -> # TODO: Still needs to support all the directions.
-      #@debugLog baseOffset
+      #- @debugLog baseOffset
       offset = $trigger.position()
       toTriggerOnly = @snap.toTrigger is on and @snap.toXAxis is off and @snap.toYAxis is off
       if @snap.toXAxis is on
-        if @isDirection 'bottom', $trigger
+        if @_isDirection 'bottom', $trigger
           offset.top += $trigger.outerHeight()
         if @snap.toYAxis is off
-          # Note arbitrary buffer offset.
+          #- Note arbitrary buffer offset.
           offset.left = baseOffset.left - @$tip.outerWidth() / 2
       if @snap.toYAxis is on
-        if @isDirection 'right', $trigger
+        if @_isDirection 'right', $trigger
           offset.left += $trigger.outerWidth()
         if @snap.toXAxis is off
           offset.top = baseOffset.top - @$tip.outerHeight() / 2
       if toTriggerOnly is on
-        if @isDirection 'bottom', $trigger
+        if @_isDirection 'bottom', $trigger
           offset.top += $trigger.outerHeight()
       offset

-    # Extend `_bindTriggers` to get initial position for snapping. This is only
-    # for snapping without snapping to the trigger, which is only what's
-    # currently supported. See `afterShow` hook.
-    _bindTriggers: ->
-      super()
-      selector = ".#{@classNames.trigger}"
-      # Modify base binding.
-      @$context.on @evt('truemouseleave'), selector, { selector },
-        (event) => @_offsetStart = null
-
-    # Extend `_positionToTrigger` to set `cursorHeight` to 0, since it won't
-    # need to be factored in if we're snapping.
+    # 𝒇 `_positionToTrigger` extends its super to set `cursorHeight` to 0, since
+    # it won't need to be factored in if we're snapping.
     _positionToTrigger: ($trigger, mouseEvent, cursorHeight=@cursorHeight) ->
       super $trigger, mouseEvent, 0

-    # `onShow` and `afterShow` are implemented such that they make the tip
-    # invisible while it's being positioned and then reveal it.
+    # ### Tip Delegation
+
+    # 𝒇 Implement `onShow` and `afterShow` delegate methods such that they make
+    # the tip invisible while it's being positioned and then reveal it.
     onShow: (triggerChanged, event) ->
-      if triggerChanged is yes
-        @$tip.css 'visibility', 'hidden'
+      @$tip.css 'visibility', 'hidden' if triggerChanged is yes
+
     afterShow: (triggerChanged, event) ->
       if triggerChanged is yes
         @$tip.css 'visibility', 'visible'
@@ -658,13 +698,11 @@
           top: event.pageY
           left: event.pageX

-    # `offsetOnTriggerMouseMove` is implemented as the main snapping positioning
-    # handler. Instead of returning false, we return our custom, snapping
-    # offset, so it gets used in lieu of the base `offset`.
+    # 𝒇 Implement `offsetOnTriggerMouseMove` delegate method as the main snapping
+    # positioning handler. Instead of returning false, we return our custom,
+    # snapping offset, so it gets used in lieu of the base `offset`.
     offsetOnTriggerMouseMove: (event, offset, $trigger) ->
-      newOffset = _.clone offset
-      newOffset = @_moveToTrigger $trigger, newOffset
-      newOffset
+      newOffset = @_moveToTrigger $trigger, _.clone(offset)

   # Export
   # ------


HIRE-165: autocomplete fix

HIRE-165: autocomplete fix

--- src/main/java/app/app.coffee
+++ src/main/java/app/app.coffee
@@ -17,9 +17,9 @@
   RestangularProvider.setBaseUrl('/api/')
   tagsInputConfigProvider.setDefaults 'tagsInput',
   	minLength: 1
+  .setDefaults 'autoComplete',
+    minLength: 1
   return
-  tagsInputConfigProovider.setDefaults 'autoComplete',
-    minLength: 1
 .controller 'NavigationCtrl', ($scope, $location, Restangular) ->
   gitProperties = Restangular.one('gitProperties')
   gitProperties.get().then (data) ->


deactive a test about pluginsEngine.

deactive a test about pluginsEngine.

--- tests/spec/core/core-pluginsEngine-spec.coffee
+++ tests/spec/core/core-pluginsEngine-spec.coffee
@@ -40,7 +40,7 @@
 				expect(consolelogs[1][0]).toBe("Error while loading plugin plugin_test")
 				done()

-	it 'env.pluginsEngine.init inside of the \'instance_test\' folder should succeed after launching grunt command in that folder', (done) ->
+	xit 'env.pluginsEngine.init inside of the \'instance_test\' folder should succeed after launching grunt command in that folder', (done) ->
 		expect(env.pluginsEngine.init).toBeDefined()
 		expect(typeof env.pluginsEngine.init).toBe("function")


Wait for package activation promise

Wait for package activation promise

--- spec/package-generator-spec.coffee
+++ spec/package-generator-spec.coffee
@@ -3,29 +3,40 @@
 {fs, WorkspaceView} = require 'atom'

 describe 'Package Generator', ->
+  [activationPromise] = []

   beforeEach ->
     atom.workspaceView = new WorkspaceView
     atom.workspaceView.openSync('sample.js')
-    atom.packages.activatePackage("package-generator")
+    activationPromise = atom.packages.activatePackage("package-generator")

   describe "when package-generator:generate-package is triggered", ->
     it "displays a miniEditor", ->
       atom.workspaceView.trigger("package-generator:generate-package")
-      packageGeneratorView = atom.workspaceView.find(".package-generator")
-      expect(packageGeneratorView).toExist()
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        packageGeneratorView = atom.workspaceView.find(".package-generator")
+        expect(packageGeneratorView).toExist()

   describe "when core:cancel is triggered", ->
     it "detaches from the DOM and focuses the the previously focused element", ->
       atom.workspaceView.attachToDom()
       atom.workspaceView.trigger("package-generator:generate-package")
-      packageGeneratorView = atom.workspaceView.find(".package-generator").view()
-      expect(packageGeneratorView.miniEditor.isFocused).toBeTruthy()
-      expect(atom.workspaceView.getActiveView().isFocused).toBeFalsy()
-
-      packageGeneratorView.trigger("core:cancel")
-      expect(packageGeneratorView.hasParent()).toBeFalsy()
-      expect(atom.workspaceView.getActiveView().isFocused).toBeTruthy()
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        packageGeneratorView = atom.workspaceView.find(".package-generator").view()
+        expect(packageGeneratorView.miniEditor.isFocused).toBeTruthy()
+        expect(atom.workspaceView.getActiveView().isFocused).toBeFalsy()
+
+        packageGeneratorView.trigger("core:cancel")
+        expect(packageGeneratorView.hasParent()).toBeFalsy()
+        expect(atom.workspaceView.getActiveView().isFocused).toBeTruthy()

   describe "when a package is generated", ->
     [packageName, packagePath, packageRoot] = []
@@ -45,17 +56,26 @@
       packageName = "CamelCaseIsForTheBirds"
       packagePath = path.join(path.dirname(packagePath), packageName)
       atom.workspaceView.trigger("package-generator:generate-package")
-      packageGeneratorView = atom.workspaceView.find(".package-generator").view()
-      packageGeneratorView.miniEditor.setText(packagePath)
-      apmExecute = spyOn(packageGeneratorView, 'runCommand')
-      packageGeneratorView.trigger "core:confirm"
-
-      expect(apmExecute).toHaveBeenCalled()
-      expect(apmExecute.mostRecentCall.args[0]).toBe atom.packages.getApmPath()
-      expect(apmExecute.mostRecentCall.args[1]).toEqual ['init', '--package', "#{path.join(path.dirname(packagePath), "camel-case-is-for-the-birds")}"]
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        packageGeneratorView = atom.workspaceView.find(".package-generator").view()
+        packageGeneratorView.miniEditor.setText(packagePath)
+        apmExecute = spyOn(packageGeneratorView, 'runCommand')
+        packageGeneratorView.trigger "core:confirm"
+
+        expect(apmExecute).toHaveBeenCalled()
+        expect(apmExecute.mostRecentCall.args[0]).toBe atom.packages.getApmPath()
+        expect(apmExecute.mostRecentCall.args[1]).toEqual ['init', '--package', "#{path.join(path.dirname(packagePath), "camel-case-is-for-the-birds")}"]

     describe 'when creating a package', ->
-      beforeEach -> atom.workspaceView.trigger("package-generator:generate-package")
+      beforeEach ->
+        atom.workspaceView.trigger("package-generator:generate-package")
+
+        waitsForPromise ->
+          activationPromise

       describe "when the package is created outside of the packages directory", ->
         it "calls `apm init` and `apm link`", ->
@@ -101,7 +121,11 @@
             expect(enablePackage.mostRecentCall.args[0]).toBe packageName

     describe 'when creating a theme', ->
-      beforeEach -> atom.workspaceView.trigger("package-generator:generate-syntax-theme")
+      beforeEach ->
+        atom.workspaceView.trigger("package-generator:generate-syntax-theme")
+
+        waitsForPromise ->
+          activationPromise

       describe "when the theme is created outside of the packages directory", ->
         it "calls `apm init` and `apm link`", ->
@@ -149,23 +173,33 @@
       atom.workspaceView.attachToDom()
       fs.makeTreeSync(packagePath)
       atom.workspaceView.trigger("package-generator:generate-package")
-      packageGeneratorView = atom.workspaceView.find(".package-generator").view()
-
-      expect(packageGeneratorView.hasParent()).toBeTruthy()
-      expect(packageGeneratorView.error).not.toBeVisible()
-      packageGeneratorView.miniEditor.setText(packagePath)
-      packageGeneratorView.trigger "core:confirm"
-      expect(packageGeneratorView.hasParent()).toBeTruthy()
-      expect(packageGeneratorView.error).toBeVisible()
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        packageGeneratorView = atom.workspaceView.find(".package-generator").view()
+
+        expect(packageGeneratorView.hasParent()).toBeTruthy()
+        expect(packageGeneratorView.error).not.toBeVisible()
+        packageGeneratorView.miniEditor.setText(packagePath)
+        packageGeneratorView.trigger "core:confirm"
+        expect(packageGeneratorView.hasParent()).toBeTruthy()
+        expect(packageGeneratorView.error).toBeVisible()

     it "opens the package", ->
       atom.workspaceView.trigger("package-generator:generate-package")
-      packageGeneratorView = atom.workspaceView.find(".package-generator").view()
-      packageGeneratorView.miniEditor.setText(packagePath)
-      apmExecute = spyOn(packageGeneratorView, 'runCommand').andCallFake (command, args, exit) ->
-        process.nextTick -> exit()
-      loadPackage = spyOn(atom.packages, 'loadPackage')
-      packageGeneratorView.trigger "core:confirm"
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        packageGeneratorView = atom.workspaceView.find(".package-generator").view()
+        packageGeneratorView.miniEditor.setText(packagePath)
+        apmExecute = spyOn(packageGeneratorView, 'runCommand').andCallFake (command, args, exit) ->
+          process.nextTick -> exit()
+        loadPackage = spyOn(atom.packages, 'loadPackage')
+        packageGeneratorView.trigger "core:confirm"

       waitsFor ->
         atom.open.callCount is 1


Fix spec

Fix spec

--- spec/tree-view-package-spec.coffee
+++ spec/tree-view-package-spec.coffee
@@ -4469,7 +4469,7 @@
         [_, dragDropEvents] = eventHelpers.buildPositionalDragEvents(null, alphaDir.querySelector('.project-root-header'), '.tree-view')

         dropEvent = dragDropEvents.bottom
-        dropEvent.dataTransfer.setData('atom-tree-view-event', true)
+        dropEvent.dataTransfer.setData('atom-tree-view-root-event', true)
         dropEvent.dataTransfer.setData('from-window-id', treeView.rootDragAndDrop.getWindowId() + 1)
         dropEvent.dataTransfer.setData('from-root-path', etaDirPath)


fixing baseurl

fixing baseurl

--- src/coffee/services/baseurl.coffee
+++ src/coffee/services/baseurl.coffee
@@ -1,2 +1,2 @@
 angular.module 'IdleLands'
-  .factory 'BaseURL', -> 'http://localhost:3001' #http://api.idle.land'+  .factory 'BaseURL', -> 'http://api.idle.land'


fix(macAutocomplete): Changed menu to min width to prevent content getting cut off

Fixes #131

--- src/directives/autocomplete.coffee
+++ src/directives/autocomplete.coffee
@@ -121,9 +121,9 @@
       #
       positionMenu = ->
         if $menuScope.items.length > 0
-          $menuScope.style       = element.offset()
-          $menuScope.style.top  += element.outerHeight()
-          $menuScope.style.width = element.outerWidth()
+          $menuScope.style          = element.offset()
+          $menuScope.style.top     += element.outerHeight()
+          $menuScope.style.minWidth = element.outerWidth()

           angular.forEach $menuScope.style, (value, key) ->
             if not isNaN(+value) and angular.isNumber +value


Removed extra asterisks

Removed extra asterisks

--- src/models/organization.coffee
+++ src/models/organization.coffee
@@ -204,7 +204,7 @@
             daysArticle = if daysRemainder > 1 then 'days' else 'day'
             response += "#{upcomingEvent.name} in #{weeks} #{if weeks > 1 then 'weeks' else 'week'}#{if daysRemainder > 0 then ', ' + daysRemainder + ' ' + daysArticle}\n"
           else
-            response += "*#{upcomingEvent.name}* *#{if days > 1 then 'in *' + days + ' days*' else '*tomorrow*'}\n"
+            response += "*#{upcomingEvent.name}* #{if days > 1 then 'in *' + days + ' days*' else '*tomorrow*'}\n"

       return response
     resetHounding: () ->


Adding a fix to make the zero line appear as 0.00

- Sometimes it appears as -0.0000000178

--- app/assets/javascripts/charts/load_chart.js.coffee
+++ app/assets/javascripts/charts/load_chart.js.coffee
@@ -152,6 +152,8 @@
     LoadChartHelper.charts[@loadChartLocation() - 1] = chart
     LoadChartHelper.updateBrush(@loadChartLocation())

+    $("g.tick.zero text").text("0.00")
+
     chart

   setGlobalBrushFocus: ()->


Refactor

--- app/assets/javascripts/components/user_show.coffee
+++ app/assets/javascripts/components/user_show.coffee
@@ -7,8 +7,7 @@
     user: []
     weights: []
   addWeight: (weight) ->
-    weights = React.addons.update(@state.weights, { $unshift: [weight] })
-    weights = weights.sort((a,b) => new Date(b.registered) - new Date(a.registered))
+    weights = React.addons.update(@state.weights, { $unshift: [weight] }).sort((a,b) => new Date(b.registered) - new Date(a.registered))
     @setState weights: weights
   deleteWeight: (weight) ->
     index = @state.weights.indexOf weight


Show loading indicator for autocomplete

Show loading indicator for autocomplete

Closes #15435

--- app/assets/javascripts/gfm_auto_complete.js.coffee
+++ app/assets/javascripts/gfm_auto_complete.js.coffee
@@ -21,6 +21,24 @@
   # Milestones
   Milestones:
     template: '
${title}
'
+
+  Loading:
+    template: '
 Loading...
'
+
+  DefaultOptions:
+    sorter: (query, items, searchKey) ->
+      return items if items[0].name? and items[0].name is 'loading'
+
+      $.fn.atwho.default.callbacks.sorter(query, items, searchKey)
+    filter: (query, data, searchKey) ->
+      return data if data[0] is 'loading'
+
+      $.fn.atwho.default.callbacks.filter(query, data, searchKey)
+    beforeInsert: (value) ->
+      if value.indexOf('undefined')
+        @at
+      else
+        value

   # Add GFM auto-completion to all input fields, that accept GFM input.
   setup: (wrap) ->
@@ -53,18 +71,37 @@
     # Emoji
     @input.atwho
       at: ':'
-      displayTpl: @Emoji.template
+      displayTpl: (value) =>
+        if value.path?
+          @Emoji.template
+        else
+          @Loading.template
       insertTpl: ':${name}:'
+      data: ['loading']
+      callbacks:
+        sorter: @DefaultOptions.sorter
+        filter: @DefaultOptions.filter
+        beforeInsert: @DefaultOptions.beforeInsert

     # Team Members
     @input.atwho
       at: '@'
-      displayTpl: @Members.template
+      displayTpl: (value) =>
+        if value.username?
+          @Members.template
+        else
+          @Loading.template
       insertTpl: '${atwho-at}${username}'
       searchKey: 'search'
+      data: ['loading']
       callbacks:
+        sorter: @DefaultOptions.sorter
+        filter: @DefaultOptions.filter
+        beforeInsert: @DefaultOptions.beforeInsert
         beforeSave: (members) ->
           $.map members, (m) ->
+            return m if not m.username?
+
             title = m.name
             title += " (#{m.count})" if m.count

@@ -76,11 +113,21 @@
       at: '#'
       alias: 'issues'
       searchKey: 'search'
-      displayTpl: @Issues.template
+      displayTpl:  (value) =>
+        if value.title?
+          @Issues.template
+        else
+          @Loading.template
+      data: ['loading']
       insertTpl: '${atwho-at}${id}'
       callbacks:
+        sorter: @DefaultOptions.sorter
+        filter: @DefaultOptions.filter
+        beforeInsert: @DefaultOptions.beforeInsert
         beforeSave: (issues) ->
           $.map issues, (i) ->
+            return i if not i.title?
+
             id:     i.iid
             title:  sanitize(i.title)
             search: "#{i.iid} #{i.title}"
@@ -89,11 +136,18 @@
       at: '%'
       alias: 'milestones'
       searchKey: 'search'
-      displayTpl: @Milestones.template
+      displayTpl:  (value) =>
+        if value.title?
+          @Milestones.template
+        else
+          @Loading.template
       insertTpl: '${atwho-at}"${title}"'
+      data: ['loading']
       callbacks:
         beforeSave: (milestones) ->
           $.map milestones, (m) ->
+            return m if not m.title?
+
             id:     m.iid
             title:  sanitize(m.title)
             search: "#{m.title}"
@@ -102,11 +156,21 @@
       at: '!'
       alias: 'mergerequests'
       searchKey: 'search'
-      displayTpl: @Issues.template
+      displayTpl:  (value) =>
+        if value.title?
+          @Issues.template
+        else
+          @Loading.template
+      data: ['loading']
       insertTpl: '${atwho-at}${id}'
       callbacks:
+        sorter: @DefaultOptions.sorter
+        filter: @DefaultOptions.filter
+        beforeInsert: @DefaultOptions.beforeInsert
         beforeSave: (merges) ->
           $.map merges, (m) ->
+            return m if not m.title?
+
             id:     m.iid
             title:  sanitize(m.title)
             search: "#{m.iid} #{m.title}"
@@ -128,3 +192,7 @@
     @input.atwho 'load', 'mergerequests', data.mergerequests
     # load emojis
     @input.atwho 'load', ':', data.emojis
+
+    # This trigger at.js again
+    # otherwise we would be stuck with loading until the user types
+    $(':focus').trigger('keyup')


sdp: Add parse()

sdp: Add parse()

--- sdp.coffee
+++ sdp.coffee
@@ -116,4 +116,169 @@
       """
     return sdpBody.replace /\n/g, "\r\n"

+  # Turn an SDP string like this...
+  #
+  #   v=0
+  #   o=- 0 0 IN IP4 127.0.0.1
+  #   s=No Name
+  #   c=IN IP4 127.0.0.1
+  #   t=0 0
+  #   a=tool:libavformat 56.15.102
+  #   m=video 0 RTP/AVP 96
+  #   b=AS:501
+  #   a=rtpmap:96 H264/90000
+  #   a=fmtp:96 packetization-mode=1; sprop-parameter-sets=Z0IAFbtA8F/y4CA8IBCo,aM4HDs
+  #   g=; profile-level-id=420015
+  #   a=control:streamid=0
+  #   m=audio 0 RTP/AVP 97
+  #   b=AS:125
+  #   a=rtpmap:97 MPEG4-GENERIC/44100/2
+  #   a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdelta
+  #   length=3; config=1210
+  #   a=control:streamid=1
+  #
+  # ...into an object like this.
+  #
+  #   { version: '0',
+  #     origin:
+  #      { username: '-',
+  #        sessId: '0',
+  #        sessVersion: '0',
+  #        nettype: 'IN',
+  #        addrtype: 'IP4',
+  #        unicastAddress: '127.0.0.1' },
+  #     sessionName: 'No Name',
+  #     connectionData:
+  #      { nettype: 'IN',
+  #        addrtype: 'IP4',
+  #        connectionAddress: '127.0.0.1' },
+  #     timing: { startTime: '0', stopTime: '0' },
+  #     attributes: { tool: 'libavformat 56.15.102' },
+  #     media:
+  #      [ { media: 'video',
+  #          port: '0',
+  #          proto: 'RTP/AVP',
+  #          fmt: 96,
+  #          bandwidth: { bwtype: 'AS', bandwidth: '501' },
+  #          attributes:
+  #           { rtpmap: '96 H264/90000',
+  #             fmtp: '96 packetization-mode=1; sprop-parameter-sets=Z0IAFbtA8F/y4CA8IBCo,aM4HDsg=; profile-level-id=420015',
+  #             control: 'streamid=0' },
+  #          clockRate: 90000,
+  #          fmtpParams:
+  #           { 'packetization-mode': '1',
+  #             'sprop-parameter-sets': 'Z0IAFbtA8F/y4CA8IBCo,aM4HDsg=',
+  #             'profile-level-id': '420015' } },
+  #        { media: 'audio',
+  #          port: '0',
+  #          proto: 'RTP/AVP',
+  #          fmt: 97,
+  #          bandwidth: { bwtype: 'AS', bandwidth: '125' },
+  #          attributes:
+  #           { rtpmap: '97 MPEG4-GENERIC/44100/2',
+  #             fmtp: '97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=1210',
+  #             control: 'streamid=1' },
+  #          clockRate: 44100,
+  #          audioChannels: 2,
+  #          fmtpParams:
+  #           { 'profile-level-id': '1',
+  #             mode: 'AAC-hbr',
+  #             sizelength: '13',
+  #             indexlength: '3',
+  #             indexdeltalength: '3',
+  #             config: '1210' } } ] }
+  parse: (str) ->
+    session = {}
+    origParams = []
+    currentMedia = null
+    for line in str.split /\r?\n/
+      if line isnt ''
+        if (match = /^(.*?)=(.*)$/.exec line)?
+          key = match[1]
+          value = match[2]
+        else
+          throw new Error "Invalid SDP line: #{line}"
+        obj = {}
+        obj[ key ] = value
+        origParams.push obj
+
+        switch key
+          when 'v'  # Version
+            session.version = value
+          when 'o'  # Origin
+            params = value.split /\s+/
+            if params.length > 6
+              console.warn "SDP: Origin has too many parameters: #{line}"
+            session.origin =
+              username: params[0]
+              sessId: params[1]
+              sessVersion: params[2]
+              nettype: params[3]
+              addrtype: params[4]
+              unicastAddress: params[5]
+          when 's'  # Session Name
+            session.sessionName = value
+          when 'c'  # Connection Data
+            params = value.split /\s+/
+            if params.length > 3
+              console.warn "SDP: Connection Data has too many parameters: #{line}"
+            session.connectionData =
+              nettype: params[0]
+              addrtype: params[1]
+              connectionAddress: params[2]
+          when 't'  # Timing
+            params = value.split /\s+/
+            if params.length > 2
+              console.warn "SDP: Timing has too many parameters: #{line}"
+            session.timing =
+              startTime: params[0]
+              stopTime: params[1]
+          when 'a'  # Attributes
+            target = currentMedia ? session
+            if not target.attributes?
+              target.attributes = {}
+            if (match = /^(.*?):(.*)$/.exec value)?  # a=<attribute>:<value>
+              attrKey = match[1]
+              attrValue = match[2]
+              target.attributes[attrKey] = attrValue
+              if attrKey is 'rtpmap'
+                if (match = /\d+\s+.*?\/(\d+)(?:\/(\d+))?/.exec attrValue)?
+                  target.clockRate = parseInt match[1]
+                  if match[2]?
+                    target.audioChannels = parseInt match[2]
+              else if attrKey is 'fmtp'
+                if (match = /^\d+\s+(.*)$/.exec attrValue)?
+                  target.fmtpParams = {}
+                  for pair in match[1].split /;\s*/
+                    if (match = /^(.*?)=(.*)$/.exec pair)?
+                      target.fmtpParams[ match[1].toLowerCase() ] = match[2]
+            else  # a=<flag>
+              target.attributes[ value ] = true
+          when 'm'  # Media Descriptions
+            params = value.split /\s+/
+            currentMedia =
+              media: params[0]
+              port: params[1]
+              proto: params[2]
+              fmt: params[3]
+            if (currentMedia.proto is 'RTP/AVP') or (currentMedia.proto is 'RTP/SAVP')
+              currentMedia.fmt = parseInt currentMedia.fmt
+            if params.length >= 5
+              currentMedia.others = params[4]
+            if not session.media?
+              session.media = []
+            session.media.push currentMedia
+          when 'b'  # Bandwidth
+            params = value.split ':'
+            if params.length > 2
+              console.warn "SDP: Bandwidth has too many parameters: #{line}"
+            target = currentMedia ? session
+            target.bandwidth =
+              bwtype: params[0]
+              bandwidth: params[1]
+          else
+            console.warn "Unknown (not implemented) SDP: #{line}"
+
+    return session
+
 module.exports = api


niceNavigate for stop and route links

niceNavigate for stop and route links

--- app/assets/transit/lib/views/application.js.coffee
+++ app/assets/transit/lib/views/application.js.coffee
@@ -3,18 +3,19 @@
   el: 'div#container'

   events:
-  	'click .icon.favorite': 'toggleFavorite'
-  	'click a.btn-route': 'showRoute'
+    'click .icon.favorite': 'toggleFavorite'
+    'click a.btn-route': 'niceNavigate'
+    'click a.stop': 'niceNavigate'

   render: =>
     $(@el).html(@template())
     this

   toggleFavorite: (evt) =>
-  	$(evt.currentTarget).toggleClass('active')
+    $(evt.currentTarget).toggleClass('active')

   # gracefully navigates to the route page when a route button is clicked
   # way smoother and faster than standard link behavior!
-  showRoute: (evt) =>
-  	evt.preventDefault()
-  	Transit.router.navigate $(evt.currentTarget).attr('href'), trigger: true
+  niceNavigate: (evt) =>
+    evt.preventDefault()
+    Transit.router.navigate $(evt.currentTarget).attr('href'), trigger: true


Fix IE 9/10 host_ready event

Fix IE 9/10 host_ready event

--- app/js/jail_iframe/classes/loading_promises.coffee
+++ app/js/jail_iframe/classes/loading_promises.coffee
@@ -1,6 +1,11 @@
 setTimeout ->
+  ok =
+    loaded: true
+    interactive: !(document.documentMode < 11)
+    complete: true

-  if /^(interactive|complete)$/.test(document.readyState)
+
+  if ok[document.readyState]
     FactlinkJailRoot.host_ready_promise.resolve()
   else
     document.addEventListener('DOMContentLoaded', -> FactlinkJailRoot.host_ready_promise.resolve())
@@ -8,5 +13,10 @@
   if 'complete' == document.readyState
     FactlinkJailRoot.host_loaded_promise.resolve()
   else
-    window.addEventListener('load', -> FactlinkJailRoot.host_loaded_promise.resolve())
+    window.addEventListener('load', ->
+      FactlinkJailRoot.host_ready_promise.resolve()
+      #IE pre 11's readyStates are weird; to ensure we can't miss anything,
+      #trigger ready at the latest on load
+      FactlinkJailRoot.host_loaded_promise.resolve()
+    )
 , 0


Setup aiming angle

Setup aiming angle

--- app/scripts/components/behaviors/burst_shot.coffee
+++ app/scripts/components/behaviors/burst_shot.coffee
@@ -8,6 +8,9 @@
       burstCooldown: 800
       cooldown: 200
       burstAmount: 10
+      aim: 0
+      angle: 0
+      angleDeviation: 0
     )
     @currentBurst = 0
     @bind('GameLoop', @_checkForShot)
@@ -20,7 +23,23 @@
         @currentBurst = 0
       else
         return if @lastShotAt < @shootConfig.cooldown
-    angle = 0
+
+    self = this
+    aimAngle = 0
+    Crafty(@shootConfig.targetType).each ->
+      aimAngle = Math.atan2(self.y - @y, self.x - @x)
+      aimAngle *= 180 / Math.PI
+
+    angle = aimAngle
+    if angle > (@shootConfig.angle + @shootConfig.aim)
+      angle = @shootConfig.angle + @shootConfig.aim
+    else if angle < (@shootConfig.angle - @shootConfig.aim)
+      angle = @shootConfig.angle - @shootConfig.aim
+
+    # Add random deviation
+    angle = angle - (@shootConfig.angleDeviation // 2) + (Math.random() * @shootConfig.angleDeviation)
+
+    angle = (angle + 360) % 360
     @_shoot(angle) if @currentBurst < @shootConfig.burstAmount

   _shoot: (angle) ->


Wall dragging no longer conflicts with wall clicking.

--- app/views/editor/level/thangs/ThangsTabView.coffee
+++ app/views/editor/level/thangs/ThangsTabView.coffee
@@ -235,8 +235,7 @@
     if @paintingWalls
       # We need to stop painting walls, but we may also stop in onExtantThangSelected.
       _.defer =>
-        @paintingWalls = false
-        @surface.camera.dragDisabled = false
+        @paintingWalls = @paintedWalls = @surface.camera.dragDisabled = false
     else if @addThangSprite
       @surface.camera.lock()
       # If we click on the background, we need to add @addThangSprite, but not if onSpriteMouseUp will fire.
@@ -306,9 +305,11 @@
     @selectedExtantSprite?.setNameLabel? null unless @selectedExtantSprite is e.sprite
     @selectedExtantThang = e.thang
     @selectedExtantSprite = e.sprite
-    if @paintingWalls
-      @paintingWalls = false
-      @surface.camera.dragDisabled = false
+    paintedAWall = @paintedWalls
+    @paintingWalls = @paintedWalls = @surface.camera.dragDisabled = false
+    if paintedAWall
+      # Skip adding a wall now, because we already dragged to add one
+      null
     else if e.thang and (key.alt or key.meta)
       # We alt-clicked, so create a clone addThang
       @selectAddThangType e.thang.spriteName, @selectedExtantThang
@@ -412,6 +413,7 @@
       )
         @addThang @addThangType, @addThangSprite.thang.pos
         @lastAddTime = new Date()
+        @paintedWalls = true
     null

   onSurfaceMouseOver: (e) ->


Only setup follow buttons for new artists cells

--- apps/home/components/artists_to_follow/view.coffee
+++ apps/home/components/artists_to_follow/view.coffee
@@ -48,23 +48,25 @@

   _setupFollowButtons: ($el = null) ->
     view = @
-    following = new Following(null, kind: 'artist') if @user
+    $el ?= @$el
+    @following = new Following(null, kind: 'artist') if @user and not @following?

-    ids = @$('.follow-button').map ->
-      id = ($el = $(this)).data 'id'
+    ids = $el.find('.follow-button').map ->
+      view._addFollowButton $(this).data('id'), $(this)

-      new FollowButton
-        context_page: "Home page"
-        context_module: "Artists to Follow rail"
-        following: following
-        model: new Backbone.Model id: id
-        modelName: 'artist'
-        hideSuggestions: true
-        el: $el
-        href: _.findWhere(view.results, id: id)?.href
-      id
+    @following?.syncFollows ids

-    following?.syncFollows ids
+  _addFollowButton: (id, $el) ->
+    new FollowButton
+      context_page: "Home page"
+      context_module: "Artists to Follow rail"
+      following: @following
+      model: new Backbone.Model id: id
+      modelName: 'artist'
+      hideSuggestions: true
+      el: $el
+      href: _.findWhere(@results, id: id)?.href
+    id

   afterFollow: (el, model) =>
     metaphysics
@@ -77,5 +79,7 @@
         @carousel.cells.flickity.remove $(el).closest('.mgr-cell')
         artists = _.reject suggested_artists, (artist) =>
           _.contains(@results, artist)
-        appended = @carousel.cells.flickity.append $(cellsTemplate results: artists)
-        @_setupFollowButtons()
+        html = $(cellsTemplate results: artists)
+        @_setupFollowButtons html
+        appended = @carousel.cells.flickity.append html
+


Select options

Select options

--- source/runtime.coffee
+++ source/runtime.coffee
@@ -46,28 +46,73 @@
   node.nodeType is 11

 valueBind = (element, value) ->
-  element.value = value()
-
-  # Because firing twice with the same value is idempotent just binding both
-  # oninput and onchange handles the widest range of inputs and browser
-  # inconsistencies.
-  element.oninput = ->
-    value(element.value)
-  element.onchange = ->
-    value(element.value)
-
-  value.observe? (newValue) ->
-    element.value = newValue
+  value = Observable value

   switch element.nodeName
     when "SELECT"
+      updateSelected = (newValue) ->
+        # This is so we can hold a non-string object as a value of the select element
+        element._value = newValue
+
+        if options = element._options
+          element.selectedIndex = options.indexOf(newValue)
+        else
+          element.value = newValue
+
       # HACK: Need to set the value, but probably don't have the option contents yet
       # so let's just do it after our execution suspends and we probably have them
       setTimeout ->
-        element.value = value()
+        updateSelected(value())
       , 0

+      update = ->
+        {value:optionValue, _value} = @children[@selectedIndex]
+
+        value(_value or optionValue)
+
+      element.oninput = element.onchange = update
+      value.observe updateSelected
+    else
+      element.value = value()
+
+      # Because firing twice with the same value is idempotent just binding both
+      # oninput and onchange handles the widest range of inputs and browser
+      # inconsistencies.
+      element.oninput = ->
+        value(element.value)
+      element.onchange = ->
+        value(element.value)
+
+      value.observe (newValue) ->
+        element.value = newValue
+
   return
+
+specialBindings =
+  SELECT:
+    options: (element, values) ->
+      values = Observable values
+
+      updateValues = (values) ->
+        empty(element)
+        element._options = values
+
+        # TODO: Handle key: value... style options
+        # TODO: Should be able to leverage more of the runtime for binding observables here
+        values.map (value, index) ->
+          option = document.createElement("option")
+          option._value = value
+          option.value = value?.value or index
+          option.textContent = value?.name or value
+
+          element.appendChild option
+          #TODO: Should select the value if it matches elemnent value
+          element.selectedIndex = index if value is element._value
+
+          return option
+
+      updateValues values()
+      values.observe updateValues

 Runtime = (context) ->
   stack = []
@@ -171,18 +216,23 @@
   observeAttribute = (name, value) ->
     element = top()

+    {nodeName} = element
+
     update = (newValue) ->
       if newValue? and newValue != false
         element.setAttribute name, newValue
       else
         element.removeAttribute name

+    # TODO: Consolidate special bindings better than if/else
     if (name is "value") and (typeof value is "function")
       valueBind(element, value)
     else if (name is "checked") and (typeof value is "function")
       element.onchange = ->
         value element.checked
       bindObservable(element, value, update)
+    else if binding = specialBindings[nodeName]?[name]
+      binding(element, value)
     # Straight up onclicks, etc.
     else if name.match(/^on/) and isEvent(name.substr(2))
       element[name] = value
@@ -235,12 +285,12 @@
       parent = lastParent()

       # TODO: Work when rendering many sibling elements
-      items.observe (newItems) ->
-        replace elements, newItems
-
-      replace = (oldElements, items) ->
+      items.observe ->
+        replace elements
+
+      replace = (oldElements) ->
         elements = []
-        items.forEach (item, index, array) ->
+        items.each (item, index, array) ->
           element = fn.call(item, item, index, array)

           if isFragment(element)
@@ -260,3 +310,6 @@
   return self

 module.exports = Runtime
+
+empty = (node) ->
+  node.removeChild(child) while child = node.firstChild


better laziness spec

better laziness spec

--- spec/BaconSpec.coffee
+++ spec/BaconSpec.coffee
@@ -1426,7 +1426,7 @@
     id = (x) ->
       calls++
       x
-    sampler = Bacon.later(5)
+    sampler = Bacon.later(5).map(id)
     property = repeat(1, [1]).toProperty().map(id)
     sampled = property.sampledBy sampler
     sampled.onValue()


Add spec to reproduce #444

Add spec to reproduce #444

--- spec/Component.coffee
+++ spec/Component.coffee
@@ -872,6 +872,49 @@
           when '<' then sin1.post new IP 'openBracket'
           when '>' then sin1.post new IP 'closeBracket'
           else sin1.post new IP 'data', data
+
+    it 'should forward brackets to error port in async components', (done) ->
+      c = new component.Component
+        inPorts:
+          in:
+            datatype: 'string'
+        outPorts:
+          out:
+            datatype: 'string'
+          error:
+            datatype: 'object'
+        process: (input, output) ->
+          str = input.getData()
+          setTimeout ->
+            if typeof str isnt 'string'
+              return output.sendDone new Error 'Input is not string'
+            output.pass str.toUpperCase()
+          , 10
+
+      c.inPorts.in.attach sin1
+      c.outPorts.out.attach sout1
+      c.outPorts.error.attach sout2
+
+      cnt = 0
+      sout1.on 'ip', (ip) ->
+        # done new Error "Unexpectd IP: #{ip.type} #{ip.data}"
+
+      count = 0
+      sout2.on 'ip', (ip) ->
+        count++
+        switch count
+          when 1
+            chai.expect(ip.type).to.equal 'openBracket'
+          when 2
+            chai.expect(ip.type).to.equal 'data'
+            chai.expect(ip.data).to.be.an 'error'
+          when 3
+            chai.expect(ip.type).to.equal 'closeBracket'
+        done() if count is 3
+
+      sin1.post new IP 'openBracket', 'foo'
+      sin1.post new IP 'data', { bar: 'baz' }
+      sin1.post new IP 'closeBracket', 'foo'

     it 'should support custom bracket forwarding mappings with auto-ordering', (done) ->
       c = new component.Component


RootView talks about packages and packageModules, not extensions

Now you call `rootView.activatePackage`, etc

--- spec/tabs-spec.coffee
+++ spec/tabs-spec.coffee
@@ -11,7 +11,7 @@
     rootView = new RootView(require.resolve('fixtures/sample.js'))
     rootView.open('sample.txt')
     rootView.simulateDomAttachment()
-    rootView.activateExtension(Tabs)
+    atom.loadPackage("tabs")
     editor = rootView.getActiveEditor()
     tabs = rootView.find('.tabs').view()


Perform ScrollbarComponent DOM updates manually

--- src/scrollbar-component.coffee
+++ src/scrollbar-component.coffee
@@ -7,33 +7,19 @@
   displayName: 'ScrollbarComponent'

   render: ->
-    {presenter, orientation, className} = @props
-
-    switch orientation
-      when 'vertical'
-        @newState = presenter.state.verticalScrollbar
-      when 'horizontal'
-        @newState = presenter.state.horizontalScrollbar
+    {orientation, className} = @props

     style = {}

-    style.display = 'none' unless @newState.visible
     style.transform = 'translateZ(0)' # See atom/atom#3559
-    switch orientation
-      when 'vertical'
-        style.width = @newState.width
-        style.bottom = @newState.bottom
-      when 'horizontal'
-        style.left = 0
-        style.right = @newState.right
-        style.height = @newState.height
+    style.left = 0 if orientation is 'horizontal'

     div {className, style},
       switch orientation
         when 'vertical'
-          div className: 'scrollbar-content', style: {height: @newState.scrollHeight}
+          div ref: 'content', className: 'scrollbar-content'
         when 'horizontal'
-          div className: 'scrollbar-content', style: {width: @newState.scrollWidth}
+          div ref: 'content', className: 'scrollbar-content'

   componentDidMount: ->
     {orientation} = @props
@@ -43,18 +29,71 @@

     @getDOMNode().addEventListener 'scroll', @onScroll

+    @updateSync()
+
   componentWillUnmount: ->
     @getDOMNode().removeEventListener 'scroll', @onScroll

   componentDidUpdate: ->
-    {orientation} = @props
+    @updateSync()
+
+  updateSync: ->
+    {presenter, orientation} = @props
     node = @getDOMNode()

+    @oldState ?= {}
     switch orientation
       when 'vertical'
-        node.scrollTop = @newState.scrollTop
+        @newState = presenter.state.verticalScrollbar
+        @updateVertical()
       when 'horizontal'
-        node.scrollLeft = @newState.scrollLeft
+        @newState = presenter.state.horizontalScrollbar
+        @updateHorizontal()
+
+    if @newState.visible isnt @oldState.visible
+      if @newState.visible
+        node.style.display = ''
+      else
+        node.style.display = 'none'
+      @oldState.visible = @newState.visible
+
+  updateVertical: ->
+    node = @getDOMNode()
+
+    if @newState.width isnt @oldState.width
+      node.style.width = @newState.width + 'px'
+      @oldState.width = @newState.width
+
+    if @newState.bottom isnt @oldState.bottom
+      node.style.bottom = @newState.bottom + 'px'
+      @oldState.bottom = @newState.bottom
+
+    if @newState.scrollTop isnt @oldState.scrollTop
+      node.scrollTop = @newState.scrollTop
+      @oldState = @newState.scrollTop
+
+    if @newState.scrollHeight isnt @oldState.scrollHeight
+      @refs.content.getDOMNode().style.height = @newState.scrollHeight + 'px'
+      @oldState = @newState.scrollHeight
+
+  updateHorizontal: ->
+    node = @getDOMNode()
+
+    if @newState.height isnt @oldState.height
+      node.style.height = @newState.height + 'px'
+      @oldState.height = @newState.height
+
+    if @newState.right isnt @oldState.right
+      node.style.right = @newState.right + 'px'
+      @oldState.right = @newState.right
+
+    if @newState.scrollLeft isnt @oldState.scrollLeft
+      node.scrollLeft = @newState.scrollLeft
+      @oldState = @newState.scrollLeft
+
+    if @newState.scrollWidth isnt @oldState.scrollWidth
+      @refs.content.getDOMNode().style.width = @newState.scrollWidth + 'px'
+      @oldState = @newState.scrollWidth

   onScroll: ->
     {orientation, onScroll} = @props