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.
- data/MIT-LICENSE +20 -0
- data/README.md +42 -0
- data/Rakefile +38 -0
- data/lib/chaplin-on-rails.rb +2 -0
- data/lib/chaplin-on-rails/engine.rb +6 -0
- data/lib/chaplin-on-rails/version.rb +3 -0
- data/lib/generators/chaplin/helpers.rb +94 -0
- data/lib/generators/chaplin/install/install_generator.rb +68 -0
- data/lib/generators/chaplin/install/templates/app_template.js.coffee +65 -0
- data/lib/generators/chaplin/install/templates/application.js.coffee +13 -0
- data/lib/generators/chaplin/install/templates/javascripts/controllers/base/controller.js.coffee +8 -0
- data/lib/generators/chaplin/install/templates/javascripts/lib/support.js.coffee +19 -0
- data/lib/generators/chaplin/install/templates/javascripts/lib/utils.js.coffee +18 -0
- data/lib/generators/chaplin/install/templates/javascripts/lib/view_helper.js.coffee +16 -0
- data/lib/generators/chaplin/install/templates/javascripts/models/base/collection.js.coffee +9 -0
- data/lib/generators/chaplin/install/templates/javascripts/models/base/model.js.coffee +9 -0
- data/lib/generators/chaplin/install/templates/javascripts/routes.js.coffee +9 -0
- data/lib/generators/chaplin/install/templates/javascripts/views/base/collection_view.coffee +11 -0
- data/lib/generators/chaplin/install/templates/javascripts/views/base/view.coffee +15 -0
- data/lib/generators/chaplin/install/templates/javascripts/views/layout.js.coffee +8 -0
- data/lib/generators/chaplin/install/templates/requirejs.yml +6 -0
- data/test/chaplin-on-rails_test.rb +19 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/controllers/home_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/helpers/home_helper.rb +2 -0
- data/test/dummy/app/views/home/index.html.erb +1 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +59 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +58 -0
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/development.log +186 -0
- data/test/dummy/log/test.log +35 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/dummy/tmp/cache/assets/C75/D70/sprockets%2F1781f0919424c1d3014e066757a81eaa +0 -0
- data/test/dummy/tmp/cache/assets/C80/150/sprockets%2F0d3881005b0646df783d5c24683d34f5 +0 -0
- data/test/dummy/tmp/cache/assets/C89/4B0/sprockets%2F1f6245087eeb4c854a03a4644c964579 +0 -0
- data/test/dummy/tmp/cache/assets/C8D/980/sprockets%2F7184f95c8e290cbc2451767559f01d08 +0 -0
- data/test/dummy/tmp/cache/assets/CA8/130/sprockets%2F695a47c24c7804e27afc2208426ae133 +0 -0
- data/test/dummy/tmp/cache/assets/CD8/370/sprockets%2F357970feca3ac29060c1e3861e2c0953 +0 -0
- data/test/dummy/tmp/cache/assets/CDE/EC0/sprockets%2F32bce1758d7525013970c7bb7d77aa28 +0 -0
- data/test/dummy/tmp/cache/assets/CEB/750/sprockets%2F72f4d157b237c37ed75f49bd217824c7 +0 -0
- data/test/dummy/tmp/cache/assets/CF6/290/sprockets%2F95f8b25659fb7a430d68b27a5f72f666 +0 -0
- data/test/dummy/tmp/cache/assets/CF9/7C0/sprockets%2F40fc2f3d2a468a00e463f1d313cb1683 +0 -0
- data/test/dummy/tmp/cache/assets/D08/A30/sprockets%2F1310a2826dcae357ba9f383e86b638a1 +0 -0
- data/test/dummy/tmp/cache/assets/D09/100/sprockets%2F4dc073204163e0850bf7fcb55ff8b552 +0 -0
- data/test/dummy/tmp/cache/assets/D0A/A40/sprockets%2F09e4e8ba330b24f9f87a63a910d422f3 +0 -0
- data/test/dummy/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
- data/test/dummy/tmp/cache/assets/D38/B80/sprockets%2F42c6d981cbd6ae6061c7088a2321a8ed +0 -0
- data/test/dummy/tmp/cache/assets/D3E/070/sprockets%2F45f1913b6d3645a3d166ff4ff250e4cc +0 -0
- data/test/dummy/tmp/cache/assets/D4E/D00/sprockets%2F1a6846f0a837ae2524e2f9ec89e6ef43 +0 -0
- data/test/dummy/tmp/cache/assets/D57/BC0/sprockets%2F50db33dbb1258913cb2fbd562ab1294b +0 -0
- data/test/dummy/tmp/cache/assets/D5A/EA0/sprockets%2Fd771ace226fc8215a3572e0aa35bb0d6 +0 -0
- data/test/dummy/tmp/cache/assets/D63/C90/sprockets%2Fb70150e50cbcbc59c55ffb3c53a35575 +0 -0
- data/test/dummy/tmp/cache/assets/D6F/C70/sprockets%2F667a4890bf4c03f50b452d1db278fcfe +0 -0
- data/test/dummy/tmp/cache/assets/D79/470/sprockets%2F1cb96b862f7c7497af494aa19d0cbd55 +0 -0
- data/test/dummy/tmp/cache/assets/D7A/930/sprockets%2F3964cc67ad46beb69c444bb51f486e2f +0 -0
- data/test/dummy/tmp/cache/assets/D8B/B40/sprockets%2F11b945abcf62b02fcfb34f52f6aa1481 +0 -0
- data/test/dummy/tmp/cache/assets/D92/990/sprockets%2F4b34320d2682dc0fd9b107f3ef2cae7d +0 -0
- data/test/dummy/tmp/cache/assets/D98/8B0/sprockets%2Fedbef6e0d0a4742346cf479f2c522eb0 +0 -0
- data/test/dummy/tmp/cache/assets/DC8/010/sprockets%2F34cce381cfe1aa4be1ca560f118dd998 +0 -0
- data/test/dummy/tmp/cache/assets/DCD/ED0/sprockets%2F77ca908f86a8be3506de6fe60b1e0baa +0 -0
- data/test/dummy/tmp/cache/assets/DCE/890/sprockets%2Fd54185fb144acd9d1d15ed0e4ebf9f44 +0 -0
- data/test/dummy/tmp/cache/assets/DCE/D00/sprockets%2Fed31b7ea3cfb14d43a87c89deb0390f7 +0 -0
- data/test/dummy/tmp/cache/assets/DCF/610/sprockets%2Fda58bc8db5dd9cb124a85c507d591fd2 +0 -0
- data/test/dummy/tmp/cache/assets/E11/4E0/sprockets%2F86e145a39f85cceeaffdff91ebb61449 +0 -0
- data/test/test_helper.rb +7 -0
- data/vendor/assets/javascripts/backbone.js +1497 -0
- data/vendor/assets/javascripts/chaplin.js.coffee +2377 -0
- data/vendor/assets/javascripts/underscore.js +1222 -0
- 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
|
+
}
|