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