perkins 0.0.1 → 0.0.2

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