chaplin-on-rails 0.7.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+ }