js_stack 1.3.1 → 1.4.0

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