yano-backbone-rails 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/CODE_OF_CONDUCT.md +49 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +41 -0
  7. data/Rakefile +2 -0
  8. data/lib/yano/backbone/rails/engine.rb +8 -0
  9. data/lib/yano/backbone/rails/version.rb +10 -0
  10. data/lib/yano/backbone/rails.rb +9 -0
  11. data/lib/yano-backbone-rails.rb +3 -0
  12. data/vendor/assets/javascripts/backbone/backbone.babysitter.js +190 -0
  13. data/vendor/assets/javascripts/backbone/backbone.babysitter.map +1 -0
  14. data/vendor/assets/javascripts/backbone/backbone.babysitter.min.js +12 -0
  15. data/vendor/assets/javascripts/backbone/backbone.babysitter.min.js.map +1 -0
  16. data/vendor/assets/javascripts/backbone/backbone.handlebars.adapter.js +7 -0
  17. data/vendor/assets/javascripts/backbone/backbone.js +1920 -0
  18. data/vendor/assets/javascripts/backbone/backbone.marionette.js +3509 -0
  19. data/vendor/assets/javascripts/backbone/backbone.marionette.min.js +3 -0
  20. data/vendor/assets/javascripts/backbone/backbone.min.js +2 -0
  21. data/vendor/assets/javascripts/backbone/backbone.radio.adapter.js +23 -0
  22. data/vendor/assets/javascripts/backbone/backbone.radio.js +350 -0
  23. data/vendor/assets/javascripts/backbone/backbone.radio.js.map +1 -0
  24. data/vendor/assets/javascripts/backbone/backbone.radio.min.js +3 -0
  25. data/vendor/assets/javascripts/backbone/backbone.radio.min.js.map +2 -0
  26. data/vendor/assets/javascripts/backbone/underscore.js +1548 -0
  27. data/vendor/assets/javascripts/backbone/underscore.min.js +6 -0
  28. data/vendor/assets/javascripts/backbone.js +10 -0
  29. data/vendor/assets/javascripts/backbone.min.js +9 -0
  30. data/yano-backbone-rails.gemspec +33 -0
  31. metadata +164 -0
@@ -0,0 +1,3509 @@
1
+ // MarionetteJS (Backbone.Marionette)
2
+ // ----------------------------------
3
+ // v2.4.5
4
+ //
5
+ // Copyright (c)2016 Derick Bailey, Muted Solutions, LLC.
6
+ // Distributed under MIT license
7
+ //
8
+ // http://marionettejs.com
9
+
10
+ (function(root, factory) {
11
+
12
+ if (typeof define === 'function' && define.amd) {
13
+ define(['backbone', 'underscore', 'backbone.wreqr', 'backbone.babysitter'], function(Backbone, _) {
14
+ return (root.Marionette = root.Mn = factory(root, Backbone, _));
15
+ });
16
+ } else if (typeof exports !== 'undefined') {
17
+ var Backbone = require('backbone');
18
+ var _ = require('underscore');
19
+ var Wreqr = require('backbone.wreqr');
20
+ var BabySitter = require('backbone.babysitter');
21
+ module.exports = factory(root, Backbone, _);
22
+ } else {
23
+ root.Marionette = root.Mn = factory(root, root.Backbone, root._);
24
+ }
25
+
26
+ }(this, function(root, Backbone, _) {
27
+ 'use strict';
28
+
29
+ var previousMarionette = root.Marionette;
30
+ var previousMn = root.Mn;
31
+
32
+ var Marionette = Backbone.Marionette = {};
33
+
34
+ Marionette.VERSION = '2.4.5';
35
+
36
+ Marionette.noConflict = function() {
37
+ root.Marionette = previousMarionette;
38
+ root.Mn = previousMn;
39
+ return this;
40
+ };
41
+
42
+ // Get the Deferred creator for later use
43
+ Marionette.Deferred = Backbone.$.Deferred;
44
+
45
+ Marionette.FEATURES = {
46
+ };
47
+
48
+ Marionette.isEnabled = function(name) {
49
+ return !!Marionette.FEATURES[name];
50
+ };
51
+
52
+ /* jshint unused: false *//* global console */
53
+
54
+ // Helpers
55
+ // -------
56
+
57
+ // Marionette.extend
58
+ // -----------------
59
+
60
+ // Borrow the Backbone `extend` method so we can use it as needed
61
+ Marionette.extend = Backbone.Model.extend;
62
+
63
+ // Marionette.isNodeAttached
64
+ // -------------------------
65
+
66
+ // Determine if `el` is a child of the document
67
+ Marionette.isNodeAttached = function(el) {
68
+ return Backbone.$.contains(document.documentElement, el);
69
+ };
70
+
71
+ // Merge `keys` from `options` onto `this`
72
+ Marionette.mergeOptions = function(options, keys) {
73
+ if (!options) { return; }
74
+ _.extend(this, _.pick(options, keys));
75
+ };
76
+
77
+ // Marionette.getOption
78
+ // --------------------
79
+
80
+ // Retrieve an object, function or other value from a target
81
+ // object or its `options`, with `options` taking precedence.
82
+ Marionette.getOption = function(target, optionName) {
83
+ if (!target || !optionName) { return; }
84
+ if (target.options && (target.options[optionName] !== undefined)) {
85
+ return target.options[optionName];
86
+ } else {
87
+ return target[optionName];
88
+ }
89
+ };
90
+
91
+ // Proxy `Marionette.getOption`
92
+ Marionette.proxyGetOption = function(optionName) {
93
+ return Marionette.getOption(this, optionName);
94
+ };
95
+
96
+ // Similar to `_.result`, this is a simple helper
97
+ // If a function is provided we call it with context
98
+ // otherwise just return the value. If the value is
99
+ // undefined return a default value
100
+ Marionette._getValue = function(value, context, params) {
101
+ if (_.isFunction(value)) {
102
+ value = params ? value.apply(context, params) : value.call(context);
103
+ }
104
+ return value;
105
+ };
106
+
107
+ // Marionette.normalizeMethods
108
+ // ----------------------
109
+
110
+ // Pass in a mapping of events => functions or function names
111
+ // and return a mapping of events => functions
112
+ Marionette.normalizeMethods = function(hash) {
113
+ return _.reduce(hash, function(normalizedHash, method, name) {
114
+ if (!_.isFunction(method)) {
115
+ method = this[method];
116
+ }
117
+ if (method) {
118
+ normalizedHash[name] = method;
119
+ }
120
+ return normalizedHash;
121
+ }, {}, this);
122
+ };
123
+
124
+ // utility method for parsing @ui. syntax strings
125
+ // into associated selector
126
+ Marionette.normalizeUIString = function(uiString, ui) {
127
+ return uiString.replace(/@ui\.[a-zA-Z-_$0-9]*/g, function(r) {
128
+ return ui[r.slice(4)];
129
+ });
130
+ };
131
+
132
+ // allows for the use of the @ui. syntax within
133
+ // a given key for triggers and events
134
+ // swaps the @ui with the associated selector.
135
+ // Returns a new, non-mutated, parsed events hash.
136
+ Marionette.normalizeUIKeys = function(hash, ui) {
137
+ return _.reduce(hash, function(memo, val, key) {
138
+ var normalizedKey = Marionette.normalizeUIString(key, ui);
139
+ memo[normalizedKey] = val;
140
+ return memo;
141
+ }, {});
142
+ };
143
+
144
+ // allows for the use of the @ui. syntax within
145
+ // a given value for regions
146
+ // swaps the @ui with the associated selector
147
+ Marionette.normalizeUIValues = function(hash, ui, properties) {
148
+ _.each(hash, function(val, key) {
149
+ if (_.isString(val)) {
150
+ hash[key] = Marionette.normalizeUIString(val, ui);
151
+ } else if (_.isObject(val) && _.isArray(properties)) {
152
+ _.extend(val, Marionette.normalizeUIValues(_.pick(val, properties), ui));
153
+ /* Value is an object, and we got an array of embedded property names to normalize. */
154
+ _.each(properties, function(property) {
155
+ var propertyVal = val[property];
156
+ if (_.isString(propertyVal)) {
157
+ val[property] = Marionette.normalizeUIString(propertyVal, ui);
158
+ }
159
+ });
160
+ }
161
+ });
162
+ return hash;
163
+ };
164
+
165
+ // Mix in methods from Underscore, for iteration, and other
166
+ // collection related features.
167
+ // Borrowing this code from Backbone.Collection:
168
+ // http://backbonejs.org/docs/backbone.html#section-121
169
+ Marionette.actAsCollection = function(object, listProperty) {
170
+ var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter',
171
+ 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
172
+ 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest',
173
+ 'last', 'without', 'isEmpty', 'pluck'];
174
+
175
+ _.each(methods, function(method) {
176
+ object[method] = function() {
177
+ var list = _.values(_.result(this, listProperty));
178
+ var args = [list].concat(_.toArray(arguments));
179
+ return _[method].apply(_, args);
180
+ };
181
+ });
182
+ };
183
+
184
+ var deprecate = Marionette.deprecate = function(message, test) {
185
+ if (_.isObject(message)) {
186
+ message = (
187
+ message.prev + ' is going to be removed in the future. ' +
188
+ 'Please use ' + message.next + ' instead.' +
189
+ (message.url ? ' See: ' + message.url : '')
190
+ );
191
+ }
192
+
193
+ if ((test === undefined || !test) && !deprecate._cache[message]) {
194
+ deprecate._warn('Deprecation warning: ' + message);
195
+ deprecate._cache[message] = true;
196
+ }
197
+ };
198
+
199
+ deprecate._console = typeof console !== 'undefined' ? console : {};
200
+ deprecate._warn = function() {
201
+ var warn = deprecate._console.warn || deprecate._console.log || function() {};
202
+ return warn.apply(deprecate._console, arguments);
203
+ };
204
+ deprecate._cache = {};
205
+
206
+ /* jshint maxstatements: 14, maxcomplexity: 7 */
207
+
208
+ // Trigger Method
209
+ // --------------
210
+
211
+ Marionette._triggerMethod = (function() {
212
+ // split the event name on the ":"
213
+ var splitter = /(^|:)(\w)/gi;
214
+
215
+ // take the event section ("section1:section2:section3")
216
+ // and turn it in to uppercase name
217
+ function getEventName(match, prefix, eventName) {
218
+ return eventName.toUpperCase();
219
+ }
220
+
221
+ return function(context, event, args) {
222
+ var noEventArg = arguments.length < 3;
223
+ if (noEventArg) {
224
+ args = event;
225
+ event = args[0];
226
+ }
227
+
228
+ // get the method name from the event name
229
+ var methodName = 'on' + event.replace(splitter, getEventName);
230
+ var method = context[methodName];
231
+ var result;
232
+
233
+ // call the onMethodName if it exists
234
+ if (_.isFunction(method)) {
235
+ // pass all args, except the event name
236
+ result = method.apply(context, noEventArg ? _.rest(args) : args);
237
+ }
238
+
239
+ // trigger the event, if a trigger method exists
240
+ if (_.isFunction(context.trigger)) {
241
+ if (noEventArg + args.length > 1) {
242
+ context.trigger.apply(context, noEventArg ? args : [event].concat(_.drop(args, 0)));
243
+ } else {
244
+ context.trigger(event);
245
+ }
246
+ }
247
+
248
+ return result;
249
+ };
250
+ })();
251
+
252
+ // Trigger an event and/or a corresponding method name. Examples:
253
+ //
254
+ // `this.triggerMethod("foo")` will trigger the "foo" event and
255
+ // call the "onFoo" method.
256
+ //
257
+ // `this.triggerMethod("foo:bar")` will trigger the "foo:bar" event and
258
+ // call the "onFooBar" method.
259
+ Marionette.triggerMethod = function(event) {
260
+ return Marionette._triggerMethod(this, arguments);
261
+ };
262
+
263
+ // triggerMethodOn invokes triggerMethod on a specific context
264
+ //
265
+ // e.g. `Marionette.triggerMethodOn(view, 'show')`
266
+ // will trigger a "show" event or invoke onShow the view.
267
+ Marionette.triggerMethodOn = function(context) {
268
+ var fnc = _.isFunction(context.triggerMethod) ?
269
+ context.triggerMethod :
270
+ Marionette.triggerMethod;
271
+
272
+ return fnc.apply(context, _.rest(arguments));
273
+ };
274
+
275
+ // DOM Refresh
276
+ // -----------
277
+
278
+ // Monitor a view's state, and after it has been rendered and shown
279
+ // in the DOM, trigger a "dom:refresh" event every time it is
280
+ // re-rendered.
281
+
282
+ Marionette.MonitorDOMRefresh = function(view) {
283
+ if (view._isDomRefreshMonitored) { return; }
284
+ view._isDomRefreshMonitored = true;
285
+
286
+ // track when the view has been shown in the DOM,
287
+ // using a Marionette.Region (or by other means of triggering "show")
288
+ function handleShow() {
289
+ view._isShown = true;
290
+ triggerDOMRefresh();
291
+ }
292
+
293
+ // track when the view has been rendered
294
+ function handleRender() {
295
+ view._isRendered = true;
296
+ triggerDOMRefresh();
297
+ }
298
+
299
+ // Trigger the "dom:refresh" event and corresponding "onDomRefresh" method
300
+ function triggerDOMRefresh() {
301
+ if (view._isShown && view._isRendered && Marionette.isNodeAttached(view.el)) {
302
+ Marionette.triggerMethodOn(view, 'dom:refresh', view);
303
+ }
304
+ }
305
+
306
+ view.on({
307
+ show: handleShow,
308
+ render: handleRender
309
+ });
310
+ };
311
+
312
+ /* jshint maxparams: 5 */
313
+
314
+ // Bind Entity Events & Unbind Entity Events
315
+ // -----------------------------------------
316
+ //
317
+ // These methods are used to bind/unbind a backbone "entity" (e.g. collection/model)
318
+ // to methods on a target object.
319
+ //
320
+ // The first parameter, `target`, must have the Backbone.Events module mixed in.
321
+ //
322
+ // The second parameter is the `entity` (Backbone.Model, Backbone.Collection or
323
+ // any object that has Backbone.Events mixed in) to bind the events from.
324
+ //
325
+ // The third parameter is a hash of { "event:name": "eventHandler" }
326
+ // configuration. Multiple handlers can be separated by a space. A
327
+ // function can be supplied instead of a string handler name.
328
+
329
+ (function(Marionette) {
330
+ 'use strict';
331
+
332
+ // Bind the event to handlers specified as a string of
333
+ // handler names on the target object
334
+ function bindFromStrings(target, entity, evt, methods) {
335
+ var methodNames = methods.split(/\s+/);
336
+
337
+ _.each(methodNames, function(methodName) {
338
+
339
+ var method = target[methodName];
340
+ if (!method) {
341
+ throw new Marionette.Error('Method "' + methodName +
342
+ '" was configured as an event handler, but does not exist.');
343
+ }
344
+
345
+ target.listenTo(entity, evt, method);
346
+ });
347
+ }
348
+
349
+ // Bind the event to a supplied callback function
350
+ function bindToFunction(target, entity, evt, method) {
351
+ target.listenTo(entity, evt, method);
352
+ }
353
+
354
+ // Bind the event to handlers specified as a string of
355
+ // handler names on the target object
356
+ function unbindFromStrings(target, entity, evt, methods) {
357
+ var methodNames = methods.split(/\s+/);
358
+
359
+ _.each(methodNames, function(methodName) {
360
+ var method = target[methodName];
361
+ target.stopListening(entity, evt, method);
362
+ });
363
+ }
364
+
365
+ // Bind the event to a supplied callback function
366
+ function unbindToFunction(target, entity, evt, method) {
367
+ target.stopListening(entity, evt, method);
368
+ }
369
+
370
+ // generic looping function
371
+ function iterateEvents(target, entity, bindings, functionCallback, stringCallback) {
372
+ if (!entity || !bindings) { return; }
373
+
374
+ // type-check bindings
375
+ if (!_.isObject(bindings)) {
376
+ throw new Marionette.Error({
377
+ message: 'Bindings must be an object or function.',
378
+ url: 'marionette.functions.html#marionettebindentityevents'
379
+ });
380
+ }
381
+
382
+ // allow the bindings to be a function
383
+ bindings = Marionette._getValue(bindings, target);
384
+
385
+ // iterate the bindings and bind them
386
+ _.each(bindings, function(methods, evt) {
387
+
388
+ // allow for a function as the handler,
389
+ // or a list of event names as a string
390
+ if (_.isFunction(methods)) {
391
+ functionCallback(target, entity, evt, methods);
392
+ } else {
393
+ stringCallback(target, entity, evt, methods);
394
+ }
395
+
396
+ });
397
+ }
398
+
399
+ // Export Public API
400
+ Marionette.bindEntityEvents = function(target, entity, bindings) {
401
+ iterateEvents(target, entity, bindings, bindToFunction, bindFromStrings);
402
+ };
403
+
404
+ Marionette.unbindEntityEvents = function(target, entity, bindings) {
405
+ iterateEvents(target, entity, bindings, unbindToFunction, unbindFromStrings);
406
+ };
407
+
408
+ // Proxy `bindEntityEvents`
409
+ Marionette.proxyBindEntityEvents = function(entity, bindings) {
410
+ return Marionette.bindEntityEvents(this, entity, bindings);
411
+ };
412
+
413
+ // Proxy `unbindEntityEvents`
414
+ Marionette.proxyUnbindEntityEvents = function(entity, bindings) {
415
+ return Marionette.unbindEntityEvents(this, entity, bindings);
416
+ };
417
+ })(Marionette);
418
+
419
+
420
+ // Error
421
+ // -----
422
+
423
+ var errorProps = ['description', 'fileName', 'lineNumber', 'name', 'message', 'number'];
424
+
425
+ Marionette.Error = Marionette.extend.call(Error, {
426
+ urlRoot: 'http://marionettejs.com/docs/v' + Marionette.VERSION + '/',
427
+
428
+ constructor: function(message, options) {
429
+ if (_.isObject(message)) {
430
+ options = message;
431
+ message = options.message;
432
+ } else if (!options) {
433
+ options = {};
434
+ }
435
+
436
+ var error = Error.call(this, message);
437
+ _.extend(this, _.pick(error, errorProps), _.pick(options, errorProps));
438
+
439
+ this.captureStackTrace();
440
+
441
+ if (options.url) {
442
+ this.url = this.urlRoot + options.url;
443
+ }
444
+ },
445
+
446
+ captureStackTrace: function() {
447
+ if (Error.captureStackTrace) {
448
+ Error.captureStackTrace(this, Marionette.Error);
449
+ }
450
+ },
451
+
452
+ toString: function() {
453
+ return this.name + ': ' + this.message + (this.url ? ' See: ' + this.url : '');
454
+ }
455
+ });
456
+
457
+ Marionette.Error.extend = Marionette.extend;
458
+
459
+ // Callbacks
460
+ // ---------
461
+
462
+ // A simple way of managing a collection of callbacks
463
+ // and executing them at a later point in time, using jQuery's
464
+ // `Deferred` object.
465
+ Marionette.Callbacks = function() {
466
+ this._deferred = Marionette.Deferred();
467
+ this._callbacks = [];
468
+ };
469
+
470
+ _.extend(Marionette.Callbacks.prototype, {
471
+
472
+ // Add a callback to be executed. Callbacks added here are
473
+ // guaranteed to execute, even if they are added after the
474
+ // `run` method is called.
475
+ add: function(callback, contextOverride) {
476
+ var promise = _.result(this._deferred, 'promise');
477
+
478
+ this._callbacks.push({cb: callback, ctx: contextOverride});
479
+
480
+ promise.then(function(args) {
481
+ if (contextOverride) { args.context = contextOverride; }
482
+ callback.call(args.context, args.options);
483
+ });
484
+ },
485
+
486
+ // Run all registered callbacks with the context specified.
487
+ // Additional callbacks can be added after this has been run
488
+ // and they will still be executed.
489
+ run: function(options, context) {
490
+ this._deferred.resolve({
491
+ options: options,
492
+ context: context
493
+ });
494
+ },
495
+
496
+ // Resets the list of callbacks to be run, allowing the same list
497
+ // to be run multiple times - whenever the `run` method is called.
498
+ reset: function() {
499
+ var callbacks = this._callbacks;
500
+ this._deferred = Marionette.Deferred();
501
+ this._callbacks = [];
502
+
503
+ _.each(callbacks, function(cb) {
504
+ this.add(cb.cb, cb.ctx);
505
+ }, this);
506
+ }
507
+ });
508
+
509
+ // Controller
510
+ // ----------
511
+
512
+ // A multi-purpose object to use as a controller for
513
+ // modules and routers, and as a mediator for workflow
514
+ // and coordination of other objects, views, and more.
515
+ Marionette.Controller = function(options) {
516
+ this.options = options || {};
517
+
518
+ if (_.isFunction(this.initialize)) {
519
+ this.initialize(this.options);
520
+ }
521
+ };
522
+
523
+ Marionette.Controller.extend = Marionette.extend;
524
+
525
+ // Controller Methods
526
+ // --------------
527
+
528
+ // Ensure it can trigger events with Backbone.Events
529
+ _.extend(Marionette.Controller.prototype, Backbone.Events, {
530
+ destroy: function() {
531
+ Marionette._triggerMethod(this, 'before:destroy', arguments);
532
+ Marionette._triggerMethod(this, 'destroy', arguments);
533
+
534
+ this.stopListening();
535
+ this.off();
536
+ return this;
537
+ },
538
+
539
+ // import the `triggerMethod` to trigger events with corresponding
540
+ // methods if the method exists
541
+ triggerMethod: Marionette.triggerMethod,
542
+
543
+ // A handy way to merge options onto the instance
544
+ mergeOptions: Marionette.mergeOptions,
545
+
546
+ // Proxy `getOption` to enable getting options from this or this.options by name.
547
+ getOption: Marionette.proxyGetOption
548
+
549
+ });
550
+
551
+ // Object
552
+ // ------
553
+
554
+ // A Base Class that other Classes should descend from.
555
+ // Object borrows many conventions and utilities from Backbone.
556
+ Marionette.Object = function(options) {
557
+ this.options = _.extend({}, _.result(this, 'options'), options);
558
+
559
+ this.initialize.apply(this, arguments);
560
+ };
561
+
562
+ Marionette.Object.extend = Marionette.extend;
563
+
564
+ // Object Methods
565
+ // --------------
566
+
567
+ // Ensure it can trigger events with Backbone.Events
568
+ _.extend(Marionette.Object.prototype, Backbone.Events, {
569
+
570
+ //this is a noop method intended to be overridden by classes that extend from this base
571
+ initialize: function() {},
572
+
573
+ destroy: function(options) {
574
+ options = options || {};
575
+
576
+ this.triggerMethod('before:destroy', options);
577
+ this.triggerMethod('destroy', options);
578
+ this.stopListening();
579
+
580
+ return this;
581
+ },
582
+
583
+ // Import the `triggerMethod` to trigger events with corresponding
584
+ // methods if the method exists
585
+ triggerMethod: Marionette.triggerMethod,
586
+
587
+ // A handy way to merge options onto the instance
588
+ mergeOptions: Marionette.mergeOptions,
589
+
590
+ // Proxy `getOption` to enable getting options from this or this.options by name.
591
+ getOption: Marionette.proxyGetOption,
592
+
593
+ // Proxy `bindEntityEvents` to enable binding view's events from another entity.
594
+ bindEntityEvents: Marionette.proxyBindEntityEvents,
595
+
596
+ // Proxy `unbindEntityEvents` to enable unbinding view's events from another entity.
597
+ unbindEntityEvents: Marionette.proxyUnbindEntityEvents
598
+ });
599
+
600
+ /* jshint maxcomplexity: 16, maxstatements: 45, maxlen: 120 */
601
+
602
+ // Region
603
+ // ------
604
+
605
+ // Manage the visual regions of your composite application. See
606
+ // http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/
607
+
608
+ Marionette.Region = Marionette.Object.extend({
609
+ constructor: function(options) {
610
+
611
+ // set options temporarily so that we can get `el`.
612
+ // options will be overriden by Object.constructor
613
+ this.options = options || {};
614
+ this.el = this.getOption('el');
615
+
616
+ // Handle when this.el is passed in as a $ wrapped element.
617
+ this.el = this.el instanceof Backbone.$ ? this.el[0] : this.el;
618
+
619
+ if (!this.el) {
620
+ throw new Marionette.Error({
621
+ name: 'NoElError',
622
+ message: 'An "el" must be specified for a region.'
623
+ });
624
+ }
625
+
626
+ this.$el = this.getEl(this.el);
627
+ Marionette.Object.call(this, options);
628
+ },
629
+
630
+ // Displays a backbone view instance inside of the region.
631
+ // Handles calling the `render` method for you. Reads content
632
+ // directly from the `el` attribute. Also calls an optional
633
+ // `onShow` and `onDestroy` method on your view, just after showing
634
+ // or just before destroying the view, respectively.
635
+ // The `preventDestroy` option can be used to prevent a view from
636
+ // the old view being destroyed on show.
637
+ // The `forceShow` option can be used to force a view to be
638
+ // re-rendered if it's already shown in the region.
639
+ show: function(view, options) {
640
+ if (!this._ensureElement()) {
641
+ return;
642
+ }
643
+
644
+ this._ensureViewIsIntact(view);
645
+ Marionette.MonitorDOMRefresh(view);
646
+
647
+ var showOptions = options || {};
648
+ var isDifferentView = view !== this.currentView;
649
+ var preventDestroy = !!showOptions.preventDestroy;
650
+ var forceShow = !!showOptions.forceShow;
651
+
652
+ // We are only changing the view if there is a current view to change to begin with
653
+ var isChangingView = !!this.currentView;
654
+
655
+ // Only destroy the current view if we don't want to `preventDestroy` and if
656
+ // the view given in the first argument is different than `currentView`
657
+ var _shouldDestroyView = isDifferentView && !preventDestroy;
658
+
659
+ // Only show the view given in the first argument if it is different than
660
+ // the current view or if we want to re-show the view. Note that if
661
+ // `_shouldDestroyView` is true, then `_shouldShowView` is also necessarily true.
662
+ var _shouldShowView = isDifferentView || forceShow;
663
+
664
+ if (isChangingView) {
665
+ this.triggerMethod('before:swapOut', this.currentView, this, options);
666
+ }
667
+
668
+ if (this.currentView && isDifferentView) {
669
+ delete this.currentView._parent;
670
+ }
671
+
672
+ if (_shouldDestroyView) {
673
+ this.empty();
674
+
675
+ // A `destroy` event is attached to the clean up manually removed views.
676
+ // We need to detach this event when a new view is going to be shown as it
677
+ // is no longer relevant.
678
+ } else if (isChangingView && _shouldShowView) {
679
+ this.currentView.off('destroy', this.empty, this);
680
+ }
681
+
682
+ if (_shouldShowView) {
683
+
684
+ // We need to listen for if a view is destroyed
685
+ // in a way other than through the region.
686
+ // If this happens we need to remove the reference
687
+ // to the currentView since once a view has been destroyed
688
+ // we can not reuse it.
689
+ view.once('destroy', this.empty, this);
690
+
691
+ // make this region the view's parent,
692
+ // It's important that this parent binding happens before rendering
693
+ // so that any events the child may trigger during render can also be
694
+ // triggered on the child's ancestor views
695
+ view._parent = this;
696
+ this._renderView(view);
697
+
698
+ if (isChangingView) {
699
+ this.triggerMethod('before:swap', view, this, options);
700
+ }
701
+
702
+ this.triggerMethod('before:show', view, this, options);
703
+ Marionette.triggerMethodOn(view, 'before:show', view, this, options);
704
+
705
+ if (isChangingView) {
706
+ this.triggerMethod('swapOut', this.currentView, this, options);
707
+ }
708
+
709
+ // An array of views that we're about to display
710
+ var attachedRegion = Marionette.isNodeAttached(this.el);
711
+
712
+ // The views that we're about to attach to the document
713
+ // It's important that we prevent _getNestedViews from being executed unnecessarily
714
+ // as it's a potentially-slow method
715
+ var displayedViews = [];
716
+
717
+ var attachOptions = _.extend({
718
+ triggerBeforeAttach: this.triggerBeforeAttach,
719
+ triggerAttach: this.triggerAttach
720
+ }, showOptions);
721
+
722
+ if (attachedRegion && attachOptions.triggerBeforeAttach) {
723
+ displayedViews = this._displayedViews(view);
724
+ this._triggerAttach(displayedViews, 'before:');
725
+ }
726
+
727
+ this.attachHtml(view);
728
+ this.currentView = view;
729
+
730
+ if (attachedRegion && attachOptions.triggerAttach) {
731
+ displayedViews = this._displayedViews(view);
732
+ this._triggerAttach(displayedViews);
733
+ }
734
+
735
+ if (isChangingView) {
736
+ this.triggerMethod('swap', view, this, options);
737
+ }
738
+
739
+ this.triggerMethod('show', view, this, options);
740
+ Marionette.triggerMethodOn(view, 'show', view, this, options);
741
+
742
+ return this;
743
+ }
744
+
745
+ return this;
746
+ },
747
+
748
+ triggerBeforeAttach: true,
749
+ triggerAttach: true,
750
+
751
+ _triggerAttach: function(views, prefix) {
752
+ var eventName = (prefix || '') + 'attach';
753
+ _.each(views, function(view) {
754
+ Marionette.triggerMethodOn(view, eventName, view, this);
755
+ }, this);
756
+ },
757
+
758
+ _displayedViews: function(view) {
759
+ return _.union([view], _.result(view, '_getNestedViews') || []);
760
+ },
761
+
762
+ _renderView: function(view) {
763
+ if (!view.supportsRenderLifecycle) {
764
+ Marionette.triggerMethodOn(view, 'before:render', view);
765
+ }
766
+ view.render();
767
+ if (!view.supportsRenderLifecycle) {
768
+ Marionette.triggerMethodOn(view, 'render', view);
769
+ }
770
+ },
771
+
772
+ _ensureElement: function() {
773
+ if (!_.isObject(this.el)) {
774
+ this.$el = this.getEl(this.el);
775
+ this.el = this.$el[0];
776
+ }
777
+
778
+ if (!this.$el || this.$el.length === 0) {
779
+ if (this.getOption('allowMissingEl')) {
780
+ return false;
781
+ } else {
782
+ throw new Marionette.Error('An "el" ' + this.$el.selector + ' must exist in DOM');
783
+ }
784
+ }
785
+ return true;
786
+ },
787
+
788
+ _ensureViewIsIntact: function(view) {
789
+ if (!view) {
790
+ throw new Marionette.Error({
791
+ name: 'ViewNotValid',
792
+ message: 'The view passed is undefined and therefore invalid. You must pass a view instance to show.'
793
+ });
794
+ }
795
+
796
+ if (view.isDestroyed) {
797
+ throw new Marionette.Error({
798
+ name: 'ViewDestroyedError',
799
+ message: 'View (cid: "' + view.cid + '") has already been destroyed and cannot be used.'
800
+ });
801
+ }
802
+ },
803
+
804
+ // Override this method to change how the region finds the DOM
805
+ // element that it manages. Return a jQuery selector object scoped
806
+ // to a provided parent el or the document if none exists.
807
+ getEl: function(el) {
808
+ return Backbone.$(el, Marionette._getValue(this.options.parentEl, this));
809
+ },
810
+
811
+ // Override this method to change how the new view is
812
+ // appended to the `$el` that the region is managing
813
+ attachHtml: function(view) {
814
+ this.$el.contents().detach();
815
+
816
+ this.el.appendChild(view.el);
817
+ },
818
+
819
+ // Destroy the current view, if there is one. If there is no
820
+ // current view, it does nothing and returns immediately.
821
+ empty: function(options) {
822
+ var view = this.currentView;
823
+
824
+ var emptyOptions = options || {};
825
+ var preventDestroy = !!emptyOptions.preventDestroy;
826
+ // If there is no view in the region
827
+ // we should not remove anything
828
+ if (!view) { return this; }
829
+
830
+ view.off('destroy', this.empty, this);
831
+ this.triggerMethod('before:empty', view);
832
+ if (!preventDestroy) {
833
+ this._destroyView();
834
+ }
835
+ this.triggerMethod('empty', view);
836
+
837
+ // Remove region pointer to the currentView
838
+ delete this.currentView;
839
+
840
+ if (preventDestroy) {
841
+ this.$el.contents().detach();
842
+ }
843
+
844
+ return this;
845
+ },
846
+
847
+ // call 'destroy' or 'remove', depending on which is found
848
+ // on the view (if showing a raw Backbone view or a Marionette View)
849
+ _destroyView: function() {
850
+ var view = this.currentView;
851
+ if (view.isDestroyed) { return; }
852
+
853
+ if (!view.supportsDestroyLifecycle) {
854
+ Marionette.triggerMethodOn(view, 'before:destroy', view);
855
+ }
856
+ if (view.destroy) {
857
+ view.destroy();
858
+ } else {
859
+ view.remove();
860
+
861
+ // appending isDestroyed to raw Backbone View allows regions
862
+ // to throw a ViewDestroyedError for this view
863
+ view.isDestroyed = true;
864
+ }
865
+ if (!view.supportsDestroyLifecycle) {
866
+ Marionette.triggerMethodOn(view, 'destroy', view);
867
+ }
868
+ },
869
+
870
+ // Attach an existing view to the region. This
871
+ // will not call `render` or `onShow` for the new view,
872
+ // and will not replace the current HTML for the `el`
873
+ // of the region.
874
+ attachView: function(view) {
875
+ if (this.currentView) {
876
+ delete this.currentView._parent;
877
+ }
878
+ view._parent = this;
879
+ this.currentView = view;
880
+ return this;
881
+ },
882
+
883
+ // Checks whether a view is currently present within
884
+ // the region. Returns `true` if there is and `false` if
885
+ // no view is present.
886
+ hasView: function() {
887
+ return !!this.currentView;
888
+ },
889
+
890
+ // Reset the region by destroying any existing view and
891
+ // clearing out the cached `$el`. The next time a view
892
+ // is shown via this region, the region will re-query the
893
+ // DOM for the region's `el`.
894
+ reset: function() {
895
+ this.empty();
896
+
897
+ if (this.$el) {
898
+ this.el = this.$el.selector;
899
+ }
900
+
901
+ delete this.$el;
902
+ return this;
903
+ }
904
+
905
+ },
906
+
907
+ // Static Methods
908
+ {
909
+
910
+ // Build an instance of a region by passing in a configuration object
911
+ // and a default region class to use if none is specified in the config.
912
+ //
913
+ // The config object should either be a string as a jQuery DOM selector,
914
+ // a Region class directly, or an object literal that specifies a selector,
915
+ // a custom regionClass, and any options to be supplied to the region:
916
+ //
917
+ // ```js
918
+ // {
919
+ // selector: "#foo",
920
+ // regionClass: MyCustomRegion,
921
+ // allowMissingEl: false
922
+ // }
923
+ // ```
924
+ //
925
+ buildRegion: function(regionConfig, DefaultRegionClass) {
926
+ if (_.isString(regionConfig)) {
927
+ return this._buildRegionFromSelector(regionConfig, DefaultRegionClass);
928
+ }
929
+
930
+ if (regionConfig.selector || regionConfig.el || regionConfig.regionClass) {
931
+ return this._buildRegionFromObject(regionConfig, DefaultRegionClass);
932
+ }
933
+
934
+ if (_.isFunction(regionConfig)) {
935
+ return this._buildRegionFromRegionClass(regionConfig);
936
+ }
937
+
938
+ throw new Marionette.Error({
939
+ message: 'Improper region configuration type.',
940
+ url: 'marionette.region.html#region-configuration-types'
941
+ });
942
+ },
943
+
944
+ // Build the region from a string selector like '#foo-region'
945
+ _buildRegionFromSelector: function(selector, DefaultRegionClass) {
946
+ return new DefaultRegionClass({el: selector});
947
+ },
948
+
949
+ // Build the region from a configuration object
950
+ // ```js
951
+ // { selector: '#foo', regionClass: FooRegion, allowMissingEl: false }
952
+ // ```
953
+ _buildRegionFromObject: function(regionConfig, DefaultRegionClass) {
954
+ var RegionClass = regionConfig.regionClass || DefaultRegionClass;
955
+ var options = _.omit(regionConfig, 'selector', 'regionClass');
956
+
957
+ if (regionConfig.selector && !options.el) {
958
+ options.el = regionConfig.selector;
959
+ }
960
+
961
+ return new RegionClass(options);
962
+ },
963
+
964
+ // Build the region directly from a given `RegionClass`
965
+ _buildRegionFromRegionClass: function(RegionClass) {
966
+ return new RegionClass();
967
+ }
968
+ });
969
+
970
+ // Region Manager
971
+ // --------------
972
+
973
+ // Manage one or more related `Marionette.Region` objects.
974
+ Marionette.RegionManager = Marionette.Controller.extend({
975
+ constructor: function(options) {
976
+ this._regions = {};
977
+ this.length = 0;
978
+
979
+ Marionette.Controller.call(this, options);
980
+
981
+ this.addRegions(this.getOption('regions'));
982
+ },
983
+
984
+ // Add multiple regions using an object literal or a
985
+ // function that returns an object literal, where
986
+ // each key becomes the region name, and each value is
987
+ // the region definition.
988
+ addRegions: function(regionDefinitions, defaults) {
989
+ regionDefinitions = Marionette._getValue(regionDefinitions, this, arguments);
990
+
991
+ return _.reduce(regionDefinitions, function(regions, definition, name) {
992
+ if (_.isString(definition)) {
993
+ definition = {selector: definition};
994
+ }
995
+ if (definition.selector) {
996
+ definition = _.defaults({}, definition, defaults);
997
+ }
998
+
999
+ regions[name] = this.addRegion(name, definition);
1000
+ return regions;
1001
+ }, {}, this);
1002
+ },
1003
+
1004
+ // Add an individual region to the region manager,
1005
+ // and return the region instance
1006
+ addRegion: function(name, definition) {
1007
+ var region;
1008
+
1009
+ if (definition instanceof Marionette.Region) {
1010
+ region = definition;
1011
+ } else {
1012
+ region = Marionette.Region.buildRegion(definition, Marionette.Region);
1013
+ }
1014
+
1015
+ this.triggerMethod('before:add:region', name, region);
1016
+
1017
+ region._parent = this;
1018
+ this._store(name, region);
1019
+
1020
+ this.triggerMethod('add:region', name, region);
1021
+ return region;
1022
+ },
1023
+
1024
+ // Get a region by name
1025
+ get: function(name) {
1026
+ return this._regions[name];
1027
+ },
1028
+
1029
+ // Gets all the regions contained within
1030
+ // the `regionManager` instance.
1031
+ getRegions: function() {
1032
+ return _.clone(this._regions);
1033
+ },
1034
+
1035
+ // Remove a region by name
1036
+ removeRegion: function(name) {
1037
+ var region = this._regions[name];
1038
+ this._remove(name, region);
1039
+
1040
+ return region;
1041
+ },
1042
+
1043
+ // Empty all regions in the region manager, and
1044
+ // remove them
1045
+ removeRegions: function() {
1046
+ var regions = this.getRegions();
1047
+ _.each(this._regions, function(region, name) {
1048
+ this._remove(name, region);
1049
+ }, this);
1050
+
1051
+ return regions;
1052
+ },
1053
+
1054
+ // Empty all regions in the region manager, but
1055
+ // leave them attached
1056
+ emptyRegions: function() {
1057
+ var regions = this.getRegions();
1058
+ _.invoke(regions, 'empty');
1059
+ return regions;
1060
+ },
1061
+
1062
+ // Destroy all regions and shut down the region
1063
+ // manager entirely
1064
+ destroy: function() {
1065
+ this.removeRegions();
1066
+ return Marionette.Controller.prototype.destroy.apply(this, arguments);
1067
+ },
1068
+
1069
+ // internal method to store regions
1070
+ _store: function(name, region) {
1071
+ if (!this._regions[name]) {
1072
+ this.length++;
1073
+ }
1074
+
1075
+ this._regions[name] = region;
1076
+ },
1077
+
1078
+ // internal method to remove a region
1079
+ _remove: function(name, region) {
1080
+ this.triggerMethod('before:remove:region', name, region);
1081
+ region.empty();
1082
+ region.stopListening();
1083
+
1084
+ delete region._parent;
1085
+ delete this._regions[name];
1086
+ this.length--;
1087
+ this.triggerMethod('remove:region', name, region);
1088
+ }
1089
+ });
1090
+
1091
+ Marionette.actAsCollection(Marionette.RegionManager.prototype, '_regions');
1092
+
1093
+
1094
+ // Template Cache
1095
+ // --------------
1096
+
1097
+ // Manage templates stored in `<script>` blocks,
1098
+ // caching them for faster access.
1099
+ Marionette.TemplateCache = function(templateId) {
1100
+ this.templateId = templateId;
1101
+ };
1102
+
1103
+ // TemplateCache object-level methods. Manage the template
1104
+ // caches from these method calls instead of creating
1105
+ // your own TemplateCache instances
1106
+ _.extend(Marionette.TemplateCache, {
1107
+ templateCaches: {},
1108
+
1109
+ // Get the specified template by id. Either
1110
+ // retrieves the cached version, or loads it
1111
+ // from the DOM.
1112
+ get: function(templateId, options) {
1113
+ var cachedTemplate = this.templateCaches[templateId];
1114
+
1115
+ if (!cachedTemplate) {
1116
+ cachedTemplate = new Marionette.TemplateCache(templateId);
1117
+ this.templateCaches[templateId] = cachedTemplate;
1118
+ }
1119
+
1120
+ return cachedTemplate.load(options);
1121
+ },
1122
+
1123
+ // Clear templates from the cache. If no arguments
1124
+ // are specified, clears all templates:
1125
+ // `clear()`
1126
+ //
1127
+ // If arguments are specified, clears each of the
1128
+ // specified templates from the cache:
1129
+ // `clear("#t1", "#t2", "...")`
1130
+ clear: function() {
1131
+ var i;
1132
+ var args = _.toArray(arguments);
1133
+ var length = args.length;
1134
+
1135
+ if (length > 0) {
1136
+ for (i = 0; i < length; i++) {
1137
+ delete this.templateCaches[args[i]];
1138
+ }
1139
+ } else {
1140
+ this.templateCaches = {};
1141
+ }
1142
+ }
1143
+ });
1144
+
1145
+ // TemplateCache instance methods, allowing each
1146
+ // template cache object to manage its own state
1147
+ // and know whether or not it has been loaded
1148
+ _.extend(Marionette.TemplateCache.prototype, {
1149
+
1150
+ // Internal method to load the template
1151
+ load: function(options) {
1152
+ // Guard clause to prevent loading this template more than once
1153
+ if (this.compiledTemplate) {
1154
+ return this.compiledTemplate;
1155
+ }
1156
+
1157
+ // Load the template and compile it
1158
+ var template = this.loadTemplate(this.templateId, options);
1159
+ this.compiledTemplate = this.compileTemplate(template, options);
1160
+
1161
+ return this.compiledTemplate;
1162
+ },
1163
+
1164
+ // Load a template from the DOM, by default. Override
1165
+ // this method to provide your own template retrieval
1166
+ // For asynchronous loading with AMD/RequireJS, consider
1167
+ // using a template-loader plugin as described here:
1168
+ // https://github.com/marionettejs/backbone.marionette/wiki/Using-marionette-with-requirejs
1169
+ loadTemplate: function(templateId, options) {
1170
+ var $template = Backbone.$(templateId);
1171
+
1172
+ if (!$template.length) {
1173
+ throw new Marionette.Error({
1174
+ name: 'NoTemplateError',
1175
+ message: 'Could not find template: "' + templateId + '"'
1176
+ });
1177
+ }
1178
+ return $template.html();
1179
+ },
1180
+
1181
+ // Pre-compile the template before caching it. Override
1182
+ // this method if you do not need to pre-compile a template
1183
+ // (JST / RequireJS for example) or if you want to change
1184
+ // the template engine used (Handebars, etc).
1185
+ compileTemplate: function(rawTemplate, options) {
1186
+ return _.template(rawTemplate, options);
1187
+ }
1188
+ });
1189
+
1190
+ // Renderer
1191
+ // --------
1192
+
1193
+ // Render a template with data by passing in the template
1194
+ // selector and the data to render.
1195
+ Marionette.Renderer = {
1196
+
1197
+ // Render a template with data. The `template` parameter is
1198
+ // passed to the `TemplateCache` object to retrieve the
1199
+ // template function. Override this method to provide your own
1200
+ // custom rendering and template handling for all of Marionette.
1201
+ render: function(template, data) {
1202
+ if (!template) {
1203
+ throw new Marionette.Error({
1204
+ name: 'TemplateNotFoundError',
1205
+ message: 'Cannot render the template since its false, null or undefined.'
1206
+ });
1207
+ }
1208
+
1209
+ var templateFunc = _.isFunction(template) ? template : Marionette.TemplateCache.get(template);
1210
+
1211
+ return templateFunc(data);
1212
+ }
1213
+ };
1214
+
1215
+
1216
+ /* jshint maxlen: 114, nonew: false */
1217
+ // View
1218
+ // ----
1219
+
1220
+ // The core view class that other Marionette views extend from.
1221
+ Marionette.View = Backbone.View.extend({
1222
+ isDestroyed: false,
1223
+ supportsRenderLifecycle: true,
1224
+ supportsDestroyLifecycle: true,
1225
+
1226
+ constructor: function(options) {
1227
+ this.render = _.bind(this.render, this);
1228
+
1229
+ options = Marionette._getValue(options, this);
1230
+
1231
+ // this exposes view options to the view initializer
1232
+ // this is a backfill since backbone removed the assignment
1233
+ // of this.options
1234
+ // at some point however this may be removed
1235
+ this.options = _.extend({}, _.result(this, 'options'), options);
1236
+
1237
+ this._behaviors = Marionette.Behaviors(this);
1238
+
1239
+ Backbone.View.call(this, this.options);
1240
+
1241
+ Marionette.MonitorDOMRefresh(this);
1242
+ },
1243
+
1244
+ // Get the template for this view
1245
+ // instance. You can set a `template` attribute in the view
1246
+ // definition or pass a `template: "whatever"` parameter in
1247
+ // to the constructor options.
1248
+ getTemplate: function() {
1249
+ return this.getOption('template');
1250
+ },
1251
+
1252
+ // Serialize a model by returning its attributes. Clones
1253
+ // the attributes to allow modification.
1254
+ serializeModel: function(model) {
1255
+ return model.toJSON.apply(model, _.rest(arguments));
1256
+ },
1257
+
1258
+ // Mix in template helper methods. Looks for a
1259
+ // `templateHelpers` attribute, which can either be an
1260
+ // object literal, or a function that returns an object
1261
+ // literal. All methods and attributes from this object
1262
+ // are copies to the object passed in.
1263
+ mixinTemplateHelpers: function(target) {
1264
+ target = target || {};
1265
+ var templateHelpers = this.getOption('templateHelpers');
1266
+ templateHelpers = Marionette._getValue(templateHelpers, this);
1267
+ return _.extend(target, templateHelpers);
1268
+ },
1269
+
1270
+ // normalize the keys of passed hash with the views `ui` selectors.
1271
+ // `{"@ui.foo": "bar"}`
1272
+ normalizeUIKeys: function(hash) {
1273
+ var uiBindings = _.result(this, '_uiBindings');
1274
+ return Marionette.normalizeUIKeys(hash, uiBindings || _.result(this, 'ui'));
1275
+ },
1276
+
1277
+ // normalize the values of passed hash with the views `ui` selectors.
1278
+ // `{foo: "@ui.bar"}`
1279
+ normalizeUIValues: function(hash, properties) {
1280
+ var ui = _.result(this, 'ui');
1281
+ var uiBindings = _.result(this, '_uiBindings');
1282
+ return Marionette.normalizeUIValues(hash, uiBindings || ui, properties);
1283
+ },
1284
+
1285
+ // Configure `triggers` to forward DOM events to view
1286
+ // events. `triggers: {"click .foo": "do:foo"}`
1287
+ configureTriggers: function() {
1288
+ if (!this.triggers) { return; }
1289
+
1290
+ // Allow `triggers` to be configured as a function
1291
+ var triggers = this.normalizeUIKeys(_.result(this, 'triggers'));
1292
+
1293
+ // Configure the triggers, prevent default
1294
+ // action and stop propagation of DOM events
1295
+ return _.reduce(triggers, function(events, value, key) {
1296
+ events[key] = this._buildViewTrigger(value);
1297
+ return events;
1298
+ }, {}, this);
1299
+ },
1300
+
1301
+ // Overriding Backbone.View's delegateEvents to handle
1302
+ // the `triggers`, `modelEvents`, and `collectionEvents` configuration
1303
+ delegateEvents: function(events) {
1304
+ this._delegateDOMEvents(events);
1305
+ this.bindEntityEvents(this.model, this.getOption('modelEvents'));
1306
+ this.bindEntityEvents(this.collection, this.getOption('collectionEvents'));
1307
+
1308
+ _.each(this._behaviors, function(behavior) {
1309
+ behavior.bindEntityEvents(this.model, behavior.getOption('modelEvents'));
1310
+ behavior.bindEntityEvents(this.collection, behavior.getOption('collectionEvents'));
1311
+ }, this);
1312
+
1313
+ return this;
1314
+ },
1315
+
1316
+ // internal method to delegate DOM events and triggers
1317
+ _delegateDOMEvents: function(eventsArg) {
1318
+ var events = Marionette._getValue(eventsArg || this.events, this);
1319
+
1320
+ // normalize ui keys
1321
+ events = this.normalizeUIKeys(events);
1322
+ if (_.isUndefined(eventsArg)) {this.events = events;}
1323
+
1324
+ var combinedEvents = {};
1325
+
1326
+ // look up if this view has behavior events
1327
+ var behaviorEvents = _.result(this, 'behaviorEvents') || {};
1328
+ var triggers = this.configureTriggers();
1329
+ var behaviorTriggers = _.result(this, 'behaviorTriggers') || {};
1330
+
1331
+ // behavior events will be overriden by view events and or triggers
1332
+ _.extend(combinedEvents, behaviorEvents, events, triggers, behaviorTriggers);
1333
+
1334
+ Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
1335
+ },
1336
+
1337
+ // Overriding Backbone.View's undelegateEvents to handle unbinding
1338
+ // the `triggers`, `modelEvents`, and `collectionEvents` config
1339
+ undelegateEvents: function() {
1340
+ Backbone.View.prototype.undelegateEvents.apply(this, arguments);
1341
+
1342
+ this.unbindEntityEvents(this.model, this.getOption('modelEvents'));
1343
+ this.unbindEntityEvents(this.collection, this.getOption('collectionEvents'));
1344
+
1345
+ _.each(this._behaviors, function(behavior) {
1346
+ behavior.unbindEntityEvents(this.model, behavior.getOption('modelEvents'));
1347
+ behavior.unbindEntityEvents(this.collection, behavior.getOption('collectionEvents'));
1348
+ }, this);
1349
+
1350
+ return this;
1351
+ },
1352
+
1353
+ // Internal helper method to verify whether the view hasn't been destroyed
1354
+ _ensureViewIsIntact: function() {
1355
+ if (this.isDestroyed) {
1356
+ throw new Marionette.Error({
1357
+ name: 'ViewDestroyedError',
1358
+ message: 'View (cid: "' + this.cid + '") has already been destroyed and cannot be used.'
1359
+ });
1360
+ }
1361
+ },
1362
+
1363
+ // Default `destroy` implementation, for removing a view from the
1364
+ // DOM and unbinding it. Regions will call this method
1365
+ // for you. You can specify an `onDestroy` method in your view to
1366
+ // add custom code that is called after the view is destroyed.
1367
+ destroy: function() {
1368
+ if (this.isDestroyed) { return this; }
1369
+
1370
+ var args = _.toArray(arguments);
1371
+
1372
+ this.triggerMethod.apply(this, ['before:destroy'].concat(args));
1373
+
1374
+ // mark as destroyed before doing the actual destroy, to
1375
+ // prevent infinite loops within "destroy" event handlers
1376
+ // that are trying to destroy other views
1377
+ this.isDestroyed = true;
1378
+ this.triggerMethod.apply(this, ['destroy'].concat(args));
1379
+
1380
+ // unbind UI elements
1381
+ this.unbindUIElements();
1382
+
1383
+ this.isRendered = false;
1384
+
1385
+ // remove the view from the DOM
1386
+ this.remove();
1387
+
1388
+ // Call destroy on each behavior after
1389
+ // destroying the view.
1390
+ // This unbinds event listeners
1391
+ // that behaviors have registered for.
1392
+ _.invoke(this._behaviors, 'destroy', args);
1393
+
1394
+ return this;
1395
+ },
1396
+
1397
+ bindUIElements: function() {
1398
+ this._bindUIElements();
1399
+ _.invoke(this._behaviors, this._bindUIElements);
1400
+ },
1401
+
1402
+ // This method binds the elements specified in the "ui" hash inside the view's code with
1403
+ // the associated jQuery selectors.
1404
+ _bindUIElements: function() {
1405
+ if (!this.ui) { return; }
1406
+
1407
+ // store the ui hash in _uiBindings so they can be reset later
1408
+ // and so re-rendering the view will be able to find the bindings
1409
+ if (!this._uiBindings) {
1410
+ this._uiBindings = this.ui;
1411
+ }
1412
+
1413
+ // get the bindings result, as a function or otherwise
1414
+ var bindings = _.result(this, '_uiBindings');
1415
+
1416
+ // empty the ui so we don't have anything to start with
1417
+ this.ui = {};
1418
+
1419
+ // bind each of the selectors
1420
+ _.each(bindings, function(selector, key) {
1421
+ this.ui[key] = this.$(selector);
1422
+ }, this);
1423
+ },
1424
+
1425
+ // This method unbinds the elements specified in the "ui" hash
1426
+ unbindUIElements: function() {
1427
+ this._unbindUIElements();
1428
+ _.invoke(this._behaviors, this._unbindUIElements);
1429
+ },
1430
+
1431
+ _unbindUIElements: function() {
1432
+ if (!this.ui || !this._uiBindings) { return; }
1433
+
1434
+ // delete all of the existing ui bindings
1435
+ _.each(this.ui, function($el, name) {
1436
+ delete this.ui[name];
1437
+ }, this);
1438
+
1439
+ // reset the ui element to the original bindings configuration
1440
+ this.ui = this._uiBindings;
1441
+ delete this._uiBindings;
1442
+ },
1443
+
1444
+ // Internal method to create an event handler for a given `triggerDef` like
1445
+ // 'click:foo'
1446
+ _buildViewTrigger: function(triggerDef) {
1447
+
1448
+ var options = _.defaults({}, triggerDef, {
1449
+ preventDefault: true,
1450
+ stopPropagation: true
1451
+ });
1452
+
1453
+ var eventName = _.isObject(triggerDef) ? options.event : triggerDef;
1454
+
1455
+ return function(e) {
1456
+ if (e) {
1457
+ if (e.preventDefault && options.preventDefault) {
1458
+ e.preventDefault();
1459
+ }
1460
+
1461
+ if (e.stopPropagation && options.stopPropagation) {
1462
+ e.stopPropagation();
1463
+ }
1464
+ }
1465
+
1466
+ var args = {
1467
+ view: this,
1468
+ model: this.model,
1469
+ collection: this.collection
1470
+ };
1471
+
1472
+ this.triggerMethod(eventName, args);
1473
+ };
1474
+ },
1475
+
1476
+ setElement: function() {
1477
+ var ret = Backbone.View.prototype.setElement.apply(this, arguments);
1478
+
1479
+ // proxy behavior $el to the view's $el.
1480
+ // This is needed because a view's $el proxy
1481
+ // is not set until after setElement is called.
1482
+ _.invoke(this._behaviors, 'proxyViewProperties', this);
1483
+
1484
+ return ret;
1485
+ },
1486
+
1487
+ // import the `triggerMethod` to trigger events with corresponding
1488
+ // methods if the method exists
1489
+ triggerMethod: function() {
1490
+ var ret = Marionette._triggerMethod(this, arguments);
1491
+
1492
+ this._triggerEventOnBehaviors(arguments);
1493
+ this._triggerEventOnParentLayout(arguments[0], _.rest(arguments));
1494
+
1495
+ return ret;
1496
+ },
1497
+
1498
+ _triggerEventOnBehaviors: function(args) {
1499
+ var triggerMethod = Marionette._triggerMethod;
1500
+ var behaviors = this._behaviors;
1501
+ // Use good ol' for as this is a very hot function
1502
+ for (var i = 0, length = behaviors && behaviors.length; i < length; i++) {
1503
+ triggerMethod(behaviors[i], args);
1504
+ }
1505
+ },
1506
+
1507
+ _triggerEventOnParentLayout: function(eventName, args) {
1508
+ var layoutView = this._parentLayoutView();
1509
+ if (!layoutView) {
1510
+ return;
1511
+ }
1512
+
1513
+ // invoke triggerMethod on parent view
1514
+ var eventPrefix = Marionette.getOption(layoutView, 'childViewEventPrefix');
1515
+ var prefixedEventName = eventPrefix + ':' + eventName;
1516
+ var callArgs = [this].concat(args);
1517
+
1518
+ Marionette._triggerMethod(layoutView, prefixedEventName, callArgs);
1519
+
1520
+ // call the parent view's childEvents handler
1521
+ var childEvents = Marionette.getOption(layoutView, 'childEvents');
1522
+
1523
+ // since childEvents can be an object or a function use Marionette._getValue
1524
+ // to handle the abstaction for us.
1525
+ childEvents = Marionette._getValue(childEvents, layoutView);
1526
+ var normalizedChildEvents = layoutView.normalizeMethods(childEvents);
1527
+
1528
+ if (normalizedChildEvents && _.isFunction(normalizedChildEvents[eventName])) {
1529
+ normalizedChildEvents[eventName].apply(layoutView, callArgs);
1530
+ }
1531
+ },
1532
+
1533
+ // This method returns any views that are immediate
1534
+ // children of this view
1535
+ _getImmediateChildren: function() {
1536
+ return [];
1537
+ },
1538
+
1539
+ // Returns an array of every nested view within this view
1540
+ _getNestedViews: function() {
1541
+ var children = this._getImmediateChildren();
1542
+
1543
+ if (!children.length) { return children; }
1544
+
1545
+ return _.reduce(children, function(memo, view) {
1546
+ if (!view._getNestedViews) { return memo; }
1547
+ return memo.concat(view._getNestedViews());
1548
+ }, children);
1549
+ },
1550
+
1551
+ // Walk the _parent tree until we find a layout view (if one exists).
1552
+ // Returns the parent layout view hierarchically closest to this view.
1553
+ _parentLayoutView: function() {
1554
+ var parent = this._parent;
1555
+
1556
+ while (parent) {
1557
+ if (parent instanceof Marionette.LayoutView) {
1558
+ return parent;
1559
+ }
1560
+ parent = parent._parent;
1561
+ }
1562
+ },
1563
+
1564
+ // Imports the "normalizeMethods" to transform hashes of
1565
+ // events=>function references/names to a hash of events=>function references
1566
+ normalizeMethods: Marionette.normalizeMethods,
1567
+
1568
+ // A handy way to merge passed-in options onto the instance
1569
+ mergeOptions: Marionette.mergeOptions,
1570
+
1571
+ // Proxy `getOption` to enable getting options from this or this.options by name.
1572
+ getOption: Marionette.proxyGetOption,
1573
+
1574
+ // Proxy `bindEntityEvents` to enable binding view's events from another entity.
1575
+ bindEntityEvents: Marionette.proxyBindEntityEvents,
1576
+
1577
+ // Proxy `unbindEntityEvents` to enable unbinding view's events from another entity.
1578
+ unbindEntityEvents: Marionette.proxyUnbindEntityEvents
1579
+ });
1580
+
1581
+ // Item View
1582
+ // ---------
1583
+
1584
+ // A single item view implementation that contains code for rendering
1585
+ // with underscore.js templates, serializing the view's model or collection,
1586
+ // and calling several methods on extended views, such as `onRender`.
1587
+ Marionette.ItemView = Marionette.View.extend({
1588
+
1589
+ // Setting up the inheritance chain which allows changes to
1590
+ // Marionette.View.prototype.constructor which allows overriding
1591
+ constructor: function() {
1592
+ Marionette.View.apply(this, arguments);
1593
+ },
1594
+
1595
+ // Serialize the model or collection for the view. If a model is
1596
+ // found, the view's `serializeModel` is called. If a collection is found,
1597
+ // each model in the collection is serialized by calling
1598
+ // the view's `serializeCollection` and put into an `items` array in
1599
+ // the resulting data. If both are found, defaults to the model.
1600
+ // You can override the `serializeData` method in your own view definition,
1601
+ // to provide custom serialization for your view's data.
1602
+ serializeData: function() {
1603
+ if (!this.model && !this.collection) {
1604
+ return {};
1605
+ }
1606
+
1607
+ var args = [this.model || this.collection];
1608
+ if (arguments.length) {
1609
+ args.push.apply(args, arguments);
1610
+ }
1611
+
1612
+ if (this.model) {
1613
+ return this.serializeModel.apply(this, args);
1614
+ } else {
1615
+ return {
1616
+ items: this.serializeCollection.apply(this, args)
1617
+ };
1618
+ }
1619
+ },
1620
+
1621
+ // Serialize a collection by serializing each of its models.
1622
+ serializeCollection: function(collection) {
1623
+ return collection.toJSON.apply(collection, _.rest(arguments));
1624
+ },
1625
+
1626
+ // Render the view, defaulting to underscore.js templates.
1627
+ // You can override this in your view definition to provide
1628
+ // a very specific rendering for your view. In general, though,
1629
+ // you should override the `Marionette.Renderer` object to
1630
+ // change how Marionette renders views.
1631
+ render: function() {
1632
+ this._ensureViewIsIntact();
1633
+
1634
+ this.triggerMethod('before:render', this);
1635
+
1636
+ this._renderTemplate();
1637
+ this.isRendered = true;
1638
+ this.bindUIElements();
1639
+
1640
+ this.triggerMethod('render', this);
1641
+
1642
+ return this;
1643
+ },
1644
+
1645
+ // Internal method to render the template with the serialized data
1646
+ // and template helpers via the `Marionette.Renderer` object.
1647
+ // Throws an `UndefinedTemplateError` error if the template is
1648
+ // any falsely value but literal `false`.
1649
+ _renderTemplate: function() {
1650
+ var template = this.getTemplate();
1651
+
1652
+ // Allow template-less item views
1653
+ if (template === false) {
1654
+ return;
1655
+ }
1656
+
1657
+ if (!template) {
1658
+ throw new Marionette.Error({
1659
+ name: 'UndefinedTemplateError',
1660
+ message: 'Cannot render the template since it is null or undefined.'
1661
+ });
1662
+ }
1663
+
1664
+ // Add in entity data and template helpers
1665
+ var data = this.mixinTemplateHelpers(this.serializeData());
1666
+
1667
+ // Render and add to el
1668
+ var html = Marionette.Renderer.render(template, data, this);
1669
+ this.attachElContent(html);
1670
+
1671
+ return this;
1672
+ },
1673
+
1674
+ // Attaches the content of a given view.
1675
+ // This method can be overridden to optimize rendering,
1676
+ // or to render in a non standard way.
1677
+ //
1678
+ // For example, using `innerHTML` instead of `$el.html`
1679
+ //
1680
+ // ```js
1681
+ // attachElContent: function(html) {
1682
+ // this.el.innerHTML = html;
1683
+ // return this;
1684
+ // }
1685
+ // ```
1686
+ attachElContent: function(html) {
1687
+ this.$el.html(html);
1688
+
1689
+ return this;
1690
+ }
1691
+ });
1692
+
1693
+ /* jshint maxstatements: 20, maxcomplexity: 7 */
1694
+
1695
+ // Collection View
1696
+ // ---------------
1697
+
1698
+ // A view that iterates over a Backbone.Collection
1699
+ // and renders an individual child view for each model.
1700
+ Marionette.CollectionView = Marionette.View.extend({
1701
+
1702
+ // used as the prefix for child view events
1703
+ // that are forwarded through the collectionview
1704
+ childViewEventPrefix: 'childview',
1705
+
1706
+ // flag for maintaining the sorted order of the collection
1707
+ sort: true,
1708
+
1709
+ // constructor
1710
+ // option to pass `{sort: false}` to prevent the `CollectionView` from
1711
+ // maintaining the sorted order of the collection.
1712
+ // This will fallback onto appending childView's to the end.
1713
+ //
1714
+ // option to pass `{comparator: compFunction()}` to allow the `CollectionView`
1715
+ // to use a custom sort order for the collection.
1716
+ constructor: function(options) {
1717
+ this.once('render', this._initialEvents);
1718
+ this._initChildViewStorage();
1719
+
1720
+ Marionette.View.apply(this, arguments);
1721
+
1722
+ this.on({
1723
+ 'before:show': this._onBeforeShowCalled,
1724
+ 'show': this._onShowCalled,
1725
+ 'before:attach': this._onBeforeAttachCalled,
1726
+ 'attach': this._onAttachCalled
1727
+ });
1728
+ this.initRenderBuffer();
1729
+ },
1730
+
1731
+ // Instead of inserting elements one by one into the page,
1732
+ // it's much more performant to insert elements into a document
1733
+ // fragment and then insert that document fragment into the page
1734
+ initRenderBuffer: function() {
1735
+ this._bufferedChildren = [];
1736
+ },
1737
+
1738
+ startBuffering: function() {
1739
+ this.initRenderBuffer();
1740
+ this.isBuffering = true;
1741
+ },
1742
+
1743
+ endBuffering: function() {
1744
+ // Only trigger attach if already shown and attached, otherwise Region#show() handles this.
1745
+ var canTriggerAttach = this._isShown && Marionette.isNodeAttached(this.el);
1746
+ var nestedViews;
1747
+
1748
+ this.isBuffering = false;
1749
+
1750
+ if (this._isShown) {
1751
+ this._triggerMethodMany(this._bufferedChildren, this, 'before:show');
1752
+ }
1753
+ if (canTriggerAttach && this._triggerBeforeAttach) {
1754
+ nestedViews = this._getNestedViews();
1755
+ this._triggerMethodMany(nestedViews, this, 'before:attach');
1756
+ }
1757
+
1758
+ this.attachBuffer(this, this._createBuffer());
1759
+
1760
+ if (canTriggerAttach && this._triggerAttach) {
1761
+ nestedViews = this._getNestedViews();
1762
+ this._triggerMethodMany(nestedViews, this, 'attach');
1763
+ }
1764
+ if (this._isShown) {
1765
+ this._triggerMethodMany(this._bufferedChildren, this, 'show');
1766
+ }
1767
+ this.initRenderBuffer();
1768
+ },
1769
+
1770
+ _triggerMethodMany: function(targets, source, eventName) {
1771
+ var args = _.drop(arguments, 3);
1772
+
1773
+ _.each(targets, function(target) {
1774
+ Marionette.triggerMethodOn.apply(target, [target, eventName, target, source].concat(args));
1775
+ });
1776
+ },
1777
+
1778
+ // Configured the initial events that the collection view
1779
+ // binds to.
1780
+ _initialEvents: function() {
1781
+ if (this.collection) {
1782
+ this.listenTo(this.collection, 'add', this._onCollectionAdd);
1783
+ this.listenTo(this.collection, 'remove', this._onCollectionRemove);
1784
+ this.listenTo(this.collection, 'reset', this.render);
1785
+
1786
+ if (this.getOption('sort')) {
1787
+ this.listenTo(this.collection, 'sort', this._sortViews);
1788
+ }
1789
+ }
1790
+ },
1791
+
1792
+ // Handle a child added to the collection
1793
+ _onCollectionAdd: function(child, collection, opts) {
1794
+ // `index` is present when adding with `at` since BB 1.2; indexOf fallback for < 1.2
1795
+ var index = opts.at !== undefined && (opts.index || collection.indexOf(child));
1796
+
1797
+ // When filtered or when there is no initial index, calculate index.
1798
+ if (this.getOption('filter') || index === false) {
1799
+ index = _.indexOf(this._filteredSortedModels(index), child);
1800
+ }
1801
+
1802
+ if (this._shouldAddChild(child, index)) {
1803
+ this.destroyEmptyView();
1804
+ var ChildView = this.getChildView(child);
1805
+ this.addChild(child, ChildView, index);
1806
+ }
1807
+ },
1808
+
1809
+ // get the child view by model it holds, and remove it
1810
+ _onCollectionRemove: function(model) {
1811
+ var view = this.children.findByModel(model);
1812
+ this.removeChildView(view);
1813
+ this.checkEmpty();
1814
+ },
1815
+
1816
+ _onBeforeShowCalled: function() {
1817
+ // Reset attach event flags at the top of the Region#show() event lifecycle; if the Region's
1818
+ // show() options permit onBeforeAttach/onAttach events, these flags will be set true again.
1819
+ this._triggerBeforeAttach = this._triggerAttach = false;
1820
+ this.children.each(function(childView) {
1821
+ Marionette.triggerMethodOn(childView, 'before:show', childView);
1822
+ });
1823
+ },
1824
+
1825
+ _onShowCalled: function() {
1826
+ this.children.each(function(childView) {
1827
+ Marionette.triggerMethodOn(childView, 'show', childView);
1828
+ });
1829
+ },
1830
+
1831
+ // If during Region#show() onBeforeAttach was fired, continue firing it for child views
1832
+ _onBeforeAttachCalled: function() {
1833
+ this._triggerBeforeAttach = true;
1834
+ },
1835
+
1836
+ // If during Region#show() onAttach was fired, continue firing it for child views
1837
+ _onAttachCalled: function() {
1838
+ this._triggerAttach = true;
1839
+ },
1840
+
1841
+ // Render children views. Override this method to
1842
+ // provide your own implementation of a render function for
1843
+ // the collection view.
1844
+ render: function() {
1845
+ this._ensureViewIsIntact();
1846
+ this.triggerMethod('before:render', this);
1847
+ this._renderChildren();
1848
+ this.isRendered = true;
1849
+ this.triggerMethod('render', this);
1850
+ return this;
1851
+ },
1852
+
1853
+ // Reorder DOM after sorting. When your element's rendering
1854
+ // do not use their index, you can pass reorderOnSort: true
1855
+ // to only reorder the DOM after a sort instead of rendering
1856
+ // all the collectionView
1857
+ reorder: function() {
1858
+ var children = this.children;
1859
+ var models = this._filteredSortedModels();
1860
+ var anyModelsAdded = _.some(models, function(model) {
1861
+ return !children.findByModel(model);
1862
+ });
1863
+
1864
+ // If there are any new models added due to filtering
1865
+ // We need to add child views
1866
+ // So render as normal
1867
+ if (anyModelsAdded) {
1868
+ this.render();
1869
+ } else {
1870
+ // get the DOM nodes in the same order as the models
1871
+ var elsToReorder = _.map(models, function(model, index) {
1872
+ var view = children.findByModel(model);
1873
+ view._index = index;
1874
+ return view.el;
1875
+ });
1876
+
1877
+ // find the views that were children before but arent in this new ordering
1878
+ var filteredOutViews = children.filter(function(view) {
1879
+ return !_.contains(elsToReorder, view.el);
1880
+ });
1881
+
1882
+ this.triggerMethod('before:reorder');
1883
+
1884
+ // since append moves elements that are already in the DOM,
1885
+ // appending the elements will effectively reorder them
1886
+ this._appendReorderedChildren(elsToReorder);
1887
+
1888
+ // remove any views that have been filtered out
1889
+ _.each(filteredOutViews, this.removeChildView, this);
1890
+ this.checkEmpty();
1891
+
1892
+ this.triggerMethod('reorder');
1893
+ }
1894
+ },
1895
+
1896
+ // Render view after sorting. Override this method to
1897
+ // change how the view renders after a `sort` on the collection.
1898
+ // An example of this would be to only `renderChildren` in a `CompositeView`
1899
+ // rather than the full view.
1900
+ resortView: function() {
1901
+ if (Marionette.getOption(this, 'reorderOnSort')) {
1902
+ this.reorder();
1903
+ } else {
1904
+ this.render();
1905
+ }
1906
+ },
1907
+
1908
+ // Internal method. This checks for any changes in the order of the collection.
1909
+ // If the index of any view doesn't match, it will render.
1910
+ _sortViews: function() {
1911
+ var models = this._filteredSortedModels();
1912
+
1913
+ // check for any changes in sort order of views
1914
+ var orderChanged = _.find(models, function(item, index) {
1915
+ var view = this.children.findByModel(item);
1916
+ return !view || view._index !== index;
1917
+ }, this);
1918
+
1919
+ if (orderChanged) {
1920
+ this.resortView();
1921
+ }
1922
+ },
1923
+
1924
+ // Internal reference to what index a `emptyView` is.
1925
+ _emptyViewIndex: -1,
1926
+
1927
+ // Internal method. Separated so that CompositeView can append to the childViewContainer
1928
+ // if necessary
1929
+ _appendReorderedChildren: function(children) {
1930
+ this.$el.append(children);
1931
+ },
1932
+
1933
+ // Internal method. Separated so that CompositeView can have
1934
+ // more control over events being triggered, around the rendering
1935
+ // process
1936
+ _renderChildren: function() {
1937
+ this.destroyEmptyView();
1938
+ this.destroyChildren({checkEmpty: false});
1939
+
1940
+ if (this.isEmpty(this.collection)) {
1941
+ this.showEmptyView();
1942
+ } else {
1943
+ this.triggerMethod('before:render:collection', this);
1944
+ this.startBuffering();
1945
+ this.showCollection();
1946
+ this.endBuffering();
1947
+ this.triggerMethod('render:collection', this);
1948
+
1949
+ // If we have shown children and none have passed the filter, show the empty view
1950
+ if (this.children.isEmpty() && this.getOption('filter')) {
1951
+ this.showEmptyView();
1952
+ }
1953
+ }
1954
+ },
1955
+
1956
+ // Internal method to loop through collection and show each child view.
1957
+ showCollection: function() {
1958
+ var ChildView;
1959
+
1960
+ var models = this._filteredSortedModels();
1961
+
1962
+ _.each(models, function(child, index) {
1963
+ ChildView = this.getChildView(child);
1964
+ this.addChild(child, ChildView, index);
1965
+ }, this);
1966
+ },
1967
+
1968
+ // Allow the collection to be sorted by a custom view comparator
1969
+ _filteredSortedModels: function(addedAt) {
1970
+ var viewComparator = this.getViewComparator();
1971
+ var models = this.collection.models;
1972
+ addedAt = Math.min(Math.max(addedAt, 0), models.length - 1);
1973
+
1974
+ if (viewComparator) {
1975
+ var addedModel;
1976
+ // Preserve `at` location, even for a sorted view
1977
+ if (addedAt) {
1978
+ addedModel = models[addedAt];
1979
+ models = models.slice(0, addedAt).concat(models.slice(addedAt + 1));
1980
+ }
1981
+ models = this._sortModelsBy(models, viewComparator);
1982
+ if (addedModel) {
1983
+ models.splice(addedAt, 0, addedModel);
1984
+ }
1985
+ }
1986
+
1987
+ // Filter after sorting in case the filter uses the index
1988
+ if (this.getOption('filter')) {
1989
+ models = _.filter(models, function(model, index) {
1990
+ return this._shouldAddChild(model, index);
1991
+ }, this);
1992
+ }
1993
+
1994
+ return models;
1995
+ },
1996
+
1997
+ _sortModelsBy: function(models, comparator) {
1998
+ if (typeof comparator === 'string') {
1999
+ return _.sortBy(models, function(model) {
2000
+ return model.get(comparator);
2001
+ }, this);
2002
+ } else if (comparator.length === 1) {
2003
+ return _.sortBy(models, comparator, this);
2004
+ } else {
2005
+ return models.sort(_.bind(comparator, this));
2006
+ }
2007
+ },
2008
+
2009
+ // Internal method to show an empty view in place of
2010
+ // a collection of child views, when the collection is empty
2011
+ showEmptyView: function() {
2012
+ var EmptyView = this.getEmptyView();
2013
+
2014
+ if (EmptyView && !this._showingEmptyView) {
2015
+ this.triggerMethod('before:render:empty');
2016
+
2017
+ this._showingEmptyView = true;
2018
+ var model = new Backbone.Model();
2019
+ this.addEmptyView(model, EmptyView);
2020
+
2021
+ this.triggerMethod('render:empty');
2022
+ }
2023
+ },
2024
+
2025
+ // Internal method to destroy an existing emptyView instance
2026
+ // if one exists. Called when a collection view has been
2027
+ // rendered empty, and then a child is added to the collection.
2028
+ destroyEmptyView: function() {
2029
+ if (this._showingEmptyView) {
2030
+ this.triggerMethod('before:remove:empty');
2031
+
2032
+ this.destroyChildren();
2033
+ delete this._showingEmptyView;
2034
+
2035
+ this.triggerMethod('remove:empty');
2036
+ }
2037
+ },
2038
+
2039
+ // Retrieve the empty view class
2040
+ getEmptyView: function() {
2041
+ return this.getOption('emptyView');
2042
+ },
2043
+
2044
+ // Render and show the emptyView. Similar to addChild method
2045
+ // but "add:child" events are not fired, and the event from
2046
+ // emptyView are not forwarded
2047
+ addEmptyView: function(child, EmptyView) {
2048
+ // Only trigger attach if already shown, attached, and not buffering, otherwise endBuffer() or
2049
+ // Region#show() handles this.
2050
+ var canTriggerAttach = this._isShown && !this.isBuffering && Marionette.isNodeAttached(this.el);
2051
+ var nestedViews;
2052
+
2053
+ // get the emptyViewOptions, falling back to childViewOptions
2054
+ var emptyViewOptions = this.getOption('emptyViewOptions') ||
2055
+ this.getOption('childViewOptions');
2056
+
2057
+ if (_.isFunction(emptyViewOptions)) {
2058
+ emptyViewOptions = emptyViewOptions.call(this, child, this._emptyViewIndex);
2059
+ }
2060
+
2061
+ // build the empty view
2062
+ var view = this.buildChildView(child, EmptyView, emptyViewOptions);
2063
+
2064
+ view._parent = this;
2065
+
2066
+ // Proxy emptyView events
2067
+ this.proxyChildEvents(view);
2068
+
2069
+ view.once('render', function() {
2070
+ // trigger the 'before:show' event on `view` if the collection view has already been shown
2071
+ if (this._isShown) {
2072
+ Marionette.triggerMethodOn(view, 'before:show', view);
2073
+ }
2074
+
2075
+ // Trigger `before:attach` following `render` to avoid adding logic and event triggers
2076
+ // to public method `renderChildView()`.
2077
+ if (canTriggerAttach && this._triggerBeforeAttach) {
2078
+ nestedViews = this._getViewAndNested(view);
2079
+ this._triggerMethodMany(nestedViews, this, 'before:attach');
2080
+ }
2081
+ }, this);
2082
+
2083
+ // Store the `emptyView` like a `childView` so we can properly remove and/or close it later
2084
+ this.children.add(view);
2085
+ this.renderChildView(view, this._emptyViewIndex);
2086
+
2087
+ // Trigger `attach`
2088
+ if (canTriggerAttach && this._triggerAttach) {
2089
+ nestedViews = this._getViewAndNested(view);
2090
+ this._triggerMethodMany(nestedViews, this, 'attach');
2091
+ }
2092
+ // call the 'show' method if the collection view has already been shown
2093
+ if (this._isShown) {
2094
+ Marionette.triggerMethodOn(view, 'show', view);
2095
+ }
2096
+ },
2097
+
2098
+ // Retrieve the `childView` class, either from `this.options.childView`
2099
+ // or from the `childView` in the object definition. The "options"
2100
+ // takes precedence.
2101
+ // This method receives the model that will be passed to the instance
2102
+ // created from this `childView`. Overriding methods may use the child
2103
+ // to determine what `childView` class to return.
2104
+ getChildView: function(child) {
2105
+ var childView = this.getOption('childView');
2106
+
2107
+ if (!childView) {
2108
+ throw new Marionette.Error({
2109
+ name: 'NoChildViewError',
2110
+ message: 'A "childView" must be specified'
2111
+ });
2112
+ }
2113
+
2114
+ return childView;
2115
+ },
2116
+
2117
+ // Render the child's view and add it to the
2118
+ // HTML for the collection view at a given index.
2119
+ // This will also update the indices of later views in the collection
2120
+ // in order to keep the children in sync with the collection.
2121
+ addChild: function(child, ChildView, index) {
2122
+ var childViewOptions = this.getOption('childViewOptions');
2123
+ childViewOptions = Marionette._getValue(childViewOptions, this, [child, index]);
2124
+
2125
+ var view = this.buildChildView(child, ChildView, childViewOptions);
2126
+
2127
+ // increment indices of views after this one
2128
+ this._updateIndices(view, true, index);
2129
+
2130
+ this.triggerMethod('before:add:child', view);
2131
+ this._addChildView(view, index);
2132
+ this.triggerMethod('add:child', view);
2133
+
2134
+ view._parent = this;
2135
+
2136
+ return view;
2137
+ },
2138
+
2139
+ // Internal method. This decrements or increments the indices of views after the
2140
+ // added/removed view to keep in sync with the collection.
2141
+ _updateIndices: function(view, increment, index) {
2142
+ if (!this.getOption('sort')) {
2143
+ return;
2144
+ }
2145
+
2146
+ if (increment) {
2147
+ // assign the index to the view
2148
+ view._index = index;
2149
+ }
2150
+
2151
+ // update the indexes of views after this one
2152
+ this.children.each(function(laterView) {
2153
+ if (laterView._index >= view._index) {
2154
+ laterView._index += increment ? 1 : -1;
2155
+ }
2156
+ });
2157
+ },
2158
+
2159
+ // Internal Method. Add the view to children and render it at
2160
+ // the given index.
2161
+ _addChildView: function(view, index) {
2162
+ // Only trigger attach if already shown, attached, and not buffering, otherwise endBuffer() or
2163
+ // Region#show() handles this.
2164
+ var canTriggerAttach = this._isShown && !this.isBuffering && Marionette.isNodeAttached(this.el);
2165
+ var nestedViews;
2166
+
2167
+ // set up the child view event forwarding
2168
+ this.proxyChildEvents(view);
2169
+
2170
+ view.once('render', function() {
2171
+ // trigger the 'before:show' event on `view` if the collection view has already been shown
2172
+ if (this._isShown && !this.isBuffering) {
2173
+ Marionette.triggerMethodOn(view, 'before:show', view);
2174
+ }
2175
+
2176
+ // Trigger `before:attach` following `render` to avoid adding logic and event triggers
2177
+ // to public method `renderChildView()`.
2178
+ if (canTriggerAttach && this._triggerBeforeAttach) {
2179
+ nestedViews = this._getViewAndNested(view);
2180
+ this._triggerMethodMany(nestedViews, this, 'before:attach');
2181
+ }
2182
+ }, this);
2183
+
2184
+ // Store the child view itself so we can properly remove and/or destroy it later
2185
+ this.children.add(view);
2186
+ this.renderChildView(view, index);
2187
+
2188
+ // Trigger `attach`
2189
+ if (canTriggerAttach && this._triggerAttach) {
2190
+ nestedViews = this._getViewAndNested(view);
2191
+ this._triggerMethodMany(nestedViews, this, 'attach');
2192
+ }
2193
+ // Trigger `show`
2194
+ if (this._isShown && !this.isBuffering) {
2195
+ Marionette.triggerMethodOn(view, 'show', view);
2196
+ }
2197
+ },
2198
+
2199
+ // render the child view
2200
+ renderChildView: function(view, index) {
2201
+ if (!view.supportsRenderLifecycle) {
2202
+ Marionette.triggerMethodOn(view, 'before:render', view);
2203
+ }
2204
+ view.render();
2205
+ if (!view.supportsRenderLifecycle) {
2206
+ Marionette.triggerMethodOn(view, 'render', view);
2207
+ }
2208
+ this.attachHtml(this, view, index);
2209
+ return view;
2210
+ },
2211
+
2212
+ // Build a `childView` for a model in the collection.
2213
+ buildChildView: function(child, ChildViewClass, childViewOptions) {
2214
+ var options = _.extend({model: child}, childViewOptions);
2215
+ var childView = new ChildViewClass(options);
2216
+ Marionette.MonitorDOMRefresh(childView);
2217
+ return childView;
2218
+ },
2219
+
2220
+ // Remove the child view and destroy it.
2221
+ // This function also updates the indices of
2222
+ // later views in the collection in order to keep
2223
+ // the children in sync with the collection.
2224
+ removeChildView: function(view) {
2225
+ if (!view) { return view; }
2226
+
2227
+ this.triggerMethod('before:remove:child', view);
2228
+
2229
+ if (!view.supportsDestroyLifecycle) {
2230
+ Marionette.triggerMethodOn(view, 'before:destroy', view);
2231
+ }
2232
+ // call 'destroy' or 'remove', depending on which is found
2233
+ if (view.destroy) {
2234
+ view.destroy();
2235
+ } else {
2236
+ view.remove();
2237
+ }
2238
+ if (!view.supportsDestroyLifecycle) {
2239
+ Marionette.triggerMethodOn(view, 'destroy', view);
2240
+ }
2241
+
2242
+ delete view._parent;
2243
+ this.stopListening(view);
2244
+ this.children.remove(view);
2245
+ this.triggerMethod('remove:child', view);
2246
+
2247
+ // decrement the index of views after this one
2248
+ this._updateIndices(view, false);
2249
+
2250
+ return view;
2251
+ },
2252
+
2253
+ // check if the collection is empty
2254
+ isEmpty: function() {
2255
+ return !this.collection || this.collection.length === 0;
2256
+ },
2257
+
2258
+ // If empty, show the empty view
2259
+ checkEmpty: function() {
2260
+ if (this.isEmpty(this.collection)) {
2261
+ this.showEmptyView();
2262
+ }
2263
+ },
2264
+
2265
+ // You might need to override this if you've overridden attachHtml
2266
+ attachBuffer: function(collectionView, buffer) {
2267
+ collectionView.$el.append(buffer);
2268
+ },
2269
+
2270
+ // Create a fragment buffer from the currently buffered children
2271
+ _createBuffer: function() {
2272
+ var elBuffer = document.createDocumentFragment();
2273
+ _.each(this._bufferedChildren, function(b) {
2274
+ elBuffer.appendChild(b.el);
2275
+ });
2276
+ return elBuffer;
2277
+ },
2278
+
2279
+ // Append the HTML to the collection's `el`.
2280
+ // Override this method to do something other
2281
+ // than `.append`.
2282
+ attachHtml: function(collectionView, childView, index) {
2283
+ if (collectionView.isBuffering) {
2284
+ // buffering happens on reset events and initial renders
2285
+ // in order to reduce the number of inserts into the
2286
+ // document, which are expensive.
2287
+ collectionView._bufferedChildren.splice(index, 0, childView);
2288
+ } else {
2289
+ // If we've already rendered the main collection, append
2290
+ // the new child into the correct order if we need to. Otherwise
2291
+ // append to the end.
2292
+ if (!collectionView._insertBefore(childView, index)) {
2293
+ collectionView._insertAfter(childView);
2294
+ }
2295
+ }
2296
+ },
2297
+
2298
+ // Internal method. Check whether we need to insert the view into
2299
+ // the correct position.
2300
+ _insertBefore: function(childView, index) {
2301
+ var currentView;
2302
+ var findPosition = this.getOption('sort') && (index < this.children.length - 1);
2303
+ if (findPosition) {
2304
+ // Find the view after this one
2305
+ currentView = this.children.find(function(view) {
2306
+ return view._index === index + 1;
2307
+ });
2308
+ }
2309
+
2310
+ if (currentView) {
2311
+ currentView.$el.before(childView.el);
2312
+ return true;
2313
+ }
2314
+
2315
+ return false;
2316
+ },
2317
+
2318
+ // Internal method. Append a view to the end of the $el
2319
+ _insertAfter: function(childView) {
2320
+ this.$el.append(childView.el);
2321
+ },
2322
+
2323
+ // Internal method to set up the `children` object for
2324
+ // storing all of the child views
2325
+ _initChildViewStorage: function() {
2326
+ this.children = new Backbone.ChildViewContainer();
2327
+ },
2328
+
2329
+ // Handle cleanup and other destroying needs for the collection of views
2330
+ destroy: function() {
2331
+ if (this.isDestroyed) { return this; }
2332
+
2333
+ this.triggerMethod('before:destroy:collection');
2334
+ this.destroyChildren({checkEmpty: false});
2335
+ this.triggerMethod('destroy:collection');
2336
+
2337
+ return Marionette.View.prototype.destroy.apply(this, arguments);
2338
+ },
2339
+
2340
+ // Destroy the child views that this collection view
2341
+ // is holding on to, if any
2342
+ destroyChildren: function(options) {
2343
+ var destroyOptions = options || {};
2344
+ var shouldCheckEmpty = true;
2345
+ var childViews = this.children.map(_.identity);
2346
+
2347
+ if (!_.isUndefined(destroyOptions.checkEmpty)) {
2348
+ shouldCheckEmpty = destroyOptions.checkEmpty;
2349
+ }
2350
+
2351
+ this.children.each(this.removeChildView, this);
2352
+
2353
+ if (shouldCheckEmpty) {
2354
+ this.checkEmpty();
2355
+ }
2356
+ return childViews;
2357
+ },
2358
+
2359
+ // Return true if the given child should be shown
2360
+ // Return false otherwise
2361
+ // The filter will be passed (child, index, collection)
2362
+ // Where
2363
+ // 'child' is the given model
2364
+ // 'index' is the index of that model in the collection
2365
+ // 'collection' is the collection referenced by this CollectionView
2366
+ _shouldAddChild: function(child, index) {
2367
+ var filter = this.getOption('filter');
2368
+ return !_.isFunction(filter) || filter.call(this, child, index, this.collection);
2369
+ },
2370
+
2371
+ // Set up the child view event forwarding. Uses a "childview:"
2372
+ // prefix in front of all forwarded events.
2373
+ proxyChildEvents: function(view) {
2374
+ var prefix = this.getOption('childViewEventPrefix');
2375
+
2376
+ // Forward all child view events through the parent,
2377
+ // prepending "childview:" to the event name
2378
+ this.listenTo(view, 'all', function() {
2379
+ var args = _.toArray(arguments);
2380
+ var rootEvent = args[0];
2381
+ var childEvents = this.normalizeMethods(_.result(this, 'childEvents'));
2382
+
2383
+ args[0] = prefix + ':' + rootEvent;
2384
+ args.splice(1, 0, view);
2385
+
2386
+ // call collectionView childEvent if defined
2387
+ if (typeof childEvents !== 'undefined' && _.isFunction(childEvents[rootEvent])) {
2388
+ childEvents[rootEvent].apply(this, args.slice(1));
2389
+ }
2390
+
2391
+ this.triggerMethod.apply(this, args);
2392
+ });
2393
+ },
2394
+
2395
+ _getImmediateChildren: function() {
2396
+ return _.values(this.children._views);
2397
+ },
2398
+
2399
+ _getViewAndNested: function(view) {
2400
+ // This will not fail on Backbone.View which does not have #_getNestedViews.
2401
+ return [view].concat(_.result(view, '_getNestedViews') || []);
2402
+ },
2403
+
2404
+ getViewComparator: function() {
2405
+ return this.getOption('viewComparator');
2406
+ }
2407
+ });
2408
+
2409
+ /* jshint maxstatements: 17, maxlen: 117 */
2410
+
2411
+ // Composite View
2412
+ // --------------
2413
+
2414
+ // Used for rendering a branch-leaf, hierarchical structure.
2415
+ // Extends directly from CollectionView and also renders an
2416
+ // a child view as `modelView`, for the top leaf
2417
+ Marionette.CompositeView = Marionette.CollectionView.extend({
2418
+
2419
+ // Setting up the inheritance chain which allows changes to
2420
+ // Marionette.CollectionView.prototype.constructor which allows overriding
2421
+ // option to pass '{sort: false}' to prevent the CompositeView from
2422
+ // maintaining the sorted order of the collection.
2423
+ // This will fallback onto appending childView's to the end.
2424
+ constructor: function() {
2425
+ Marionette.CollectionView.apply(this, arguments);
2426
+ },
2427
+
2428
+ // Configured the initial events that the composite view
2429
+ // binds to. Override this method to prevent the initial
2430
+ // events, or to add your own initial events.
2431
+ _initialEvents: function() {
2432
+
2433
+ // Bind only after composite view is rendered to avoid adding child views
2434
+ // to nonexistent childViewContainer
2435
+
2436
+ if (this.collection) {
2437
+ this.listenTo(this.collection, 'add', this._onCollectionAdd);
2438
+ this.listenTo(this.collection, 'remove', this._onCollectionRemove);
2439
+ this.listenTo(this.collection, 'reset', this._renderChildren);
2440
+
2441
+ if (this.getOption('sort')) {
2442
+ this.listenTo(this.collection, 'sort', this._sortViews);
2443
+ }
2444
+ }
2445
+ },
2446
+
2447
+ // Retrieve the `childView` to be used when rendering each of
2448
+ // the items in the collection. The default is to return
2449
+ // `this.childView` or Marionette.CompositeView if no `childView`
2450
+ // has been defined
2451
+ getChildView: function(child) {
2452
+ var childView = this.getOption('childView') || this.constructor;
2453
+
2454
+ return childView;
2455
+ },
2456
+
2457
+ // Serialize the model for the view.
2458
+ // You can override the `serializeData` method in your own view
2459
+ // definition, to provide custom serialization for your view's data.
2460
+ serializeData: function() {
2461
+ var data = {};
2462
+
2463
+ if (this.model) {
2464
+ data = _.partial(this.serializeModel, this.model).apply(this, arguments);
2465
+ }
2466
+
2467
+ return data;
2468
+ },
2469
+
2470
+ // Renders the model and the collection.
2471
+ render: function() {
2472
+ this._ensureViewIsIntact();
2473
+ this._isRendering = true;
2474
+ this.resetChildViewContainer();
2475
+
2476
+ this.triggerMethod('before:render', this);
2477
+
2478
+ this._renderTemplate();
2479
+ this._renderChildren();
2480
+
2481
+ this._isRendering = false;
2482
+ this.isRendered = true;
2483
+ this.triggerMethod('render', this);
2484
+ return this;
2485
+ },
2486
+
2487
+ _renderChildren: function() {
2488
+ if (this.isRendered || this._isRendering) {
2489
+ Marionette.CollectionView.prototype._renderChildren.call(this);
2490
+ }
2491
+ },
2492
+
2493
+ // Render the root template that the children
2494
+ // views are appended to
2495
+ _renderTemplate: function() {
2496
+ var data = {};
2497
+ data = this.serializeData();
2498
+ data = this.mixinTemplateHelpers(data);
2499
+
2500
+ this.triggerMethod('before:render:template');
2501
+
2502
+ var template = this.getTemplate();
2503
+ var html = Marionette.Renderer.render(template, data, this);
2504
+ this.attachElContent(html);
2505
+
2506
+ // the ui bindings is done here and not at the end of render since they
2507
+ // will not be available until after the model is rendered, but should be
2508
+ // available before the collection is rendered.
2509
+ this.bindUIElements();
2510
+ this.triggerMethod('render:template');
2511
+ },
2512
+
2513
+ // Attaches the content of the root.
2514
+ // This method can be overridden to optimize rendering,
2515
+ // or to render in a non standard way.
2516
+ //
2517
+ // For example, using `innerHTML` instead of `$el.html`
2518
+ //
2519
+ // ```js
2520
+ // attachElContent: function(html) {
2521
+ // this.el.innerHTML = html;
2522
+ // return this;
2523
+ // }
2524
+ // ```
2525
+ attachElContent: function(html) {
2526
+ this.$el.html(html);
2527
+
2528
+ return this;
2529
+ },
2530
+
2531
+ // You might need to override this if you've overridden attachHtml
2532
+ attachBuffer: function(compositeView, buffer) {
2533
+ var $container = this.getChildViewContainer(compositeView);
2534
+ $container.append(buffer);
2535
+ },
2536
+
2537
+ // Internal method. Append a view to the end of the $el.
2538
+ // Overidden from CollectionView to ensure view is appended to
2539
+ // childViewContainer
2540
+ _insertAfter: function(childView) {
2541
+ var $container = this.getChildViewContainer(this, childView);
2542
+ $container.append(childView.el);
2543
+ },
2544
+
2545
+ // Internal method. Append reordered childView'.
2546
+ // Overidden from CollectionView to ensure reordered views
2547
+ // are appended to childViewContainer
2548
+ _appendReorderedChildren: function(children) {
2549
+ var $container = this.getChildViewContainer(this);
2550
+ $container.append(children);
2551
+ },
2552
+
2553
+ // Internal method to ensure an `$childViewContainer` exists, for the
2554
+ // `attachHtml` method to use.
2555
+ getChildViewContainer: function(containerView, childView) {
2556
+ if (!!containerView.$childViewContainer) {
2557
+ return containerView.$childViewContainer;
2558
+ }
2559
+
2560
+ var container;
2561
+ var childViewContainer = Marionette.getOption(containerView, 'childViewContainer');
2562
+ if (childViewContainer) {
2563
+
2564
+ var selector = Marionette._getValue(childViewContainer, containerView);
2565
+
2566
+ if (selector.charAt(0) === '@' && containerView.ui) {
2567
+ container = containerView.ui[selector.substr(4)];
2568
+ } else {
2569
+ container = containerView.$(selector);
2570
+ }
2571
+
2572
+ if (container.length <= 0) {
2573
+ throw new Marionette.Error({
2574
+ name: 'ChildViewContainerMissingError',
2575
+ message: 'The specified "childViewContainer" was not found: ' + containerView.childViewContainer
2576
+ });
2577
+ }
2578
+
2579
+ } else {
2580
+ container = containerView.$el;
2581
+ }
2582
+
2583
+ containerView.$childViewContainer = container;
2584
+ return container;
2585
+ },
2586
+
2587
+ // Internal method to reset the `$childViewContainer` on render
2588
+ resetChildViewContainer: function() {
2589
+ if (this.$childViewContainer) {
2590
+ this.$childViewContainer = undefined;
2591
+ }
2592
+ }
2593
+ });
2594
+
2595
+ // Layout View
2596
+ // -----------
2597
+
2598
+ // Used for managing application layoutViews, nested layoutViews and
2599
+ // multiple regions within an application or sub-application.
2600
+ //
2601
+ // A specialized view class that renders an area of HTML and then
2602
+ // attaches `Region` instances to the specified `regions`.
2603
+ // Used for composite view management and sub-application areas.
2604
+ Marionette.LayoutView = Marionette.ItemView.extend({
2605
+ regionClass: Marionette.Region,
2606
+
2607
+ options: {
2608
+ destroyImmediate: false
2609
+ },
2610
+
2611
+ // used as the prefix for child view events
2612
+ // that are forwarded through the layoutview
2613
+ childViewEventPrefix: 'childview',
2614
+
2615
+ // Ensure the regions are available when the `initialize` method
2616
+ // is called.
2617
+ constructor: function(options) {
2618
+ options = options || {};
2619
+
2620
+ this._firstRender = true;
2621
+ this._initializeRegions(options);
2622
+
2623
+ Marionette.ItemView.call(this, options);
2624
+ },
2625
+
2626
+ // LayoutView's render will use the existing region objects the
2627
+ // first time it is called. Subsequent calls will destroy the
2628
+ // views that the regions are showing and then reset the `el`
2629
+ // for the regions to the newly rendered DOM elements.
2630
+ render: function() {
2631
+ this._ensureViewIsIntact();
2632
+
2633
+ if (this._firstRender) {
2634
+ // if this is the first render, don't do anything to
2635
+ // reset the regions
2636
+ this._firstRender = false;
2637
+ } else {
2638
+ // If this is not the first render call, then we need to
2639
+ // re-initialize the `el` for each region
2640
+ this._reInitializeRegions();
2641
+ }
2642
+
2643
+ return Marionette.ItemView.prototype.render.apply(this, arguments);
2644
+ },
2645
+
2646
+ // Handle destroying regions, and then destroy the view itself.
2647
+ destroy: function() {
2648
+ if (this.isDestroyed) { return this; }
2649
+ // #2134: remove parent element before destroying the child views, so
2650
+ // removing the child views doesn't retrigger repaints
2651
+ if (this.getOption('destroyImmediate') === true) {
2652
+ this.$el.remove();
2653
+ }
2654
+ this.regionManager.destroy();
2655
+ return Marionette.ItemView.prototype.destroy.apply(this, arguments);
2656
+ },
2657
+
2658
+ showChildView: function(regionName, view, options) {
2659
+ var region = this.getRegion(regionName);
2660
+ return region.show.apply(region, _.rest(arguments));
2661
+ },
2662
+
2663
+ getChildView: function(regionName) {
2664
+ return this.getRegion(regionName).currentView;
2665
+ },
2666
+
2667
+ // Add a single region, by name, to the layoutView
2668
+ addRegion: function(name, definition) {
2669
+ var regions = {};
2670
+ regions[name] = definition;
2671
+ return this._buildRegions(regions)[name];
2672
+ },
2673
+
2674
+ // Add multiple regions as a {name: definition, name2: def2} object literal
2675
+ addRegions: function(regions) {
2676
+ this.regions = _.extend({}, this.regions, regions);
2677
+ return this._buildRegions(regions);
2678
+ },
2679
+
2680
+ // Remove a single region from the LayoutView, by name
2681
+ removeRegion: function(name) {
2682
+ delete this.regions[name];
2683
+ return this.regionManager.removeRegion(name);
2684
+ },
2685
+
2686
+ // Provides alternative access to regions
2687
+ // Accepts the region name
2688
+ // getRegion('main')
2689
+ getRegion: function(region) {
2690
+ return this.regionManager.get(region);
2691
+ },
2692
+
2693
+ // Get all regions
2694
+ getRegions: function() {
2695
+ return this.regionManager.getRegions();
2696
+ },
2697
+
2698
+ // internal method to build regions
2699
+ _buildRegions: function(regions) {
2700
+ var defaults = {
2701
+ regionClass: this.getOption('regionClass'),
2702
+ parentEl: _.partial(_.result, this, 'el')
2703
+ };
2704
+
2705
+ return this.regionManager.addRegions(regions, defaults);
2706
+ },
2707
+
2708
+ // Internal method to initialize the regions that have been defined in a
2709
+ // `regions` attribute on this layoutView.
2710
+ _initializeRegions: function(options) {
2711
+ var regions;
2712
+ this._initRegionManager();
2713
+
2714
+ regions = Marionette._getValue(this.regions, this, [options]) || {};
2715
+
2716
+ // Enable users to define `regions` as instance options.
2717
+ var regionOptions = this.getOption.call(options, 'regions');
2718
+
2719
+ // enable region options to be a function
2720
+ regionOptions = Marionette._getValue(regionOptions, this, [options]);
2721
+
2722
+ _.extend(regions, regionOptions);
2723
+
2724
+ // Normalize region selectors hash to allow
2725
+ // a user to use the @ui. syntax.
2726
+ regions = this.normalizeUIValues(regions, ['selector', 'el']);
2727
+
2728
+ this.addRegions(regions);
2729
+ },
2730
+
2731
+ // Internal method to re-initialize all of the regions by updating the `el` that
2732
+ // they point to
2733
+ _reInitializeRegions: function() {
2734
+ this.regionManager.invoke('reset');
2735
+ },
2736
+
2737
+ // Enable easy overriding of the default `RegionManager`
2738
+ // for customized region interactions and business specific
2739
+ // view logic for better control over single regions.
2740
+ getRegionManager: function() {
2741
+ return new Marionette.RegionManager();
2742
+ },
2743
+
2744
+ // Internal method to initialize the region manager
2745
+ // and all regions in it
2746
+ _initRegionManager: function() {
2747
+ this.regionManager = this.getRegionManager();
2748
+ this.regionManager._parent = this;
2749
+
2750
+ this.listenTo(this.regionManager, 'before:add:region', function(name) {
2751
+ this.triggerMethod('before:add:region', name);
2752
+ });
2753
+
2754
+ this.listenTo(this.regionManager, 'add:region', function(name, region) {
2755
+ this[name] = region;
2756
+ this.triggerMethod('add:region', name, region);
2757
+ });
2758
+
2759
+ this.listenTo(this.regionManager, 'before:remove:region', function(name) {
2760
+ this.triggerMethod('before:remove:region', name);
2761
+ });
2762
+
2763
+ this.listenTo(this.regionManager, 'remove:region', function(name, region) {
2764
+ delete this[name];
2765
+ this.triggerMethod('remove:region', name, region);
2766
+ });
2767
+ },
2768
+
2769
+ _getImmediateChildren: function() {
2770
+ return _.chain(this.regionManager.getRegions())
2771
+ .pluck('currentView')
2772
+ .compact()
2773
+ .value();
2774
+ }
2775
+ });
2776
+
2777
+
2778
+ // Behavior
2779
+ // --------
2780
+
2781
+ // A Behavior is an isolated set of DOM /
2782
+ // user interactions that can be mixed into any View.
2783
+ // Behaviors allow you to blackbox View specific interactions
2784
+ // into portable logical chunks, keeping your views simple and your code DRY.
2785
+
2786
+ Marionette.Behavior = Marionette.Object.extend({
2787
+ constructor: function(options, view) {
2788
+ // Setup reference to the view.
2789
+ // this comes in handle when a behavior
2790
+ // wants to directly talk up the chain
2791
+ // to the view.
2792
+ this.view = view;
2793
+ this.defaults = _.result(this, 'defaults') || {};
2794
+ this.options = _.extend({}, this.defaults, options);
2795
+ // Construct an internal UI hash using
2796
+ // the views UI hash and then the behaviors UI hash.
2797
+ // This allows the user to use UI hash elements
2798
+ // defined in the parent view as well as those
2799
+ // defined in the given behavior.
2800
+ this.ui = _.extend({}, _.result(view, 'ui'), _.result(this, 'ui'));
2801
+
2802
+ Marionette.Object.apply(this, arguments);
2803
+ },
2804
+
2805
+ // proxy behavior $ method to the view
2806
+ // this is useful for doing jquery DOM lookups
2807
+ // scoped to behaviors view.
2808
+ $: function() {
2809
+ return this.view.$.apply(this.view, arguments);
2810
+ },
2811
+
2812
+ // Stops the behavior from listening to events.
2813
+ // Overrides Object#destroy to prevent additional events from being triggered.
2814
+ destroy: function() {
2815
+ this.stopListening();
2816
+
2817
+ return this;
2818
+ },
2819
+
2820
+ proxyViewProperties: function(view) {
2821
+ this.$el = view.$el;
2822
+ this.el = view.el;
2823
+ }
2824
+ });
2825
+
2826
+ /* jshint maxlen: 143 */
2827
+ // Behaviors
2828
+ // ---------
2829
+
2830
+ // Behaviors is a utility class that takes care of
2831
+ // gluing your behavior instances to their given View.
2832
+ // The most important part of this class is that you
2833
+ // **MUST** override the class level behaviorsLookup
2834
+ // method for things to work properly.
2835
+
2836
+ Marionette.Behaviors = (function(Marionette, _) {
2837
+ // Borrow event splitter from Backbone
2838
+ var delegateEventSplitter = /^(\S+)\s*(.*)$/;
2839
+
2840
+ function Behaviors(view, behaviors) {
2841
+
2842
+ if (!_.isObject(view.behaviors)) {
2843
+ return {};
2844
+ }
2845
+
2846
+ // Behaviors defined on a view can be a flat object literal
2847
+ // or it can be a function that returns an object.
2848
+ behaviors = Behaviors.parseBehaviors(view, behaviors || _.result(view, 'behaviors'));
2849
+
2850
+ // Wraps several of the view's methods
2851
+ // calling the methods first on each behavior
2852
+ // and then eventually calling the method on the view.
2853
+ Behaviors.wrap(view, behaviors, _.keys(methods));
2854
+ return behaviors;
2855
+ }
2856
+
2857
+ var methods = {
2858
+ behaviorTriggers: function(behaviorTriggers, behaviors) {
2859
+ var triggerBuilder = new BehaviorTriggersBuilder(this, behaviors);
2860
+ return triggerBuilder.buildBehaviorTriggers();
2861
+ },
2862
+
2863
+ behaviorEvents: function(behaviorEvents, behaviors) {
2864
+ var _behaviorsEvents = {};
2865
+
2866
+ _.each(behaviors, function(b, i) {
2867
+ var _events = {};
2868
+ var behaviorEvents = _.clone(_.result(b, 'events')) || {};
2869
+
2870
+ // Normalize behavior events hash to allow
2871
+ // a user to use the @ui. syntax.
2872
+ behaviorEvents = Marionette.normalizeUIKeys(behaviorEvents, getBehaviorsUI(b));
2873
+
2874
+ var j = 0;
2875
+ _.each(behaviorEvents, function(behaviour, key) {
2876
+ var match = key.match(delegateEventSplitter);
2877
+
2878
+ // Set event name to be namespaced using the view cid,
2879
+ // the behavior index, and the behavior event index
2880
+ // to generate a non colliding event namespace
2881
+ // http://api.jquery.com/event.namespace/
2882
+ var eventName = match[1] + '.' + [this.cid, i, j++, ' '].join('');
2883
+ var selector = match[2];
2884
+
2885
+ var eventKey = eventName + selector;
2886
+ var handler = _.isFunction(behaviour) ? behaviour : b[behaviour];
2887
+ if (!handler) { return; }
2888
+ _events[eventKey] = _.bind(handler, b);
2889
+ }, this);
2890
+
2891
+ _behaviorsEvents = _.extend(_behaviorsEvents, _events);
2892
+ }, this);
2893
+
2894
+ return _behaviorsEvents;
2895
+ }
2896
+ };
2897
+
2898
+ _.extend(Behaviors, {
2899
+
2900
+ // Placeholder method to be extended by the user.
2901
+ // The method should define the object that stores the behaviors.
2902
+ // i.e.
2903
+ //
2904
+ // ```js
2905
+ // Marionette.Behaviors.behaviorsLookup: function() {
2906
+ // return App.Behaviors
2907
+ // }
2908
+ // ```
2909
+ behaviorsLookup: function() {
2910
+ throw new Marionette.Error({
2911
+ message: 'You must define where your behaviors are stored.',
2912
+ url: 'marionette.behaviors.html#behaviorslookup'
2913
+ });
2914
+ },
2915
+
2916
+ // Takes care of getting the behavior class
2917
+ // given options and a key.
2918
+ // If a user passes in options.behaviorClass
2919
+ // default to using that. Otherwise delegate
2920
+ // the lookup to the users `behaviorsLookup` implementation.
2921
+ getBehaviorClass: function(options, key) {
2922
+ if (options.behaviorClass) {
2923
+ return options.behaviorClass;
2924
+ }
2925
+
2926
+ // Get behavior class can be either a flat object or a method
2927
+ return Marionette._getValue(Behaviors.behaviorsLookup, this, [options, key])[key];
2928
+ },
2929
+
2930
+ // Iterate over the behaviors object, for each behavior
2931
+ // instantiate it and get its grouped behaviors.
2932
+ parseBehaviors: function(view, behaviors) {
2933
+ return _.chain(behaviors).map(function(options, key) {
2934
+ var BehaviorClass = Behaviors.getBehaviorClass(options, key);
2935
+
2936
+ var behavior = new BehaviorClass(options, view);
2937
+ var nestedBehaviors = Behaviors.parseBehaviors(view, _.result(behavior, 'behaviors'));
2938
+
2939
+ return [behavior].concat(nestedBehaviors);
2940
+ }).flatten().value();
2941
+ },
2942
+
2943
+ // Wrap view internal methods so that they delegate to behaviors. For example,
2944
+ // `onDestroy` should trigger destroy on all of the behaviors and then destroy itself.
2945
+ // i.e.
2946
+ //
2947
+ // `view.delegateEvents = _.partial(methods.delegateEvents, view.delegateEvents, behaviors);`
2948
+ wrap: function(view, behaviors, methodNames) {
2949
+ _.each(methodNames, function(methodName) {
2950
+ view[methodName] = _.partial(methods[methodName], view[methodName], behaviors);
2951
+ });
2952
+ }
2953
+ });
2954
+
2955
+ // Class to build handlers for `triggers` on behaviors
2956
+ // for views
2957
+ function BehaviorTriggersBuilder(view, behaviors) {
2958
+ this._view = view;
2959
+ this._behaviors = behaviors;
2960
+ this._triggers = {};
2961
+ }
2962
+
2963
+ _.extend(BehaviorTriggersBuilder.prototype, {
2964
+ // Main method to build the triggers hash with event keys and handlers
2965
+ buildBehaviorTriggers: function() {
2966
+ _.each(this._behaviors, this._buildTriggerHandlersForBehavior, this);
2967
+ return this._triggers;
2968
+ },
2969
+
2970
+ // Internal method to build all trigger handlers for a given behavior
2971
+ _buildTriggerHandlersForBehavior: function(behavior, i) {
2972
+ var triggersHash = _.clone(_.result(behavior, 'triggers')) || {};
2973
+
2974
+ triggersHash = Marionette.normalizeUIKeys(triggersHash, getBehaviorsUI(behavior));
2975
+
2976
+ _.each(triggersHash, _.bind(this._setHandlerForBehavior, this, behavior, i));
2977
+ },
2978
+
2979
+ // Internal method to create and assign the trigger handler for a given
2980
+ // behavior
2981
+ _setHandlerForBehavior: function(behavior, i, eventName, trigger) {
2982
+ // Unique identifier for the `this._triggers` hash
2983
+ var triggerKey = trigger.replace(/^\S+/, function(triggerName) {
2984
+ return triggerName + '.' + 'behaviortriggers' + i;
2985
+ });
2986
+
2987
+ this._triggers[triggerKey] = this._view._buildViewTrigger(eventName);
2988
+ }
2989
+ });
2990
+
2991
+ function getBehaviorsUI(behavior) {
2992
+ return behavior._uiBindings || behavior.ui;
2993
+ }
2994
+
2995
+ return Behaviors;
2996
+
2997
+ })(Marionette, _);
2998
+
2999
+
3000
+ // App Router
3001
+ // ----------
3002
+
3003
+ // Reduce the boilerplate code of handling route events
3004
+ // and then calling a single method on another object.
3005
+ // Have your routers configured to call the method on
3006
+ // your object, directly.
3007
+ //
3008
+ // Configure an AppRouter with `appRoutes`.
3009
+ //
3010
+ // App routers can only take one `controller` object.
3011
+ // It is recommended that you divide your controller
3012
+ // objects in to smaller pieces of related functionality
3013
+ // and have multiple routers / controllers, instead of
3014
+ // just one giant router and controller.
3015
+ //
3016
+ // You can also add standard routes to an AppRouter.
3017
+
3018
+ Marionette.AppRouter = Backbone.Router.extend({
3019
+
3020
+ constructor: function(options) {
3021
+ this.options = options || {};
3022
+
3023
+ Backbone.Router.apply(this, arguments);
3024
+
3025
+ var appRoutes = this.getOption('appRoutes');
3026
+ var controller = this._getController();
3027
+ this.processAppRoutes(controller, appRoutes);
3028
+ this.on('route', this._processOnRoute, this);
3029
+ },
3030
+
3031
+ // Similar to route method on a Backbone Router but
3032
+ // method is called on the controller
3033
+ appRoute: function(route, methodName) {
3034
+ var controller = this._getController();
3035
+ this._addAppRoute(controller, route, methodName);
3036
+ },
3037
+
3038
+ // process the route event and trigger the onRoute
3039
+ // method call, if it exists
3040
+ _processOnRoute: function(routeName, routeArgs) {
3041
+ // make sure an onRoute before trying to call it
3042
+ if (_.isFunction(this.onRoute)) {
3043
+ // find the path that matches the current route
3044
+ var routePath = _.invert(this.getOption('appRoutes'))[routeName];
3045
+ this.onRoute(routeName, routePath, routeArgs);
3046
+ }
3047
+ },
3048
+
3049
+ // Internal method to process the `appRoutes` for the
3050
+ // router, and turn them in to routes that trigger the
3051
+ // specified method on the specified `controller`.
3052
+ processAppRoutes: function(controller, appRoutes) {
3053
+ if (!appRoutes) { return; }
3054
+
3055
+ var routeNames = _.keys(appRoutes).reverse(); // Backbone requires reverted order of routes
3056
+
3057
+ _.each(routeNames, function(route) {
3058
+ this._addAppRoute(controller, route, appRoutes[route]);
3059
+ }, this);
3060
+ },
3061
+
3062
+ _getController: function() {
3063
+ return this.getOption('controller');
3064
+ },
3065
+
3066
+ _addAppRoute: function(controller, route, methodName) {
3067
+ var method = controller[methodName];
3068
+
3069
+ if (!method) {
3070
+ throw new Marionette.Error('Method "' + methodName + '" was not found on the controller');
3071
+ }
3072
+
3073
+ this.route(route, methodName, _.bind(method, controller));
3074
+ },
3075
+
3076
+ mergeOptions: Marionette.mergeOptions,
3077
+
3078
+ // Proxy `getOption` to enable getting options from this or this.options by name.
3079
+ getOption: Marionette.proxyGetOption,
3080
+
3081
+ triggerMethod: Marionette.triggerMethod,
3082
+
3083
+ bindEntityEvents: Marionette.proxyBindEntityEvents,
3084
+
3085
+ unbindEntityEvents: Marionette.proxyUnbindEntityEvents
3086
+ });
3087
+
3088
+ // Application
3089
+ // -----------
3090
+
3091
+ // Contain and manage the composite application as a whole.
3092
+ // Stores and starts up `Region` objects, includes an
3093
+ // event aggregator as `app.vent`
3094
+ Marionette.Application = Marionette.Object.extend({
3095
+ constructor: function(options) {
3096
+ this._initializeRegions(options);
3097
+ this._initCallbacks = new Marionette.Callbacks();
3098
+ this.submodules = {};
3099
+ _.extend(this, options);
3100
+ this._initChannel();
3101
+ Marionette.Object.apply(this, arguments);
3102
+ },
3103
+
3104
+ // Command execution, facilitated by Backbone.Wreqr.Commands
3105
+ execute: function() {
3106
+ this.commands.execute.apply(this.commands, arguments);
3107
+ },
3108
+
3109
+ // Request/response, facilitated by Backbone.Wreqr.RequestResponse
3110
+ request: function() {
3111
+ return this.reqres.request.apply(this.reqres, arguments);
3112
+ },
3113
+
3114
+ // Add an initializer that is either run at when the `start`
3115
+ // method is called, or run immediately if added after `start`
3116
+ // has already been called.
3117
+ addInitializer: function(initializer) {
3118
+ this._initCallbacks.add(initializer);
3119
+ },
3120
+
3121
+ // kick off all of the application's processes.
3122
+ // initializes all of the regions that have been added
3123
+ // to the app, and runs all of the initializer functions
3124
+ start: function(options) {
3125
+ this.triggerMethod('before:start', options);
3126
+ this._initCallbacks.run(options, this);
3127
+ this.triggerMethod('start', options);
3128
+ },
3129
+
3130
+ // Add regions to your app.
3131
+ // Accepts a hash of named strings or Region objects
3132
+ // addRegions({something: "#someRegion"})
3133
+ // addRegions({something: Region.extend({el: "#someRegion"}) });
3134
+ addRegions: function(regions) {
3135
+ return this._regionManager.addRegions(regions);
3136
+ },
3137
+
3138
+ // Empty all regions in the app, without removing them
3139
+ emptyRegions: function() {
3140
+ return this._regionManager.emptyRegions();
3141
+ },
3142
+
3143
+ // Removes a region from your app, by name
3144
+ // Accepts the regions name
3145
+ // removeRegion('myRegion')
3146
+ removeRegion: function(region) {
3147
+ return this._regionManager.removeRegion(region);
3148
+ },
3149
+
3150
+ // Provides alternative access to regions
3151
+ // Accepts the region name
3152
+ // getRegion('main')
3153
+ getRegion: function(region) {
3154
+ return this._regionManager.get(region);
3155
+ },
3156
+
3157
+ // Get all the regions from the region manager
3158
+ getRegions: function() {
3159
+ return this._regionManager.getRegions();
3160
+ },
3161
+
3162
+ // Create a module, attached to the application
3163
+ module: function(moduleNames, moduleDefinition) {
3164
+
3165
+ // Overwrite the module class if the user specifies one
3166
+ var ModuleClass = Marionette.Module.getClass(moduleDefinition);
3167
+
3168
+ var args = _.toArray(arguments);
3169
+ args.unshift(this);
3170
+
3171
+ // see the Marionette.Module object for more information
3172
+ return ModuleClass.create.apply(ModuleClass, args);
3173
+ },
3174
+
3175
+ // Enable easy overriding of the default `RegionManager`
3176
+ // for customized region interactions and business-specific
3177
+ // view logic for better control over single regions.
3178
+ getRegionManager: function() {
3179
+ return new Marionette.RegionManager();
3180
+ },
3181
+
3182
+ // Internal method to initialize the regions that have been defined in a
3183
+ // `regions` attribute on the application instance
3184
+ _initializeRegions: function(options) {
3185
+ var regions = _.isFunction(this.regions) ? this.regions(options) : this.regions || {};
3186
+
3187
+ this._initRegionManager();
3188
+
3189
+ // Enable users to define `regions` in instance options.
3190
+ var optionRegions = Marionette.getOption(options, 'regions');
3191
+
3192
+ // Enable region options to be a function
3193
+ if (_.isFunction(optionRegions)) {
3194
+ optionRegions = optionRegions.call(this, options);
3195
+ }
3196
+
3197
+ // Overwrite current regions with those passed in options
3198
+ _.extend(regions, optionRegions);
3199
+
3200
+ this.addRegions(regions);
3201
+
3202
+ return this;
3203
+ },
3204
+
3205
+ // Internal method to set up the region manager
3206
+ _initRegionManager: function() {
3207
+ this._regionManager = this.getRegionManager();
3208
+ this._regionManager._parent = this;
3209
+
3210
+ this.listenTo(this._regionManager, 'before:add:region', function() {
3211
+ Marionette._triggerMethod(this, 'before:add:region', arguments);
3212
+ });
3213
+
3214
+ this.listenTo(this._regionManager, 'add:region', function(name, region) {
3215
+ this[name] = region;
3216
+ Marionette._triggerMethod(this, 'add:region', arguments);
3217
+ });
3218
+
3219
+ this.listenTo(this._regionManager, 'before:remove:region', function() {
3220
+ Marionette._triggerMethod(this, 'before:remove:region', arguments);
3221
+ });
3222
+
3223
+ this.listenTo(this._regionManager, 'remove:region', function(name) {
3224
+ delete this[name];
3225
+ Marionette._triggerMethod(this, 'remove:region', arguments);
3226
+ });
3227
+ },
3228
+
3229
+ // Internal method to setup the Wreqr.radio channel
3230
+ _initChannel: function() {
3231
+ this.channelName = _.result(this, 'channelName') || 'global';
3232
+ this.channel = _.result(this, 'channel') || Backbone.Wreqr.radio.channel(this.channelName);
3233
+ this.vent = _.result(this, 'vent') || this.channel.vent;
3234
+ this.commands = _.result(this, 'commands') || this.channel.commands;
3235
+ this.reqres = _.result(this, 'reqres') || this.channel.reqres;
3236
+ }
3237
+ });
3238
+
3239
+ /* jshint maxparams: 9 */
3240
+
3241
+ // Module
3242
+ // ------
3243
+
3244
+ // A simple module system, used to create privacy and encapsulation in
3245
+ // Marionette applications
3246
+ Marionette.Module = function(moduleName, app, options) {
3247
+ this.moduleName = moduleName;
3248
+ this.options = _.extend({}, this.options, options);
3249
+ // Allow for a user to overide the initialize
3250
+ // for a given module instance.
3251
+ this.initialize = options.initialize || this.initialize;
3252
+
3253
+ // Set up an internal store for sub-modules.
3254
+ this.submodules = {};
3255
+
3256
+ this._setupInitializersAndFinalizers();
3257
+
3258
+ // Set an internal reference to the app
3259
+ // within a module.
3260
+ this.app = app;
3261
+
3262
+ if (_.isFunction(this.initialize)) {
3263
+ this.initialize(moduleName, app, this.options);
3264
+ }
3265
+ };
3266
+
3267
+ Marionette.Module.extend = Marionette.extend;
3268
+
3269
+ // Extend the Module prototype with events / listenTo, so that the module
3270
+ // can be used as an event aggregator or pub/sub.
3271
+ _.extend(Marionette.Module.prototype, Backbone.Events, {
3272
+
3273
+ // By default modules start with their parents.
3274
+ startWithParent: true,
3275
+
3276
+ // Initialize is an empty function by default. Override it with your own
3277
+ // initialization logic when extending Marionette.Module.
3278
+ initialize: function() {},
3279
+
3280
+ // Initializer for a specific module. Initializers are run when the
3281
+ // module's `start` method is called.
3282
+ addInitializer: function(callback) {
3283
+ this._initializerCallbacks.add(callback);
3284
+ },
3285
+
3286
+ // Finalizers are run when a module is stopped. They are used to teardown
3287
+ // and finalize any variables, references, events and other code that the
3288
+ // module had set up.
3289
+ addFinalizer: function(callback) {
3290
+ this._finalizerCallbacks.add(callback);
3291
+ },
3292
+
3293
+ // Start the module, and run all of its initializers
3294
+ start: function(options) {
3295
+ // Prevent re-starting a module that is already started
3296
+ if (this._isInitialized) { return; }
3297
+
3298
+ // start the sub-modules (depth-first hierarchy)
3299
+ _.each(this.submodules, function(mod) {
3300
+ // check to see if we should start the sub-module with this parent
3301
+ if (mod.startWithParent) {
3302
+ mod.start(options);
3303
+ }
3304
+ });
3305
+
3306
+ // run the callbacks to "start" the current module
3307
+ this.triggerMethod('before:start', options);
3308
+
3309
+ this._initializerCallbacks.run(options, this);
3310
+ this._isInitialized = true;
3311
+
3312
+ this.triggerMethod('start', options);
3313
+ },
3314
+
3315
+ // Stop this module by running its finalizers and then stop all of
3316
+ // the sub-modules for this module
3317
+ stop: function() {
3318
+ // if we are not initialized, don't bother finalizing
3319
+ if (!this._isInitialized) { return; }
3320
+ this._isInitialized = false;
3321
+
3322
+ this.triggerMethod('before:stop');
3323
+
3324
+ // stop the sub-modules; depth-first, to make sure the
3325
+ // sub-modules are stopped / finalized before parents
3326
+ _.invoke(this.submodules, 'stop');
3327
+
3328
+ // run the finalizers
3329
+ this._finalizerCallbacks.run(undefined, this);
3330
+
3331
+ // reset the initializers and finalizers
3332
+ this._initializerCallbacks.reset();
3333
+ this._finalizerCallbacks.reset();
3334
+
3335
+ this.triggerMethod('stop');
3336
+ },
3337
+
3338
+ // Configure the module with a definition function and any custom args
3339
+ // that are to be passed in to the definition function
3340
+ addDefinition: function(moduleDefinition, customArgs) {
3341
+ this._runModuleDefinition(moduleDefinition, customArgs);
3342
+ },
3343
+
3344
+ // Internal method: run the module definition function with the correct
3345
+ // arguments
3346
+ _runModuleDefinition: function(definition, customArgs) {
3347
+ // If there is no definition short circut the method.
3348
+ if (!definition) { return; }
3349
+
3350
+ // build the correct list of arguments for the module definition
3351
+ var args = _.flatten([
3352
+ this,
3353
+ this.app,
3354
+ Backbone,
3355
+ Marionette,
3356
+ Backbone.$, _,
3357
+ customArgs
3358
+ ]);
3359
+
3360
+ definition.apply(this, args);
3361
+ },
3362
+
3363
+ // Internal method: set up new copies of initializers and finalizers.
3364
+ // Calling this method will wipe out all existing initializers and
3365
+ // finalizers.
3366
+ _setupInitializersAndFinalizers: function() {
3367
+ this._initializerCallbacks = new Marionette.Callbacks();
3368
+ this._finalizerCallbacks = new Marionette.Callbacks();
3369
+ },
3370
+
3371
+ // import the `triggerMethod` to trigger events with corresponding
3372
+ // methods if the method exists
3373
+ triggerMethod: Marionette.triggerMethod
3374
+ });
3375
+
3376
+ // Class methods to create modules
3377
+ _.extend(Marionette.Module, {
3378
+
3379
+ // Create a module, hanging off the app parameter as the parent object.
3380
+ create: function(app, moduleNames, moduleDefinition) {
3381
+ var module = app;
3382
+
3383
+ // get the custom args passed in after the module definition and
3384
+ // get rid of the module name and definition function
3385
+ var customArgs = _.drop(arguments, 3);
3386
+
3387
+ // Split the module names and get the number of submodules.
3388
+ // i.e. an example module name of `Doge.Wow.Amaze` would
3389
+ // then have the potential for 3 module definitions.
3390
+ moduleNames = moduleNames.split('.');
3391
+ var length = moduleNames.length;
3392
+
3393
+ // store the module definition for the last module in the chain
3394
+ var moduleDefinitions = [];
3395
+ moduleDefinitions[length - 1] = moduleDefinition;
3396
+
3397
+ // Loop through all the parts of the module definition
3398
+ _.each(moduleNames, function(moduleName, i) {
3399
+ var parentModule = module;
3400
+ module = this._getModule(parentModule, moduleName, app, moduleDefinition);
3401
+ this._addModuleDefinition(parentModule, module, moduleDefinitions[i], customArgs);
3402
+ }, this);
3403
+
3404
+ // Return the last module in the definition chain
3405
+ return module;
3406
+ },
3407
+
3408
+ _getModule: function(parentModule, moduleName, app, def, args) {
3409
+ var options = _.extend({}, def);
3410
+ var ModuleClass = this.getClass(def);
3411
+
3412
+ // Get an existing module of this name if we have one
3413
+ var module = parentModule[moduleName];
3414
+
3415
+ if (!module) {
3416
+ // Create a new module if we don't have one
3417
+ module = new ModuleClass(moduleName, app, options);
3418
+ parentModule[moduleName] = module;
3419
+ // store the module on the parent
3420
+ parentModule.submodules[moduleName] = module;
3421
+ }
3422
+
3423
+ return module;
3424
+ },
3425
+
3426
+ // ## Module Classes
3427
+ //
3428
+ // Module classes can be used as an alternative to the define pattern.
3429
+ // The extend function of a Module is identical to the extend functions
3430
+ // on other Backbone and Marionette classes.
3431
+ // This allows module lifecyle events like `onStart` and `onStop` to be called directly.
3432
+ getClass: function(moduleDefinition) {
3433
+ var ModuleClass = Marionette.Module;
3434
+
3435
+ if (!moduleDefinition) {
3436
+ return ModuleClass;
3437
+ }
3438
+
3439
+ // If all of the module's functionality is defined inside its class,
3440
+ // then the class can be passed in directly. `MyApp.module("Foo", FooModule)`.
3441
+ if (moduleDefinition.prototype instanceof ModuleClass) {
3442
+ return moduleDefinition;
3443
+ }
3444
+
3445
+ return moduleDefinition.moduleClass || ModuleClass;
3446
+ },
3447
+
3448
+ // Add the module definition and add a startWithParent initializer function.
3449
+ // This is complicated because module definitions are heavily overloaded
3450
+ // and support an anonymous function, module class, or options object
3451
+ _addModuleDefinition: function(parentModule, module, def, args) {
3452
+ var fn = this._getDefine(def);
3453
+ var startWithParent = this._getStartWithParent(def, module);
3454
+
3455
+ if (fn) {
3456
+ module.addDefinition(fn, args);
3457
+ }
3458
+
3459
+ this._addStartWithParent(parentModule, module, startWithParent);
3460
+ },
3461
+
3462
+ _getStartWithParent: function(def, module) {
3463
+ var swp;
3464
+
3465
+ if (_.isFunction(def) && (def.prototype instanceof Marionette.Module)) {
3466
+ swp = module.constructor.prototype.startWithParent;
3467
+ return _.isUndefined(swp) ? true : swp;
3468
+ }
3469
+
3470
+ if (_.isObject(def)) {
3471
+ swp = def.startWithParent;
3472
+ return _.isUndefined(swp) ? true : swp;
3473
+ }
3474
+
3475
+ return true;
3476
+ },
3477
+
3478
+ _getDefine: function(def) {
3479
+ if (_.isFunction(def) && !(def.prototype instanceof Marionette.Module)) {
3480
+ return def;
3481
+ }
3482
+
3483
+ if (_.isObject(def)) {
3484
+ return def.define;
3485
+ }
3486
+
3487
+ return null;
3488
+ },
3489
+
3490
+ _addStartWithParent: function(parentModule, module, startWithParent) {
3491
+ module.startWithParent = module.startWithParent && startWithParent;
3492
+
3493
+ if (!module.startWithParent || !!module.startWithParentIsConfigured) {
3494
+ return;
3495
+ }
3496
+
3497
+ module.startWithParentIsConfigured = true;
3498
+
3499
+ parentModule.addInitializer(function(options) {
3500
+ if (module.startWithParent) {
3501
+ module.start(options);
3502
+ }
3503
+ });
3504
+ }
3505
+ });
3506
+
3507
+
3508
+ return Marionette;
3509
+ }));