PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.12.5
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.12.5
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
wpfunnels / admin / assets / lib / backbone / backbone.marionette.js

backbone.marionette.js in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.12.5, at admin/assets/lib/backbone/backbone.marionette.js

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