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

3,969 lines 130.1 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 = '#' === templateId.charAt(0)
1628 ? Backbone.$('script' + templateId)
1629 : Backbone.$(templateId);
1630
1631 if (!$template.length) {
1632 throw new Marionette.Error({
1633 name: 'NoTemplateError',
1634 message: 'Could not find template: "' + templateId + '"'
1635 });
1636 }
1637 return $template.html();
1638 },
1639
1640 // Pre-compile the template before caching it. Override
1641 // this method if you do not need to pre-compile a template
1642 // (JST / RequireJS for example) or if you want to change
1643 // the template engine used (Handebars, etc).
1644 compileTemplate: function(rawTemplate, options) {
1645 return _.template(rawTemplate, options);
1646 }
1647 });
1648
1649 // Renderer
1650 // --------
1651
1652 // Render a template with data by passing in the template
1653 // selector and the data to render.
1654 Marionette.Renderer = {
1655
1656 // Render a template with data. The `template` parameter is
1657 // passed to the `TemplateCache` object to retrieve the
1658 // template function. Override this method to provide your own
1659 // custom rendering and template handling for all of Marionette.
1660 render: function(template, data) {
1661 if (!template) {
1662 throw new Marionette.Error({
1663 name: 'TemplateNotFoundError',
1664 message: 'Cannot render the template since its false, null or undefined.'
1665 });
1666 }
1667
1668 var templateFunc = _.isFunction(template) ? template : Marionette.TemplateCache.get(template);
1669
1670 return templateFunc(data);
1671 }
1672 };
1673
1674
1675 /* jshint maxlen: 114, nonew: false */
1676 // View
1677 // ----
1678
1679 // The core view class that other Marionette views extend from.
1680 Marionette.View = Backbone.View.extend({
1681 isDestroyed: false,
1682 supportsRenderLifecycle: true,
1683 supportsDestroyLifecycle: true,
1684
1685 constructor: function(options) {
1686 this.render = _.bind(this.render, this);
1687
1688 options = Marionette._getValue(options, this);
1689
1690 // this exposes view options to the view initializer
1691 // this is a backfill since backbone removed the assignment
1692 // of this.options
1693 // at some point however this may be removed
1694 this.options = _.extend({}, _.result(this, 'options'), options);
1695
1696 this._behaviors = Marionette.Behaviors(this);
1697
1698 Backbone.View.call(this, this.options);
1699
1700 Marionette.MonitorDOMRefresh(this);
1701 },
1702
1703 // Get the template for this view
1704 // instance. You can set a `template` attribute in the view
1705 // definition or pass a `template: "whatever"` parameter in
1706 // to the constructor options.
1707 getTemplate: function() {
1708 return this.getOption('template');
1709 },
1710
1711 // Serialize a model by returning its attributes. Clones
1712 // the attributes to allow modification.
1713 serializeModel: function(model) {
1714 return model.toJSON.apply(model, _.rest(arguments));
1715 },
1716
1717 // Mix in template helper methods. Looks for a
1718 // `templateHelpers` attribute, which can either be an
1719 // object literal, or a function that returns an object
1720 // literal. All methods and attributes from this object
1721 // are copies to the object passed in.
1722 mixinTemplateHelpers: function(target) {
1723 target = target || {};
1724 var templateHelpers = this.getOption('templateHelpers');
1725 templateHelpers = Marionette._getValue(templateHelpers, this);
1726 return _.extend(target, templateHelpers);
1727 },
1728
1729 // normalize the keys of passed hash with the views `ui` selectors.
1730 // `{"@ui.foo": "bar"}`
1731 normalizeUIKeys: function(hash) {
1732 var uiBindings = _.result(this, '_uiBindings');
1733 return Marionette.normalizeUIKeys(hash, uiBindings || _.result(this, 'ui'));
1734 },
1735
1736 // normalize the values of passed hash with the views `ui` selectors.
1737 // `{foo: "@ui.bar"}`
1738 normalizeUIValues: function(hash, properties) {
1739 var ui = _.result(this, 'ui');
1740 var uiBindings = _.result(this, '_uiBindings');
1741 return Marionette.normalizeUIValues(hash, uiBindings || ui, properties);
1742 },
1743
1744 // Configure `triggers` to forward DOM events to view
1745 // events. `triggers: {"click .foo": "do:foo"}`
1746 configureTriggers: function() {
1747 if (!this.triggers) { return; }
1748
1749 // Allow `triggers` to be configured as a function
1750 var triggers = this.normalizeUIKeys(_.result(this, 'triggers'));
1751
1752 // Configure the triggers, prevent default
1753 // action and stop propagation of DOM events
1754 return _.reduce(triggers, function(events, value, key) {
1755 events[key] = this._buildViewTrigger(value);
1756 return events;
1757 }, {}, this);
1758 },
1759
1760 // Overriding Backbone.View's delegateEvents to handle
1761 // the `triggers`, `modelEvents`, and `collectionEvents` configuration
1762 delegateEvents: function(events) {
1763 this._delegateDOMEvents(events);
1764 this.bindEntityEvents(this.model, this.getOption('modelEvents'));
1765 this.bindEntityEvents(this.collection, this.getOption('collectionEvents'));
1766
1767 _.each(this._behaviors, function(behavior) {
1768 behavior.bindEntityEvents(this.model, behavior.getOption('modelEvents'));
1769 behavior.bindEntityEvents(this.collection, behavior.getOption('collectionEvents'));
1770 }, this);
1771
1772 return this;
1773 },
1774
1775 // internal method to delegate DOM events and triggers
1776 _delegateDOMEvents: function(eventsArg) {
1777 var events = Marionette._getValue(eventsArg || this.events, this);
1778
1779 // normalize ui keys
1780 events = this.normalizeUIKeys(events);
1781 if (_.isUndefined(eventsArg)) {this.events = events;}
1782
1783 var combinedEvents = {};
1784
1785 // look up if this view has behavior events
1786 var behaviorEvents = _.result(this, 'behaviorEvents') || {};
1787 var triggers = this.configureTriggers();
1788 var behaviorTriggers = _.result(this, 'behaviorTriggers') || {};
1789
1790 // behavior events will be overriden by view events and or triggers
1791 _.extend(combinedEvents, behaviorEvents, events, triggers, behaviorTriggers);
1792
1793 Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
1794 },
1795
1796 // Overriding Backbone.View's undelegateEvents to handle unbinding
1797 // the `triggers`, `modelEvents`, and `collectionEvents` config
1798 undelegateEvents: function() {
1799 Backbone.View.prototype.undelegateEvents.apply(this, arguments);
1800
1801 this.unbindEntityEvents(this.model, this.getOption('modelEvents'));
1802 this.unbindEntityEvents(this.collection, this.getOption('collectionEvents'));
1803
1804 _.each(this._behaviors, function(behavior) {
1805 behavior.unbindEntityEvents(this.model, behavior.getOption('modelEvents'));
1806 behavior.unbindEntityEvents(this.collection, behavior.getOption('collectionEvents'));
1807 }, this);
1808
1809 return this;
1810 },
1811
1812 // Internal helper method to verify whether the view hasn't been destroyed
1813 _ensureViewIsIntact: function() {
1814 if (this.isDestroyed) {
1815 throw new Marionette.Error({
1816 name: 'ViewDestroyedError',
1817 message: 'View (cid: "' + this.cid + '") has already been destroyed and cannot be used.'
1818 });
1819 }
1820 },
1821
1822 // Default `destroy` implementation, for removing a view from the
1823 // DOM and unbinding it. Regions will call this method
1824 // for you. You can specify an `onDestroy` method in your view to
1825 // add custom code that is called after the view is destroyed.
1826 destroy: function() {
1827 if (this.isDestroyed) { return this; }
1828
1829 var args = _.toArray(arguments);
1830
1831 this.triggerMethod.apply(this, ['before:destroy'].concat(args));
1832
1833 // mark as destroyed before doing the actual destroy, to
1834 // prevent infinite loops within "destroy" event handlers
1835 // that are trying to destroy other views
1836 this.isDestroyed = true;
1837 this.triggerMethod.apply(this, ['destroy'].concat(args));
1838
1839 // unbind UI elements
1840 this.unbindUIElements();
1841
1842 this.isRendered = false;
1843
1844 // remove the view from the DOM
1845 this.remove();
1846
1847 // Call destroy on each behavior after
1848 // destroying the view.
1849 // This unbinds event listeners
1850 // that behaviors have registered for.
1851 _.invoke(this._behaviors, 'destroy', args);
1852
1853 return this;
1854 },
1855
1856 bindUIElements: function() {
1857 this._bindUIElements();
1858 _.invoke(this._behaviors, this._bindUIElements);
1859 },
1860
1861 // This method binds the elements specified in the "ui" hash inside the view's code with
1862 // the associated jQuery selectors.
1863 _bindUIElements: function() {
1864 if (!this.ui) { return; }
1865
1866 // store the ui hash in _uiBindings so they can be reset later
1867 // and so re-rendering the view will be able to find the bindings
1868 if (!this._uiBindings) {
1869 this._uiBindings = this.ui;
1870 }
1871
1872 // get the bindings result, as a function or otherwise
1873 var bindings = _.result(this, '_uiBindings');
1874
1875 // empty the ui so we don't have anything to start with
1876 this.ui = {};
1877
1878 // bind each of the selectors
1879 _.each(bindings, function(selector, key) {
1880 this.ui[key] = this.$(selector);
1881 }, this);
1882 },
1883
1884 // This method unbinds the elements specified in the "ui" hash
1885 unbindUIElements: function() {
1886 this._unbindUIElements();
1887 _.invoke(this._behaviors, this._unbindUIElements);
1888 },
1889
1890 _unbindUIElements: function() {
1891 if (!this.ui || !this._uiBindings) { return; }
1892
1893 // delete all of the existing ui bindings
1894 _.each(this.ui, function($el, name) {
1895 delete this.ui[name];
1896 }, this);
1897
1898 // reset the ui element to the original bindings configuration
1899 this.ui = this._uiBindings;
1900 delete this._uiBindings;
1901 },
1902
1903 // Internal method to create an event handler for a given `triggerDef` like
1904 // 'click:foo'
1905 _buildViewTrigger: function(triggerDef) {
1906
1907 var options = _.defaults({}, triggerDef, {
1908 preventDefault: true,
1909 stopPropagation: true
1910 });
1911
1912 var eventName = _.isObject(triggerDef) ? options.event : triggerDef;
1913
1914 return function(e) {
1915 if (e) {
1916 if (e.preventDefault && options.preventDefault) {
1917 e.preventDefault();
1918 }
1919
1920 if (e.stopPropagation && options.stopPropagation) {
1921 e.stopPropagation();
1922 }
1923 }
1924
1925 var args = {
1926 view: this,
1927 model: this.model,
1928 collection: this.collection
1929 };
1930
1931 this.triggerMethod(eventName, args);
1932 };
1933 },
1934
1935 setElement: function() {
1936 var ret = Backbone.View.prototype.setElement.apply(this, arguments);
1937
1938 // proxy behavior $el to the view's $el.
1939 // This is needed because a view's $el proxy
1940 // is not set until after setElement is called.
1941 _.invoke(this._behaviors, 'proxyViewProperties', this);
1942
1943 return ret;
1944 },
1945
1946 // import the `triggerMethod` to trigger events with corresponding
1947 // methods if the method exists
1948 triggerMethod: function() {
1949 var ret = Marionette._triggerMethod(this, arguments);
1950
1951 this._triggerEventOnBehaviors(arguments);
1952 this._triggerEventOnParentLayout(arguments[0], _.rest(arguments));
1953
1954 return ret;
1955 },
1956
1957 _triggerEventOnBehaviors: function(args) {
1958 var triggerMethod = Marionette._triggerMethod;
1959 var behaviors = this._behaviors;
1960 // Use good ol' for as this is a very hot function
1961 for (var i = 0, length = behaviors && behaviors.length; i < length; i++) {
1962 triggerMethod(behaviors[i], args);
1963 }
1964 },
1965
1966 _triggerEventOnParentLayout: function(eventName, args) {
1967 var layoutView = this._parentLayoutView();
1968 if (!layoutView) {
1969 return;
1970 }
1971
1972 // invoke triggerMethod on parent view
1973 var eventPrefix = Marionette.getOption(layoutView, 'childViewEventPrefix');
1974 var prefixedEventName = eventPrefix + ':' + eventName;
1975 var callArgs = [this].concat(args);
1976
1977 Marionette._triggerMethod(layoutView, prefixedEventName, callArgs);
1978
1979 // call the parent view's childEvents handler
1980 var childEvents = Marionette.getOption(layoutView, 'childEvents');
1981
1982 // since childEvents can be an object or a function use Marionette._getValue
1983 // to handle the abstaction for us.
1984 childEvents = Marionette._getValue(childEvents, layoutView);
1985 var normalizedChildEvents = layoutView.normalizeMethods(childEvents);
1986
1987 if (normalizedChildEvents && _.isFunction(normalizedChildEvents[eventName])) {
1988 normalizedChildEvents[eventName].apply(layoutView, callArgs);
1989 }
1990 },
1991
1992 // This method returns any views that are immediate
1993 // children of this view
1994 _getImmediateChildren: function() {
1995 return [];
1996 },
1997
1998 // Returns an array of every nested view within this view
1999 _getNestedViews: function() {
2000 var children = this._getImmediateChildren();
2001
2002 if (!children.length) { return children; }
2003
2004 return _.reduce(children, function(memo, view) {
2005 if (!view._getNestedViews) { return memo; }
2006 return memo.concat(view._getNestedViews());
2007 }, children);
2008 },
2009
2010 // Walk the _parent tree until we find a layout view (if one exists).
2011 // Returns the parent layout view hierarchically closest to this view.
2012 _parentLayoutView: function() {
2013 var parent = this._parent;
2014
2015 while (parent) {
2016 if (parent instanceof Marionette.LayoutView) {
2017 return parent;
2018 }
2019 parent = parent._parent;
2020 }
2021 },
2022
2023 // Imports the "normalizeMethods" to transform hashes of
2024 // events=>function references/names to a hash of events=>function references
2025 normalizeMethods: Marionette.normalizeMethods,
2026
2027 // A handy way to merge passed-in options onto the instance
2028 mergeOptions: Marionette.mergeOptions,
2029
2030 // Proxy `getOption` to enable getting options from this or this.options by name.
2031 getOption: Marionette.proxyGetOption,
2032
2033 // Proxy `bindEntityEvents` to enable binding view's events from another entity.
2034 bindEntityEvents: Marionette.proxyBindEntityEvents,
2035
2036 // Proxy `unbindEntityEvents` to enable unbinding view's events from another entity.
2037 unbindEntityEvents: Marionette.proxyUnbindEntityEvents
2038 });
2039
2040 // Item View
2041 // ---------
2042
2043 // A single item view implementation that contains code for rendering
2044 // with underscore.js templates, serializing the view's model or collection,
2045 // and calling several methods on extended views, such as `onRender`.
2046 Marionette.ItemView = Marionette.View.extend({
2047
2048 // Setting up the inheritance chain which allows changes to
2049 // Marionette.View.prototype.constructor which allows overriding
2050 constructor: function() {
2051 Marionette.View.apply(this, arguments);
2052 },
2053
2054 // Serialize the model or collection for the view. If a model is
2055 // found, the view's `serializeModel` is called. If a collection is found,
2056 // each model in the collection is serialized by calling
2057 // the view's `serializeCollection` and put into an `items` array in
2058 // the resulting data. If both are found, defaults to the model.
2059 // You can override the `serializeData` method in your own view definition,
2060 // to provide custom serialization for your view's data.
2061 serializeData: function() {
2062 if (!this.model && !this.collection) {
2063 return {};
2064 }
2065
2066 var args = [this.model || this.collection];
2067 if (arguments.length) {
2068 args.push.apply(args, arguments);
2069 }
2070
2071 if (this.model) {
2072 return this.serializeModel.apply(this, args);
2073 } else {
2074 return {
2075 items: this.serializeCollection.apply(this, args)
2076 };
2077 }
2078 },
2079
2080 // Serialize a collection by serializing each of its models.
2081 serializeCollection: function(collection) {
2082 return collection.toJSON.apply(collection, _.rest(arguments));
2083 },
2084
2085 // Render the view, defaulting to underscore.js templates.
2086 // You can override this in your view definition to provide
2087 // a very specific rendering for your view. In general, though,
2088 // you should override the `Marionette.Renderer` object to
2089 // change how Marionette renders views.
2090 render: function() {
2091 this._ensureViewIsIntact();
2092
2093 this.triggerMethod('before:render', this);
2094
2095 this._renderTemplate();
2096 this.isRendered = true;
2097 this.bindUIElements();
2098
2099 this.triggerMethod('render', this);
2100
2101 return this;
2102 },
2103
2104 // Internal method to render the template with the serialized data
2105 // and template helpers via the `Marionette.Renderer` object.
2106 // Throws an `UndefinedTemplateError` error if the template is
2107 // any falsely value but literal `false`.
2108 _renderTemplate: function() {
2109 var template = this.getTemplate();
2110
2111 // Allow template-less item views
2112 if (template === false) {
2113 return;
2114 }
2115
2116 if (!template) {
2117 throw new Marionette.Error({
2118 name: 'UndefinedTemplateError',
2119 message: 'Cannot render the template since it is null or undefined.'
2120 });
2121 }
2122
2123 // Add in entity data and template helpers
2124 var data = this.mixinTemplateHelpers(this.serializeData());
2125
2126 // Render and add to el
2127 var html = Marionette.Renderer.render(template, data, this);
2128 this.attachElContent(html);
2129
2130 return this;
2131 },
2132
2133 // Attaches the content of a given view.
2134 // This method can be overridden to optimize rendering,
2135 // or to render in a non standard way.
2136 //
2137 // For example, using `innerHTML` instead of `$el.html`
2138 //
2139 // ```js
2140 // attachElContent: function(html) {
2141 // this.el.innerHTML = html;
2142 // return this;
2143 // }
2144 // ```
2145 attachElContent: function(html) {
2146 this.$el.html(html);
2147
2148 return this;
2149 }
2150 });
2151
2152 /* jshint maxstatements: 20, maxcomplexity: 7 */
2153
2154 // Collection View
2155 // ---------------
2156
2157 // A view that iterates over a Backbone.Collection
2158 // and renders an individual child view for each model.
2159 Marionette.CollectionView = Marionette.View.extend({
2160
2161 // used as the prefix for child view events
2162 // that are forwarded through the collectionview
2163 childViewEventPrefix: 'childview',
2164
2165 // flag for maintaining the sorted order of the collection
2166 sort: true,
2167
2168 // constructor
2169 // option to pass `{sort: false}` to prevent the `CollectionView` from
2170 // maintaining the sorted order of the collection.
2171 // This will fallback onto appending childView's to the end.
2172 //
2173 // option to pass `{comparator: compFunction()}` to allow the `CollectionView`
2174 // to use a custom sort order for the collection.
2175 constructor: function(options) {
2176 this.once('render', this._initialEvents);
2177 this._initChildViewStorage();
2178
2179 Marionette.View.apply(this, arguments);
2180
2181 this.on({
2182 'before:show': this._onBeforeShowCalled,
2183 'show': this._onShowCalled,
2184 'before:attach': this._onBeforeAttachCalled,
2185 'attach': this._onAttachCalled
2186 });
2187 this.initRenderBuffer();
2188 },
2189
2190 // Instead of inserting elements one by one into the page,
2191 // it's much more performant to insert elements into a document
2192 // fragment and then insert that document fragment into the page
2193 initRenderBuffer: function() {
2194 this._bufferedChildren = [];
2195 },
2196
2197 startBuffering: function() {
2198 this.initRenderBuffer();
2199 this.isBuffering = true;
2200 },
2201
2202 endBuffering: function() {
2203 // Only trigger attach if already shown and attached, otherwise Region#show() handles this.
2204 var canTriggerAttach = this._isShown && Marionette.isNodeAttached(this.el);
2205 var nestedViews;
2206
2207 this.isBuffering = false;
2208
2209 if (this._isShown) {
2210 this._triggerMethodMany(this._bufferedChildren, this, 'before:show');
2211 }
2212 if (canTriggerAttach && this._triggerBeforeAttach) {
2213 nestedViews = this._getNestedViews();
2214 this._triggerMethodMany(nestedViews, this, 'before:attach');
2215 }
2216
2217 this.attachBuffer(this, this._createBuffer());
2218
2219 if (canTriggerAttach && this._triggerAttach) {
2220 nestedViews = this._getNestedViews();
2221 this._triggerMethodMany(nestedViews, this, 'attach');
2222 }
2223 if (this._isShown) {
2224 this._triggerMethodMany(this._bufferedChildren, this, 'show');
2225 }
2226 this.initRenderBuffer();
2227 },
2228
2229 _triggerMethodMany: function(targets, source, eventName) {
2230 var args = _.drop(arguments, 3);
2231
2232 _.each(targets, function(target) {
2233 Marionette.triggerMethodOn.apply(target, [target, eventName, target, source].concat(args));
2234 });
2235 },
2236
2237 // Configured the initial events that the collection view
2238 // binds to.
2239 _initialEvents: function() {
2240 if (this.collection) {
2241 this.listenTo(this.collection, 'add', this._onCollectionAdd);
2242 this.listenTo(this.collection, 'remove', this._onCollectionRemove);
2243 this.listenTo(this.collection, 'reset', this.render);
2244
2245 if (this.getOption('sort')) {
2246 this.listenTo(this.collection, 'sort', this._sortViews);
2247 }
2248 }
2249 },
2250
2251 // Handle a child added to the collection
2252 _onCollectionAdd: function(child, collection, opts) {
2253 // `index` is present when adding with `at` since BB 1.2; indexOf fallback for < 1.2
2254 var index = opts.at !== undefined && (opts.index || collection.indexOf(child));
2255
2256 // When filtered or when there is no initial index, calculate index.
2257 if (this.getOption('filter') || index === false) {
2258 index = _.indexOf(this._filteredSortedModels(index), child);
2259 }
2260
2261 if (this._shouldAddChild(child, index)) {
2262 this.destroyEmptyView();
2263 var ChildView = this.getChildView(child);
2264 this.addChild(child, ChildView, index);
2265 }
2266 },
2267
2268 // get the child view by model it holds, and remove it
2269 _onCollectionRemove: function(model) {
2270 var view = this.children.findByModel(model);
2271 this.removeChildView(view);
2272 this.checkEmpty();
2273 },
2274
2275 _onBeforeShowCalled: function() {
2276 // Reset attach event flags at the top of the Region#show() event lifecycle; if the Region's
2277 // show() options permit onBeforeAttach/onAttach events, these flags will be set true again.
2278 this._triggerBeforeAttach = this._triggerAttach = false;
2279 this.children.each(function(childView) {
2280 Marionette.triggerMethodOn(childView, 'before:show', childView);
2281 });
2282 },
2283
2284 _onShowCalled: function() {
2285 this.children.each(function(childView) {
2286 Marionette.triggerMethodOn(childView, 'show', childView);
2287 });
2288 },
2289
2290 // If during Region#show() onBeforeAttach was fired, continue firing it for child views
2291 _onBeforeAttachCalled: function() {
2292 this._triggerBeforeAttach = true;
2293 },
2294
2295 // If during Region#show() onAttach was fired, continue firing it for child views
2296 _onAttachCalled: function() {
2297 this._triggerAttach = true;
2298 },
2299
2300 // Render children views. Override this method to
2301 // provide your own implementation of a render function for
2302 // the collection view.
2303 render: function() {
2304 this._ensureViewIsIntact();
2305 this.triggerMethod('before:render', this);
2306 this._renderChildren();
2307 this.isRendered = true;
2308 this.triggerMethod('render', this);
2309 return this;
2310 },
2311
2312 // Reorder DOM after sorting. When your element's rendering
2313 // do not use their index, you can pass reorderOnSort: true
2314 // to only reorder the DOM after a sort instead of rendering
2315 // all the collectionView
2316 reorder: function() {
2317 var children = this.children;
2318 var models = this._filteredSortedModels();
2319 var anyModelsAdded = _.some(models, function(model) {
2320 return !children.findByModel(model);
2321 });
2322
2323 // If there are any new models added due to filtering
2324 // We need to add child views
2325 // So render as normal
2326 if (anyModelsAdded) {
2327 this.render();
2328 } else {
2329 // get the DOM nodes in the same order as the models
2330 var elsToReorder = _.map(models, function(model, index) {
2331 var view = children.findByModel(model);
2332 view._index = index;
2333 return view.el;
2334 });
2335
2336 // find the views that were children before but arent in this new ordering
2337 var filteredOutViews = children.filter(function(view) {
2338 return !_.contains(elsToReorder, view.el);
2339 });
2340
2341 this.triggerMethod('before:reorder');
2342
2343 // since append moves elements that are already in the DOM,
2344 // appending the elements will effectively reorder them
2345 this._appendReorderedChildren(elsToReorder);
2346
2347 // remove any views that have been filtered out
2348 _.each(filteredOutViews, this.removeChildView, this);
2349 this.checkEmpty();
2350
2351 this.triggerMethod('reorder');
2352 }
2353 },
2354
2355 // Render view after sorting. Override this method to
2356 // change how the view renders after a `sort` on the collection.
2357 // An example of this would be to only `renderChildren` in a `CompositeView`
2358 // rather than the full view.
2359 resortView: function() {
2360 if (Marionette.getOption(this, 'reorderOnSort')) {
2361 this.reorder();
2362 } else {
2363 this.render();
2364 }
2365 },
2366
2367 // Internal method. This checks for any changes in the order of the collection.
2368 // If the index of any view doesn't match, it will render.
2369 _sortViews: function() {
2370 var models = this._filteredSortedModels();
2371
2372 // check for any changes in sort order of views
2373 var orderChanged = _.find(models, function(item, index) {
2374 var view = this.children.findByModel(item);
2375 return !view || view._index !== index;
2376 }, this);
2377
2378 if (orderChanged) {
2379 this.resortView();
2380 }
2381 },
2382
2383 // Internal reference to what index a `emptyView` is.
2384 _emptyViewIndex: -1,
2385
2386 // Internal method. Separated so that CompositeView can append to the childViewContainer
2387 // if necessary
2388 _appendReorderedChildren: function(children) {
2389 this.$el.append(children);
2390 },
2391
2392 // Internal method. Separated so that CompositeView can have
2393 // more control over events being triggered, around the rendering
2394 // process
2395 _renderChildren: function() {
2396 this.destroyEmptyView();
2397 this.destroyChildren({checkEmpty: false});
2398
2399 if (this.isEmpty(this.collection)) {
2400 this.showEmptyView();
2401 } else {
2402 this.triggerMethod('before:render:collection', this);
2403 this.startBuffering();
2404 this.showCollection();
2405 this.endBuffering();
2406 this.triggerMethod('render:collection', this);
2407
2408 // If we have shown children and none have passed the filter, show the empty view
2409 if (this.children.isEmpty() && this.getOption('filter')) {
2410 this.showEmptyView();
2411 }
2412 }
2413 },
2414
2415 // Internal method to loop through collection and show each child view.
2416 showCollection: function() {
2417 var ChildView;
2418
2419 var models = this._filteredSortedModels();
2420
2421 _.each(models, function(child, index) {
2422 ChildView = this.getChildView(child);
2423 this.addChild(child, ChildView, index);
2424 }, this);
2425 },
2426
2427 // Allow the collection to be sorted by a custom view comparator
2428 _filteredSortedModels: function(addedAt) {
2429 var viewComparator = this.getViewComparator();
2430 var models = this.collection.models;
2431 addedAt = Math.min(Math.max(addedAt, 0), models.length - 1);
2432
2433 if (viewComparator) {
2434 var addedModel;
2435 // Preserve `at` location, even for a sorted view
2436 if (addedAt) {
2437 addedModel = models[addedAt];
2438 models = models.slice(0, addedAt).concat(models.slice(addedAt + 1));
2439 }
2440 models = this._sortModelsBy(models, viewComparator);
2441 if (addedModel) {
2442 models.splice(addedAt, 0, addedModel);
2443 }
2444 }
2445
2446 // Filter after sorting in case the filter uses the index
2447 if (this.getOption('filter')) {
2448 models = _.filter(models, function(model, index) {
2449 return this._shouldAddChild(model, index);
2450 }, this);
2451 }
2452
2453 return models;
2454 },
2455
2456 _sortModelsBy: function(models, comparator) {
2457 if (typeof comparator === 'string') {
2458 return _.sortBy(models, function(model) {
2459 return model.get(comparator);
2460 }, this);
2461 } else if (comparator.length === 1) {
2462 return _.sortBy(models, comparator, this);
2463 } else {
2464 return models.sort(_.bind(comparator, this));
2465 }
2466 },
2467
2468 // Internal method to show an empty view in place of
2469 // a collection of child views, when the collection is empty
2470 showEmptyView: function() {
2471 var EmptyView = this.getEmptyView();
2472
2473 if (EmptyView && !this._showingEmptyView) {
2474 this.triggerMethod('before:render:empty');
2475
2476 this._showingEmptyView = true;
2477 var model = new Backbone.Model();
2478 this.addEmptyView(model, EmptyView);
2479
2480 this.triggerMethod('render:empty');
2481 }
2482 },
2483
2484 // Internal method to destroy an existing emptyView instance
2485 // if one exists. Called when a collection view has been
2486 // rendered empty, and then a child is added to the collection.
2487 destroyEmptyView: function() {
2488 if (this._showingEmptyView) {
2489 this.triggerMethod('before:remove:empty');
2490
2491 this.destroyChildren();
2492 delete this._showingEmptyView;
2493
2494 this.triggerMethod('remove:empty');
2495 }
2496 },
2497
2498 // Retrieve the empty view class
2499 getEmptyView: function() {
2500 return this.getOption('emptyView');
2501 },
2502
2503 // Render and show the emptyView. Similar to addChild method
2504 // but "add:child" events are not fired, and the event from
2505 // emptyView are not forwarded
2506 addEmptyView: function(child, EmptyView) {
2507 // Only trigger attach if already shown, attached, and not buffering, otherwise endBuffer() or
2508 // Region#show() handles this.
2509 var canTriggerAttach = this._isShown && !this.isBuffering && Marionette.isNodeAttached(this.el);
2510 var nestedViews;
2511
2512 // get the emptyViewOptions, falling back to childViewOptions
2513 var emptyViewOptions = this.getOption('emptyViewOptions') ||
2514 this.getOption('childViewOptions');
2515
2516 if (_.isFunction(emptyViewOptions)) {
2517 emptyViewOptions = emptyViewOptions.call(this, child, this._emptyViewIndex);
2518 }
2519
2520 // build the empty view
2521 var view = this.buildChildView(child, EmptyView, emptyViewOptions);
2522
2523 view._parent = this;
2524
2525 // Proxy emptyView events
2526 this.proxyChildEvents(view);
2527
2528 view.once('render', function() {
2529 // trigger the 'before:show' event on `view` if the collection view has already been shown
2530 if (this._isShown) {
2531 Marionette.triggerMethodOn(view, 'before:show', view);
2532 }
2533
2534 // Trigger `before:attach` following `render` to avoid adding logic and event triggers
2535 // to public method `renderChildView()`.
2536 if (canTriggerAttach && this._triggerBeforeAttach) {
2537 nestedViews = this._getViewAndNested(view);
2538 this._triggerMethodMany(nestedViews, this, 'before:attach');
2539 }
2540 }, this);
2541
2542 // Store the `emptyView` like a `childView` so we can properly remove and/or close it later
2543 this.children.add(view);
2544 this.renderChildView(view, this._emptyViewIndex);
2545
2546 // Trigger `attach`
2547 if (canTriggerAttach && this._triggerAttach) {
2548 nestedViews = this._getViewAndNested(view);
2549 this._triggerMethodMany(nestedViews, this, 'attach');
2550 }
2551 // call the 'show' method if the collection view has already been shown
2552 if (this._isShown) {
2553 Marionette.triggerMethodOn(view, 'show', view);
2554 }
2555 },
2556
2557 // Retrieve the `childView` class, either from `this.options.childView`
2558 // or from the `childView` in the object definition. The "options"
2559 // takes precedence.
2560 // This method receives the model that will be passed to the instance
2561 // created from this `childView`. Overriding methods may use the child
2562 // to determine what `childView` class to return.
2563 getChildView: function(child) {
2564 var childView = this.getOption('childView');
2565
2566 if (!childView) {
2567 throw new Marionette.Error({
2568 name: 'NoChildViewError',
2569 message: 'A "childView" must be specified'
2570 });
2571 }
2572
2573 return childView;
2574 },
2575
2576 // Render the child's view and add it to the
2577 // HTML for the collection view at a given index.
2578 // This will also update the indices of later views in the collection
2579 // in order to keep the children in sync with the collection.
2580 addChild: function(child, ChildView, index) {
2581 var childViewOptions = this.getOption('childViewOptions');
2582 childViewOptions = Marionette._getValue(childViewOptions, this, [child, index]);
2583
2584 var view = this.buildChildView(child, ChildView, childViewOptions);
2585
2586 // increment indices of views after this one
2587 this._updateIndices(view, true, index);
2588
2589 this.triggerMethod('before:add:child', view);
2590 this._addChildView(view, index);
2591 this.triggerMethod('add:child', view);
2592
2593 view._parent = this;
2594
2595 return view;
2596 },
2597
2598 // Internal method. This decrements or increments the indices of views after the
2599 // added/removed view to keep in sync with the collection.
2600 _updateIndices: function(view, increment, index) {
2601 if (!this.getOption('sort')) {
2602 return;
2603 }
2604
2605 if (increment) {
2606 // assign the index to the view
2607 view._index = index;
2608 }
2609
2610 // update the indexes of views after this one
2611 this.children.each(function(laterView) {
2612 if (laterView._index >= view._index) {
2613 laterView._index += increment ? 1 : -1;
2614 }
2615 });
2616 },
2617
2618 // Internal Method. Add the view to children and render it at
2619 // the given index.
2620 _addChildView: function(view, index) {
2621 // Only trigger attach if already shown, attached, and not buffering, otherwise endBuffer() or
2622 // Region#show() handles this.
2623 var canTriggerAttach = this._isShown && !this.isBuffering && Marionette.isNodeAttached(this.el);
2624 var nestedViews;
2625
2626 // set up the child view event forwarding
2627 this.proxyChildEvents(view);
2628
2629 view.once('render', function() {
2630 // trigger the 'before:show' event on `view` if the collection view has already been shown
2631 if (this._isShown && !this.isBuffering) {
2632 Marionette.triggerMethodOn(view, 'before:show', view);
2633 }
2634
2635 // Trigger `before:attach` following `render` to avoid adding logic and event triggers
2636 // to public method `renderChildView()`.
2637 if (canTriggerAttach && this._triggerBeforeAttach) {
2638 nestedViews = this._getViewAndNested(view);
2639 this._triggerMethodMany(nestedViews, this, 'before:attach');
2640 }
2641 }, this);
2642
2643 // Store the child view itself so we can properly remove and/or destroy it later
2644 this.children.add(view);
2645 this.renderChildView(view, index);
2646
2647 // Trigger `attach`
2648 if (canTriggerAttach && this._triggerAttach) {
2649 nestedViews = this._getViewAndNested(view);
2650 this._triggerMethodMany(nestedViews, this, 'attach');
2651 }
2652 // Trigger `show`
2653 if (this._isShown && !this.isBuffering) {
2654 Marionette.triggerMethodOn(view, 'show', view);
2655 }
2656 },
2657
2658 // render the child view
2659 renderChildView: function(view, index) {
2660 if (!view.supportsRenderLifecycle) {
2661 Marionette.triggerMethodOn(view, 'before:render', view);
2662 }
2663 view.render();
2664 if (!view.supportsRenderLifecycle) {
2665 Marionette.triggerMethodOn(view, 'render', view);
2666 }
2667 this.attachHtml(this, view, index);
2668 return view;
2669 },
2670
2671 // Build a `childView` for a model in the collection.
2672 buildChildView: function(child, ChildViewClass, childViewOptions) {
2673 var options = _.extend({model: child}, childViewOptions);
2674 var childView = new ChildViewClass(options);
2675 Marionette.MonitorDOMRefresh(childView);
2676 return childView;
2677 },
2678
2679 // Remove the child view and destroy it.
2680 // This function also updates the indices of
2681 // later views in the collection in order to keep
2682 // the children in sync with the collection.
2683 removeChildView: function(view) {
2684 if (!view) { return view; }
2685
2686 this.triggerMethod('before:remove:child', view);
2687
2688 if (!view.supportsDestroyLifecycle) {
2689 Marionette.triggerMethodOn(view, 'before:destroy', view);
2690 }
2691 // call 'destroy' or 'remove', depending on which is found
2692 if (view.destroy) {
2693 view.destroy();
2694 } else {
2695 view.remove();
2696 }
2697 if (!view.supportsDestroyLifecycle) {
2698 Marionette.triggerMethodOn(view, 'destroy', view);
2699 }
2700
2701 delete view._parent;
2702 this.stopListening(view);
2703 this.children.remove(view);
2704 this.triggerMethod('remove:child', view);
2705
2706 // decrement the index of views after this one
2707 this._updateIndices(view, false);
2708
2709 return view;
2710 },
2711
2712 // check if the collection is empty
2713 isEmpty: function() {
2714 return !this.collection || this.collection.length === 0;
2715 },
2716
2717 // If empty, show the empty view
2718 checkEmpty: function() {
2719 if (this.isEmpty(this.collection)) {
2720 this.showEmptyView();
2721 }
2722 },
2723
2724 // You might need to override this if you've overridden attachHtml
2725 attachBuffer: function(collectionView, buffer) {
2726 collectionView.$el.append(buffer);
2727 },
2728
2729 // Create a fragment buffer from the currently buffered children
2730 _createBuffer: function() {
2731 var elBuffer = document.createDocumentFragment();
2732 _.each(this._bufferedChildren, function(b) {
2733 elBuffer.appendChild(b.el);
2734 });
2735 return elBuffer;
2736 },
2737
2738 // Append the HTML to the collection's `el`.
2739 // Override this method to do something other
2740 // than `.append`.
2741 attachHtml: function(collectionView, childView, index) {
2742 if (collectionView.isBuffering) {
2743 // buffering happens on reset events and initial renders
2744 // in order to reduce the number of inserts into the
2745 // document, which are expensive.
2746 collectionView._bufferedChildren.splice(index, 0, childView);
2747 } else {
2748 // If we've already rendered the main collection, append
2749 // the new child into the correct order if we need to. Otherwise
2750 // append to the end.
2751 if (!collectionView._insertBefore(childView, index)) {
2752 collectionView._insertAfter(childView);
2753 }
2754 }
2755 },
2756
2757 // Internal method. Check whether we need to insert the view into
2758 // the correct position.
2759 _insertBefore: function(childView, index) {
2760 var currentView;
2761 var findPosition = this.getOption('sort') && (index < this.children.length - 1);
2762 if (findPosition) {
2763 // Find the view after this one
2764 currentView = this.children.find(function(view) {
2765 return view._index === index + 1;
2766 });
2767 }
2768
2769 if (currentView) {
2770 currentView.$el.before(childView.el);
2771 return true;
2772 }
2773
2774 return false;
2775 },
2776
2777 // Internal method. Append a view to the end of the $el
2778 _insertAfter: function(childView) {
2779 this.$el.append(childView.el);
2780 },
2781
2782 // Internal method to set up the `children` object for
2783 // storing all of the child views
2784 _initChildViewStorage: function() {
2785 this.children = new Backbone.ChildViewContainer();
2786 },
2787
2788 // Handle cleanup and other destroying needs for the collection of views
2789 destroy: function() {
2790 if (this.isDestroyed) { return this; }
2791
2792 this.triggerMethod('before:destroy:collection');
2793 this.destroyChildren({checkEmpty: false});
2794 this.triggerMethod('destroy:collection');
2795
2796 return Marionette.View.prototype.destroy.apply(this, arguments);
2797 },
2798
2799 // Destroy the child views that this collection view
2800 // is holding on to, if any
2801 destroyChildren: function(options) {
2802 var destroyOptions = options || {};
2803 var shouldCheckEmpty = true;
2804 var childViews = this.children.map(_.identity);
2805
2806 if (!_.isUndefined(destroyOptions.checkEmpty)) {
2807 shouldCheckEmpty = destroyOptions.checkEmpty;
2808 }
2809
2810 this.children.each(this.removeChildView, this);
2811
2812 if (shouldCheckEmpty) {
2813 this.checkEmpty();
2814 }
2815 return childViews;
2816 },
2817
2818 // Return true if the given child should be shown
2819 // Return false otherwise
2820 // The filter will be passed (child, index, collection)
2821 // Where
2822 // 'child' is the given model
2823 // 'index' is the index of that model in the collection
2824 // 'collection' is the collection referenced by this CollectionView
2825 _shouldAddChild: function(child, index) {
2826 var filter = this.getOption('filter');
2827 return !_.isFunction(filter) || filter.call(this, child, index, this.collection);
2828 },
2829
2830 // Set up the child view event forwarding. Uses a "childview:"
2831 // prefix in front of all forwarded events.
2832 proxyChildEvents: function(view) {
2833 var prefix = this.getOption('childViewEventPrefix');
2834
2835 // Forward all child view events through the parent,
2836 // prepending "childview:" to the event name
2837 this.listenTo(view, 'all', function() {
2838 var args = _.toArray(arguments);
2839 var rootEvent = args[0];
2840 var childEvents = this.normalizeMethods(_.result(this, 'childEvents'));
2841
2842 args[0] = prefix + ':' + rootEvent;
2843 args.splice(1, 0, view);
2844
2845 // call collectionView childEvent if defined
2846 if (typeof childEvents !== 'undefined' && _.isFunction(childEvents[rootEvent])) {
2847 childEvents[rootEvent].apply(this, args.slice(1));
2848 }
2849
2850 this.triggerMethod.apply(this, args);
2851 });
2852 },
2853
2854 _getImmediateChildren: function() {
2855 return _.values(this.children._views);
2856 },
2857
2858 _getViewAndNested: function(view) {
2859 // This will not fail on Backbone.View which does not have #_getNestedViews.
2860 return [view].concat(_.result(view, '_getNestedViews') || []);
2861 },
2862
2863 getViewComparator: function() {
2864 return this.getOption('viewComparator');
2865 }
2866 });
2867
2868 /* jshint maxstatements: 17, maxlen: 117 */
2869
2870 // Composite View
2871 // --------------
2872
2873 // Used for rendering a branch-leaf, hierarchical structure.
2874 // Extends directly from CollectionView and also renders an
2875 // a child view as `modelView`, for the top leaf
2876 Marionette.CompositeView = Marionette.CollectionView.extend({
2877
2878 // Setting up the inheritance chain which allows changes to
2879 // Marionette.CollectionView.prototype.constructor which allows overriding
2880 // option to pass '{sort: false}' to prevent the CompositeView from
2881 // maintaining the sorted order of the collection.
2882 // This will fallback onto appending childView's to the end.
2883 constructor: function() {
2884 Marionette.CollectionView.apply(this, arguments);
2885 },
2886
2887 // Configured the initial events that the composite view
2888 // binds to. Override this method to prevent the initial
2889 // events, or to add your own initial events.
2890 _initialEvents: function() {
2891
2892 // Bind only after composite view is rendered to avoid adding child views
2893 // to nonexistent childViewContainer
2894
2895 if (this.collection) {
2896 this.listenTo(this.collection, 'add', this._onCollectionAdd);
2897 this.listenTo(this.collection, 'remove', this._onCollectionRemove);
2898 this.listenTo(this.collection, 'reset', this._renderChildren);
2899
2900 if (this.getOption('sort')) {
2901 this.listenTo(this.collection, 'sort', this._sortViews);
2902 }
2903 }
2904 },
2905
2906 // Retrieve the `childView` to be used when rendering each of
2907 // the items in the collection. The default is to return
2908 // `this.childView` or Marionette.CompositeView if no `childView`
2909 // has been defined
2910 getChildView: function(child) {
2911 var childView = this.getOption('childView') || this.constructor;
2912
2913 return childView;
2914 },
2915
2916 // Serialize the model for the view.
2917 // You can override the `serializeData` method in your own view
2918 // definition, to provide custom serialization for your view's data.
2919 serializeData: function() {
2920 var data = {};
2921
2922 if (this.model) {
2923 data = _.partial(this.serializeModel, this.model).apply(this, arguments);
2924 }
2925
2926 return data;
2927 },
2928
2929 // Renders the model and the collection.
2930 render: function() {
2931 this._ensureViewIsIntact();
2932 this._isRendering = true;
2933 this.resetChildViewContainer();
2934
2935 this.triggerMethod('before:render', this);
2936
2937 this._renderTemplate();
2938 this._renderChildren();
2939
2940 this._isRendering = false;
2941 this.isRendered = true;
2942 this.triggerMethod('render', this);
2943 return this;
2944 },
2945
2946 _renderChildren: function() {
2947 if (this.isRendered || this._isRendering) {
2948 Marionette.CollectionView.prototype._renderChildren.call(this);
2949 }
2950 },
2951
2952 // Render the root template that the children
2953 // views are appended to
2954 _renderTemplate: function() {
2955 var data = {};
2956 data = this.serializeData();
2957 data = this.mixinTemplateHelpers(data);
2958
2959 this.triggerMethod('before:render:template');
2960
2961 var template = this.getTemplate();
2962 var html = Marionette.Renderer.render(template, data, this);
2963 this.attachElContent(html);
2964
2965 // the ui bindings is done here and not at the end of render since they
2966 // will not be available until after the model is rendered, but should be
2967 // available before the collection is rendered.
2968 this.bindUIElements();
2969 this.triggerMethod('render:template');
2970 },
2971
2972 // Attaches the content of the root.
2973 // This method can be overridden to optimize rendering,
2974 // or to render in a non standard way.
2975 //
2976 // For example, using `innerHTML` instead of `$el.html`
2977 //
2978 // ```js
2979 // attachElContent: function(html) {
2980 // this.el.innerHTML = html;
2981 // return this;
2982 // }
2983 // ```
2984 attachElContent: function(html) {
2985 this.$el.html(html);
2986
2987 return this;
2988 },
2989
2990 // You might need to override this if you've overridden attachHtml
2991 attachBuffer: function(compositeView, buffer) {
2992 var $container = this.getChildViewContainer(compositeView);
2993 $container.append(buffer);
2994 },
2995
2996 // Internal method. Append a view to the end of the $el.
2997 // Overidden from CollectionView to ensure view is appended to
2998 // childViewContainer
2999 _insertAfter: function(childView) {
3000 var $container = this.getChildViewContainer(this, childView);
3001 $container.append(childView.el);
3002 },
3003
3004 // Internal method. Append reordered childView'.
3005 // Overidden from CollectionView to ensure reordered views
3006 // are appended to childViewContainer
3007 _appendReorderedChildren: function(children) {
3008 var $container = this.getChildViewContainer(this);
3009 $container.append(children);
3010 },
3011
3012 // Internal method to ensure an `$childViewContainer` exists, for the
3013 // `attachHtml` method to use.
3014 getChildViewContainer: function(containerView, childView) {
3015 if (!!containerView.$childViewContainer) {
3016 return containerView.$childViewContainer;
3017 }
3018
3019 var container;
3020 var childViewContainer = Marionette.getOption(containerView, 'childViewContainer');
3021 if (childViewContainer) {
3022
3023 var selector = Marionette._getValue(childViewContainer, containerView);
3024
3025 if (selector.charAt(0) === '@' && containerView.ui) {
3026 container = containerView.ui[selector.substr(4)];
3027 } else {
3028 container = containerView.$(selector);
3029 }
3030
3031 if (container.length <= 0) {
3032 throw new Marionette.Error({
3033 name: 'ChildViewContainerMissingError',
3034 message: 'The specified "childViewContainer" was not found: ' + containerView.childViewContainer
3035 });
3036 }
3037
3038 } else {
3039 container = containerView.$el;
3040 }
3041
3042 containerView.$childViewContainer = container;
3043 return container;
3044 },
3045
3046 // Internal method to reset the `$childViewContainer` on render
3047 resetChildViewContainer: function() {
3048 if (this.$childViewContainer) {
3049 this.$childViewContainer = undefined;
3050 }
3051 }
3052 });
3053
3054 // Layout View
3055 // -----------
3056
3057 // Used for managing application layoutViews, nested layoutViews and
3058 // multiple regions within an application or sub-application.
3059 //
3060 // A specialized view class that renders an area of HTML and then
3061 // attaches `Region` instances to the specified `regions`.
3062 // Used for composite view management and sub-application areas.
3063 Marionette.LayoutView = Marionette.ItemView.extend({
3064 regionClass: Marionette.Region,
3065
3066 options: {
3067 destroyImmediate: false
3068 },
3069
3070 // used as the prefix for child view events
3071 // that are forwarded through the layoutview
3072 childViewEventPrefix: 'childview',
3073
3074 // Ensure the regions are available when the `initialize` method
3075 // is called.
3076 constructor: function(options) {
3077 options = options || {};
3078
3079 this._firstRender = true;
3080 this._initializeRegions(options);
3081
3082 Marionette.ItemView.call(this, options);
3083 },
3084
3085 // LayoutView's render will use the existing region objects the
3086 // first time it is called. Subsequent calls will destroy the
3087 // views that the regions are showing and then reset the `el`
3088 // for the regions to the newly rendered DOM elements.
3089 render: function() {
3090 this._ensureViewIsIntact();
3091
3092 if (this._firstRender) {
3093 // if this is the first render, don't do anything to
3094 // reset the regions
3095 this._firstRender = false;
3096 } else {
3097 // If this is not the first render call, then we need to
3098 // re-initialize the `el` for each region
3099 this._reInitializeRegions();
3100 }
3101
3102 return Marionette.ItemView.prototype.render.apply(this, arguments);
3103 },
3104
3105 // Handle destroying regions, and then destroy the view itself.
3106 destroy: function() {
3107 if (this.isDestroyed) { return this; }
3108 // #2134: remove parent element before destroying the child views, so
3109 // removing the child views doesn't retrigger repaints
3110 if (this.getOption('destroyImmediate') === true) {
3111 this.$el.remove();
3112 }
3113 this.regionManager.destroy();
3114 return Marionette.ItemView.prototype.destroy.apply(this, arguments);
3115 },
3116
3117 showChildView: function(regionName, view, options) {
3118 var region = this.getRegion(regionName);
3119 return region.show.apply(region, _.rest(arguments));
3120 },
3121
3122 getChildView: function(regionName) {
3123 return this.getRegion(regionName).currentView;
3124 },
3125
3126 // Add a single region, by name, to the layoutView
3127 addRegion: function(name, definition) {
3128 var regions = {};
3129 regions[name] = definition;
3130 return this._buildRegions(regions)[name];
3131 },
3132
3133 // Add multiple regions as a {name: definition, name2: def2} object literal
3134 addRegions: function(regions) {
3135 this.regions = _.extend({}, this.regions, regions);
3136 return this._buildRegions(regions);
3137 },
3138
3139 // Remove a single region from the LayoutView, by name
3140 removeRegion: function(name) {
3141 delete this.regions[name];
3142 return this.regionManager.removeRegion(name);
3143 },
3144
3145 // Provides alternative access to regions
3146 // Accepts the region name
3147 // getRegion('main')
3148 getRegion: function(region) {
3149 return this.regionManager.get(region);
3150 },
3151
3152 // Get all regions
3153 getRegions: function() {
3154 return this.regionManager.getRegions();
3155 },
3156
3157 // internal method to build regions
3158 _buildRegions: function(regions) {
3159 var defaults = {
3160 regionClass: this.getOption('regionClass'),
3161 parentEl: _.partial(_.result, this, 'el')
3162 };
3163
3164 return this.regionManager.addRegions(regions, defaults);
3165 },
3166
3167 // Internal method to initialize the regions that have been defined in a
3168 // `regions` attribute on this layoutView.
3169 _initializeRegions: function(options) {
3170 var regions;
3171 this._initRegionManager();
3172
3173 regions = Marionette._getValue(this.regions, this, [options]) || {};
3174
3175 // Enable users to define `regions` as instance options.
3176 var regionOptions = this.getOption.call(options, 'regions');
3177
3178 // enable region options to be a function
3179 regionOptions = Marionette._getValue(regionOptions, this, [options]);
3180
3181 _.extend(regions, regionOptions);
3182
3183 // Normalize region selectors hash to allow
3184 // a user to use the @ui. syntax.
3185 regions = this.normalizeUIValues(regions, ['selector', 'el']);
3186
3187 this.addRegions(regions);
3188 },
3189
3190 // Internal method to re-initialize all of the regions by updating the `el` that
3191 // they point to
3192 _reInitializeRegions: function() {
3193 this.regionManager.invoke('reset');
3194 },
3195
3196 // Enable easy overriding of the default `RegionManager`
3197 // for customized region interactions and business specific
3198 // view logic for better control over single regions.
3199 getRegionManager: function() {
3200 return new Marionette.RegionManager();
3201 },
3202
3203 // Internal method to initialize the region manager
3204 // and all regions in it
3205 _initRegionManager: function() {
3206 this.regionManager = this.getRegionManager();
3207 this.regionManager._parent = this;
3208
3209 this.listenTo(this.regionManager, 'before:add:region', function(name) {
3210 this.triggerMethod('before:add:region', name);
3211 });
3212
3213 this.listenTo(this.regionManager, 'add:region', function(name, region) {
3214 this[name] = region;
3215 this.triggerMethod('add:region', name, region);
3216 });
3217
3218 this.listenTo(this.regionManager, 'before:remove:region', function(name) {
3219 this.triggerMethod('before:remove:region', name);
3220 });
3221
3222 this.listenTo(this.regionManager, 'remove:region', function(name, region) {
3223 delete this[name];
3224 this.triggerMethod('remove:region', name, region);
3225 });
3226 },
3227
3228 _getImmediateChildren: function() {
3229 return _.chain(this.regionManager.getRegions())
3230 .pluck('currentView')
3231 .compact()
3232 .value();
3233 }
3234 });
3235
3236
3237 // Behavior
3238 // --------
3239
3240 // A Behavior is an isolated set of DOM /
3241 // user interactions that can be mixed into any View.
3242 // Behaviors allow you to blackbox View specific interactions
3243 // into portable logical chunks, keeping your views simple and your code DRY.
3244
3245 Marionette.Behavior = Marionette.Object.extend({
3246 constructor: function(options, view) {
3247 // Setup reference to the view.
3248 // this comes in handle when a behavior
3249 // wants to directly talk up the chain
3250 // to the view.
3251 this.view = view;
3252 this.defaults = _.result(this, 'defaults') || {};
3253 this.options = _.extend({}, this.defaults, options);
3254 // Construct an internal UI hash using
3255 // the views UI hash and then the behaviors UI hash.
3256 // This allows the user to use UI hash elements
3257 // defined in the parent view as well as those
3258 // defined in the given behavior.
3259 this.ui = _.extend({}, _.result(view, 'ui'), _.result(this, 'ui'));
3260
3261 Marionette.Object.apply(this, arguments);
3262 },
3263
3264 // proxy behavior $ method to the view
3265 // this is useful for doing jquery DOM lookups
3266 // scoped to behaviors view.
3267 $: function() {
3268 return this.view.$.apply(this.view, arguments);
3269 },
3270
3271 // Stops the behavior from listening to events.
3272 // Overrides Object#destroy to prevent additional events from being triggered.
3273 destroy: function() {
3274 this.stopListening();
3275
3276 return this;
3277 },
3278
3279 proxyViewProperties: function(view) {
3280 this.$el = view.$el;
3281 this.el = view.el;
3282 }
3283 });
3284
3285 /* jshint maxlen: 143 */
3286 // Behaviors
3287 // ---------
3288
3289 // Behaviors is a utility class that takes care of
3290 // gluing your behavior instances to their given View.
3291 // The most important part of this class is that you
3292 // **MUST** override the class level behaviorsLookup
3293 // method for things to work properly.
3294
3295 Marionette.Behaviors = (function(Marionette, _) {
3296 // Borrow event splitter from Backbone
3297 var delegateEventSplitter = /^(\S+)\s*(.*)$/;
3298
3299 function Behaviors(view, behaviors) {
3300
3301 if (!_.isObject(view.behaviors)) {
3302 return {};
3303 }
3304
3305 // Behaviors defined on a view can be a flat object literal
3306 // or it can be a function that returns an object.
3307 behaviors = Behaviors.parseBehaviors(view, behaviors || _.result(view, 'behaviors'));
3308
3309 // Wraps several of the view's methods
3310 // calling the methods first on each behavior
3311 // and then eventually calling the method on the view.
3312 Behaviors.wrap(view, behaviors, _.keys(methods));
3313 return behaviors;
3314 }
3315
3316 var methods = {
3317 behaviorTriggers: function(behaviorTriggers, behaviors) {
3318 var triggerBuilder = new BehaviorTriggersBuilder(this, behaviors);
3319 return triggerBuilder.buildBehaviorTriggers();
3320 },
3321
3322 behaviorEvents: function(behaviorEvents, behaviors) {
3323 var _behaviorsEvents = {};
3324
3325 _.each(behaviors, function(b, i) {
3326 var _events = {};
3327 var behaviorEvents = _.clone(_.result(b, 'events')) || {};
3328
3329 // Normalize behavior events hash to allow
3330 // a user to use the @ui. syntax.
3331 behaviorEvents = Marionette.normalizeUIKeys(behaviorEvents, getBehaviorsUI(b));
3332
3333 var j = 0;
3334 _.each(behaviorEvents, function(behaviour, key) {
3335 var match = key.match(delegateEventSplitter);
3336
3337 // Set event name to be namespaced using the view cid,
3338 // the behavior index, and the behavior event index
3339 // to generate a non colliding event namespace
3340 // http://api.jquery.com/event.namespace/
3341 var eventName = match[1] + '.' + [this.cid, i, j++, ' '].join('');
3342 var selector = match[2];
3343
3344 var eventKey = eventName + selector;
3345 var handler = _.isFunction(behaviour) ? behaviour : b[behaviour];
3346 if (!handler) { return; }
3347 _events[eventKey] = _.bind(handler, b);
3348 }, this);
3349
3350 _behaviorsEvents = _.extend(_behaviorsEvents, _events);
3351 }, this);
3352
3353 return _behaviorsEvents;
3354 }
3355 };
3356
3357 _.extend(Behaviors, {
3358
3359 // Placeholder method to be extended by the user.
3360 // The method should define the object that stores the behaviors.
3361 // i.e.
3362 //
3363 // ```js
3364 // Marionette.Behaviors.behaviorsLookup: function() {
3365 // return App.Behaviors
3366 // }
3367 // ```
3368 behaviorsLookup: function() {
3369 throw new Marionette.Error({
3370 message: 'You must define where your behaviors are stored.',
3371 url: 'marionette.behaviors.html#behaviorslookup'
3372 });
3373 },
3374
3375 // Takes care of getting the behavior class
3376 // given options and a key.
3377 // If a user passes in options.behaviorClass
3378 // default to using that. Otherwise delegate
3379 // the lookup to the users `behaviorsLookup` implementation.
3380 getBehaviorClass: function(options, key) {
3381 if (options.behaviorClass) {
3382 return options.behaviorClass;
3383 }
3384
3385 // Get behavior class can be either a flat object or a method
3386 return Marionette._getValue(Behaviors.behaviorsLookup, this, [options, key])[key];
3387 },
3388
3389 // Iterate over the behaviors object, for each behavior
3390 // instantiate it and get its grouped behaviors.
3391 parseBehaviors: function(view, behaviors) {
3392 return _.chain(behaviors).map(function(options, key) {
3393 var BehaviorClass = Behaviors.getBehaviorClass(options, key);
3394
3395 var behavior = new BehaviorClass(options, view);
3396 var nestedBehaviors = Behaviors.parseBehaviors(view, _.result(behavior, 'behaviors'));
3397
3398 return [behavior].concat(nestedBehaviors);
3399 }).flatten().value();
3400 },
3401
3402 // Wrap view internal methods so that they delegate to behaviors. For example,
3403 // `onDestroy` should trigger destroy on all of the behaviors and then destroy itself.
3404 // i.e.
3405 //
3406 // `view.delegateEvents = _.partial(methods.delegateEvents, view.delegateEvents, behaviors);`
3407 wrap: function(view, behaviors, methodNames) {
3408 _.each(methodNames, function(methodName) {
3409 view[methodName] = _.partial(methods[methodName], view[methodName], behaviors);
3410 });
3411 }
3412 });
3413
3414 // Class to build handlers for `triggers` on behaviors
3415 // for views
3416 function BehaviorTriggersBuilder(view, behaviors) {
3417 this._view = view;
3418 this._behaviors = behaviors;
3419 this._triggers = {};
3420 }
3421
3422 _.extend(BehaviorTriggersBuilder.prototype, {
3423 // Main method to build the triggers hash with event keys and handlers
3424 buildBehaviorTriggers: function() {
3425 _.each(this._behaviors, this._buildTriggerHandlersForBehavior, this);
3426 return this._triggers;
3427 },
3428
3429 // Internal method to build all trigger handlers for a given behavior
3430 _buildTriggerHandlersForBehavior: function(behavior, i) {
3431 var triggersHash = _.clone(_.result(behavior, 'triggers')) || {};
3432
3433 triggersHash = Marionette.normalizeUIKeys(triggersHash, getBehaviorsUI(behavior));
3434
3435 _.each(triggersHash, _.bind(this._setHandlerForBehavior, this, behavior, i));
3436 },
3437
3438 // Internal method to create and assign the trigger handler for a given
3439 // behavior
3440 _setHandlerForBehavior: function(behavior, i, eventName, trigger) {
3441 // Unique identifier for the `this._triggers` hash
3442 var triggerKey = trigger.replace(/^\S+/, function(triggerName) {
3443 return triggerName + '.' + 'behaviortriggers' + i;
3444 });
3445
3446 this._triggers[triggerKey] = this._view._buildViewTrigger(eventName);
3447 }
3448 });
3449
3450 function getBehaviorsUI(behavior) {
3451 return behavior._uiBindings || behavior.ui;
3452 }
3453
3454 return Behaviors;
3455
3456 })(Marionette, _);
3457
3458
3459 // App Router
3460 // ----------
3461
3462 // Reduce the boilerplate code of handling route events
3463 // and then calling a single method on another object.
3464 // Have your routers configured to call the method on
3465 // your object, directly.
3466 //
3467 // Configure an AppRouter with `appRoutes`.
3468 //
3469 // App routers can only take one `controller` object.
3470 // It is recommended that you divide your controller
3471 // objects in to smaller pieces of related functionality
3472 // and have multiple routers / controllers, instead of
3473 // just one giant router and controller.
3474 //
3475 // You can also add standard routes to an AppRouter.
3476
3477 Marionette.AppRouter = Backbone.Router.extend({
3478
3479 constructor: function(options) {
3480 this.options = options || {};
3481
3482 Backbone.Router.apply(this, arguments);
3483
3484 var appRoutes = this.getOption('appRoutes');
3485 var controller = this._getController();
3486 this.processAppRoutes(controller, appRoutes);
3487 this.on('route', this._processOnRoute, this);
3488 },
3489
3490 // Similar to route method on a Backbone Router but
3491 // method is called on the controller
3492 appRoute: function(route, methodName) {
3493 var controller = this._getController();
3494 this._addAppRoute(controller, route, methodName);
3495 },
3496
3497 // process the route event and trigger the onRoute
3498 // method call, if it exists
3499 _processOnRoute: function(routeName, routeArgs) {
3500 // make sure an onRoute before trying to call it
3501 if (_.isFunction(this.onRoute)) {
3502 // find the path that matches the current route
3503 var routePath = _.invert(this.getOption('appRoutes'))[routeName];
3504 this.onRoute(routeName, routePath, routeArgs);
3505 }
3506 },
3507
3508 // Internal method to process the `appRoutes` for the
3509 // router, and turn them in to routes that trigger the
3510 // specified method on the specified `controller`.
3511 processAppRoutes: function(controller, appRoutes) {
3512 if (!appRoutes) { return; }
3513
3514 var routeNames = _.keys(appRoutes).reverse(); // Backbone requires reverted order of routes
3515
3516 _.each(routeNames, function(route) {
3517 this._addAppRoute(controller, route, appRoutes[route]);
3518 }, this);
3519 },
3520
3521 _getController: function() {
3522 return this.getOption('controller');
3523 },
3524
3525 _addAppRoute: function(controller, route, methodName) {
3526 var method = controller[methodName];
3527
3528 if (!method) {
3529 throw new Marionette.Error('Method "' + methodName + '" was not found on the controller');
3530 }
3531
3532 this.route(route, methodName, _.bind(method, controller));
3533 },
3534
3535 mergeOptions: Marionette.mergeOptions,
3536
3537 // Proxy `getOption` to enable getting options from this or this.options by name.
3538 getOption: Marionette.proxyGetOption,
3539
3540 triggerMethod: Marionette.triggerMethod,
3541
3542 bindEntityEvents: Marionette.proxyBindEntityEvents,
3543
3544 unbindEntityEvents: Marionette.proxyUnbindEntityEvents
3545 });
3546
3547 // Application
3548 // -----------
3549
3550 // Contain and manage the composite application as a whole.
3551 // Stores and starts up `Region` objects, includes an
3552 // event aggregator as `app.vent`
3553 Marionette.Application = Marionette.Object.extend({
3554 constructor: function(options) {
3555 this._initializeRegions(options);
3556 this._initCallbacks = new Marionette.Callbacks();
3557 this.submodules = {};
3558 _.extend(this, options);
3559 this._initChannel();
3560 Marionette.Object.apply(this, arguments);
3561 },
3562
3563 // Command execution, facilitated by Backbone.Wreqr.Commands
3564 execute: function() {
3565 this.commands.execute.apply(this.commands, arguments);
3566 },
3567
3568 // Request/response, facilitated by Backbone.Wreqr.RequestResponse
3569 request: function() {
3570 return this.reqres.request.apply(this.reqres, arguments);
3571 },
3572
3573 // Add an initializer that is either run at when the `start`
3574 // method is called, or run immediately if added after `start`
3575 // has already been called.
3576 addInitializer: function(initializer) {
3577 this._initCallbacks.add(initializer);
3578 },
3579
3580 // kick off all of the application's processes.
3581 // initializes all of the regions that have been added
3582 // to the app, and runs all of the initializer functions
3583 start: function(options) {
3584 this.triggerMethod('before:start', options);
3585 this._initCallbacks.run(options, this);
3586 this.triggerMethod('start', options);
3587 },
3588
3589 // Add regions to your app.
3590 // Accepts a hash of named strings or Region objects
3591 // addRegions({something: "#someRegion"})
3592 // addRegions({something: Region.extend({el: "#someRegion"}) });
3593 addRegions: function(regions) {
3594 return this._regionManager.addRegions(regions);
3595 },
3596
3597 // Empty all regions in the app, without removing them
3598 emptyRegions: function() {
3599 return this._regionManager.emptyRegions();
3600 },
3601
3602 // Removes a region from your app, by name
3603 // Accepts the regions name
3604 // removeRegion('myRegion')
3605 removeRegion: function(region) {
3606 return this._regionManager.removeRegion(region);
3607 },
3608
3609 // Provides alternative access to regions
3610 // Accepts the region name
3611 // getRegion('main')
3612 getRegion: function(region) {
3613 return this._regionManager.get(region);
3614 },
3615
3616 // Get all the regions from the region manager
3617 getRegions: function() {
3618 return this._regionManager.getRegions();
3619 },
3620
3621 // Create a module, attached to the application
3622 module: function(moduleNames, moduleDefinition) {
3623
3624 // Overwrite the module class if the user specifies one
3625 var ModuleClass = Marionette.Module.getClass(moduleDefinition);
3626
3627 var args = _.toArray(arguments);
3628 args.unshift(this);
3629
3630 // see the Marionette.Module object for more information
3631 return ModuleClass.create.apply(ModuleClass, args);
3632 },
3633
3634 // Enable easy overriding of the default `RegionManager`
3635 // for customized region interactions and business-specific
3636 // view logic for better control over single regions.
3637 getRegionManager: function() {
3638 return new Marionette.RegionManager();
3639 },
3640
3641 // Internal method to initialize the regions that have been defined in a
3642 // `regions` attribute on the application instance
3643 _initializeRegions: function(options) {
3644 var regions = _.isFunction(this.regions) ? this.regions(options) : this.regions || {};
3645
3646 this._initRegionManager();
3647
3648 // Enable users to define `regions` in instance options.
3649 var optionRegions = Marionette.getOption(options, 'regions');
3650
3651 // Enable region options to be a function
3652 if (_.isFunction(optionRegions)) {
3653 optionRegions = optionRegions.call(this, options);
3654 }
3655
3656 // Overwrite current regions with those passed in options
3657 _.extend(regions, optionRegions);
3658
3659 this.addRegions(regions);
3660
3661 return this;
3662 },
3663
3664 // Internal method to set up the region manager
3665 _initRegionManager: function() {
3666 this._regionManager = this.getRegionManager();
3667 this._regionManager._parent = this;
3668
3669 this.listenTo(this._regionManager, 'before:add:region', function() {
3670 Marionette._triggerMethod(this, 'before:add:region', arguments);
3671 });
3672
3673 this.listenTo(this._regionManager, 'add:region', function(name, region) {
3674 this[name] = region;
3675 Marionette._triggerMethod(this, 'add:region', arguments);
3676 });
3677
3678 this.listenTo(this._regionManager, 'before:remove:region', function() {
3679 Marionette._triggerMethod(this, 'before:remove:region', arguments);
3680 });
3681
3682 this.listenTo(this._regionManager, 'remove:region', function(name) {
3683 delete this[name];
3684 Marionette._triggerMethod(this, 'remove:region', arguments);
3685 });
3686 },
3687
3688 // Internal method to setup the Wreqr.radio channel
3689 _initChannel: function() {
3690 this.channelName = _.result(this, 'channelName') || 'global';
3691 this.channel = _.result(this, 'channel') || Backbone.Wreqr.radio.channel(this.channelName);
3692 this.vent = _.result(this, 'vent') || this.channel.vent;
3693 this.commands = _.result(this, 'commands') || this.channel.commands;
3694 this.reqres = _.result(this, 'reqres') || this.channel.reqres;
3695 }
3696 });
3697
3698 /* jshint maxparams: 9 */
3699
3700 // Module
3701 // ------
3702
3703 // A simple module system, used to create privacy and encapsulation in
3704 // Marionette applications
3705 Marionette.Module = function(moduleName, app, options) {
3706 this.moduleName = moduleName;
3707 this.options = _.extend({}, this.options, options);
3708 // Allow for a user to overide the initialize
3709 // for a given module instance.
3710 this.initialize = options.initialize || this.initialize;
3711
3712 // Set up an internal store for sub-modules.
3713 this.submodules = {};
3714
3715 this._setupInitializersAndFinalizers();
3716
3717 // Set an internal reference to the app
3718 // within a module.
3719 this.app = app;
3720
3721 if (_.isFunction(this.initialize)) {
3722 this.initialize(moduleName, app, this.options);
3723 }
3724 };
3725
3726 Marionette.Module.extend = Marionette.extend;
3727
3728 // Extend the Module prototype with events / listenTo, so that the module
3729 // can be used as an event aggregator or pub/sub.
3730 _.extend(Marionette.Module.prototype, Backbone.Events, {
3731
3732 // By default modules start with their parents.
3733 startWithParent: true,
3734
3735 // Initialize is an empty function by default. Override it with your own
3736 // initialization logic when extending Marionette.Module.
3737 initialize: function() {},
3738
3739 // Initializer for a specific module. Initializers are run when the
3740 // module's `start` method is called.
3741 addInitializer: function(callback) {
3742 this._initializerCallbacks.add(callback);
3743 },
3744
3745 // Finalizers are run when a module is stopped. They are used to teardown
3746 // and finalize any variables, references, events and other code that the
3747 // module had set up.
3748 addFinalizer: function(callback) {
3749 this._finalizerCallbacks.add(callback);
3750 },
3751
3752 // Start the module, and run all of its initializers
3753 start: function(options) {
3754 // Prevent re-starting a module that is already started
3755 if (this._isInitialized) { return; }
3756
3757 // start the sub-modules (depth-first hierarchy)
3758 _.each(this.submodules, function(mod) {
3759 // check to see if we should start the sub-module with this parent
3760 if (mod.startWithParent) {
3761 mod.start(options);
3762 }
3763 });
3764
3765 // run the callbacks to "start" the current module
3766 this.triggerMethod('before:start', options);
3767
3768 this._initializerCallbacks.run(options, this);
3769 this._isInitialized = true;
3770
3771 this.triggerMethod('start', options);
3772 },
3773
3774 // Stop this module by running its finalizers and then stop all of
3775 // the sub-modules for this module
3776 stop: function() {
3777 // if we are not initialized, don't bother finalizing
3778 if (!this._isInitialized) { return; }
3779 this._isInitialized = false;
3780
3781 this.triggerMethod('before:stop');
3782
3783 // stop the sub-modules; depth-first, to make sure the
3784 // sub-modules are stopped / finalized before parents
3785 _.invoke(this.submodules, 'stop');
3786
3787 // run the finalizers
3788 this._finalizerCallbacks.run(undefined, this);
3789
3790 // reset the initializers and finalizers
3791 this._initializerCallbacks.reset();
3792 this._finalizerCallbacks.reset();
3793
3794 this.triggerMethod('stop');
3795 },
3796
3797 // Configure the module with a definition function and any custom args
3798 // that are to be passed in to the definition function
3799 addDefinition: function(moduleDefinition, customArgs) {
3800 this._runModuleDefinition(moduleDefinition, customArgs);
3801 },
3802
3803 // Internal method: run the module definition function with the correct
3804 // arguments
3805 _runModuleDefinition: function(definition, customArgs) {
3806 // If there is no definition short circut the method.
3807 if (!definition) { return; }
3808
3809 // build the correct list of arguments for the module definition
3810 var args = _.flatten([
3811 this,
3812 this.app,
3813 Backbone,
3814 Marionette,
3815 Backbone.$, _,
3816 customArgs
3817 ]);
3818
3819 definition.apply(this, args);
3820 },
3821
3822 // Internal method: set up new copies of initializers and finalizers.
3823 // Calling this method will wipe out all existing initializers and
3824 // finalizers.
3825 _setupInitializersAndFinalizers: function() {
3826 this._initializerCallbacks = new Marionette.Callbacks();
3827 this._finalizerCallbacks = new Marionette.Callbacks();
3828 },
3829
3830 // import the `triggerMethod` to trigger events with corresponding
3831 // methods if the method exists
3832 triggerMethod: Marionette.triggerMethod
3833 });
3834
3835 // Class methods to create modules
3836 _.extend(Marionette.Module, {
3837
3838 // Create a module, hanging off the app parameter as the parent object.
3839 create: function(app, moduleNames, moduleDefinition) {
3840 var module = app;
3841
3842 // get the custom args passed in after the module definition and
3843 // get rid of the module name and definition function
3844 var customArgs = _.drop(arguments, 3);
3845
3846 // Split the module names and get the number of submodules.
3847 // i.e. an example module name of `Doge.Wow.Amaze` would
3848 // then have the potential for 3 module definitions.
3849 moduleNames = moduleNames.split('.');
3850 var length = moduleNames.length;
3851
3852 // store the module definition for the last module in the chain
3853 var moduleDefinitions = [];
3854 moduleDefinitions[length - 1] = moduleDefinition;
3855
3856 // Loop through all the parts of the module definition
3857 _.each(moduleNames, function(moduleName, i) {
3858 var parentModule = module;
3859 module = this._getModule(parentModule, moduleName, app, moduleDefinition);
3860 this._addModuleDefinition(parentModule, module, moduleDefinitions[i], customArgs);
3861 }, this);
3862
3863 // Return the last module in the definition chain
3864 return module;
3865 },
3866
3867 _getModule: function(parentModule, moduleName, app, def, args) {
3868 var options = _.extend({}, def);
3869 var ModuleClass = this.getClass(def);
3870
3871 // Get an existing module of this name if we have one
3872 var module = parentModule[moduleName];
3873
3874 if (!module) {
3875 // Create a new module if we don't have one
3876 module = new ModuleClass(moduleName, app, options);
3877 parentModule[moduleName] = module;
3878 // store the module on the parent
3879 parentModule.submodules[moduleName] = module;
3880 }
3881
3882 return module;
3883 },
3884
3885 // ## Module Classes
3886 //
3887 // Module classes can be used as an alternative to the define pattern.
3888 // The extend function of a Module is identical to the extend functions
3889 // on other Backbone and Marionette classes.
3890 // This allows module lifecyle events like `onStart` and `onStop` to be called directly.
3891 getClass: function(moduleDefinition) {
3892 var ModuleClass = Marionette.Module;
3893
3894 if (!moduleDefinition) {
3895 return ModuleClass;
3896 }
3897
3898 // If all of the module's functionality is defined inside its class,
3899 // then the class can be passed in directly. `MyApp.module("Foo", FooModule)`.
3900 if (moduleDefinition.prototype instanceof ModuleClass) {
3901 return moduleDefinition;
3902 }
3903
3904 return moduleDefinition.moduleClass || ModuleClass;
3905 },
3906
3907 // Add the module definition and add a startWithParent initializer function.
3908 // This is complicated because module definitions are heavily overloaded
3909 // and support an anonymous function, module class, or options object
3910 _addModuleDefinition: function(parentModule, module, def, args) {
3911 var fn = this._getDefine(def);
3912 var startWithParent = this._getStartWithParent(def, module);
3913
3914 if (fn) {
3915 module.addDefinition(fn, args);
3916 }
3917
3918 this._addStartWithParent(parentModule, module, startWithParent);
3919 },
3920
3921 _getStartWithParent: function(def, module) {
3922 var swp;
3923
3924 if (_.isFunction(def) && (def.prototype instanceof Marionette.Module)) {
3925 swp = module.constructor.prototype.startWithParent;
3926 return _.isUndefined(swp) ? true : swp;
3927 }
3928
3929 if (_.isObject(def)) {
3930 swp = def.startWithParent;
3931 return _.isUndefined(swp) ? true : swp;
3932 }
3933
3934 return true;
3935 },
3936
3937 _getDefine: function(def) {
3938 if (_.isFunction(def) && !(def.prototype instanceof Marionette.Module)) {
3939 return def;
3940 }
3941
3942 if (_.isObject(def)) {
3943 return def.define;
3944 }
3945
3946 return null;
3947 },
3948
3949 _addStartWithParent: function(parentModule, module, startWithParent) {
3950 module.startWithParent = module.startWithParent && startWithParent;
3951
3952 if (!module.startWithParent || !!module.startWithParentIsConfigured) {
3953 return;
3954 }
3955
3956 module.startWithParentIsConfigured = true;
3957
3958 parentModule.addInitializer(function(options) {
3959 if (module.startWithParent) {
3960 module.start(options);
3961 }
3962 });
3963 }
3964 });
3965
3966
3967 return Marionette;
3968 }));
3969