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