PluginProbe
Elementor Website Builder – more than just a page builder / 3.0.12
Elementor Website Builder – more than just a page builder v3.0.12
4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 All 452 releases
elementor / assets / lib / backbone / backbone.marionette.js

backbone.marionette.js in Elementor Website Builder – more than just a page builder 3.0.12, at 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