chaplin-on-rails 0.7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +42 -0
  3. data/Rakefile +38 -0
  4. data/lib/chaplin-on-rails.rb +2 -0
  5. data/lib/chaplin-on-rails/engine.rb +6 -0
  6. data/lib/chaplin-on-rails/version.rb +3 -0
  7. data/lib/generators/chaplin/helpers.rb +94 -0
  8. data/lib/generators/chaplin/install/install_generator.rb +68 -0
  9. data/lib/generators/chaplin/install/templates/app_template.js.coffee +65 -0
  10. data/lib/generators/chaplin/install/templates/application.js.coffee +13 -0
  11. data/lib/generators/chaplin/install/templates/javascripts/controllers/base/controller.js.coffee +8 -0
  12. data/lib/generators/chaplin/install/templates/javascripts/lib/support.js.coffee +19 -0
  13. data/lib/generators/chaplin/install/templates/javascripts/lib/utils.js.coffee +18 -0
  14. data/lib/generators/chaplin/install/templates/javascripts/lib/view_helper.js.coffee +16 -0
  15. data/lib/generators/chaplin/install/templates/javascripts/models/base/collection.js.coffee +9 -0
  16. data/lib/generators/chaplin/install/templates/javascripts/models/base/model.js.coffee +9 -0
  17. data/lib/generators/chaplin/install/templates/javascripts/routes.js.coffee +9 -0
  18. data/lib/generators/chaplin/install/templates/javascripts/views/base/collection_view.coffee +11 -0
  19. data/lib/generators/chaplin/install/templates/javascripts/views/base/view.coffee +15 -0
  20. data/lib/generators/chaplin/install/templates/javascripts/views/layout.js.coffee +8 -0
  21. data/lib/generators/chaplin/install/templates/requirejs.yml +6 -0
  22. data/test/chaplin-on-rails_test.rb +19 -0
  23. data/test/dummy/README.rdoc +261 -0
  24. data/test/dummy/Rakefile +7 -0
  25. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  26. data/test/dummy/app/controllers/application_controller.rb +3 -0
  27. data/test/dummy/app/controllers/home_controller.rb +5 -0
  28. data/test/dummy/app/helpers/application_helper.rb +2 -0
  29. data/test/dummy/app/helpers/home_helper.rb +2 -0
  30. data/test/dummy/app/views/home/index.html.erb +1 -0
  31. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  32. data/test/dummy/config.ru +4 -0
  33. data/test/dummy/config/application.rb +59 -0
  34. data/test/dummy/config/boot.rb +10 -0
  35. data/test/dummy/config/database.yml +25 -0
  36. data/test/dummy/config/environment.rb +5 -0
  37. data/test/dummy/config/environments/development.rb +37 -0
  38. data/test/dummy/config/environments/production.rb +67 -0
  39. data/test/dummy/config/environments/test.rb +37 -0
  40. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  41. data/test/dummy/config/initializers/inflections.rb +15 -0
  42. data/test/dummy/config/initializers/mime_types.rb +5 -0
  43. data/test/dummy/config/initializers/secret_token.rb +7 -0
  44. data/test/dummy/config/initializers/session_store.rb +8 -0
  45. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  46. data/test/dummy/config/locales/en.yml +5 -0
  47. data/test/dummy/config/routes.rb +58 -0
  48. data/test/dummy/db/development.sqlite3 +0 -0
  49. data/test/dummy/db/test.sqlite3 +0 -0
  50. data/test/dummy/log/development.log +186 -0
  51. data/test/dummy/log/test.log +35 -0
  52. data/test/dummy/public/404.html +26 -0
  53. data/test/dummy/public/422.html +26 -0
  54. data/test/dummy/public/500.html +25 -0
  55. data/test/dummy/public/favicon.ico +0 -0
  56. data/test/dummy/script/rails +6 -0
  57. data/test/dummy/tmp/cache/assets/C75/D70/sprockets%2F1781f0919424c1d3014e066757a81eaa +0 -0
  58. data/test/dummy/tmp/cache/assets/C80/150/sprockets%2F0d3881005b0646df783d5c24683d34f5 +0 -0
  59. data/test/dummy/tmp/cache/assets/C89/4B0/sprockets%2F1f6245087eeb4c854a03a4644c964579 +0 -0
  60. data/test/dummy/tmp/cache/assets/C8D/980/sprockets%2F7184f95c8e290cbc2451767559f01d08 +0 -0
  61. data/test/dummy/tmp/cache/assets/CA8/130/sprockets%2F695a47c24c7804e27afc2208426ae133 +0 -0
  62. data/test/dummy/tmp/cache/assets/CD8/370/sprockets%2F357970feca3ac29060c1e3861e2c0953 +0 -0
  63. data/test/dummy/tmp/cache/assets/CDE/EC0/sprockets%2F32bce1758d7525013970c7bb7d77aa28 +0 -0
  64. data/test/dummy/tmp/cache/assets/CEB/750/sprockets%2F72f4d157b237c37ed75f49bd217824c7 +0 -0
  65. data/test/dummy/tmp/cache/assets/CF6/290/sprockets%2F95f8b25659fb7a430d68b27a5f72f666 +0 -0
  66. data/test/dummy/tmp/cache/assets/CF9/7C0/sprockets%2F40fc2f3d2a468a00e463f1d313cb1683 +0 -0
  67. data/test/dummy/tmp/cache/assets/D08/A30/sprockets%2F1310a2826dcae357ba9f383e86b638a1 +0 -0
  68. data/test/dummy/tmp/cache/assets/D09/100/sprockets%2F4dc073204163e0850bf7fcb55ff8b552 +0 -0
  69. data/test/dummy/tmp/cache/assets/D0A/A40/sprockets%2F09e4e8ba330b24f9f87a63a910d422f3 +0 -0
  70. data/test/dummy/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
  71. data/test/dummy/tmp/cache/assets/D38/B80/sprockets%2F42c6d981cbd6ae6061c7088a2321a8ed +0 -0
  72. data/test/dummy/tmp/cache/assets/D3E/070/sprockets%2F45f1913b6d3645a3d166ff4ff250e4cc +0 -0
  73. data/test/dummy/tmp/cache/assets/D4E/D00/sprockets%2F1a6846f0a837ae2524e2f9ec89e6ef43 +0 -0
  74. data/test/dummy/tmp/cache/assets/D57/BC0/sprockets%2F50db33dbb1258913cb2fbd562ab1294b +0 -0
  75. data/test/dummy/tmp/cache/assets/D5A/EA0/sprockets%2Fd771ace226fc8215a3572e0aa35bb0d6 +0 -0
  76. data/test/dummy/tmp/cache/assets/D63/C90/sprockets%2Fb70150e50cbcbc59c55ffb3c53a35575 +0 -0
  77. data/test/dummy/tmp/cache/assets/D6F/C70/sprockets%2F667a4890bf4c03f50b452d1db278fcfe +0 -0
  78. data/test/dummy/tmp/cache/assets/D79/470/sprockets%2F1cb96b862f7c7497af494aa19d0cbd55 +0 -0
  79. data/test/dummy/tmp/cache/assets/D7A/930/sprockets%2F3964cc67ad46beb69c444bb51f486e2f +0 -0
  80. data/test/dummy/tmp/cache/assets/D8B/B40/sprockets%2F11b945abcf62b02fcfb34f52f6aa1481 +0 -0
  81. data/test/dummy/tmp/cache/assets/D92/990/sprockets%2F4b34320d2682dc0fd9b107f3ef2cae7d +0 -0
  82. data/test/dummy/tmp/cache/assets/D98/8B0/sprockets%2Fedbef6e0d0a4742346cf479f2c522eb0 +0 -0
  83. data/test/dummy/tmp/cache/assets/DC8/010/sprockets%2F34cce381cfe1aa4be1ca560f118dd998 +0 -0
  84. data/test/dummy/tmp/cache/assets/DCD/ED0/sprockets%2F77ca908f86a8be3506de6fe60b1e0baa +0 -0
  85. data/test/dummy/tmp/cache/assets/DCE/890/sprockets%2Fd54185fb144acd9d1d15ed0e4ebf9f44 +0 -0
  86. data/test/dummy/tmp/cache/assets/DCE/D00/sprockets%2Fed31b7ea3cfb14d43a87c89deb0390f7 +0 -0
  87. data/test/dummy/tmp/cache/assets/DCF/610/sprockets%2Fda58bc8db5dd9cb124a85c507d591fd2 +0 -0
  88. data/test/dummy/tmp/cache/assets/E11/4E0/sprockets%2F86e145a39f85cceeaffdff91ebb61449 +0 -0
  89. data/test/test_helper.rb +7 -0
  90. data/vendor/assets/javascripts/backbone.js +1497 -0
  91. data/vendor/assets/javascripts/chaplin.js.coffee +2377 -0
  92. data/vendor/assets/javascripts/underscore.js +1222 -0
  93. metadata +292 -0
@@ -0,0 +1,2377 @@
1
+ ###
2
+ Chaplin 0.7.0-pre.
3
+
4
+ Chaplin may be freely distributed under the MIT license.
5
+ For all details and documentation:
6
+ http://chaplinjs.org
7
+ ###
8
+
9
+ define 'chaplin/application', [
10
+ 'backbone'
11
+ 'chaplin/mediator'
12
+ 'chaplin/dispatcher'
13
+ 'chaplin/views/layout'
14
+ 'chaplin/lib/router'
15
+ 'chaplin/lib/event_broker'
16
+ ], (Backbone, mediator, Dispatcher, Layout, Router, EventBroker) ->
17
+ 'use strict'
18
+
19
+ # The application bootstrapper
20
+ # ----------------------------
21
+
22
+ class Application
23
+
24
+ # Borrow the static extend method from Backbone
25
+ @extend = Backbone.Model.extend
26
+
27
+ # Mixin an EventBroker
28
+ _(@prototype).extend EventBroker
29
+
30
+ # The site title used in the document title
31
+ title: ''
32
+
33
+ # The application instantiates these three core modules
34
+ dispatcher: null
35
+ layout: null
36
+ router: null
37
+
38
+ initialize: ->
39
+
40
+ initDispatcher: (options) ->
41
+ @dispatcher = new Dispatcher options
42
+
43
+ initLayout: (options = {}) ->
44
+ options.title ?= @title
45
+ @layout = new Layout options
46
+
47
+ # Instantiate the dispatcher
48
+ # --------------------------
49
+
50
+ # Pass the function typically returned by routes.coffee
51
+ initRouter: (routes, options) ->
52
+ # Save the reference for testing introspection only.
53
+ # Modules should communicate with each other via Pub/Sub.
54
+ @router = new Router options
55
+
56
+ # Register all routes declared in routes.coffee
57
+ routes? @router.match
58
+
59
+ # After registering the routes, start Backbone.history
60
+ @router.startHistory()
61
+
62
+ # Disposal
63
+ # --------
64
+
65
+ disposed: false
66
+
67
+ dispose: ->
68
+ return if @disposed
69
+
70
+ properties = ['dispatcher', 'layout', 'router']
71
+ for prop in properties when this[prop]?
72
+ this[prop].dispose()
73
+ delete this[prop]
74
+
75
+ @disposed = true
76
+
77
+ # You’re frozen when your heart’s not open
78
+ Object.freeze? this
79
+
80
+ define 'chaplin/mediator', [
81
+ 'underscore'
82
+ 'backbone'
83
+ 'chaplin/lib/support'
84
+ 'chaplin/lib/utils'
85
+ ], (_, Backbone, support, utils) ->
86
+ 'use strict'
87
+
88
+ # Mediator
89
+ # --------
90
+
91
+ # The mediator is a simple object all others modules use to communicate
92
+ # with each other. It implements the Publish/Subscribe pattern.
93
+ #
94
+ # Additionally, it holds objects which need to be shared between modules.
95
+ # In this case, a `user` property is created for getting the user object
96
+ # and a `setUser` method for setting the user.
97
+ #
98
+ # This module returns the singleton object. This is the
99
+ # application-wide mediator you might load into modules
100
+ # which need to talk to other modules using Publish/Subscribe.
101
+
102
+ # Start with a simple object
103
+ mediator = {}
104
+
105
+ # Publish / Subscribe
106
+ # -------------------
107
+
108
+ # Mixin event methods from Backbone.Events,
109
+ # create Publish/Subscribe aliases
110
+ mediator.subscribe = Backbone.Events.on
111
+ mediator.unsubscribe = Backbone.Events.off
112
+ mediator.publish = Backbone.Events.trigger
113
+
114
+ # Initialize an empty callback list so we might seal the mediator later
115
+ mediator._callbacks = null
116
+
117
+ # Make properties readonly
118
+ utils.readonly mediator, 'subscribe', 'unsubscribe', 'publish'
119
+
120
+ # Sealing the mediator
121
+ # --------------------
122
+
123
+ # After adding all needed properties, you should seal the mediator
124
+ # using this method
125
+ mediator.seal = ->
126
+ # Prevent extensions and make all properties non-configurable
127
+ if support.propertyDescriptors and Object.seal
128
+ Object.seal mediator
129
+
130
+ # Make the method readonly
131
+ utils.readonly mediator, 'seal'
132
+
133
+ # Return our creation
134
+ mediator
135
+
136
+ define 'chaplin/dispatcher', [
137
+ 'underscore'
138
+ 'backbone'
139
+ 'chaplin/lib/utils'
140
+ 'chaplin/lib/event_broker'
141
+ ], (_, Backbone, utils, EventBroker) ->
142
+ 'use strict'
143
+
144
+ class Dispatcher
145
+
146
+ # Borrow the static extend method from Backbone
147
+ @extend = Backbone.Model.extend
148
+
149
+ # Mixin an EventBroker
150
+ _(@prototype).extend EventBroker
151
+
152
+ # The previous controller name
153
+ previousControllerName: null
154
+
155
+ # The current controller, its name, main view and parameters
156
+ currentControllerName: null
157
+ currentController: null
158
+ currentAction: null
159
+ currentParams: null
160
+
161
+ # The current URL
162
+ url: null
163
+
164
+ constructor: ->
165
+ @initialize arguments...
166
+
167
+ initialize: (options = {}) ->
168
+ # Merge the options
169
+ @settings = _(options).defaults
170
+ controllerPath: 'controllers/'
171
+ controllerSuffix: '_controller'
172
+
173
+ # Listen to global events
174
+ @subscribeEvent 'matchRoute', @matchRoute
175
+
176
+ # Controller management
177
+ # Starting and disposing controllers
178
+ # ----------------------------------
179
+
180
+ # Handler for the global matchRoute event
181
+ matchRoute: (route, params, options) ->
182
+ @startupController route.controller, route.action, params, options
183
+
184
+ # The standard flow is:
185
+ #
186
+ # 1. Test if it’s a new controller/action with new params
187
+ # 1. Hide the previous view
188
+ # 2. Dispose the previous controller
189
+ # 3. Instantiate the new controller, call the controller action
190
+ # 4. Show the new view
191
+ #
192
+ startupController: (controllerName, action = 'index', params = {},
193
+ options = {}) ->
194
+ # Set some routing options
195
+
196
+ # Whether to update the URL after controller startup
197
+ # Default to true unless explicitly set to false
198
+ if options.changeURL isnt false
199
+ options.changeURL = true
200
+
201
+ # Whether to force the controller startup even
202
+ # if current and new controllers and params match
203
+ # Default to false unless explicitly set to true
204
+ if options.forceStartup isnt true
205
+ options.forceStartup = false
206
+
207
+ # Stop if the desired controller/action is already active
208
+ # with the same params
209
+ return if not options.forceStartup and
210
+ @currentControllerName is controllerName and
211
+ @currentAction is action and
212
+ # Deep parameters check is not nice but the simplest way for now
213
+ (not @currentParams or _(params).isEqual(@currentParams))
214
+
215
+ # Fetch the new controller, then go on
216
+ @loadController controllerName, (ControllerConstructor) =>
217
+ @controllerLoaded controllerName, action, params, options,
218
+ ControllerConstructor
219
+
220
+ # Load the constructor for a given controller name.
221
+ # The default implementation uses require() from a AMD module loader
222
+ # like RequireJS to fetch the constructor.
223
+ loadController: (controllerName, handler) ->
224
+ fileName = utils.underscorize(controllerName) +
225
+ @settings.controllerSuffix
226
+ moduleName = @settings.controllerPath + fileName
227
+ if define?.amd
228
+ require [moduleName], handler
229
+ else
230
+ handler require moduleName
231
+
232
+ controllerLoaded: (controllerName, action, params, options,
233
+ ControllerConstructor) ->
234
+ # Initialize the new controller
235
+ controller = new ControllerConstructor params, options
236
+
237
+ # Execute before actions if necessary
238
+ methodName = if controller.beforeAction
239
+ 'executeBeforeActionChain'
240
+ else
241
+ 'executeAction'
242
+ this[methodName](controller, controllerName, action, params, options)
243
+
244
+ # Handler for the controller lazy-loading
245
+ executeAction: (controller, controllerName, action, params, options) ->
246
+ # Shortcuts for the previous controller
247
+ currentControllerName = @currentControllerName or null
248
+ currentController = @currentController or null
249
+
250
+ @previousControllerName = currentControllerName
251
+
252
+ # Dispose the previous controller
253
+ if currentController
254
+ # Notify the rest of the world beforehand
255
+ @publishEvent 'beforeControllerDispose', currentController
256
+ # Passing the params and the new controller name
257
+ currentController.dispose params, controllerName
258
+
259
+ # Add the previous controller name to the routing options
260
+ options.previousControllerName = currentControllerName
261
+
262
+ # Call the controller action with params and options
263
+ controller[action] params, options
264
+
265
+ # Stop if the action triggered a redirect
266
+ return if controller.redirected
267
+
268
+ # Save the new controller
269
+ @currentControllerName = controllerName
270
+ @currentController = controller
271
+ @currentAction = action
272
+ @currentParams = params
273
+
274
+ # Adjust the URL
275
+ @adjustURL params, options
276
+
277
+ # We're done! Spread the word!
278
+ @publishEvent 'startupController',
279
+ previousControllerName: @previousControllerName
280
+ controller: @currentController
281
+ controllerName: @currentControllerName
282
+ params: @currentParams
283
+ options: options
284
+
285
+ # Before actions with chained execution
286
+ executeBeforeActionChain: (controller, controllerName, action, params,
287
+ options) ->
288
+ beforeActions = []
289
+ args = arguments
290
+
291
+ # Before actions can be extended by subclasses, so we need to check the
292
+ # whole prototype chain for matching before actions. Before actions in
293
+ # parent classes are executed before actions in child classes.
294
+ for acts in utils.getAllPropertyVersions controller, 'beforeAction'
295
+ # Iterate over the before actions in search for a matching
296
+ # name with the arguments’ action name
297
+ for name, beforeAction of acts
298
+ # Do not add this object more than once
299
+ if name is action or RegExp("^#{name}$").test(action)
300
+ if typeof beforeAction is 'string'
301
+ beforeAction = controller[beforeAction]
302
+ if typeof beforeAction isnt 'function'
303
+ throw new Error 'Controller#executeBeforeActionChain: ' +
304
+ "#{beforeAction} is not a valid beforeAction method for #{name}."
305
+ # Save the before action
306
+ beforeActions.push beforeAction
307
+
308
+ # Save returned value and also immediately return in case the value is false
309
+ next = (method, previous = null) =>
310
+ # Stop if the action triggered a redirect
311
+ return if controller.redirected
312
+
313
+ # End of chain, finally start the action
314
+ unless method
315
+ @executeAction args...
316
+ return
317
+
318
+ previous = method.call controller, params, options, previous
319
+
320
+ # Detect a CommonJS promise in order to use pipelining below,
321
+ # otherwise execute next method directly
322
+ if previous and typeof previous.then is 'function'
323
+ previous.then (data) ->
324
+ next beforeActions.shift(), data
325
+ else
326
+ next beforeActions.shift(), previous
327
+
328
+ # Start beforeAction execution chain
329
+ next beforeActions.shift()
330
+
331
+ # Change the URL to the new controller using the router
332
+ adjustURL: (params, options) ->
333
+ return unless options.path?
334
+
335
+ url = options.path +
336
+ if options.queryString then "?#{options.queryString}" else ""
337
+
338
+ # Tell the router to actually change the current URL
339
+ @publishEvent '!router:changeURL', url, options if options.changeURL
340
+
341
+ # Save the URL
342
+ @url = url
343
+
344
+ # Disposal
345
+ # --------
346
+
347
+ disposed: false
348
+
349
+ dispose: ->
350
+ return if @disposed
351
+
352
+ @unsubscribeAllEvents()
353
+
354
+ @disposed = true
355
+
356
+ # You’re frozen when your heart’s not open
357
+ Object.freeze? this
358
+
359
+ define 'chaplin/controllers/controller', [
360
+ 'underscore'
361
+ 'backbone'
362
+ 'chaplin/lib/event_broker'
363
+ ], (_, Backbone, EventBroker) ->
364
+ 'use strict'
365
+
366
+ class Controller
367
+
368
+ # Borrow the static extend method from Backbone
369
+ @extend = Backbone.Model.extend
370
+
371
+ # Mixin Backbone events and EventBroker.
372
+ _(@prototype).extend Backbone.Events
373
+ _(@prototype).extend EventBroker
374
+
375
+ view: null
376
+
377
+ # Internal flag which stores whether `redirectTo`
378
+ # was called in the current action
379
+ redirected: false
380
+
381
+ # You should set a `title` property on the derived controller. Like this:
382
+ # title: 'foo'
383
+
384
+ constructor: ->
385
+ @initialize arguments...
386
+
387
+ initialize: ->
388
+ # Empty per default
389
+
390
+ adjustTitle: (subtitle) ->
391
+ @publishEvent '!adjustTitle', subtitle
392
+
393
+ # Redirection
394
+ # -----------
395
+
396
+ # Redirect to URL.
397
+ redirectTo: (url, options = {}) ->
398
+ @redirected = true
399
+ @publishEvent '!router:route', url, options, (routed) ->
400
+ unless routed
401
+ throw new Error 'Controller#redirectTo: no route matched'
402
+
403
+ # Redirect to named route.
404
+ redirectToRoute: (name, params, options) ->
405
+ @redirected = true
406
+ @publishEvent '!router:routeByName', name, params, options, (routed) ->
407
+ unless routed
408
+ throw new Error 'Controller#redirectToRoute: no route matched'
409
+
410
+ # Disposal
411
+ # --------
412
+
413
+ disposed: false
414
+
415
+ dispose: ->
416
+ return if @disposed
417
+
418
+ # Dispose and delete all members which are disposable
419
+ for own prop of this
420
+ obj = this[prop]
421
+ if obj and typeof obj.dispose is 'function'
422
+ obj.dispose()
423
+ delete this[prop]
424
+
425
+ # Unbind handlers of global events
426
+ @unsubscribeAllEvents()
427
+
428
+ # Remove properties which are not disposable
429
+ properties = ['redirected']
430
+ delete this[prop] for prop in properties
431
+
432
+ # Finished
433
+ @disposed = true
434
+
435
+ # You're frozen when your heart’s not open
436
+ Object.freeze? this
437
+
438
+ define 'chaplin/models/collection', [
439
+ 'underscore'
440
+ 'backbone'
441
+ 'chaplin/lib/event_broker'
442
+ 'chaplin/models/model'
443
+ ], (_, Backbone, EventBroker, Model) ->
444
+ 'use strict'
445
+
446
+ # Abstract class which extends the standard Backbone collection
447
+ # in order to add some functionality
448
+ class Collection extends Backbone.Collection
449
+
450
+ # Mixin an EventBroker
451
+ _(@prototype).extend EventBroker
452
+
453
+ # Use the Chaplin model per default, not Backbone.Model
454
+ model: Model
455
+
456
+ # Mixin a Deferred
457
+ initDeferred: ->
458
+ _(this).extend $.Deferred()
459
+
460
+ # Serializes collection
461
+ serialize: ->
462
+ for model in @models
463
+ if model instanceof Model
464
+ # Use optimized Chaplin serialization
465
+ model.serialize()
466
+ else
467
+ # Fall back to unoptimized Backbone stuff
468
+ model.toJSON()
469
+
470
+ # Adds a collection atomically, i.e. throws no event until
471
+ # all members have been added
472
+ addAtomic: (models, options = {}) ->
473
+ return unless models.length
474
+ options.silent = true
475
+ direction = if typeof options.at is 'number' then 'pop' else 'shift'
476
+ while model = models[direction]()
477
+ @add model, options
478
+ @trigger 'reset'
479
+
480
+ # Disposal
481
+ # --------
482
+
483
+ disposed: false
484
+
485
+ dispose: ->
486
+ return if @disposed
487
+
488
+ # Fire an event to notify associated views
489
+ @trigger 'dispose', this
490
+
491
+ # Empty the list silently, but do not dispose all models since
492
+ # they might be referenced elsewhere
493
+ @reset [], silent: true
494
+
495
+ # Unbind all global event handlers
496
+ @unsubscribeAllEvents()
497
+
498
+ # Remove all event handlers on this module
499
+ @off()
500
+
501
+ # If the model is a Deferred, reject it
502
+ # This does nothing if it was resolved before
503
+ @reject?()
504
+
505
+ # Remove model constructor reference, internal model lists
506
+ # and event handlers
507
+ properties = [
508
+ 'model',
509
+ 'models', '_byId', '_byCid',
510
+ '_callbacks'
511
+ ]
512
+ delete this[prop] for prop in properties
513
+
514
+ # Finished
515
+ @disposed = true
516
+
517
+ # You’re frozen when your heart’s not open
518
+ Object.freeze? this
519
+
520
+ define 'chaplin/models/model', [
521
+ 'underscore'
522
+ 'backbone'
523
+ 'chaplin/lib/utils'
524
+ 'chaplin/lib/event_broker'
525
+ ], (_, Backbone, utils, EventBroker) ->
526
+ 'use strict'
527
+
528
+ class Model extends Backbone.Model
529
+
530
+ # Mixin an EventBroker
531
+ _(@prototype).extend EventBroker
532
+
533
+ # Mixin a Deferred
534
+ initDeferred: ->
535
+ _(this).extend $.Deferred()
536
+
537
+ # This method is used to get the attributes for the view template
538
+ # and might be overwritten by decorators which cannot create a
539
+ # proper `attributes` getter due to ECMAScript 3 limits.
540
+ getAttributes: ->
541
+ @attributes
542
+
543
+ # Private helper function for serializing attributes recursively,
544
+ # creating objects which delegate to the original attributes
545
+ # in order to protect them from changes.
546
+ serializeAttributes = (model, attributes, modelStack) ->
547
+ # Create a delegator object
548
+ delegator = utils.beget attributes
549
+
550
+ # Add model to stack
551
+ if modelStack
552
+ modelStack.push model
553
+ else
554
+ modelStack = [model]
555
+
556
+ # Map model/collection to their attributes. Create a property
557
+ # on the delegator that shadows the original attribute.
558
+ for key, value of attributes
559
+
560
+ # Handle models
561
+ if value instanceof Backbone.Model
562
+ delegator[key] = serializeModelAttributes value, model, modelStack
563
+
564
+ # Handle collections
565
+ else if value instanceof Backbone.Collection
566
+ serializedModels = []
567
+ for otherModel in value.models
568
+ serializedModels.push(
569
+ serializeModelAttributes(otherModel, model, modelStack)
570
+ )
571
+ delegator[key] = serializedModels
572
+
573
+ # Remove model from stack
574
+ modelStack.pop()
575
+
576
+ # Return the delegator
577
+ delegator
578
+
579
+ # Serialize the attributes of a given model
580
+ # in the context of a given tree
581
+ serializeModelAttributes = (model, currentModel, modelStack) ->
582
+ # Nullify circular references
583
+ if model is currentModel or model in modelStack
584
+ return null
585
+ # Serialize recursively
586
+ attributes = if typeof model.getAttributes is 'function'
587
+ # Chaplin models
588
+ model.getAttributes()
589
+ else
590
+ # Backbone models
591
+ model.attributes
592
+ serializeAttributes model, attributes, modelStack
593
+
594
+ # Return an object which delegates to the attributes
595
+ # (i.e. an object which has the attributes as prototype)
596
+ # so primitive values might be added and altered safely.
597
+ # Map models to their attributes, recursively.
598
+ serialize: ->
599
+ serializeAttributes this, @getAttributes()
600
+
601
+ # Disposal
602
+ # --------
603
+
604
+ disposed: false
605
+
606
+ dispose: ->
607
+ return if @disposed
608
+
609
+ # Fire an event to notify associated collections and views
610
+ @trigger 'dispose', this
611
+
612
+ # Unbind all global event handlers
613
+ @unsubscribeAllEvents()
614
+
615
+ # Remove all event handlers on this module
616
+ @off()
617
+
618
+ # If the model is a Deferred, reject it
619
+ # This does nothing if it was resolved before
620
+ @reject?()
621
+
622
+ # Remove the collection reference, internal attribute hashes
623
+ # and event handlers
624
+ properties = [
625
+ 'collection',
626
+ 'attributes', 'changed'
627
+ '_escapedAttributes', '_previousAttributes',
628
+ '_silent', '_pending',
629
+ '_callbacks'
630
+ ]
631
+ delete this[prop] for prop in properties
632
+
633
+ # Finished
634
+ @disposed = true
635
+
636
+ # You’re frozen when your heart’s not open
637
+ Object.freeze? this
638
+
639
+ define 'chaplin/views/layout', [
640
+ 'underscore'
641
+ 'backbone'
642
+ 'chaplin/lib/utils'
643
+ 'chaplin/lib/event_broker'
644
+ ], (_, Backbone, utils, EventBroker) ->
645
+ 'use strict'
646
+
647
+ # Shortcut to access the DOM manipulation library
648
+ $ = Backbone.$
649
+
650
+ class Layout # This class does not extend View
651
+
652
+ # Borrow the static extend method from Backbone
653
+ @extend = Backbone.Model.extend
654
+
655
+ # Mixin an EventBroker
656
+ _(@prototype).extend EventBroker
657
+
658
+ # The site title used in the document title.
659
+ # This should be set in your app-specific Application class
660
+ # and passed as an option
661
+ title: ''
662
+
663
+ # An hash to register events, like in Backbone.View
664
+ # It is only meant for events that are app-wide
665
+ # independent from any view
666
+ events: {}
667
+
668
+ # Register @el, @$el and @cid for delegating events
669
+ el: document
670
+ $el: $(document)
671
+ cid: 'chaplin-layout'
672
+
673
+ constructor: ->
674
+ @initialize arguments...
675
+
676
+ initialize: (options = {}) ->
677
+ @title = options.title
678
+ @settings = _(options).defaults
679
+ titleTemplate: _.template("<%= subtitle %> \u2013 <%= title %>")
680
+ openExternalToBlank: false
681
+ routeLinks: 'a, .go-to'
682
+ skipRouting: '.noscript'
683
+ # Per default, jump to the top of the page
684
+ scrollTo: [0, 0]
685
+
686
+ @subscribeEvent 'beforeControllerDispose', @hideOldView
687
+ @subscribeEvent 'startupController', @showNewView
688
+ @subscribeEvent '!adjustTitle', @adjustTitle
689
+
690
+ # Set the app link routing
691
+ if @settings.routeLinks
692
+ @startLinkRouting()
693
+
694
+ # Set app wide event handlers
695
+ @delegateEvents()
696
+
697
+ # Take (un)delegateEvents from Backbone
698
+ # -------------------------------------
699
+ delegateEvents: Backbone.View::delegateEvents
700
+ undelegateEvents: Backbone.View::undelegateEvents
701
+
702
+ # Controller startup and disposal
703
+ # -------------------------------
704
+
705
+ # Handler for the global beforeControllerDispose event
706
+ hideOldView: (controller) ->
707
+ # Reset the scroll position
708
+ scrollTo = @settings.scrollTo
709
+ if scrollTo
710
+ window.scrollTo scrollTo[0], scrollTo[1]
711
+
712
+ # Hide the current view
713
+ view = controller.view
714
+ view.$el.hide() if view
715
+
716
+ # Handler for the global startupController event
717
+ # Show the new view
718
+ showNewView: (context) ->
719
+ view = context.controller.view
720
+ view.$el.show() if view
721
+
722
+ # Handler for the global startupController event
723
+ # Change the document title to match the new controller
724
+ # Get the title from the title property of the current controller
725
+ adjustTitle: (subtitle = '') ->
726
+ title = @settings.titleTemplate {@title, subtitle}
727
+
728
+ # Internet Explorer < 9 workaround
729
+ setTimeout (-> document.title = title), 50
730
+
731
+ # Automatic routing of internal links
732
+ # -----------------------------------
733
+
734
+ startLinkRouting: ->
735
+ if @settings.routeLinks
736
+ $(document).on 'click', @settings.routeLinks, @openLink
737
+
738
+ stopLinkRouting: ->
739
+ if @settings.routeLinks
740
+ $(document).off 'click', @settings.routeLinks
741
+
742
+ # Handle all clicks on A elements and try to route them internally
743
+ openLink: (event) =>
744
+ return if utils.modifierKeyPressed(event)
745
+
746
+ el = event.currentTarget
747
+ $el = $(el)
748
+ isAnchor = el.nodeName is 'A'
749
+
750
+ # Get the href and perform checks on it
751
+ href = $el.attr('href') or $el.data('href') or null
752
+
753
+ # Basic href checks
754
+ return if href is null or href is undefined or
755
+ # Technically an empty string is a valid relative URL
756
+ # but it doesn’t make sense to route it.
757
+ href is '' or
758
+ # Exclude fragment links
759
+ href.charAt(0) is '#'
760
+
761
+ # Checks for A elements
762
+ return if isAnchor and (
763
+ # Exclude links marked as external
764
+ $el.attr('target') is '_blank' or
765
+ $el.attr('rel') is 'external' or
766
+ # Exclude links to non-HTTP ressources
767
+ el.protocol not in ['http:', 'https:', 'file:']
768
+ )
769
+
770
+ # Apply skipRouting option
771
+ skipRouting = @settings.skipRouting
772
+ type = typeof skipRouting
773
+ return if type is 'function' and not skipRouting(href, el) or
774
+ type is 'string' and $el.is skipRouting
775
+
776
+ # Handle external links
777
+ internal = not isAnchor or el.hostname in [location.hostname, '']
778
+ unless internal
779
+ if @settings.openExternalToBlank
780
+ # Open external links normally in a new tab
781
+ event.preventDefault()
782
+ window.open el.href
783
+ return
784
+
785
+ if isAnchor
786
+ path = el.pathname
787
+ queryString = el.search.substring 1
788
+ # Append leading slash for IE8
789
+ path = "/#{path}" if path.charAt(0) isnt '/'
790
+ else
791
+ [path, queryString] = href.split '?'
792
+ queryString ?= ''
793
+
794
+ # Create routing options and callback
795
+ options = {queryString}
796
+ callback = (routed) ->
797
+ # Prevent default handling if the URL could be routed
798
+ if routed
799
+ event.preventDefault()
800
+ else unless isAnchor
801
+ location.href = path
802
+ return
803
+
804
+ # Pass to the router, try to route the path internally
805
+ @publishEvent '!router:route', path, options, callback
806
+
807
+ return
808
+
809
+ # Disposal
810
+ # --------
811
+
812
+ disposed: false
813
+
814
+ dispose: ->
815
+ return if @disposed
816
+
817
+ @stopLinkRouting()
818
+ @unsubscribeAllEvents()
819
+ @undelegateEvents()
820
+
821
+ delete @title
822
+
823
+ @disposed = true
824
+
825
+ # You’re frozen when your heart’s not open
826
+ Object.freeze? this
827
+
828
+ define 'chaplin/views/view', [
829
+ 'underscore'
830
+ 'backbone'
831
+ 'chaplin/lib/utils'
832
+ 'chaplin/lib/event_broker'
833
+ 'chaplin/models/model'
834
+ 'chaplin/models/collection'
835
+ ], (_, Backbone, utils, EventBroker, Model, Collection) ->
836
+ 'use strict'
837
+
838
+ # Shortcut to access the DOM manipulation library
839
+ $ = Backbone.$
840
+
841
+ class View extends Backbone.View
842
+
843
+ # Mixin an EventBroker
844
+ _(@prototype).extend EventBroker
845
+
846
+ # Automatic rendering
847
+ # -------------------
848
+
849
+ # Flag whether to render the view automatically on initialization.
850
+ # As an alternative you might pass a `render` option to the constructor.
851
+ autoRender: false
852
+
853
+ # Automatic inserting into DOM
854
+ # ----------------------------
855
+
856
+ # View container element
857
+ # Set this property in a derived class to specify the container element.
858
+ # Normally this is a selector string but it might also be an element or
859
+ # jQuery object.
860
+ # The view is automatically inserted into the container when it’s rendered.
861
+ # As an alternative you might pass a `container` option to the constructor.
862
+ container: null
863
+
864
+ # Method which is used for adding the view to the DOM
865
+ # Like jQuery’s `html`, `prepend`, `append`, `after`, `before` etc.
866
+ containerMethod: 'append'
867
+
868
+ # Subviews
869
+ # --------
870
+
871
+ # List of subviews
872
+ subviews: null
873
+ subviewsByName: null
874
+
875
+ constructor: (options) ->
876
+ # Wrap `initialize` so `afterInitialize` is called afterwards
877
+ # Only wrap if there is an overriding method, otherwise we
878
+ # can call the `after-` method directly
879
+ unless @initialize is View::initialize
880
+ utils.wrapMethod this, 'initialize'
881
+
882
+ # Wrap `render` so `afterRender` is called afterwards
883
+ if @render is View::render
884
+ @render = _(@render).bind this
885
+ else
886
+ utils.wrapMethod this, 'render'
887
+
888
+ # Copy some options to instance properties
889
+ if options
890
+ _(this).extend _.pick options, ['autoRender', 'container', 'containerMethod']
891
+
892
+ # Call Backbone’s constructor
893
+ super
894
+
895
+ # Inheriting classes must call `super` in their `initialize` method to
896
+ # properly inflate subviews and set up options
897
+ initialize: (options) ->
898
+ # No super call here, Backbone’s `initialize` is a no-op
899
+
900
+ # Initialize subviews
901
+ @subviews = []
902
+ @subviewsByName = {}
903
+
904
+ # Listen for disposal of the model or collection.
905
+ # If the model is disposed, automatically dispose the associated view
906
+ @listenTo @model, 'dispose', @dispose if @model
907
+ @listenTo @collection, 'dispose', @dispose if @collection
908
+
909
+ # Call `afterInitialize` if `initialize` was not wrapped
910
+ unless @initializeIsWrapped
911
+ @afterInitialize()
912
+
913
+ # This method is called after a specific `initialize` of a derived class
914
+ afterInitialize: ->
915
+ # Render automatically if set by options or instance property
916
+ @render() if @autoRender
917
+
918
+ # User input event handling
919
+ # -------------------------
920
+
921
+ # Event handling using event delegation
922
+ # Register a handler for a specific event type
923
+ # For the whole view:
924
+ # delegate(eventType, handler)
925
+ # e.g.
926
+ # @delegate('click', @clicked)
927
+ # For an element in the passing a selector:
928
+ # delegate(eventType, selector, handler)
929
+ # e.g.
930
+ # @delegate('click', 'button.confirm', @confirm)
931
+ delegate: (eventType, second, third) ->
932
+ if typeof eventType isnt 'string'
933
+ throw new TypeError 'View#delegate: first argument must be a string'
934
+
935
+ if arguments.length is 2
936
+ handler = second
937
+ else if arguments.length is 3
938
+ selector = second
939
+ if typeof selector isnt 'string'
940
+ throw new TypeError 'View#delegate: ' +
941
+ 'second argument must be a string'
942
+ handler = third
943
+ else
944
+ throw new TypeError 'View#delegate: ' +
945
+ 'only two or three arguments are allowed'
946
+
947
+ if typeof handler isnt 'function'
948
+ throw new TypeError 'View#delegate: ' +
949
+ 'handler argument must be function'
950
+
951
+ # Add an event namespace
952
+ list = ("#{event}.delegate#{@cid}" for event in eventType.split(' '))
953
+ events = list.join(' ')
954
+
955
+ # Bind the handler to the view
956
+ handler = _(handler).bind(this)
957
+
958
+ if selector
959
+ # Register handler
960
+ @$el.on events, selector, handler
961
+ else
962
+ # Register handler
963
+ @$el.on events, handler
964
+
965
+ # Return the bound handler
966
+ handler
967
+
968
+ # Copy of original backbone method without `undelegateEvents` call.
969
+ _delegateEvents: (events) ->
970
+ # Call Backbone.delegateEvents on all superclasses events.
971
+ return unless events or (events = getValue(this, 'events'))
972
+ for key of events
973
+ method = events[key]
974
+ method = this[method] unless _.isFunction(method)
975
+ unless method
976
+ throw new Error("Method '#{events[key]}' does not exist")
977
+ match = key.match(/^(\S+)\s*(.*)$/)
978
+ eventName = match[1]
979
+ selector = match[2]
980
+ method = _.bind(method, this)
981
+ eventName += ".delegateEvents#{@cid}"
982
+ if selector is ''
983
+ @$el.bind eventName, method
984
+ else
985
+ @$el.delegate selector, eventName, method
986
+
987
+ # Override Backbones method to combine the events
988
+ # of the parent view if it exists.
989
+ delegateEvents: ->
990
+ @undelegateEvents()
991
+ for events in utils.getAllPropertyVersions this, 'events'
992
+ @_delegateEvents events
993
+ return
994
+
995
+ # Remove all handlers registered with @delegate.
996
+ undelegate: ->
997
+ @$el.unbind ".delegate#{@cid}"
998
+
999
+ # Subviews
1000
+ # --------
1001
+
1002
+ # Getting or adding a subview
1003
+ subview: (name, view) ->
1004
+ if name and view
1005
+ # Add the subview, ensure it’s unique
1006
+ @removeSubview name
1007
+ @subviews.push view
1008
+ @subviewsByName[name] = view
1009
+ view
1010
+ else if name
1011
+ # Get and return the subview by the given name
1012
+ @subviewsByName[name]
1013
+
1014
+ # Removing a subview
1015
+ removeSubview: (nameOrView) ->
1016
+ return unless nameOrView
1017
+
1018
+ if typeof nameOrView is 'string'
1019
+ # Name given, search for a subview by name
1020
+ name = nameOrView
1021
+ view = @subviewsByName[name]
1022
+ else
1023
+ # View instance given, search for the corresponding name
1024
+ view = nameOrView
1025
+ for otherName, otherView of @subviewsByName
1026
+ if view is otherView
1027
+ name = otherName
1028
+ break
1029
+
1030
+ # Break if no view and name were found
1031
+ return unless name and view and view.dispose
1032
+
1033
+ # Dispose the view
1034
+ view.dispose()
1035
+
1036
+ # Remove the subview from the lists
1037
+ index = _(@subviews).indexOf(view)
1038
+ if index > -1
1039
+ @subviews.splice index, 1
1040
+ delete @subviewsByName[name]
1041
+
1042
+ # Rendering
1043
+ # ---------
1044
+
1045
+ # Get the model/collection data for the templating function
1046
+ # Uses optimized Chaplin serialization if available.
1047
+ getTemplateData: ->
1048
+ templateData = if @model
1049
+ if @model instanceof Model
1050
+ @model.serialize()
1051
+ else
1052
+ utils.beget @model.attributes
1053
+ else if @collection
1054
+ # Collection: Serialize all models.
1055
+ items = if @collection instanceof Collection
1056
+ @collection.serialize()
1057
+ else
1058
+ @collection.map (model) ->
1059
+ utils.beget model.attributes
1060
+ {items}
1061
+ else
1062
+ # Empty object.
1063
+ {}
1064
+
1065
+ modelOrCollection = @model or @collection
1066
+ if modelOrCollection
1067
+ # If the model/collection is a Deferred, add a `resolved` flag,
1068
+ # but only if it’s not present yet
1069
+ if typeof modelOrCollection.state is 'function' and
1070
+ not ('resolved' of templateData)
1071
+ templateData.resolved = modelOrCollection.state() is 'resolved'
1072
+
1073
+ # If the model/collection is a SyncMachine, add a `synced` flag,
1074
+ # but only if it’s not present yet
1075
+ if typeof modelOrCollection.isSynced is 'function' and
1076
+ not ('synced' of templateData)
1077
+ templateData.synced = modelOrCollection.isSynced()
1078
+
1079
+ templateData
1080
+
1081
+ # Returns the compiled template function
1082
+ getTemplateFunction: ->
1083
+ # Chaplin doesn’t define how you load and compile templates in order to
1084
+ # render views. The example application uses Handlebars and RequireJS
1085
+ # to load and compile templates on the client side. See the derived
1086
+ # View class in the example application:
1087
+ # https://github.com/chaplinjs/facebook-example/blob/master/coffee/views/base/view.coffee
1088
+ #
1089
+ # If you precompile templates to JavaScript functions on the server,
1090
+ # you might just return a reference to that function.
1091
+ # Several precompilers create a global `JST` hash which stores the
1092
+ # template functions. You can get the function by the template name:
1093
+ # JST[@templateName]
1094
+
1095
+ throw new Error 'View#getTemplateFunction must be overridden'
1096
+
1097
+ # Main render function
1098
+ # This method is bound to the instance in the constructor (see above)
1099
+ render: ->
1100
+ # Do not render if the object was disposed
1101
+ # (render might be called as an event handler which wasn’t
1102
+ # removed correctly)
1103
+ return false if @disposed
1104
+
1105
+ templateFunc = @getTemplateFunction()
1106
+ if typeof templateFunc is 'function'
1107
+
1108
+ # Call the template function passing the template data
1109
+ html = templateFunc @getTemplateData()
1110
+
1111
+ # Replace HTML
1112
+ # ------------
1113
+
1114
+ # This is a workaround for an apparent issue with jQuery 1.7’s
1115
+ # innerShiv feature. Using @$el.html(html) caused issues with
1116
+ # HTML5-only tags in IE7 and IE8.
1117
+ @$el.empty().append html
1118
+
1119
+ # Call `afterRender` if `render` was not wrapped
1120
+ @afterRender() unless @renderIsWrapped
1121
+
1122
+ # Return the view
1123
+ this
1124
+
1125
+ # This method is called after a specific `render` of a derived class
1126
+ afterRender: ->
1127
+ # Automatically append to DOM if the container element is set
1128
+ if @container
1129
+ # Append the view to the DOM
1130
+ $(@container)[@containerMethod] @el
1131
+ # Trigger an event
1132
+ @trigger 'addedToDOM'
1133
+
1134
+ # Disposal
1135
+ # --------
1136
+
1137
+ disposed: false
1138
+
1139
+ dispose: ->
1140
+ return if @disposed
1141
+
1142
+ throw new Error('Your `initialize` method must include a super call to
1143
+ Chaplin `initialize`') unless @subviews?
1144
+
1145
+ # Dispose subviews
1146
+ subview.dispose() for subview in @subviews
1147
+
1148
+ # Unbind handlers of global events
1149
+ @unsubscribeAllEvents()
1150
+
1151
+ # Unbind all referenced handlers
1152
+ @stopListening()
1153
+
1154
+ # Remove all event handlers on this module
1155
+ @off()
1156
+
1157
+ # Remove the topmost element from DOM. This also removes all event
1158
+ # handlers from the element and all its children.
1159
+ @$el.remove()
1160
+
1161
+ # Remove element references, options,
1162
+ # model/collection references and subview lists
1163
+ properties = [
1164
+ 'el', '$el',
1165
+ 'options', 'model', 'collection',
1166
+ 'subviews', 'subviewsByName',
1167
+ '_callbacks'
1168
+ ]
1169
+ delete this[prop] for prop in properties
1170
+
1171
+ # Finished
1172
+ @disposed = true
1173
+
1174
+ # You’re frozen when your heart’s not open
1175
+ Object.freeze? this
1176
+
1177
+ define 'chaplin/views/collection_view', [
1178
+ 'backbone'
1179
+ 'underscore'
1180
+ 'chaplin/views/view'
1181
+ ], (Backbone, _, View) ->
1182
+ 'use strict'
1183
+
1184
+ # Shortcut to access the DOM manipulation library
1185
+ $ = Backbone.$
1186
+
1187
+ # General class for rendering Collections.
1188
+ # Derive this class and declare at least `itemView` or override
1189
+ # `getView`. `getView` gets an item model and should instantiate
1190
+ # and return a corresponding item view.
1191
+ class CollectionView extends View
1192
+
1193
+ # Configuration options
1194
+ # ---------------------
1195
+
1196
+ # These options may be overwritten in derived classes.
1197
+
1198
+ # A class of item in collection.
1199
+ # This property has to be overridden by a derived class.
1200
+ itemView: null
1201
+
1202
+ # Automatic rendering
1203
+
1204
+ # Per default, render the view itself and all items on creation
1205
+ autoRender: true
1206
+ renderItems: true
1207
+
1208
+ # Animation
1209
+
1210
+ # When new items are added, their views are faded in.
1211
+ # Animation duration in milliseconds (set to 0 to disable fade in)
1212
+ animationDuration: 500
1213
+
1214
+ # By default, fading in is done by javascript function which can be
1215
+ # slow on mobile devices. CSS animations are faster,
1216
+ # but require user’s manual definitions.
1217
+ # CSS classes used are: animated-item-view, animated-item-view-end.
1218
+ useCssAnimation: false
1219
+
1220
+ # Selectors and Elements
1221
+
1222
+ # A collection view may have a template and use one of its child elements
1223
+ # as the container of the item views. If you specify `listSelector`, the
1224
+ # item views will be appended to this element. If empty, $el is used.
1225
+ listSelector: null
1226
+
1227
+ # The actual element which is fetched using `listSelector`
1228
+ $list: null
1229
+
1230
+ # Selector for a fallback element which is shown if the collection is empty.
1231
+ fallbackSelector: null
1232
+
1233
+ # The actual element which is fetched using `fallbackSelector`
1234
+ $fallback: null
1235
+
1236
+ # Selector for a loading indicator element which is shown
1237
+ # while the collection is syncing.
1238
+ loadingSelector: null
1239
+
1240
+ # The actual element which is fetched using `loadingSelector`
1241
+ $loading: null
1242
+
1243
+ # Selector which identifies child elements belonging to collection
1244
+ # If empty, all children of $list are considered
1245
+ itemSelector: null
1246
+
1247
+ # Filtering
1248
+
1249
+ # The filter function, if any
1250
+ filterer: null
1251
+
1252
+ # A function that will be executed after each filter.
1253
+ # Hides excluded items by default.
1254
+ filterCallback: (view, included) ->
1255
+ view.$el.stop(true, true).toggle included
1256
+
1257
+ # View lists
1258
+
1259
+ # Track a list of the visible views
1260
+ visibleItems: null
1261
+
1262
+ # Constructor
1263
+ # -----------
1264
+
1265
+ constructor: (options) ->
1266
+ # Apply options to view instance
1267
+ if (options)
1268
+ _(this).extend _.pick options, ['renderItems', 'itemView']
1269
+
1270
+ super
1271
+
1272
+ # Initialization
1273
+ # --------------
1274
+
1275
+ initialize: (options = {}) ->
1276
+ super
1277
+
1278
+ # Initialize list for visible items
1279
+ @visibleItems = []
1280
+
1281
+ # Start observing the collection
1282
+ @addCollectionListeners()
1283
+
1284
+ # Apply a filter if one provided
1285
+ @filter options.filterer if options.filterer?
1286
+
1287
+ # Binding of collection listeners
1288
+ addCollectionListeners: ->
1289
+ @listenTo @collection, 'add', @itemAdded
1290
+ @listenTo @collection, 'remove', @itemRemoved
1291
+ @listenTo @collection, 'reset sort', @itemsResetted
1292
+
1293
+ # Rendering
1294
+ # ---------
1295
+
1296
+ # Override View#getTemplateData, don’t serialize collection items here.
1297
+ getTemplateData: ->
1298
+ templateData = {length: @collection.length}
1299
+
1300
+ # If the collection is a Deferred, add a `resolved` flag
1301
+ if typeof @collection.state is 'function'
1302
+ templateData.resolved = @collection.state() is 'resolved'
1303
+
1304
+ # If the collection is a SyncMachine, add a `synced` flag
1305
+ if typeof @collection.isSynced is 'function'
1306
+ templateData.synced = @collection.isSynced()
1307
+
1308
+ templateData
1309
+
1310
+ # In contrast to normal views, a template is not mandatory
1311
+ # for CollectionViews. Provide an empty `getTemplateFunction`.
1312
+ getTemplateFunction: ->
1313
+
1314
+ # Main render method (should be called only once)
1315
+ render: ->
1316
+ super
1317
+
1318
+ # Set the $list property with the actual list container
1319
+ @$list = if @listSelector then @$(@listSelector) else @$el
1320
+
1321
+ @initFallback()
1322
+ @initLoadingIndicator()
1323
+
1324
+ # Render all items
1325
+ @renderAllItems() if @renderItems
1326
+
1327
+ # Adding / Removing
1328
+ # -----------------
1329
+
1330
+ # When an item is added, create a new view and insert it
1331
+ itemAdded: (item, collection, options = {}) =>
1332
+ @renderAndInsertItem item, options.index
1333
+
1334
+ # When an item is removed, remove the corresponding view from DOM and caches
1335
+ itemRemoved: (item) =>
1336
+ @removeViewForItem item
1337
+
1338
+ # When all items are resetted, render all anew
1339
+ itemsResetted: =>
1340
+ @renderAllItems()
1341
+
1342
+ # Fallback message when the collection is empty
1343
+ # ---------------------------------------------
1344
+
1345
+ initFallback: ->
1346
+ return unless @fallbackSelector
1347
+
1348
+ # Set the $fallback property
1349
+ @$fallback = @$(@fallbackSelector)
1350
+
1351
+ # Listen for visible items changes
1352
+ @on 'visibilityChange', @showHideFallback
1353
+
1354
+ # Listen for sync events on the collection
1355
+ @listenTo @collection, 'syncStateChange', @showHideFallback
1356
+
1357
+ # Set visibility initially
1358
+ @showHideFallback()
1359
+
1360
+ # Show fallback if no item is visible and the collection is synced
1361
+ showHideFallback: =>
1362
+ visible = @visibleItems.length is 0 and (
1363
+ if typeof @collection.isSynced is 'function'
1364
+ # Collection is a SyncMachine
1365
+ @collection.isSynced()
1366
+ else
1367
+ # Assume it is synced
1368
+ true
1369
+ )
1370
+ @$fallback.toggle visible
1371
+
1372
+ # Loading indicator
1373
+ # -----------------
1374
+
1375
+ initLoadingIndicator: ->
1376
+ # The loading indicator only works for Collections
1377
+ # which are SyncMachines.
1378
+ return unless @loadingSelector and
1379
+ typeof @collection.isSyncing is 'function'
1380
+
1381
+ # Set the $loading property
1382
+ @$loading = @$(@loadingSelector)
1383
+
1384
+ # Listen for sync events on the collection
1385
+ @listenTo @collection, 'syncStateChange', @showHideLoadingIndicator
1386
+
1387
+ # Set visibility initially
1388
+ @showHideLoadingIndicator()
1389
+
1390
+ showHideLoadingIndicator: ->
1391
+ # Only show the loading indicator if the collection is empty.
1392
+ # Otherwise loading more items in order to append them would
1393
+ # show the loading indicator. If you want the indicator to
1394
+ # show up in this case, you need to overwrite this method to
1395
+ # disable the check.
1396
+ visible = @collection.length is 0 and @collection.isSyncing()
1397
+ @$loading.toggle visible
1398
+
1399
+ # Filtering
1400
+ # ---------
1401
+
1402
+ # Filters only child item views from all current subviews.
1403
+ getItemViews: ->
1404
+ itemViews = {}
1405
+ for name, view of @subviewsByName when name.slice(0, 9) is 'itemView:'
1406
+ itemViews[name.slice(9)] = view
1407
+ itemViews
1408
+
1409
+ # Applies a filter to the collection view.
1410
+ # Expects an iterator function as first parameter
1411
+ # which need to return true or false.
1412
+ # Optional filter callback which is called to
1413
+ # show/hide the view or mark it otherwise as filtered.
1414
+ filter: (filterer, filterCallback) ->
1415
+ # Save the filterer and filterCallback functions
1416
+ @filterer = filterer
1417
+ @filterCallback = filterCallback if filterCallback
1418
+ filterCallback ?= @filterCallback
1419
+
1420
+ # Show/hide existing views
1421
+ unless _(@getItemViews()).isEmpty()
1422
+ for item, index in @collection.models
1423
+
1424
+ # Apply filter to the item
1425
+ included = if typeof filterer is 'function'
1426
+ filterer item, index
1427
+ else
1428
+ true
1429
+
1430
+ # Show/hide the view accordingly
1431
+ view = @subview "itemView:#{item.cid}"
1432
+ # A view has not been created for this item yet
1433
+ unless view
1434
+ throw new Error 'CollectionView#filter: ' +
1435
+ "no view found for #{item.cid}"
1436
+
1437
+ # Show/hide or mark the view accordingly
1438
+ @filterCallback view, included
1439
+
1440
+ # Update visibleItems list, but do not trigger an event immediately
1441
+ @updateVisibleItems view.model, included, false
1442
+
1443
+ # Trigger a combined `visibilityChange` event
1444
+ @trigger 'visibilityChange', @visibleItems
1445
+
1446
+ # Item view rendering
1447
+ # -------------------
1448
+
1449
+ # Render and insert all items
1450
+ renderAllItems: =>
1451
+ items = @collection.models
1452
+
1453
+ # Reset visible items
1454
+ @visibleItems = []
1455
+
1456
+ # Collect remaining views
1457
+ remainingViewsByCid = {}
1458
+ for item in items
1459
+ view = @subview "itemView:#{item.cid}"
1460
+ if view
1461
+ # View remains
1462
+ remainingViewsByCid[item.cid] = view
1463
+
1464
+ # Remove old views of items not longer in the list
1465
+ for own cid, view of @getItemViews() when cid not of remainingViewsByCid
1466
+ # Remove the view
1467
+ @removeSubview "itemView:#{cid}"
1468
+
1469
+ # Re-insert remaining items; render and insert new items
1470
+ for item, index in items
1471
+ # Check if view was already created
1472
+ view = @subview "itemView:#{item.cid}"
1473
+ if view
1474
+ # Re-insert the view
1475
+ @insertView item, view, index, false
1476
+ else
1477
+ # Create a new view, render and insert it
1478
+ @renderAndInsertItem item, index
1479
+
1480
+ # If no view was created, trigger `visibilityChange` event manually
1481
+ unless items.length
1482
+ @trigger 'visibilityChange', @visibleItems
1483
+
1484
+ # Render the view for an item
1485
+ renderAndInsertItem: (item, index) ->
1486
+ view = @renderItem item
1487
+ @insertView item, view, index
1488
+
1489
+ # Instantiate and render an item using the `viewsByCid` hash as a cache
1490
+ renderItem: (item) ->
1491
+ # Get the existing view
1492
+ view = @subview "itemView:#{item.cid}"
1493
+
1494
+ # Instantiate a new view if necessary
1495
+ unless view
1496
+ view = @getView item
1497
+ # Save the view in the subviews
1498
+ @subview "itemView:#{item.cid}", view
1499
+
1500
+ # Render in any case
1501
+ view.render()
1502
+
1503
+ view
1504
+
1505
+ # Returns an instance of the view class. Override this
1506
+ # method to use several item view constructors depending
1507
+ # on the model type or data.
1508
+ getView: (model) ->
1509
+ if @itemView
1510
+ new @itemView {model}
1511
+ else
1512
+ throw new Error 'The CollectionView#itemView property ' +
1513
+ 'must be defined or the getView() must be overridden.'
1514
+
1515
+ # Inserts a view into the list at the proper position
1516
+ insertView: (item, view, index = null, enableAnimation = true) ->
1517
+ # Get the insertion offset
1518
+ position = if typeof index is 'number'
1519
+ index
1520
+ else
1521
+ @collection.indexOf item
1522
+
1523
+ # Is the item included in the filter?
1524
+ included = if typeof @filterer is 'function'
1525
+ @filterer item, position
1526
+ else
1527
+ true
1528
+
1529
+ # Get the view’s top element
1530
+ viewEl = view.el
1531
+ $viewEl = view.$el
1532
+
1533
+ if included
1534
+ # Make view transparent if animation is enabled
1535
+ if enableAnimation
1536
+ if @useCssAnimation
1537
+ $viewEl.addClass 'animated-item-view'
1538
+ else
1539
+ $viewEl.css 'opacity', 0
1540
+ else
1541
+ # Hide the view if it’s filtered
1542
+ @filterCallback view, included
1543
+
1544
+ # Insert the view into the list
1545
+ $list = @$list
1546
+
1547
+ # Get the children which originate from item views
1548
+ children = if @itemSelector
1549
+ $list.children @itemSelector
1550
+ else
1551
+ $list.children()
1552
+
1553
+ # Check if it needs to be inserted
1554
+ unless children.get(position) is viewEl
1555
+ length = children.length
1556
+ if length is 0 or position is length
1557
+ # Insert at the end
1558
+ $list.append viewEl
1559
+ else
1560
+ # Insert at the right position
1561
+ if position is 0
1562
+ $next = children.eq position
1563
+ $next.before viewEl
1564
+ else
1565
+ $previous = children.eq position - 1
1566
+ $previous.after viewEl
1567
+
1568
+ # Tell the view that it was added to its parent.
1569
+ view.trigger 'addedToParent'
1570
+
1571
+ # Update the list of visible items, trigger a `visibilityChange` event
1572
+ @updateVisibleItems item, included
1573
+
1574
+ # Fade the view in if it was made transparent before
1575
+ if enableAnimation and included
1576
+ if @useCssAnimation
1577
+ # Wait for DOM state change.
1578
+ setTimeout =>
1579
+ $viewEl.addClass 'animated-item-view-end'
1580
+ , 0
1581
+ else
1582
+ $viewEl.animate {opacity: 1}, @animationDuration
1583
+
1584
+ return
1585
+
1586
+ # Remove the view for an item
1587
+ removeViewForItem: (item) ->
1588
+ # Remove item from visibleItems list, trigger a `visibilityChange` event
1589
+ @updateVisibleItems item, false
1590
+ @removeSubview "itemView:#{item.cid}"
1591
+
1592
+ # List of visible items
1593
+ # ---------------------
1594
+
1595
+ # Update visibleItems list and trigger a `visibilityChanged` event
1596
+ # if an item changed its visibility
1597
+ updateVisibleItems: (item, includedInFilter, triggerEvent = true) ->
1598
+ visibilityChanged = false
1599
+
1600
+ visibleItemsIndex = _(@visibleItems).indexOf item
1601
+ includedInVisibleItems = visibleItemsIndex > -1
1602
+
1603
+ if includedInFilter and not includedInVisibleItems
1604
+ # Add item to the visible items list
1605
+ @visibleItems.push item
1606
+ visibilityChanged = true
1607
+
1608
+ else if not includedInFilter and includedInVisibleItems
1609
+ # Remove item from the visible items list
1610
+ @visibleItems.splice visibleItemsIndex, 1
1611
+ visibilityChanged = true
1612
+
1613
+ # Trigger a `visibilityChange` event if the visible items changed
1614
+ if visibilityChanged and triggerEvent
1615
+ @trigger 'visibilityChange', @visibleItems
1616
+
1617
+ visibilityChanged
1618
+
1619
+ # Disposal
1620
+ # --------
1621
+
1622
+ dispose: ->
1623
+ return if @disposed
1624
+
1625
+ # Remove jQuery objects, item view cache and visible items list
1626
+ properties = [
1627
+ '$list', '$fallback', '$loading',
1628
+ 'visibleItems'
1629
+ ]
1630
+ delete this[prop] for prop in properties
1631
+
1632
+ # Self-disposal
1633
+ super
1634
+
1635
+ define 'chaplin/lib/route', [
1636
+ 'underscore'
1637
+ 'backbone'
1638
+ 'chaplin/lib/event_broker'
1639
+ 'chaplin/controllers/controller'
1640
+ ], (_, Backbone, EventBroker, Controller) ->
1641
+ 'use strict'
1642
+
1643
+ class Route
1644
+
1645
+ # Borrow the static extend method from Backbone
1646
+ @extend = Backbone.Model.extend
1647
+
1648
+ # Mixin an EventBroker
1649
+ _(@prototype).extend EventBroker
1650
+
1651
+ reservedParams = ['path', 'changeURL']
1652
+ # Taken from Backbone.Router
1653
+ escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g
1654
+
1655
+ queryStringFieldSeparator = '&'
1656
+ queryStringValueSeparator = '='
1657
+
1658
+ # Create a route for a URL pattern and a controller action
1659
+ # e.g. new Route '/users/:id', 'users#show'
1660
+ constructor: (@pattern, @controller, @action, @options = {}) ->
1661
+ # Store the name on the route if given
1662
+ @name = @options.name if @options.name?
1663
+
1664
+ # Initialize list of :params which the route will use.
1665
+ @paramNames = []
1666
+
1667
+ # Check if the action is a reserved name
1668
+ if _(Controller.prototype).has @action
1669
+ throw new Error 'Route: You should not use existing controller properties as action names'
1670
+
1671
+ @createRegExp()
1672
+
1673
+ reverse: (params) ->
1674
+ url = @pattern
1675
+ # TODO: add support for regular expressions in reverser.
1676
+ return false if _.isRegExp url
1677
+ notEnoughParams = 'Route#reverse: Not enough parameters to reverse'
1678
+
1679
+ if _.isArray params
1680
+ # Ensure we have enough parameters
1681
+ throw new Error notEnoughParams if params.length < @paramNames.length
1682
+
1683
+ index = 0
1684
+ url = url.replace /[:*][^\/\?]+/g, (match) ->
1685
+ result = params[index]
1686
+ index += 1
1687
+ result
1688
+ else
1689
+ # From a params hash; we need to be able to return
1690
+ # the actual URL this route represents
1691
+ # Iterate and attempt to replace params in pattern
1692
+ for name in @paramNames
1693
+ value = params[name]
1694
+ throw new Error notEnoughParams if value is undefined
1695
+ url = url.replace ///[:*]#{name}///g, value
1696
+
1697
+ # If the url tests out good; return the url; else, false
1698
+ if @test url then url else false
1699
+
1700
+ createRegExp: ->
1701
+ if _.isRegExp @pattern
1702
+ @regExp = @pattern
1703
+ @paramNames = @options.names if _.isArray @options.names
1704
+ return
1705
+
1706
+ pattern = @pattern
1707
+ # Escape magic characters
1708
+ .replace(escapeRegExp, '\\$&')
1709
+ # Replace named parameters, collecting their names
1710
+ .replace(/(?::|\*)(\w+)/g, @addParamName)
1711
+
1712
+ # Create the actual regular expression
1713
+ # Match until the end of the URL or the begin of query string
1714
+ @regExp = ///^#{pattern}(?=\?|$)///
1715
+
1716
+ addParamName: (match, paramName) =>
1717
+ # Test if parameter name is reserved
1718
+ if _(reservedParams).include(paramName)
1719
+ throw new Error "Route#addParamName: parameter name #{paramName} is reserved"
1720
+ # Save parameter name
1721
+ @paramNames.push paramName
1722
+ # Replace with a character class
1723
+ if match.charAt(0) is ':'
1724
+ # Regexp for :foo
1725
+ '([^\/\?]+)'
1726
+ else
1727
+ # Regexp for *foo
1728
+ '(.*?)'
1729
+
1730
+ # Test if the route matches to a path (called by Backbone.History#loadUrl)
1731
+ test: (path) ->
1732
+ # Test the main RegExp
1733
+ matched = @regExp.test path
1734
+ return false unless matched
1735
+
1736
+ # Apply the parameter constraints
1737
+ constraints = @options.constraints
1738
+ if constraints
1739
+ params = @extractParams path
1740
+ for own name, constraint of constraints
1741
+ unless constraint.test(params[name])
1742
+ return false
1743
+
1744
+ return true
1745
+
1746
+ # The handler which is called by Backbone.History when the route matched.
1747
+ # It is also called by Router#route which might pass options.
1748
+ handler: (path, options = {}) =>
1749
+ # If no query string was passed, use the current
1750
+ queryString = options.queryString ? @getCurrentQueryString()
1751
+
1752
+ # Build params hash
1753
+ params = @buildParams path, queryString
1754
+
1755
+ # Add a `path` routing option with the whole path match
1756
+ options.path = path
1757
+
1758
+ # Publish a global matchRoute event passing the route and the params
1759
+ # Original options hash forwarded to allow further forwarding to backbone
1760
+ @publishEvent 'matchRoute', this, params, options
1761
+
1762
+ # Returns the query string for the current document
1763
+ getCurrentQueryString: ->
1764
+ location.search.substring 1
1765
+
1766
+ # Create a proper Rails-like params hash, not an array like Backbone
1767
+ buildParams: (path, queryString) ->
1768
+ _.extend {},
1769
+ # Add params from query string
1770
+ @extractQueryParams(queryString),
1771
+ # Add named params from pattern matches
1772
+ @extractParams(path),
1773
+ # Add additional params from options
1774
+ # (they might overwrite params extracted from URL)
1775
+ @options.params
1776
+
1777
+ # Extract named parameters from the URL path
1778
+ extractParams: (path) ->
1779
+ params = {}
1780
+
1781
+ # Apply the regular expression
1782
+ matches = @regExp.exec path
1783
+
1784
+ # Fill the hash using the paramNames and the matches
1785
+ for match, index in matches.slice(1)
1786
+ paramName = if @paramNames.length then @paramNames[index] else index
1787
+ params[paramName] = match
1788
+
1789
+ params
1790
+
1791
+ # Extract parameters from the query string
1792
+ extractQueryParams: (queryString) ->
1793
+ params = {}
1794
+ return params unless queryString
1795
+ pairs = queryString.split queryStringFieldSeparator
1796
+ for pair in pairs
1797
+ continue unless pair.length
1798
+ [field, value] = pair.split queryStringValueSeparator
1799
+ continue unless field.length
1800
+ field = decodeURIComponent field
1801
+ value = decodeURIComponent value
1802
+ current = params[field]
1803
+ if current
1804
+ # Handle multiple params with same name:
1805
+ # Aggregate them in an array
1806
+ if current.push
1807
+ # Add the existing array
1808
+ current.push value
1809
+ else
1810
+ # Create a new array
1811
+ params[field] = [current, value]
1812
+ else
1813
+ params[field] = value
1814
+
1815
+ params
1816
+
1817
+ define 'chaplin/lib/router', [
1818
+ 'underscore'
1819
+ 'backbone'
1820
+ 'chaplin/mediator'
1821
+ 'chaplin/lib/event_broker'
1822
+ 'chaplin/lib/route'
1823
+ ], (_, Backbone, mediator, EventBroker, Route) ->
1824
+ 'use strict'
1825
+
1826
+ # The router which is a replacement for Backbone.Router.
1827
+ # Like the standard router, it creates a Backbone.History
1828
+ # instance and registers routes on it.
1829
+
1830
+ class Router # This class does not extend Backbone.Router
1831
+
1832
+ # Borrow the static extend method from Backbone
1833
+ @extend = Backbone.Model.extend
1834
+
1835
+ # Mixin an EventBroker
1836
+ _(@prototype).extend EventBroker
1837
+
1838
+ constructor: (@options = {}) ->
1839
+ _(@options).defaults
1840
+ pushState: true
1841
+
1842
+ @subscribeEvent '!router:route', @routeHandler
1843
+ @subscribeEvent '!router:routeByName', @routeByNameHandler
1844
+ @subscribeEvent '!router:reverse', @reverseHandler
1845
+ @subscribeEvent '!router:changeURL', @changeURLHandler
1846
+
1847
+ @createHistory()
1848
+
1849
+ # Create a Backbone.History instance
1850
+ createHistory: ->
1851
+ Backbone.history or= new Backbone.History()
1852
+
1853
+ startHistory: ->
1854
+ # Start the Backbone.History instance to start routing
1855
+ # This should be called after all routes have been registered
1856
+ Backbone.history.start @options
1857
+
1858
+ # Stop the current Backbone.History instance from observing URL changes
1859
+ stopHistory: ->
1860
+ Backbone.history.stop() if Backbone.History.started
1861
+
1862
+ # Connect an address with a controller action
1863
+ # Creates a route on the Backbone.History instance
1864
+ match: (pattern, target, options = {}) =>
1865
+ if arguments.length is 2 and typeof target is 'object'
1866
+ # Handles cases like `match 'url', controller: 'c', action: 'a'`.
1867
+ options = target
1868
+ {controller, action} = options
1869
+ unless controller and action
1870
+ throw new Error 'Router#match must receive either target or options.controller & options.action'
1871
+ else
1872
+ # Handles `match 'url', 'c#a'`.
1873
+ {controller, action} = options
1874
+ if controller or action
1875
+ throw new Error 'Router#match cannot use both target and options.controller / action'
1876
+ # Separate target into controller and controller action.
1877
+ [controller, action] = target.split('#')
1878
+
1879
+ # Create the route
1880
+ route = new Route pattern, controller, action, options
1881
+ # Register the route at the Backbone.History instance.
1882
+ # Don’t use Backbone.history.route here because it calls
1883
+ # handlers.unshift, inserting the handler at the top of the list.
1884
+ # Since we want routes to match in the order they were specified,
1885
+ # we’re appending the route at the end.
1886
+ Backbone.history.handlers.push {route, callback: route.handler}
1887
+ route
1888
+
1889
+ # Route a given URL path manually, returns whether a route matched
1890
+ # This looks quite like Backbone.History::loadUrl but it
1891
+ # accepts an absolute URL with a leading slash (e.g. /foo)
1892
+ # and passes the routing options to the callback function.
1893
+ route: (path, options = {}) =>
1894
+ _(options).defaults
1895
+ # Update the URL programmatically after routing
1896
+ changeURL: true
1897
+
1898
+ # Remove leading hash or slash
1899
+ path = path.replace /^(\/#|\/)/, ''
1900
+ for handler in Backbone.history.handlers
1901
+ if handler.route.test(path)
1902
+ handler.callback path, options
1903
+ return true
1904
+ false
1905
+
1906
+ reverseHandler: (name, params, callback) ->
1907
+ callback @reverse name, params
1908
+
1909
+ # Find the URL for a given name using the registered routes and
1910
+ # provided parameters.
1911
+ reverse: (name, params) ->
1912
+ # First filter the route handlers to those that are of the same
1913
+ # name
1914
+ for handler in Backbone.history.handlers when handler.route.name is name
1915
+ # Attempt to reverse using the provided parameter hash
1916
+ url = handler.route.reverse params
1917
+
1918
+ # Return the url if we got a valid one; else we continue on
1919
+ return url if url isnt false
1920
+
1921
+ # We didn't get anything
1922
+ false
1923
+
1924
+ # Handler for the global !router:route event
1925
+ routeHandler: (path, options, callback) ->
1926
+ # Support old signature: Assume only path and callback were passed
1927
+ # if we only got two arguments
1928
+ if arguments.length is 2 and typeof options is 'function'
1929
+ callback = options
1930
+ options = {}
1931
+
1932
+ routed = @route path, options
1933
+ callback? routed
1934
+
1935
+ routeByNameHandler: (name, params, options, callback) ->
1936
+ # Support old signature: Assume options wasn't passed
1937
+ # if we only got three arguments
1938
+ if arguments.length is 3 and typeof options is 'function'
1939
+ callback = options
1940
+ options = {}
1941
+
1942
+ path = @reverse name, params
1943
+ return unless path
1944
+ @routeHandler path, options, callback
1945
+
1946
+ # Change the current URL, add a history entry.
1947
+ changeURL: (url, options = {}) ->
1948
+ navigateOptions =
1949
+ # Do not trigger or replace per default
1950
+ trigger: options.trigger is true
1951
+ replace: options.replace is true
1952
+
1953
+ # Navigate to the passed URL and forward options to Backbone
1954
+ Backbone.history.navigate url, navigateOptions
1955
+
1956
+ # Handler for the global !router:changeURL event
1957
+ # Accepts both the url and an options hash that is forwarded to Backbone
1958
+ changeURLHandler: (url, options) ->
1959
+ @changeURL url, options
1960
+
1961
+ # Disposal
1962
+ # --------
1963
+
1964
+ disposed: false
1965
+
1966
+ dispose: ->
1967
+ return if @disposed
1968
+
1969
+ # Stop Backbone.History instance and remove it
1970
+ @stopHistory()
1971
+ delete Backbone.history
1972
+
1973
+ @unsubscribeAllEvents()
1974
+
1975
+ # Finished
1976
+ @disposed = true
1977
+
1978
+ # You’re frozen when your heart’s not open
1979
+ Object.freeze? this
1980
+
1981
+ define 'chaplin/lib/delayer', ->
1982
+ 'use strict'
1983
+
1984
+ # Delayer
1985
+ # -------
1986
+ #
1987
+ # Add functionality to set unique, named timeouts and intervals
1988
+ # so they can be cleared afterwards when disposing the object.
1989
+ # This is especially useful in your custom View class which inherits
1990
+ # from the standard Chaplin.View.
1991
+ #
1992
+ # Mixin this object to add the delayer capability to any object:
1993
+ # _(object).extend Delayer
1994
+ #
1995
+ # Or to a prototype of a class:
1996
+ # _(@prototype).extend Delayer
1997
+ #
1998
+ # In the dispose method, call `clearDelayed` to remove all pending
1999
+ # timeouts and running intervals:
2000
+ #
2001
+ # dispose: ->
2002
+ # return if @disposed
2003
+ # @clearDelayed()
2004
+ # super
2005
+
2006
+ Delayer =
2007
+
2008
+ setTimeout: (name, time, handler) ->
2009
+ @timeouts ?= {}
2010
+ @clearTimeout name
2011
+ wrappedHandler = =>
2012
+ delete @timeouts[name]
2013
+ handler()
2014
+ handle = setTimeout wrappedHandler, time
2015
+ @timeouts[name] = handle
2016
+ handle
2017
+
2018
+ clearTimeout: (name) ->
2019
+ return unless @timeouts and @timeouts[name]?
2020
+ clearTimeout @timeouts[name]
2021
+ delete @timeouts[name]
2022
+ return
2023
+
2024
+ clearAllTimeouts: ->
2025
+ return unless @timeouts
2026
+ for name, handle of @timeouts
2027
+ @clearTimeout name
2028
+ return
2029
+
2030
+ setInterval: (name, time, handler) ->
2031
+ @clearInterval name
2032
+ @intervals ?= {}
2033
+ handle = setInterval handler, time
2034
+ @intervals[name] = handle
2035
+ handle
2036
+
2037
+ clearInterval: (name) ->
2038
+ return unless @intervals and @intervals[name]
2039
+ clearInterval @intervals[name]
2040
+ delete @intervals[name]
2041
+ return
2042
+
2043
+ clearAllIntervals: ->
2044
+ return unless @intervals
2045
+ for name, handle of @intervals
2046
+ @clearInterval name
2047
+ return
2048
+
2049
+ clearDelayed: ->
2050
+ @clearAllTimeouts()
2051
+ @clearAllIntervals()
2052
+ return
2053
+
2054
+ # You’re frozen when your heart’s not open
2055
+ Object.freeze? Delayer
2056
+
2057
+ Delayer
2058
+
2059
+ define 'chaplin/lib/event_broker', [
2060
+ 'chaplin/mediator'
2061
+ ], (mediator) ->
2062
+ 'use strict'
2063
+
2064
+ # Add functionality to subscribe and publish to global
2065
+ # Publish/Subscribe events so they can be removed afterwards
2066
+ # when disposing the object.
2067
+ #
2068
+ # Mixin this object to add the subscriber capability to any object:
2069
+ # _(object).extend EventBroker
2070
+ # Or to a prototype of a class:
2071
+ # _(@prototype).extend EventBroker
2072
+ #
2073
+ # Since Backbone 0.9.2 this abstraction just serves the purpose
2074
+ # that a handler cannot be registered twice for the same event.
2075
+
2076
+ EventBroker =
2077
+
2078
+ subscribeEvent: (type, handler) ->
2079
+ if typeof type isnt 'string'
2080
+ throw new TypeError 'EventBroker#subscribeEvent: ' +
2081
+ 'type argument must be a string'
2082
+ if typeof handler isnt 'function'
2083
+ throw new TypeError 'EventBroker#subscribeEvent: ' +
2084
+ 'handler argument must be a function'
2085
+
2086
+ # Ensure that a handler isn’t registered twice
2087
+ mediator.unsubscribe type, handler, this
2088
+
2089
+ # Register global handler, force context to the subscriber
2090
+ mediator.subscribe type, handler, this
2091
+
2092
+ unsubscribeEvent: (type, handler) ->
2093
+ if typeof type isnt 'string'
2094
+ throw new TypeError 'EventBroker#unsubscribeEvent: ' +
2095
+ 'type argument must be a string'
2096
+ if typeof handler isnt 'function'
2097
+ throw new TypeError 'EventBroker#unsubscribeEvent: ' +
2098
+ 'handler argument must be a function'
2099
+
2100
+ # Remove global handler
2101
+ mediator.unsubscribe type, handler
2102
+
2103
+ # Unbind all global handlers
2104
+ unsubscribeAllEvents: ->
2105
+ # Remove all handlers with a context of this subscriber
2106
+ mediator.unsubscribe null, null, this
2107
+
2108
+ publishEvent: (type, args...) ->
2109
+ if typeof type isnt 'string'
2110
+ throw new TypeError 'EventBroker#publishEvent: ' +
2111
+ 'type argument must be a string'
2112
+
2113
+ # Publish global handler
2114
+ mediator.publish type, args...
2115
+
2116
+ # You’re frozen when your heart’s not open
2117
+ Object.freeze? EventBroker
2118
+
2119
+ EventBroker
2120
+
2121
+ define 'chaplin/lib/support', ->
2122
+ 'use strict'
2123
+
2124
+ # Feature detection
2125
+ # -----------------
2126
+
2127
+ support =
2128
+
2129
+ # Test for defineProperty support
2130
+ # (IE 8 knows the method but will throw an exception)
2131
+ propertyDescriptors: do ->
2132
+ unless typeof Object.defineProperty is 'function' and
2133
+ typeof Object.defineProperties is 'function'
2134
+ return false
2135
+ try
2136
+ o = {}
2137
+ Object.defineProperty o, 'foo', value: 'bar'
2138
+ return o.foo is 'bar'
2139
+ catch error
2140
+ return false
2141
+
2142
+ support
2143
+
2144
+ define 'chaplin/lib/sync_machine', ->
2145
+ 'use strict'
2146
+
2147
+ # Simple finite state machine for synchronization of models/collections
2148
+ # Three states: unsynced, syncing and synced
2149
+ # Several transitions between them
2150
+ # Fires Backbone events on every transition
2151
+ # (unsynced, syncing, synced; syncStateChange)
2152
+ # Provides shortcut methods to call handlers when a given state is reached
2153
+ # (named after the events above)
2154
+
2155
+ UNSYNCED = 'unsynced'
2156
+ SYNCING = 'syncing'
2157
+ SYNCED = 'synced'
2158
+
2159
+ STATE_CHANGE = 'syncStateChange'
2160
+
2161
+ SyncMachine =
2162
+
2163
+ _syncState: UNSYNCED
2164
+ _previousSyncState: null
2165
+
2166
+ # Get the current state
2167
+ # ---------------------
2168
+
2169
+ syncState: ->
2170
+ @_syncState
2171
+
2172
+ isUnsynced: ->
2173
+ @_syncState is UNSYNCED
2174
+
2175
+ isSynced: ->
2176
+ @_syncState is SYNCED
2177
+
2178
+ isSyncing: ->
2179
+ @_syncState is SYNCING
2180
+
2181
+ # Transitions
2182
+ # -----------
2183
+
2184
+ unsync: ->
2185
+ if @_syncState in [SYNCING, SYNCED]
2186
+ @_previousSync = @_syncState
2187
+ @_syncState = UNSYNCED
2188
+ @trigger @_syncState, this, @_syncState
2189
+ @trigger STATE_CHANGE, this, @_syncState
2190
+ # when UNSYNCED do nothing
2191
+ return
2192
+
2193
+ beginSync: ->
2194
+ if @_syncState in [UNSYNCED, SYNCED]
2195
+ @_previousSync = @_syncState
2196
+ @_syncState = SYNCING
2197
+ @trigger @_syncState, this, @_syncState
2198
+ @trigger STATE_CHANGE, this, @_syncState
2199
+ # when SYNCING do nothing
2200
+ return
2201
+
2202
+ finishSync: ->
2203
+ if @_syncState is SYNCING
2204
+ @_previousSync = @_syncState
2205
+ @_syncState = SYNCED
2206
+ @trigger @_syncState, this, @_syncState
2207
+ @trigger STATE_CHANGE, this, @_syncState
2208
+ # when SYNCED, UNSYNCED do nothing
2209
+ return
2210
+
2211
+ abortSync: ->
2212
+ if @_syncState is SYNCING
2213
+ @_syncState = @_previousSync
2214
+ @_previousSync = @_syncState
2215
+ @trigger @_syncState, this, @_syncState
2216
+ @trigger STATE_CHANGE, this, @_syncState
2217
+ # when UNSYNCED, SYNCED do nothing
2218
+ return
2219
+
2220
+ # Create shortcut methods to bind a handler to a state change
2221
+ # -----------------------------------------------------------
2222
+
2223
+ for event in [UNSYNCED, SYNCING, SYNCED, STATE_CHANGE]
2224
+ do (event) ->
2225
+ SyncMachine[event] = (callback, context = @) ->
2226
+ @on event, callback, context
2227
+ callback.call(context) if @_syncState is event
2228
+
2229
+ # You’re frozen when your heart’s not open
2230
+ Object.freeze? SyncMachine
2231
+
2232
+ SyncMachine
2233
+
2234
+ define 'chaplin/lib/utils', [
2235
+ 'chaplin/lib/support'
2236
+ ], (support) ->
2237
+ 'use strict'
2238
+
2239
+ # Utilities
2240
+ # ---------
2241
+
2242
+ utils =
2243
+
2244
+ # Object Helpers
2245
+ # --------------
2246
+
2247
+ # Prototypal delegation. Create an object which delegates
2248
+ # to another object.
2249
+ beget: do ->
2250
+ if typeof Object.create is 'function'
2251
+ Object.create
2252
+ else
2253
+ ctor = ->
2254
+ (obj) ->
2255
+ ctor.prototype = obj
2256
+ new ctor
2257
+
2258
+ # Make properties readonly and not configurable
2259
+ # using ECMAScript 5 property descriptors
2260
+ readonly: do ->
2261
+ if support.propertyDescriptors
2262
+ readonlyDescriptor =
2263
+ writable: false
2264
+ enumerable: true
2265
+ configurable: false
2266
+ (obj, properties...) ->
2267
+ for prop in properties
2268
+ readonlyDescriptor.value = obj[prop]
2269
+ Object.defineProperty obj, prop, readonlyDescriptor
2270
+ true
2271
+ else
2272
+ ->
2273
+ false
2274
+
2275
+ # Get the whole chain of object prototypes.
2276
+ getPrototypeChain: (object) ->
2277
+ chain = [object.constructor.prototype]
2278
+ chain.push object while object = object.constructor?.__super__
2279
+ chain
2280
+
2281
+ # Get all property versions from object’s prototype chain.
2282
+ # E.g. if object1 & object2 have `prop` and object2 inherits from
2283
+ # object1, it will get [object1prop, object2prop].
2284
+ getAllPropertyVersions: (object, property) ->
2285
+ _(utils.getPrototypeChain object)
2286
+ .chain()
2287
+ .pluck(property)
2288
+ .compact()
2289
+ .uniq()
2290
+ .value()
2291
+ .reverse()
2292
+
2293
+ # Function Helpers
2294
+ # ----------------
2295
+
2296
+ # Wrap a method in order to call the corresponding
2297
+ # `after-` method automatically (e.g. `afterRender` or
2298
+ # `afterInitialize`)
2299
+ wrapMethod: (instance, name) ->
2300
+ # Enclose the original function
2301
+ func = instance[name]
2302
+ # Set a flag
2303
+ instance["#{name}IsWrapped"] = true
2304
+ # Create the wrapper method
2305
+ instance[name] = ->
2306
+ # Stop if the instance was already disposed
2307
+ return false if instance.disposed
2308
+ # Call the original method
2309
+ func.apply instance, arguments
2310
+ # Call the corresponding `after-` method
2311
+ instance["after#{utils.upcase(name)}"] arguments...
2312
+ # Return the view
2313
+ instance
2314
+
2315
+ # String Helpers
2316
+ # --------------
2317
+
2318
+ # Upcase the first character
2319
+ upcase: (str) ->
2320
+ str.charAt(0).toUpperCase() + str.substring(1)
2321
+
2322
+ # underScoreHelper -> under_score_helper
2323
+ underscorize: (string) ->
2324
+ string.replace /[A-Z]/g, (char, index) ->
2325
+ (if index isnt 0 then '_' else '') + char.toLowerCase()
2326
+
2327
+ # Event handling helpers
2328
+ # ----------------------
2329
+
2330
+ # Returns whether a modifier key is pressed during a keypress or mouse click
2331
+ modifierKeyPressed: (event) ->
2332
+ event.shiftKey or event.altKey or event.ctrlKey or event.metaKey
2333
+
2334
+ # Finish
2335
+ # ------
2336
+
2337
+ # Seal the utils object
2338
+ Object.seal? utils
2339
+
2340
+ utils
2341
+
2342
+ define 'chaplin', [
2343
+ 'chaplin/application'
2344
+ 'chaplin/mediator'
2345
+ 'chaplin/dispatcher'
2346
+ 'chaplin/controllers/controller'
2347
+ 'chaplin/models/collection'
2348
+ 'chaplin/models/model'
2349
+ 'chaplin/views/layout'
2350
+ 'chaplin/views/view'
2351
+ 'chaplin/views/collection_view'
2352
+ 'chaplin/lib/route'
2353
+ 'chaplin/lib/router'
2354
+ 'chaplin/lib/delayer'
2355
+ 'chaplin/lib/event_broker'
2356
+ 'chaplin/lib/support'
2357
+ 'chaplin/lib/sync_machine'
2358
+ 'chaplin/lib/utils'
2359
+ ], (Application, mediator, Dispatcher, Controller, Collection, Model, Layout, View, CollectionView, Route, Router, Delayer, EventBroker, support, SyncMachine, utils) ->
2360
+ {
2361
+ Application,
2362
+ mediator,
2363
+ Dispatcher,
2364
+ Controller,
2365
+ Collection,
2366
+ Model,
2367
+ Layout,
2368
+ View,
2369
+ CollectionView,
2370
+ Route,
2371
+ Router,
2372
+ Delayer,
2373
+ EventBroker,
2374
+ support,
2375
+ SyncMachine,
2376
+ utils
2377
+ }