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.
@@ -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