PluginProbe
Elementor Website Builder – more than just a page builder / 3.27.4
Elementor Website Builder – more than just a page builder v3.27.4
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.27.4, at assets/lib/backbone/backbone.marionette.js

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