/public/javascripts/application.coffee

https://github.com/fortnightlabs/tractor · CoffeeScript · 234 lines · 199 code · 33 blank · 2 comment · 35 complexity · 4b785fc690aae2e5eb210afcf9ed354b MD5 · raw file

  1. Tractor =
  2. if exports?
  3. Backbone = require './vendor/backbone'
  4. _ = require 'underscore'
  5. exports
  6. else # TODO dry
  7. Backbone = window.Backbone
  8. _ = window._
  9. window.Tractor = {}
  10. Backbone.Model.prototype.idAttribute = '_id'
  11. Tractor.Project = Backbone.Model.extend()
  12. Tractor.Projects = Backbone.Collection.extend
  13. model: Tractor.Project
  14. url: '/projects'
  15. Tractor.Item = Backbone.Model.extend
  16. toggle: -> @set selected: !@get('selected')
  17. parse: (r) ->
  18. r.start = new Date r.start
  19. r.end = new Date r.end
  20. r.hour = r.start.getHours()
  21. r
  22. description: ->
  23. if info = @get('info')
  24. if info.title
  25. info.title
  26. else if info.subject
  27. "#{info.subject} (#{info.sender})"
  28. class Tractor.Items extends Backbone.Collection
  29. model: Tractor.Item
  30. initialize: ->
  31. lazyUpdateTotals = _.debounce @updateTotals, 1
  32. @bind 'reset', lazyUpdateTotals, this
  33. @bind 'remove', lazyUpdateTotals, this
  34. @bind 'change:selected', lazyUpdateTotals, this
  35. @bind 'change:projectId', lazyUpdateTotals, this
  36. @updateTotals()
  37. updateTotals: =>
  38. # TODO speed up (bulk selection)
  39. projects = {}
  40. apps = {}
  41. totals =
  42. length: @length
  43. duration: 0
  44. selected: 0
  45. apps: apps
  46. projects: projects
  47. @each (item) ->
  48. totals.duration += duration = item.get 'duration'
  49. totals.selected += duration if item.get 'selected'
  50. apps[app] = true if app = item.get 'app'
  51. project = item.get('projectId') || 'unassigned'
  52. projects[project] = (projects[project] || 0) + duration
  53. @totals = totals
  54. @trigger 'change:totals', this, @totals
  55. selected: ->
  56. @chain().filter (i) -> i.get 'selected'
  57. class Tractor.Group extends Backbone.Model
  58. defaults:
  59. open: false
  60. initialize: ->
  61. @collection = new Tractor.Items @attributes.collection
  62. @collection.each ((i) -> i.group = this), this
  63. @collection.bind 'remove', @resetAttributes, this
  64. @collection.bind 'change:selected', @resetAttributes, this
  65. @collection.bind 'change:totals', @resetAttributes, this
  66. @collection.bind 'change:cursor', @changeCursor, this
  67. @collection.bind 'change:projectId', @echo('change:projectId'), this
  68. @bind 'change:open', @changeOpen, this
  69. @resetAttributes()
  70. resetAttributes: ->
  71. @set
  72. projectId: @collection.first()?.get('projectId')
  73. start: @collection.first()?.get('start')
  74. end: @collection.last()?.get('end')
  75. selected: @collection.all (i) -> i.get 'selected'
  76. duration: @collection.totals.duration
  77. totals: @collection.totals
  78. @set { open: true }, { silent: true } if !@get('projectId')
  79. changeCursor: (item, val) ->
  80. @set cursor: @collection.any((i) -> i.get 'cursor')
  81. echo: (event) ->
  82. (model, val, options) -> @trigger event, model, val, options
  83. changeOpen: (group, val) ->
  84. if @get 'cursor'
  85. i = @collection.first()
  86. i.trigger 'change:cursor', i, true
  87. class Tractor.Hour extends Backbone.Collection
  88. model: Tractor.Group
  89. initialize: (models, options) ->
  90. @hour = @first()?.get('start')
  91. @bind 'change:totals', @updateTotals, this
  92. @bind 'change:projectId', @resetGroups, this
  93. @resetGroups()
  94. items: ->
  95. if @length is 0 or @first() instanceof Tractor.Item
  96. @models
  97. else
  98. @reduce (r, group) ->
  99. r.concat group.collection.models
  100. , []
  101. updateTotals: ->
  102. @totals = @reduce (t, group) ->
  103. totals = group.get 'totals'
  104. t.length += totals.length
  105. t.duration += totals.duration
  106. t.selected += totals.selected
  107. for projectId, duration of totals.projects
  108. t.projects[projectId] = (t.projects[projectId] || 0) + duration
  109. # TODO _.extend t.apps, totals.apps
  110. t
  111. ,
  112. length: 0
  113. duration: 0
  114. selected: 0
  115. apps: {}
  116. projects: {}
  117. @selected = @totals.duration == @totals.selected
  118. resetGroups: ->
  119. groups = []
  120. lastProjectId = lastGroup = null
  121. _.each @items(), (i) ->
  122. projectId = i.get('projectId') || null
  123. if not lastGroup or projectId isnt lastProjectId
  124. groups.push lastGroup = []
  125. lastProjectId = projectId
  126. lastGroup.push i
  127. , this
  128. @reset _.map(groups, (g) -> new Tractor.Group collection: g)
  129. @updateTotals()
  130. class Tractor.AllItems extends Tractor.Items
  131. url: '/items'
  132. parse: (response) ->
  133. _.map response, Tractor.Item.prototype.parse
  134. initialize: ->
  135. super *arguments
  136. @bind 'reset', @resetHours, this
  137. @bind 'remove', @changeCursorOnRemove, this
  138. @bind 'change:cursor', @changeCursor, this
  139. @bind 'change:selected', @selectRange, this
  140. resetHours: ->
  141. @_cursor = [0, 0]
  142. @at(0)?.set { cursor: true }, { silent: true }
  143. @hours = []
  144. @chain()
  145. .groupBy((item) -> item.get 'hour')
  146. .each((items, h) => @hours[h] = new Tractor.Hour items)
  147. hoursFor: (items) ->
  148. hours = items.invoke('get', 'hour').uniq().value()
  149. _(@hours[h] for h in hours)
  150. cursor: -> _(@models[@_cursor[0] .. @_cursor[1]]).chain()
  151. next: (n = 1) -> @at Math.min(@_cursor[1] + n, @length - 1)
  152. prev: (n = 1) -> @at Math.max(@_cursor[0] - n, 0)
  153. nextAssigned: (assigned) ->
  154. max = @length - 1 - @_cursor[1]
  155. n = 1
  156. if assigned or @next().get('projectId')?
  157. n++ while n < max && @next(n).get('projectId')?
  158. @next n
  159. else
  160. n++ until n == max || @next(n).get('projectId')?
  161. @next n - 1
  162. prevAssigned: (assigned) ->
  163. max = @_cursor[0]
  164. n = 1
  165. prev =
  166. if assigned or @prev().get('projectId')?
  167. n++ while n < max && @prev(n).get('projectId')?
  168. @prev n
  169. else
  170. n++ until n == max || @prev(n).get('projectId')?
  171. @prev n - 1
  172. changeCursor: (model, val) ->
  173. return unless val
  174. @cursor().without(model).invoke 'set', cursor: false if val
  175. i = @indexOf model # TODO slow
  176. group = model.group
  177. @_cursor =
  178. if group.get('projectId') and not group.get('open')
  179. items = group.collection
  180. items.invoke 'set', { cursor: true }, { silent: true }
  181. [ @indexOf(items.first()), @indexOf(items.last()) ]
  182. else
  183. [ i, i ]
  184. changeCursorOnRemove: (model) ->
  185. return
  186. if model.get('start') <= @at(@_cursor[0]).get('start')
  187. @_cursor[0]--
  188. @_cursor[1]--
  189. @cursor().invoke 'set', cursor: true
  190. selectedOrCursor: ->
  191. selected = @selected()
  192. if selected.value().length > 0
  193. selected
  194. else
  195. @cursor()
  196. selectRange: (model, val, options) ->
  197. if val and options.range
  198. i = @indexOf model
  199. for o in @models[Math.min(i, @_cursor[0]) .. Math.max(i, @_cursor[1])]
  200. o.set selected: true
  201. true