perkins 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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, _);