combo documentation

combo documentation

--- lib/combo.coffee
+++ lib/combo.coffee
@@ -1,623 +1,646 @@
-class @Combo
-  # public ---------
-
-  # minium search term length to initate filtering
-  # minLength: 1
-
-  # max height in px on scrollbar window
-  maxHeight: 300
-
-  # number of list items in a page (PAGEUP / PAGEDOWN)
-  pageSize: 10
-
-  # whether the list should expand on input focus
-  expandOnFocus: false
-
-  # whether the active item should be selected on key TAB
-  selectOnTab: true
-
-  # sets the attribute tabindex
-  tabIndex: null
-
-  # if input is nonempty and no exact matches exists,
-  # the input will be replaced by either the empty string or
-  # the currently selected item
-  #
-  # input must be empty or selected item
-  forceSelectionFromList: false
-
-  # input may never be empty
-  # unless list is empty?
-  allowEmpty: true
-
-  # enable whether list can be closed
-  keepListOpen: false
-
-  # empty list text displayed as faux item
-  emptyListText: '(ingen valgmuligheder)'
-
-  # on list open by button click, does not reapply filters i.e. show entire list instead
-  forceAllOnButtonClick: true
-
-  # apply to filters, none, inText, firstInText, firstInWord, wholeWord
-  matchBy: 'inText'
-
-  # only shows enabled items in list
-  onlyShowEnabled: false
-
-  # enable spellcheck attribute
-  spellcheck: false
-
-  # value that - if set - will be used as submit value for the selected item
-  valueField: 'id'
-
-  # text that will be shown in textbox when item is selected
-  titleField: null
-
-  # text that will be shown in picker, and in textbox if not title is present
-  displayField: 'text'
-
-  # additional numerical searchable text, null means disabled
-  litraField: null
-
-  # value that decides if the item is enabled
-  enabledField: true
-
-  # e.g. given { modifier: '!', field: 'isReallySpecial' }, combo will only show records with the isReallySpecial field set to true when query is prefixed with !
-  modifiers: []
-
-  # e.g. given { alias: 'lag', field: 'layers' }, combo will be able to search the fields layers specifically using 'lag:' and a value
-  specifications: []
-
-  # ---------
-  source: []
-
-  disabled: true
-  activeLi: null
-  isExpanded: false
-
-  key:
-    DOWN: 40
-    END: 35
-    ENTER: 13
-    ESCAPE: 27
-    HOME: 36
-    INSERT: 45
-    LEFT: 37
-    PAGE_DOWN: 34
-    PAGE_UP: 33
-    RIGHT: 39
-    SPACE: 32
-    TAB: 9
-    UP: 38
-    BACKSPACE: 8
-    DELETE: 46
-    NUMPAD_ENTER: 108
-
-  constructor: (options = {}) ->
-    @[key] = value for own key, value of options
-
-    @container = $('
')
-
-    @inputContainer = $('
')
-      .appendTo(@container)
-
-    @input = $('<input type="text" class="combo-input" autocomplete="off" disabled="disabled"/>')
-      .attr('spellcheck', @spellcheck)
-      .appendTo(@inputContainer)
-      .bind
-        keydown: @onKeyDown
-        keyup: @onKeyUp
-        mouseup: @onMouseUp
-        # circumvent http://bugs.jquery.com/ticket/6705
-        focus: _.throttle @onFocus, 0
-        blur: @onBlur
-
-    if @tabIndex?
-      @input.attr tabindex: @tabIndex
-
-    @button = $('<button class="combo-button" tabindex="-1" disabled="disabled" />')
-      .insertAfter(@inputContainer)
-      .bind
-        click: @onButtonClick
-        mousedown: @suppressNextBlur
-
-    @listContainer = $('
')
-      .css
-        maxHeight: 0
-      .insertAfter(@button)
-
-    @list = $('
')
-      .css
-        maxHeight: 0
-      .bind
-        mousedown: @onListMouseDown
-      .appendTo(@listContainer)
-
-    # must set actual html element as context for selector (see .live() reference)
-
-    # TODO: CHANGE, need to use namespace?
-    # $('li', @list[0]).on 'click', @onListClick
-    @container.on 'click', '.combo-list li', @onListClick
-
-    @list.bgiframe() if $.fn.bgiframe
-
-    @
-
-  appendTo: (selector) ->
-    @container.appendTo $(selector)
-
-  load: (source) ->
-    @source = for item in source
-      fixedItem =
-        litra: @evaluate @litraField, item
-        value: @evaluate @valueField, item
-        display: @evaluate @displayField, item
-        title: @stripMarkup(
-          if title = @evaluate @titleField, item then title
-          else @evaluate @displayField, item
-        )
-        enabled: @evaluate @enabledField, item
-
-      for modifier in @modifiers
-        fixedItem[modifier.field] = @evaluate modifier.field, item
-
-      for specification in @specifications
-        fixedItem[specification.field] = @evaluate specification.field, item
-
-      fixedItem
-
-    @enable()
-    @ensureSelection()
-    @lastQuery = @input.val()
-
-    @input.trigger 'loaded'
-
-  setValue: (value) =>
-    for item in @source when item.value is value
-      @selectItem item, forced: yes
-      return
-    @input.val value
-
-  getSelectedValue: =>
-    item = @getSelectedItemAndIndex()?.item
-    if item? then item.value else null
-
-  getSelectedItem: =>
-    @getSelectedItemAndIndex()?.item
-
-  getSelectedIndex: =>
-    @getSelectedItemAndIndex()?.index
-
-  getSelectedItemAndIndex: =>
-    return {item, index} for item, index in @source when item.title is @input.val()
-
-  hasSelection: ->
-    @getSelectedItemAndIndex()?
-
-  getValue: =>
-    @getSelectedValue() ? @getRawValue()
-
-  getRawValue: =>
-    @input.val()
-
-  isEmpty: =>
-    @input.val() is '' or @input.val() is null
-
-  selectLi: (li) =>
-    console.log "selectLi"
-    @selectItem @source[$(li).data('combo-id')]
-    @refocus()
-
-  selectItem: (item, options = {}) =>
-    console.log "selectItem"
-    return if not item.enabled and not options.forced
-    @input.val item.title
-    @lastQuery = @input.val()
-    @updateLastSelection()
-    @internalCollapse()
-    _.delay (=> @input.trigger 'itemSelect', title: item.title), 10
-
-  onListClick: (event) =>
-    console.log "onListMouseDown"
-    @selectLi event.currentTarget
-
-  onListMouseDown: (event) =>
-    # if it's a list item (or subelement of list item), prevent the
-    # close-on-blur in case of a "slow" click on the list (long mousedown)
-    @suppressNextBlur()
-
-    # if it's the scrollbar, send focus back to input after scrolling
-    # this can't be done in mouseup as chrome won't send mouseup for
-    # clicks on the scrollbar - only the list
-    if not $(event.target).closest('ul.combo-list li').length
+(($, window) ->
+  class @Combo
+    # public ---------
+
+    # minium search term length to initate filtering
+    # minLength: 1
+
+    # max height in px on scrollbar window
+    maxHeight: 300
+
+    # number of list items in a page (PAGEUP / PAGEDOWN)
+    pageSize: 10
+
+    # whether the list should expand on input focus
+    expandOnFocus: false
+
+    # whether the active item should be selected on key TAB
+    selectOnTab: true
+
+    # sets the attribute tabindex
+    tabIndex: null
+
+    # if input is nonempty and no exact matches exists,
+    # the input will be replaced by either the empty string or
+    # the currently selected item
+    #
+    # input must be empty or selected item
+    forceSelectionFromList: false
+
+    # input may never be empty
+    # unless list is empty?
+    allowEmpty: true
+
+    # enable whether list can be closed
+    keepListOpen: false
+
+    # empty list text displayed as faux item
+    emptyListText: '(ingen valgmuligheder)'
+
+    # on list open by button click, does not reapply filters i.e. show entire list instead
+    forceAllOnButtonClick: true
+
+    # apply to filters, none, inText, firstInText, firstInWord, wholeWord
+    matchBy: 'inText'
+
+    # only shows enabled items in list
+    onlyShowEnabled: false
+
+    # enable spellcheck attribute
+    spellcheck: false
+
+    # value that - if set - will be used as submit value for the selected item
+    valueField: 'id'
+
+    # text that will be shown in textbox when item is selected
+    titleField: null
+
+    # text that will be shown in picker, and in textbox if not title is present
+    displayField: 'text'
+
+    # additional numerical searchable text, null means disabled
+    litraField: null
+
+    # value that decides if the item is enabled
+    enabledField: true
+
+    # e.g. given { modifier: '!', field: 'isReallySpecial' }, combo will only show records with the isReallySpecial field set to true when query is prefixed with !
+    modifiers: []
+
+    # e.g. given { alias: 'lag', field: 'layers' }, combo will be able to search the fields layers specifically using 'lag:' and a value
+    specifications: []
+
+    # ---------
+    source: []
+
+    disabled: true
+    activeLi: null
+    isExpanded: false
+
+    key:
+      DOWN: 40
+      END: 35
+      ENTER: 13
+      ESCAPE: 27
+      HOME: 36
+      INSERT: 45
+      LEFT: 37
+      PAGE_DOWN: 34
+      PAGE_UP: 33
+      RIGHT: 39
+      SPACE: 32
+      TAB: 9
+      UP: 38
+      BACKSPACE: 8
+      DELETE: 46
+      NUMPAD_ENTER: 108
+
+    constructor: (wrapper, options = {}) ->
+      @[key] = value for own key, value of options
+
+      @container = $('
')
+      $(wrapper).append(@container)
+
+      @inputContainer = $('
')
+        .appendTo(@container)
+
+      @input = $('<input type="text" class="combo-input" autocomplete="off" disabled="disabled"/>')
+        .attr('spellcheck', @spellcheck)
+        .appendTo(@inputContainer)
+        .bind
+          keydown: @onKeyDown
+          keyup: @onKeyUp
+          mouseup: @onMouseUp
+          # circumvent http://bugs.jquery.com/ticket/6705
+          focus: _.throttle @onFocus, 0
+          blur: @onBlur
+
+      if @tabIndex?
+        @input.attr tabindex: @tabIndex
+
+      @button = $('<button class="combo-button" tabindex="-1" disabled="disabled" />')
+        .insertAfter(@inputContainer)
+        .bind
+          click: @onButtonClick
+          mousedown: @suppressNextBlur
+
+      @listContainer = $('
')
+        .css
+          maxHeight: 0
+        .insertAfter(@button)
+
+      @list = $('
')
+        .css
+          maxHeight: 0
+        .bind
+          mousedown: @onListMouseDown
+        .appendTo(@listContainer)
+
+      # must set actual html element as context for selector (see .live() reference)
+      @container.on 'click', '.combo-list li', @onListClick
+
+      @list.bgiframe() if $.fn.bgiframe
+
+      if options.source
+        @load(options.source)
+
+      @
+
+    appendTo: (selector) ->
+      @container.appendTo $(selector)
+
+    load: (source) ->
+      @source = for item in source
+        fixedItem =
+          litra: @evaluate @litraField, item
+          value: @evaluate @valueField, item
+          display: @evaluate @displayField, item
+          title: @stripMarkup(
+            if title = @evaluate @titleField, item then title
+            else @evaluate @displayField, item
+          )
+          enabled: @evaluate @enabledField, item
+
+        for modifier in @modifiers
+          fixedItem[modifier.field] = @evaluate modifier.field, item
+
+        for specification in @specifications
+          fixedItem[specification.field] = @evaluate specification.field, item
+
+        fixedItem
+
+      @enable()
+      @ensureSelection()
+      @lastQuery = @input.val()
+
+      @container.trigger 'loaded'
+
+    setValue: (value) =>
+      for item in @source when item.value is value
+        @selectItem item, forced: yes
+        return
+      @input.val value
+
+    getSelectedValue: =>
+      item = @getSelectedItemAndIndex()?.item
+      if item? then item.value else null
+
+    getSelectedItem: =>
+      @getSelectedItemAndIndex()?.item
+
+    getSelectedIndex: =>
+      @getSelectedItemAndIndex()?.index
+
+    getSelectedItemAndIndex: =>
+      return {item, index} for item, index in @source when item.title is @input.val()
+
+    hasSelection: ->
+      @getSelectedItemAndIndex()?
+
+    getValue: =>
+      @getSelectedValue() ? @getRawValue()
+
+    getRawValue: =>
+      @input.val()
+
+    isEmpty: =>
+      @input.val() is '' or @input.val() is null
+
+    selectLi: (li) =>
+      console.log "selectLi"
+      @selectItem @source[$(li).data('combo-id')]
       @refocus()

-  onButtonClick: (event) =>
-    console.log "onButtonClick"
-    return if @disabled
-
-    # if it's open and is not empty, close it
-    if @isExpanded and $('li', @list).length
+    selectItem: (item, options = {}) =>
+      console.log "selectItem"
+      return if not item.enabled and not options.forced
+      @input.val item.title
+      @lastQuery = @input.val()
+      @updateLastSelection()
       @internalCollapse()
-    else
-      @searchAndExpand forceAll: @forceAllOnButtonClick
-
-    @focus()
-
-  onKeyDown: (event) =>
-    console.log "onKeyDown"
-    return if @disabled
-
-    if @isExpanded
-      switch event.keyCode
-        when @key.HOME
-          @moveHome() and event.preventDefault()
-        when @key.END
-          @moveEnd() and event.preventDefault()
-        when @key.PAGE_UP
-          @movePreviousPage()
-          event.preventDefault()
-        when @key.PAGE_DOWN
-          @moveNextPage()
-          event.preventDefault()
-        when @key.UP
-          @movePrevious()
-          event.preventDefault()
-        when @key.DOWN
-          @moveNext()
-          event.preventDefault()
-        when @key.ENTER, @key.NUMPAD_ENTER
-          # Opera still allows the keypress to occur which causes forms to submit
-          @input.one 'keypress', (keypress) => keypress.preventDefault()
-          @selectLi @activeLi if @activeLi
-          event.preventDefault()
-          event.stopPropagation()
-        when @key.TAB
-          @selectLi @activeLi if @activeLi and @selectOnTab
-          if @source.length
-            # allow tab on empty list
+      _.delay (=> @container.trigger 'itemSelect', title: item.title), 10
+
+    onListClick: (event) =>
+      console.log "onListMouseDown"
+      @selectLi event.currentTarget
+
+    onListMouseDown: (event) =>
+      # if it's a list item (or subelement of list item), prevent the
+      # close-on-blur in case of a "slow" click on the list (long mousedown)
+      @suppressNextBlur()
+
+      # if it's the scrollbar, send focus back to input after scrolling
+      # this can't be done in mouseup as chrome won't send mouseup for
+      # clicks on the scrollbar - only the list
+      if not $(event.target).closest('ul.combo-list li').length
+        @refocus()
+
+    onButtonClick: (event) =>
+      console.log "onButtonClick"
+      return if @disabled
+
+      # if it's open and is not empty, close it
+      if @isExpanded and $('li', @list).length
+        @internalCollapse()
+      else
+        @searchAndExpand forceAll: @forceAllOnButtonClick
+
+      @focus()
+
+    onKeyDown: (event) =>
+      console.log "onKeyDown"
+      return if @disabled
+
+      if @isExpanded
+        switch event.keyCode
+          when @key.HOME
+            @moveHome() and event.preventDefault()
+          when @key.END
+            @moveEnd() and event.preventDefault()
+          when @key.PAGE_UP
+            @movePreviousPage()
+            event.preventDefault()
+          when @key.PAGE_DOWN
+            @moveNextPage()
+            event.preventDefault()
+          when @key.UP
+            @movePrevious()
+            event.preventDefault()
+          when @key.DOWN
+            @moveNext()
+            event.preventDefault()
+          when @key.ENTER, @key.NUMPAD_ENTER
+            # Opera still allows the keypress to occur which causes forms to submit
+            @input.one 'keypress', (keypress) => keypress.preventDefault()
+            @selectLi @activeLi if @activeLi
             event.preventDefault()
             event.stopPropagation()
-
-        when @key.ESCAPE
-          @internalCollapse()
-    else
+          when @key.TAB
+            @selectLi @activeLi if @activeLi and @selectOnTab
+            if @source.length
+              # allow tab on empty list
+              event.preventDefault()
+              event.stopPropagation()
+
+          when @key.ESCAPE
+            @internalCollapse()
+      else
+        switch event.keyCode
+          when @key.PAGE_UP, @key.PAGE_DOWN, @key.UP, @key.DOWN
+            @searchAndExpand()
+            event.preventDefault()
+          when @key.ENTER
+            @input.trigger 'enterpress'
+            event.preventDefault();
+          when @key.ESCAPE
+            @input.select()
+
+    onKeyUp: (event) =>
+      return if @disabled
+
+      @updateLastSelection()
+
+      return if @lastQuery is @input.val()
+
       switch event.keyCode
-        when @key.PAGE_UP, @key.PAGE_DOWN, @key.UP, @key.DOWN
-          @searchAndExpand()
-          event.preventDefault()
-        when @key.ENTER
-          @input.trigger 'enterpress'
-          event.preventDefault();
-        when @key.ESCAPE
-          @input.select()
-
-  onKeyUp: (event) =>
-    return if @disabled
-
-    @updateLastSelection()
-
-    return if @lastQuery is @input.val()
-
-    switch event.keyCode
-      when @key.BACKSPACE, @key.DELETE, @key.ENTER
-        # do not open the list when deleting chars
-        return unless @isExpanded
-
-    @searchAndExpand()
-
-  onMouseUp: =>
-    console.log "onMouseUp"
-    return if @disabled
-    @updateLastSelection()
-
-  onFocus: (event, data) =>
-    clearTimeout @closing
-    return if @disabled
-
-    if not data?.forcedFocus
-      @input.trigger 'enter'
-      @searchAndExpand() if @expandOnFocus
-      _.defer => @input.select()
-
-  onBlur: (event) =>
-    if @disabled or @blurIsSuppressed
-      @blurIsSuppressed = false
-      return
-
-    @ensureSelection()
-    @internalCollapse()
-    @input.trigger 'leave'
-
-  ensureSelection: ->
-    return if @disabled or not @forceSelectionFromList or @hasSelection()
-
-    if @lastSelection? and (not @isEmpty() or (@isEmpty() and not @allowEmpty))
-      @selectItem @lastSelection.item
-    else
-      if not @allowEmpty
-        @selectItem @source[0] if @source.length
-      else
-        @input.val ''
-        @lastSelection = null
-
-  on: (args...) -> @input.on args...
-  off: (args...) -> @input.off args...
-
-  updateLastSelection: =>
-    console.log 'updateLastSelection'
-    return if not @forceSelectionFromList
-    currentSelection = @getSelectedItemAndIndex()
-    @lastSelection = currentSelection if currentSelection?
-
-  suppressNextBlur: => @blurIsSuppressed = true if @input.is(':focus')
-
-  refocus: ->
-    # every time a focus is forced from within the combo effects that should
-    # only follow a 'natural' focus must be suppressed
-    # every focus is delayed a tiny bit to accomodate for timing issues where the current
-    # focus might not yet have changed by last mouse/key event
-    _.delay (=>
-      if not @input.is(':focus')
-        @input.trigger 'focus', { forcedFocus: true }),
-    1
-
-  focus: =>
-    _.delay (=>
-      if not @input.is(':focus')
-        @input.trigger 'focus'),
-    1
-
-  activateSelectedItem: =>
-    index = @getSelectedIndex()
-    return if not index?
-    for li in @list.children() when $(li).data('combo-id') == index
-      @activate $(li)
-      $(li).addClass('selected')
-
-  stripMarkup: (text) ->
-    text?.replace(/
]*>/g, " | ").replace(/<(?:.|\s)*?>/g, '').replace(/[\n\r]/g, '')
-
-  searchAndExpand: (options = {}) =>
-    @lastQuery = @input.val()
-
-    if @hasSelection() or options.forceAll
-      @renderFullList()
-      @activateSelectedItem()
-      @expand(options)
-      return
-
-    @renderFilteredList()
-    @activate $('li:first', @list)
-    @expand()
-
-  buildFilters: (queryString) ->
-    filters = []
-    queryString = queryString.replace(/([.*+?^${}()|[\]\/\\])/g, "\\$1")
-    queryString = queryString.replace(/([<>])/g, "")
-    firstChar = queryString.substr(0, 1)
-
-    for modifier in @modifiers
-      if firstChar == modifier.modifier
+        when @key.BACKSPACE, @key.DELETE, @key.ENTER
+          # do not open the list when deleting chars
+          return unless @isExpanded
+
+      @searchAndExpand()
+
+    onMouseUp: =>
+      console.log "onMouseUp"
+      return if @disabled
+      @updateLastSelection()
+
+    onFocus: (event, data) =>
+      clearTimeout @closing
+      return if @disabled
+
+      if not data?.forcedFocus
+        @input.trigger 'enter'
+        @searchAndExpand() if @expandOnFocus
+        _.defer => @input.select()
+
+    onBlur: (event) =>
+      if @disabled or @blurIsSuppressed
+        @blurIsSuppressed = false
+        return
+
+      @ensureSelection()
+      @internalCollapse()
+      @input.trigger 'leave'
+
+    ensureSelection: ->
+      return if @disabled or not @forceSelectionFromList or @hasSelection()
+
+      if @lastSelection? and (not @isEmpty() or (@isEmpty() and not @allowEmpty))
+        @selectItem @lastSelection.item
+      else
+        if not @allowEmpty
+          @selectItem @source[0] if @source.length
+        else
+          @input.val ''
+          @lastSelection = null
+
+    # on: (args...) -> @input.on args...
+    # off: (args...) -> @input.off args...
+
+    updateLastSelection: =>
+      console.log 'updateLastSelection'
+      return if not @forceSelectionFromList
+      currentSelection = @getSelectedItemAndIndex()
+      @lastSelection = currentSelection if currentSelection?
+
+    suppressNextBlur: => @blurIsSuppressed = true if @input.is(':focus')
+
+    refocus: ->
+      # every time a focus is forced from within the combo effects that should
+      # only follow a 'natural' focus must be suppressed
+      # every focus is delayed a tiny bit to accomodate for timing issues where the current
+      # focus might not yet have changed by last mouse/key event
+      _.delay (=>
+        if not @input.is(':focus')
+          @input.trigger 'focus', { forcedFocus: true }),
+      1
+
+    focus: =>
+      _.delay (=>
+        if not @input.is(':focus')
+          @input.trigger 'focus'),
+      1
+
+    activateSelectedItem: =>
+      index = @getSelectedIndex()
+      return if not index?
+      for li in @list.children() when $(li).data('combo-id') == index
+        @activate $(li)
+        $(li).addClass('selected')
+
+    stripMarkup: (text) ->
+      text?.replace(/
]*>/g, " | ").replace(/<(?:.|\s)*?>/g, '').replace(/[\n\r]/g, '')
+
+    searchAndExpand: (options = {}) =>
+      @lastQuery = @input.val()
+
+      if @hasSelection() or options.forceAll
+        @renderFullList()
+        @activateSelectedItem()
+        @expand(options)
+        return
+
+      @renderFilteredList()
+      @activate $('li:first', @list)
+      @expand()
+
+    buildFilters: (queryString) ->
+      filters = []
+      queryString = queryString.replace(/([.*+?^${}()|[\]\/\\])/g, "\\$1")
+      queryString = queryString.replace(/([<>])/g, "")
+      firstChar = queryString.substr(0, 1)
+
+      for modifier in @modifiers
+        if firstChar == modifier.modifier
+          filters.push
+            property: modifier.field
+            predicate: (value) -> value
+          queryString = queryString.substr(1)
+
+      for specification in @specifications
+        specFinder = new RegExp(specification.alias + ":\\s*(\\w+)")
+        specsInQuery = specFinder.exec(queryString)
+        if specsInQuery
+          filters.push
+            property: specification.field
+            predicate: (value) -> value is specsInQuery[1]
+          queryString = queryString.replace(specFinder, "")
+
+      queryStringSplit = queryString.split(" ")
+      if @litraField?? and queryString.match(/^[#,]\w[\w\\\.,]*(\s|$)/)
+        first = queryStringSplit.shift()
         filters.push
-          property: modifier.field
-          predicate: (value) -> value
-        queryString = queryString.substr(1)
-
-    for specification in @specifications
-      specFinder = new RegExp(specification.alias + ":\\s*(\\w+)")
-      specsInQuery = specFinder.exec(queryString)
-      if specsInQuery
-        filters.push
-          property: specification.field
-          predicate: (value) -> value is specsInQuery[1]
-        queryString = queryString.replace(specFinder, "")
-
-    queryStringSplit = queryString.split(" ")
-    if @litraField?? and queryString.match(/^[#,]\w[\w\\\.,]*(\s|$)/)
-      first = queryStringSplit.shift()
-      filters.push
-        property: 'litra'
-        regex: new RegExp("^()(" + first.substr(1).replace(/,/g, "\\.") + ")", "i")
-        predicate: (value) -> @regex.test value
-
-    for currentWord in queryStringSplit when currentWord isnt ""
-      currentWord = currentWord.replace(/\\\*/g, "[\\wæøåÆØÅ]*")
-      currentWord = currentWord.replace(/\\\+/g, " ")
-      dontSearchInsideTags = "(?![^><\\[\\]]*(>|\\]))"
-      switch @matchBy
-        when "none"
-          break
-        when "inText"
-          filters.push
-            property: 'display'
-            regex: new RegExp("()(" + currentWord + ")()" + dontSearchInsideTags, "i")
-            predicate: (value) -> @regex.test value
-        when "firstInText"
-          filters.push
-            property: 'display'
-            regex: new RegExp("^()(" + currentWord + ")()" + dontSearchInsideTags, "i")
-            predicate: (value) -> @regex.test value
-        when "firstInWord"
-          filters.push
-            property: 'display'
-            regex: new RegExp("(^|[^\\wæøåÆØÅ\\[\\]])(" + currentWord + ")()" + dontSearchInsideTags, "i")
-            predicate: (value) -> @regex.test value
-        when "wholeWord"
-          filters.push
-            property: 'display'
-            regex: new RegExp("(^|[^\\wæøåÆØÅ\\[\\]])(" + currentWord + ")($|[^\\wæøåÆØÅ\\[\\]])", "i")
-            predicate: (value) -> @regex.test value
-        else
-          throw new Error "matchBy not set to a valid value"
-
-    filters
-
-  positionList: ->
-    @listContainer.css
-      zIndex: @container.css('zIndex') + 1
-
-  renderFilteredList: =>
-    filters = if @input.val() is '' then [] else @buildFilters @input.val()
-    @renderList @source, filters
-
-  renderFullList: =>
-    @renderList @source, []
-
-  renderList: (items, filters) =>
-    # for performance use native html manipulation
-    # be aware never to attach events or data to list elements!
-
-    htmls = for item, index in items when not @onlyShowEnabled or item.enabled
-      @renderItem item, index, filters
-
-    if htmls.length
-      @list[0].innerHTML = htmls.join('')
-    else
-      @list[0].innerHTML = "
#{@emptyListText}
"
-
-  renderItem: (item, index, filters) =>
-    for filter in filters
-      return unless filter.predicate item[filter.property]
-
-    if @litraField? and (litra = item.litra)?
-      text = "[#{litra}] #{@highlightValue(item, 'display', filters)}"
-    else
-      text = @highlightValue(item, 'display', filters)
-
-    classes = [
-      if @onlyShowEnabled or item.enabled then 'enabled' else 'disabled'
-    ]
-
-    "
#{text}
"
-
-  highlightValue: (item, property, filters) =>
-    value = item[property]
-    return null if not value?
-
-    for filter in filters when filter.property == property and filter.regex?
-      value = value.replace(filter.regex, "$2")
-    value
-
-  moveNext: ->
-    if @activeLi
-      @activate @activeLi.next() unless @lastItemIsActive()
-    else
-      @moveHome()
-
-  movePrevious: ->
-    if @activeLi
-      @activate @activeLi.prev() unless @firstItemIsActive()
-    else
-      @moveEnd()
-
-  moveNextPage: ->
-    @moveHome() unless @activeLi
-    rest = @activeLi.nextAll()
-    if rest.length >= @pageSize
-      @activate rest.eq @pageSize-1
-    else
-      @moveEnd()
-
-  movePreviousPage: ->
-    @activate $('li:last', @list) unless @activeLi
-    rest = @activeLi.prevAll()
-    if rest.length >= @pageSize
-      @activate rest.eq @pageSize-1
-    else
-      @moveHome()
-
-  moveHome: ->
-    @activate $('li:first', @list)
-
-  moveEnd: ->
-    @activate $('li:last', @list)
-
-  activate: (item) ->
-    @activeLi?.removeClass 'active'
-
-    if item? and item.length
-      item.addClass 'active'
-      @activeLi = item
+          property: 'litra'
+          regex: new RegExp("^()(" + first.substr(1).replace(/,/g, "\\.") + ")", "i")
+          predicate: (value) -> @regex.test value
+
+      for currentWord in queryStringSplit when currentWord isnt ""
+        currentWord = currentWord.replace(/\\\*/g, "[\\wæøåÆØÅ]*")
+        currentWord = currentWord.replace(/\\\+/g, " ")
+        dontSearchInsideTags = "(?![^><\\[\\]]*(>|\\]))"
+        switch @matchBy
+          when "none"
+            break
+          when "inText"
+            filters.push
+              property: 'display'
+              regex: new RegExp("()(" + currentWord + ")()" + dontSearchInsideTags, "i")
+              predicate: (value) -> @regex.test value
+          when "firstInText"
+            filters.push
+              property: 'display'
+              regex: new RegExp("^()(" + currentWord + ")()" + dontSearchInsideTags, "i")
+              predicate: (value) -> @regex.test value
+          when "firstInWord"
+            filters.push
+              property: 'display'
+              regex: new RegExp("(^|[^\\wæøåÆØÅ\\[\\]])(" + currentWord + ")()" + dontSearchInsideTags, "i")
+              predicate: (value) -> @regex.test value
+          when "wholeWord"
+            filters.push
+              property: 'display'
+              regex: new RegExp("(^|[^\\wæøåÆØÅ\\[\\]])(" + currentWord + ")($|[^\\wæøåÆØÅ\\[\\]])", "i")
+              predicate: (value) -> @regex.test value
+          else
+            throw new Error "matchBy not set to a valid value"
+
+      filters
+
+    positionList: ->
+      @listContainer.css
+        zIndex: @container.css('zIndex') + 1
+
+    renderFilteredList: =>
+      filters = if @input.val() is '' then [] else @buildFilters @input.val()
+      @renderList @source, filters
+
+    renderFullList: =>
+      @renderList @source, []
+
+    renderList: (items, filters) =>
+      # for performance use native html manipulation
+      # be aware never to attach events or data to list elements!
+
+      htmls = for item, index in items when not @onlyShowEnabled or item.enabled
+        @renderItem item, index, filters
+
+      if htmls.length
+        @list[0].innerHTML = htmls.join('')
+      else
+        @list[0].innerHTML = "
#{@emptyListText}
"
+
+    renderItem: (item, index, filters) =>
+      for filter in filters
+        return unless filter.predicate item[filter.property]
+
+      if @litraField? and (litra = item.litra)?
+        text = "[#{litra}] #{@highlightValue(item, 'display', filters)}"
+      else
+        text = @highlightValue(item, 'display', filters)
+
+      classes = [
+        if @onlyShowEnabled or item.enabled then 'enabled' else 'disabled'
+      ]
+
+      "
#{text}
"
+
+    highlightValue: (item, property, filters) =>
+      value = item[property]
+      return null if not value?
+
+      for filter in filters when filter.property == property and filter.regex?
+        value = value.replace(filter.regex, "$2")
+      value
+
+    moveNext: ->
+      if @activeLi
+        @activate @activeLi.next() unless @lastItemIsActive()
+      else
+        @moveHome()
+
+    movePrevious: ->
+      if @activeLi
+        @activate @activeLi.prev() unless @firstItemIsActive()
+      else
+        @moveEnd()
+
+    moveNextPage: ->
+      @moveHome() unless @activeLi
+      rest = @activeLi.nextAll()
+      if rest.length >= @pageSize
+        @activate rest.eq @pageSize-1
+      else
+        @moveEnd()
+
+    movePreviousPage: ->
+      @activate $('li:last', @list) unless @activeLi
+      rest = @activeLi.prevAll()
+      if rest.length >= @pageSize
+        @activate rest.eq @pageSize-1
+      else
+        @moveHome()
+
+    moveHome: ->
+      @activate $('li:first', @list)
+
+    moveEnd: ->
+      @activate $('li:last', @list)
+
+    activate: (item) ->
+      @activeLi?.removeClass 'active'
+
+      if item? and item.length
+        item.addClass 'active'
+        @activeLi = item
+        @scrollIntoView()
+      else
+        @activeLi = null
+
+      @activeLi
+
+    firstItemIsActive: => @activeLi?[0] is $("li:first", @list)[0]
+    lastItemIsActive: => @activeLi?[0] is $('li:last', @list)[0]
+
+    scrollIntoView: =>
+      return unless @isExpanded and @activeLi
+
+      #for performance just reset scroll to top if the first item is active
+      if @firstItemIsActive()
+        @list.scrollTop 0 if @list[0].scrollTop > 0
+        return
+
+      # for performance only get list height once
+      hasScroll = @list.prop('scrollHeight') > @maxHeight
+
+      if hasScroll
+        listTopBorder = parseFloat($.css(@list[0], 'borderTopWidth')) || 0
+        listTopPadding = parseFloat($.css(@list[0], 'paddingTop')) || 0
+        itemOffset = @activeLi.offset().top - @list.offset().top - listTopBorder - listTopPadding
+        listHeight = @maxHeight
+        currentScroll = @list.scrollTop()
+        itemHeight = @activeLi.outerHeight()
+        scroll =
+          if itemOffset < 0
+            currentScroll + itemOffset
+          else if itemOffset + itemHeight > listHeight
+            currentScroll + itemOffset - listHeight + itemHeight
+        @list.scrollTop scroll
+
+    expand: (options = {}) =>
+      return if @disabled
+      return if @isExpanded
+      @container.addClass 'expanded'
+      @isExpanded = true
+      @listContainer.animate {maxHeight: @maxHeight}, duration: 60
+      @list.animate {maxHeight: @maxHeight}, duration: 60, callback: options.callback
+      @positionList()
       @scrollIntoView()
-    else
-      @activeLi = null
-
-    @activeLi
-
-  firstItemIsActive: => @activeLi?[0] is $("li:first", @list)[0]
-  lastItemIsActive: => @activeLi?[0] is $('li:last', @list)[0]
-
-  scrollIntoView: =>
-    return unless @isExpanded and @activeLi
-
-    #for performance just reset scroll to top if the first item is active
-    if @firstItemIsActive()
-      @list.scrollTop 0 if @list[0].scrollTop > 0
-      return
-
-    # for performance only get list height once
-    hasScroll = @list.prop('scrollHeight') > @maxHeight
-
-    if hasScroll
-      listTopBorder = parseFloat($.css(@list[0], 'borderTopWidth')) || 0
-      listTopPadding = parseFloat($.css(@list[0], 'paddingTop')) || 0
-      itemOffset = @activeLi.offset().top - @list.offset().top - listTopBorder - listTopPadding
-      listHeight = @maxHeight
-      currentScroll = @list.scrollTop()
-      itemHeight = @activeLi.outerHeight()
-      scroll =
-        if itemOffset < 0
-          currentScroll + itemOffset
-        else if itemOffset + itemHeight > listHeight
-          currentScroll + itemOffset - listHeight + itemHeight
-      @list.scrollTop scroll
-
-  expand: (options = {}) =>
-    return if @disabled
-    return if @isExpanded
-    @container.addClass 'expanded'
-    @isExpanded = true
-    @listContainer.animate {maxHeight: @maxHeight}, duration: 60
-    @list.animate {maxHeight: @maxHeight}, duration: 60, callback: options.callback
-    @positionList()
-    @scrollIntoView()
-
-  internalCollapse: =>
-    if @keepListOpen
-      @searchAndExpand()
-    else
-      @collapse()
-
-  collapse: (options = {}) =>
-    @container.removeClass 'expanded'
-    @isExpanded = false
-    @listContainer.animate {maxHeight: 0}, duration: 60
-    @list.animate {maxHeight: 0}, duration: 60, callback: options.callback
-
-  disable: =>
-    @disabled = true
-    @input.attr disabled: true
-    @button.attr disabled: true
-
-  enable: =>
-    @disabled = false
-    @input.attr disabled: false
-    @button.attr disabled: false
-
-  detach: ->
-    @container.detach()
-
-  destroy: ->
-    @container.remove()
-
-  evaluate: (fieldGetter, item) ->
-    if not fieldGetter?
-      null
-    else if _.isFunction(fieldGetter)
-      fieldGetter(item)
-    else if _.isFunction(item[fieldGetter])
-      item[fieldGetter]()
-    else
-      item[fieldGetter]+
+    internalCollapse: =>
+      if @keepListOpen
+        @searchAndExpand()
+      else
+        @collapse()
+
+    collapse: (options = {}) =>
+      @container.removeClass 'expanded'
+      @isExpanded = false
+      @listContainer.animate {maxHeight: 0}, duration: 60
+      @list.animate {maxHeight: 0}, duration: 60, callback: options.callback
+
+    disable: =>
+      @disabled = true
+      @input.attr disabled: true
+      @button.attr disabled: true
+
+    enable: =>
+      @disabled = false
+      @input.attr disabled: false
+      @button.attr disabled: false
+
+    detach: ->
+      @container.detach()
+
+    destroy: ->
+      @container.remove()
+
+    evaluate: (fieldGetter, item) ->
+      if not fieldGetter?
+        null
+      else if _.isFunction(fieldGetter)
+        fieldGetter(item)
+      else if _.isFunction(item[fieldGetter])
+        item[fieldGetter]()
+      else
+        item[fieldGetter]
+
+
+#====================================================
+# PLUGIN DEFINITION
+#====================================================
+
+  # Define the plugin
+  $.fn.extend combo: (option, args...) ->
+
+    @each ->
+      el = $(@)
+      plugin = el.data('plugin')
+
+      if !plugin
+        el.data('plugin', (data = new Combo(@, option)))
+      if typeof option == 'string'
+        # element.combo(functionName, args)
+        plugin[option].apply(plugin, args)
+
+#====================================================
+)(window.jQuery, window)


tweak config

tweak config

--- config/application.coffee
+++ config/application.coffee
@@ -8,6 +8,14 @@
 module.exports = require(process.env['LINEMAN_MAIN']).config.extend "application",

   loadNpmTasks: ["grunt-markdown-blog"]
+
+  enableSass: true
+  enableAssetFingerprint: true
+
+  # Use grunt-markdown-blog in lieu of Lineman's built-in pages task
+  prependTasks:
+    common: "markdown:dev"
+    dist: "markdown:dist"

   markdown:
     options:
@@ -44,17 +52,9 @@
       context:
         cdn: "http://cdn4.testdouble.com/"

-  # Use grunt-markdown-blog in lieu of Lineman's built-in pages task
-  prependTasks:
-    common: "markdown:dev"
-    dist: "markdown:dist"
-
-  enableSass: true
   sass:
     options:
       bundleExec: true
-
-  enableAssetFingerprint: true

   watch:
     markdown:


Generate coverage output file

Generate coverage output file

--- lib/gocov.coffee
+++ lib/gocov.coffee
@@ -1,4 +1,5 @@
 spawn = require('child_process').spawn
+temp = require('temp')
 {Subscriber, Emitter} = require 'emissary'
 GocovAreaView = require './gocov/gocov-area-view'
 GocovParser = require './gocov/gocov-parser'
@@ -38,8 +39,27 @@
       @resetCoverage()

   runCoverage: =>
-    for area in areas
-      area.processCoverageFile()
+    tempDir = temp.mkdirSync()
+    gopath = @dispatch.buildGoPath()
+    editorView = atom.workspaceView.getActiveView()
+    buffer = editorView?.getEditor()?.getBuffer()
+    env = process.env
+    env['GOPATH'] = gopath
+    re = new RegExp(buffer.getBaseName() + '$')
+    cwd = buffer.getPath().replace(re, '')
+    cmd = atom.config.get('go-plus.goExecutablePath')
+    cmd = @dispatch.replaceTokensInPath(cmd, true)
+    console.log cmd, "test -coverprofile=#{tempDir}/coverage.out"
+    proc = spawn(cmd, ["test", "-coverprofile=#{tempDir}/coverage.out"], {cwd: cwd, env: env})
+    proc.on 'error', (error) =>
+      return unless error?
+      console.log @name + ': error launching command [go] – ' + error  + ' – current PATH: [' + process.env.PATH + ']'
+    proc.stderr.on 'data', (data) => console.log 'go test: ' + data if data?
+    proc.stdout.on 'data', (data) => console.log 'go test: ' + data if data?
+    proc.on 'close', (code) =>
+      console.log 'gocov: [go test] exited with code [' + code + ']' if code isnt 0
+      for area in areas
+        area.processCoverageFile()

   resetCoverage: =>
     for area in areas


Default isortPath as just the bin name

Default isortPath as just the bin name

--- lib/index.coffee
+++ lib/index.coffee
@@ -2,7 +2,7 @@

 module.exports =
   configDefaults:
-    isortPath: "/usr/bin/isort"
+    isortPath: "isort"
     sortOnSave: false
     checkOnSave: true


Removed direct dependancy on otTypes in index

--- lib/index.coffee
+++ lib/index.coffee
@@ -2,7 +2,6 @@
 {EventEmitter} = require 'events'
 assert = require 'assert'
 redisLib = require 'redis'
-otTypes = require 'ot-types'
 ot = require './ot'

 exports.mongo = require './mongo'


disabled all autosave stuff for edit mode

--- public/dreamcatcher/controllers/entries_controller.coffee
+++ public/dreamcatcher/controllers/entries_controller.coffee
@@ -3,56 +3,47 @@
   fields: ['#entry_title','#entry_body','#sharing-list','#entryType-list']
   interval: 5000
   userModel: Dreamcatcher.Models.User
-
+
   init: ->
     @entryCookie = new Dreamcatcher.Classes.CookieHelper "dc_new_entry",true
+    @currentEntryType = $('#entryMode').data 'id'
     @posted = false
     @retrieveState()
     @initSelectMenu()
     @saveState()
-
+
   saveState: ->
     return if @posted
-    entryBody = $('#entry_body').val().trim()
-    if entryBody.length > 0
-      entry = {}
-      entry['type'] = $('#entryMode').data 'id'
-      entry['id'] = $('#entryId').data 'id'
+    if @currentEntryType is 'new'
+      entryBody = $('#entry_body').val().trim()
+      if entryBody.length > 0
+        entry = {}
+        entry['type'] = $('#entryMode').data 'id'
+        entry['id'] = $('#entryId').data 'id'

-      entry[field] = $(field).val() for field in @fields
-      entry['#currentImages'] = $('#currentImages').html()
-      @entryCookie.set entry
-    setTimeout =>
-      @saveState()
-    , @interval
+        entry[field] = $(field).val() for field in @fields
+        entry['#currentImages'] = $('#currentImages').html()
+        @entryCookie.set entry
+      setTimeout =>
+        @saveState()
+      , @interval

   retrieveState: ->
-    entry = @entryCookie.get()
-    currentEntryType = $('#entryMode').data 'id'
-    if entry?
-      if entry['type'] is currentEntryType  # make sure our cookie entry type matches new or edit like the current form
-        currentEntryId = parseInt($('#entryId').data('id')) if currentEntryType is 'edit'
-
-      if currentEntryType is 'edit' and currentEntryId isnt entry['id']
-        @stateRetrieved = false
-      else
-        @populateForm(entry)
-        @stateRetrieved = true
-        @clearState()
-
-      log "entry[type] #{entry['type']} entry[id]: #{entry['id']} currentEntryType: #{currentEntryType} currentEntryId: #{currentEntryId}"
-
-
-  # populate form with saved state so they can see it, then confirm they want to use it
-  populateForm: (entry) ->
-    $(field).val entry[field] for field in @fields
-    $('#currentImages').html entry['#currentImages']
-    @clearForm() unless confirm 'you have an unsaved entry.\n\nwould you like to use it?' # reset form unless confirmation
+    entry = @entryCookie.get()
+
+    if entry? and @currentEntryType is 'new'
+      $(field).val entry[field] for field in @fields
+      $('#currentImages').html entry['#currentImages']
+      @stateRetrieved = true
+      unless confirm 'you have an unsaved entry.\n\n would you like to use it?'
+        @clearForm()

+    @clearState()
+
   clearForm: ->
     $(field).val '' for field in @fields
     $('#currentImages').html ''
-
+
   clearState: ->
     @entryCookie.clear()


Invoke XHR handshake immediately

Invoke XHR handshake immediately

--- lib/index.coffee
+++ lib/index.coffee
@@ -48,9 +48,10 @@
         process.nextTick =>
           @emit 'ready'
           @off 'ready'
+
     @setOutboundTimer()  if @batchRequests
-    unless @useWebsockets
-      @once 'ready', => process.nextTick @bound 'xhrHandshake'
+
+    @xhrHandshake()  unless @useWebsockets

     @api = @createRemoteApiShims @apiDescriptor


pre-populate name of sch saving if sch already has a name (has been saved before)

--- public/scripts/backbone-schedule/views/modals/save.coffee
+++ public/scripts/backbone-schedule/views/modals/save.coffee
@@ -25,6 +25,9 @@

     render: ->
       @$el.html @saveTemplate()
+      name = Shark.schedule.get('name')
+      if name != ''
+        @$el.find('[name=savename]').val(name)

   SaveView
 )


fix bug in minifying non-js files

fix bug in minifying non-js files

--- lib/minjs.coffee
+++ lib/minjs.coffee
@@ -7,6 +7,7 @@

 	if !path or path.match(/\.min\.(js|css)$/gi) or !path.match(/\.(js|css)$/gi)
 		console.log("File not minifiable.");
+		return

 	if path.indexOf('.js') == path.length - 3 and path.indexOf('.min.js') < 0
 		result = minjs.minify(editor.getText(), {fromString: true});


SettingsMenu: Fix undefined method call error for mark/unmark troll user

--- workers/social/lib/social/models/account.coffee
+++ workers/social/lib/social/models/account.coffee
@@ -971,7 +971,7 @@
       callback new KodingError 'Access denied'

   markUserAsExemptInSocialAPI: (client, exempt, callback)->
-    {markAsTroll, unmarkAsTroll} = require './socialapi/helper'
+    {markAsTroll, unmarkAsTroll} = require './socialapi/requests'
     @createSocialApiId (err, accountId)->
       return callback err if err
       return callback {message: "account id is not set"} unless accountId


Social: add canEditPost back to the account.coffee

--- workers/social/lib/social/models/account.coffee
+++ workers/social/lib/social/models/account.coffee
@@ -421,6 +421,8 @@
     @notifyOriginWhen 'PrivateMessageSent', 'FollowHappened'
     @notifyGroupWhen 'FollowHappened'

+  canEditPost: permit 'edit posts'
+
   createSocialApiId:(callback)->
     return callback null, @socialApiId  if @socialApiId
     {createAccount} = require './socialapi/requests'


TrollMode: Set isExempt in mongo after successful update attempt of socialapi

--- workers/social/lib/social/models/account.coffee
+++ workers/social/lib/social/models/account.coffee
@@ -1,5 +1,6 @@
 jraphical   = require 'jraphical'
 KodingError = require '../error'
+ApiError    = require './socialapi/error'

 likeableActivities = [
   'JNewStatusUpdate'
@@ -429,7 +430,7 @@
     return callback null, @socialApiId  if @socialApiId
     {createAccount} = require './socialapi/requests'
     createAccount {id: @getId(), nickname: @profile.nickname}, (err, account)=>
-      return callback err if err
+      return callback new ApiError err  if err
       return callback {message: "Account is not set, malformed response from social api"} unless account?.id
       @update $set: socialApiId: account.id, (err)->
         # check for error
@@ -955,7 +956,11 @@

   markUserAsExempt: secure (client, exempt, callback)->
     {delegate} = client.connection
-    if delegate.can 'flag', this
+    return callback new KodingError 'Access denied'  unless delegate.can 'flag', this
+
+    # mark user as troll in social api
+    @markUserAsExemptInSocialAPI client, exempt, (err, data)->
+      return callback new ApiError err  if err
       @update $set: {isExempt: exempt}, callback
       # this is for backwards comp. will remove later...
       if exempt
@@ -963,17 +968,10 @@
       else
         @update {$pullAll: globalFlags: ["exempt"]}, ()->

-      # mark user as troll in social api
-      @markUserAsExemptInSocialAPI client, exempt, (err, data)->
-        console.error err if err
-
-    else
-      callback new KodingError 'Access denied'
-
   markUserAsExemptInSocialAPI: (client, exempt, callback)->
     {markAsTroll, unmarkAsTroll} = require './socialapi/requests'
     @createSocialApiId (err, accountId)->
-      return callback err if err
+      return callback err  if err
       return callback {message: "account id is not set"} unless accountId

       if exempt


payment: display subscription/plan description in `VMPlanView`

--- client/Environments/views/vms/vmplanview.coffee
+++ client/Environments/views/vms/vmplanview.coffee
@@ -9,6 +9,6 @@
     total = KD.utils.formatMoney total / 100
     """
     {h4{#(title) or #(plan.title)}}
-
ctor ligula, nec semper tortor metus ut dolor. Nulla varius vitae leo et ultrices. Cras sagittis vulputate imperdiet. Pellentesque ut varius

+    {p{#(description) or #(plan.description) or ""}}
     #{total}
     """


resurrection.activity: emit input widget submit event with error and activity arguments

--- client/Social/Activity/views/inputwidget.coffee
+++ client/Social/Activity/views/inputwidget.coffee
@@ -71,7 +71,7 @@
         fn data, (err, activity) =>
           @reset yes
           @embedBox.resetEmbedAndHide()
-          @emit "Submit"
+          @emit "Submit", err, activity
           callback? err, activity

           KD.mixpanel "Status update create, success", {length:activity?.body?.length}


Activity: removed populate logs

Activity: removed populate logs

--- client/Social/Activity/views/messagepane.coffee
+++ client/Social/Activity/views/messagepane.coffee
@@ -282,9 +282,7 @@

       return KD.showError err  if err

-      console.time('populate')
       @listController.hideLazyLoader()
-      console.timeEnd('populate')
       items.forEach (item) => KD.utils.defer => @appendMessage item

       KD.utils.defer @bound 'focus'


activity/flux: add loadComments action

activity/flux: add loadComments action

--- client/activity/lib/flux/actions/message.coffee
+++ client/activity/lib/flux/actions/message.coffee
@@ -186,6 +186,31 @@
           dispatch EDIT_MESSAGE_SUCCESS, {
             message, messageId
           }
+
+
+###*
+ * Action to load comments with given options.
+ *
+ * @param {string} messageId
+ * @param {string} from
+ * @param {number} limit
+###
+loadComments = (messageId, from, limit) ->
+
+  { socialapi } = kd.singletons
+  { LOAD_COMMENTS_BEGIN
+    LOAD_COMMENTS_FAIL
+    LOAD_COMMENT_SUCCESS } = actionTypes
+
+  dispatch LOAD_COMMENTS_BEGIN, { messageId, from, limit }
+
+  socialapi.message.listReplies {messageId, from, limit}, (err, comments) ->
+    if err
+      dispatch LOAD_COMMENTS_FAIL, { err, messageId, from, limit }
+      return
+
+    comments.forEach (comment) ->
+      dispatch LOAD_COMMENT_SUCCESS, { messageId, comment }

 module.exports = {
@@ -195,5 +220,6 @@
   unlikeMessage
   removeMessage
   editMessage
+  loadComments
 }


StatusBar: Added enter/exit fullscreen button.

--- client/ide/views/statusbar/statusbarmenu.coffee
+++ client/ide/views/statusbar/statusbarmenu.coffee
@@ -30,8 +30,13 @@
     items.separator   = type: 'separator'

   addDefaultMenuItems: (items, appManager) ->
-    items.Shortcuts   = callback: -> appManager.tell 'IDE', 'showShortcutsView'
-    items.Contribute  = callback: -> KD.utils.createExternalLink 'https://github.com/koding/IDE'
-    items.Quit        = callback: ->
+    mainView = KD.getSingleton 'mainView'
+    fsText   = if mainView.isFullscreen() then 'Exit' else 'Enter'
+
+    items.Shortcuts  = callback: -> appManager.tell 'IDE', 'showShortcutsView'
+    items["#{fsText} Fullscreen"] = callback: -> mainView.toggleFullscreen()
+    items.Contribute = callback: -> KD.utils.createExternalLink 'https://github.com/koding/IDE'
+    items.Quit       = callback: ->
       appManager.quitByName 'IDE'
+      mainView.toggleFullscreen()  if mainView.isFullscreen()
       KD.getSingleton('router').handleRoute '/Activity'


follow linkgroup bug fixed

follow linkgroup bug fixed

--- client/landing/app/CommonViews/LinkViews.coffee
+++ client/landing/app/CommonViews/LinkViews.coffee
@@ -149,15 +149,15 @@
       click       : =>
         new FollowedModalView {group}, @getData()

-    sep = ''
+    sep = ' '
     if participants[0] instanceof bongo.api.JAccount
       sep = ', '
     switch totalCount
       when 0 then ""
       when 1 then "{{> @participant0}}"
       when 2 then "{{> @participant0}} and {{> @participant1}}"
-      when 3 then "{{> @participant0}}#{sep}{{> @participant1}}#{sep}{{> @participant2}}"
-      when 4 then "{{> @participant0}}#{sep}{{> @participant1}}#{sep}{{> @participant2}}#{sep}{{> @participant3}"
+      when 3 then "{{> @participant0}}#{sep}{{> @participant1}} and {{> @participant2}}"
+      when 4 then "{{> @participant0}}#{sep}{{> @participant1}}#{sep}{{> @participant2}} and {{> @participant3}}"
       else "{{> @participant0}}#{sep}{{> @participant1}}#{sep}{{> @participant2}} and {{> @more}}"

   render:->


Add UX/UI Designer role

Add UX/UI Designer role

--- client/src/project/services/projectUtils.coffee
+++ client/src/project/services/projectUtils.coffee
@@ -19,6 +19,9 @@
   ,
     label: 'Commercial'
     value: 'com'
+  ,
+    label: 'UX/UI Designer'
+    value: 'UX'
   ]
   getDailyRecipient: ->
     [


use dt-stream in tests

use dt-stream in tests

--- src/test/template.coffee
+++ src/test/template.coffee
@@ -1,11 +1,12 @@
 path = require 'path'
 { readFile } = require 'fs'
 { Template } = require '../dynamictemplate'
+streamify = require 'dt-stream'

 module.exports =

     simple: (æ) ->
-        xml = new Template ->
+        xml = streamify new Template ->
             @$tag('test')
         xml.on 'end', æ.done
         xml.on 'data', (tag) -> æ.equal "<test/>", tag
@@ -13,7 +14,7 @@

     html5: (æ) ->
         content = ["a", "b", "c"]
-        xml = new Template schema:5, doctype:on, ->
+        xml = streamify new Template schema:5, doctype:on, ->
             @$html ->
                 @body ->
                     file = path.join(__dirname,"..","filename")
@@ -26,7 +27,7 @@

         xml.ready æ.done
-        xml.on 'data', (tag) -> æ.equal results.shift(), tag
+        xml.stream.on 'data', (tag) -> æ.equal results.shift(), tag
         results = [
             '<!DOCTYPE html>'
             '<html>'
@@ -50,14 +51,14 @@
         ]

     pretty: (æ) ->
-        xml = new Template schema:5, pretty:" ", doctype:on, ->
+        xml = streamify new Template schema:5, pretty:" ", doctype:on, ->
             @$html ->
                 @$head ->
                     @$title "holla"
                 @$body ->
                     @$p "hello world"
         xml.ready æ.done
-        xml.on 'data', (tag) -> æ.equal results.shift(), tag
+        xml.stream.on 'data', (tag) -> æ.equal results.shift(), tag
         results = [
             '<!DOCTYPE html>\n'
             '<html>\n'
@@ -91,12 +92,12 @@
                 @end()

-        xml = template data =
+        xml = streamify template data =
             title:'test'
             content:'..'

         xml.ready æ.done
-        xml.on 'data', (tag) -> æ.equal results.shift(), tag
+        xml.stream.on 'data', (tag) -> æ.equal results.shift(), tag
         results = [
             '<html>'
             '<head>'


Not needed

Not needed

--- apps/apply/client/views/step2.coffee
+++ apps/apply/client/views/step2.coffee
@@ -4,7 +4,6 @@
 Form = require '../../../../components/mixins/form.coffee'
 template = -> require('../templates/step2.jade') arguments...
 formTemplates =
-  initial: -> require('../templates/forms/step2/partner.jade') arguments... # This should be removed
   gallery: -> require('../templates/forms/step2/partner.jade') arguments...
   institution: -> require('../templates/forms/step2/institution.jade') arguments...
   fair: -> require('../templates/forms/step2/partner.jade') arguments...


Track clicking on another artwork by the same artist explicitly

--- apps/artwork/client/analytics.coffee
+++ apps/artwork/client/analytics.coffee
@@ -29,3 +29,6 @@

   $document.on 'click', '#layered-search-results .artwork-item', ->
     trackSnowplow 'genome', 'click_related_artwork', artwork.get('_id'), 'artwork'
+
+  $document.on 'click', '#artist-artworks-section .artwork-item', ->
+    trackSnowplow 'artwork', 'click_artwork_by_artist', artwork.get('_id'), 'artwork'


remove un-used file

remove un-used file

--- src/components/questions/exercise-details.cjsx
+++ src/components/questions/exercise-details.cjsx
@@ -1,161 +0,0 @@
-React     = require 'react'
-BS        = require 'react-bootstrap'
-keymaster = require 'keymaster'
-
-{ExerciseStore}   = require '../../flux/exercise'
-{ExercisePreview} = require 'openstax-react-components'
-ExerciseHelpers   = require '../../helpers/exercise'
-NoExercisesFound  = require './no-exercises-found'
-Icon              = require '../icon'
-ScrollTo          = require '../scroll-to-mixin'
-
-KEYBINDING_SCOPE  = 'exercise-details'
-
-ExerciseDetails = React.createClass
-
-  propTypes:
-    selectedExercise: React.PropTypes.object.isRequired
-    selectedSection:  React.PropTypes.string
-    exercises: React.PropTypes.object.isRequired
-    onSectionChange: React.PropTypes.func.isRequired
-    onExerciseToggle: React.PropTypes.func.isRequired
-    onShowCardViewClick: React.PropTypes.func.isRequired
-    isExerciseIdSelected: React.PropTypes.func.isRequired
-
-  mixins: [ScrollTo]
-  scrollingTargetDOM: -> document
-  getScrollTopOffset: -> 40
-
-  getInitialState: -> {}
-
-  componentDidMount:   ->
-    @scrollToSelector('.questions-controls', {immediate: true})
-
-  componentWillUnmount: ->
-    keymaster.deleteScope(KEYBINDING_SCOPE)
-
-  componentWillMount: ->
-    keymaster('left',  KEYBINDING_SCOPE, @keyOnPrev)
-    keymaster('right', KEYBINDING_SCOPE, @keyOnNext)
-    keymaster.setScope(KEYBINDING_SCOPE)
-
-    {selectedExercise} = @props
-    exercises = @flattenExercises(@props)
-    currentIndex = currentSection = 0
-    for exercise, index in exercises
-      if selectedExercise?.id is exercise.id
-        currentIndex = index
-        currentSection = ExerciseStore.getChapterSectionOfExercise(exercise)
-        break
-    @setState({exercises, currentIndex, currentSection})
-
-  componentWillReceiveProps: (nextProps) ->
-    exercises = @flattenExercises(nextProps)
-    {selectedSection} = nextProps
-    nextState = {exercises}
-    if selectedSection and selectedSection isnt @state.currentSection
-      for exercise, index in exercises
-        section = ExerciseStore.getChapterSectionOfExercise(exercise)
-        if selectedSection is section
-          nextState.currentSection = selectedSection
-          nextState.currentIndex = index
-          break
-    @setState(nextState)
-
-  flattenExercises: (props) ->
-    groups = props.exercises.grouped
-    exercises = []
-    for section in _.keys(groups).sort()
-      for exercise in groups[section]
-        exercises.push exercise
-    return exercises
-
-  toggleNavHighlight: (type) ->
-    nav = @getDOMNode().querySelector(".page-navigation.#{type}")
-    nav.classList.add('active')
-    _.delay ->
-      nav.classList.remove('active')
-    , 300
-
-  keyOnPrev: ->
-    return unless @getValidMovements().prev
-    @toggleNavHighlight('prev')
-    @onPrev()
-  keyOnNext: ->
-    return unless @getValidMovements().next
-    @toggleNavHighlight('next')
-    @onNext()
-  onPrev: -> @moveTo(@state.currentIndex - 1)
-  onNext: -> @moveTo(@state.currentIndex + 1)
-
-  moveTo: (index) ->
-    exercise = @state.exercises[index]
-    section = ExerciseStore.getChapterSectionOfExercise(exercise)
-    unless @state.currentSection is section
-      # defer is needed to allow setState to complete before callback is fired
-      # otherwise component recieves props with the new section and doesn't know it's already on it
-      # causing it to jump to the first exercise in section
-      _.defer => @props.onSectionChange(section)
-    @setState({currentIndex: index, currentSection: section})
-
-  getValidMovements: ->
-    prev: @state.currentIndex isnt 0
-    next: @state.currentIndex isnt @state.exercises.length - 1
-
-  toggleFeedback: ->
-    @setState(displayFeedback: not @state.displayFeedback)
-
-  reportError: (ev, exercise) ->
-    window.open(ExerciseHelpers.troubleUrl(exerciseId: exercise.content.uid), '_blank')
-
-  render: ->
-    exercise = @state.exercises[@state.currentIndex] or _.first(@state.exercises)
-    unless exercise
-      return <NoExercisesFound />
-
-    moves = @getValidMovements()
-
-    actions = ExerciseHelpers.buildPreviewActions(exercise, @props.onExerciseToggle)
-    if @state.displayFeedback
-      actions['feedback-off'] =
-        message: 'Hide Feedback'
-        handler: @toggleFeedback
-    else
-      actions['feedback-on'] =
-        message: 'Preview Feedback'
-        handler: @toggleFeedback
-    actions['report-error'] =
-        message: 'Report an error'
-        handler: @reportError
-
-

-
-

-
-          <Icon type="th-large" /> Back to Card View
-
-

-
-

-
-        {

-

-
 if moves.prev}
-
-        <ExercisePreview
-          className='exercise-card'
-          isVerticallyTruncated={false}
-          isSelected={@props.isExerciseIdSelected(exercise.id)}
-          displayFeedback={@state.displayFeedback}
-          exercise={exercise}
-          actionsOnSide={true}
-          overlayActions={actions}
-        />
-

-      {

-

-
 if moves.next}
-
-

-
-module.exports = ExerciseDetails


pass client to server

pass client to server

--- src/controllers/authenticate-controller.coffee
+++ src/controllers/authenticate-controller.coffee
@@ -1,11 +1,13 @@
+redis             = require 'redis'
 Authenticator     = require '../models/authenticator'
 MeshbluAuthParser = require '../helpers/meshblu-auth-parser'

 class AuthenticateController
   constructor: (options={}, dependencies={}) ->
     {@authenticator} = dependencies
-
+
     @authenticator ?= new Authenticator
+      client: redis.createClient process.env.REDIS_URI
     @authParser = new MeshbluAuthParser

   authenticate: (request, response) =>


style: Use multi-line Coffee Script strings

--- src/expand-uri-template-with-parameters.coffee
+++ src/expand-uri-template-with-parameters.coffee
@@ -11,9 +11,10 @@
   try
     parsed = ut.parse uriTemplate
   catch e
-    text = "\n Failed to parse URI template: #{uriTemplate}"
-    text += "\n Error: #{e}"
-    result.errors.push text
+    result.errors.push("""\
+      Failed to parse URI template: #{uriTemplate}
+      Error: #{e}\
+    """)
     return result

   # get parameters from expression object
@@ -30,10 +31,10 @@
     for uriParameter in uriParameters
       if Object.keys(parameters).indexOf(uriParameter) is -1
         ambiguous = true
-        text = "\nAmbiguous URI parameter in template: #{uriTemplate} " + \
-               "\nParameter not defined in API description document: " + \
-               "'" + uriParameter + "'"
-        result.warnings.push text
+        result.warnings.push("""\
+          Ambiguous URI parameter in template: #{uriTemplate}
+          Parameter not defined in API description document: #{uriParameter}\
+        """)

     unless ambiguous
       toExpand = {}


trying to make grid responsive

trying to make grid responsive

--- src/scripts/directives/d3DotGrid.coffee
+++ src/scripts/directives/d3DotGrid.coffee
@@ -2,118 +2,122 @@
 [
 	'$timeout'
 	($timeout) ->
-		restrict: 'A'
-		replace: true
-		template: ''
+		data = {}
+		return {
+			restrict: 'A'
+			replace: true
+			template: ''

-		link: (scope, element, attrs) ->
-			console.log('dot-grid')
+			link: (scope, element, attrs) ->
+				console.log('dot-grid')

-			scope.$on 'draw', (event, container, width, height) ->
-				#event.stopPropagation()
-				console.log "drawing"
-				scope.setSpacing(container, width, height)
-				#scope.data = [{x:0,y:0},{x:20,y:20},{x:40,y:40}]
-				draw()
+				scope.$on 'draw', (event, container, width, height) ->
+					#event.stopPropagation()
+					console.log "drawing"
+					scope.setSpacing(container, width, height)
+					#scope.data = [{x:0,y:0},{x:20,y:20},{x:40,y:40}]
+					draw()

-			scope.$on 'resize', (event, container, width, height) ->
-				console.log "drawing"
-				scope.setSpacing(container, width, height)
-				draw()
+				scope.$on 'resize', (event, container, width, height) ->
+					console.log "redrawing"
+					scope.setSpacing(container, width, height)
+					draw()

-			draw = ->
+				draw = ->

-				columns = scope.container.selectAll("g")
-				.data(scope.data)
-				.enter().append("g")
-
-				circles = columns.selectAll("circle")
-				.data((d,i) -> d)
-				.attr("cx", (d, i, j)->j*scope._hspace)
-				.attr("cy", (d, i, j)->i*scope._vspace)
+					data = ({
+						x:col*scope._hspace
+						y:row*scope._vspace
+					} for row in [0..scope.rows-1] for col in [0..scope.cols-1])

-				circles.enter().append("circle")
-				.attr("r", scope.radius)
-				.attr("class", "origin")
-				.attr("cx", (d, i, j)->j*scope._hspace)
-				.attr("cy", (d, i, j)->i*scope._vspace)
+					columns = scope.container.selectAll("g")
+					.data(data)
+					.enter().append("g")
+
+					circles = columns.selectAll("circle")
+					.data((d) -> d)

-				circles.exit().remove()
-
-			scope.setSpacing = (container, width, height) ->
-				@container = container
-				@width = width
-				@height = height
-				@rows = @down
-				@cols = @along
-				@_vspace = @vspace
-				@_hspace = @hspace
-
-				if @vspace == "auto"
-					@_vspace = @height / (@rows - 1)
-					@rows = Math.floor(@height/@_vspace + 1)
-
-				if @hspace == "auto"
-					@_hspace = @width / (@cols - 1)
-					@cols = Math.floor(@width/@_hspace + 1)
-
-				if !isNaN(@minspace)
-					@_vspace = Math.max(@_vspace, @minspace)
-					@_hspace = Math.max(@_hspace, @minspace)
-
-				if @square
-					space = Math.min(@_vspace, @_hspace)
-					@_vspace = @_hspace = space
-
-				scope.data = ({x:col*@_hspace, y:row*@_vspace} for row in [0..@rows-1] for col in [0..@cols-1] )
-				console.log @_hspace, @width, @cols
-				console.log scope.data
-
-			###
-			draw = ->
-				[0..scope.down-1].forEach (row) -> drawRow(row)
-
-			drawRow = (row) ->
-				[0..scope.along-1].forEach (col) ->
-					scope.container.append("circle")
+					circles.enter().append("circle")
 					.attr("r", scope.radius)
 					.attr("class", "origin")
-					.attr("transform", "translate(" + col*scope.hspace + "," + row*scope.vspace + ")")
+					.attr("cx", (d)->d.x)
+					.attr("cy", (d)->d.y)

-			scope.$on 'resize', (event, container, width, height) ->
-				console.log "drawing"
-				scope.setSpacing(container, width, height)
-				resize()
+					circles.exit().remove()
+
+				scope.setSpacing = (container, width, height) ->
+					@container = container
+					@width = width
+					@height = height
+					@rows = @down
+					@cols = @along
+					@_vspace = @vspace
+					@_hspace = @hspace
+
+					if @vspace == "auto"
+						@_vspace = @height / (@rows - 1)
+						@rows = Math.floor(@height/@_vspace + 1)
+
+					if @hspace == "auto"
+						@_hspace = @width / (@cols - 1)
+						@cols = Math.floor(@width/@_hspace + 1)
+
+					if !isNaN(@minspace)
+						@_vspace = Math.max(@_vspace, @minspace)
+						@_hspace = Math.max(@_hspace, @minspace)
+
+					if @square
+						space = Math.min(@_vspace, @_hspace)
+						@_vspace = @_hspace = space
+
+					#console.log @_hspace, @width, @cols
+					#console.log scope.data
+
+				###
+				draw = ->
+					[0..scope.down-1].forEach (row) -> drawRow(row)
+
+				drawRow = (row) ->
+					[0..scope.along-1].forEach (col) ->
+						scope.container.append("circle")
+						.attr("r", scope.radius)
+						.attr("class", "origin")
+						.attr("transform", "translate(" + col*scope.hspace + "," + row*scope.vspace + ")")
+
+				scope.$on 'resize', (event, container, width, height) ->
+					console.log "drawing"
+					scope.setSpacing(container, width, height)
+					resize()

-			resize = ->
-				[0..scope.down-1].forEach (row) -> resizeRow(row)
+				resize = ->
+					[0..scope.down-1].forEach (row) -> resizeRow(row)

-			resizeRow = ->
-				[0..scope.along-1].forEach (col) ->
-					circle
-			###
+				resizeRow = ->
+					[0..scope.along-1].forEach (col) ->
+						circle
+				###

-			attrs.$observe 'along', (val) ->
-				scope.along = if val? then ~~val else 10
-				console.log "along = ", scope.along
+				attrs.$observe 'along', (val) ->
+					scope.along = if val? then ~~val else 10
+					console.log "along = ", scope.along

-			attrs.$observe 'down', (val) ->
-				scope.down = if val? then ~~val else 10
-				console.log "down = ", scope.down
+				attrs.$observe 'down', (val) ->
+					scope.down = if val? then ~~val else 10
+					console.log "down = ", scope.down

-			attrs.$observe 'hspace', (val) ->
-				scope.hspace = if val=="auto" then "auto" else if val? then ~~val else 10
-				console.log "hspace = ", scope.hspace
+				attrs.$observe 'hspace', (val) ->
+					scope.hspace = if val=="auto" then "auto" else if val? then ~~val else 10
+					console.log "hspace = ", scope.hspace

-			attrs.$observe 'vspace', (val) ->
-				scope.vspace = if val=="auto" then "auto" else if val? then ~~val else 10
-				console.log "vspace = ", scope.vspace
+				attrs.$observe 'vspace', (val) ->
+					scope.vspace = if val=="auto" then "auto" else if val? then ~~val else 10
+					console.log "vspace = ", scope.vspace

-			attrs.$observe 'radius', (r) ->
-				scope.radius = if r? then ~~r else 3
+				attrs.$observe 'radius', (r) ->
+					scope.radius = if r? then ~~r else 3

-			attrs.$observe 'square', (val) ->
-				scope.square = val? and val != "false"
-
+				attrs.$observe 'square', (val) ->
+					scope.square = val? and val != "false"
+		}
 ]


Adjusting CAH to use respond instead of hear

--- src/scripts/hubotagainsthumanity.coffee
+++ src/scripts/hubotagainsthumanity.coffee
@@ -8,9 +8,9 @@
 #   None
 #
 # Commands:
-#   q card - Returns a question
-#   card me - Displays an answer
-#   card 2 - Displays two answers for questions with two blanks
+#   hubot q card - Returns a question
+#   hubot card me - Displays an answer
+#   hubot card 2 - Displays two answers for questions with two blanks
 #
 # Author:
 #   Jonny Campbell (@jonnycampbell)
@@ -794,9 +794,9 @@
 ]

 module.exports = (robot) ->
-  robot.hear /card(?: me)?(?: )(\d+)?/i, (msg) ->
+  robot.respond /card(?: me)?(?: )(\d+)?/i, (msg) ->
     count = if msg.match[1]? then parseInt(msg.match[1], 10) else 1
     msg.send msg.random answers for i in [1..count]

-  robot.hear /q(?:uestion)? card/i, (msg) ->
+  robot.respond /q(?:uestion)? card/i, (msg) ->
     msg.send msg.random questions


removing commented code

removing commented code

--- src/scripts/models/contents/node.coffee
+++ src/scripts/models/contents/node.coffee
@@ -80,7 +80,6 @@
             $.ajax
               url: "#{ARCHIVE}/extras/#{@getVersionedId()}"
               dataType: 'json'
-              #async: false
             .done (response) =>
               @set('downloads', response.downloads)
               @set('isLatest', response.isLatest)


fix widgets settings update method

fix widgets settings update method

--- src/services/widgets/widgets.svc.coffee
+++ src/services/widgets/widgets.svc.coffee
@@ -88,7 +88,11 @@
         return _self.updateAllSimilarWidgets(ImpacDashboardsSvc.getCurrentDashboard(), changedGlobalSetting)

       widget.isLoading = true if needContentReload
-      meta = _.reduce(_.map(widget.settings, (set) -> set.toMetadata() ), (result, setMeta) -> angular.merge(result, setMeta))
+      meta = _.reduce(
+        _.map( widget.settings, (set) -> set.toMetadata() ),
+        (result={}, setMeta) ->
+          angular.merge(result, setMeta)
+      )

       _self.update(widget, { metadata: meta }).then(
         (updatedWidget) ->


fix demo_data mode

fix demo_data mode

--- src/services/widgets/widgets.svc.coffee
+++ src/services/widgets/widgets.svc.coffee
@@ -161,16 +161,14 @@
       widget.format() if angular.isDefined(widget.format)

     paramsForWidget = (widget, opts = {}) ->
-      refreshCache = !!opts.refreshCache
-      demo = !!opts.demo
       metadata = angular.copy(widget.metadata)
       metadata.utc_offset ||= moment().utcOffset()

-      demoData = ImpacTheming.get().dhbConfig.designerMode.enabled || demo
       params =
         metadata: metadata
-        demo_data: demoData
-      params.refresh_cache = true if refreshCache
+        demo_data: ImpacTheming.get().dhbConfig.designerMode.enabled || !!opts.demo
+
+      params.refresh_cache = true if !!opts.refreshCache
       params

     getConfig = ->
@@ -217,12 +215,12 @@
                   $log.error('Impac! - WidgetsSvc: Cannot retrieve demo data for widget:', widget)
                   $q.resolve(widget)
                 else
-                  _self.show(widget, { refreshCache: params.refreshCache, demo: true })
+                  _self.show(widget, { refreshCache: params.refresh_cache, demo: true })

               else
                 # Push new content to widget, and initialize it
                 name = success.data.name
-                angular.extend widget, { content: content, originalName: name, demoData: params.demoData }
+                angular.extend widget, { content: content, originalName: name, demoData: params.demo_data }
                 initWidget(widget)
                 $q.resolve(widget)


Improved error when called without new

Improved error when called without new

Fixes #39

--- lib/pages.coffee
+++ lib/pages.coffee
@@ -53,6 +53,8 @@
         continue  unless i?
         i.stop()
   constructor: (collection, settings) ->
+    if !@setCollection
+      throw "Please use the `new` constructor style " + (new Error).stack.split("\n")[2].trim()
     @setCollection collection
     @setDefaults()
     @applySettings settings
@@ -397,4 +399,4 @@
       @lastPage = page
     @checkQueue()

-Meteor.Pagination = Pages+Meteor.Pagination = Pages


fix some things

fix some things

--- src/server/middleware/request_info.coffee
+++ src/server/middleware/request_info.coffee
@@ -7,14 +7,14 @@
   return next() unless req.session.user_id
   Cache.get "request:#{req.session.user_id}", (cache_request) ->
     if cache_request
-      res.locals.REQUEST = if cache_request[0] then cache_request[0] else cache_request
+      res.locals.REQUEST = cache_request
       console.log "hay cache, seteo res locals", res.locals.REQUEST
       next()
     else
       Request.connect (client) ->
         Request.get_request_by_user_id client, req.session.user_id, (request) ->
           if request
-            request_data = request[0]
+            request_data = request
             Cache.set "request:#{req.session.user_id}", request_data, (cache_request) ->
               res.locals.REQUEST = request_data
               console.log "no hay cache, seteo res locals", res.locals.REQUEST


scope outcome and hide modal on close

scope outcome and hide modal on close

--- components/email/client/editorial_signup.coffee
+++ components/email/client/editorial_signup.coffee
@@ -86,24 +86,24 @@

   showEditorialCTA: ->
     @test = splitTest('editorial_cta_banner')
-    outcome = @test.outcome()
-    if outcome is 'banner'
+    @outcome = @test.outcome()
+    if @outcome is 'banner'
       @$('#main-layout-container').css('margin-top', '53px').before editorialCTABannerTemplate
-        mode: outcome
+        mode: @outcome
         email: sd.CURRENT_USER?.email or ''
         image: sd.EDITORIAL_CTA_BANNER_IMG
       setTimeout((=> @$('.articles-es-cta--banner').height(315).attr('data-state', 'in') ), 1000)
-    else if outcome is 'modal'
+    else if @outcome is 'modal'
       @$('#modal-container').append editorialCTABannerTemplate
-        mode: outcome
+        mode: @outcome
         email: sd.CURRENT_USER?.email or ''
         image: sd.EDITORIAL_CTA_BANNER_IMG
       @$('#articles-show').waypoint (direction) =>
         if direction is 'down'
           setTimeout((=> @$('.articles-es-cta--banner').attr('data-state', 'in').css('opacity', 1)), 2000)
-    else if outcome is 'old_modal'
+    else if @outcome is 'old_modal'
       mediator.on 'auction-reminders:none', @setupCTAWaypoints
-    analyticsHooks.trigger('view:editorial-signup', type: outcome )
+    analyticsHooks.trigger('view:editorial-signup', type: @outcome )

   hideEditorialCTA: (e) ->
     e?.preventDefault()
@@ -111,6 +111,7 @@
     @onDismiss()
     cta.height(0) if cta.hasClass('banner')
     cta.css('opacity', 0)
+    cta.hide() if cta.hasClass('modal')
     setTimeout((=> cta.attr('data-state', 'out')), 2000)

   setupCTAWaypoints: =>
@@ -169,4 +170,4 @@

   onDismiss: ->
     @ctaBarView.logDimissal()
-    analyticsHooks.trigger('dismiss:editorial-signup')
+    analyticsHooks.trigger('dismiss:editorial-signup', type: @outcome)


tests(baseService): add test for defaulting limit to 20 in process function

--- src/spec/client/services/base.spec.coffee
+++ src/spec/client/services/base.spec.coffee
@@ -389,6 +389,24 @@
           expect(=> @service.process()).toThrow new Error 'Please provide a function to process the elements'
           expect(@restMock.GET).not.toHaveBeenCalled()

+        it 'should set the limit to 20 if it is 0', ->
+          spyOn(@restMock, 'GET')
+
+          @service._params.query.perPage = 0
+
+          fn = (payload) ->
+            Promise.resolve payload.body.results[0]
+
+          @service.process(fn)
+          .then () ->
+
+            actual = @service._params.query.limit
+            expected = 20
+
+            expect(actual).toEqual(expected)
+            done()
+          .catch (error) -> done(_.prettify(error))
+
         it 'should not accumulate results if explicitly set', (done) ->
           offset = -20
           count = 1


Avoid emitting a modified status event when batch is true

--- lib/table.coffee
+++ lib/table.coffee
@@ -261,10 +261,10 @@
     else
       @rows.splice index, 0, row

-    @emitModifiedStatusChange()
     @emitter.emit 'did-add-row', {row, index}

     unless batch
+      @emitModifiedStatusChange()
       @emitter.emit 'did-change-rows', {
         oldRange: {start: index, end: index}
         newRange: {start: index, end: index+1}
@@ -285,6 +285,7 @@

     createdRows = rows.map (row,i) => @addRowAt(index+i, row, true)

+    @emitModifiedStatusChange()
     @emitter.emit 'did-change-rows', {
       oldRange: {start: index, end: index}
       newRange: {start: index, end: index+rows.length}
@@ -310,9 +311,9 @@
     row = @rows[index]
     @rows.splice(index, 1)

-    @emitModifiedStatusChange()
     @emitter.emit 'did-remove-row', {row, index}
     unless batch
+      @emitModifiedStatusChange()
       @emitter.emit 'did-change-rows', {
         oldRange: {start: index, end: index+1}
         newRange: {start: index, end: index}


Remove irrelevant attribute

Remove irrelevant attribute

--- resources/assets/coffee/forum-posts-seek.coffee
+++ resources/assets/coffee/forum-posts-seek.coffee
@@ -80,5 +80,4 @@
     @postPosition = Math.max(postPosition, 1)

     @tooltip[0].style.left = "#{x}px"
-    @tooltip[0].style.bottom = seekbarPos.top
     @tooltipNumber[0].textContent = @postPosition


reload fix

reload fix

--- build/grunt.coffee
+++ build/grunt.coffee
@@ -101,7 +101,7 @@
   grunt.loadNpmTasks "grunt-contrib-stylus"

   ## default
-  grunt.registerTask "default", "simplemocha copy jaded coffee stylus start watch"
+  grunt.registerTask "default", "reload simplemocha copy jaded coffee stylus start watch"

   ## start
   grunt.registerTask "start", "start up servers", ->


Fix browser prefix detection

Fix browser prefix detection

--- lib/utils.coffee
+++ lib/utils.coffee
@@ -26,7 +26,7 @@
     -> id += 1

   hasChromePrefix: (url) ->
-    chromePrefixes = [ "about", "view-source", "chrome-extension", "data" ]
+    chromePrefixes = [ "about:", "view-source:", "chrome-extension:", "data:" ]
     for prefix in chromePrefixes
       return true if url.startsWith prefix
     false


Fix view inheritance

Fix view inheritance

--- app/assets/javascripts/rglossa/speech/views/cwb/result_view.coffee
+++ app/assets/javascripts/rglossa/speech/views/cwb/result_view.coffee
@@ -1,2 +1,4 @@
-App.SpeechCwbResultView = Em.View.extend
+#= require rglossa/views/cwb/result_view
+
+App.SpeechCwbResultView = App.CwbResultView.extend
   templateName: 'cwb/result'


Use angular locale for credit card months

--- app/assets/javascripts/sprangular/directives/creditCardForm.coffee
+++ app/assets/javascripts/sprangular/directives/creditCardForm.coffee
@@ -3,21 +3,9 @@
   templateUrl: 'credit_cards/form.html'
   scope:
     creditCard: '='
-  controller: ($scope) ->
-    $scope.months = [
-      {index: 1, name: 'January'},
-      {index: 2, name: 'February'},
-      {index: 3, name: 'March'},
-      {index: 4, name: 'April'},
-      {index: 5, name: 'May'},
-      {index: 6, name: 'June'},
-      {index: 7, name: 'July'},
-      {index: 8, name: 'August'},
-      {index: 9, name: 'September'},
-      {index: 10, name: 'October'},
-      {index: 11, name: 'November'},
-      {index: 12, name: 'December'}
-    ]
+  controller: ($scope, $locale) ->
+    $scope.months = _.map $locale.DATETIME_FORMATS.MONTH, (month, index) ->
+      {"index": index, "name": month}

     currentYear = (new Date).getFullYear()
     $scope.years = [currentYear .. currentYear+15]


Make the landing gear extend a little before retracting the cannon.

--- src/public/scripts/models/entity.player.coffee
+++ src/public/scripts/models/entity.player.coffee
@@ -85,11 +85,17 @@

 			rotationQuaternion = new Three.Quaternion().setFromEuler(@rotation)

-			# Check if the player is within landing range of the planet.
-			if intersect = @getIntersect(@world.planet, 1, 2)
-				@baseExtended = true
-				@cannon.extended = false
-				surfaceNormal = intersect.face.normal
+			# Check if the player is within landing range of the planet. If she is,
+			# retract the cannon, extend the landing gear, and set the ufo to attract
+			# to a level position with relation to the normal of the surface below it.
+			if intersect = @getIntersect(@world.planet, 1, 3)
+				if intersect.distance < 2
+					@cannon.extended = false
+					surfaceNormal = intersect.face.normal
+				else
+					@cannon.extended = true
+					surfaceNormal = @position
+				@baseExtended = true
 			else
 				@baseExtended = false
 				@cannon.extended = true
@@ -102,6 +108,9 @@
 				@addAngularForce(new Three.Euler(19 * @flyRight * dt, 0, 0, 'YXZ'))

 			# Attract player to a straight position with relation to the planet surface.
+			# The normal is determined in the previous step: when the player's close to
+			# the surface, the normal will be calculated from the surface normal. Else,
+			# the normal will be perpendicular to the planet sphere.
 			levelRotation = @calculateLevelRotation(surfaceNormal)
 			levelRotationQuaternion = new Three.Quaternion().setFromEuler(levelRotation)

@@ -109,7 +118,8 @@
 			force = new Three.Euler().setFromQuaternion(forceQuaternion)
 			@addAngularForce(force)

-			# Add thrust straight downward from the player.
+			# Add thrust straight downward from the player. If the player's boosting,
+			# the thrust will be significantly higher than then she's not.
 			thrustVector = new Three.Vector3(0, 1, 0).applyQuaternion(rotationQuaternion)

 			if @boost
@@ -125,13 +135,16 @@

 			# Update physics.
 			super(dt, not @landed)
-
+
+			# Update our cannon.
+			@cannon.update(dt)
+
 			# Make the ufo bounce upward a bit when landed.
 			if @landed
 				targetPosition = @landedPosition.clone().add(@landedPosition.clone().setLength(.8))
 				@position.lerp(targetPosition, dt * 8)

-			# Retract or extend the base
+			# Retract or extend the base.
 			if @baseExtended
 				@_ufoBase.position.lerp(new Three.Vector3(0, 0, 0), dt * 8)
 				@_ufoBase.scale.lerp(new Three.Vector3(1, 2, 1), dt * 8)
@@ -142,13 +155,11 @@
 				@addAngularForce(new Three.Euler(0, @cannon.rotation.y * 20 * dt, 0, 'YXZ'))
 				@cannon.addAngularForce(new Three.Euler(0, -@cannon.rotation.y * 20 * dt, 0, 'YXZ'))

-			# Update our cannon.
-			@cannon.update(dt)
-
 			# Update alien animation.
-			@animation.update(dt)
-
-		# Fires a projectile.
+			@animation.update(dt)
+
+		# Fires a projectile when the cannon is ready, which is a fixed amount of time
+		# after the last shot.
 		#
 		fire: ( ) ->
 			if @_cannonReady and @cannon.extended


smoothing

smoothing

--- src/Shaders.coffee
+++ src/Shaders.coffee
@@ -87,12 +87,29 @@
       "float index1 = zp1 * uTileWidth * uTileHeight + yp * uTileWidth + xp;"
       "float index2 = zp2 * uTileWidth * uTileHeight + yp * uTileWidth + xp;"

+      #
       # Get 2D pixel texture coordinates
+      #
+
+      # First frame
       "float xt1 = mod(index1, uDimension);"
       "float yt1 = floor(index1 / uDimension);"

+      # "float xt1_0 = mod(index1 - 1.0, uDimension);"
+      # "float yt1_0 = floor((index1 - 1.0) / uDimension);"
+      #
+      # "float xt1_2 = mod(index1 + 1.0, uDimension);"
+      # "float yt1_2 = floor((index1 + 1.0) / uDimension);"
+
+      # Second frame
       "float xt2 = mod(index2, uDimension);"
       "float yt2 = floor(index2 / uDimension);"
+
+      # "float xt2_0 = mod(index2 - 1.0, uDimension);"
+      # "float yt2_0 = floor((index2 - 1.0) / uDimension);"
+      #
+      # "float xt2_2 = mod(index2 + 1.0, uDimension);"
+      # "float yt2_2 = floor((index2 + 1.0) / uDimension);"

       # Get normalized 2D texture coordinates
       "texturePosition1.x = xt1 / uDimension;"
@@ -101,10 +118,36 @@
       "texturePosition2.x = xt2 / uDimension;"
       "texturePosition2.y = yt2 / uDimension;"

+      # "vec2 texturePosition1_0;"
+      # "vec2 texturePosition1_2;"
+      # "vec2 texturePosition2_0;"
+      # "vec2 texturePosition2_2;"
+      #
+      # "texturePosition1_0.x = xt1_0 / uDimension;"
+      # "texturePosition1_0.y = yt1_0 / uDimension;"
+      #
+      # "texturePosition1_2.x = xt1_2 / uDimension;"
+      # "texturePosition1_2.x = yt1_2 / uDimension;"
+      #
+      # "texturePosition2_0.x = xt2_0 / uDimension;"
+      # "texturePosition2_0.y = yt2_0 / uDimension;"
+      #
+      # "texturePosition2_2.x = xt2_2 / uDimension;"
+      # "texturePosition2_2.x = yt2_2 / uDimension;"
+
       # Get texture values
       "float value1 = texture2D(uVolData, texturePosition1).x;"
       "float value2 = texture2D(uVolData, texturePosition2).x;"

+      # "float value1_0 = texture2D(uVolData, texturePosition1_0).x;"
+      # "float value1_2 = texture2D(uVolData, texturePosition1_2).x;"
+      #
+      # "float value2_0 = texture2D(uVolData, texturePosition2_0).x;"
+      # "float value2_2 = texture2D(uVolData, texturePosition2_2).x;"
+
+      # "value1 = (value1_0 + value1 + value1_2) / 3.0;"
+      # "value2 = (value2_0 + value2 + value2_2) / 3.0;"
+
       # Scale pixels
       "value1 = (value1 - uMinimum) / uRange;"
       "value2 = (value2 - uMinimum) / uRange;"


Finalize possible tests for Node

Finalize possible tests for Node

--- src/tests/spec/public/library/node.spec.coffee
+++ src/tests/spec/public/library/node.spec.coffee
@@ -351,4 +351,21 @@
 					expect(node.connect.mostRecentCall.args).toEqual([
 							'1'
 							false
-						])+						])
+
+			describe 'when connected with server', ->
+				it 'should set own id to the one from the server', ->
+					spyOn(node.server, 'ping')
+					node._onServerConnect('1')
+					expect(node.id).toBe('1')
+
+				it 'should calculate the latency to the server', ->
+					spyOn(node.server, 'ping')
+					node._onServerConnect('1')
+
+					# bad approach, might fail sometimes
+					now = Date.now()
+					delta = node.server.ping.mostRecentCall.args[0](100, 300)
+					# formula should be: serverTime - (currentLocalTime - latency / 2)
+					expect(node._timeDelta).toBe(delta)
+					expect(delta).toEqual(300 - (now - 50))


Fixed issue #907 - “Add link”-screen is not really useful.

--- app/assets/javascripts/app/controllers/widget/link.coffee
+++ app/assets/javascripts/app/controllers/widget/link.coffee
@@ -110,8 +110,8 @@
       processData: true
       success: (data, status, xhr) =>
         App.Collection.loadAssets(data.assets)
-        @ticket_ids_by_customer    = data.ticket_ids_by_customer
-        @ticket_ids_recent_viewed  = data.ticket_ids_recent_viewed
+        @ticketIdsByCustomer    = data.ticket_ids_by_customer
+        @ticketIdsRecentViewed  = data.ticket_ids_recent_viewed
         @render()
     )

@@ -122,30 +122,26 @@
       object:         @object
     ))

-    list = []
-    for ticket_id in @ticket_ids_by_customer
+    ticketIdsByCustomer = []
+    for ticket_id in @ticketIdsByCustomer
       if ticket_id isnt @ticket.id
-        ticketItem = App.Ticket.fullLocal(ticket_id)
-        list.push ticketItem
-    new App.ControllerTable(
-      el:       content.find('#ticket-merge-customer-tickets')
-      overview: ['number', 'title', 'state', 'group', 'created_at']
-      model:    App.Ticket
-      objects:  list
-      radio:    true
+        ticketIdsByCustomer.push ticket_id
+    new App.TicketList(
+      tableId:    'ticket-merge-customer-tickets'
+      el:         content.find('#ticket-merge-customer-tickets')
+      ticket_ids: ticketIdsByCustomer
+      radio:      true
     )

-    list = []
-    for ticket_id in @ticket_ids_recent_viewed
+    ticketIdsByRecentView = []
+    for ticket_id in @ticketIdsRecentViewed
       if ticket_id isnt @ticket.id
-        ticketItem = App.Ticket.fullLocal( ticket_id )
-        list.push ticketItem
-    new App.ControllerTable(
-      el:       content.find('#ticket-merge-recent-tickets')
-      overview: ['number', 'title', 'state', 'group', 'created_at']
-      model:    App.Ticket
-      objects:  list
-      radio:    true
+        ticketIdsByRecentView.push ticket_id
+    new App.TicketList(
+      tableId:    'ticket-merge-recent-tickets'
+      el:         content.find('#ticket-merge-recent-tickets')
+      ticket_ids: ticketIdsByRecentView
+      radio:      true
     )

     content.delegate('[name="ticket_number"]', 'focus', (e) ->


Improved loadAssets, just execute if we have assets to load (ignore empty lists).

--- app/assets/javascripts/app/lib/app_post/collection.coffee
+++ app/assets/javascripts/app/lib/app_post/collection.coffee
@@ -64,13 +64,13 @@
     appObject.refresh(params.data, clear: true)

   loadAssets: (assets) ->
-    @log 'debug', 'loadAssets', assets
+    return if _.isEmpty(assets)

     # process not existing assets first / to avoid not exising ref errors
-    loadAssetsLater = []
+    loadAssetsLater = {}
     for type, collections of assets
       later = @load(type: type, data: collections, later: true)
-      if later
+      if !_.isEmpty(later)
         loadAssetsLater[type] = later

     # process existing assets


chore(widgets): returns null in the `output` widget block

--- app/assets/javascripts/awesome_ui/inputs/output.js.coffee
+++ app/assets/javascripts/awesome_ui/inputs/output.js.coffee
@@ -8,3 +8,5 @@
     element.textContent = target.value

   element.textContent = target.value
+
+  null


Fixes measure calculation when layer is reverted. Related to #954.

--- app/assets/javascripts/leaflet/reactive_measure.js.coffee
+++ app/assets/javascripts/leaflet/reactive_measure.js.coffee
@@ -218,6 +218,15 @@
       this._poly.on 'editdrag', @__onHandlerDrag, this

   removeHooks: () ->
+
+    g = new L.GeographicUtil.Polygon @_poly.getLatLngsAsArray()
+
+    measure =
+      perimeter: g.perimeter()
+      area: g.area()
+
+    @_reactiveMeasureControl.updateContent measure, {selection: false}
+
     if L.EditToolbar.reactiveMeasure
       this._poly.off 'editdrag'

@@ -397,7 +406,7 @@
     if measure['area']
       text += "#{L.GeometryUtil.readableArea(measure.area, !!@options.metric)}"

-    if options.selection?
+    if options.selection? && options.selection is true
       L.DomUtil.addClass @_container, 'selection'
     else
       L.DomUtil.removeClass @_container, 'selection'


Patch missing event listener on Leaflet.Draw

--- app/assets/javascripts/leaflet/reactive_measure.js.coffee
+++ app/assets/javascripts/leaflet/reactive_measure.js.coffee
@@ -225,13 +225,13 @@

     if options.onTop
       L.DomUtil.addClass(@_container, 'leaflet-draw-tooltip-top')
-      L.DomEvent.on this._container, 'mouseenter', this.__onMouseEnter, this
-      L.DomEvent.on this._container, 'mouseleave', this.__onMouseLeave, this
+      L.DomEvent.on @_container, 'mouseenter', @__onMouseEnter, this
+      L.DomEvent.on @_container, 'mouseleave', @__onMouseLeave, this

   dispose: ->
     @_map.off 'mouseover'
-    L.DomEvent.off this._container, 'mouseenter', this.__onMouseEnter, this
-    L.DomEvent.off this._container, 'mouseleave', this.__onMouseLeave, this
+    L.DomEvent.off @_container, 'mouseenter', @__onMouseEnter, this
+    L.DomEvent.off @_container, 'mouseleave', @__onMouseLeave, this
     @__dispose.apply this, arguments

   __onMouseEnter: ->
@@ -292,3 +292,13 @@
     L.EditToolbar.reactiveMeasure = !!options.reactiveMeasure
     @__initialize.apply this, arguments
     return
+
+
+#Patch Leaflet.Draw
+L.EditToolbar.Edit.include
+  __removeHooks: L.EditToolbar.Edit::removeHooks
+
+  removeHooks: ->
+    @__removeHooks.apply @, arguments
+    if @_map
+      @_map.off 'draw:editvertex', @_updateTooltip, @


Don't show PAs on click above zoom 4

Don't show PAs on click above zoom 4

No point clicking that high up, and it just makes initial double click
zooms more difficult.

--- app/assets/javascripts/modules/maps/interactive.js.coffee
+++ app/assets/javascripts/modules/maps/interactive.js.coffee
@@ -17,6 +17,8 @@
       openPopup()

   handleMapClick: (e) =>
+    return if @map.getZoom() < 4
+
     coords = e.latlng
     params = {
       lon: coords.lng


use `maiko_url` from chooser

use `maiko_url` from chooser

--- app/assets/javascripts/typus_maiko/models/asset.js.coffee
+++ app/assets/javascripts/typus_maiko/models/asset.js.coffee
@@ -1,4 +1,7 @@
 class Typus.Maiko.Asset extends Backbone.Model
+
+  urlKeys:
+    ['public_url', 'maiko_url']

   defaults:
     caption: []
@@ -22,9 +25,10 @@
   # Set / normalize the url.
   # Provides backwards compatibility with typus_fluxiom
   setUrl: ->
-    if public_url = @get 'public_url'
-      @set 'url', public_url
-      @unset 'public_url'
+    for key in @urlKeys
+      if value = @get key
+        @set 'url', value
+        @unset key

   # Set asset thumbnail.
   # Provides backwards compatibility with typus_fluxiom


landing: show more intelligent has payment method message

--- client/landing/site.landing/coffee/team/tabs/teampaymenttab.coffee
+++ client/landing/site.landing/coffee/team/tabs/teampaymenttab.coffee
@@ -17,10 +17,9 @@
     @hasPaymentMethodView = new KDCustomHTMLView
       cssClass: 'payment-method-entry-form has-payment-method-wrapper'

-    @hasPaymentMethodView.addSubView new KDCustomHTMLView
+    @hasPaymentMethodView.addSubView @hasPaymentLabel = new KDCustomHTMLView
       tagName: 'div'
       cssClass: 'has-payment-method-label'
-      partial: 'Looks like we already got your info.'

     @hasPaymentMethodView.addSubView new KDCustomHTMLView
       tagName: 'a'
@@ -139,6 +138,9 @@
         wrapper = @$().find '.has-payment-method-wrapper'
         wrapper.toggleClass 'hidden', off

+        {payment: {last4}} = KD.utils.getTeamData()
+        @hasPaymentLabel.updatePartial "We have **** **** **** #{last4} on file."
+

   pistachio: ->


Use the same menu label on all platforms. Closes #15

--- menus/win32.cson
+++ menus/win32.cson
@@ -1,7 +1,7 @@
 'context-menu':
   '.platform-win32 .tree-view, .platform-win32 atom-text-editor': [
     {
-      label: 'Open Cmd Prompt Here',
+      label: 'Open Terminal Here',
       command: 'open-terminal-here:open'
     }
   ]


check if input exists before focus

check if input exists before focus

--- shared/src/components/notifications/email.cjsx
+++ shared/src/components/notifications/email.cjsx
@@ -59,7 +59,7 @@
       @onPinCheck()

   renderPin: ->
-    _.defer => @refs.verifyInput.focus()
+    _.defer => @refs.verifyInput?.focus()


Fixes forgot-to-destroy-battle bug

Fixes forgot-to-destroy-battle bug

--- controllers/socket.coffee
+++ controllers/socket.coffee
@@ -100,6 +100,7 @@
       @broadcast notice: data
       @emit users: users.toJSON()
       @broadcast users: users.toJSON()
+      battle.destroy()
     , Battle.DURATION

   @on declineChallenge: ->


Disable changing placeholder after status update change.

--- client/app/MainApp/avatararea.coffee
+++ client/app/MainApp/avatararea.coffee
@@ -187,7 +187,7 @@
           title    : 'Message posted!'
           duration : 2000
         @statusField.setValue ""
-        @statusField.setPlaceHolder reply.body
+        # @statusField.setPlaceHolder reply.body
         @hide()

       else