update jetsync lib

update jetsync lib

--- lib/jetsync.coffee
+++ lib/jetsync.coffee
@@ -41,6 +41,10 @@
     console.assert(JetRetain.isRetain(subject), "Copy called on non-retain", subject)
     attributes = _.clone(subject.attributes)
     return new JetRetain(subject.start, subject.end, attributes)
+
+  # True if this retain contains part or all of other.
+  contains: (other) ->
+    return @start <= other.start and @end >= other.start

   getLength: ->
     return @end - @start
@@ -153,6 +157,23 @@
       index += length
     return changes

+  getDocIndex: (elemIndex) ->
+    return _.reduce(@deltas.slice(0, elemIndex), (length, elem) ->
+      return length + elem.getLength()
+    , 0)
+
+  getElemIndexAndOffset: (docIndex) ->
+    console.assert docIndex < @endLength, "docIndex of length #{docIndex} is
+      greater than or equal to delta length #{@endLength}"
+    count = 0
+    elemIndex = 0
+    for elem in @deltas
+      if count + elem.getLength() > docIndex
+        offset = docIndex - count
+        return [elemIndex, offset]
+      elemIndex += 1
+      count += elem.getLength()
+
   @copy: (subject) ->
     changes = []
     for delta in subject.deltas
@@ -231,36 +252,91 @@
   # 2. Retains in deltaC become retains in deltaB, which reference the original
   #    retain in deltaA.
   decompose: (deltaA, deltaC) ->
-    console.assert(JetDelta.isDelta(deltaA), "Decompose called when deltaA is not a JetDelta, type: " + typeof deltaA)
-    console.assert(JetDelta.isDelta(deltaC), "Decompose called when deltaC is not a JetDelta, type: " + typeof deltaC)
+    console.assert(JetDelta.isDelta(deltaA), "Decompose2 called when deltaA is not a JetDelta, type: " + typeof deltaA)
+    console.assert(JetDelta.isDelta(deltaC), "Decompose2 called when deltaC is not a JetDelta, type: " + typeof deltaC)
     console.assert(deltaA.startLength == deltaC.startLength, "startLength #{deltaA.startLength} / startLength #{deltaC.startLength} mismatch")
+
+    # Temporary lcs solution, lifted from
+    # http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Longest_common_substring
+    # This is also not the most optimal solution. This dp approach is O(n*m), but
+    # using a suffix tree is O(n + m).
+    # Returns the lcs, its length, and the offset into _str1_ that the lcs can
+    # be found.
+    longestCommonSubstring = `function (str1, str2) {
+      if (!str1 || !str2) {
+        return {
+          length: 0,
+          sequence: "",
+          offset: 0
+        };
+      }
+
+      var sequence = "",
+          str1Length = str1.length,
+          str2Length = str2.length,
+          num = new Array(str1Length),
+          maxlen = 0,
+          lastSubsBegin = 0;
+
+      for (var i = 0; i < str1Length; i++) {
+              var subArray = new Array(str2Length);
+              for (var j = 0; j < str2Length; j++)
+                      subArray[j] = 0;
+              num[i] = subArray;
+      }
+
+      for (var i = 0; i < str1Length; i++)
+      {
+              for (var j = 0; j < str2Length; j++)
+              {
+                      if (str1[i] !== str2[j])
+                              num[i][j] = 0;
+                      else
+                      {
+                              if ((i === 0) || (j === 0))
+                                      num[i][j] = 1;
+                              else
+                                      num[i][j] = 1 + num[i - 1][j - 1];
+
+                              if (num[i][j] > maxlen)
+                              {
+                                      maxlen = num[i][j];
+                                      var thisSubsBegin = i - num[i][j] + 1;
+                                      if (lastSubsBegin === thisSubsBegin)
+                                      {//if the current LCS is the same as the last time this block ran
+                                              sequence += str1[i];
+                                      }
+                                      else //this block resets the string builder if a different LCS is found
+                                      {
+                                              lastSubsBegin = thisSubsBegin;
+                                              sequence= ""; //clear it
+                                              sequence += str1.substr(lastSubsBegin, (i + 1) - lastSubsBegin);
+                                      }
+                              }
+                      }
+              }
+      }
+      return {
+              length: maxlen,
+              sequence: sequence,
+              offset: thisSubsBegin
+      };
+      }`
+
+    # Returns hash with keys _offsetA_, _offsetC_, _sequence_, _length_
+    getBestMatch = (strA, strC) ->
+      meta = longestCommonSubstring(strA, strC)
+      # Clean up the output of longestCommonSubstring for our purposes
+      meta['offsetA'] = meta.offset
+      meta['offsetC'] = strC.indexOf(meta.sequence)
+      delete meta.offset
+      return meta

     deltaA = JetDelta.copy(deltaA)
     deltaC = JetDelta.copy(deltaC)
     deltaA.normalizeChanges()
     deltaC.normalizeChanges()

-    getCommonPrefixLength = (a, b) ->
-      i = count = 0
-      len = Math.min(a.length, b.length)
-      while i < len
-        break if a.charAt(i) != b.charAt(i)
-        i += 1
-        count += 1
-      return count
-
-    advance = (elem, advanceBy, whichElem) ->
-      if advanceBy == elem.getLength()
-        deltaA.deltas.shift() if whichElem == "A"
-        deltaC.deltas.shift() if whichElem == "C"
-      else
-        if JetDelta.isInsert(elem)
-          elem.text = elem.text.substring(advanceBy)
-        else
-          console.assert JetDelta.isRetain(elem)
-          elem.start += advanceBy
-
-    # XXX: Define this on JetDelta?
     decomposeAttributes = (elemA, elemC) ->
       decomposedAttributes = {}
       attributes = elemA.attributes
@@ -273,75 +349,110 @@
           decomposedAttributes[key] = null
       return decomposedAttributes

+    retains = []
+    findRetain = (deltas, retain) ->
+      index = 0
+      docIndex = 0
+      for elem in deltas
+        if elem.start <= retain.start and elem.end > retain.start
+          docIndex += retain.start - elem.start
+          return {docIndex: docIndex, elemIndex: index}
+        else
+          docIndex += elem.getLength()
+          index += 1
+      console.assert false, "Retain #{retain} is in deltaC but not deltaA"
+    JetSync.findRetain = findRetain
+    for elem in deltaC.deltas
+      if JetDelta.isRetain(elem)
+        retains.push(findRetain(deltaA.deltas, elem))
+
+    leftSearchBound = 0 # docIndex we may start our search for a match at
+    decomposeInserts = (deltas, insert, docOffset = 0) ->
+      decomposed = []
+      if insert.getLength() == 0
+        return decomposed
+      else if deltas.length == 0
+        decomposed.push(insert)
+        return decomposed
+
+      bestElemIndex = bestDocIndex = bestMarker = undefined
+      index = docIndex = 0
+      # Find the best match for _insert_ across all _deltas_
+      for elem in deltas
+        if JetDelta.isInsert(elem)
+          marker = getBestMatch(elem.text, insert.text)
+          if marker.sequence != ""
+            if bestMarker == undefined or marker.length > bestMarker.length
+              bestMarker = marker
+              bestElemIndex = index
+              bestDocIndex = docIndex
+              if docOffset + docIndex + marker.offsetA + marker.length > leftSearchBound
+                leftSearchBound = docOffset + docIndex + marker.offsetA + marker.length
+        index += 1
+        docIndex += elem.getLength()
+      # If a match was found
+      if bestElemIndex != undefined
+        leftSlice = deltas.slice(0, bestElemIndex)
+        if deltas[bestElemIndex].text.substring(0, bestMarker.offsetA).length > 0
+          leftSlice.push(new JetInsert(deltas[bestElemIndex].text.substring(0, bestMarker.offsetA), deltas[bestElemIndex].attributes))
+        leftInsert = new JetInsert(insert.text.substring(0, bestMarker.offsetC), insert.attributes)
+        decomposed = decomposed.concat(decomposeInserts(leftSlice, leftInsert, docOffset))
+
+        start = docOffset + bestDocIndex + bestMarker.offsetA
+        decomposed.push(new JetRetain(start, start + bestMarker.length, decomposeAttributes(deltas[bestElemIndex], insert)))
+
+        rightSlice = deltas.slice(bestElemIndex + 1)
+        if deltas[bestElemIndex].text.substring(bestMarker.offsetA + bestMarker.length).length > 0
+          rightSlice.unshift(new
+          JetInsert(deltas[bestElemIndex].text.substring(bestMarker.offsetA + bestMarker.length), deltas[bestElemIndex].attributes))
+        rightInsert = new JetInsert(insert.text.substring(bestMarker.offsetC + bestMarker.length), insert.attributes)
+        decomposed = decomposed.concat(decomposeInserts(rightSlice, rightInsert, start + bestMarker.length))
+      else
+        decomposed.push(insert)
+      return decomposed
+    JetSync.decomposeInserts = decomposeInserts
+
     decomposed = []
-    docIndex = 0 # Character based index into deltaA
-    while deltaC.deltas.length > 0 and deltaA.deltas.length > 0
-      elemA = _.first(deltaA.deltas)
-      elemC = _.first(deltaC.deltas)
-      if JetDelta.isInsert(elemC)
-        commonPrefixLength = 0
-        lookAhead = 0
-        if JetDelta.isInsert(elemA)
-          while lookAhead < elemA.getLength()
-            commonPrefixLength = getCommonPrefixLength(elemC.text, elemA.text.substring(lookAhead))
-            if commonPrefixLength != 0 then break
-            lookAhead += 1
-        if commonPrefixLength > 0
-          docIndex += lookAhead
-          decomposed = decomposed.concat(new JetRetain(docIndex, docIndex + commonPrefixLength, decomposeAttributes(elemA, elemC)))
-          docIndex += commonPrefixLength
-          advance(elemA, lookAhead + commonPrefixLength, "A")
-          advance(elemC, commonPrefixLength, "C")
+    for elem in deltaC.deltas
+      if JetDelta.isInsert(elem)
+        rightSearchBound = if retains.length > 0 then _.first(retains).elemIndex else deltaA.deltas.length
+        if leftSearchBound >= deltaA.endLength
+          decomposed.push(new JetInsert(elem.text, elem.attributes))
+          continue
+        [leftElem, offset] = deltaA.getElemIndexAndOffset(leftSearchBound)
+        deltasToSearch = deltaA.deltas.slice(leftElem + 1, rightSearchBound)
+        if JetDelta.isInsert(deltaA.deltas[leftElem])
+          origInsert = deltaA.deltas[leftElem]
+          deltasToSearch.unshift(new JetInsert(origInsert.text.substring(offset), origInsert.attributes))
         else
-          take = 1
-          if JetDelta.isInsert(elemA)
-            index = 1
-            while index < elemC.text.length
-              commonPrefixLength = 0
-              lookAhead = 0
-              while lookAhead < elemA.getLength()
-                commonPrefixLength = getCommonPrefixLength(elemA.text.substring(lookAhead), elemC.text.substring(index))
-                if commonPrefixLength != 0 then break
-                lookAhead += 1
-              if commonPrefixLength == 0
-                take++
-                index++
-              else
-                break
+          origRetain = deltaA.deltas[leftElem]
+          deltasToSearch.unshift(new JetRetain(origRetain.start + offset, origRetain.end, origRetain.attributes))
+        decomposed = decomposed.concat(decomposeInserts(deltasToSearch, elem, leftSearchBound))
+      else
+        console.assert JetDelta.isRetain(elem), "Expecting retain but got #{typeof elem}"
+        console.assert retains.length > 0, "Shifting from the empty list"
+
+        matchingRetains = []
+        length = 0
+        indexes = retains.shift()
+        while length < elem.getLength()
+          retain = deltaA.deltas[indexes.elemIndex]
+          [elemIndex, offset] = deltaA.getElemIndexAndOffset(indexes.docIndex)
+          console.assert elemIndex == indexes.elemIndex, "Inconsistent indexes when matching retains. Got #{elemIndex} but expected #{indexes.elemIndex}"
+          retain = new JetRetain(retain.start + offset, retain.end, retain.attributes)
+          if length + retain.getLength() <= elem.getLength()
+            matchingRetains.push(new JetRetain(indexes.docIndex, indexes.docIndex + retain.getLength(), decomposeAttributes(retain, elem)))
+            length += retain.getLength()
+            indexes.docIndex += retain.getLength()
           else
-            # Take as much as we can while still leaving enough to cover the
-            # remainder of deltaA
-            deltaARemaining = deltaA.endLength - docIndex
-            if deltaARemaining >= elemC.getLength()
-              take = elemC.getLength()
-            else
-              take = elemC.getLength() - deltaARemaining
-          decomposed = decomposed.concat(new JetInsert(elemC.text.substr(0, take), _.clone(elemC.attributes)))
-          advance(elemC, take, "C")
-      else
-        # Search for the retain in deltaA that matches the retain elemC
-        deltasSeen = 0
-        for elem in deltaA.deltas
-          if JetInsert.isInsert(elem)
-            docIndex += elem.getLength()
-          else
-            if elem.start <= elemC.start && elem.end > elemC.start
-              take = Math.min(elemC.getLength(), elem.getLength())
-              docIndex += elemC.start - elem.start
-              decomposed = decomposed.concat(new JetRetain(docIndex, docIndex + take, decomposeAttributes(elem, elemC)))
-              docIndex += take
-              deltaA.deltas.splice(0, deltasSeen)
-              advance(elem, elemC.start - elem.start + take, "A")
-              advance(elemC, take, "C")
-              break
-            else
-              docIndex += elem.getLength()
-          deltasSeen++
-    while (deltaC.deltas.length > 0)
-      elemC = _.first(deltaC.deltas)
-      console.assert(JetDelta.isInsert(elemC), "Received Retain when expecting insert: #{elemC}")
-      decomposed = decomposed.concat(elemC)
-      deltaC.deltas.shift()
+            trim = length + retain.getLength() - elem.getLength()
+            matchingRetains.push(new JetRetain(indexes.docIndex, indexes.docIndex + retain.getLength() - trim, decomposeAttributes(retain, elem)))
+            length += retain.getLength() - trim
+            indexes.docIndex += retain.getLength() - trim
+          indexes.elemIndex += 1
+        leftSearchBound = indexes.docIndex
+        decomposed = decomposed.concat(matchingRetains)
+
     deltaB = new JetDelta(deltaA.endLength, deltaC.endLength, decomposed)
     deltaB.compact()
     console.assert(JetDelta.isDelta(deltaB), "Decomposed returning invalid JetDelta", deltaB)


Highlight current program element

Highlight current program element

--- src/config/controls.coffee
+++ src/config/controls.coffee
@@ -54,7 +54,15 @@
       _CHILDREN: (world) ->
         turtle = world.get('current')
         program = turtle.get('program')
-        program.map (signal) -> {_LABEL: signal}
+        counter = turtle.get('program_counter')
+        index = 0
+        result = []
+        for signal in program
+          dict = {_LABEL: signal}
+          dict['stroke'] = "yellow" if index == counter
+          result.push dict
+          index = index + 1
+        result
     }
     {_LABEL: "program_selector"}
   ]


Add atom prefix to rootView reference

Add atom prefix to rootView reference

--- lib/styleguide.coffee
+++ lib/styleguide.coffee
@@ -15,4 +15,4 @@
     atom.project.registerOpener (filePath) ->
       createStyleguideView(uri: styleguideUri) if filePath is styleguideUri

-    atom.rootView.command 'styleguide:show', -> rootView.open(styleguideUri)
+    atom.rootView.command 'styleguide:show', -> atom.rootView.open(styleguideUri)


Fix window print in wallet receive dialog

--- app/src/controllers/wallet/receive/wallet_receive_index_dialog_view_controller.coffee
+++ app/src/controllers/wallet/receive/wallet_receive_index_dialog_view_controller.coffee
@@ -36,7 +36,7 @@
     window.open 'mailto:?body=' + @_receivingAddress()

   print: ->
-    @renderedSelector.print()
+    window.print()

   _listenEvents: ->
     @view.amountInput.on 'keydown', (e) =>


Really stupid browserify bug

Really stupid browserify bug

Hey @jasonkarns wtf is happening here?

Before this patch, if I ran this locally:

```
$ rm dist/testdouble.js && npm run build && cat dist/testdouble.js | grep captor

> testdouble@0.3.1 prebuild /Users/justin/code/testdouble/testdouble.js
> npm run compile

> testdouble@0.3.1 compile /Users/justin/code/testdouble/testdouble.js
> coffee --output generated --compile lib

> testdouble@0.3.1 build /Users/justin/code/testdouble/testdouble.js
> browserify . --standalone testdouble --outfile $npm_package_config_build_file

```

So the grep didn't find the word captor (and a test failed).

After this patch, however:

```
$ rm dist/testdouble.js && npm run build && cat dist/testdouble.js | grep captor

> testdouble@0.3.1 prebuild /Users/justin/code/testdouble/testdouble.js
> npm run compile

> testdouble@0.3.1 compile /Users/justin/code/testdouble/testdouble.js
> coffee --output generated --compile lib

> testdouble@0.3.1 build /Users/justin/code/testdouble/testdouble.js
> browserify . --standalone testdouble --outfile $npm_package_config_build_file

    var captor;
    return captor = {
            captor.value = actual;
  var _, captor,
  captor = require('./captor');
    captor: captor,
},{"./captor":4,"lodash":12}],6:[function(require,module,exports){
```

What makes this even _weirder_ is that it didn't fail on CI. Here's
the relevant line proving that from the travis build:

https://travis-ci.org/testdouble/testdouble.js/builds/87193165#L359

What on earth? Does browserify just not know how to resolve /index?
But only locally?

--- lib/testdouble.coffee
+++ lib/testdouble.coffee
@@ -2,7 +2,7 @@
   create: require('./create')
   when: require('./when')
   verify: require('./verify')
-  matchers: require('./matchers')
+  matchers: require('./matchers/index')
   explain: require('./explain')
   reset: require('./store').reset
   version: process.env.npm_package_version || require('../package.json').version


update leftLabelHTML

update leftLabelHTML

--- snippets/core.nsL.cson
+++ snippets/core.nsL.cson
@@ -1,7 +1,7 @@
 '.source.nsl':
   '#define':
     body: '#define ${1:gflag}$2'
-    leftLabelHTML: "!"
+    leftLabelHTML: "!"
     prefix: 'define'
   '#if…#else':
     body: """
@@ -11,7 +11,7 @@
       \t${2:// your code here}
       #ifend$3
     """
-    leftLabelHTML: "!"
+    leftLabelHTML: "!"
     prefix: 'if…else'
   '#if':
     body: """
@@ -19,7 +19,7 @@
       \t${1:// your code here}
       #ifend$2
     """
-    leftLabelHTML: "!"
+    leftLabelHTML: "!"
     prefix: 'if'
   '#macro':
     body: """
@@ -27,7 +27,7 @@
       \t${1:// your code here}
       #macroend$2
     """
-    leftLabelHTML: "!"
+    leftLabelHTML: "!"
     prefix: 'macro'
   '#nsis':
     body: """
@@ -35,7 +35,7 @@
       \t${1:# your code here}
       #nsisend$2
     """
-    leftLabelHTML: "#"
+    leftLabelHTML: "#"
     prefix: 'nsis'
   'Abort()':
     body: 'Abort(${1});$2'
@@ -122,7 +122,7 @@
       case ${1:value}:
       \t${2:// your code here}
     """
-    leftLabelHTML: "#"
+    leftLabelHTML: "#"
     prefix: 'case'
   'ChangeUI()':
     body: 'ChangeUI(${1});$2'
@@ -553,7 +553,7 @@
       \t${2:// your code here}
       }
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'onInit()'
   'onInstFailed()':
     body: """
@@ -561,7 +561,7 @@
       \t${2:// your code here}
       }
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'onInstFailed()'
   'onInstSuccess()':
     body: """
@@ -569,7 +569,7 @@
       \t${2:// your code here}
       }
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'onInstSuccess()'
   'onMouseOverSection()':
     body: """
@@ -577,7 +577,7 @@
       \t${2:// your code here}
       }
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'onMouseOverSection()'
   'onRebootFailed()':
     body: """
@@ -585,7 +585,7 @@
       \t${2:// your code here}
       }
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'onRebootFailed()'
   'onSelChange()':
     body: """
@@ -593,7 +593,7 @@
       \t${2:// your code here}
       }
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'onSelChange()'
   'onUserAbort()':
     body: """
@@ -601,7 +601,7 @@
       \t${2:// your code here}
       }
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'onUserAbort()'
   'onVerifyInstDir()':
     body: """
@@ -609,7 +609,7 @@
       \t${2:// your code here}
       }
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'onVerifyInstDir()'
   'OutFile()':
     body: 'OutFile("${1:file_name}.exe");$2'
@@ -629,7 +629,7 @@
       \t${2:// your code here}
       }
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'page Components()'
   'page Directory()':
     body: """
@@ -637,7 +637,7 @@
       \t${2:// your code here}
       }
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'page Directory()'
   'page InstConfirm()':
     body: """
@@ -645,7 +645,7 @@
       \t${2:// your code here}
       }
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'page InstConfirm()'
   'page InstFiles()':
     body: """
@@ -653,7 +653,7 @@
       \t${2:// your code here}
       }
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'page InstFiles()'
   'page License()':
     body: """
@@ -661,7 +661,7 @@
       \t${2:// your code here}
       }
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'page License()'
   'Pop()':
     body: 'Pop(${1});$2'
@@ -749,7 +749,7 @@
       \t${4:// your code here}
       }$5
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'section()'
   'SectionGetFlags()':
     body: 'SectionGetFlags(${1});$2'
@@ -1093,7 +1093,7 @@
       \t${2:// your code here}
       }$3
     """
-    leftLabelHTML: "#"
+    leftLabelHTML: "#"
     prefix: 'switch()'
   'switch():1':
     body: """
@@ -1104,7 +1104,7 @@
       \t\t${4:// your code here}
       }$5
     """
-    leftLabelHTML: "#"
+    leftLabelHTML: "#"
     prefix: 'switch():1'
   'switch():2':
     body: """
@@ -1117,7 +1117,7 @@
       \t\t${6:// your code here}
       }$7
     """
-    leftLabelHTML: "#"
+    leftLabelHTML: "#"
     prefix: 'switch():2'
   'switch():3':
     body: """
@@ -1132,7 +1132,7 @@
       \t\t${8:// your code here}
       }$9
     """
-    leftLabelHTML: "#"
+    leftLabelHTML: "#"
     prefix: 'switch():3'
   'switch():4':
     body: """
@@ -1149,7 +1149,7 @@
       \t\t${10:// your code here}
       }$11
     """
-    leftLabelHTML: "#"
+    leftLabelHTML: "#"
     prefix: 'switch():4'
   'switch():5':
     body: """
@@ -1168,7 +1168,7 @@
       \t\t${12:// your code here}
       }$13
     """
-    leftLabelHTML: "#"
+    leftLabelHTML: "#"
     prefix: 'switch():5'
   'un.onGUIEnd()':
     body: """
@@ -1176,7 +1176,7 @@
       \t${2:// your code here}
       }$3
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'un.onGUIEnd()'
   'un.onGUIInit()':
     body: """
@@ -1184,7 +1184,7 @@
       \t${2:// your code here}
       }$3
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'un.onGUIInit()'
   'un.onInit()':
     body: """
@@ -1192,7 +1192,7 @@
       \t${2:// your code here}
       }$3
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'un.onInit()'
   'un.onRebootFailed()':
     body: """
@@ -1200,7 +1200,7 @@
       \t${2:// your code here}
       }$3
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'un.onRebootFailed()'
   'un.onSelChange()':
     body: """
@@ -1208,7 +1208,7 @@
       \t${2:// your code here}
       }$3
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'un.onSelChange()'
   'un.onUninstFailed()':
     body: """
@@ -1216,7 +1216,7 @@
       \t${2:// your code here}
       }$3
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'un.onUninstFailed()'
   'un.onUninstSuccess()':
     body: """
@@ -1224,7 +1224,7 @@
       \t${2:// your code here}
       }$3
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'un.onUninstSuccess()'
   'un.onUserAbort¡()':
     body: """
@@ -1232,7 +1232,7 @@
       \t${2:// your code here}
       }$3
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'un.onUserAbort¡()'
   'uninstall page Components()':
     body: """
@@ -1240,7 +1240,7 @@
       \t${2:// your code here}
       }$3
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'uninstall page Components()'
   'uninstall page Directory()':
     body: """
@@ -1248,7 +1248,7 @@
       \t${2:// your code here}
       }$3
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'uninstall page Directory()'
   'uninstall page InstFiles()':
     body: """
@@ -1256,7 +1256,7 @@
       \t${2:// your code here}
       }$3
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'uninstall page InstFiles()'
   'uninstall page License()':
     body: """
@@ -1264,7 +1264,7 @@
       \t${2:// your code here}
       }$3
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'uninstall page License()'
   'uninstall page uninstConfirm()':
     body: """
@@ -1272,7 +1272,7 @@
       \t${2:// your code here}
       }$3
     """
-    leftLabelHTML: "E"
+    leftLabelHTML: "E"
     prefix: 'uninstall page uninstConfirm()'
   'UninstallButtonText()':
     body: 'UninstallButtonText("${1:text}");$2'
@@ -1312,7 +1312,7 @@
       \t${2:// your code here}
       }$3
     """
-    leftLabelHTML: "#"
+    leftLabelHTML: "#"
     prefix: 'while()'
   'WindowIcon()':
     body: 'WindowIcon(${1});$2'


protocol: Start converting responses at request location

--- lib/runtime.coffee
+++ lib/runtime.coffee
@@ -437,19 +437,24 @@
     else
         return Promise.resolve([])

-
+# send a single message to device and back
 sendMessage = (runtime, message) ->
-  temp = new commandstream.Buffer 1024
+  temp = new commandstream.Buffer commandstream.cmdFormat.commandSize
   g = runtime.graph
   index = commandstream.toCommandStreamBuffer message, runtime.library, g.nodeMap, g.componentMap, temp, 0
   data = temp.slice(0, index)
-  return runtime.device.request data
+  return runtime.device.request(data).then (responseCmd) ->
+    responses = commandstream.fromCommand runtime.library, runtime.graph, responseCmd
+    if responses.length != 1
+      throw new Error("Expected single response to request #{message}")
+    response = responses[0]
+    return response

 handleNetworkCommand = (command, payload, connection, runtime, transport, debugLevel) ->
-    if command in ['start', 'stop', 'getstatus']
-        m = { protocol: 'network', command: command, payload: payload }
-        return sendMessage(runtime, m).then () ->
-          sendAck connection, m
+    if command in ['start', 'stop', 'getstatus']
+        req = { protocol: 'network', command: command, payload: payload }
+        return sendMessage(runtime, req).then (resp) ->
+          sendAck connection, resp
     else if command in ['debug']
         # does nothing relevant, response not expected
         return Promise.resolve()


AppController: Collaboration related changes.

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -127,6 +127,9 @@
       if app instanceof IDEAppController
         KD.singletons.windowController.notifyWindowResizeListeners()

+
+    @firebase = new Firebase 'https://ace-tryout.firebaseio.com'
+
   bindRouteHandler: ->
     {router, mainView} = KD.singletons

@@ -473,6 +476,7 @@
       ideViewLength  = 0
       ideViewLength += ideView.tabView.panes.length  for ideView in @ideViews
       delete @generatedPanes[pane.view.hash]
+      @serialize()

       @statusBar.showInformation()  if ideViewLength is 0

@@ -483,8 +487,14 @@
       @logChange change

   registerPane: (pane) ->
+    {view} = pane
+    unless view?.hash?
+      return warn 'view.hash not found, returning'
+
     @generatedPanes or= {}
-    @generatedPanes[pane.view.hash] = yes
+    @generatedPanes[view.hash] = yes
+    @serialize()
+
     view.on 'ChangeHappened', (change) =>
       @logChange change

@@ -674,10 +684,9 @@
     panes = []

     @forEachSubViewInIDEViews_ (pane) ->
-      panes.push pane.serialize()
-
-    firebase = new Firebase 'https://ace-tryout.firebaseio.com/'
-    firebase.child('ali').set data: JSON.stringify panes
+      panes.push pane.hash
+
+    @firebase.child(KD.nick()).child('panels').set panes

   updateContent: (data) ->
@@ -689,9 +698,7 @@

   deserialize: ->
-    firebase = new Firebase 'https://ace-tryout.firebaseio.com'
-
-    firebase.child('ali').child('data').on 'value', (data) =>
+    @firebase.child('ali').child('data').on 'value', (data) =>
       snapshot = JSON.parse data.val()

       for pane in snapshot
@@ -731,3 +738,31 @@
     @firebase.child(KD.nick()).child('changes').push change

+  startListening: (username) ->
+    @firebase.child(username).child('changes').on 'child_added', (snapshot) =>
+      change = snapshot.val()
+      return if change.origin is KD.nick()
+
+      if change.type is 'NewPaneCreated'
+        {context} = change
+        if context.paneType is 'editor'
+          {path, content} = context.file
+          return if path.indexOf('localfile:/') > -1
+
+          file = FSHelper.createFileInstance path
+          file.paneHash = context.paneHash
+          @openFile file, content
+
+      else if change.type is 'CursorActivity'
+        @forEachSubViewInIDEViews_ 'editor', (view) ->
+          {paneHash, cursor} = change.context
+
+          if view.hash is paneHash
+            view.setCursor cursor
+
+      else if change.type is 'ContentChange'
+        @forEachSubViewInIDEViews_ 'editor', (view) ->
+          {paneHash, content} = change.context
+
+          if view.hash is paneHash
+            view.setContent content


Setup class hierarchy, add helper methods

--- client/lanes/models/Base.coffee
+++ client/lanes/models/Base.coffee
@@ -5,7 +5,10 @@
         errors: 'object'
         changes: { type: 'collection', setOnce: true }
         lastServerMessage: { type: 'string' }
+        parent: 'state'
+
     derived:
+
         errorMessage:
             deps:['errors'], fn: ->
                 if !@errors then ''
@@ -54,12 +57,20 @@
     # This allows a subclass to specify the model type that should be checked.
     modelForAccess: -> this

+    # is the record saved
+    isPersistent: ->
+        !!( this.api_path() && !this.isNew() )
+
     # used by PubSub to record a remote change to the model
     addChangeSet: (change)->
         this.changes ||= new Lanes.Models.ChangeSetCollection( parent: this )
         change.record = this
         change = this.changes.add(change)
         this.set( change.value() )
+
+    api_path: ->
+        path = if this.FILE then _.last(this.FILE.path) else ''
+        _.pluralize(_.dasherize(path))

     urlRoot: ->
         Lanes.Models.Config.api_path + '/' + _.result(this,'api_path')
@@ -73,19 +84,11 @@
         return base + encodeURIComponent(this.getId())

-    # A record is considered loaded if it has the id set and some attributes set
-    isLoaded: ->
-        !this.isNew() && !_.isEmpty( _.omit(this.attributes,this.idAttribute) )
-
-    # is the record saved
-    isPersistent: ->
-        !!( this.api_path && ! this.isNew() )
-
     # Ensures the assocations given in "needed" are loaded
     withAssociations: (names...,options={})->
-        scope = options.scope || this
         if _.isString(options)
             names.push(options); options={}
+        scope = options.scope || this
         needed = this.associations.nonLoaded(this,names)
         if _.isEmpty( needed )
             options.success.call(scope, this  ) if options.success
@@ -108,7 +111,14 @@
             new this(attrs,options)

     # Calls Ampersand State's set method, then sets any associations that are present as well
-    set: (attrs,options)->
+    set: (key, value, options)->
+        # Handle both `"key", value` and `{key: value}` -style arguments.
+        if _.isObject(key) || key == null
+            attrs = key;
+            options = value;
+        else
+            attrs = {}
+            attrs[key] = value
         super
         this.associations.set(this,attrs) if this.associations
         this
@@ -152,9 +162,12 @@
         options = _.clone(options)
         handlers = Lanes.Models.Sync.wrapRequest(this,options)
         _.extend(options,{limit:1,ignoreUnsaved:true})
-
         this.sync('read', this, options)
         handlers.promise
+
+    @fetch: (options={})->
+        record = new this()
+        record.fetch(options)

     # Removes the model's record from the server (if it is persistent)
     # and then fires the "destroy" event
@@ -186,7 +199,8 @@
             data = this.getAttributes(props:true, true)
         else
             data = this.unsavedAttributes()
-        _.extend(data, this.associations.dataForSave(this, options)) if this.associations
+        if this.associations && (!_.isEmpty(data) || !this.isNew())
+            _.extend(data, this.associations.dataForSave(this, options))
         data

@@ -197,7 +211,49 @@

     # True if the model has "name" as eitehr a prop or session attribute
     hasAttribute: (name)->
-        !!this._definition[name]
+        !! (this._definition[name] || this._derived[name])
+
+    # Uses Lanes.Models.Sync by default
+    sync: (options...)->
+        Lanes.Models.Sync.perform(options...)
+
+    # When the model is extended it auto-creates the created_at and updated_at
+    # and sets up the AssociationMap
+    @extended: (klass)->
+        return if klass::abstractClass
+        klass::props   ||= {}
+        klass::session ||= {}
+
+        klass::session['created_at'] ||= 'date'
+        klass::session['updated_at'] ||= 'date'
+
+        if klass::associations
+            klass::associations = new Lanes.Models.AssocationMap(klass)
+
+    @afterExtended: (klass)->
+        if !klass::abstractClass && !klass.Collection
+            class DefaultCollection
+                constructor: -> super
+                model: klass
+            klass.Collection = Lanes.Models.Collection.extend(DefaultCollection)
+
+    Lanes.lib.ModuleSupport.includeInto(@)
+    @include Lanes.lib.results
+
+
+class State
+    constructor: -> super
+
+Lanes.Models.State = Lanes.lib.MakeBaseClass( Lanes.Vendor.Ampersand.State, State )
+
+# ------------------------------------------------------------------ #
+# The BasicModel is just a very thin layer over State                #
+# ------------------------------------------------------------------ #
+class BasicModel
+    constructor: -> super
+    isModel: true
+    isPersistent: ->
+        false

     # Check if an attribute named "name" can be set to "value"
     # Returns an empty string if value, and an appropriate error message if not
@@ -207,46 +263,12 @@
             "Cannot be empty"
         else
             ''
-    # Use Sync directly
-    sync: Lanes.Models.Sync.perform
-
-    # When the model is extended it auto-creates the created_at and updated_at
-    # and sets up the AssociationMap
-    @extended: (klass)->
-        return if klass::abstractClass
-        klass::props   ||= {}
-        klass::session ||= {}
-
-        klass::session['created_at'] ||= 'date'
-        klass::session['updated_at'] ||= 'date'
-
-        if klass::associations
-            klass::associations = new Lanes.Models.AssocationMap(klass)
-
-    @afterExtended: (klass)->
-        if !klass::abstractClass && !klass.Collection
-            class DefaultCollection
-                constructor: -> super
-                model: klass
-            klass.Collection = Lanes.Models.Collection.extend(DefaultCollection)
-
-
-Lanes.Models.Base = Lanes.lib.MakeBaseClass( Lanes.Vendor.Ampersand.State, BaseModel )
-
-
-
-# ------------------------------------------------------------------ #
-# The BasicModel is just a very thin layer over State                #
-# ------------------------------------------------------------------ #
-class BasicModel
-    constructor: -> super
-    isPersistent: -> false
-    isModel: true
-
-Lanes.Models.BasicModel = Lanes.lib.MakeBaseClass( Lanes.Vendor.Ampersand.State, BasicModel )
-
-
-class State
-    constructor: -> super
-
-Lanes.Models.State = Lanes.lib.MakeBaseClass( Lanes.Vendor.Ampersand.State, State )
+
+    # sets the model from a user interaction
+    # subviews may override this to provide a custom implementation
+    setFromView: (key,value)->
+        this.set(key,value)
+
+Lanes.Models.BasicModel = State.extend( BasicModel )
+
+Lanes.Models.Base = BasicModel.extend( BaseModel )


Use new observer methods

Use new observer methods

--- lib/whitespace.coffee
+++ lib/whitespace.coffee
@@ -1,45 +1,46 @@
-{Subscriber} = require 'emissary'
+{CompositeDisposable} = require 'atom'

 module.exports =
 class Whitespace
-  Subscriber.includeInto(this)
-
   constructor: ->
-    @subscribe atom.workspace.eachEditor (editor) =>
+    @subscriptions = new CompositeDisposable
+    @subscriptions.add atom.workspace.eachEditor (editor) =>
       @handleEvents(editor)

-    @subscribeToCommand atom.workspaceView, 'whitespace:remove-trailing-whitespace', =>
-      if editor = atom.workspace.getActiveEditor()
-        @removeTrailingWhitespace(editor, editor.getGrammar().scopeName)
-
-    @subscribeToCommand atom.workspaceView, 'whitespace:convert-tabs-to-spaces', =>
-      if editor = atom.workspace.getActiveEditor()
-        @convertTabsToSpaces(editor)
-
-    @subscribeToCommand atom.workspaceView, 'whitespace:convert-spaces-to-tabs', =>
-      if editor = atom.workspace.getActiveEditor()
-        @convertSpacesToTabs(editor)
+    @subscriptions.add atom.commands.add 'atom-workspace',
+      'whitespace:remove-trailing-whitespace': =>
+        if editor = atom.workspace.getActiveEditor()
+          @removeTrailingWhitespace(editor, editor.getGrammar().scopeName)
+      'whitespace:convert-tabs-to-spaces': =>
+        if editor = atom.workspace.getActiveEditor()
+          @convertTabsToSpaces(editor)
+      'whitespace:convert-spaces-to-tabs': =>
+        if editor = atom.workspace.getActiveEditor()
+          @convertSpacesToTabs(editor)

   destroy: ->
-    @unsubscribe()
+    @subscriptions.dispose()

   handleEvents: (editor) ->
     buffer = editor.getBuffer()
-    bufferSavedSubscription = @subscribe buffer, 'will-be-saved', =>
+    bufferSavedSubscription = buffer.onWillSave =>
       buffer.transact =>
         scopeDescriptor = editor.getRootScopeDescriptor()
         if atom.config.get(scopeDescriptor, 'whitespace.removeTrailingWhitespace')
           @removeTrailingWhitespace(editor, editor.getGrammar().scopeName)
-
         if atom.config.get(scopeDescriptor, 'whitespace.ensureSingleTrailingNewline')
           @ensureSingleTrailingNewline(editor)

-    @subscribe editor, 'destroyed', =>
-      bufferSavedSubscription.off()
-      @unsubscribe(editor)
+    editorDestroyedSubscription = editor.onDidDestroy ->
+      bufferSavedSubscription.dispose()
+      editorDestroyedSubscription.dispose()
+    bufferDestroyedSubscription = buffer.onDidDestroy ->
+      bufferDestroyedSubscription.dispose()
+      bufferSavedSubscription.dispose()

-    @subscribe buffer, 'destroyed', =>
-      @unsubscribe(buffer)
+    @subscriptions.add(bufferSavedSubscription)
+    @subscriptions.add(editorDestroyedSubscription)
+    @subscriptions.add(bufferDestroyedSubscription)

   removeTrailingWhitespace: (editor, grammarScopeName) ->
     buffer = editor.getBuffer()


Off subscription when deactivated

Off subscription when deactivated

--- lib/whitespace.coffee
+++ lib/whitespace.coffee
@@ -4,27 +4,32 @@
     ensureSingleTrailingNewline: true

   activate: ->
-    project.eachEditSession (editSession) => @whitespaceBeforeSave(editSession)
+    @editorSubscription = atom.project.eachEditSession (editor) ->
+      bufferSubscription = editor.getBuffer().on 'will-be-saved', (buffer) ->
+        buffer.transact ->
+          if atom.config.get('whitespace.removeTrailingWhitespace')
+            removeTrailingWhitespace(editor)

-  whitespaceBeforeSave: (editSession) ->
-    buffer = editSession.buffer
-    saveHandler = ->
-      buffer.transact ->
-        if config.get('whitespace.removeTrailingWhitespace')
-          buffer.scan /[ \t]+$/g, ({match, replace}) ->
-            # GFM permits two whitespaces at the end of a line--trim anything else
-            unless editSession.getGrammar().scopeName is "source.gfm" and match[0] is "  "
-              replace('')
+          if atom.config.get('whitespace.ensureSingleTrailingNewline')
+            ensureSingleTrailingNewline(editor)

-        if config.get('whitespace.ensureSingleTrailingNewline')
-          if buffer.getLastLine() is ''
-            row = buffer.getLastRow() - 1
-            while row and buffer.lineForRow(row) is ''
-              buffer.deleteRow(row--)
-          else
-            selectedBufferRanges = editSession.getSelectedBufferRanges()
-            buffer.append('\n')
-            editSession.setSelectedBufferRanges(selectedBufferRanges)
+      editor.on 'destroyed', -> bufferSubscription.off()

-    buffer.on('will-be-saved', saveHandler)
-    editSession.on 'destroyed', -> buffer.off('will-be-saved', saveHandler)
+  deactivate: ->
+    @editorSubscription.off()
+
+removeTrailingWhitespace = (editor) ->
+  editor.getBuffer().scan /[ \t]+$/g, ({match, replace}) ->
+    # GFM permits two whitespaces at the end of a line
+    unless match[0] is '  ' and editor.getGrammar().scopeName is 'source.gfm'
+      replace('')
+
+ensureSingleTrailingNewline = (editor) ->
+  buffer = editor.getBuffer()
+  if buffer.getLastLine() is ''
+    row = buffer.getLastRow() - 1
+    buffer.deleteRow(row--) while row and buffer.lineForRow(row) is ''
+  else
+    selectedBufferRanges = editor.getSelectedBufferRanges()
+    buffer.append('\n')
+    editor.setSelectedBufferRanges(selectedBufferRanges)


make custom base URLs work

make custom base URLs work

--- SingularityUI/app/application.coffee
+++ SingularityUI/app/application.coffee
@@ -40,9 +40,13 @@

         @router = new Router

+        # so sneaky
+        el = document.createElement('a')
+        el.href = config.appRoot
+
         Backbone.history.start
             pushState: true
-            root: config.appRoot
+            root: el.pathname

         Object.freeze? @


Defer getting of selections *after* insertion

--- lib/y-richtext.coffee
+++ lib/y-richtext.coffee
@@ -156,11 +156,11 @@
             return false
         return true

-      selections = (@_get "selections")
+
       if noneIsNull delta.attributes
-        operation = selections.select
+        operation = (@_get "selections").select
       else
-        operation = selections.unselect
+        operation = (@_get "selections").unselect

       if delta.insert?
         @insertHelper position, delta.insert


add support for external url

add support for external url

--- hubot/fleet.coffee
+++ hubot/fleet.coffee
@@ -6,6 +6,7 @@
 #
 # Configuration:
 #   HUBOT_FLEET_URL - contains fleet url
+#   HUBOT_FLEET_SHOW_URL - if necessary, a different url for display purposes
 #

 class Fleet
@@ -15,7 +16,7 @@
   done: 0

   constructor: (@scenario, @client) ->
-    history = process.env.HUBOT_FLEET_URL + '#/history/' + @scenario
+    history = (process.env.HUBOT_FLEET_SHOW_URL or process.env.HUBOT_FLEET_URL) + '#/history/' + @scenario
     @client.send "executing " + scenario + " (waiting 2 seconds for acks, reporting to: " + history + ')'

   process: (msg) ->


Install mod error fix

Install mod error fix

--- client/views/install_mod.coffee
+++ client/views/install_mod.coffee
@@ -10,15 +10,24 @@

 Template.installMod.events
   "click .installBtn": ->
-    Meteor.call "installMod", Router.current().params.mod
-    installingNot = $.pnotify
-      title: "Installing..."
-      text: "Your manager has been told to install the mod. Please wait."
-      type: "success"
-      nonblock: true
-      hide: false
-      closer: false
-      sticker: false
+    Meteor.call "installMod", Router.current().params.mod, (err,res)->
+      if err?
+        $.notify
+          title: "Can't Install Mod"
+          text: err.reason
+          type: "error"
+          nonblock: true
+          closer: false
+          sticker: false
+      else
+        installingNot = $.pnotify
+          title: "Installing..."
+          text: "Your manager has been told to install the mod. Please wait."
+          type: "success"
+          nonblock: true
+          hide: false
+          closer: false
+          sticker: false
   "click .managerBtn": ->
     Session.set("managerStatus", "Waiting for launcher to connect...")
     window.open "https://s3-us-west-2.amazonaws.com/d2mpclient/launcher.exe"


Remove Atom file type settings for `.mutt_account` and `.mutt_provider`.

These extensions were previously used for mutt configuration files, but
they are now obsolete and no longer needed.

--- home/.atom/config.cson
+++ home/.atom/config.cson
@@ -67,8 +67,6 @@
     "^.htoprc": "text.generic-config"
     "^.inputrc": "text.generic-config"
     "^.mailcap": "source.shell"
-    "mutt_account": "source.shell"
-    "mutt_provider": "source.shell"
     ".*muttrc": "source.shell"
     "^.profile$": "source.shell"
     "^.psqlrc": "source.sql"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine_selector.coffee
+++ src/engine_selector.coffee
@@ -38,7 +38,7 @@
       equals engine.closest(".enemy", player.position()), enemy1
       equals engine.closest(".enemy", player2.position()), enemy2

-  @
+  @name closest
   @param {String} selector
   @param {Point} position
   ###


reasonable prevention of crash

reasonable prevention of crash

--- coffeescript/lib/bot/bot.coffee
+++ coffeescript/lib/bot/bot.coffee
@@ -75,7 +75,7 @@

       tick = @race.currentTick
       @physics.initThrottleAndAccelerationRatio dataPoint(tick - 2), dataPoint(tick - 1)
-      winston.verbose @physics.throttleAndAccelerationRatio
+      winston.verbose "#{@logPrefix()};throttleAndAccelerationRatio:", @physics.throttleAndAccelerationRatio

     initMaxSlipFactor: ->
       return if @maxSlipInitialized or @race.getCarAngle(@color) is 0
@@ -86,8 +86,8 @@
       b = v / r * 180 / Math.PI
       bslip = b - a
       vslip = r * b * Math.PI / 180
-      @factorMaxSlip = vslip * vslip / r
-      console.log @logPrefix()  + ";initMaxSlipFactor;a:#{a}, v:#{v}, r:#{r}, b:#{b}, bs:#{bslip}, vs:#{vslip}, fac:#{@factorMaxSlip}"
+      @factorMaxSlip = 0.97 * vslip * vslip / r
+      winston.warn @logPrefix()  + ";initMaxSlipFactor;a:#{a}, v:#{v}, r:#{r}, b:#{b}, bs:#{bslip}, vs:#{vslip}, fac:#{@factorMaxSlip}"
       @maxSlipInitialized = yes

     setThrottle: (throttle) ->
@@ -137,26 +137,40 @@

     adjustVelocity: ->
       if @race.straightToFinish @color
-        @targetVelocity = 50
-        @adjustThrottle()
-        return
-
-      straightDistance = @race.straightDistanceAhead(@color)
-      bendedPieceIndex = @race.nextBendedPieceIndex(@color)
-      radius = @race.getRadiusOnLane bendedPieceIndex, @race.getCarLane(@color)
-      @targetVelocity = @maxVelocityForRadius radius
-      @adjustThrottle straightDistance
+        @targetVelocity = 100
+        return @adjustThrottle()
+
+      if @onBendedPiece() and @predictAngle() > @race.maxAngle - 2
+        @targetVelocity = @maxVelocityForRadius @getRadius() / 2
+        return @adjustThrottle()
+
+      bendedPieces = @race.bendedPiecesAhead @color
+
+      throttles = for {radius, distance} in bendedPieces
+        targetVelocity = @maxVelocityForRadius radius
+        @throttleForVelocityInDistance targetVelocity, distance
+
+      @setThrottle(Math.min throttles...)
+
+    predictAngle: ->
+      angle = @race.getCarAngle(@color)
+      angleS = @race.getAngularSpeed(@color)
+      angleSC = @race.getAngularSpeedChange(@color)
+      Math.abs(angle + 5 * angleS + 4 * angleSC)

     maxVelocityForRadius: (radius) ->
-      # v^2 / r = 0.39
-
+      # v^2 / r = @factorMaxSlip
       Math.sqrt(@factorMaxSlip * radius)

     adjustThrottle: (distance = 0) ->
+      @setThrottle @throttleForVelocityInDistance @targetVelocity, distance
+
+    throttleForVelocityInDistance: (targetVelocity, distance = 0) ->
+      if distance > 0
+        distance += 30
       velocity = @race.getVelocity @color
-      @setThrottle @physics.optimalThrottleForVelocityInDistance @targetVelocity, distance, velocity
-
+      @physics.optimalThrottleForVelocityInDistance targetVelocity, distance, velocity

 #    predictAngleSpeedChange: ->
 #      return 0 unless @angleParams?


No need to send cookies to server

No need to send cookies to server

--- files.coffee
+++ files.coffee
@@ -557,15 +557,12 @@
         Meteor.setTimeout ->
           if (not cookie.has('x_mtok') and Meteor.connection._lastSessionId) or (cookie.has('x_mtok') and (cookie.get('x_mtok') isnt Meteor.connection._lastSessionId))
             cookie.set 'x_mtok', Meteor.connection._lastSessionId, null, '/'
-            cookie.send()
           return
         , 25
         return

       unsetTokenCookie = ->
-        if cookie.has 'x_mtok'
-          cookie.remove 'x_mtok'
-          cookie.send()
+        cookie.remove 'x_mtok' if cookie.has 'x_mtok'
         return

       if Accounts?


Minor rixes

Minor rixes

 - Make sure target directory is writable in async way
 - Fix `.write()` method

--- files.coffee
+++ files.coffee
@@ -150,7 +150,10 @@
       @storagePath       = @storagePath.replace /\/$/, ''
       @storagePath       = nodePath.normalize @storagePath

-      fs.mkdirsSync @storagePath
+      fs.mkdirs @storagePath, {mode: @permissions}, (error) ->
+        if error
+          throw new Meteor.Error 401, "[FilesCollection.#{@collectionName}] Path #{@storagePath} is not writable!", error
+        return

       check @strict, Boolean
       check @throttle, Match.OneOf false, Number
@@ -633,6 +636,7 @@

       {extension, extensionWithDot} = @getExt fileName

+      self      = @
       path      = "#{@storagePath}/#{randFileName}#{extensionWithDot}"

       opts.type = @getMimeType opts
@@ -653,7 +657,7 @@
         if error
           callback and callback error
         else
-          result._id = @collection.insert _.clone result
+          result._id = self.collection.insert _.clone result
           callback and callback null, result

       return @


Fuse cash projection and cash flow legend icons

--- src/components/widgets/accounts-cash-projection/accounts-cash-projection.directive.coffee
+++ src/components/widgets/accounts-cash-projection/accounts-cash-projection.directive.coffee
@@ -58,7 +58,6 @@
         $scope.trxList.totalRecords = response.data.meta.record_count
     )

-
   # Widget specific methods
   # --------------------------------------
   w.initContext = ->
@@ -77,6 +76,7 @@
       serie.name == "Cash flow"
     cashFlowSerie.data = []
     cashFlowSerie.type = 'area'
+    cashFlowSerie.showInLegend = false

     totalOffset = 0.0
     if w.metadata.offset && w.metadata.offset.current && w.metadata.offset.current.length > 0
@@ -118,10 +118,12 @@
     }
     $scope.trxList.fetch().finally(-> $scope.trxList.show())

+  imgSrc = (name) -> ImpacAssets.get(_.camelCase(name + 'LegendIcon'))
+  imgTemplate = (src, name) -> "
#{name}"
   legendFormatter = ->
-    series = this
-    imgSrc = ImpacAssets.get(_.camelCase(series.name + 'LegendIcon'))
-    "
  #{series.name}"
+    name = this.name
+    return imgTemplate(imgSrc(name), name) unless name == 'Projected cash'
+    imgTemplate(imgSrc(name), name) + '
' + imgTemplate(imgSrc('cashFlow'), 'Cash flow')

   w.format = ->
     # Chart basic options


update

update

--- src/main/coffeescript/nz/SceneBattle.coffee
+++ src/main/coffeescript/nz/SceneBattle.coffee
@@ -78,6 +78,10 @@
         sprite.x = map.x
         sprite.y = map.y
       @characters = characters
+      @setupKeyboradHander()
+      @setupCursorHandler (e) ->
+        console.log e
+        return
     return

   setup: ->


too many users; removing some

too many users; removing some

--- users.coffee
+++ users.coffee
@@ -18,135 +18,6 @@
 	"Helga Mendoza"
 	"Alyce Miller"
 	"Ali Bayne"
-	"Lupe Briggs"
-	"Sterling Rohde"
-	"Aaron Spillman"
-	"Phyllis Schultz"
-	"Nettie Lindsay"
-	"Emily Rocha"
-	"Natasha Sykes"
-	"Trisha Boyer"
-	"Lester Beane"
-	"Vilma Snow"
-	"Candy Boyer"
-	"Alisha Monroe"
-	"Harvey Null"
-	"Corrine Cobb"
-	"Gabriel Luong"
-	"Erma Long"
-	"Johnathon Broadnax"
-	"Clayton McDaniel"
-	"Donny Timmerman"
-	"Dewey Magnuson"
-	"Natasha Foster"
-	"Hugh Segal"
-	"Ofelia Harding"
-	"Cindy Webb"
-	"Fabian Stricklin"
-	"Roosevelt Maki"
-	"Cathryn Norton"
-	"Marisol Massey"
-	"Rachel Cotton"
-	"Kristen Moran"
-	"Jana Melton"
-	"Vance Nickel"
-	"Crystal Moses"
-	"Vicente Phifer"
-	"Trudy McMillan"
-	"Madelyn Harrington"
-	"Kelley Curry"
-	"Randy Banner"
-	"Denise Boyle"
-	"Maureen Robinson"
-	"Patrick Burkholder"
-	"Erika Mills"
-	"Harry Ingraham"
-	"Bruno Magnuson"
-	"Joshua Conte"
-	"Monica Savage"
-	"Kristie Williams"
-	"Damon Haddad"
-	"Brigitte Dunlap"
-	"Darla Hendricks"
-	"Rae Richard"
-	"April Lowe"
-	"Deidre Brady"
-	"Lynne Talley"
-	"Kurt Beaudry"
-	"Jacqueline Hurst"
-	"Hollis Pellegrino"
-	"Todd Stoker"
-	"Carlene Lane"
-	"Irvin Null"
-	"Vicki Beasley"
-	"Marina Marsh"
-	"Annette Page"
-	"Bonnie Dunlap"
-	"Angelia Moses"
-	"Chase Gillum"
-	"Marisol Moon"
-	"Brandie Moody"
-	"Delores Kane"
-	"Vince Buxton"
-	"Bernice Browning"
-	"Jolene Sosa"
-	"Naomi Bray"
-	"Josefa Baird"
-	"Loraine Graves"
-	"Juan Hargrave"
-	"Alfredo Ferro"
-	"Carla Love"
-	"Maxine Myers"
-	"Oliver Orton"
-	"Beverley Woodard"
-	"Barry Soileau"
-	"Socorro Sullivan"
-	"Lenore Hammond"
-	"Lynda Goff"
-	"Iva Day"
-	"Pamela Jarvis"
-	"Laurel Dominguez"
-	"Leola Guerrero"
-	"Earl Schramm"
-	"Daniel Ballinger"
-	"Julie Clemons"
-	"Alphonso Bracken"
-	"Julie Brewer"
-	"Louisa Webster"
-	"Celia Brennan"
-	"Casandra Johns"
-	"Ronnie Ferrari"
-	"Brandy Gaines"
-	"Rosanne Dominguez"
-	"Howard Towle"
-	"Emery Ceja"
-	"Inez Melton"
-	"Leann Brewer"
-	"Leann Olson"
-	"Selma York"
-	"Mitchell Pearsall"
-	"Gabriela Phelps"
-	"Lillie Cox"
-	"Anton Rhoden"
-	"Amy Patel"
-	"Miranda Sanchez"
-	"Courtney Labelle"
-	"Gertrude Ratliff"
-	"Dexter Poling"
-	"Levi McCormick"
-	"Lynnette Donovan"
-	"Lana Martinez"
-	"Heath Odum"
-	"Summer Pacheco"
-	"Joanne Sweeney"
-	"Maurice Loya"
-	"Lucille Guerra"
-	"Wm Olds"
-	"Angelo Betz"
-	"Elvis Kish"
-	"Angela Espinoza"
-	"Joseph Rhyne"
-	"Janine Burnett"
-	"Janelle McPherson"
-	"Estella Kaufman"
+	"Audrey Oliver"
+	"Clifford Word"
 ]


kdimage: added blur prototype, removed weird resampling algorithm for now

--- components/image/KDImage.coffee
+++ components/image/KDImage.coffee
@@ -80,103 +80,120 @@
     )
     callback canvas.toDataURL(@format)

+  @process 'blur', (data, {radius}, callback)->
+    canvas = document.createElement('canvas')
+    canvas.width = data.width
+    canvas.height = data.height
+    ctx = canvas.getContext('2d')
+    ctx.drawImage(data
+      0, 0, data.width, data.height)
+    ctx.globalAlpha = 0.5
+    for y in [-radius..radius] by 2
+      for x in [-radius..radius] by 2
+        ctx.drawImage(data, x, y)
+        if x>=0 and y>=0
+          ctx.drawImage(data, -(x-1), -(y-1))
+    ctx.globalAlpha = 1.0;
+    callback canvas.toDataURL(@format)
+
   @process 'resample', (data, {scaledWidth, lobesKernel}, callback)->
-    #returns a function that calculates lanczos weight
-    lanczosCreate = (lobes)->
-      return (x)->
-        if x > lobes
-          return 0
-        x *= Math.PI
-        if Math.abs(x) < 1e-16
-          return 1
-        xx = x / lobes
-        return Math.sin(x) * Math.sin(xx) / x / xx
-
-    # elem: canvas element, img: image element, sx: scaled width, lobes: kernel radius
-    resampler = (resampleCanvas, img, sx, lobes, kallback)=>
-      @canvas                 = resampleCanvas
-      @canvas.width           = img.width
-      @canvas.height          = img.height
-      @canvas.style.display   = "none"
-      @ctx                    = @canvas.getContext "2d"
-      @ctx.drawImage img, 0, 0
-      @img                    = img
-      @src = @ctx.getImageData 0, 0, img.width, img.height
-      @dest =
-        width: sx
-        height: Math.round(img.height * sx / img.width)
-      @dest.data = new Array(@dest.width*@dest.height*3)
-      @lanczos = lanczosCreate lobes
-      @ratio = img.width / sx
-      @rcp_ratio = 2 / @ratio
-      @range2 = Math.ceil(@ratio * lobes / 2)
-      @cacheLanc = {}
-      @center = {}
-      @icenter = {}
-      setTimeout =>
-        @process1(@, 0)
-      ,0
-
-      @process1 = (self, u)=>
-        self.center.x = (u + 0.5) * self.ratio
-        self.icenter.x = Math.floor(self.center.x)
-        for v in [0...self.dest.height]
-          self.center.y = (v + 0.5) * self.ratio
-          self.icenter.y = Math.floor(self.center.y)
-          a = r = g = b = 0
-          for i in [(self.icenter.x-self.range2)..(self.icenter.x+self.range2)]
-            if (i < 0 || i >= self.src.width)
-              continue
-            f_x = Math.floor(1000 * Math.abs(i - self.center.x))
-            if (!self.cacheLanc[f_x])
-              self.cacheLanc[f_x] = {}
-            for j in [(self.icenter.y-self.range2)..(self.icenter.y+self.range2)]
-              if (j < 0 || j >= self.src.height)
-                continue
-              f_y = Math.floor(1000 * Math.abs(j - self.center.y))
-              if (self.cacheLanc[f_x][f_y] == undefined)
-                self.cacheLanc[f_x][f_y] = self.lanczos(Math.sqrt(Math.pow(f_x * self.rcp_ratio, 2) + Math.pow(f_y * self.rcp_ratio, 2)) / 1000)
-              weight = self.cacheLanc[f_x][f_y]
-              if (weight > 0)
-                idx = (j * self.src.width + i) * 4
-                a += weight
-                r += weight * self.src.data[idx]
-                g += weight * self.src.data[idx + 1]
-                b += weight * self.src.data[idx + 2]
-          idx = (v * self.dest.width + u) * 3
-          self.dest.data[idx] = r / a
-          self.dest.data[idx + 1] = g / a
-          self.dest.data[idx + 2] = b / a
-
-        if (++u < self.dest.width)
-          setTimeout =>
-            self.process1(self, u)
-          ,0
-        else
-          setTimeout =>
-            self.process2 self
-          ,0
-
-      @process2 = (self)=>
-        self.canvas.width = self.dest.width;
-        self.canvas.height = self.dest.height;
-        self.ctx.drawImage(self.img, 0, 0);
-        self.src = self.ctx.getImageData(0, 0, self.dest.width, self.dest.height);
-        for i in [0...self.dest.width]
-          for j in [0...self.dest.height]
-            idx = (j * self.dest.width + i) * 3;
-            idx2 = (j * self.dest.width + i) * 4;
-            self.src.data[idx2] = self.dest.data[idx];
-            self.src.data[idx2 + 1] = self.dest.data[idx + 1];
-            self.src.data[idx2 + 2] = self.dest.data[idx + 2];
-        self.ctx.putImageData(self.src, 0, 0);
-        self.canvas.style.display = "block";
-        kallback self
-
-    canvas = document.createElement('canvas')
-
-    resampler canvas, data, scaledWidth, lobesKernel, (res)=>
-      callback res.canvas.toDataURL(@format)
+    callback data
+    # #returns a function that calculates lanczos weight
+    # lanczosCreate = (lobes)->
+    #   return (x)->
+    #     if x > lobes
+    #       return 0
+    #     x *= Math.PI
+    #     if Math.abs(x) < 1e-16
+    #       return 1
+    #     xx = x / lobes
+    #     return Math.sin(x) * Math.sin(xx) / x / xx
+
+    # # elem: canvas element, img: image element, sx: scaled width, lobes: kernel radius
+    # resampler = (resampleCanvas, img, sx, lobes, kallback)=>
+    #   @canvas                 = resampleCanvas
+    #   @canvas.width           = img.width
+    #   @canvas.height          = img.height
+    #   @canvas.style.display   = "none"
+    #   @ctx                    = @canvas.getContext "2d"
+    #   @ctx.drawImage img, 0, 0
+    #   @img                    = img
+    #   @src = @ctx.getImageData 0, 0, img.width, img.height
+    #   @dest =
+    #     width: sx
+    #     height: Math.round(img.height * sx / img.width)
+    #   @dest.data = new Array(@dest.width*@dest.height*3)
+    #   @lanczos = lanczosCreate lobes
+    #   @ratio = img.width / sx
+    #   @rcp_ratio = 2 / @ratio
+    #   @range2 = Math.ceil(@ratio * lobes / 2)
+    #   @cacheLanc = {}
+    #   @center = {}
+    #   @icenter = {}
+    #   setTimeout =>
+    #     @process1(@, 0)
+    #   ,0
+
+    #   @process1 = (self, u)=>
+    #     self.center.x = (u + 0.5) * self.ratio
+    #     self.icenter.x = Math.floor(self.center.x)
+    #     for v in [0...self.dest.height]
+    #       self.center.y = (v + 0.5) * self.ratio
+    #       self.icenter.y = Math.floor(self.center.y)
+    #       a = r = g = b = 0
+    #       for i in [(self.icenter.x-self.range2)..(self.icenter.x+self.range2)]
+    #         if (i < 0 || i >= self.src.width)
+    #           continue
+    #         f_x = Math.floor(1000 * Math.abs(i - self.center.x))
+    #         if (!self.cacheLanc[f_x])
+    #           self.cacheLanc[f_x] = {}
+    #         for j in [(self.icenter.y-self.range2)..(self.icenter.y+self.range2)]
+    #           if (j < 0 || j >= self.src.height)
+    #             continue
+    #           f_y = Math.floor(1000 * Math.abs(j - self.center.y))
+    #           if (self.cacheLanc[f_x][f_y] == undefined)
+    #             self.cacheLanc[f_x][f_y] = self.lanczos(Math.sqrt(Math.pow(f_x * self.rcp_ratio, 2) + Math.pow(f_y * self.rcp_ratio, 2)) / 1000)
+    #           weight = self.cacheLanc[f_x][f_y]
+    #           if (weight > 0)
+    #             idx = (j * self.src.width + i) * 4
+    #             a += weight
+    #             r += weight * self.src.data[idx]
+    #             g += weight * self.src.data[idx + 1]
+    #             b += weight * self.src.data[idx + 2]
+    #       idx = (v * self.dest.width + u) * 3
+    #       self.dest.data[idx] = r / a
+    #       self.dest.data[idx + 1] = g / a
+    #       self.dest.data[idx + 2] = b / a
+
+    #     if (++u < self.dest.width)
+    #       setTimeout =>
+    #         self.process1(self, u)
+    #       ,0
+    #     else
+    #       setTimeout =>
+    #         self.process2 self
+    #       ,0
+
+    #   @process2 = (self)=>
+    #     self.canvas.width = self.dest.width;
+    #     self.canvas.height = self.dest.height;
+    #     self.ctx.drawImage(self.img, 0, 0);
+    #     self.src = self.ctx.getImageData(0, 0, self.dest.width, self.dest.height);
+    #     for i in [0...self.dest.width]
+    #       for j in [0...self.dest.height]
+    #         idx = (j * self.dest.width + i) * 3;
+    #         idx2 = (j * self.dest.width + i) * 4;
+    #         self.src.data[idx2] = self.dest.data[idx];
+    #         self.src.data[idx2 + 1] = self.dest.data[idx + 1];
+    #         self.src.data[idx2 + 2] = self.dest.data[idx + 2];
+    #     self.ctx.putImageData(self.src, 0, 0);
+    #     self.canvas.style.display = "block";
+    #     kallback self
+
+    # canvas = document.createElement('canvas')
+
+    # resampler canvas, data, scaledWidth, lobesKernel, (res)=>
+    #   callback res.canvas.toDataURL(@format)

   createView:->
     new KDCustomHTMLView


[client][app][activity][view] added block user menu

--- client/landing/app/Applications/Activity.kdapplication/views/activityitemchild.coffee
+++ client/landing/app/Applications/Activity.kdapplication/views/activityitemchild.coffee
@@ -115,11 +115,15 @@
         menu =
           'Mark User as Troll' :
             callback           : ->
-              mainController.markUserAsTroll data
+              mainController.blockUser data

       menu['Delete Post'] =
         callback : =>
           @confirmDeletePost data
+
+      menu['Block User'] =
+        callback : ->
+          mainController.openBlockUserModal data

       return menu


Fixed a bug in prepend function in dom.coffee

--- src/base/dom.coffee
+++ src/base/dom.coffee
@@ -291,11 +291,11 @@
 					append_node = content

 		if append
-			CUI.util.assert(append_node instanceof Node, "CUI.dom.append", "Content needs to be instanceof Node, string, boolean, or number.", node: content)
+			CUI.util.assert(append_node instanceof Node, "CUI.dom.append", "Content needs to be instanceof Node, string, boolean, or number.", node: append_node)
 			node.appendChild(append_node)
 		else
-			CUI.util.assert(append_node instanceof Node, "CUI.dom.prepend", "Content needs to be instanceof Node, string, boolean, or number.", node: content)
-			node.insertBefore(content, node.firstChild)
+			CUI.util.assert(append_node instanceof Node, "CUI.dom.prepend", "Content needs to be instanceof Node, string, boolean, or number.", node: append_node)
+			node.insertBefore(append_node, node.firstChild)

 		return node


Minor: remove commented code

Minor: remove commented code

--- src/resources/forms/orbeon/builder/resources/cell/control-resources-editor/actions.coffee
+++ src/resources/forms/orbeon/builder/resources/cell/control-resources-editor/actions.coffee
@@ -64,7 +64,6 @@

     afterTinyMCEInitialized = (f) ->
         if tinymceObject.initialized then f()
-#        else tinymceObject.onInit.add(f)
         else tinymceObject.onPostRender.add(f)

     # Function to initialize the TinyMCE, memoized so it runs at most once


Doubleclick zoom event disabled

Doubleclick zoom event disabled

--- lib/foundry-xray/app/assets/javascripts/lib/drawer.js.coffee
+++ lib/foundry-xray/app/assets/javascripts/lib/drawer.js.coffee
@@ -15,7 +15,7 @@
           s = d3.event.scale
           @group.attr("transform", "translate(" + t + ") scale(" + s + ")");

-      @svg.call zoom
+      @svg.call(zoom).on("dblclick.zoom", null)

     @group


use standard error functions in srgb

use standard error functions in srgb

These may not have been accessible when srgb was created, but they are
good now.

fixes CNVS-28191

Test plan:
  * open the screen reader gradebook
  * do things that will cause an error (for example, try setting the
    default grade for several assignments to "EX"
  * make sure the error is properly read by the screenreader each time

Change-Id: I7cd915b54d36aa4b43d7b5cc077e0d0740aadb71
Reviewed-on: https://gerrit.instructure.com/76058
Tested-by: Jenkins
Reviewed-by: Shahbaz Javeed <6692d7be3fa215e5483a121d6fb314def6049877@instructure.com>
Reviewed-by: Keith T. Garner <a1a80143a840c140f31c3495ebbd9ec906e09fed@instructure.com>
QA-Review: KC Naegle <9fbecd016338f9910821a8a5d5d0ae4fc9abfaa5@instructure.com>
Product-Review: Christi Wruck

--- app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee
+++ app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee
@@ -73,16 +73,6 @@
         $('#gradebook-export').prop('disabled', true)
         $('#last-exported-gradebook').hide()
         @pollGradebookCsvProgress(attachmentProgress)
-    ).on('init')
-
-    errors: (->
-      # this is a sad, sad hack
-      # until we can get flash notifications working app-wide for screenreaders
-      if Ember.$('#flash_message_holder li').size() > 0
-        close = Ember.$('#flash_message_holder li a').text().trim()
-        message = Ember.$('#flash_message_holder li').text().replace(close,'').trim()
-        node = Ember.$("#{htmlEscape(message)}")
-        Ember.$(node).appendTo(Ember.$('#flash_screenreader_holder'))
     ).on('init')

     contextUrl: contextUrl


Update bindings so that the target can be a keypath OR a primitive type (not observed). [#388]

--- src/bindings.coffee
+++ src/bindings.coffee
@@ -32,6 +32,15 @@
       root: @view.rootInterface
       adapters: @view.adapters

+  parseTarget: =>
+    token = Rivets.TypeParser.parse @keypath
+
+    if token.type is 0
+      @value = token.value
+    else
+      @observer = @observe @view.models, @keypath, @sync
+      @model = @observer.target
+
   # Applies all the current formatters to the supplied value and returns the
   # formatted value.
   formattedValue: (value) =>
@@ -79,37 +88,40 @@

   # Syncs up the view binding with the model.
   sync: =>
-    if @model isnt @observer.target
-      observer.unobserve() for observer in @dependencies
-      @dependencies = []
-
-      if (@model = @observer.target)? and @options.dependencies?.length
-        for dependency in @options.dependencies
-          observer = @observe @model, dependency, @sync
-          @dependencies.push observer
-
-    @set @observer.value()
+    @set if @observer
+      if @model isnt @observer.target
+        observer.unobserve() for observer in @dependencies
+        @dependencies = []
+
+        if (@model = @observer.target)? and @options.dependencies?.length
+          for dependency in @options.dependencies
+            observer = @observe @model, dependency, @sync
+            @dependencies.push observer
+
+      @observer.value()
+    else
+      @value

   # Publishes the value currently set on the input element back to the model.
   publish: =>
-    value = Rivets.Util.getInputValue @el
-
-    for formatter in @formatters.slice(0).reverse()
-      args = formatter.split /\s+/
-      id = args.shift()
-
-      if @view.formatters[id]?.publish
-        value = @view.formatters[id].publish value, args...
-
-    @observer.setValue value
+    if @observer
+      value = Rivets.Util.getInputValue @el
+
+      for formatter in @formatters.slice(0).reverse()
+        args = formatter.split /\s+/
+        id = args.shift()
+
+        if @view.formatters[id]?.publish
+          value = @view.formatters[id].publish value, args...
+
+      @observer.setValue value

   # Subscribes to the model for changes at the specified keypath. Bi-directional
   # routines will also listen for changes on the element to propagate them back
   # to the model.
   bind: =>
     @binder.bind?.call @, @el
-    @observer = @observe @view.models, @keypath, @sync
-    @model = @observer.target
+    @parseTarget()

     if @model? and @options.dependencies?.length
       for dependency in @options.dependencies
@@ -121,7 +133,7 @@
   # Unsubscribes from the model and the element.
   unbind: =>
     @binder.unbind?.call @, @el
-    @observer.unobserve()
+    @observer?.unobserve()

     observer.unobserve() for observer in @dependencies
     @dependencies = []
@@ -134,7 +146,7 @@
   # Updates the binding's model from what is currently set on the view. Unbinds
   # the old model first and then re-binds with the new model.
   update: (models = {}) =>
-    @model = @observer.target
+    @model = @observer?.target
     @unbind()
     @binder.update?.call @, models
     @bind()


Add classes to cancel and done buttons, call done()

--- src/io/web-renderer.coffee
+++ src/io/web-renderer.coffee
@@ -123,7 +123,7 @@

 				#{("
#{i}
" for i in items).join ''}

-			Cancel
+			Cancel
 		"

 		$('#menu .items').on 'click', 'li', ->
@@ -140,8 +140,8 @@

 				#{("
#{i}
" for i in items).join ''}

-			Cancel
-			Done
+			Cancel
+			Done
 		"

 		updateChecked = (i) ->
@@ -164,7 +164,7 @@
 			opts?.onCancel?()

 		$('#menu #done').click ->
-
+			done()

 		[updateChecked, done]


cooldown finilazation

cooldown finilazation

--- src/cooldown.coffee
+++ src/cooldown.coffee
@@ -10,7 +10,7 @@

   cooldown: (name, options={target: 0, approachBy: 1, value: null}) ->
     options.target ||= 0
-    u
+    unless options.

     unless I.cooldowns[name]
       I.cooldowns[name] = options


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/cooldown.coffee
+++ src/cooldown.coffee
@@ -15,4 +15,4 @@
       if options.value?
         I[name] = options.value
       else
-        I[name] = 100 unless I[name]
+        I[name] = 0 unless I[name]


remove usage of deprecated mongoose function

--- src/models/torrents.coffee
+++ src/models/torrents.coffee
@@ -31,7 +31,7 @@
 ANNOUNCE_INTERVAL = 300

 Torrent.statics.findTorrents = (q, callback) ->
-  q.only ['title', 'size', 'dateUploaded', 'category', 'permalink', 'snatches', 'infohash']
+  q.select { title : 1, size : 1, dateUploaded : 1, category : 1, permalink : 1, snatches : 1, infohash :1 }, { _id : 0 }
   q.sort 'dateUploaded', -1
   q.exec (err, docs) ->
     multi = redis.multi()


Send headers in http errors, fixes #81

Send headers in http errors, fixes #81

--- src/octonode/client.coffee
+++ src/octonode/client.coffee
@@ -21,7 +21,7 @@

 # Specialized error
 class HttpError extends Error
-  constructor: (@message, @statusCode) ->
+  constructor: (@message, @statusCode, @headers) ->

 # Initiate class
 class Client
@@ -86,13 +86,13 @@

   errorHandle: (res, body, callback) ->
     # TODO: More detailed HTTP error message
-    return callback(new HttpError('Error ' + res.statusCode, res.statusCode)) if Math.floor(res.statusCode/100) is 5
+    return callback(new HttpError('Error ' + res.statusCode, res.statusCode, res.headers)) if Math.floor(res.statusCode/100) is 5
     try
       body = JSON.parse(body || '{}')
     catch err
       return callback(err)
-    return callback(new HttpError(body.message, res.statusCode)) if body.message and res.statusCode is 422
-    return callback(new HttpError(body.message, res.statusCode)) if body.message and res.statusCode in [400, 401, 404]
+    return callback(new HttpError(body.message, res.statusCode, res.headers)) if body.message and res.statusCode is 422
+    return callback(new HttpError(body.message, res.statusCode, res.headers)) if body.message and res.statusCode in [400, 401, 404]
     callback null, res.statusCode, body, res.headers

   # Github api GET request


view questions should be working

view questions should be working

--- scripts/chat.coffee
+++ scripts/chat.coffee
@@ -107,7 +107,7 @@
 			msg.send "OK, I've removed question ID \"#{question_cull}\" from the question bank"

 		# see questions
-		robot.hear /view4 (.*)$/i, (msg) ->
+		robot.hear /view4/i, (msg) ->
 			robot.logger.info robot.brain.data.questions
 			msg.send "Questions displayed in log."


weeks events

weeks events

--- scripts/week.coffee
+++ scripts/week.coffee
@@ -1,13 +1,3 @@
-# Description:
-#   Example scripts for you to examine and try out.
-#
-# Notes:
-#   They are commented out by default, because most of them are pretty silly and
-#   wouldn't be useful and amusing enough for day to day huboting.
-#   Uncomment the ones you want to try and experiment with.
-#
-#   These are from the scripting documentation: https://github.com/github/hubot/blob/master/docs/scripting.md
-
 module.exports = (robot) ->

  robot.hear /weeks events/i, (resp) ->
@@ -16,11 +6,13 @@
               .get() (err, res, body) ->
                   {parseString} = require 'xml2js'
                   parseString body, (err, result) ->
-                    listofe = result.results.items[0].item
-                    time = parseInt(listofe[0].time[0])
-                    dateOfEvent = new Date(time)
-                    resp.send "Event Name : " + listofe[0].name[0] +
-                            "\n Date : " + dateOfEvent.getDate() + "/" + (dateOfEvent.getMonth()+1) +
-                            "\n Sign Up: " + listofe[0].event_url[0]
-
+                    for a in [0...len = result.results.items.length]
+                      console.log(result.results.items[a].item)
+                      listofe = result.results.items[a].item
+                      time = parseInt(listofe[a].time[0])
+                      dateOfEvent = new Date(time)
+                      resp.send "Event Name : " + listofe[a].name[0] +
+                                "\n Date : " + dateOfEvent.getDate() + "/" + (dateOfEvent.getMonth()+1) +
+                                "\n Sign Up: " + (listofe[a].event_url[0])
+


remove unnecessary check for exising battleId

battleId generation is now cryptographically secure so we will never
have a collision

--- server/index.coffee
+++ server/index.coffee
@@ -29,7 +29,6 @@
   # Creates a battle and returns its battleId
   createBattle: (players...) =>
     battleId = @generateBattleId()
-    battleId = @generateBattleId()  while battleId of @battles
     @battles[battleId] = new Battle(engine: this, players: players)
     battleId


remove .only from spec

remove .only from spec

--- test/integration/send-message-2-spec.coffee
+++ test/integration/send-message-2-spec.coffee
@@ -9,7 +9,7 @@
 JobManager     = require 'meshblu-core-job-manager'
 HydrantManager = require 'meshblu-core-manager-hydrant'

-describe.only 'SendMessage2: broadcast+send', ->
+describe 'SendMessage2: broadcast+send', ->
   beforeEach (done) ->
     @db            = mongojs 'meshblu-core-test'
     @devices    = @db.collection 'devices'


Fix WriteStream API

Fix WriteStream API

--- test/unit/coffee/DocArchive/MongoAWS.coffee
+++ test/unit/coffee/DocArchive/MongoAWS.coffee
@@ -39,7 +39,8 @@
 		beforeEach (done) ->
 			@awssdk.config = { update: sinon.stub() }
 			@awssdk.S3 = sinon.stub()
-			@S3S.WriteStream = MemoryStream.createWriteStream
+			@S3S.WriteStream = () ->
+				MemoryStream.createWriteStream()
 			@db.docHistory = {}
 			@db.docHistory.findOne = sinon.stub().callsArgWith(1, null, {"pack":"hello"})


groups: invitation modals minor styling

groups: invitation modals minor styling

--- client/app/Applications/Groups.kdapplication/Views/groupsinvitationrequestsview.coffee
+++ client/app/Applications/Groups.kdapplication/Views/groupsinvitationrequestsview.coffee
@@ -42,7 +42,7 @@
       controller.hideLazyLoader()
       return warn err if err
       controller.removeAllItems() if removeAllItems
-      controller.instantiateListItems requests
+      controller.instantiateListItems requests
       if requests?.length > 0
         controller.setLastTimestamp requests.last.timestamp_
         controller.emit 'teasersLoaded', requests.length
@@ -129,8 +129,8 @@
     modal = new KDModalViewWithForms
       title                  : options.title
       overlay                : yes
-      width                  : 300
-      height                 : 'auto'
+      width                  : options.width or 400
+      height                 : options.height or 'auto'
       tabs                   :
         forms                :
           invite             :
@@ -144,7 +144,7 @@
                   diameter   : 12
               Cancel         :
                 style        : 'modal-cancel'
-                callback     : (event)=> modal.destroy()
+                callback     : -> modal.destroy()
             fields           : options.fields

     form = modal.modalTabs.forms.invite
@@ -219,7 +219,7 @@

   showErrorMessage:(err)->
     warn err
-    new KDNotificationView
+    new KDNotificationView
       title    : if err.name is 'KodingError' then err.message else 'An error occured! Please try again later.'
       duration : 2000


Prevent Enter key from submitting whole form

--- app/assets/javascripts/minutes.coffee
+++ app/assets/javascripts/minutes.coffee
@@ -350,6 +350,9 @@
     add_tags(str_to_ary($('#tag-name').val()))
     $('#tag-name').val('') # clear input field
     redisplay()
+
+  $('#tag-name').on 'keypress', (e) ->
+    e.preventDefault() if e.keyCode == 13 # Enter

   redisplay() if $('#tag-names').val()?
   setupAutoCompleteTag('#tag-name')


grab the most recent drawing task if there are more than one

--- app/classifier/tasks/combo/index.cjsx
+++ app/classifier/tasks/combo/index.cjsx
@@ -66,10 +66,14 @@

     PersistInsideSubject: (props) ->
       allComboAnnotations = []
+      allComboTypes = []
       props.classification.annotations.forEach (annotation) ->
         taskDescription = props.workflow.tasks[annotation.task]
         if taskDescription.type is 'combo'
           allComboAnnotations.push annotation.value...
+          annotation.value.forEach (a) ->
+            allComboTypes.push(props.workflow.tasks[a.task].type)
+

       <g className="combo-task-persist-inside-subject-container">
         {Object.keys(props.taskTypes).map (taskType) ->
@@ -83,8 +87,8 @@
                   allComboAnnotations[idx] = annotation
                   props.onChange Object.assign({}, props.annotation, { value: allComboAnnotations })
               if props.workflow.tasks[props.annotation?.task]?.type is 'combo'
-                allTaskTypes = props.task.tasks.map (childTaskKey) -> props.workflow.tasks[childTaskKey].type
-                idx = allTaskTypes.indexOf(taskType)
+                idx = allComboTypes.lastIndexOf(taskType)
+                console.log(allComboAnnotations, allComboTypes, idx)
                 if idx > -1
                   # if the current annotation is for the combo task pass in the `inner` annotations
                   fauxAnnotation = allComboAnnotations[idx]


Use goog.dom.BufferedViewportSizeMonitor.

It prevents loops etc. Thank you Closure Library.

--- app/client/js/react/pages/song.coffee
+++ app/client/js/react/pages/song.coffee
@@ -1,6 +1,7 @@
 goog.provide 'app.react.pages.Song'

 goog.require 'app.songs.Song'
+goog.require 'goog.dom.BufferedViewportSizeMonitor'
 goog.require 'goog.dom.ViewportSizeMonitor'
 goog.require 'goog.dom.classlist'
 goog.require 'goog.string'
@@ -17,11 +18,8 @@
     {div,article,menu,h1} = React.DOM
     {a} = touch.none 'a'

-    # add touch pro a.. zkontrolovat, je vsude
-
     @create = React.createClass

-      ###* @type {goog.dom.ViewportSizeMonitor} ###
       viewportMonitor: null

       render: ->
@@ -43,7 +41,8 @@

       componentDidMount: ->
         @setLyricsMaxFontSize()
-        @viewportMonitor = new goog.dom.ViewportSizeMonitor
+        @viewportMonitor = new goog.dom
+          .BufferedViewportSizeMonitor new goog.dom.ViewportSizeMonitor
         @viewportMonitor.listen 'resize', @setLyricsMaxFontSize

       componentWillUnmount: ->


Fixed bug #778: Now stored filters are removed when detect the filter items are deleted

--- app/coffee/modules/issues/list.coffee
+++ app/coffee/modules/issues/list.coffee
@@ -123,6 +123,21 @@
             deferred.resolve(result)
         return deferred.promise

+    removeNotExistingFiltersFromUrl: ->
+        currentSearch = @location.search()
+        urlfilters = @.getUrlFilters()
+        for filterName, filterValue of urlfilters
+            if filterName == "page" or filterName == "orderBy"
+                continue
+
+            splittedValues = (parseInt(val) for val in "#{filterValue}".split(","))
+            existingValues = _.intersection(splittedValues, _.map(@scope.filters[filterName], "id"))
+            if splittedValues.length != existingValues.length
+                @location.search(filterName, existingValues.join())
+
+        if currentSearch != @location.search()
+           @location.replace()
+
     markSelectedFilters: (filters, urlfilters) ->
         # Build selected filters (from url) fast lookup data structure
         searchdata = {}
@@ -193,6 +208,8 @@
                 @scope.filters.createdBy = usersFiltersFormat data.created_by, "createdBy", "Unknown"
                 @scope.filters.types = choicesFiltersFormat data.types, "types", @scope.issueTypeById
                 @scope.filters.tags = tagsFilterFormat data.tags
+
+                @.removeNotExistingFiltersFromUrl()

                 @.markSelectedFilters(@scope.filters, urlfilters)


Environments newui: removed approval on connecting items

--- client/app/Applications/Environments.kdapplication/views/scene/environmentsceneview.coffee
+++ client/app/Applications/Environments.kdapplication/views/scene/environmentsceneview.coffee
@@ -52,15 +52,13 @@
         return new KDNotificationView
           title : "A domain name can only be bound to one VM."

-    @askForApprove items, 'create', (modal)=>
-      if machine? # Assign a domain to a machine
-        jDomain = domain.dia.data.domain # JDomain
-        vmName  = machine.dia.data.title # JVM.hostnameAlias
-        jDomain.bindVM hostnameAlias: vmName, (err)=>
-          modal.destroy()
-          return KD.showError err  if err
-          jDomain.hostnameAlias.push vmName
-          createConnection()
+    if machine? # Assign a domain to a machine
+      jDomain = domain.dia.data.domain # JDomain
+      vmName  = machine.dia.data.title # JVM.hostnameAlias
+      jDomain.bindVM hostnameAlias: vmName, (err)=>
+        return KD.showError err  if err
+        jDomain.hostnameAlias.push vmName
+        createConnection()

   updateConnections:->
     for _mkey, machine of @boxes.machines.dias


Correct auto indents of properties and end tags.

--- lib/auto-indent.coffee
+++ lib/auto-indent.coffee
@@ -196,17 +196,12 @@
           else if match[3]? # tags ending />
             if isFirstTagOfLine and indentRecalc
               stackOfTagsStillOpen.push parentTagIdx = stackOfTagsStillOpen.pop()
-              # work backwards on property rows
-              if tagStack[parentTagIdx].row+1 <= ( row-1 )
-                for propertyRow in [tagStack[parentTagIdx].row+1..row-1]
-                  @indentRow  propertyRow,
-                    tagStack[parentTagIdx].firstTagInLineIndentation,0,1
               if firstTagInLineIndentation is firstCharIndentation
                 @indentForClosingBracket  row,
                   tagStack[parentTagIdx],
                   @eslintIndentOptions.jsxClosingBracketLocation[1].selfClosing
               else
-                @indentRow  propertyRow,
+                @indentRow  row,
                   tagStack[parentTagIdx].firstTagInLineIndentation,0,1
               line = @editor.lineTextForBufferRow row
               JSXREGEXP.test('') #force regex to start again
@@ -233,17 +228,12 @@
           else if match[7]? # tags ending >
             if isFirstTagOfLine and indentRecalc
               stackOfTagsStillOpen.push parentTagIdx = stackOfTagsStillOpen.pop()
-              # work backwards on property rows if any exist
-              if tagStack[parentTagIdx].row+1 <= ( row-1 )
-                for propertyRow in [tagStack[parentTagIdx].row+1..row-1]
-                  @indentRow  propertyRow,
-                    tagStack[parentTagIdx].firstTagInLineIndentation,0,1
               if firstTagInLineIndentation is firstCharIndentation
                 @indentForClosingBracket  row,
                   tagStack[parentTagIdx],
                   @eslintIndentOptions.jsxClosingBracketLocation[1].nonEmpty
               else
-                @indentRow  propertyRow,
+                @indentRow  row,
                   tagStack[parentTagIdx].firstTagInLineIndentation,0,1
               line = @editor.lineTextForBufferRow row
               JSXREGEXP.test('') #force regex to start again
@@ -268,9 +258,13 @@
           tagStack[idxOfTags-1].type is JSXTAG_CLOSE or
           tagStack[idxOfTags-1].type is JSXTAG_SELFCLOSE_END
             @indentRow  row, previousLineIndent
+        if tagStack[idxOfTags-1].type is JSXTAG_OPEN
+            @indentRow  row,
+              tagStack[idxOfTags-1].firstTagInLineIndentation,0,1
+
       previousLineIndent = @editor.indentationForBufferRow row
       if row is range.end.row - 1 # the row that was inserted
-        indent =  @editor.indentationForBufferRow row - 1 # will be returned
+        indent =  @editor.indentationForBufferRow row # will be returned
     indent


Revert "use helper to preserve cell widths" because the other rows' cells' widths are not preserved.

This reverts commit 4e610c8302be12f686e8fe1262b4f9e1e04b0cad.

--- app/assets/javascripts/calls.coffee
+++ app/assets/javascripts/calls.coffee
@@ -2,10 +2,10 @@
 # All this logic will automatically be available in application.js.
 # You can use CoffeeScript in this file: http://coffeescript.org/

-# http://stackoverflow.com/questions/1307705/jquery-ui-sortable-with-table-and-tr-width/1372954#1372954
-# http://benw.me/posts/sortable-bootstrap-tables/
+jQuery ->

-jQuery ->
+    $('#call_list td').each (index, element) ->
+      $(element).css('width', $(element).width())

     $('#call_list').sortable(
       placeholder: "ui-state-highlight"
@@ -27,19 +27,13 @@
         for child, value of children
           if value.id? then order.push(value.id)
         console.log(order)
-      helper: (e, tr) ->
-        originals = tr.children()
-        helper = tr.clone()
-        helper.children().each (index, element) ->
-          $(element).css('width', originals.eq(index).width())
-        return helper

-#      $.ajax(
-#        type: 'POST'
-#        url: '/something/reorder_pregnancies'
-#        dataType: 'json'
-#         data: { order }
-#      )
+        $.ajax(
+          type: 'POST'
+          url: '/something/reorder_pregnancies'
+          dataType: 'json'
+          data: { order }
+        )
     )


gracefully handle components w/o src dir

--- csi.coffee
+++ csi.coffee
@@ -68,7 +68,7 @@
 installTo = (tgtDir, link = false, src, name = null) ->
   src ||= sourceDirectory()
   name ||= componentName()
-  if not exists join(tgtDir, name)
+  if exists(src) and not exists(join(tgtDir, name))
     if link
       log "link-installing #{name} to #{tgtDir}"
       orig = resolve process.cwd()
@@ -129,7 +129,7 @@
   t.bfs dirTree = {path: "."}, (n) ->
     if /test[^\/]*\.js$/.test(n.path) and basename(n.path)[0] isnt "."
       results.push(n.path)
-    if fs.statSync(join(dir, n.path)).isDirectory()
+    if exists(join(dir, n.path)) and fs.statSync(join(dir, n.path)).isDirectory()
       n.children = fs.readdirSync(join(dir, n.path)).map (d) ->
         path: join n.path, d
   results


Fixes zoo teach link on education page

Fixes zoo teach link on education page

- So it's not parsed as a user

--- app/pages/about/education-page.cjsx
+++ app/pages/about/education-page.cjsx
@@ -16,7 +16,7 @@

       ### Take part in the conversation

-      Keep up with the latest Zooniverse educational efforts on the [Zooniverse Blog](http://blog.zooniverse.org/) or by following [@zooteach](https://twitter.com/ZooTeach) on Twitter. You can also talk with other Zooniverse educators and peers interested in using people-powered research in all sorts of learning environments on the [Zooniverse Education Talk board](http://zooniverse.org/talk/16).
+      Keep up with the latest Zooniverse educational efforts on the [Zooniverse Blog](http://blog.zooniverse.org/) or by following [@zooteach](https://twitter.com/ZooTeach) on Twitter. You can also talk with other Zooniverse educators and peers interested in using people-powered research in all sorts of learning environments on the [Zooniverse Education Talk board](http://zooniverse.org/talk/16).

       ### How are educators are using Zooniverse?


FIX: Fix bug preventing portfolios from being deleted

--- src/app/projects/project-portfolio-wizard/portfolio-review-step/portfolio-review-step.coffee
+++ src/app/projects/project-portfolio-wizard/portfolio-review-step/portfolio-review-step.coffee
@@ -7,10 +7,13 @@
   restrict: 'E'
   templateUrl: 'projects/project-portfolio-wizard/portfolio-review-step/portfolio-review-step.tpl.html'
   controller: ($scope, alertService, Project) ->
-    $scope.hasLSR = $scope.projectHasLearningSummaryReport()
-    $scope.hasTasksSelected = $scope.selectedTasks().length > 0
-    $scope.portfolioIsCompiling = $scope.project.compile_portfolio
-    $scope.canCompilePortfolio = (not $scope.portfolioIsCompiling) and $scope.hasTasksSelected and $scope.hasLSR and not $scope.project.portfolio_available
+
+    # Watch when portfolio value is changed to reassess
+    $scope.$watch 'project.portfolio_available', ->
+      $scope.hasLSR = $scope.projectHasLearningSummaryReport()
+      $scope.hasTasksSelected = $scope.selectedTasks().length > 0
+      $scope.portfolioIsCompiling = $scope.project.compile_portfolio
+      $scope.canCompilePortfolio = (not $scope.portfolioIsCompiling) and $scope.hasTasksSelected and $scope.hasLSR and not $scope.project.portfolio_available

     #
     # Compile portfolio
@@ -25,9 +28,9 @@
     # PDF Local Funcs
     #
     $scope.deletePortfolio = () ->
-      $scope.fileUploader.api.delete {
+      $scope.portfolioSubmission.delete {
         id: $scope.project.project_id
       }, (response) ->
-        $scope.activePortfolioTab = $scope.tabData.compileStep
         $scope.project.portfolio_available = false
+        alertService.add('info', "Portfolio has been deleted!", 5000)
 )


Fixed drag in KDView.

Fixed drag in KDView.

--- core/KDView.coffee
+++ core/KDView.coffee
@@ -86,6 +86,8 @@
     @setInstanceVariables options
     @defaultInit options,data

+    @setClass 'kddraggable' if o.draggable
+
     if location.hostname is "localhost"
       @listenTo
         KDEventTypes        : "click"
@@ -562,15 +564,39 @@
       if eventNames.test(eventName) and "function" is typeof cb
         @on eventName, cb

+  setEmptyDragState: ->
+    @dragState =
+      containment : null     # a parent KDView
+      handle      : null     # a parent KDView or a child selector
+      axis        : null     # a String 'x' or 'y' or 'diagonal'
+      direction   :
+        current   :
+          x       : null     # a String 'left' or 'right'
+          y       : null     # a String 'up'   or 'down'
+        global    :
+          x       : null     # a String 'left' or 'right'
+          y       : null     # a String 'up'   or 'down'
+      position    :
+        relative  :
+          x       : 0        # a Number
+          y       : 0        # a Number
+        initial   :
+          x       : 0        # a Number
+          y       : 0        # a Number
+        global    :
+          x       : 0        # a Number
+          y       : 0        # a Number
+      meta        :
+        top       : 0        # a Number
+        right     : 0        # a Number
+        bottom    : 0        # a Number
+        left      : 0        # a Number
+
+
   setDraggable:(options = {})->
-
     options = {} if options is yes

-    @dragState =
-      containment : options.containment             # a parent KDView
-      handle      : options.handle                  # a parent KDView or a child selector
-      axis        : options.axis                    # a String 'x' or 'y' or 'diagonal'
-
+    @setEmptyDragState()
     handle = if options.handle and options.handle instanceof KDView then handle else @

     handle.on "mousedown", (event)=>
@@ -578,21 +604,25 @@
         return if $(event.target).closest(options.handle).length is 0

       @dragIsAllowed = yes
-
-      top    = parseInt (@$()[0].style.top or 0), 10
-      right  = parseInt (@$()[0].style.right or 0), 10
-      bottom = parseInt (@$()[0].style.bottom or 0), 10
-      left   = parseInt (@$()[0].style.left or 0), 10
-
-      @dragState.startX     = event.pageX
-      @dragState.startY     = event.pageY
-      @dragState.top        = top
-      @dragState.right      = right
-      @dragState.bottom     = bottom
-      @dragState.left       = left
-
-      @dragState.directionX = unless isNaN left then "left" else "right"
-      @dragState.directionY = unless isNaN top  then "top"  else "bottom"
+      @setEmptyDragState()
+
+      dragState             = @dragState
+
+      # TODO: should move these lines
+      dragState.containment = options.containment
+      dragState.handle      = options.handle
+      dragState.axis        = options.axis
+
+      dragMeta              = dragState.meta
+      dragEl                = @$()[0]
+      dragMeta.top          = parseInt(dragEl.style.top,    10) or 0
+      dragMeta.right        = parseInt(dragEl.style.right,  10) or 0
+      dragMeta.bottom       = parseInt(dragEl.style.bottom, 10) or 0
+      dragMeta.left         = parseInt(dragEl.style.left,   10) or 0
+
+      dragPos = @dragState.position
+      dragPos.initial.x     = event.pageX
+      dragPos.initial.y     = event.pageY

       @getSingleton('windowController').setDragView @
       @emit "DragStarted", event, @dragState
@@ -601,18 +631,47 @@
       return no

   drag:(event, delta)->
-
     {directionX, directionY, axis} = @dragState
-    {x, y} = delta
-
-    y    = -y if directionY is "bottom"
-    x    = -x if directionX is "right"
-    posY = @dragState[directionY] + y
-    posX = @dragState[directionX] + x
-
+
+    {x, y}       = delta
+    dragPos      = @dragState.position
+    dragRelPos   = dragPos.relative
+    dragInitPos  = dragPos.initial
+    dragGlobPos  = dragPos.global
+    dragDir      = @dragState.direction
+    dragGlobDir  = dragDir.global
+    dragCurDir   = dragDir.current
+
+    if x > dragRelPos.x
+      dragCurDir.x  = 'right'
+    else if x < dragRelPos.x
+      dragCurDir.x  = 'left'
+
+    if y > dragRelPos.y
+      dragCurDir.y  = 'bottom'
+    else if y < dragRelPos.y
+      dragCurDir.y  = 'top'
+
+    dragGlobPos.x = dragInitPos.x + x
+    dragGlobPos.y = dragInitPos.y + y
+
+    dragGlobDir.x = if x > 0 then 'right'  else 'left'
+    dragGlobDir.y = if y > 0 then 'bottom' else 'top'
+
+    el = @$()
     if @dragIsAllowed
-      @$().css directionX, posX unless axis is 'y'
-      @$().css directionY, posY unless axis is 'x'
+      dragMeta   = @dragState.meta
+      targetPosX = if dragMeta.right  and not dragMeta.left then 'right'  else 'left'
+      targetPosY = if dragMeta.bottom and not dragMeta.top  then 'bottom' else 'top'
+
+      newX = if targetPosX is 'left' then dragMeta.left + dragRelPos.x else dragMeta.right  - dragRelPos.x
+      newY = if targetPosY is 'top'  then dragMeta.top  + dragRelPos.y else dragMeta.bottom - dragRelPos.y
+
+      el.css targetPosX, newX unless axis is 'y'
+      el.css targetPosY, newY unless axis is 'x'
+
+    dragRelPos.x = x
+    dragRelPos.y = y

     @emit "DragInAction", x, y


Update translations.coffee

Update translations.coffee

--- addon/locales/zh-tw/translations.coffee
+++ addon/locales/zh-tw/translations.coffee
@@ -53,15 +53,15 @@
   "crossroads_foundation": "十字路會"

   "app_review":
-    "title": "Rate us!" "給我們評分吧!"
-    "message": "It won’t take more than a minute and helps to promote our app. Thanks for your support :)" "不需一分鐘便能幫助推廣我們的應用程式。感謝您的支持:)＂
-    "cancel_button_label": "No, Thanks" "不用了, 謝謝"
-    "later_button_label": "Remind Me Later" "稍後再提醒我"
-    "rate_button_label": "Sure" "好的"
-    "yes_button_label": "Yes!" "是!"
-    "no_button_label": "Not really" "不是"
-    "app_rate_prompt_title": "Thank you, We appreciate your kindness. Give GoodCity a rating?" "謝謝，我們非常感激您的善舉。給好人好巿評分？
-    "feedback_prompt_title": "Mind giving us some feedback?" "請分享閣下的意見。＂
+    "title":"給我們評分吧!"
+    "message": "不需一分鐘便能幫助推廣我們的應用程式。感謝您的支持:)＂
+    "cancel_button_label":"不用了, 謝謝"
+    "later_button_label":"稍後再提醒我"
+    "rate_button_label":"好的"
+    "yes_button_label":"是!"
+    "no_button_label":"不是"
+    "app_rate_prompt_title":"謝謝，我們非常感激您的善心和慷慨。要給好人好巿評分嗎？
+    "feedback_prompt_title":"請分享閣下的意見。＂

   "menu":
     "terms": "使用條款"