thorax-rails 0.0.0
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.rdoc +19 -0
- data/Rakefile +50 -0
- data/lib/generators/thorax/install/install_generator.rb +30 -0
- data/lib/generators/thorax/thorax_helpers.rb +55 -0
- data/lib/thorax.rb +6 -0
- data/vendor/assets/javascripts/backbone.js +1498 -0
- data/vendor/assets/javascripts/handlebars.js +45 -0
- data/vendor/assets/javascripts/thorax.js +3091 -0
- data/vendor/assets/javascripts/undersccore.js +1221 -0
- metadata +218 -0
@@ -0,0 +1,3091 @@
|
|
1
|
+
/*
|
2
|
+
Copyright (c) 2011-2013 @WalmartLabs
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
of this software and associated documentation files (the "Software"), to
|
6
|
+
deal in the Software without restriction, including without limitation the
|
7
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
8
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in
|
12
|
+
all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
19
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
20
|
+
DEALINGS IN THE SOFTWARE.
|
21
|
+
*/
|
22
|
+
|
23
|
+
;;
|
24
|
+
(function() {
|
25
|
+
|
26
|
+
/*global cloneInheritVars, createInheritVars, resetInheritVars, createRegistryWrapper, getValue, inheritVars, createErrorMessage */
|
27
|
+
|
28
|
+
//support zepto.forEach on jQuery
|
29
|
+
if (!$.fn.forEach) {
|
30
|
+
$.fn.forEach = function(iterator, context) {
|
31
|
+
$.fn.each.call(this, function(index) {
|
32
|
+
iterator.call(context || this, this, index);
|
33
|
+
});
|
34
|
+
};
|
35
|
+
}
|
36
|
+
|
37
|
+
var viewNameAttributeName = 'data-view-name',
|
38
|
+
viewCidAttributeName = 'data-view-cid',
|
39
|
+
viewHelperAttributeName = 'data-view-helper';
|
40
|
+
|
41
|
+
//view instances
|
42
|
+
var viewsIndexedByCid = {};
|
43
|
+
|
44
|
+
if (!Handlebars.templates) {
|
45
|
+
Handlebars.templates = {};
|
46
|
+
}
|
47
|
+
|
48
|
+
var Thorax = this.Thorax = {
|
49
|
+
templatePathPrefix: '',
|
50
|
+
//view classes
|
51
|
+
Views: {},
|
52
|
+
//certain error prone pieces of code (on Android only it seems)
|
53
|
+
//are wrapped in a try catch block, then trigger this handler in
|
54
|
+
//the catch, with the name of the function or event that was
|
55
|
+
//trying to be executed. Override this with a custom handler
|
56
|
+
//to debug / log / etc
|
57
|
+
onException: function(name, err) {
|
58
|
+
throw err;
|
59
|
+
},
|
60
|
+
//deprecated, here to ensure existing projects aren't mucked with
|
61
|
+
templates: Handlebars.templates
|
62
|
+
};
|
63
|
+
|
64
|
+
Thorax.View = Backbone.View.extend({
|
65
|
+
constructor: function() {
|
66
|
+
var response = Backbone.View.apply(this, arguments);
|
67
|
+
_.each(inheritVars, function(obj) {
|
68
|
+
if (obj.ctor) {
|
69
|
+
obj.ctor.call(this, response);
|
70
|
+
}
|
71
|
+
}, this);
|
72
|
+
return response;
|
73
|
+
},
|
74
|
+
_configure: function(options) {
|
75
|
+
var self = this;
|
76
|
+
|
77
|
+
this._referenceCount = 0;
|
78
|
+
|
79
|
+
this._objectOptionsByCid = {};
|
80
|
+
this._boundDataObjectsByCid = {};
|
81
|
+
|
82
|
+
// Setup object event tracking
|
83
|
+
_.each(inheritVars, function(obj) {
|
84
|
+
self[obj.name] = [];
|
85
|
+
});
|
86
|
+
|
87
|
+
viewsIndexedByCid[this.cid] = this;
|
88
|
+
this.children = {};
|
89
|
+
this._renderCount = 0;
|
90
|
+
|
91
|
+
//this.options is removed in Thorax.View, we merge passed
|
92
|
+
//properties directly with the view and template context
|
93
|
+
_.extend(this, options || {});
|
94
|
+
|
95
|
+
// Setup helpers
|
96
|
+
bindHelpers.call(this);
|
97
|
+
|
98
|
+
_.each(inheritVars, function(obj) {
|
99
|
+
if (obj.configure) {
|
100
|
+
obj.configure.call(this);
|
101
|
+
}
|
102
|
+
}, this);
|
103
|
+
|
104
|
+
this.trigger('configure');
|
105
|
+
},
|
106
|
+
|
107
|
+
setElement : function() {
|
108
|
+
var response = Backbone.View.prototype.setElement.apply(this, arguments);
|
109
|
+
this.name && this.$el.attr(viewNameAttributeName, this.name);
|
110
|
+
this.$el.attr(viewCidAttributeName, this.cid);
|
111
|
+
return response;
|
112
|
+
},
|
113
|
+
|
114
|
+
_addChild: function(view) {
|
115
|
+
if (this.children[view.cid]) {
|
116
|
+
return;
|
117
|
+
}
|
118
|
+
view.retain();
|
119
|
+
this.children[view.cid] = view;
|
120
|
+
// _helperOptions is used to detect if is HelperView
|
121
|
+
// we do not want to remove child in this case as
|
122
|
+
// we are adding the HelperView to the declaring view
|
123
|
+
// (whatever view used the view helper in it's template)
|
124
|
+
// but it's parent will not equal the declaring view
|
125
|
+
// in the case of a nested helper, which will cause an error.
|
126
|
+
// In either case it's not necessary to ever call
|
127
|
+
// _removeChild on a HelperView as _addChild should only
|
128
|
+
// be called when a HelperView is created.
|
129
|
+
if (view.parent && view.parent !== this && !view._helperOptions) {
|
130
|
+
view.parent._removeChild(view);
|
131
|
+
}
|
132
|
+
view.parent = this;
|
133
|
+
this.trigger('child', view);
|
134
|
+
return view;
|
135
|
+
},
|
136
|
+
|
137
|
+
_removeChild: function(view) {
|
138
|
+
delete this.children[view.cid];
|
139
|
+
view.parent = null;
|
140
|
+
view.release();
|
141
|
+
return view;
|
142
|
+
},
|
143
|
+
|
144
|
+
_destroy: function(options) {
|
145
|
+
_.each(this._boundDataObjectsByCid, this.unbindDataObject, this);
|
146
|
+
this.trigger('destroyed');
|
147
|
+
delete viewsIndexedByCid[this.cid];
|
148
|
+
|
149
|
+
_.each(this.children, function(child) {
|
150
|
+
this._removeChild(child);
|
151
|
+
}, this);
|
152
|
+
|
153
|
+
if (this.el) {
|
154
|
+
this.undelegateEvents();
|
155
|
+
this.remove(); // Will call stopListening()
|
156
|
+
this.off(); // Kills off remaining events
|
157
|
+
}
|
158
|
+
|
159
|
+
// Absolute worst case scenario, kill off some known fields to minimize the impact
|
160
|
+
// of being retained.
|
161
|
+
this.el = this.$el = undefined;
|
162
|
+
this.parent = undefined;
|
163
|
+
this.model = this.collection = this._collection = undefined;
|
164
|
+
this._helperOptions = undefined;
|
165
|
+
},
|
166
|
+
|
167
|
+
render: function(output) {
|
168
|
+
if (this._rendering) {
|
169
|
+
// Nested rendering of the same view instances can lead to some very nasty issues with
|
170
|
+
// the root render process overwriting any updated data that may have been output in the child
|
171
|
+
// execution. If in a situation where you need to rerender in response to an event that is
|
172
|
+
// triggered sync in the rendering lifecycle it's recommended to defer the subsequent render
|
173
|
+
// or refactor so that all preconditions are known prior to exec.
|
174
|
+
throw new Error(createErrorMessage('nested-render'));
|
175
|
+
}
|
176
|
+
|
177
|
+
this._previousHelpers = _.filter(this.children, function(child) {
|
178
|
+
return child._helperOptions;
|
179
|
+
});
|
180
|
+
|
181
|
+
var children = {};
|
182
|
+
_.each(this.children, function(child, key) {
|
183
|
+
if (!child._helperOptions) {
|
184
|
+
children[key] = child;
|
185
|
+
}
|
186
|
+
});
|
187
|
+
this.children = children;
|
188
|
+
|
189
|
+
this.trigger('before:rendered');
|
190
|
+
this._rendering = true;
|
191
|
+
|
192
|
+
try {
|
193
|
+
if (_.isUndefined(output) || (!_.isElement(output) && !Thorax.Util.is$(output) && !(output && output.el) && !_.isString(output) && !_.isFunction(output))) {
|
194
|
+
// try one more time to assign the template, if we don't
|
195
|
+
// yet have one we must raise
|
196
|
+
assignTemplate.call(this, 'template', {
|
197
|
+
required: true
|
198
|
+
});
|
199
|
+
output = this.renderTemplate(this.template);
|
200
|
+
} else if (_.isFunction(output)) {
|
201
|
+
output = this.renderTemplate(output);
|
202
|
+
}
|
203
|
+
|
204
|
+
// Destroy any helpers that may be lingering
|
205
|
+
_.each(this._previousHelpers, function(child) {
|
206
|
+
this._removeChild(child);
|
207
|
+
}, this);
|
208
|
+
this._previousHelpers = undefined;
|
209
|
+
|
210
|
+
//accept a view, string, Handlebars.SafeString or DOM element
|
211
|
+
this.html((output && output.el) || (output && output.string) || output);
|
212
|
+
|
213
|
+
++this._renderCount;
|
214
|
+
this.trigger('rendered');
|
215
|
+
} finally {
|
216
|
+
this._rendering = false;
|
217
|
+
}
|
218
|
+
|
219
|
+
return output;
|
220
|
+
},
|
221
|
+
|
222
|
+
context: function() {
|
223
|
+
return _.extend({}, (this.model && this.model.attributes) || {});
|
224
|
+
},
|
225
|
+
|
226
|
+
_getContext: function() {
|
227
|
+
return _.extend({}, this, getValue(this, 'context') || {});
|
228
|
+
},
|
229
|
+
|
230
|
+
// Private variables in handlebars / options.data in template helpers
|
231
|
+
_getData: function(data) {
|
232
|
+
return {
|
233
|
+
view: this,
|
234
|
+
cid: _.uniqueId('t'),
|
235
|
+
yield: function() {
|
236
|
+
// fn is seeded by template helper passing context to data
|
237
|
+
return data.fn && data.fn(data);
|
238
|
+
}
|
239
|
+
};
|
240
|
+
},
|
241
|
+
|
242
|
+
renderTemplate: function(file, context, ignoreErrors) {
|
243
|
+
var template;
|
244
|
+
context = context || this._getContext();
|
245
|
+
if (_.isFunction(file)) {
|
246
|
+
template = file;
|
247
|
+
} else {
|
248
|
+
template = Thorax.Util.getTemplate(file, ignoreErrors);
|
249
|
+
}
|
250
|
+
if (!template) {
|
251
|
+
return '';
|
252
|
+
} else {
|
253
|
+
return template(context, {
|
254
|
+
helpers: this.helpers,
|
255
|
+
data: this._getData(context)
|
256
|
+
});
|
257
|
+
}
|
258
|
+
},
|
259
|
+
|
260
|
+
ensureRendered: function() {
|
261
|
+
!this._renderCount && this.render();
|
262
|
+
},
|
263
|
+
shouldRender: function(flag) {
|
264
|
+
// Render if flag is truthy or if we have already rendered and flag is undefined/null
|
265
|
+
return flag || (flag == null && this._renderCount);
|
266
|
+
},
|
267
|
+
conditionalRender: function(flag) {
|
268
|
+
if (this.shouldRender(flag)) {
|
269
|
+
this.render();
|
270
|
+
}
|
271
|
+
},
|
272
|
+
|
273
|
+
appendTo: function(el) {
|
274
|
+
this.ensureRendered();
|
275
|
+
$(el).append(this.el);
|
276
|
+
this.trigger('ready', {target: this});
|
277
|
+
},
|
278
|
+
|
279
|
+
html: function(html) {
|
280
|
+
if (_.isUndefined(html)) {
|
281
|
+
return this.el.innerHTML;
|
282
|
+
} else {
|
283
|
+
// Event for IE element fixes
|
284
|
+
this.trigger('before:append');
|
285
|
+
var element = this._replaceHTML(html);
|
286
|
+
this.trigger('append');
|
287
|
+
return element;
|
288
|
+
}
|
289
|
+
},
|
290
|
+
|
291
|
+
release: function() {
|
292
|
+
--this._referenceCount;
|
293
|
+
if (this._referenceCount <= 0) {
|
294
|
+
this._destroy();
|
295
|
+
}
|
296
|
+
},
|
297
|
+
|
298
|
+
retain: function(owner) {
|
299
|
+
++this._referenceCount;
|
300
|
+
if (owner) {
|
301
|
+
// Not using listenTo helper as we want to run once the owner is destroyed
|
302
|
+
this.listenTo(owner, 'destroyed', owner.release);
|
303
|
+
}
|
304
|
+
},
|
305
|
+
|
306
|
+
_replaceHTML: function(html) {
|
307
|
+
this.el.innerHTML = "";
|
308
|
+
return this.$el.append(html);
|
309
|
+
},
|
310
|
+
|
311
|
+
_anchorClick: function(event) {
|
312
|
+
var target = $(event.currentTarget),
|
313
|
+
href = target.attr('href');
|
314
|
+
// Route anything that starts with # or / (excluding //domain urls)
|
315
|
+
if (href && (href[0] === '#' || (href[0] === '/' && href[1] !== '/'))) {
|
316
|
+
Backbone.history.navigate(href, {
|
317
|
+
trigger: true
|
318
|
+
});
|
319
|
+
return false;
|
320
|
+
}
|
321
|
+
return true;
|
322
|
+
}
|
323
|
+
});
|
324
|
+
|
325
|
+
Thorax.View.extend = function() {
|
326
|
+
createInheritVars(this);
|
327
|
+
|
328
|
+
var child = Backbone.View.extend.apply(this, arguments);
|
329
|
+
child.__parent__ = this;
|
330
|
+
|
331
|
+
resetInheritVars(child);
|
332
|
+
|
333
|
+
return child;
|
334
|
+
};
|
335
|
+
|
336
|
+
createRegistryWrapper(Thorax.View, Thorax.Views);
|
337
|
+
|
338
|
+
function bindHelpers() {
|
339
|
+
if (this.helpers) {
|
340
|
+
_.each(this.helpers, function(helper, name) {
|
341
|
+
var view = this;
|
342
|
+
this.helpers[name] = function() {
|
343
|
+
var args = _.toArray(arguments),
|
344
|
+
options = _.last(args);
|
345
|
+
options.context = this;
|
346
|
+
return helper.apply(view, args);
|
347
|
+
};
|
348
|
+
}, this);
|
349
|
+
}
|
350
|
+
}
|
351
|
+
|
352
|
+
//$(selector).view() helper
|
353
|
+
$.fn.view = function(options) {
|
354
|
+
options = _.defaults(options || {}, {
|
355
|
+
helper: true
|
356
|
+
});
|
357
|
+
var selector = '[' + viewCidAttributeName + ']';
|
358
|
+
if (!options.helper) {
|
359
|
+
selector += ':not([' + viewHelperAttributeName + '])';
|
360
|
+
}
|
361
|
+
var el = $(this).closest(selector);
|
362
|
+
return (el && viewsIndexedByCid[el.attr(viewCidAttributeName)]) || false;
|
363
|
+
};
|
364
|
+
|
365
|
+
;;
|
366
|
+
/*global createRegistryWrapper:true, cloneEvents: true */
|
367
|
+
function createErrorMessage(code) {
|
368
|
+
return 'Error "' + code + '". For more information visit http://thoraxjs.org/error-codes.html' + '#' + code;
|
369
|
+
}
|
370
|
+
|
371
|
+
function createRegistryWrapper(klass, hash) {
|
372
|
+
var $super = klass.extend;
|
373
|
+
klass.extend = function() {
|
374
|
+
var child = $super.apply(this, arguments);
|
375
|
+
if (child.prototype.name) {
|
376
|
+
hash[child.prototype.name] = child;
|
377
|
+
}
|
378
|
+
return child;
|
379
|
+
};
|
380
|
+
}
|
381
|
+
|
382
|
+
function registryGet(object, type, name, ignoreErrors) {
|
383
|
+
var target = object[type],
|
384
|
+
value;
|
385
|
+
if (_.indexOf(name, '.') >= 0) {
|
386
|
+
var bits = name.split(/\./);
|
387
|
+
name = bits.pop();
|
388
|
+
_.each(bits, function(key) {
|
389
|
+
target = target[key];
|
390
|
+
});
|
391
|
+
}
|
392
|
+
target && (value = target[name]);
|
393
|
+
if (!value && !ignoreErrors) {
|
394
|
+
throw new Error(type + ': ' + name + ' does not exist.');
|
395
|
+
} else {
|
396
|
+
return value;
|
397
|
+
}
|
398
|
+
}
|
399
|
+
|
400
|
+
function assignView(attributeName, options) {
|
401
|
+
var ViewClass;
|
402
|
+
// if attribute is the name of view to fetch
|
403
|
+
if (_.isString(this[attributeName])) {
|
404
|
+
ViewClass = Thorax.Util.getViewClass(this[attributeName], true);
|
405
|
+
// else try and fetch the view based on the name
|
406
|
+
} else if (this.name && !_.isFunction(this[attributeName])) {
|
407
|
+
ViewClass = Thorax.Util.getViewClass(this.name + (options.extension || ''), true);
|
408
|
+
}
|
409
|
+
// if we found something, assign it
|
410
|
+
if (ViewClass && !_.isFunction(this[attributeName])) {
|
411
|
+
this[attributeName] = ViewClass;
|
412
|
+
}
|
413
|
+
// if nothing was found and it's required, throw
|
414
|
+
if (options.required && !_.isFunction(this[attributeName])) {
|
415
|
+
throw new Error('View ' + (this.name || this.cid) + ' requires: ' + attributeName);
|
416
|
+
}
|
417
|
+
}
|
418
|
+
|
419
|
+
function assignTemplate(attributeName, options) {
|
420
|
+
var template;
|
421
|
+
// if attribute is the name of template to fetch
|
422
|
+
if (_.isString(this[attributeName])) {
|
423
|
+
template = Thorax.Util.getTemplate(this[attributeName], true);
|
424
|
+
// else try and fetch the template based on the name
|
425
|
+
} else if (this.name && !_.isFunction(this[attributeName])) {
|
426
|
+
template = Thorax.Util.getTemplate(this.name + (options.extension || ''), true);
|
427
|
+
}
|
428
|
+
// CollectionView and LayoutView have a defaultTemplate that may be used if none
|
429
|
+
// was found, regular views must have a template if render() is called
|
430
|
+
if (!template && attributeName === 'template' && this._defaultTemplate) {
|
431
|
+
template = this._defaultTemplate;
|
432
|
+
}
|
433
|
+
// if we found something, assign it
|
434
|
+
if (template && !_.isFunction(this[attributeName])) {
|
435
|
+
this[attributeName] = template;
|
436
|
+
}
|
437
|
+
// if nothing was found and it's required, throw
|
438
|
+
if (options.required && !_.isFunction(this[attributeName])) {
|
439
|
+
throw new Error('View ' + (this.name || this.cid) + ' requires: ' + attributeName);
|
440
|
+
}
|
441
|
+
}
|
442
|
+
|
443
|
+
// getValue is used instead of _.result because we
|
444
|
+
// need an extra scope parameter, and will minify
|
445
|
+
// better than _.result
|
446
|
+
function getValue(object, prop, scope) {
|
447
|
+
if (!(object && object[prop])) {
|
448
|
+
return null;
|
449
|
+
}
|
450
|
+
return _.isFunction(object[prop])
|
451
|
+
? object[prop].call(scope || object)
|
452
|
+
: object[prop];
|
453
|
+
}
|
454
|
+
|
455
|
+
var inheritVars = {};
|
456
|
+
function createInheritVars(self) {
|
457
|
+
// Ensure that we have our static event objects
|
458
|
+
_.each(inheritVars, function(obj) {
|
459
|
+
if (!self[obj.name]) {
|
460
|
+
self[obj.name] = [];
|
461
|
+
}
|
462
|
+
});
|
463
|
+
}
|
464
|
+
function resetInheritVars(self) {
|
465
|
+
// Ensure that we have our static event objects
|
466
|
+
_.each(inheritVars, function(obj) {
|
467
|
+
self[obj.name] = [];
|
468
|
+
});
|
469
|
+
}
|
470
|
+
function walkInheritTree(source, fieldName, isStatic, callback) {
|
471
|
+
var tree = [];
|
472
|
+
if (_.has(source, fieldName)) {
|
473
|
+
tree.push(source);
|
474
|
+
}
|
475
|
+
var iterate = source;
|
476
|
+
if (isStatic) {
|
477
|
+
while (iterate = iterate.__parent__) {
|
478
|
+
if (_.has(iterate, fieldName)) {
|
479
|
+
tree.push(iterate);
|
480
|
+
}
|
481
|
+
}
|
482
|
+
} else {
|
483
|
+
iterate = iterate.constructor;
|
484
|
+
while (iterate) {
|
485
|
+
if (iterate.prototype && _.has(iterate.prototype, fieldName)) {
|
486
|
+
tree.push(iterate.prototype);
|
487
|
+
}
|
488
|
+
iterate = iterate.__super__ && iterate.__super__.constructor;
|
489
|
+
}
|
490
|
+
}
|
491
|
+
|
492
|
+
var i = tree.length;
|
493
|
+
while (i--) {
|
494
|
+
_.each(getValue(tree[i], fieldName, source), callback);
|
495
|
+
}
|
496
|
+
}
|
497
|
+
|
498
|
+
function objectEvents(target, eventName, callback, context) {
|
499
|
+
if (_.isObject(callback)) {
|
500
|
+
var spec = inheritVars[eventName];
|
501
|
+
if (spec && spec.event) {
|
502
|
+
if (target && target.listenTo && target[eventName] && target[eventName].cid) {
|
503
|
+
addEvents(target, callback, context, eventName);
|
504
|
+
} else {
|
505
|
+
addEvents(target['_' + eventName + 'Events'], callback, context);
|
506
|
+
}
|
507
|
+
return true;
|
508
|
+
}
|
509
|
+
}
|
510
|
+
}
|
511
|
+
// internal listenTo function will error on destroyed
|
512
|
+
// race condition
|
513
|
+
function listenTo(object, target, eventName, callback, context) {
|
514
|
+
// getEventCallback will resolve if it is a string or a method
|
515
|
+
// and return a method
|
516
|
+
var callbackMethod = getEventCallback(callback, object),
|
517
|
+
destroyedCount = 0;
|
518
|
+
|
519
|
+
function eventHandler() {
|
520
|
+
if (object.el) {
|
521
|
+
callbackMethod.apply(context, arguments);
|
522
|
+
} else {
|
523
|
+
// If our event handler is removed by destroy while another event is processing then we
|
524
|
+
// we might see one latent event percolate through due to caching in the event loop. If we
|
525
|
+
// see multiple events this is a concern and a sign that something was not cleaned properly.
|
526
|
+
if (destroyedCount) {
|
527
|
+
throw new Error('destroyed-event:' + object.name + ':' + eventName);
|
528
|
+
}
|
529
|
+
destroyedCount++;
|
530
|
+
}
|
531
|
+
}
|
532
|
+
eventHandler._callback = callbackMethod._callback || callbackMethod;
|
533
|
+
eventHandler._thoraxBind = true;
|
534
|
+
object.listenTo(target, eventName, eventHandler);
|
535
|
+
}
|
536
|
+
|
537
|
+
function addEvents(target, source, context, listenToObject) {
|
538
|
+
function addEvent(callback, eventName) {
|
539
|
+
if (listenToObject) {
|
540
|
+
listenTo(target, target[listenToObject], eventName, callback, context || target);
|
541
|
+
} else {
|
542
|
+
target.push([eventName, callback, context]);
|
543
|
+
}
|
544
|
+
}
|
545
|
+
|
546
|
+
_.each(source, function(callback, eventName) {
|
547
|
+
if (_.isArray(callback)) {
|
548
|
+
_.each(callback, function(cb) {
|
549
|
+
addEvent(cb, eventName);
|
550
|
+
});
|
551
|
+
} else {
|
552
|
+
addEvent(callback, eventName);
|
553
|
+
}
|
554
|
+
});
|
555
|
+
}
|
556
|
+
|
557
|
+
function getOptionsData(options) {
|
558
|
+
if (!options || !options.data) {
|
559
|
+
throw new Error(createErrorMessage('handlebars-no-data'));
|
560
|
+
}
|
561
|
+
return options.data;
|
562
|
+
}
|
563
|
+
|
564
|
+
// In helpers "tagName" or "tag" may be specified, as well
|
565
|
+
// as "class" or "className". Normalize to "tagName" and
|
566
|
+
// "className" to match the property names used by Backbone
|
567
|
+
// jQuery, etc. Special case for "className" in
|
568
|
+
// Thorax.Util.tag: will be rewritten as "class" in
|
569
|
+
// generated HTML.
|
570
|
+
function normalizeHTMLAttributeOptions(options) {
|
571
|
+
if (options.tag) {
|
572
|
+
options.tagName = options.tag;
|
573
|
+
delete options.tag;
|
574
|
+
}
|
575
|
+
if (options['class']) {
|
576
|
+
options.className = options['class'];
|
577
|
+
delete options['class'];
|
578
|
+
}
|
579
|
+
}
|
580
|
+
|
581
|
+
Thorax.Util = {
|
582
|
+
getViewInstance: function(name, attributes) {
|
583
|
+
var ViewClass = Thorax.Util.getViewClass(name, true);
|
584
|
+
return ViewClass ? new ViewClass(attributes || {}) : name;
|
585
|
+
},
|
586
|
+
|
587
|
+
getViewClass: function(name, ignoreErrors) {
|
588
|
+
if (_.isString(name)) {
|
589
|
+
return registryGet(Thorax, 'Views', name, ignoreErrors);
|
590
|
+
} else if (_.isFunction(name)) {
|
591
|
+
return name;
|
592
|
+
} else {
|
593
|
+
return false;
|
594
|
+
}
|
595
|
+
},
|
596
|
+
|
597
|
+
getTemplate: function(file, ignoreErrors) {
|
598
|
+
//append the template path prefix if it is missing
|
599
|
+
var pathPrefix = Thorax.templatePathPrefix,
|
600
|
+
template;
|
601
|
+
if (pathPrefix && file.substr(0, pathPrefix.length) !== pathPrefix) {
|
602
|
+
file = pathPrefix + file;
|
603
|
+
}
|
604
|
+
|
605
|
+
// Without extension
|
606
|
+
file = file.replace(/\.handlebars$/, '');
|
607
|
+
template = Handlebars.templates[file];
|
608
|
+
if (!template) {
|
609
|
+
// With extension
|
610
|
+
file = file + '.handlebars';
|
611
|
+
template = Handlebars.templates[file];
|
612
|
+
}
|
613
|
+
|
614
|
+
if (!template && !ignoreErrors) {
|
615
|
+
throw new Error('templates: ' + file + ' does not exist.');
|
616
|
+
}
|
617
|
+
return template;
|
618
|
+
},
|
619
|
+
|
620
|
+
//'selector' is not present in $('<p></p>')
|
621
|
+
//TODO: investigage a better detection method
|
622
|
+
is$: function(obj) {
|
623
|
+
return _.isObject(obj) && ('length' in obj);
|
624
|
+
},
|
625
|
+
expandToken: function(input, scope) {
|
626
|
+
if (input && input.indexOf && input.indexOf('{{') >= 0) {
|
627
|
+
var re = /(?:\{?[^{]+)|(?:\{\{([^}]+)\}\})/g,
|
628
|
+
match,
|
629
|
+
ret = [];
|
630
|
+
function deref(token, scope) {
|
631
|
+
if (token.match(/^("|')/) && token.match(/("|')$/)) {
|
632
|
+
return token.replace(/(^("|')|('|")$)/g, '');
|
633
|
+
}
|
634
|
+
var segments = token.split('.'),
|
635
|
+
len = segments.length;
|
636
|
+
for (var i = 0; scope && i < len; i++) {
|
637
|
+
if (segments[i] !== 'this') {
|
638
|
+
scope = scope[segments[i]];
|
639
|
+
}
|
640
|
+
}
|
641
|
+
return scope;
|
642
|
+
}
|
643
|
+
while (match = re.exec(input)) {
|
644
|
+
if (match[1]) {
|
645
|
+
var params = match[1].split(/\s+/);
|
646
|
+
if (params.length > 1) {
|
647
|
+
var helper = params.shift();
|
648
|
+
params = _.map(params, function(param) { return deref(param, scope); });
|
649
|
+
if (Handlebars.helpers[helper]) {
|
650
|
+
ret.push(Handlebars.helpers[helper].apply(scope, params));
|
651
|
+
} else {
|
652
|
+
// If the helper is not defined do nothing
|
653
|
+
ret.push(match[0]);
|
654
|
+
}
|
655
|
+
} else {
|
656
|
+
ret.push(deref(params[0], scope));
|
657
|
+
}
|
658
|
+
} else {
|
659
|
+
ret.push(match[0]);
|
660
|
+
}
|
661
|
+
}
|
662
|
+
input = ret.join('');
|
663
|
+
}
|
664
|
+
return input;
|
665
|
+
},
|
666
|
+
tag: function(attributes, content, scope) {
|
667
|
+
var htmlAttributes = _.omit(attributes, 'tagName'),
|
668
|
+
tag = attributes.tagName || 'div';
|
669
|
+
return '<' + tag + ' ' + _.map(htmlAttributes, function(value, key) {
|
670
|
+
if (_.isUndefined(value) || key === 'expand-tokens') {
|
671
|
+
return '';
|
672
|
+
}
|
673
|
+
var formattedValue = value;
|
674
|
+
if (scope) {
|
675
|
+
formattedValue = Thorax.Util.expandToken(value, scope);
|
676
|
+
}
|
677
|
+
return (key === 'className' ? 'class' : key) + '="' + Handlebars.Utils.escapeExpression(formattedValue) + '"';
|
678
|
+
}).join(' ') + '>' + (_.isUndefined(content) ? '' : content) + '</' + tag + '>';
|
679
|
+
}
|
680
|
+
};
|
681
|
+
|
682
|
+
;;
|
683
|
+
Thorax.Mixins = {};
|
684
|
+
|
685
|
+
_.extend(Thorax.View, {
|
686
|
+
mixin: function(name) {
|
687
|
+
Thorax.Mixins[name](this);
|
688
|
+
},
|
689
|
+
registerMixin: function(name, callback, methods) {
|
690
|
+
Thorax.Mixins[name] = function(obj) {
|
691
|
+
var isInstance = !!obj.cid;
|
692
|
+
if (methods) {
|
693
|
+
_.extend(isInstance ? obj : obj.prototype, methods);
|
694
|
+
}
|
695
|
+
if (isInstance) {
|
696
|
+
callback.call(obj);
|
697
|
+
} else {
|
698
|
+
obj.on('configure', callback);
|
699
|
+
}
|
700
|
+
};
|
701
|
+
}
|
702
|
+
});
|
703
|
+
|
704
|
+
Thorax.View.prototype.mixin = function(name) {
|
705
|
+
Thorax.Mixins[name](this);
|
706
|
+
};
|
707
|
+
|
708
|
+
;;
|
709
|
+
/*global createInheritVars, inheritVars, listenTo, objectEvents, walkInheritTree */
|
710
|
+
// Save a copy of the _on method to call as a $super method
|
711
|
+
var _on = Thorax.View.prototype.on;
|
712
|
+
|
713
|
+
inheritVars.event = {
|
714
|
+
name: '_events',
|
715
|
+
|
716
|
+
configure: function() {
|
717
|
+
var self = this;
|
718
|
+
walkInheritTree(this.constructor, '_events', true, function(event) {
|
719
|
+
self.on.apply(self, event);
|
720
|
+
});
|
721
|
+
walkInheritTree(this, 'events', false, function(handler, eventName) {
|
722
|
+
self.on(eventName, handler, self);
|
723
|
+
});
|
724
|
+
}
|
725
|
+
};
|
726
|
+
|
727
|
+
_.extend(Thorax.View, {
|
728
|
+
on: function(eventName, callback) {
|
729
|
+
createInheritVars(this);
|
730
|
+
|
731
|
+
if (objectEvents(this, eventName, callback)) {
|
732
|
+
return this;
|
733
|
+
}
|
734
|
+
|
735
|
+
//accept on({"rendered": handler})
|
736
|
+
if (_.isObject(eventName)) {
|
737
|
+
_.each(eventName, function(value, key) {
|
738
|
+
this.on(key, value);
|
739
|
+
}, this);
|
740
|
+
} else {
|
741
|
+
//accept on({"rendered": [handler, handler]})
|
742
|
+
if (_.isArray(callback)) {
|
743
|
+
_.each(callback, function(cb) {
|
744
|
+
this._events.push([eventName, cb]);
|
745
|
+
}, this);
|
746
|
+
//accept on("rendered", handler)
|
747
|
+
} else {
|
748
|
+
this._events.push([eventName, callback]);
|
749
|
+
}
|
750
|
+
}
|
751
|
+
return this;
|
752
|
+
}
|
753
|
+
});
|
754
|
+
|
755
|
+
_.extend(Thorax.View.prototype, {
|
756
|
+
on: function(eventName, callback, context) {
|
757
|
+
if (objectEvents(this, eventName, callback, context)) {
|
758
|
+
return this;
|
759
|
+
}
|
760
|
+
|
761
|
+
if (_.isObject(eventName) && arguments.length < 3) {
|
762
|
+
//accept on({"rendered": callback})
|
763
|
+
_.each(eventName, function(value, key) {
|
764
|
+
this.on(key, value, callback || this); // callback is context in this form of the call
|
765
|
+
}, this);
|
766
|
+
} else {
|
767
|
+
//accept on("rendered", callback, context)
|
768
|
+
//accept on("click a", callback, context)
|
769
|
+
_.each((_.isArray(callback) ? callback : [callback]), function(callback) {
|
770
|
+
var params = eventParamsFromEventItem.call(this, eventName, callback, context || this);
|
771
|
+
if (params.type === 'DOM' && !this._eventsDelegated) {
|
772
|
+
//will call _addEvent during delegateEvents()
|
773
|
+
if (!this._eventsToDelegate) {
|
774
|
+
this._eventsToDelegate = [];
|
775
|
+
}
|
776
|
+
this._eventsToDelegate.push(params);
|
777
|
+
} else {
|
778
|
+
this._addEvent(params);
|
779
|
+
}
|
780
|
+
}, this);
|
781
|
+
}
|
782
|
+
return this;
|
783
|
+
},
|
784
|
+
delegateEvents: function(events) {
|
785
|
+
this.undelegateEvents();
|
786
|
+
if (events) {
|
787
|
+
if (_.isFunction(events)) {
|
788
|
+
events = events.call(this);
|
789
|
+
}
|
790
|
+
this._eventsToDelegate = [];
|
791
|
+
this.on(events);
|
792
|
+
}
|
793
|
+
this._eventsToDelegate && _.each(this._eventsToDelegate, this._addEvent, this);
|
794
|
+
this._eventsDelegated = true;
|
795
|
+
},
|
796
|
+
//params may contain:
|
797
|
+
//- name
|
798
|
+
//- originalName
|
799
|
+
//- selector
|
800
|
+
//- type "view" || "DOM"
|
801
|
+
//- handler
|
802
|
+
_addEvent: function(params) {
|
803
|
+
// If this is recursvie due to listenTo delegate below then pass through to super class
|
804
|
+
if (params.handler._thoraxBind) {
|
805
|
+
return _on.call(this, params.name, params.handler, params.context || this);
|
806
|
+
}
|
807
|
+
|
808
|
+
var boundHandler = bindEventHandler.call(this, params.type + '-event:', params);
|
809
|
+
|
810
|
+
if (params.type === 'view') {
|
811
|
+
// If we have our context set to an outside view then listen rather than directly bind so
|
812
|
+
// we can cleanup properly.
|
813
|
+
if (params.context && params.context !== this && params.context instanceof Thorax.View) {
|
814
|
+
listenTo(params.context, this, params.name, boundHandler, params.context);
|
815
|
+
} else {
|
816
|
+
_on.call(this, params.name, boundHandler, params.context || this);
|
817
|
+
}
|
818
|
+
} else {
|
819
|
+
if (!params.nested) {
|
820
|
+
boundHandler = containHandlerToCurentView(boundHandler, this.cid);
|
821
|
+
}
|
822
|
+
|
823
|
+
var name = params.name + '.delegateEvents' + this.cid;
|
824
|
+
if (params.selector) {
|
825
|
+
this.$el.on(name, params.selector, boundHandler);
|
826
|
+
} else {
|
827
|
+
this.$el.on(name, boundHandler);
|
828
|
+
}
|
829
|
+
}
|
830
|
+
}
|
831
|
+
});
|
832
|
+
|
833
|
+
Thorax.View.prototype.bind = Thorax.View.prototype.on;
|
834
|
+
|
835
|
+
// When view is ready trigger ready event on all
|
836
|
+
// children that are present, then register an
|
837
|
+
// event that will trigger ready on new children
|
838
|
+
// when they are added
|
839
|
+
Thorax.View.on('ready', function(options) {
|
840
|
+
if (!this._isReady) {
|
841
|
+
this._isReady = true;
|
842
|
+
function triggerReadyOnChild(child) {
|
843
|
+
child._isReady || child.trigger('ready', options);
|
844
|
+
}
|
845
|
+
_.each(this.children, triggerReadyOnChild);
|
846
|
+
this.on('child', triggerReadyOnChild);
|
847
|
+
}
|
848
|
+
});
|
849
|
+
|
850
|
+
var eventSplitter = /^(nested\s+)?(\S+)(?:\s+(.+))?/;
|
851
|
+
|
852
|
+
var domEvents = [],
|
853
|
+
domEventRegexp;
|
854
|
+
function pushDomEvents(events) {
|
855
|
+
domEvents.push.apply(domEvents, events);
|
856
|
+
domEventRegexp = new RegExp('^(nested\\s+)?(' + domEvents.join('|') + ')(?:\\s|$)');
|
857
|
+
}
|
858
|
+
pushDomEvents([
|
859
|
+
'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout',
|
860
|
+
'touchstart', 'touchend', 'touchmove',
|
861
|
+
'click', 'dblclick',
|
862
|
+
'keyup', 'keydown', 'keypress',
|
863
|
+
'submit', 'change',
|
864
|
+
'focus', 'blur'
|
865
|
+
]);
|
866
|
+
|
867
|
+
function containHandlerToCurentView(handler, cid) {
|
868
|
+
return function(event) {
|
869
|
+
var view = $(event.target).view({helper: false});
|
870
|
+
if (view && view.cid === cid) {
|
871
|
+
event.originalContext = this;
|
872
|
+
handler(event);
|
873
|
+
}
|
874
|
+
};
|
875
|
+
}
|
876
|
+
|
877
|
+
function bindEventHandler(eventName, params) {
|
878
|
+
eventName += params.originalName;
|
879
|
+
|
880
|
+
var callback = params.handler,
|
881
|
+
method = _.isFunction(callback) ? callback : this[callback];
|
882
|
+
if (!method) {
|
883
|
+
throw new Error('Event "' + callback + '" does not exist ' + (this.name || this.cid) + ':' + eventName);
|
884
|
+
}
|
885
|
+
|
886
|
+
var context = params.context || this;
|
887
|
+
function ret() {
|
888
|
+
try {
|
889
|
+
method.apply(context, arguments);
|
890
|
+
} catch (e) {
|
891
|
+
Thorax.onException('thorax-exception: ' + (context.name || context.cid) + ':' + eventName, e);
|
892
|
+
}
|
893
|
+
}
|
894
|
+
// Backbone will delegate to _callback in off calls so we should still be able to support
|
895
|
+
// calling off on specific handlers.
|
896
|
+
ret._callback = method;
|
897
|
+
ret._thoraxBind = true;
|
898
|
+
return ret;
|
899
|
+
}
|
900
|
+
|
901
|
+
function eventParamsFromEventItem(name, handler, context) {
|
902
|
+
var params = {
|
903
|
+
originalName: name,
|
904
|
+
handler: _.isString(handler) ? this[handler] : handler
|
905
|
+
};
|
906
|
+
if (name.match(domEventRegexp)) {
|
907
|
+
var match = eventSplitter.exec(name);
|
908
|
+
params.nested = !!match[1];
|
909
|
+
params.name = match[2];
|
910
|
+
params.type = 'DOM';
|
911
|
+
params.selector = match[3];
|
912
|
+
} else {
|
913
|
+
params.name = name;
|
914
|
+
params.type = 'view';
|
915
|
+
}
|
916
|
+
params.context = context;
|
917
|
+
return params;
|
918
|
+
}
|
919
|
+
|
920
|
+
;;
|
921
|
+
/*global getOptionsData, normalizeHTMLAttributeOptions, viewHelperAttributeName */
|
922
|
+
var viewPlaceholderAttributeName = 'data-view-tmp',
|
923
|
+
viewTemplateOverrides = {};
|
924
|
+
|
925
|
+
// Will be shared by HelperView and CollectionHelperView
|
926
|
+
var helperViewPrototype = {
|
927
|
+
_ensureElement: function() {
|
928
|
+
Thorax.View.prototype._ensureElement.apply(this, arguments);
|
929
|
+
this.$el.attr(viewHelperAttributeName, this._helperName);
|
930
|
+
},
|
931
|
+
_getContext: function() {
|
932
|
+
return this.parent._getContext.apply(this.parent, arguments);
|
933
|
+
}
|
934
|
+
};
|
935
|
+
|
936
|
+
Thorax.HelperView = Thorax.View.extend(helperViewPrototype);
|
937
|
+
|
938
|
+
// Ensure nested inline helpers will always have this.parent
|
939
|
+
// set to the view containing the template
|
940
|
+
function getParent(parent) {
|
941
|
+
// The `view` helper is a special case as it embeds
|
942
|
+
// a view instead of creating a new one
|
943
|
+
while (parent._helperName && parent._helperName !== 'view') {
|
944
|
+
parent = parent.parent;
|
945
|
+
}
|
946
|
+
return parent;
|
947
|
+
}
|
948
|
+
|
949
|
+
Handlebars.registerViewHelper = function(name, ViewClass, callback) {
|
950
|
+
if (arguments.length === 2) {
|
951
|
+
if (ViewClass.factory) {
|
952
|
+
callback = ViewClass.callback;
|
953
|
+
} else {
|
954
|
+
callback = ViewClass;
|
955
|
+
ViewClass = Thorax.HelperView;
|
956
|
+
}
|
957
|
+
}
|
958
|
+
|
959
|
+
var viewOptionWhiteList = ViewClass.attributeWhiteList;
|
960
|
+
|
961
|
+
Handlebars.registerHelper(name, function() {
|
962
|
+
var args = _.toArray(arguments),
|
963
|
+
options = args.pop(),
|
964
|
+
declaringView = getOptionsData(options).view,
|
965
|
+
expandTokens = options.hash['expand-tokens'];
|
966
|
+
|
967
|
+
if (expandTokens) {
|
968
|
+
delete options.hash['expand-tokens'];
|
969
|
+
_.each(options.hash, function(value, key) {
|
970
|
+
options.hash[key] = Thorax.Util.expandToken(value, this);
|
971
|
+
}, this);
|
972
|
+
}
|
973
|
+
|
974
|
+
var viewOptions = {
|
975
|
+
inverse: options.inverse,
|
976
|
+
options: options.hash,
|
977
|
+
declaringView: declaringView,
|
978
|
+
parent: getParent(declaringView),
|
979
|
+
_helperName: name,
|
980
|
+
_helperOptions: {
|
981
|
+
options: cloneHelperOptions(options),
|
982
|
+
args: _.clone(args)
|
983
|
+
}
|
984
|
+
};
|
985
|
+
|
986
|
+
|
987
|
+
normalizeHTMLAttributeOptions(options.hash);
|
988
|
+
var htmlAttributes = _.clone(options.hash);
|
989
|
+
if (viewOptionWhiteList) {
|
990
|
+
_.each(viewOptionWhiteList, function(dest, source) {
|
991
|
+
delete htmlAttributes[source];
|
992
|
+
if (!_.isUndefined(options.hash[source])) {
|
993
|
+
viewOptions[dest] = options.hash[source];
|
994
|
+
}
|
995
|
+
});
|
996
|
+
}
|
997
|
+
if(htmlAttributes.tagName) {
|
998
|
+
viewOptions.tagName = htmlAttributes.tagName;
|
999
|
+
}
|
1000
|
+
viewOptions.attributes = function() {
|
1001
|
+
var attrs = (ViewClass.prototype && ViewClass.prototype.attributes) || {};
|
1002
|
+
if (_.isFunction(attrs)) {
|
1003
|
+
attrs = attrs.apply(this, arguments);
|
1004
|
+
}
|
1005
|
+
_.extend(attrs, _.omit(htmlAttributes, ['tagName']));
|
1006
|
+
// backbone wants "class"
|
1007
|
+
if (attrs.className) {
|
1008
|
+
attrs['class'] = attrs.className;
|
1009
|
+
delete attrs.className;
|
1010
|
+
}
|
1011
|
+
return attrs;
|
1012
|
+
};
|
1013
|
+
|
1014
|
+
if (options.fn) {
|
1015
|
+
// Only assign if present, allow helper view class to
|
1016
|
+
// declare template
|
1017
|
+
viewOptions.template = options.fn;
|
1018
|
+
} else if (ViewClass && ViewClass.prototype && !ViewClass.prototype.template) {
|
1019
|
+
// ViewClass may also be an instance or object with factory method
|
1020
|
+
// so need to do this check
|
1021
|
+
viewOptions.template = Handlebars.VM.noop;
|
1022
|
+
}
|
1023
|
+
|
1024
|
+
// Check to see if we have an existing instance that we can reuse
|
1025
|
+
var instance = _.find(declaringView._previousHelpers, function(child) {
|
1026
|
+
return compareHelperOptions(viewOptions, child);
|
1027
|
+
});
|
1028
|
+
|
1029
|
+
// Create the instance if we don't already have one
|
1030
|
+
if (!instance) {
|
1031
|
+
if (ViewClass.factory) {
|
1032
|
+
instance = ViewClass.factory(args, viewOptions);
|
1033
|
+
if (!instance) {
|
1034
|
+
return '';
|
1035
|
+
}
|
1036
|
+
|
1037
|
+
instance._helperName = viewOptions._helperName;
|
1038
|
+
instance._helperOptions = viewOptions._helperOptions;
|
1039
|
+
} else {
|
1040
|
+
instance = new ViewClass(viewOptions);
|
1041
|
+
}
|
1042
|
+
|
1043
|
+
args.push(instance);
|
1044
|
+
declaringView._addChild(instance);
|
1045
|
+
declaringView.trigger.apply(declaringView, ['helper', name].concat(args));
|
1046
|
+
declaringView.trigger.apply(declaringView, ['helper:' + name].concat(args));
|
1047
|
+
|
1048
|
+
callback && callback.apply(this, args);
|
1049
|
+
} else {
|
1050
|
+
declaringView._previousHelpers = _.without(declaringView._previousHelpers, instance);
|
1051
|
+
declaringView.children[instance.cid] = instance;
|
1052
|
+
}
|
1053
|
+
|
1054
|
+
htmlAttributes[viewPlaceholderAttributeName] = instance.cid;
|
1055
|
+
if (ViewClass.modifyHTMLAttributes) {
|
1056
|
+
ViewClass.modifyHTMLAttributes(htmlAttributes, instance);
|
1057
|
+
}
|
1058
|
+
return new Handlebars.SafeString(Thorax.Util.tag(htmlAttributes, '', expandTokens ? this : null));
|
1059
|
+
});
|
1060
|
+
var helper = Handlebars.helpers[name];
|
1061
|
+
return helper;
|
1062
|
+
};
|
1063
|
+
|
1064
|
+
Thorax.View.on('append', function(scope, callback) {
|
1065
|
+
(scope || this.$el).find('[' + viewPlaceholderAttributeName + ']').forEach(function(el) {
|
1066
|
+
var placeholderId = el.getAttribute(viewPlaceholderAttributeName),
|
1067
|
+
view = this.children[placeholderId];
|
1068
|
+
if (view) {
|
1069
|
+
//see if the view helper declared an override for the view
|
1070
|
+
//if not, ensure the view has been rendered at least once
|
1071
|
+
if (viewTemplateOverrides[placeholderId]) {
|
1072
|
+
view.render(viewTemplateOverrides[placeholderId]);
|
1073
|
+
delete viewTemplateOverrides[placeholderId];
|
1074
|
+
} else {
|
1075
|
+
view.ensureRendered();
|
1076
|
+
}
|
1077
|
+
$(el).replaceWith(view.el);
|
1078
|
+
callback && callback(view.el);
|
1079
|
+
}
|
1080
|
+
}, this);
|
1081
|
+
});
|
1082
|
+
|
1083
|
+
|
1084
|
+
/**
|
1085
|
+
* Clones the helper options, dropping items that are known to change
|
1086
|
+
* between rendering cycles as appropriate.
|
1087
|
+
*/
|
1088
|
+
function cloneHelperOptions(options) {
|
1089
|
+
var ret = _.pick(options, 'fn', 'inverse', 'hash', 'data');
|
1090
|
+
ret.data = _.omit(options.data, 'cid', 'view', 'yield');
|
1091
|
+
return ret;
|
1092
|
+
}
|
1093
|
+
|
1094
|
+
/**
|
1095
|
+
* Checks for basic equality between two sets of parameters for a helper view.
|
1096
|
+
*
|
1097
|
+
* Checked fields include:
|
1098
|
+
* - _helperName
|
1099
|
+
* - All args
|
1100
|
+
* - Hash
|
1101
|
+
* - Data
|
1102
|
+
* - Function and Invert (id based if possible)
|
1103
|
+
*
|
1104
|
+
* This method allows us to determine if the inputs to a given view are the same. If they
|
1105
|
+
* are then we make the assumption that the rendering will be the same (or the child view will
|
1106
|
+
* otherwise rerendering it by monitoring it's parameters as necessary) and reuse the view on
|
1107
|
+
* rerender of the parent view.
|
1108
|
+
*/
|
1109
|
+
function compareHelperOptions(a, b) {
|
1110
|
+
function compareValues(a, b) {
|
1111
|
+
return _.every(a, function(value, key) {
|
1112
|
+
return b[key] === value;
|
1113
|
+
});
|
1114
|
+
}
|
1115
|
+
|
1116
|
+
if (a._helperName !== b._helperName) {
|
1117
|
+
return false;
|
1118
|
+
}
|
1119
|
+
|
1120
|
+
a = a._helperOptions;
|
1121
|
+
b = b._helperOptions;
|
1122
|
+
|
1123
|
+
// Implements a first level depth comparison
|
1124
|
+
return a.args.length === b.args.length
|
1125
|
+
&& compareValues(a.args, b.args)
|
1126
|
+
&& _.isEqual(_.keys(a.options), _.keys(b.options))
|
1127
|
+
&& _.every(a.options, function(value, key) {
|
1128
|
+
if (key === 'data' || key === 'hash') {
|
1129
|
+
return compareValues(a.options[key], b.options[key]);
|
1130
|
+
} else if (key === 'fn' || key === 'inverse') {
|
1131
|
+
if (b.options[key] === value) {
|
1132
|
+
return true;
|
1133
|
+
}
|
1134
|
+
|
1135
|
+
var other = b.options[key] || {};
|
1136
|
+
return value && _.has(value, 'program') && !value.depth && other.program === value.program;
|
1137
|
+
}
|
1138
|
+
return b.options[key] === value;
|
1139
|
+
});
|
1140
|
+
}
|
1141
|
+
|
1142
|
+
;;
|
1143
|
+
/*global getValue, inheritVars, walkInheritTree */
|
1144
|
+
|
1145
|
+
function dataObject(type, spec) {
|
1146
|
+
spec = inheritVars[type] = _.defaults({
|
1147
|
+
name: '_' + type + 'Events',
|
1148
|
+
event: true
|
1149
|
+
}, spec);
|
1150
|
+
|
1151
|
+
// Add a callback in the view constructor
|
1152
|
+
spec.ctor = function() {
|
1153
|
+
if (this[type]) {
|
1154
|
+
// Need to null this.model/collection so setModel/Collection will
|
1155
|
+
// not treat it as the old model/collection and immediately return
|
1156
|
+
var object = this[type];
|
1157
|
+
this[type] = null;
|
1158
|
+
this[spec.set](object);
|
1159
|
+
}
|
1160
|
+
};
|
1161
|
+
|
1162
|
+
function setObject(dataObject, options) {
|
1163
|
+
var old = this[type],
|
1164
|
+
$el = getValue(this, spec.$el);
|
1165
|
+
|
1166
|
+
if (dataObject === old) {
|
1167
|
+
return this;
|
1168
|
+
}
|
1169
|
+
if (old) {
|
1170
|
+
this.unbindDataObject(old);
|
1171
|
+
}
|
1172
|
+
|
1173
|
+
if (dataObject) {
|
1174
|
+
this[type] = dataObject;
|
1175
|
+
|
1176
|
+
if (spec.loading) {
|
1177
|
+
spec.loading.call(this);
|
1178
|
+
}
|
1179
|
+
|
1180
|
+
this.bindDataObject(type, dataObject, _.extend({}, this.options, options));
|
1181
|
+
$el && $el.attr(spec.cidAttrName, dataObject.cid);
|
1182
|
+
dataObject.trigger('set', dataObject, old);
|
1183
|
+
} else {
|
1184
|
+
this[type] = false;
|
1185
|
+
if (spec.change) {
|
1186
|
+
spec.change.call(this, false);
|
1187
|
+
}
|
1188
|
+
$el && $el.removeAttr(spec.cidAttrName);
|
1189
|
+
}
|
1190
|
+
this.trigger('change:data-object', type, dataObject, old);
|
1191
|
+
return this;
|
1192
|
+
}
|
1193
|
+
|
1194
|
+
Thorax.View.prototype[spec.set] = setObject;
|
1195
|
+
}
|
1196
|
+
|
1197
|
+
_.extend(Thorax.View.prototype, {
|
1198
|
+
getObjectOptions: function(dataObject) {
|
1199
|
+
return dataObject && this._objectOptionsByCid[dataObject.cid];
|
1200
|
+
},
|
1201
|
+
|
1202
|
+
bindDataObject: function(type, dataObject, options) {
|
1203
|
+
if (this._boundDataObjectsByCid[dataObject.cid]) {
|
1204
|
+
return false;
|
1205
|
+
}
|
1206
|
+
this._boundDataObjectsByCid[dataObject.cid] = dataObject;
|
1207
|
+
|
1208
|
+
var options = this._modifyDataObjectOptions(dataObject, _.extend({}, inheritVars[type].defaultOptions, options));
|
1209
|
+
this._objectOptionsByCid[dataObject.cid] = options;
|
1210
|
+
|
1211
|
+
bindEvents.call(this, type, dataObject, this.constructor);
|
1212
|
+
bindEvents.call(this, type, dataObject, this);
|
1213
|
+
|
1214
|
+
var spec = inheritVars[type];
|
1215
|
+
spec.bindCallback && spec.bindCallback.call(this, dataObject, options);
|
1216
|
+
|
1217
|
+
if (dataObject.shouldFetch && dataObject.shouldFetch(options)) {
|
1218
|
+
loadObject(dataObject, options);
|
1219
|
+
} else if (inheritVars[type].change) {
|
1220
|
+
// want to trigger built in rendering without triggering event on model
|
1221
|
+
inheritVars[type].change.call(this, dataObject, options);
|
1222
|
+
}
|
1223
|
+
|
1224
|
+
return true;
|
1225
|
+
},
|
1226
|
+
|
1227
|
+
unbindDataObject: function (dataObject) {
|
1228
|
+
if (!this._boundDataObjectsByCid[dataObject.cid]) {
|
1229
|
+
return false;
|
1230
|
+
}
|
1231
|
+
delete this._boundDataObjectsByCid[dataObject.cid];
|
1232
|
+
this.stopListening(dataObject);
|
1233
|
+
delete this._objectOptionsByCid[dataObject.cid];
|
1234
|
+
return true;
|
1235
|
+
},
|
1236
|
+
|
1237
|
+
_modifyDataObjectOptions: function(dataObject, options) {
|
1238
|
+
return options;
|
1239
|
+
}
|
1240
|
+
});
|
1241
|
+
|
1242
|
+
function bindEvents(type, target, source) {
|
1243
|
+
var context = this;
|
1244
|
+
walkInheritTree(source, '_' + type + 'Events', true, function(event) {
|
1245
|
+
listenTo(context, target, event[0], event[1], event[2] || context);
|
1246
|
+
});
|
1247
|
+
}
|
1248
|
+
|
1249
|
+
function loadObject(dataObject, options) {
|
1250
|
+
if (dataObject.load) {
|
1251
|
+
dataObject.load(function() {
|
1252
|
+
options && options.success && options.success(dataObject);
|
1253
|
+
}, options);
|
1254
|
+
} else {
|
1255
|
+
dataObject.fetch(options);
|
1256
|
+
}
|
1257
|
+
}
|
1258
|
+
|
1259
|
+
function getEventCallback(callback, context) {
|
1260
|
+
if (_.isFunction(callback)) {
|
1261
|
+
return callback;
|
1262
|
+
} else {
|
1263
|
+
return context[callback];
|
1264
|
+
}
|
1265
|
+
}
|
1266
|
+
|
1267
|
+
;;
|
1268
|
+
/*global createRegistryWrapper, dataObject, getValue, inheritVars */
|
1269
|
+
var modelCidAttributeName = 'data-model-cid';
|
1270
|
+
|
1271
|
+
Thorax.Model = Backbone.Model.extend({
|
1272
|
+
isEmpty: function() {
|
1273
|
+
return !this.isPopulated();
|
1274
|
+
},
|
1275
|
+
isPopulated: function() {
|
1276
|
+
// We are populated if we have attributes set
|
1277
|
+
var attributes = _.clone(this.attributes),
|
1278
|
+
defaults = getValue(this, 'defaults') || {};
|
1279
|
+
for (var default_key in defaults) {
|
1280
|
+
if (attributes[default_key] != defaults[default_key]) {
|
1281
|
+
return true;
|
1282
|
+
}
|
1283
|
+
delete attributes[default_key];
|
1284
|
+
}
|
1285
|
+
var keys = _.keys(attributes);
|
1286
|
+
return keys.length > 1 || (keys.length === 1 && keys[0] !== this.idAttribute);
|
1287
|
+
},
|
1288
|
+
shouldFetch: function(options) {
|
1289
|
+
// url() will throw if model has no `urlRoot` and no `collection`
|
1290
|
+
// or has `collection` and `collection` has no `url`
|
1291
|
+
var url;
|
1292
|
+
try {
|
1293
|
+
url = getValue(this, 'url');
|
1294
|
+
} catch(e) {
|
1295
|
+
url = false;
|
1296
|
+
}
|
1297
|
+
return options.fetch && !!url && !this.isPopulated();
|
1298
|
+
}
|
1299
|
+
});
|
1300
|
+
|
1301
|
+
Thorax.Models = {};
|
1302
|
+
createRegistryWrapper(Thorax.Model, Thorax.Models);
|
1303
|
+
|
1304
|
+
dataObject('model', {
|
1305
|
+
set: 'setModel',
|
1306
|
+
defaultOptions: {
|
1307
|
+
render: undefined, // Default to deferred rendering
|
1308
|
+
fetch: true,
|
1309
|
+
success: false,
|
1310
|
+
invalid: true
|
1311
|
+
},
|
1312
|
+
change: onModelChange,
|
1313
|
+
$el: '$el',
|
1314
|
+
cidAttrName: modelCidAttributeName
|
1315
|
+
});
|
1316
|
+
|
1317
|
+
function onModelChange(model, options) {
|
1318
|
+
if (options && options.serializing) {
|
1319
|
+
return;
|
1320
|
+
}
|
1321
|
+
|
1322
|
+
var modelOptions = this.getObjectOptions(model) || {};
|
1323
|
+
// !modelOptions will be true when setModel(false) is called
|
1324
|
+
this.conditionalRender(modelOptions.render);
|
1325
|
+
}
|
1326
|
+
|
1327
|
+
Thorax.View.on({
|
1328
|
+
model: {
|
1329
|
+
invalid: function(model, errors) {
|
1330
|
+
if (this.getObjectOptions(model).invalid) {
|
1331
|
+
this.trigger('invalid', errors, model);
|
1332
|
+
}
|
1333
|
+
},
|
1334
|
+
error: function(model, resp, options) {
|
1335
|
+
this.trigger('error', resp, model);
|
1336
|
+
},
|
1337
|
+
change: function(model, options) {
|
1338
|
+
// Indirect refernece to allow for overrides
|
1339
|
+
inheritVars.model.change.call(this, model, options);
|
1340
|
+
}
|
1341
|
+
}
|
1342
|
+
});
|
1343
|
+
|
1344
|
+
$.fn.model = function(view) {
|
1345
|
+
var $this = $(this),
|
1346
|
+
modelElement = $this.closest('[' + modelCidAttributeName + ']'),
|
1347
|
+
modelCid = modelElement && modelElement.attr(modelCidAttributeName);
|
1348
|
+
if (modelCid) {
|
1349
|
+
var view = view || $this.view();
|
1350
|
+
if (view && view.model && view.model.cid === modelCid) {
|
1351
|
+
return view.model || false;
|
1352
|
+
}
|
1353
|
+
var collection = $this.collection(view);
|
1354
|
+
if (collection) {
|
1355
|
+
return collection.get(modelCid);
|
1356
|
+
}
|
1357
|
+
}
|
1358
|
+
return false;
|
1359
|
+
};
|
1360
|
+
|
1361
|
+
;;
|
1362
|
+
/*global assignView, assignTemplate, createRegistryWrapper, dataObject, getEventCallback, getValue, modelCidAttributeName, viewCidAttributeName */
|
1363
|
+
var _fetch = Backbone.Collection.prototype.fetch,
|
1364
|
+
_set = Backbone.Collection.prototype.set,
|
1365
|
+
_replaceHTML = Thorax.View.prototype._replaceHTML,
|
1366
|
+
collectionCidAttributeName = 'data-collection-cid',
|
1367
|
+
collectionEmptyAttributeName = 'data-collection-empty',
|
1368
|
+
collectionElementAttributeName = 'data-collection-element',
|
1369
|
+
ELEMENT_NODE_TYPE = 1;
|
1370
|
+
|
1371
|
+
Thorax.Collection = Backbone.Collection.extend({
|
1372
|
+
model: Thorax.Model || Backbone.Model,
|
1373
|
+
initialize: function() {
|
1374
|
+
this.cid = _.uniqueId('collection');
|
1375
|
+
return Backbone.Collection.prototype.initialize.apply(this, arguments);
|
1376
|
+
},
|
1377
|
+
isEmpty: function() {
|
1378
|
+
if (this.length > 0) {
|
1379
|
+
return false;
|
1380
|
+
} else {
|
1381
|
+
return this.length === 0 && this.isPopulated();
|
1382
|
+
}
|
1383
|
+
},
|
1384
|
+
isPopulated: function() {
|
1385
|
+
return this._fetched || this.length > 0 || (!this.length && !getValue(this, 'url'));
|
1386
|
+
},
|
1387
|
+
shouldFetch: function(options) {
|
1388
|
+
return options.fetch && !!getValue(this, 'url') && !this.isPopulated();
|
1389
|
+
},
|
1390
|
+
fetch: function(options) {
|
1391
|
+
options = options || {};
|
1392
|
+
var success = options.success;
|
1393
|
+
options.success = function(collection, response) {
|
1394
|
+
collection._fetched = true;
|
1395
|
+
success && success(collection, response);
|
1396
|
+
};
|
1397
|
+
return _fetch.apply(this, arguments);
|
1398
|
+
},
|
1399
|
+
set: function(models, options) {
|
1400
|
+
this._fetched = !!models;
|
1401
|
+
return _set.call(this, models, options);
|
1402
|
+
}
|
1403
|
+
});
|
1404
|
+
|
1405
|
+
_.extend(Thorax.View.prototype, {
|
1406
|
+
getCollectionViews: function(collection) {
|
1407
|
+
return _.filter(this.children, function(child) {
|
1408
|
+
if (!(child instanceof Thorax.CollectionView)) {
|
1409
|
+
return false;
|
1410
|
+
}
|
1411
|
+
|
1412
|
+
return !collection || (child.collection === collection);
|
1413
|
+
});
|
1414
|
+
},
|
1415
|
+
updateFilter: function(collection) {
|
1416
|
+
_.invoke(this.getCollectionViews(collection), 'updateFilter');
|
1417
|
+
}
|
1418
|
+
});
|
1419
|
+
|
1420
|
+
Thorax.Collections = {};
|
1421
|
+
createRegistryWrapper(Thorax.Collection, Thorax.Collections);
|
1422
|
+
|
1423
|
+
dataObject('collection', {
|
1424
|
+
set: 'setCollection',
|
1425
|
+
bindCallback: onSetCollection,
|
1426
|
+
defaultOptions: {
|
1427
|
+
render: undefined, // Default to deferred rendering
|
1428
|
+
fetch: true,
|
1429
|
+
success: false,
|
1430
|
+
invalid: true,
|
1431
|
+
change: true // Wether or not to re-render on model:change
|
1432
|
+
},
|
1433
|
+
change: onCollectionReset,
|
1434
|
+
$el: 'getCollectionElement',
|
1435
|
+
cidAttrName: collectionCidAttributeName
|
1436
|
+
});
|
1437
|
+
|
1438
|
+
Thorax.CollectionView = Thorax.View.extend({
|
1439
|
+
_defaultTemplate: Handlebars.VM.noop,
|
1440
|
+
_collectionSelector: '[' + collectionElementAttributeName + ']',
|
1441
|
+
|
1442
|
+
// preserve collection element if it was not created with {{collection}} helper
|
1443
|
+
_replaceHTML: function(html) {
|
1444
|
+
if (this.collection && this.getObjectOptions(this.collection) && this._renderCount) {
|
1445
|
+
var element;
|
1446
|
+
var oldCollectionElement = this.getCollectionElement();
|
1447
|
+
element = _replaceHTML.call(this, html);
|
1448
|
+
if (!oldCollectionElement.attr('data-view-cid')) {
|
1449
|
+
this.getCollectionElement().replaceWith(oldCollectionElement);
|
1450
|
+
}
|
1451
|
+
} else {
|
1452
|
+
return _replaceHTML.call(this, html);
|
1453
|
+
}
|
1454
|
+
},
|
1455
|
+
|
1456
|
+
render: function() {
|
1457
|
+
var shouldRender = this.shouldRender();
|
1458
|
+
|
1459
|
+
Thorax.View.prototype.render.apply(this, arguments);
|
1460
|
+
if (!shouldRender) {
|
1461
|
+
this.renderCollection();
|
1462
|
+
}
|
1463
|
+
},
|
1464
|
+
|
1465
|
+
//appendItem(model [,index])
|
1466
|
+
//appendItem(html_string, index)
|
1467
|
+
//appendItem(view, index)
|
1468
|
+
appendItem: function(model, index, options) {
|
1469
|
+
//empty item
|
1470
|
+
if (!model) {
|
1471
|
+
return;
|
1472
|
+
}
|
1473
|
+
var itemView,
|
1474
|
+
$el = this.getCollectionElement();
|
1475
|
+
options = _.defaults(options || {}, {
|
1476
|
+
filter: true
|
1477
|
+
});
|
1478
|
+
//if index argument is a view
|
1479
|
+
index && index.el && (index = $el.children().indexOf(index.el) + 1);
|
1480
|
+
//if argument is a view, or html string
|
1481
|
+
if (model.el || _.isString(model)) {
|
1482
|
+
itemView = model;
|
1483
|
+
model = false;
|
1484
|
+
} else {
|
1485
|
+
index = index || this.collection.indexOf(model) || 0;
|
1486
|
+
itemView = this.renderItem(model, index);
|
1487
|
+
}
|
1488
|
+
|
1489
|
+
if (itemView) {
|
1490
|
+
if (itemView.cid) {
|
1491
|
+
itemView.ensureRendered();
|
1492
|
+
this._addChild(itemView);
|
1493
|
+
}
|
1494
|
+
|
1495
|
+
//if the renderer's output wasn't contained in a tag, wrap it in a div
|
1496
|
+
//plain text, or a mixture of top level text nodes and element nodes
|
1497
|
+
//will get wrapped
|
1498
|
+
if (_.isString(itemView) && !itemView.match(/^\s*</m)) {
|
1499
|
+
itemView = '<div>' + itemView + '</div>';
|
1500
|
+
}
|
1501
|
+
var itemElement = itemView.$el || $($.trim(itemView)).filter(function() {
|
1502
|
+
//filter out top level whitespace nodes
|
1503
|
+
return this.nodeType === ELEMENT_NODE_TYPE;
|
1504
|
+
});
|
1505
|
+
|
1506
|
+
if (model) {
|
1507
|
+
itemElement.attr(modelCidAttributeName, model.cid);
|
1508
|
+
}
|
1509
|
+
var previousModel = index > 0 ? this.collection.at(index - 1) : false;
|
1510
|
+
if (!previousModel) {
|
1511
|
+
$el.prepend(itemElement);
|
1512
|
+
} else {
|
1513
|
+
//use last() as appendItem can accept multiple nodes from a template
|
1514
|
+
var last = $el.children('[' + modelCidAttributeName + '="' + previousModel.cid + '"]').last();
|
1515
|
+
last.after(itemElement);
|
1516
|
+
}
|
1517
|
+
|
1518
|
+
this.trigger('append', null, function(el) {
|
1519
|
+
el.setAttribute(modelCidAttributeName, model.cid);
|
1520
|
+
});
|
1521
|
+
|
1522
|
+
if (!options.silent) {
|
1523
|
+
this.trigger('rendered:item', this, this.collection, model, itemElement, index);
|
1524
|
+
}
|
1525
|
+
if (options.filter) {
|
1526
|
+
applyItemVisiblityFilter.call(this, model);
|
1527
|
+
}
|
1528
|
+
}
|
1529
|
+
return itemView;
|
1530
|
+
},
|
1531
|
+
|
1532
|
+
// updateItem only useful if there is no item view, otherwise
|
1533
|
+
// itemView.render() provides the same functionality
|
1534
|
+
updateItem: function(model) {
|
1535
|
+
var $el = this.getCollectionElement(),
|
1536
|
+
viewEl = $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]');
|
1537
|
+
|
1538
|
+
// NOP For views
|
1539
|
+
if (viewEl.attr(viewCidAttributeName)) {
|
1540
|
+
return;
|
1541
|
+
}
|
1542
|
+
|
1543
|
+
this.removeItem(viewEl);
|
1544
|
+
this.appendItem(model);
|
1545
|
+
},
|
1546
|
+
|
1547
|
+
removeItem: function(model) {
|
1548
|
+
var viewEl = model;
|
1549
|
+
if (model.cid) {
|
1550
|
+
var $el = this.getCollectionElement();
|
1551
|
+
viewEl = $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]');
|
1552
|
+
}
|
1553
|
+
if (!viewEl.length) {
|
1554
|
+
return false;
|
1555
|
+
}
|
1556
|
+
viewEl.remove();
|
1557
|
+
var viewCid = viewEl.attr(viewCidAttributeName),
|
1558
|
+
child = this.children[viewCid];
|
1559
|
+
if (child) {
|
1560
|
+
this._removeChild(child);
|
1561
|
+
}
|
1562
|
+
return true;
|
1563
|
+
},
|
1564
|
+
|
1565
|
+
renderCollection: function() {
|
1566
|
+
if (this.collection) {
|
1567
|
+
if (this.collection.isEmpty()) {
|
1568
|
+
handleChangeFromNotEmptyToEmpty.call(this);
|
1569
|
+
} else {
|
1570
|
+
handleChangeFromEmptyToNotEmpty.call(this);
|
1571
|
+
this.collection.forEach(function(item, i) {
|
1572
|
+
this.appendItem(item, i);
|
1573
|
+
}, this);
|
1574
|
+
}
|
1575
|
+
this.trigger('rendered:collection', this, this.collection);
|
1576
|
+
} else {
|
1577
|
+
handleChangeFromNotEmptyToEmpty.call(this);
|
1578
|
+
}
|
1579
|
+
},
|
1580
|
+
emptyClass: 'empty',
|
1581
|
+
renderEmpty: function() {
|
1582
|
+
if (!this.emptyView) {
|
1583
|
+
assignView.call(this, 'emptyView', {
|
1584
|
+
extension: '-empty'
|
1585
|
+
});
|
1586
|
+
}
|
1587
|
+
if (!this.emptyTemplate && !this.emptyView) {
|
1588
|
+
assignTemplate.call(this, 'emptyTemplate', {
|
1589
|
+
extension: '-empty',
|
1590
|
+
required: false
|
1591
|
+
});
|
1592
|
+
}
|
1593
|
+
if (this.emptyView) {
|
1594
|
+
var viewOptions = {};
|
1595
|
+
if (this.emptyTemplate) {
|
1596
|
+
viewOptions.template = this.emptyTemplate;
|
1597
|
+
}
|
1598
|
+
var view = Thorax.Util.getViewInstance(this.emptyView, viewOptions);
|
1599
|
+
view.ensureRendered();
|
1600
|
+
return view;
|
1601
|
+
} else {
|
1602
|
+
return this.emptyTemplate && this.renderTemplate(this.emptyTemplate);
|
1603
|
+
}
|
1604
|
+
},
|
1605
|
+
renderItem: function(model, i) {
|
1606
|
+
if (!this.itemView) {
|
1607
|
+
assignView.call(this, 'itemView', {
|
1608
|
+
extension: '-item',
|
1609
|
+
required: false
|
1610
|
+
});
|
1611
|
+
}
|
1612
|
+
if (!this.itemTemplate && !this.itemView) {
|
1613
|
+
assignTemplate.call(this, 'itemTemplate', {
|
1614
|
+
extension: '-item',
|
1615
|
+
// only require an itemTemplate if an itemView
|
1616
|
+
// is not present
|
1617
|
+
required: !this.itemView
|
1618
|
+
});
|
1619
|
+
}
|
1620
|
+
if (this.itemView) {
|
1621
|
+
var viewOptions = {
|
1622
|
+
model: model
|
1623
|
+
};
|
1624
|
+
if (this.itemTemplate) {
|
1625
|
+
viewOptions.template = this.itemTemplate;
|
1626
|
+
}
|
1627
|
+
return Thorax.Util.getViewInstance(this.itemView, viewOptions);
|
1628
|
+
} else {
|
1629
|
+
return this.renderTemplate(this.itemTemplate, this.itemContext(model, i));
|
1630
|
+
}
|
1631
|
+
},
|
1632
|
+
itemContext: function(model /*, i */) {
|
1633
|
+
return model.attributes;
|
1634
|
+
},
|
1635
|
+
appendEmpty: function() {
|
1636
|
+
var $el = this.getCollectionElement();
|
1637
|
+
$el.empty();
|
1638
|
+
var emptyContent = this.renderEmpty();
|
1639
|
+
emptyContent && this.appendItem(emptyContent, 0, {
|
1640
|
+
silent: true,
|
1641
|
+
filter: false
|
1642
|
+
});
|
1643
|
+
this.trigger('rendered:empty', this, this.collection);
|
1644
|
+
},
|
1645
|
+
getCollectionElement: function() {
|
1646
|
+
var element = this.$(this._collectionSelector);
|
1647
|
+
return element.length === 0 ? this.$el : element;
|
1648
|
+
},
|
1649
|
+
|
1650
|
+
updateFilter: function() {
|
1651
|
+
applyVisibilityFilter.call(this);
|
1652
|
+
}
|
1653
|
+
});
|
1654
|
+
|
1655
|
+
Thorax.CollectionView.on({
|
1656
|
+
collection: {
|
1657
|
+
reset: onCollectionReset,
|
1658
|
+
sort: onCollectionReset,
|
1659
|
+
change: function(model) {
|
1660
|
+
var options = this.getObjectOptions(this.collection);
|
1661
|
+
if (options && options.change) {
|
1662
|
+
this.updateItem(model);
|
1663
|
+
}
|
1664
|
+
applyItemVisiblityFilter.call(this, model);
|
1665
|
+
},
|
1666
|
+
add: function(model) {
|
1667
|
+
var $el = this.getCollectionElement();
|
1668
|
+
this.collection.length === 1 && $el.length && handleChangeFromEmptyToNotEmpty.call(this);
|
1669
|
+
if ($el.length) {
|
1670
|
+
var index = this.collection.indexOf(model);
|
1671
|
+
this.appendItem(model, index);
|
1672
|
+
}
|
1673
|
+
},
|
1674
|
+
remove: function(model) {
|
1675
|
+
var $el = this.getCollectionElement();
|
1676
|
+
this.removeItem(model);
|
1677
|
+
this.collection.length === 0 && $el.length && handleChangeFromNotEmptyToEmpty.call(this);
|
1678
|
+
}
|
1679
|
+
}
|
1680
|
+
});
|
1681
|
+
|
1682
|
+
Thorax.View.on({
|
1683
|
+
collection: {
|
1684
|
+
invalid: function(collection, message) {
|
1685
|
+
if (this.getObjectOptions(collection).invalid) {
|
1686
|
+
this.trigger('invalid', message, collection);
|
1687
|
+
}
|
1688
|
+
},
|
1689
|
+
error: function(collection, resp, options) {
|
1690
|
+
this.trigger('error', resp, collection);
|
1691
|
+
}
|
1692
|
+
}
|
1693
|
+
});
|
1694
|
+
|
1695
|
+
function onCollectionReset(collection) {
|
1696
|
+
// Undefined to force conditional render
|
1697
|
+
var options = this.getObjectOptions(collection) || undefined;
|
1698
|
+
if (this.shouldRender(options && options.render)) {
|
1699
|
+
this.renderCollection && this.renderCollection();
|
1700
|
+
}
|
1701
|
+
}
|
1702
|
+
|
1703
|
+
// Even if the view is not a CollectionView
|
1704
|
+
// ensureRendered() to provide similar behavior
|
1705
|
+
// to a model
|
1706
|
+
function onSetCollection(collection) {
|
1707
|
+
// Undefined to force conditional render
|
1708
|
+
var options = this.getObjectOptions(collection) || undefined;
|
1709
|
+
if (this.shouldRender(options && options.render)) {
|
1710
|
+
// Ensure that something is there if we are going to render the collection.
|
1711
|
+
this.ensureRendered();
|
1712
|
+
}
|
1713
|
+
}
|
1714
|
+
|
1715
|
+
function applyVisibilityFilter() {
|
1716
|
+
if (this.itemFilter) {
|
1717
|
+
this.collection.forEach(applyItemVisiblityFilter, this);
|
1718
|
+
}
|
1719
|
+
}
|
1720
|
+
|
1721
|
+
function applyItemVisiblityFilter(model) {
|
1722
|
+
var $el = this.getCollectionElement();
|
1723
|
+
this.itemFilter && $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]')[itemShouldBeVisible.call(this, model) ? 'show' : 'hide']();
|
1724
|
+
}
|
1725
|
+
|
1726
|
+
function itemShouldBeVisible(model) {
|
1727
|
+
return this.itemFilter(model, this.collection.indexOf(model));
|
1728
|
+
}
|
1729
|
+
|
1730
|
+
function handleChangeFromEmptyToNotEmpty() {
|
1731
|
+
var $el = this.getCollectionElement();
|
1732
|
+
this.emptyClass && $el.removeClass(this.emptyClass);
|
1733
|
+
$el.removeAttr(collectionEmptyAttributeName);
|
1734
|
+
$el.empty();
|
1735
|
+
}
|
1736
|
+
|
1737
|
+
function handleChangeFromNotEmptyToEmpty() {
|
1738
|
+
var $el = this.getCollectionElement();
|
1739
|
+
this.emptyClass && $el.addClass(this.emptyClass);
|
1740
|
+
$el.attr(collectionEmptyAttributeName, true);
|
1741
|
+
this.appendEmpty();
|
1742
|
+
}
|
1743
|
+
|
1744
|
+
//$(selector).collection() helper
|
1745
|
+
$.fn.collection = function(view) {
|
1746
|
+
if (view && view.collection) {
|
1747
|
+
return view.collection;
|
1748
|
+
}
|
1749
|
+
var $this = $(this),
|
1750
|
+
collectionElement = $this.closest('[' + collectionCidAttributeName + ']'),
|
1751
|
+
collectionCid = collectionElement && collectionElement.attr(collectionCidAttributeName);
|
1752
|
+
if (collectionCid) {
|
1753
|
+
view = $this.view();
|
1754
|
+
if (view) {
|
1755
|
+
return view.collection;
|
1756
|
+
}
|
1757
|
+
}
|
1758
|
+
return false;
|
1759
|
+
};
|
1760
|
+
|
1761
|
+
;;
|
1762
|
+
/*global inheritVars */
|
1763
|
+
|
1764
|
+
inheritVars.model.defaultOptions.populate = true;
|
1765
|
+
|
1766
|
+
var oldModelChange = inheritVars.model.change;
|
1767
|
+
inheritVars.model.change = function(model, options) {
|
1768
|
+
this._isChanging = true;
|
1769
|
+
oldModelChange.apply(this, arguments);
|
1770
|
+
this._isChanging = false;
|
1771
|
+
|
1772
|
+
if (options && options.serializing) {
|
1773
|
+
return;
|
1774
|
+
}
|
1775
|
+
|
1776
|
+
var populate = populateOptions(this);
|
1777
|
+
if (this._renderCount && populate) {
|
1778
|
+
this.populate(!populate.context && this.model.attributes, populate);
|
1779
|
+
}
|
1780
|
+
};
|
1781
|
+
|
1782
|
+
_.extend(Thorax.View.prototype, {
|
1783
|
+
//serializes a form present in the view, returning the serialized data
|
1784
|
+
//as an object
|
1785
|
+
//pass {set:false} to not update this.model if present
|
1786
|
+
//can pass options, callback or event in any order
|
1787
|
+
serialize: function() {
|
1788
|
+
var callback, options, event;
|
1789
|
+
//ignore undefined arguments in case event was null
|
1790
|
+
for (var i = 0; i < arguments.length; ++i) {
|
1791
|
+
if (_.isFunction(arguments[i])) {
|
1792
|
+
callback = arguments[i];
|
1793
|
+
} else if (_.isObject(arguments[i])) {
|
1794
|
+
if ('stopPropagation' in arguments[i] && 'preventDefault' in arguments[i]) {
|
1795
|
+
event = arguments[i];
|
1796
|
+
} else {
|
1797
|
+
options = arguments[i];
|
1798
|
+
}
|
1799
|
+
}
|
1800
|
+
}
|
1801
|
+
|
1802
|
+
if (event && !this._preventDuplicateSubmission(event)) {
|
1803
|
+
return;
|
1804
|
+
}
|
1805
|
+
|
1806
|
+
options = _.extend({
|
1807
|
+
set: true,
|
1808
|
+
validate: true,
|
1809
|
+
children: true
|
1810
|
+
}, options || {});
|
1811
|
+
|
1812
|
+
var attributes = options.attributes || {};
|
1813
|
+
|
1814
|
+
//callback has context of element
|
1815
|
+
var view = this;
|
1816
|
+
var errors = [];
|
1817
|
+
eachNamedInput(this, options, function(element) {
|
1818
|
+
var value = view._getInputValue(element, options, errors);
|
1819
|
+
if (!_.isUndefined(value)) {
|
1820
|
+
objectAndKeyFromAttributesAndName(attributes, element.name, {mode: 'serialize'}, function(object, key) {
|
1821
|
+
if (!object[key]) {
|
1822
|
+
object[key] = value;
|
1823
|
+
} else if (_.isArray(object[key])) {
|
1824
|
+
object[key].push(value);
|
1825
|
+
} else {
|
1826
|
+
object[key] = [object[key], value];
|
1827
|
+
}
|
1828
|
+
});
|
1829
|
+
}
|
1830
|
+
});
|
1831
|
+
|
1832
|
+
if (!options._silent) {
|
1833
|
+
this.trigger('serialize', attributes, options);
|
1834
|
+
}
|
1835
|
+
|
1836
|
+
if (options.validate) {
|
1837
|
+
var validateInputErrors = this.validateInput(attributes);
|
1838
|
+
if (validateInputErrors && validateInputErrors.length) {
|
1839
|
+
errors = errors.concat(validateInputErrors);
|
1840
|
+
}
|
1841
|
+
this.trigger('validate', attributes, errors, options);
|
1842
|
+
if (errors.length) {
|
1843
|
+
this.trigger('invalid', errors);
|
1844
|
+
return;
|
1845
|
+
}
|
1846
|
+
}
|
1847
|
+
|
1848
|
+
if (options.set && this.model) {
|
1849
|
+
if (!this.model.set(attributes, {silent: options.silent, serializing: true})) {
|
1850
|
+
return false;
|
1851
|
+
}
|
1852
|
+
}
|
1853
|
+
|
1854
|
+
callback && callback.call(this, attributes, _.bind(resetSubmitState, this));
|
1855
|
+
return attributes;
|
1856
|
+
},
|
1857
|
+
|
1858
|
+
_preventDuplicateSubmission: function(event, callback) {
|
1859
|
+
event.preventDefault();
|
1860
|
+
|
1861
|
+
var form = $(event.target);
|
1862
|
+
if ((event.target.tagName || '').toLowerCase() !== 'form') {
|
1863
|
+
// Handle non-submit events by gating on the form
|
1864
|
+
form = $(event.target).closest('form');
|
1865
|
+
}
|
1866
|
+
|
1867
|
+
if (!form.attr('data-submit-wait')) {
|
1868
|
+
form.attr('data-submit-wait', 'true');
|
1869
|
+
if (callback) {
|
1870
|
+
callback.call(this, event);
|
1871
|
+
}
|
1872
|
+
return true;
|
1873
|
+
} else {
|
1874
|
+
return false;
|
1875
|
+
}
|
1876
|
+
},
|
1877
|
+
|
1878
|
+
//populate a form from the passed attributes or this.model if present
|
1879
|
+
populate: function(attributes, options) {
|
1880
|
+
options = _.extend({
|
1881
|
+
children: true
|
1882
|
+
}, options || {});
|
1883
|
+
|
1884
|
+
var value,
|
1885
|
+
attributes = attributes || this._getContext();
|
1886
|
+
|
1887
|
+
//callback has context of element
|
1888
|
+
eachNamedInput(this, options, function(element) {
|
1889
|
+
objectAndKeyFromAttributesAndName(attributes, element.name, {mode: 'populate'}, function(object, key) {
|
1890
|
+
value = object && object[key];
|
1891
|
+
|
1892
|
+
if (!_.isUndefined(value)) {
|
1893
|
+
//will only execute if we have a name that matches the structure in attributes
|
1894
|
+
var isBinary = element.type === 'checkbox' || element.type === 'radio';
|
1895
|
+
if (isBinary && _.isBoolean(value)) {
|
1896
|
+
element.checked = value;
|
1897
|
+
} else if (isBinary) {
|
1898
|
+
element.checked = value == element.value;
|
1899
|
+
} else {
|
1900
|
+
element.value = value;
|
1901
|
+
}
|
1902
|
+
}
|
1903
|
+
});
|
1904
|
+
});
|
1905
|
+
|
1906
|
+
++this._populateCount;
|
1907
|
+
if (!options._silent) {
|
1908
|
+
this.trigger('populate', attributes);
|
1909
|
+
}
|
1910
|
+
},
|
1911
|
+
|
1912
|
+
//perform form validation, implemented by child class
|
1913
|
+
validateInput: function(/* attributes, options, errors */) {},
|
1914
|
+
|
1915
|
+
_getInputValue: function(input /* , options, errors */) {
|
1916
|
+
if (input.type === 'checkbox' || input.type === 'radio') {
|
1917
|
+
if (input.checked) {
|
1918
|
+
return input.getAttribute('value') || true;
|
1919
|
+
}
|
1920
|
+
} else if (input.multiple === true) {
|
1921
|
+
var values = [];
|
1922
|
+
$('option', input).each(function() {
|
1923
|
+
if (this.selected) {
|
1924
|
+
values.push(this.value);
|
1925
|
+
}
|
1926
|
+
});
|
1927
|
+
return values;
|
1928
|
+
} else {
|
1929
|
+
return input.value;
|
1930
|
+
}
|
1931
|
+
},
|
1932
|
+
|
1933
|
+
_populateCount: 0
|
1934
|
+
});
|
1935
|
+
|
1936
|
+
// Keeping state in the views
|
1937
|
+
Thorax.View.on({
|
1938
|
+
'before:rendered': function() {
|
1939
|
+
if (!this._renderCount) { return; }
|
1940
|
+
|
1941
|
+
var modelOptions = this.getObjectOptions(this.model);
|
1942
|
+
// When we have previously populated and rendered the view, reuse the user data
|
1943
|
+
this.previousFormData = filterObject(
|
1944
|
+
this.serialize(_.extend({ set: false, validate: false, _silent: true }, modelOptions)),
|
1945
|
+
function(value) { return value !== '' && value != null; }
|
1946
|
+
);
|
1947
|
+
},
|
1948
|
+
rendered: function() {
|
1949
|
+
var populate = populateOptions(this);
|
1950
|
+
|
1951
|
+
if (populate && !this._isChanging && !this._populateCount) {
|
1952
|
+
this.populate(!populate.context && this.model.attributes, populate);
|
1953
|
+
}
|
1954
|
+
if (this.previousFormData) {
|
1955
|
+
this.populate(this.previousFormData, _.extend({_silent: true}, populate));
|
1956
|
+
}
|
1957
|
+
|
1958
|
+
this.previousFormData = null;
|
1959
|
+
}
|
1960
|
+
});
|
1961
|
+
|
1962
|
+
function filterObject(object, callback) {
|
1963
|
+
_.each(object, function (value, key) {
|
1964
|
+
if (_.isObject(value)) {
|
1965
|
+
return filterObject(value, callback);
|
1966
|
+
}
|
1967
|
+
if (callback(value, key, object) === false) {
|
1968
|
+
delete object[key];
|
1969
|
+
}
|
1970
|
+
});
|
1971
|
+
return object;
|
1972
|
+
}
|
1973
|
+
|
1974
|
+
Thorax.View.on({
|
1975
|
+
invalid: onErrorOrInvalidData,
|
1976
|
+
error: onErrorOrInvalidData,
|
1977
|
+
deactivated: function() {
|
1978
|
+
if (this.$el) {
|
1979
|
+
resetSubmitState.call(this);
|
1980
|
+
}
|
1981
|
+
}
|
1982
|
+
});
|
1983
|
+
|
1984
|
+
function onErrorOrInvalidData () {
|
1985
|
+
resetSubmitState.call(this);
|
1986
|
+
|
1987
|
+
// If we errored with a model we want to reset the content but leave the UI
|
1988
|
+
// intact. If the user updates the data and serializes any overwritten data
|
1989
|
+
// will be restored.
|
1990
|
+
if (this.model && this.model.previousAttributes) {
|
1991
|
+
this.model.set(this.model.previousAttributes(), {
|
1992
|
+
silent: true
|
1993
|
+
});
|
1994
|
+
}
|
1995
|
+
}
|
1996
|
+
|
1997
|
+
function eachNamedInput(view, options, iterator) {
|
1998
|
+
var i = 0;
|
1999
|
+
|
2000
|
+
$('select,input,textarea', options.root || view.el).each(function() {
|
2001
|
+
if (!options.children) {
|
2002
|
+
if (view !== $(this).view({helper: false})) {
|
2003
|
+
return;
|
2004
|
+
}
|
2005
|
+
}
|
2006
|
+
if (this.type !== 'button' && this.type !== 'cancel' && this.type !== 'submit' && this.name) {
|
2007
|
+
iterator(this, i);
|
2008
|
+
++i;
|
2009
|
+
}
|
2010
|
+
});
|
2011
|
+
}
|
2012
|
+
|
2013
|
+
//calls a callback with the correct object fragment and key from a compound name
|
2014
|
+
function objectAndKeyFromAttributesAndName(attributes, name, options, callback) {
|
2015
|
+
var key,
|
2016
|
+
object = attributes,
|
2017
|
+
keys = name.split('['),
|
2018
|
+
mode = options.mode;
|
2019
|
+
|
2020
|
+
for (var i = 0; i < keys.length - 1; ++i) {
|
2021
|
+
key = keys[i].replace(']', '');
|
2022
|
+
if (!object[key]) {
|
2023
|
+
if (mode === 'serialize') {
|
2024
|
+
object[key] = {};
|
2025
|
+
} else {
|
2026
|
+
return callback(undefined, key);
|
2027
|
+
}
|
2028
|
+
}
|
2029
|
+
object = object[key];
|
2030
|
+
}
|
2031
|
+
key = keys[keys.length - 1].replace(']', '');
|
2032
|
+
callback(object, key);
|
2033
|
+
}
|
2034
|
+
|
2035
|
+
function resetSubmitState() {
|
2036
|
+
this.$('form').removeAttr('data-submit-wait');
|
2037
|
+
}
|
2038
|
+
|
2039
|
+
function populateOptions(view) {
|
2040
|
+
var modelOptions = view.getObjectOptions(view.model) || {};
|
2041
|
+
return modelOptions.populate === true ? {} : modelOptions.populate;
|
2042
|
+
}
|
2043
|
+
|
2044
|
+
;;
|
2045
|
+
/*global getOptionsData, normalizeHTMLAttributeOptions, createErrorMessage */
|
2046
|
+
var layoutCidAttributeName = 'data-layout-cid';
|
2047
|
+
|
2048
|
+
Thorax.LayoutView = Thorax.View.extend({
|
2049
|
+
_defaultTemplate: Handlebars.VM.noop,
|
2050
|
+
render: function() {
|
2051
|
+
var response = Thorax.View.prototype.render.apply(this, arguments);
|
2052
|
+
if (this.template === Handlebars.VM.noop) {
|
2053
|
+
// if there is no template setView will append to this.$el
|
2054
|
+
ensureLayoutCid.call(this);
|
2055
|
+
} else {
|
2056
|
+
// if a template was specified is must declare a layout-element
|
2057
|
+
ensureLayoutViewsTargetElement.call(this);
|
2058
|
+
}
|
2059
|
+
return response;
|
2060
|
+
},
|
2061
|
+
setView: function(view, options) {
|
2062
|
+
options = _.extend({
|
2063
|
+
scroll: true
|
2064
|
+
}, options || {});
|
2065
|
+
if (_.isString(view)) {
|
2066
|
+
view = new (Thorax.Util.registryGet(Thorax, 'Views', view, false))();
|
2067
|
+
}
|
2068
|
+
this.ensureRendered();
|
2069
|
+
var oldView = this._view, append, remove, complete;
|
2070
|
+
if (view === oldView) {
|
2071
|
+
return false;
|
2072
|
+
}
|
2073
|
+
this.trigger('change:view:start', view, oldView, options);
|
2074
|
+
|
2075
|
+
remove = _.bind(function() {
|
2076
|
+
if (oldView) {
|
2077
|
+
oldView.$el.remove();
|
2078
|
+
triggerLifecycleEvent.call(oldView, 'deactivated', options);
|
2079
|
+
this._removeChild(oldView);
|
2080
|
+
}
|
2081
|
+
}, this);
|
2082
|
+
|
2083
|
+
append = _.bind(function() {
|
2084
|
+
if (view) {
|
2085
|
+
view.ensureRendered();
|
2086
|
+
triggerLifecycleEvent.call(this, 'activated', options);
|
2087
|
+
view.trigger('activated', options);
|
2088
|
+
this._view = view;
|
2089
|
+
var targetElement = getLayoutViewsTargetElement.call(this);
|
2090
|
+
this._view.appendTo(targetElement);
|
2091
|
+
this._addChild(view);
|
2092
|
+
} else {
|
2093
|
+
this._view = undefined;
|
2094
|
+
}
|
2095
|
+
}, this);
|
2096
|
+
|
2097
|
+
complete = _.bind(function() {
|
2098
|
+
this.trigger('change:view:end', view, oldView, options);
|
2099
|
+
}, this);
|
2100
|
+
|
2101
|
+
if (!options.transition) {
|
2102
|
+
remove();
|
2103
|
+
append();
|
2104
|
+
complete();
|
2105
|
+
} else {
|
2106
|
+
options.transition(view, oldView, append, remove, complete);
|
2107
|
+
}
|
2108
|
+
|
2109
|
+
return view;
|
2110
|
+
},
|
2111
|
+
|
2112
|
+
getView: function() {
|
2113
|
+
return this._view;
|
2114
|
+
}
|
2115
|
+
});
|
2116
|
+
|
2117
|
+
Handlebars.registerHelper('layout-element', function(options) {
|
2118
|
+
var view = getOptionsData(options).view;
|
2119
|
+
// duck type check for LayoutView
|
2120
|
+
if (!view.getView) {
|
2121
|
+
throw new Error(createErrorMessage('layout-element-helper'));
|
2122
|
+
}
|
2123
|
+
options.hash[layoutCidAttributeName] = view.cid;
|
2124
|
+
normalizeHTMLAttributeOptions(options.hash);
|
2125
|
+
return new Handlebars.SafeString(Thorax.Util.tag.call(this, options.hash, '', this));
|
2126
|
+
});
|
2127
|
+
|
2128
|
+
function triggerLifecycleEvent(eventName, options) {
|
2129
|
+
options = options || {};
|
2130
|
+
options.target = this;
|
2131
|
+
this.trigger(eventName, options);
|
2132
|
+
_.each(this.children, function(child) {
|
2133
|
+
child.trigger(eventName, options);
|
2134
|
+
});
|
2135
|
+
}
|
2136
|
+
|
2137
|
+
function ensureLayoutCid() {
|
2138
|
+
++this._renderCount;
|
2139
|
+
//set the layoutCidAttributeName on this.$el if there was no template
|
2140
|
+
this.$el.attr(layoutCidAttributeName, this.cid);
|
2141
|
+
}
|
2142
|
+
|
2143
|
+
function ensureLayoutViewsTargetElement() {
|
2144
|
+
if (!this.$('[' + layoutCidAttributeName + '="' + this.cid + '"]')[0]) {
|
2145
|
+
throw new Error('No layout element found in ' + (this.name || this.cid));
|
2146
|
+
}
|
2147
|
+
}
|
2148
|
+
|
2149
|
+
function getLayoutViewsTargetElement() {
|
2150
|
+
return this.$('[' + layoutCidAttributeName + '="' + this.cid + '"]')[0] || this.el[0] || this.el;
|
2151
|
+
}
|
2152
|
+
|
2153
|
+
;;
|
2154
|
+
/* global createErrorMessage */
|
2155
|
+
|
2156
|
+
Thorax.CollectionHelperView = Thorax.CollectionView.extend({
|
2157
|
+
// Forward render events to the parent
|
2158
|
+
events: {
|
2159
|
+
'rendered:item': forwardRenderEvent('rendered:item'),
|
2160
|
+
'rendered:collection': forwardRenderEvent('rendered:collection'),
|
2161
|
+
'rendered:empty': forwardRenderEvent('rendered:empty')
|
2162
|
+
},
|
2163
|
+
|
2164
|
+
// Thorax.CollectionView allows a collectionSelector
|
2165
|
+
// to be specified, disallow in a collection helper
|
2166
|
+
// as it will cause problems when neseted
|
2167
|
+
getCollectionElement: function() {
|
2168
|
+
return this.$el;
|
2169
|
+
},
|
2170
|
+
|
2171
|
+
constructor: function(options) {
|
2172
|
+
// need to fetch templates if template name was passed
|
2173
|
+
if (options.options['item-template']) {
|
2174
|
+
options.itemTemplate = Thorax.Util.getTemplate(options.options['item-template']);
|
2175
|
+
}
|
2176
|
+
if (options.options['empty-template']) {
|
2177
|
+
options.emptyTemplate = Thorax.Util.getTemplate(options.options['empty-template']);
|
2178
|
+
}
|
2179
|
+
|
2180
|
+
// Handlebars.VM.noop is passed in the handlebars options object as
|
2181
|
+
// a default for fn and inverse, if a block was present. Need to
|
2182
|
+
// check to ensure we don't pick the empty / null block up.
|
2183
|
+
if (!options.itemTemplate && options.template && options.template !== Handlebars.VM.noop) {
|
2184
|
+
options.itemTemplate = options.template;
|
2185
|
+
options.template = Handlebars.VM.noop;
|
2186
|
+
}
|
2187
|
+
if (!options.emptyTemplate && options.inverse && options.inverse !== Handlebars.VM.noop) {
|
2188
|
+
options.emptyTemplate = options.inverse;
|
2189
|
+
options.inverse = Handlebars.VM.noop;
|
2190
|
+
}
|
2191
|
+
|
2192
|
+
var shouldBindItemContext = _.isFunction(options.itemContext),
|
2193
|
+
shouldBindItemFilter = _.isFunction(options.itemFilter);
|
2194
|
+
|
2195
|
+
var response = Thorax.HelperView.call(this, options);
|
2196
|
+
|
2197
|
+
if (shouldBindItemContext) {
|
2198
|
+
this.itemContext = _.bind(this.itemContext, this.parent);
|
2199
|
+
} else if (_.isString(this.itemContext)) {
|
2200
|
+
this.itemContext = _.bind(this.parent[this.itemContext], this.parent);
|
2201
|
+
}
|
2202
|
+
|
2203
|
+
if (shouldBindItemFilter) {
|
2204
|
+
this.itemFilter = _.bind(this.itemFilter, this.parent);
|
2205
|
+
} else if (_.isString(this.itemFilter)) {
|
2206
|
+
this.itemFilter = _.bind(this.parent[this.itemFilter], this.parent);
|
2207
|
+
}
|
2208
|
+
|
2209
|
+
if (this.parent.name) {
|
2210
|
+
if (!this.emptyView && !this.parent.renderEmpty) {
|
2211
|
+
this.emptyView = Thorax.Util.getViewClass(this.parent.name + '-empty', true);
|
2212
|
+
}
|
2213
|
+
if (!this.emptyTemplate && !this.parent.renderEmpty) {
|
2214
|
+
this.emptyTemplate = Thorax.Util.getTemplate(this.parent.name + '-empty', true);
|
2215
|
+
}
|
2216
|
+
if (!this.itemView && !this.parent.renderItem) {
|
2217
|
+
this.itemView = Thorax.Util.getViewClass(this.parent.name + '-item', true);
|
2218
|
+
}
|
2219
|
+
if (!this.itemTemplate && !this.parent.renderItem) {
|
2220
|
+
// item template must be present if an itemView is not
|
2221
|
+
this.itemTemplate = Thorax.Util.getTemplate(this.parent.name + '-item', !!this.itemView);
|
2222
|
+
}
|
2223
|
+
}
|
2224
|
+
|
2225
|
+
return response;
|
2226
|
+
},
|
2227
|
+
setAsPrimaryCollectionHelper: function() {
|
2228
|
+
_.each(forwardableProperties, function(propertyName) {
|
2229
|
+
forwardMissingProperty.call(this, propertyName);
|
2230
|
+
}, this);
|
2231
|
+
|
2232
|
+
var self = this;
|
2233
|
+
_.each(['itemFilter', 'itemContext', 'renderItem', 'renderEmpty'], function(propertyName) {
|
2234
|
+
if (self.parent[propertyName]) {
|
2235
|
+
self[propertyName] = function() {
|
2236
|
+
return self.parent[propertyName].apply(self.parent, arguments);
|
2237
|
+
};
|
2238
|
+
}
|
2239
|
+
});
|
2240
|
+
}
|
2241
|
+
});
|
2242
|
+
|
2243
|
+
_.extend(Thorax.CollectionHelperView.prototype, helperViewPrototype);
|
2244
|
+
|
2245
|
+
|
2246
|
+
Thorax.CollectionHelperView.attributeWhiteList = {
|
2247
|
+
'item-context': 'itemContext',
|
2248
|
+
'item-filter': 'itemFilter',
|
2249
|
+
'item-template': 'itemTemplate',
|
2250
|
+
'empty-template': 'emptyTemplate',
|
2251
|
+
'item-view': 'itemView',
|
2252
|
+
'empty-view': 'emptyView',
|
2253
|
+
'empty-class': 'emptyClass'
|
2254
|
+
};
|
2255
|
+
|
2256
|
+
function forwardRenderEvent(eventName) {
|
2257
|
+
return function() {
|
2258
|
+
var args = _.toArray(arguments);
|
2259
|
+
args.unshift(eventName);
|
2260
|
+
this.parent.trigger.apply(this.parent, args);
|
2261
|
+
};
|
2262
|
+
}
|
2263
|
+
|
2264
|
+
var forwardableProperties = [
|
2265
|
+
'itemTemplate',
|
2266
|
+
'itemView',
|
2267
|
+
'emptyTemplate',
|
2268
|
+
'emptyView'
|
2269
|
+
];
|
2270
|
+
|
2271
|
+
function forwardMissingProperty(propertyName) {
|
2272
|
+
var parent = getParent(this);
|
2273
|
+
if (!this[propertyName]) {
|
2274
|
+
var prop = parent[propertyName];
|
2275
|
+
if (prop){
|
2276
|
+
this[propertyName] = prop;
|
2277
|
+
}
|
2278
|
+
}
|
2279
|
+
}
|
2280
|
+
|
2281
|
+
Handlebars.registerViewHelper('collection', Thorax.CollectionHelperView, function(collection, view) {
|
2282
|
+
if (arguments.length === 1) {
|
2283
|
+
view = collection;
|
2284
|
+
collection = view.parent.collection;
|
2285
|
+
collection && view.setAsPrimaryCollectionHelper();
|
2286
|
+
view.$el.attr(collectionElementAttributeName, 'true');
|
2287
|
+
// propagate future changes to the parent's collection object
|
2288
|
+
// to the helper view
|
2289
|
+
view.listenTo(view.parent, 'change:data-object', function(type, dataObject) {
|
2290
|
+
if (type === 'collection') {
|
2291
|
+
view.setAsPrimaryCollectionHelper();
|
2292
|
+
view.setCollection(dataObject);
|
2293
|
+
}
|
2294
|
+
});
|
2295
|
+
}
|
2296
|
+
collection && view.setCollection(collection);
|
2297
|
+
});
|
2298
|
+
|
2299
|
+
Handlebars.registerHelper('collection-element', function(options) {
|
2300
|
+
if (!getOptionsData(options).view.renderCollection) {
|
2301
|
+
throw new Error(createErrorMessage('collection-element-helper'));
|
2302
|
+
}
|
2303
|
+
var hash = options.hash;
|
2304
|
+
normalizeHTMLAttributeOptions(hash);
|
2305
|
+
hash.tagName = hash.tagName || 'div';
|
2306
|
+
hash[collectionElementAttributeName] = true;
|
2307
|
+
return new Handlebars.SafeString(Thorax.Util.tag.call(this, hash, '', this));
|
2308
|
+
});
|
2309
|
+
|
2310
|
+
;;
|
2311
|
+
Handlebars.registerHelper('empty', function(dataObject, options) {
|
2312
|
+
if (arguments.length === 1) {
|
2313
|
+
options = dataObject;
|
2314
|
+
}
|
2315
|
+
var view = getOptionsData(options).view;
|
2316
|
+
if (arguments.length === 1) {
|
2317
|
+
dataObject = view.model;
|
2318
|
+
}
|
2319
|
+
// listeners for the empty helper rather than listeners
|
2320
|
+
// that are themselves empty
|
2321
|
+
if (!view._emptyListeners) {
|
2322
|
+
view._emptyListeners = {};
|
2323
|
+
}
|
2324
|
+
// duck type check for collection
|
2325
|
+
if (dataObject && !view._emptyListeners[dataObject.cid] && dataObject.models && ('length' in dataObject)) {
|
2326
|
+
view._emptyListeners[dataObject.cid] = true;
|
2327
|
+
view.listenTo(dataObject, 'remove', function() {
|
2328
|
+
if (dataObject.length === 0) {
|
2329
|
+
view.render();
|
2330
|
+
}
|
2331
|
+
});
|
2332
|
+
view.listenTo(dataObject, 'add', function() {
|
2333
|
+
if (dataObject.length === 1) {
|
2334
|
+
view.render();
|
2335
|
+
}
|
2336
|
+
});
|
2337
|
+
view.listenTo(dataObject, 'reset', function() {
|
2338
|
+
view.render();
|
2339
|
+
});
|
2340
|
+
}
|
2341
|
+
return !dataObject || dataObject.isEmpty() ? options.fn(this) : options.inverse(this);
|
2342
|
+
});
|
2343
|
+
|
2344
|
+
;;
|
2345
|
+
Handlebars.registerHelper('template', function(name, options) {
|
2346
|
+
var context = _.extend({fn: options && options.fn}, this, options ? options.hash : {});
|
2347
|
+
var output = getOptionsData(options).view.renderTemplate(name, context);
|
2348
|
+
return new Handlebars.SafeString(output);
|
2349
|
+
});
|
2350
|
+
|
2351
|
+
Handlebars.registerHelper('yield', function(options) {
|
2352
|
+
return getOptionsData(options).yield && options.data.yield();
|
2353
|
+
});
|
2354
|
+
|
2355
|
+
;;
|
2356
|
+
Handlebars.registerHelper('url', function(url) {
|
2357
|
+
var fragment;
|
2358
|
+
if (arguments.length > 2) {
|
2359
|
+
fragment = _.map(_.head(arguments, arguments.length - 1), encodeURIComponent).join('/');
|
2360
|
+
} else {
|
2361
|
+
var options = arguments[1],
|
2362
|
+
hash = (options && options.hash) || options;
|
2363
|
+
if (hash && hash['expand-tokens']) {
|
2364
|
+
fragment = Thorax.Util.expandToken(url, this);
|
2365
|
+
} else {
|
2366
|
+
fragment = url;
|
2367
|
+
}
|
2368
|
+
}
|
2369
|
+
if (Backbone.history._hasPushState) {
|
2370
|
+
var root = Backbone.history.options.root;
|
2371
|
+
if (root === '/' && fragment.substr(0, 1) === '/') {
|
2372
|
+
return fragment;
|
2373
|
+
} else {
|
2374
|
+
return root + fragment;
|
2375
|
+
}
|
2376
|
+
} else {
|
2377
|
+
return '#' + fragment;
|
2378
|
+
}
|
2379
|
+
});
|
2380
|
+
|
2381
|
+
;;
|
2382
|
+
/*global viewTemplateOverrides, createErrorMessage */
|
2383
|
+
Handlebars.registerViewHelper('view', {
|
2384
|
+
factory: function(args, options) {
|
2385
|
+
var View = args.length >= 1 ? args[0] : Thorax.View;
|
2386
|
+
return Thorax.Util.getViewInstance(View, options.options);
|
2387
|
+
},
|
2388
|
+
// ensure generated placeholder tag in template
|
2389
|
+
// will match tag of view instance
|
2390
|
+
modifyHTMLAttributes: function(htmlAttributes, instance) {
|
2391
|
+
htmlAttributes.tagName = instance.el.tagName.toLowerCase();
|
2392
|
+
},
|
2393
|
+
callback: function(view) {
|
2394
|
+
var instance = arguments[arguments.length-1],
|
2395
|
+
options = instance._helperOptions.options,
|
2396
|
+
placeholderId = instance.cid;
|
2397
|
+
// view will be the argument passed to the helper, if it was
|
2398
|
+
// a string, a new instance was created on the fly, ok to pass
|
2399
|
+
// hash arguments, otherwise need to throw as templates should
|
2400
|
+
// not introduce side effects to existing view instances
|
2401
|
+
if (!_.isString(view) && options.hash && _.keys(options.hash).length > 0) {
|
2402
|
+
throw new Error(createErrorMessage('view-helper-hash-args'));
|
2403
|
+
}
|
2404
|
+
if (options.fn) {
|
2405
|
+
viewTemplateOverrides[placeholderId] = options.fn;
|
2406
|
+
}
|
2407
|
+
}
|
2408
|
+
});
|
2409
|
+
|
2410
|
+
;;
|
2411
|
+
/* global createErrorMessage */
|
2412
|
+
|
2413
|
+
var callMethodAttributeName = 'data-call-method',
|
2414
|
+
triggerEventAttributeName = 'data-trigger-event';
|
2415
|
+
|
2416
|
+
Handlebars.registerHelper('button', function(method, options) {
|
2417
|
+
if (arguments.length === 1) {
|
2418
|
+
options = method;
|
2419
|
+
method = options.hash.method;
|
2420
|
+
}
|
2421
|
+
var hash = options.hash,
|
2422
|
+
expandTokens = hash['expand-tokens'];
|
2423
|
+
delete hash['expand-tokens'];
|
2424
|
+
if (!method && !options.hash.trigger) {
|
2425
|
+
throw new Error(createErrorMessage('button-trigger'));
|
2426
|
+
}
|
2427
|
+
normalizeHTMLAttributeOptions(hash);
|
2428
|
+
hash.tagName = hash.tagName || 'button';
|
2429
|
+
hash.trigger && (hash[triggerEventAttributeName] = hash.trigger);
|
2430
|
+
delete hash.trigger;
|
2431
|
+
method && (hash[callMethodAttributeName] = method);
|
2432
|
+
return new Handlebars.SafeString(Thorax.Util.tag(hash, options.fn ? options.fn(this) : '', expandTokens ? this : null));
|
2433
|
+
});
|
2434
|
+
|
2435
|
+
Handlebars.registerHelper('link', function() {
|
2436
|
+
var args = _.toArray(arguments),
|
2437
|
+
options = args.pop(),
|
2438
|
+
hash = options.hash,
|
2439
|
+
// url is an array that will be passed to the url helper
|
2440
|
+
url = args.length === 0 ? [hash.href] : args,
|
2441
|
+
expandTokens = hash['expand-tokens'];
|
2442
|
+
delete hash['expand-tokens'];
|
2443
|
+
if (!url[0] && url[0] !== '') {
|
2444
|
+
throw new Error(createErrorMessage('link-href'));
|
2445
|
+
}
|
2446
|
+
normalizeHTMLAttributeOptions(hash);
|
2447
|
+
url.push(options);
|
2448
|
+
hash.href = Handlebars.helpers.url.apply(this, url);
|
2449
|
+
hash.tagName = hash.tagName || 'a';
|
2450
|
+
hash.trigger && (hash[triggerEventAttributeName] = options.hash.trigger);
|
2451
|
+
delete hash.trigger;
|
2452
|
+
hash[callMethodAttributeName] = '_anchorClick';
|
2453
|
+
return new Handlebars.SafeString(Thorax.Util.tag(hash, options.fn ? options.fn(this) : '', expandTokens ? this : null));
|
2454
|
+
});
|
2455
|
+
|
2456
|
+
var clickSelector = '[' + callMethodAttributeName + '], [' + triggerEventAttributeName + ']';
|
2457
|
+
|
2458
|
+
function handleClick(event) {
|
2459
|
+
var $this = $(this),
|
2460
|
+
view = $this.view({helper: false}),
|
2461
|
+
methodName = $this.attr(callMethodAttributeName),
|
2462
|
+
eventName = $this.attr(triggerEventAttributeName),
|
2463
|
+
methodResponse = false;
|
2464
|
+
methodName && (methodResponse = view[methodName].call(view, event));
|
2465
|
+
eventName && view.trigger(eventName, event);
|
2466
|
+
this.tagName === 'A' && methodResponse === false && event.preventDefault();
|
2467
|
+
}
|
2468
|
+
|
2469
|
+
var lastClickHandlerEventName;
|
2470
|
+
|
2471
|
+
function registerClickHandler() {
|
2472
|
+
unregisterClickHandler();
|
2473
|
+
lastClickHandlerEventName = Thorax._fastClickEventName || 'click';
|
2474
|
+
$(document).on(lastClickHandlerEventName, clickSelector, handleClick);
|
2475
|
+
}
|
2476
|
+
|
2477
|
+
function unregisterClickHandler() {
|
2478
|
+
lastClickHandlerEventName && $(document).off(lastClickHandlerEventName, clickSelector, handleClick);
|
2479
|
+
}
|
2480
|
+
|
2481
|
+
$(document).ready(function() {
|
2482
|
+
if (!Thorax._fastClickEventName) {
|
2483
|
+
registerClickHandler();
|
2484
|
+
}
|
2485
|
+
});
|
2486
|
+
|
2487
|
+
;;
|
2488
|
+
var elementPlaceholderAttributeName = 'data-element-tmp';
|
2489
|
+
|
2490
|
+
Handlebars.registerHelper('element', function(element, options) {
|
2491
|
+
normalizeHTMLAttributeOptions(options.hash);
|
2492
|
+
var cid = _.uniqueId('element'),
|
2493
|
+
declaringView = getOptionsData(options).view;
|
2494
|
+
options.hash[elementPlaceholderAttributeName] = cid;
|
2495
|
+
declaringView._elementsByCid || (declaringView._elementsByCid = {});
|
2496
|
+
declaringView._elementsByCid[cid] = element;
|
2497
|
+
return new Handlebars.SafeString(Thorax.Util.tag(options.hash));
|
2498
|
+
});
|
2499
|
+
|
2500
|
+
Thorax.View.on('append', function(scope, callback) {
|
2501
|
+
(scope || this.$el).find('[' + elementPlaceholderAttributeName + ']').forEach(function(el) {
|
2502
|
+
var $el = $(el),
|
2503
|
+
cid = $el.attr(elementPlaceholderAttributeName),
|
2504
|
+
element = this._elementsByCid[cid];
|
2505
|
+
// A callback function may be specified as the value
|
2506
|
+
if (_.isFunction(element)) {
|
2507
|
+
element = element.call(this);
|
2508
|
+
}
|
2509
|
+
$el.replaceWith(element);
|
2510
|
+
callback && callback(element);
|
2511
|
+
}, this);
|
2512
|
+
});
|
2513
|
+
|
2514
|
+
;;
|
2515
|
+
/* global createErrorMessage */
|
2516
|
+
|
2517
|
+
Handlebars.registerHelper('super', function(options) {
|
2518
|
+
var declaringView = getOptionsData(options).view,
|
2519
|
+
parent = declaringView.constructor && declaringView.constructor.__super__;
|
2520
|
+
if (parent) {
|
2521
|
+
var template = parent.template;
|
2522
|
+
if (!template) {
|
2523
|
+
if (!parent.name) {
|
2524
|
+
throw new Error(createErrorMessage('super-parent'));
|
2525
|
+
}
|
2526
|
+
template = parent.name;
|
2527
|
+
}
|
2528
|
+
if (_.isString(template)) {
|
2529
|
+
template = Thorax.Util.getTemplate(template, false);
|
2530
|
+
}
|
2531
|
+
return new Handlebars.SafeString(template(this, options));
|
2532
|
+
} else {
|
2533
|
+
return '';
|
2534
|
+
}
|
2535
|
+
});
|
2536
|
+
|
2537
|
+
;;
|
2538
|
+
/*global collectionOptionNames, inheritVars, createErrorMessage */
|
2539
|
+
|
2540
|
+
var loadStart = 'load:start',
|
2541
|
+
loadEnd = 'load:end',
|
2542
|
+
rootObject;
|
2543
|
+
|
2544
|
+
Thorax.setRootObject = function(obj) {
|
2545
|
+
rootObject = obj;
|
2546
|
+
};
|
2547
|
+
|
2548
|
+
Thorax.loadHandler = function(start, end, context) {
|
2549
|
+
var loadCounter = _.uniqueId('load');
|
2550
|
+
return function(message, background, object) {
|
2551
|
+
var self = context || this;
|
2552
|
+
self._loadInfo = self._loadInfo || {};
|
2553
|
+
var loadInfo = self._loadInfo[loadCounter];
|
2554
|
+
|
2555
|
+
function startLoadTimeout() {
|
2556
|
+
|
2557
|
+
// If the timeout has been set already but has not triggered yet do nothing
|
2558
|
+
// Otherwise set a new timeout (either initial or for going from background to
|
2559
|
+
// non-background loading)
|
2560
|
+
if (loadInfo.timeout && !loadInfo.run) {
|
2561
|
+
return;
|
2562
|
+
}
|
2563
|
+
|
2564
|
+
var loadingTimeout = self._loadingTimeoutDuration !== undefined ?
|
2565
|
+
self._loadingTimeoutDuration : Thorax.View.prototype._loadingTimeoutDuration;
|
2566
|
+
loadInfo.timeout = setTimeout(function() {
|
2567
|
+
try {
|
2568
|
+
// We have a slight race condtion in here where the end event may have occurred
|
2569
|
+
// but the end timeout has not executed. Rather than killing a cumulative timeout
|
2570
|
+
// immediately we'll protect from that case here
|
2571
|
+
if (loadInfo.events.length) {
|
2572
|
+
loadInfo.run = true;
|
2573
|
+
start.call(self, loadInfo.message, loadInfo.background, loadInfo);
|
2574
|
+
}
|
2575
|
+
} catch (e) {
|
2576
|
+
Thorax.onException('loadStart', e);
|
2577
|
+
}
|
2578
|
+
}, loadingTimeout * 1000);
|
2579
|
+
}
|
2580
|
+
|
2581
|
+
if (!loadInfo) {
|
2582
|
+
loadInfo = self._loadInfo[loadCounter] = _.extend({
|
2583
|
+
isLoading: function() {
|
2584
|
+
return loadInfo.events.length;
|
2585
|
+
},
|
2586
|
+
|
2587
|
+
cid: loadCounter,
|
2588
|
+
events: [],
|
2589
|
+
timeout: 0,
|
2590
|
+
message: message,
|
2591
|
+
background: !!background
|
2592
|
+
}, Backbone.Events);
|
2593
|
+
startLoadTimeout();
|
2594
|
+
} else {
|
2595
|
+
clearTimeout(loadInfo.endTimeout);
|
2596
|
+
|
2597
|
+
loadInfo.message = message;
|
2598
|
+
if (!background && loadInfo.background) {
|
2599
|
+
loadInfo.background = false;
|
2600
|
+
startLoadTimeout();
|
2601
|
+
}
|
2602
|
+
}
|
2603
|
+
|
2604
|
+
// Prevent binds to the same object multiple times as this can cause very bad things
|
2605
|
+
// to happen for the load;load;end;end execution flow.
|
2606
|
+
if (_.indexOf(loadInfo.events, object) >= 0) {
|
2607
|
+
return;
|
2608
|
+
}
|
2609
|
+
|
2610
|
+
loadInfo.events.push(object);
|
2611
|
+
|
2612
|
+
object.on(loadEnd, function endCallback() {
|
2613
|
+
var loadingEndTimeout = self._loadingTimeoutEndDuration;
|
2614
|
+
if (loadingEndTimeout === void 0) {
|
2615
|
+
// If we are running on a non-view object pull the default timeout
|
2616
|
+
loadingEndTimeout = Thorax.View.prototype._loadingTimeoutEndDuration;
|
2617
|
+
}
|
2618
|
+
|
2619
|
+
var events = loadInfo.events,
|
2620
|
+
index = _.indexOf(events, object);
|
2621
|
+
if (index >= 0 && !object.isLoading()) {
|
2622
|
+
events.splice(index, 1);
|
2623
|
+
|
2624
|
+
if (_.indexOf(events, object) < 0) {
|
2625
|
+
// Last callback for this particlar object, remove the bind
|
2626
|
+
object.off(loadEnd, endCallback);
|
2627
|
+
}
|
2628
|
+
}
|
2629
|
+
|
2630
|
+
if (!events.length) {
|
2631
|
+
clearTimeout(loadInfo.endTimeout);
|
2632
|
+
loadInfo.endTimeout = setTimeout(function() {
|
2633
|
+
try {
|
2634
|
+
if (!events.length) {
|
2635
|
+
if (loadInfo.run) {
|
2636
|
+
// Emit the end behavior, but only if there is a paired start
|
2637
|
+
end && end.call(self, loadInfo.background, loadInfo);
|
2638
|
+
loadInfo.trigger(loadEnd, loadInfo);
|
2639
|
+
}
|
2640
|
+
|
2641
|
+
// If stopping make sure we don't run a start
|
2642
|
+
clearTimeout(loadInfo.timeout);
|
2643
|
+
loadInfo = self._loadInfo[loadCounter] = undefined;
|
2644
|
+
}
|
2645
|
+
} catch (e) {
|
2646
|
+
Thorax.onException('loadEnd', e);
|
2647
|
+
}
|
2648
|
+
}, loadingEndTimeout * 1000);
|
2649
|
+
}
|
2650
|
+
});
|
2651
|
+
};
|
2652
|
+
};
|
2653
|
+
|
2654
|
+
/**
|
2655
|
+
* Helper method for propagating load:start events to other objects.
|
2656
|
+
*
|
2657
|
+
* Forwards load:start events that occur on `source` to `dest`.
|
2658
|
+
*/
|
2659
|
+
Thorax.forwardLoadEvents = function(source, dest, once) {
|
2660
|
+
function load(message, backgound, object) {
|
2661
|
+
if (once) {
|
2662
|
+
source.off(loadStart, load);
|
2663
|
+
}
|
2664
|
+
dest.trigger(loadStart, message, backgound, object);
|
2665
|
+
}
|
2666
|
+
source.on(loadStart, load);
|
2667
|
+
return {
|
2668
|
+
off: function() {
|
2669
|
+
source.off(loadStart, load);
|
2670
|
+
}
|
2671
|
+
};
|
2672
|
+
};
|
2673
|
+
|
2674
|
+
//
|
2675
|
+
// Data load event generation
|
2676
|
+
//
|
2677
|
+
|
2678
|
+
/**
|
2679
|
+
* Mixing for generating load:start and load:end events.
|
2680
|
+
*/
|
2681
|
+
Thorax.mixinLoadable = function(target, useParent) {
|
2682
|
+
_.extend(target, {
|
2683
|
+
//loading config
|
2684
|
+
_loadingClassName: 'loading',
|
2685
|
+
_loadingTimeoutDuration: 0.33,
|
2686
|
+
_loadingTimeoutEndDuration: 0.10,
|
2687
|
+
|
2688
|
+
// Propagates loading view parameters to the AJAX layer
|
2689
|
+
onLoadStart: function(message, background, object) {
|
2690
|
+
var that = useParent ? this.parent : this;
|
2691
|
+
|
2692
|
+
// Protect against race conditions
|
2693
|
+
if (!that || !that.el) {
|
2694
|
+
return;
|
2695
|
+
}
|
2696
|
+
|
2697
|
+
if (!that.nonBlockingLoad && !background && rootObject && rootObject !== this) {
|
2698
|
+
rootObject.trigger(loadStart, message, background, object);
|
2699
|
+
}
|
2700
|
+
that._isLoading = true;
|
2701
|
+
$(that.el).addClass(that._loadingClassName);
|
2702
|
+
// used by loading helpers
|
2703
|
+
that.trigger('change:load-state', 'start', background);
|
2704
|
+
},
|
2705
|
+
onLoadEnd: function(/* background, object */) {
|
2706
|
+
var that = useParent ? this.parent : this;
|
2707
|
+
|
2708
|
+
// Protect against race conditions
|
2709
|
+
if (!that || !that.el) {
|
2710
|
+
return;
|
2711
|
+
}
|
2712
|
+
|
2713
|
+
that._isLoading = false;
|
2714
|
+
$(that.el).removeClass(that._loadingClassName);
|
2715
|
+
// used by loading helper
|
2716
|
+
that.trigger('change:load-state', 'end');
|
2717
|
+
}
|
2718
|
+
});
|
2719
|
+
};
|
2720
|
+
|
2721
|
+
Thorax.mixinLoadableEvents = function(target, useParent) {
|
2722
|
+
_.extend(target, {
|
2723
|
+
_loadCount: 0,
|
2724
|
+
|
2725
|
+
isLoading: function() {
|
2726
|
+
return this._loadCount > 0;
|
2727
|
+
},
|
2728
|
+
|
2729
|
+
loadStart: function(message, background) {
|
2730
|
+
this._loadCount++;
|
2731
|
+
|
2732
|
+
var that = useParent ? this.parent : this;
|
2733
|
+
that.trigger(loadStart, message, background, that);
|
2734
|
+
},
|
2735
|
+
loadEnd: function() {
|
2736
|
+
this._loadCount--;
|
2737
|
+
|
2738
|
+
var that = useParent ? this.parent : this;
|
2739
|
+
that.trigger(loadEnd, that);
|
2740
|
+
}
|
2741
|
+
});
|
2742
|
+
};
|
2743
|
+
|
2744
|
+
Thorax.mixinLoadable(Thorax.View.prototype);
|
2745
|
+
Thorax.mixinLoadableEvents(Thorax.View.prototype);
|
2746
|
+
|
2747
|
+
|
2748
|
+
if (Thorax.HelperView) {
|
2749
|
+
Thorax.mixinLoadable(Thorax.HelperView.prototype, true);
|
2750
|
+
Thorax.mixinLoadableEvents(Thorax.HelperView.prototype, true);
|
2751
|
+
}
|
2752
|
+
|
2753
|
+
if (Thorax.CollectionHelperView) {
|
2754
|
+
Thorax.mixinLoadable(Thorax.CollectionHelperView.prototype, true);
|
2755
|
+
Thorax.mixinLoadableEvents(Thorax.CollectionHelperView.prototype, true);
|
2756
|
+
}
|
2757
|
+
|
2758
|
+
Thorax.sync = function(method, dataObj, options) {
|
2759
|
+
var self = this,
|
2760
|
+
complete = options.complete;
|
2761
|
+
|
2762
|
+
options.complete = function() {
|
2763
|
+
self._request = undefined;
|
2764
|
+
self._aborted = false;
|
2765
|
+
|
2766
|
+
complete && complete.apply(this, arguments);
|
2767
|
+
};
|
2768
|
+
this._request = Backbone.sync.apply(this, arguments);
|
2769
|
+
|
2770
|
+
return this._request;
|
2771
|
+
};
|
2772
|
+
|
2773
|
+
function bindToRoute(callback, failback) {
|
2774
|
+
var fragment = Backbone.history.getFragment(),
|
2775
|
+
routeChanged = false;
|
2776
|
+
|
2777
|
+
function routeHandler() {
|
2778
|
+
if (fragment === Backbone.history.getFragment()) {
|
2779
|
+
return;
|
2780
|
+
}
|
2781
|
+
routeChanged = true;
|
2782
|
+
res.cancel();
|
2783
|
+
failback && failback();
|
2784
|
+
}
|
2785
|
+
|
2786
|
+
Backbone.history.on('route', routeHandler);
|
2787
|
+
|
2788
|
+
function finalizer() {
|
2789
|
+
Backbone.history.off('route', routeHandler);
|
2790
|
+
if (!routeChanged) {
|
2791
|
+
callback.apply(this, arguments);
|
2792
|
+
}
|
2793
|
+
}
|
2794
|
+
|
2795
|
+
var res = _.bind(finalizer, this);
|
2796
|
+
res.cancel = function() {
|
2797
|
+
Backbone.history.off('route', routeHandler);
|
2798
|
+
};
|
2799
|
+
|
2800
|
+
return res;
|
2801
|
+
}
|
2802
|
+
|
2803
|
+
function loadData(callback, failback, options) {
|
2804
|
+
if (this.isPopulated()) {
|
2805
|
+
// Defer here to maintain async callback behavior for all loading cases
|
2806
|
+
return _.defer(callback, this);
|
2807
|
+
}
|
2808
|
+
|
2809
|
+
if (arguments.length === 2 && !_.isFunction(failback) && _.isObject(failback)) {
|
2810
|
+
options = failback;
|
2811
|
+
failback = false;
|
2812
|
+
}
|
2813
|
+
|
2814
|
+
var self = this,
|
2815
|
+
routeChanged = false,
|
2816
|
+
successCallback = bindToRoute(_.bind(callback, self), function() {
|
2817
|
+
routeChanged = true;
|
2818
|
+
if (self._request) {
|
2819
|
+
self._aborted = true;
|
2820
|
+
self._request.abort();
|
2821
|
+
}
|
2822
|
+
failback && failback.call(self, false);
|
2823
|
+
});
|
2824
|
+
|
2825
|
+
this.fetch(_.defaults({
|
2826
|
+
success: successCallback,
|
2827
|
+
error: function() {
|
2828
|
+
successCallback.cancel();
|
2829
|
+
if (!routeChanged && failback) {
|
2830
|
+
failback.apply(self, [true].concat(_.toArray(arguments)));
|
2831
|
+
}
|
2832
|
+
}
|
2833
|
+
}, options));
|
2834
|
+
}
|
2835
|
+
|
2836
|
+
function fetchQueue(options, $super) {
|
2837
|
+
if (options.resetQueue) {
|
2838
|
+
// WARN: Should ensure that loaders are protected from out of band data
|
2839
|
+
// when using this option
|
2840
|
+
this.fetchQueue = undefined;
|
2841
|
+
} else if (this.fetchQueue) {
|
2842
|
+
// concurrent set/reset fetch events are not advised
|
2843
|
+
var reset = (this.fetchQueue[0] || {}).reset;
|
2844
|
+
if (reset !== options.reset) {
|
2845
|
+
// fetch with concurrent set & reset not allowed
|
2846
|
+
throw new Error(createErrorMessage('mixed-fetch'));
|
2847
|
+
}
|
2848
|
+
}
|
2849
|
+
|
2850
|
+
if (!this.fetchQueue) {
|
2851
|
+
// Kick off the request
|
2852
|
+
this.fetchQueue = [options];
|
2853
|
+
options = _.defaults({
|
2854
|
+
success: flushQueue(this, this.fetchQueue, 'success'),
|
2855
|
+
error: flushQueue(this, this.fetchQueue, 'error'),
|
2856
|
+
complete: flushQueue(this, this.fetchQueue, 'complete')
|
2857
|
+
}, options);
|
2858
|
+
|
2859
|
+
// Handle callers that do not pass in a super class and wish to implement their own
|
2860
|
+
// fetch behavior
|
2861
|
+
if ($super) {
|
2862
|
+
$super.call(this, options);
|
2863
|
+
}
|
2864
|
+
return options;
|
2865
|
+
} else {
|
2866
|
+
// Currently fetching. Queue and process once complete
|
2867
|
+
this.fetchQueue.push(options);
|
2868
|
+
}
|
2869
|
+
}
|
2870
|
+
|
2871
|
+
function flushQueue(self, fetchQueue, handler) {
|
2872
|
+
return function() {
|
2873
|
+
var args = arguments;
|
2874
|
+
|
2875
|
+
// Flush the queue. Executes any callback handlers that
|
2876
|
+
// may have been passed in the fetch options.
|
2877
|
+
_.each(fetchQueue, function(options) {
|
2878
|
+
if (options[handler]) {
|
2879
|
+
options[handler].apply(this, args);
|
2880
|
+
}
|
2881
|
+
}, this);
|
2882
|
+
|
2883
|
+
// Reset the queue if we are still the active request
|
2884
|
+
if (self.fetchQueue === fetchQueue) {
|
2885
|
+
self.fetchQueue = undefined;
|
2886
|
+
}
|
2887
|
+
};
|
2888
|
+
}
|
2889
|
+
|
2890
|
+
var klasses = [];
|
2891
|
+
Thorax.Model && klasses.push(Thorax.Model);
|
2892
|
+
Thorax.Collection && klasses.push(Thorax.Collection);
|
2893
|
+
|
2894
|
+
_.each(klasses, function(DataClass) {
|
2895
|
+
var $fetch = DataClass.prototype.fetch;
|
2896
|
+
Thorax.mixinLoadableEvents(DataClass.prototype, false);
|
2897
|
+
_.extend(DataClass.prototype, {
|
2898
|
+
sync: Thorax.sync,
|
2899
|
+
|
2900
|
+
fetch: function(options) {
|
2901
|
+
options = options || {};
|
2902
|
+
if (DataClass === Thorax.Collection) {
|
2903
|
+
if (!_.find(['reset', 'remove', 'add', 'update'], function(key) { return !_.isUndefined(options[key]); })) {
|
2904
|
+
// use backbone < 1.0 behavior to allow triggering of reset events
|
2905
|
+
options.reset = true;
|
2906
|
+
}
|
2907
|
+
}
|
2908
|
+
|
2909
|
+
if (!options.loadTriggered) {
|
2910
|
+
var self = this;
|
2911
|
+
|
2912
|
+
function endWrapper(method) {
|
2913
|
+
var $super = options[method];
|
2914
|
+
options[method] = function() {
|
2915
|
+
self.loadEnd();
|
2916
|
+
$super && $super.apply(this, arguments);
|
2917
|
+
};
|
2918
|
+
}
|
2919
|
+
|
2920
|
+
endWrapper('success');
|
2921
|
+
endWrapper('error');
|
2922
|
+
self.loadStart(undefined, options.background);
|
2923
|
+
}
|
2924
|
+
|
2925
|
+
return fetchQueue.call(this, options || {}, $fetch);
|
2926
|
+
},
|
2927
|
+
|
2928
|
+
load: function(callback, failback, options) {
|
2929
|
+
if (arguments.length === 2 && !_.isFunction(failback)) {
|
2930
|
+
options = failback;
|
2931
|
+
failback = false;
|
2932
|
+
}
|
2933
|
+
|
2934
|
+
options = options || {};
|
2935
|
+
if (!options.background && !this.isPopulated() && rootObject) {
|
2936
|
+
// Make sure that the global scope sees the proper load events here
|
2937
|
+
// if we are loading in standalone mode
|
2938
|
+
if (this.isLoading()) {
|
2939
|
+
// trigger directly because load:start has already been triggered
|
2940
|
+
rootObject.trigger(loadStart, options.message, options.background, this);
|
2941
|
+
} else {
|
2942
|
+
Thorax.forwardLoadEvents(this, rootObject, true);
|
2943
|
+
}
|
2944
|
+
}
|
2945
|
+
|
2946
|
+
loadData.call(this, callback, failback, options);
|
2947
|
+
}
|
2948
|
+
});
|
2949
|
+
});
|
2950
|
+
|
2951
|
+
Thorax.Util.bindToRoute = bindToRoute;
|
2952
|
+
|
2953
|
+
// Propagates loading view parameters to the AJAX layer
|
2954
|
+
Thorax.View.prototype._modifyDataObjectOptions = function(dataObject, options) {
|
2955
|
+
options.ignoreErrors = this.ignoreFetchError;
|
2956
|
+
options.background = this.nonBlockingLoad;
|
2957
|
+
return options;
|
2958
|
+
};
|
2959
|
+
|
2960
|
+
// Thorax.CollectionHelperView inherits from CollectionView
|
2961
|
+
// not HelperView so need to set it manually
|
2962
|
+
Thorax.HelperView.prototype._modifyDataObjectOptions = Thorax.CollectionHelperView.prototype._modifyDataObjectOptions = function(dataObject, options) {
|
2963
|
+
options.ignoreErrors = this.parent.ignoreFetchError;
|
2964
|
+
options.background = this.parent.nonBlockingLoad;
|
2965
|
+
return options;
|
2966
|
+
};
|
2967
|
+
|
2968
|
+
inheritVars.collection.loading = function() {
|
2969
|
+
var loadingView = this.loadingView,
|
2970
|
+
loadingTemplate = this.loadingTemplate,
|
2971
|
+
loadingPlacement = this.loadingPlacement;
|
2972
|
+
//add "loading-view" and "loading-template" options to collection helper
|
2973
|
+
if (loadingView || loadingTemplate) {
|
2974
|
+
var callback = Thorax.loadHandler(_.bind(function() {
|
2975
|
+
var item;
|
2976
|
+
if (this.collection.length === 0) {
|
2977
|
+
this.$el.empty();
|
2978
|
+
}
|
2979
|
+
if (loadingView) {
|
2980
|
+
var instance = Thorax.Util.getViewInstance(loadingView);
|
2981
|
+
this._addChild(instance);
|
2982
|
+
if (loadingTemplate) {
|
2983
|
+
instance.render(loadingTemplate);
|
2984
|
+
} else {
|
2985
|
+
instance.render();
|
2986
|
+
}
|
2987
|
+
item = instance;
|
2988
|
+
} else {
|
2989
|
+
item = this.renderTemplate(loadingTemplate);
|
2990
|
+
}
|
2991
|
+
var index = loadingPlacement
|
2992
|
+
? loadingPlacement.call(this)
|
2993
|
+
: this.collection.length
|
2994
|
+
;
|
2995
|
+
this.appendItem(item, index);
|
2996
|
+
this.$el.children().eq(index).attr('data-loading-element', this.collection.cid);
|
2997
|
+
}, this), _.bind(function() {
|
2998
|
+
this.$el.find('[data-loading-element="' + this.collection.cid + '"]').remove();
|
2999
|
+
}, this),
|
3000
|
+
this.collection);
|
3001
|
+
|
3002
|
+
this.listenTo(this.collection, 'load:start', callback);
|
3003
|
+
}
|
3004
|
+
};
|
3005
|
+
|
3006
|
+
if (Thorax.CollectionHelperView) {
|
3007
|
+
_.extend(Thorax.CollectionHelperView.attributeWhiteList, {
|
3008
|
+
'loading-template': 'loadingTemplate',
|
3009
|
+
'loading-view': 'loadingView',
|
3010
|
+
'loading-placement': 'loadingPlacement'
|
3011
|
+
});
|
3012
|
+
}
|
3013
|
+
|
3014
|
+
Thorax.View.on({
|
3015
|
+
'load:start': Thorax.loadHandler(
|
3016
|
+
function(message, background, object) {
|
3017
|
+
this.onLoadStart(message, background, object);
|
3018
|
+
},
|
3019
|
+
function(background, object) {
|
3020
|
+
this.onLoadEnd(object);
|
3021
|
+
}),
|
3022
|
+
|
3023
|
+
collection: {
|
3024
|
+
'load:start': function(message, background, object) {
|
3025
|
+
this.trigger(loadStart, message, background, object);
|
3026
|
+
}
|
3027
|
+
},
|
3028
|
+
model: {
|
3029
|
+
'load:start': function(message, background, object) {
|
3030
|
+
this.trigger(loadStart, message, background, object);
|
3031
|
+
}
|
3032
|
+
}
|
3033
|
+
});
|
3034
|
+
|
3035
|
+
;;
|
3036
|
+
Handlebars.registerHelper('loading', function(options) {
|
3037
|
+
var view = getOptionsData(options).view;
|
3038
|
+
view.off('change:load-state', onLoadStateChange, view);
|
3039
|
+
view.on('change:load-state', onLoadStateChange, view);
|
3040
|
+
return view._isLoading ? options.fn(this) : options.inverse(this);
|
3041
|
+
});
|
3042
|
+
|
3043
|
+
function onLoadStateChange() {
|
3044
|
+
this.render();
|
3045
|
+
}
|
3046
|
+
;;
|
3047
|
+
/*global _replaceHTML */
|
3048
|
+
var isIE = (/msie [\w.]+/).exec(navigator.userAgent.toLowerCase());
|
3049
|
+
|
3050
|
+
if (isIE) {
|
3051
|
+
// IE will lose a reference to the elements if view.el.innerHTML = '';
|
3052
|
+
// If they are removed one by one the references are not lost.
|
3053
|
+
// For instance a view's childrens' `el`s will be lost if the view
|
3054
|
+
// sets it's `el.innerHTML`.
|
3055
|
+
Thorax.View.on('before:append', function() {
|
3056
|
+
// note that detach is not available in Zepto,
|
3057
|
+
// but IE should never run with Zepto
|
3058
|
+
if (this._renderCount > 0) {
|
3059
|
+
_.each(this._elementsByCid, function(element) {
|
3060
|
+
$(element).detach();
|
3061
|
+
});
|
3062
|
+
_.each(this.children, function(child) {
|
3063
|
+
child.$el.detach();
|
3064
|
+
});
|
3065
|
+
}
|
3066
|
+
});
|
3067
|
+
|
3068
|
+
// Once nodes are detached their innerHTML gets nuked in IE
|
3069
|
+
// so create a deep clone. This method is identical to the
|
3070
|
+
// main implementation except for ".clone(true, true)" which
|
3071
|
+
// will perform a deep clone with events and data
|
3072
|
+
Thorax.CollectionView.prototype._replaceHTML = function(html) {
|
3073
|
+
if (this.getObjectOptions(this.collection) && this._renderCount) {
|
3074
|
+
var element;
|
3075
|
+
var oldCollectionElement = this.getCollectionElement().clone(true, true);
|
3076
|
+
element = _replaceHTML.call(this, html);
|
3077
|
+
if (!oldCollectionElement.attr('data-view-cid')) {
|
3078
|
+
this.getCollectionElement().replaceWith(oldCollectionElement);
|
3079
|
+
}
|
3080
|
+
} else {
|
3081
|
+
return _replaceHTML.call(this, html);
|
3082
|
+
}
|
3083
|
+
};
|
3084
|
+
}
|
3085
|
+
|
3086
|
+
;;
|
3087
|
+
|
3088
|
+
|
3089
|
+
})();
|
3090
|
+
|
3091
|
+
//@ sourceMappingURL=thorax.js.map
|