diff options
| author | mo khan <mo@mokhan.ca> | 2014-06-25 21:56:57 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2014-06-25 21:57:11 -0600 |
| commit | 241f8161d34bbd815db54e3d106186a3fadcbb71 (patch) | |
| tree | 678f33abecc7e1ff1a25c38666dde131eb25da62 | |
| parent | 634357ed961d39e9ea0cb4a996afd69a59ea1ad2 (diff) | |
install backbone marionette.
| -rw-r--r-- | app/assets/javascripts/application.js | 1 | ||||
| -rw-r--r-- | vendor/assets/javascripts/backbone.marionette.js | 3253 |
2 files changed, 3254 insertions, 0 deletions
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 3085f831..ef9a80fe 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -27,6 +27,7 @@ //= require backbone //= require backbone_rails_sync //= require backbone_datalink +//= require backbone.marionette //= require backbone/cake //= require backbone-model-file-upload //= require_tree . diff --git a/vendor/assets/javascripts/backbone.marionette.js b/vendor/assets/javascripts/backbone.marionette.js new file mode 100644 index 00000000..d1ebfdf6 --- /dev/null +++ b/vendor/assets/javascripts/backbone.marionette.js @@ -0,0 +1,3253 @@ +// MarionetteJS (Backbone.Marionette) +// ---------------------------------- +// v2.0.1 +// +// Copyright (c)2014 Derick Bailey, Muted Solutions, LLC. +// Distributed under MIT license +// +// http://marionettejs.com + + +/*! + * Includes BabySitter + * https://github.com/marionettejs/backbone.babysitter/ + * + * Includes Wreqr + * https://github.com/marionettejs/backbone.wreqr/ + */ + + +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define(['backbone', 'underscore'], function(Backbone, _) { + return (root.Marionette = factory(root, Backbone, _)); + }); + } else if (typeof exports !== 'undefined') { + var Backbone = require('backbone'); + var _ = require('underscore'); + module.exports = factory(root, Backbone, _); + } else { + root.Marionette = factory(root, root.Backbone, root._); + } + +}(this, function(root, Backbone, _) { + 'use strict'; + + // Backbone.BabySitter + // ------------------- + // v0.1.4 + // + // Copyright (c)2014 Derick Bailey, Muted Solutions, LLC. + // Distributed under MIT license + // + // http://github.com/marionettejs/backbone.babysitter + (function(Backbone, _) { + "use strict"; + var previousChildViewContainer = Backbone.ChildViewContainer; + // BabySitter.ChildViewContainer + // ----------------------------- + // + // Provide a container to store, retrieve and + // shut down child views. + Backbone.ChildViewContainer = function(Backbone, _) { + // Container Constructor + // --------------------- + var Container = function(views) { + this._views = {}; + this._indexByModel = {}; + this._indexByCustom = {}; + this._updateLength(); + _.each(views, this.add, this); + }; + // Container Methods + // ----------------- + _.extend(Container.prototype, { + // Add a view to this container. Stores the view + // by `cid` and makes it searchable by the model + // cid (and model itself). Optionally specify + // a custom key to store an retrieve the view. + add: function(view, customIndex) { + var viewCid = view.cid; + // store the view + this._views[viewCid] = view; + // index it by model + if (view.model) { + this._indexByModel[view.model.cid] = viewCid; + } + // index by custom + if (customIndex) { + this._indexByCustom[customIndex] = viewCid; + } + this._updateLength(); + return this; + }, + // Find a view by the model that was attached to + // it. Uses the model's `cid` to find it. + findByModel: function(model) { + return this.findByModelCid(model.cid); + }, + // Find a view by the `cid` of the model that was attached to + // it. Uses the model's `cid` to find the view `cid` and + // retrieve the view using it. + findByModelCid: function(modelCid) { + var viewCid = this._indexByModel[modelCid]; + return this.findByCid(viewCid); + }, + // Find a view by a custom indexer. + findByCustom: function(index) { + var viewCid = this._indexByCustom[index]; + return this.findByCid(viewCid); + }, + // Find by index. This is not guaranteed to be a + // stable index. + findByIndex: function(index) { + return _.values(this._views)[index]; + }, + // retrieve a view by its `cid` directly + findByCid: function(cid) { + return this._views[cid]; + }, + // Remove a view + remove: function(view) { + var viewCid = view.cid; + // delete model index + if (view.model) { + delete this._indexByModel[view.model.cid]; + } + // delete custom index + _.any(this._indexByCustom, function(cid, key) { + if (cid === viewCid) { + delete this._indexByCustom[key]; + return true; + } + }, this); + // remove the view from the container + delete this._views[viewCid]; + // update the length + this._updateLength(); + return this; + }, + // Call a method on every view in the container, + // passing parameters to the call method one at a + // time, like `function.call`. + call: function(method) { + this.apply(method, _.tail(arguments)); + }, + // Apply a method on every view in the container, + // passing parameters to the call method one at a + // time, like `function.apply`. + apply: function(method, args) { + _.each(this._views, function(view) { + if (_.isFunction(view[method])) { + view[method].apply(view, args || []); + } + }); + }, + // Update the `.length` attribute on this container + _updateLength: function() { + this.length = _.size(this._views); + } + }); + // Borrowing this code from Backbone.Collection: + // http://backbonejs.org/docs/backbone.html#section-106 + // + // Mix in methods from Underscore, for iteration, and other + // collection related features. + 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" ]; + _.each(methods, function(method) { + Container.prototype[method] = function() { + var views = _.values(this._views); + var args = [ views ].concat(_.toArray(arguments)); + return _[method].apply(_, args); + }; + }); + // return the public API + return Container; + }(Backbone, _); + Backbone.ChildViewContainer.VERSION = "0.1.4"; + Backbone.ChildViewContainer.noConflict = function() { + Backbone.ChildViewContainer = previousChildViewContainer; + return this; + }; + return Backbone.ChildViewContainer; + })(Backbone, _); + // Backbone.Wreqr (Backbone.Marionette) + // ---------------------------------- + // v1.3.1 + // + // Copyright (c)2014 Derick Bailey, Muted Solutions, LLC. + // Distributed under MIT license + // + // http://github.com/marionettejs/backbone.wreqr + (function(Backbone, _) { + "use strict"; + var previousWreqr = Backbone.Wreqr; + var Wreqr = Backbone.Wreqr = {}; + Backbone.Wreqr.VERSION = "1.3.1"; + Backbone.Wreqr.noConflict = function() { + Backbone.Wreqr = previousWreqr; + return this; + }; + // Handlers + // -------- + // A registry of functions to call, given a name + Wreqr.Handlers = function(Backbone, _) { + "use strict"; + // Constructor + // ----------- + var Handlers = function(options) { + this.options = options; + this._wreqrHandlers = {}; + if (_.isFunction(this.initialize)) { + this.initialize(options); + } + }; + Handlers.extend = Backbone.Model.extend; + // Instance Members + // ---------------- + _.extend(Handlers.prototype, Backbone.Events, { + // Add multiple handlers using an object literal configuration + setHandlers: function(handlers) { + _.each(handlers, function(handler, name) { + var context = null; + if (_.isObject(handler) && !_.isFunction(handler)) { + context = handler.context; + handler = handler.callback; + } + this.setHandler(name, handler, context); + }, this); + }, + // Add a handler for the given name, with an + // optional context to run the handler within + setHandler: function(name, handler, context) { + var config = { + callback: handler, + context: context + }; + this._wreqrHandlers[name] = config; + this.trigger("handler:add", name, handler, context); + }, + // Determine whether or not a handler is registered + hasHandler: function(name) { + return !!this._wreqrHandlers[name]; + }, + // Get the currently registered handler for + // the specified name. Throws an exception if + // no handler is found. + getHandler: function(name) { + var config = this._wreqrHandlers[name]; + if (!config) { + return; + } + return function() { + var args = Array.prototype.slice.apply(arguments); + return config.callback.apply(config.context, args); + }; + }, + // Remove a handler for the specified name + removeHandler: function(name) { + delete this._wreqrHandlers[name]; + }, + // Remove all handlers from this registry + removeAllHandlers: function() { + this._wreqrHandlers = {}; + } + }); + return Handlers; + }(Backbone, _); + // Wreqr.CommandStorage + // -------------------- + // + // Store and retrieve commands for execution. + Wreqr.CommandStorage = function() { + "use strict"; + // Constructor function + var CommandStorage = function(options) { + this.options = options; + this._commands = {}; + if (_.isFunction(this.initialize)) { + this.initialize(options); + } + }; + // Instance methods + _.extend(CommandStorage.prototype, Backbone.Events, { + // Get an object literal by command name, that contains + // the `commandName` and the `instances` of all commands + // represented as an array of arguments to process + getCommands: function(commandName) { + var commands = this._commands[commandName]; + // we don't have it, so add it + if (!commands) { + // build the configuration + commands = { + command: commandName, + instances: [] + }; + // store it + this._commands[commandName] = commands; + } + return commands; + }, + // Add a command by name, to the storage and store the + // args for the command + addCommand: function(commandName, args) { + var command = this.getCommands(commandName); + command.instances.push(args); + }, + // Clear all commands for the given `commandName` + clearCommands: function(commandName) { + var command = this.getCommands(commandName); + command.instances = []; + } + }); + return CommandStorage; + }(); + // Wreqr.Commands + // -------------- + // + // A simple command pattern implementation. Register a command + // handler and execute it. + Wreqr.Commands = function(Wreqr) { + "use strict"; + return Wreqr.Handlers.extend({ + // default storage type + storageType: Wreqr.CommandStorage, + constructor: function(options) { + this.options = options || {}; + this._initializeStorage(this.options); + this.on("handler:add", this._executeCommands, this); + var args = Array.prototype.slice.call(arguments); + Wreqr.Handlers.prototype.constructor.apply(this, args); + }, + // Execute a named command with the supplied args + execute: function(name, args) { + name = arguments[0]; + args = Array.prototype.slice.call(arguments, 1); + if (this.hasHandler(name)) { + this.getHandler(name).apply(this, args); + } else { + this.storage.addCommand(name, args); + } + }, + // Internal method to handle bulk execution of stored commands + _executeCommands: function(name, handler, context) { + var command = this.storage.getCommands(name); + // loop through and execute all the stored command instances + _.each(command.instances, function(args) { + handler.apply(context, args); + }); + this.storage.clearCommands(name); + }, + // Internal method to initialize storage either from the type's + // `storageType` or the instance `options.storageType`. + _initializeStorage: function(options) { + var storage; + var StorageType = options.storageType || this.storageType; + if (_.isFunction(StorageType)) { + storage = new StorageType(); + } else { + storage = StorageType; + } + this.storage = storage; + } + }); + }(Wreqr); + // Wreqr.RequestResponse + // --------------------- + // + // A simple request/response implementation. Register a + // request handler, and return a response from it + Wreqr.RequestResponse = function(Wreqr) { + "use strict"; + return Wreqr.Handlers.extend({ + request: function() { + var name = arguments[0]; + var args = Array.prototype.slice.call(arguments, 1); + if (this.hasHandler(name)) { + return this.getHandler(name).apply(this, args); + } + } + }); + }(Wreqr); + // Event Aggregator + // ---------------- + // A pub-sub object that can be used to decouple various parts + // of an application through event-driven architecture. + Wreqr.EventAggregator = function(Backbone, _) { + "use strict"; + var EA = function() {}; + // Copy the `extend` function used by Backbone's classes + EA.extend = Backbone.Model.extend; + // Copy the basic Backbone.Events on to the event aggregator + _.extend(EA.prototype, Backbone.Events); + return EA; + }(Backbone, _); + // Wreqr.Channel + // -------------- + // + // An object that wraps the three messaging systems: + // EventAggregator, RequestResponse, Commands + Wreqr.Channel = function(Wreqr) { + "use strict"; + var Channel = function(channelName) { + this.vent = new Backbone.Wreqr.EventAggregator(); + this.reqres = new Backbone.Wreqr.RequestResponse(); + this.commands = new Backbone.Wreqr.Commands(); + this.channelName = channelName; + }; + _.extend(Channel.prototype, { + // Remove all handlers from the messaging systems of this channel + reset: function() { + this.vent.off(); + this.vent.stopListening(); + this.reqres.removeAllHandlers(); + this.commands.removeAllHandlers(); + return this; + }, + // Connect a hash of events; one for each messaging system + connectEvents: function(hash, context) { + this._connect("vent", hash, context); + return this; + }, + connectCommands: function(hash, context) { + this._connect("commands", hash, context); + return this; + }, + connectRequests: function(hash, context) { + this._connect("reqres", hash, context); + return this; + }, + // Attach the handlers to a given message system `type` + _connect: function(type, hash, context) { + if (!hash) { + return; + } + context = context || this; + var method = type === "vent" ? "on" : "setHandler"; + _.each(hash, function(fn, eventName) { + this[type][method](eventName, _.bind(fn, context)); + }, this); + } + }); + return Channel; + }(Wreqr); + // Wreqr.Radio + // -------------- + // + // An object that lets you communicate with many channels. + Wreqr.radio = function(Wreqr) { + "use strict"; + var Radio = function() { + this._channels = {}; + this.vent = {}; + this.commands = {}; + this.reqres = {}; + this._proxyMethods(); + }; + _.extend(Radio.prototype, { + channel: function(channelName) { + if (!channelName) { + throw new Error("Channel must receive a name"); + } + return this._getChannel(channelName); + }, + _getChannel: function(channelName) { + var channel = this._channels[channelName]; + if (!channel) { + channel = new Wreqr.Channel(channelName); + this._channels[channelName] = channel; + } + return channel; + }, + _proxyMethods: function() { + _.each([ "vent", "commands", "reqres" ], function(system) { + _.each(messageSystems[system], function(method) { + this[system][method] = proxyMethod(this, system, method); + }, this); + }, this); + } + }); + var messageSystems = { + vent: [ "on", "off", "trigger", "once", "stopListening", "listenTo", "listenToOnce" ], + commands: [ "execute", "setHandler", "setHandlers", "removeHandler", "removeAllHandlers" ], + reqres: [ "request", "setHandler", "setHandlers", "removeHandler", "removeAllHandlers" ] + }; + var proxyMethod = function(radio, system, method) { + return function(channelName) { + var messageSystem = radio._getChannel(channelName)[system]; + var args = Array.prototype.slice.call(arguments, 1); + return messageSystem[method].apply(messageSystem, args); + }; + }; + return new Radio(); + }(Wreqr); + return Backbone.Wreqr; + })(Backbone, _); + + var previousMarionette = root.Marionette; + + var Marionette = Backbone.Marionette = {}; + + Marionette.VERSION = '2.0.1'; + + Marionette.noConflict = function() { + root.Marionette = previousMarionette; + return this; + }; + + Backbone.Marionette = Marionette; + + // Get the Deferred creator for later use + Marionette.Deferred = Backbone.$.Deferred; + + /* jshint unused: false */ + + // Helpers + // ------- + + // For slicing `arguments` in functions + var slice = Array.prototype.slice; + + function throwError(message, name) { + var error = new Error(message); + error.name = name || 'Error'; + throw error; + } + + // Marionette.extend + // ----------------- + + // Borrow the Backbone `extend` method so we can use it as needed + Marionette.extend = Backbone.Model.extend; + + // Marionette.getOption + // -------------------- + + // Retrieve an object, function or other value from a target + // object or its `options`, with `options` taking precedence. + Marionette.getOption = function(target, optionName) { + if (!target || !optionName) { return; } + var value; + + if (target.options && (target.options[optionName] !== undefined)) { + value = target.options[optionName]; + } else { + value = target[optionName]; + } + + return value; + }; + + // Proxy `Marionette.getOption` + Marionette.proxyGetOption = function(optionName) { + return Marionette.getOption(this, optionName); + }; + + // Marionette.normalizeMethods + // ---------------------- + + // Pass in a mapping of events => functions or function names + // and return a mapping of events => functions + Marionette.normalizeMethods = function(hash) { + var normalizedHash = {}, method; + _.each(hash, function(fn, name) { + method = fn; + if (!_.isFunction(method)) { + method = this[method]; + } + if (!method) { + return; + } + normalizedHash[name] = method; + }, this); + return normalizedHash; + }; + + + // allows for the use of the @ui. syntax within + // a given key for triggers and events + // swaps the @ui with the associated selector + Marionette.normalizeUIKeys = function(hash, ui) { + if (typeof(hash) === 'undefined') { + return; + } + + _.each(_.keys(hash), function(v) { + var pattern = /@ui.[a-zA-Z_$0-9]*/g; + if (v.match(pattern)) { + hash[v.replace(pattern, function(r) { + return ui[r.slice(4)]; + })] = hash[v]; + delete hash[v]; + } + }); + + return hash; + }; + + // Mix in methods from Underscore, for iteration, and other + // collection related features. + // Borrowing this code from Backbone.Collection: + // http://backbonejs.org/docs/backbone.html#section-106 + Marionette.actAsCollection = function(object, listProperty) { + 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']; + + _.each(methods, function(method) { + object[method] = function() { + var list = _.values(_.result(this, listProperty)); + var args = [list].concat(_.toArray(arguments)); + return _[method].apply(_, args); + }; + }); + }; + + // Trigger an event and/or a corresponding method name. Examples: + // + // `this.triggerMethod("foo")` will trigger the "foo" event and + // call the "onFoo" method. + // + // `this.triggerMethod("foo:bar")` will trigger the "foo:bar" event and + // call the "onFooBar" method. + Marionette.triggerMethod = (function() { + + // split the event name on the ":" + var splitter = /(^|:)(\w)/gi; + + // take the event section ("section1:section2:section3") + // and turn it in to uppercase name + function getEventName(match, prefix, eventName) { + return eventName.toUpperCase(); + } + + // actual triggerMethod implementation + var triggerMethod = function(event) { + // get the method name from the event name + var methodName = 'on' + event.replace(splitter, getEventName); + var method = this[methodName]; + var result; + + // call the onMethodName if it exists + if (_.isFunction(method)) { + // pass all arguments, except the event name + result = method.apply(this, _.tail(arguments)); + } + + // trigger the event, if a trigger method exists + if (_.isFunction(this.trigger)) { + this.trigger.apply(this, arguments); + } + + return result; + }; + + return triggerMethod; + })(); + + // DOMRefresh + // ---------- + // + // Monitor a view's state, and after it has been rendered and shown + // in the DOM, trigger a "dom:refresh" event every time it is + // re-rendered. + + Marionette.MonitorDOMRefresh = (function(documentElement) { + // track when the view has been shown in the DOM, + // using a Marionette.Region (or by other means of triggering "show") + function handleShow(view) { + view._isShown = true; + triggerDOMRefresh(view); + } + + // track when the view has been rendered + function handleRender(view) { + view._isRendered = true; + triggerDOMRefresh(view); + } + + // Trigger the "dom:refresh" event and corresponding "onDomRefresh" method + function triggerDOMRefresh(view) { + if (view._isShown && view._isRendered && isInDOM(view)) { + if (_.isFunction(view.triggerMethod)) { + view.triggerMethod('dom:refresh'); + } + } + } + + function isInDOM(view) { + return documentElement.contains(view.el); + } + + // Export public API + return function(view) { + view.listenTo(view, 'show', function() { + handleShow(view); + }); + + view.listenTo(view, 'render', function() { + handleRender(view); + }); + }; + })(document.documentElement); + + + /* jshint maxparams: 5 */ + + // Marionette.bindEntityEvents & unbindEntityEvents + // --------------------------- + // + // These methods are used to bind/unbind a backbone "entity" (collection/model) + // to methods on a target object. + // + // The first parameter, `target`, must have a `listenTo` method from the + // EventBinder object. + // + // The second parameter is the entity (Backbone.Model or Backbone.Collection) + // to bind the events from. + // + // The third parameter is a hash of { "event:name": "eventHandler" } + // configuration. Multiple handlers can be separated by a space. A + // function can be supplied instead of a string handler name. + + (function(Marionette) { + 'use strict'; + + // Bind the event to handlers specified as a string of + // handler names on the target object + function bindFromStrings(target, entity, evt, methods) { + var methodNames = methods.split(/\s+/); + + _.each(methodNames, function(methodName) { + + var method = target[methodName]; + if (!method) { + throwError('Method "' + methodName + + '" was configured as an event handler, but does not exist.'); + } + + target.listenTo(entity, evt, method); + }); + } + + // Bind the event to a supplied callback function + function bindToFunction(target, entity, evt, method) { + target.listenTo(entity, evt, method); + } + + // Bind the event to handlers specified as a string of + // handler names on the target object + function unbindFromStrings(target, entity, evt, methods) { + var methodNames = methods.split(/\s+/); + + _.each(methodNames, function(methodName) { + var method = target[methodName]; + target.stopListening(entity, evt, method); + }); + } + + // Bind the event to a supplied callback function + function unbindToFunction(target, entity, evt, method) { + target.stopListening(entity, evt, method); + } + + + // generic looping function + function iterateEvents(target, entity, bindings, functionCallback, stringCallback) { + if (!entity || !bindings) { return; } + + // allow the bindings to be a function + if (_.isFunction(bindings)) { + bindings = bindings.call(target); + } + + // iterate the bindings and bind them + _.each(bindings, function(methods, evt) { + + // allow for a function as the handler, + // or a list of event names as a string + if (_.isFunction(methods)) { + functionCallback(target, entity, evt, methods); + } else { + stringCallback(target, entity, evt, methods); + } + + }); + } + + // Export Public API + Marionette.bindEntityEvents = function(target, entity, bindings) { + iterateEvents(target, entity, bindings, bindToFunction, bindFromStrings); + }; + + Marionette.unbindEntityEvents = function(target, entity, bindings) { + iterateEvents(target, entity, bindings, unbindToFunction, unbindFromStrings); + }; + + // Proxy `bindEntityEvents` + Marionette.proxyBindEntityEvents = function(entity, bindings) { + return Marionette.bindEntityEvents(this, entity, bindings); + }; + + // Proxy `unbindEntityEvents` + Marionette.proxyUnbindEntityEvents = function(entity, bindings) { + return Marionette.unbindEntityEvents(this, entity, bindings); + }; + })(Marionette); + + + // Callbacks + // --------- + + // A simple way of managing a collection of callbacks + // and executing them at a later point in time, using jQuery's + // `Deferred` object. + Marionette.Callbacks = function() { + this._deferred = Marionette.Deferred(); + this._callbacks = []; + }; + + _.extend(Marionette.Callbacks.prototype, { + + // Add a callback to be executed. Callbacks added here are + // guaranteed to execute, even if they are added after the + // `run` method is called. + add: function(callback, contextOverride) { + var promise = _.result(this._deferred, 'promise'); + + this._callbacks.push({cb: callback, ctx: contextOverride}); + + promise.then(function(args) { + if (contextOverride){ args.context = contextOverride; } + callback.call(args.context, args.options); + }); + }, + + // Run all registered callbacks with the context specified. + // Additional callbacks can be added after this has been run + // and they will still be executed. + run: function(options, context) { + this._deferred.resolve({ + options: options, + context: context + }); + }, + + // Resets the list of callbacks to be run, allowing the same list + // to be run multiple times - whenever the `run` method is called. + reset: function() { + var callbacks = this._callbacks; + this._deferred = Marionette.Deferred(); + this._callbacks = []; + + _.each(callbacks, function(cb) { + this.add(cb.cb, cb.ctx); + }, this); + } + }); + + // Marionette Controller + // --------------------- + // + // A multi-purpose object to use as a controller for + // modules and routers, and as a mediator for workflow + // and coordination of other objects, views, and more. + Marionette.Controller = function(options) { + this.triggerMethod = Marionette.triggerMethod; + this.options = options || {}; + + if (_.isFunction(this.initialize)) { + this.initialize(this.options); + } + }; + + Marionette.Controller.extend = Marionette.extend; + + // Controller Methods + // -------------- + + // Ensure it can trigger events with Backbone.Events + _.extend(Marionette.Controller.prototype, Backbone.Events, { + destroy: function() { + var args = Array.prototype.slice.call(arguments); + this.triggerMethod.apply(this, ['before:destroy'].concat(args)); + this.triggerMethod.apply(this, ['destroy'].concat(args)); + + this.stopListening(); + this.off(); + }, + + // import the `triggerMethod` to trigger events with corresponding + // methods if the method exists + triggerMethod: Marionette.triggerMethod, + + // Proxy `getOption` to enable getting options from this or this.options by name. + getOption: Marionette.proxyGetOption + + }); + + /* jshint maxcomplexity: 10, maxstatements: 27 */ + + // Region + // ------ + // + // Manage the visual regions of your composite application. See + // http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/ + + Marionette.Region = function(options) { + this.options = options || {}; + this.el = this.getOption('el'); + + // Handle when this.el is passed in as a $ wrapped element. + this.el = this.el instanceof Backbone.$ ? this.el[0] : this.el; + + if (!this.el) { + throwError('An "el" must be specified for a region.', 'NoElError'); + } + + this.$el = this.getEl(this.el); + + if (this.initialize) { + var args = Array.prototype.slice.apply(arguments); + this.initialize.apply(this, args); + } + }; + + + // Region Class methods + // ------------------- + + _.extend(Marionette.Region, { + + // Build an instance of a region by passing in a configuration object + // and a default region class to use if none is specified in the config. + // + // The config object should either be a string as a jQuery DOM selector, + // a Region class directly, or an object literal that specifies both + // a selector and regionClass: + // + // ```js + // { + // selector: "#foo", + // regionClass: MyCustomRegion + // } + // ``` + // + buildRegion: function(regionConfig, defaultRegionClass) { + var regionIsString = _.isString(regionConfig); + var regionSelectorIsString = _.isString(regionConfig.selector); + var regionClassIsUndefined = _.isUndefined(regionConfig.regionClass); + var regionIsClass = _.isFunction(regionConfig); + + if (!regionIsClass && !regionIsString && !regionSelectorIsString) { + throwError('Region must be specified as a Region class,' + + 'a selector string or an object with selector property'); + } + + var selector, RegionClass; + + // get the selector for the region + + if (regionIsString) { + selector = regionConfig; + } + + if (regionConfig.selector) { + selector = regionConfig.selector; + delete regionConfig.selector; + } + + // get the class for the region + + if (regionIsClass) { + RegionClass = regionConfig; + } + + if (!regionIsClass && regionClassIsUndefined) { + RegionClass = defaultRegionClass; + } + + if (regionConfig.regionClass) { + RegionClass = regionConfig.regionClass; + delete regionConfig.regionClass; + } + + if (regionIsString || regionIsClass) { + regionConfig = {}; + } + + regionConfig.el = selector; + + // build the region instance + var region = new RegionClass(regionConfig); + + // override the `getEl` function if we have a parentEl + // this must be overridden to ensure the selector is found + // on the first use of the region. if we try to assign the + // region's `el` to `parentEl.find(selector)` in the object + // literal to build the region, the element will not be + // guaranteed to be in the DOM already, and will cause problems + if (regionConfig.parentEl) { + region.getEl = function(el) { + if (_.isObject(el)) { + return Backbone.$(el); + } + var parentEl = regionConfig.parentEl; + if (_.isFunction(parentEl)) { + parentEl = parentEl(); + } + return parentEl.find(el); + }; + } + + return region; + } + + }); + + // Region Instance Methods + // ----------------------- + + _.extend(Marionette.Region.prototype, Backbone.Events, { + + // Displays a backbone view instance inside of the region. + // Handles calling the `render` method for you. Reads content + // directly from the `el` attribute. Also calls an optional + // `onShow` and `onDestroy` method on your view, just after showing + // or just before destroying the view, respectively. + // The `preventDestroy` option can be used to prevent a view from + // the old view being destroyed on show. + // The `forceShow` option can be used to force a view to be + // re-rendered if it's already shown in the region. + + show: function(view, options){ + this._ensureElement(); + + var showOptions = options || {}; + var isDifferentView = view !== this.currentView; + var preventDestroy = !!showOptions.preventDestroy; + var forceShow = !!showOptions.forceShow; + + // we are only changing the view if there is a view to change to begin with + var isChangingView = !!this.currentView; + + // only destroy the view if we don't want to preventDestroy and the view is different + var _shouldDestroyView = !preventDestroy && isDifferentView; + + if (_shouldDestroyView) { + this.empty(); + } + + // show the view if the view is different or if you want to re-show the view + var _shouldShowView = isDifferentView || forceShow; + + if (_shouldShowView) { + view.render(); + + if (isChangingView) { + this.triggerMethod('before:swap', view); + } + + this.triggerMethod('before:show', view); + this.triggerMethod.call(view, 'before:show'); + + this.attachHtml(view); + this.currentView = view; + + if (isChangingView) { + this.triggerMethod('swap', view); + } + + this.triggerMethod('show', view); + + if (_.isFunction(view.triggerMethod)) { + view.triggerMethod('show'); + } else { + this.triggerMethod.call(view, 'show'); + } + + return this; + } + + return this; + }, + + _ensureElement: function(){ + if (!_.isObject(this.el)) { + this.$el = this.getEl(this.el); + this.el = this.$el[0]; + } + + if (!this.$el || this.$el.length === 0) { + throwError('An "el" ' + this.$el.selector + ' must exist in DOM'); + } + }, + + // Override this method to change how the region finds the + // DOM element that it manages. Return a jQuery selector object. + getEl: function(el) { + return Backbone.$(el); + }, + + // Override this method to change how the new view is + // appended to the `$el` that the region is managing + attachHtml: function(view) { + // empty the node and append new view + this.el.innerHTML=''; + this.el.appendChild(view.el); + }, + + // Destroy the current view, if there is one. If there is no + // current view, it does nothing and returns immediately. + empty: function() { + var view = this.currentView; + if (!view || view.isDestroyed) { return; } + + this.triggerMethod('before:empty', view); + + // call 'destroy' or 'remove', depending on which is found + if (view.destroy) { view.destroy(); } + else if (view.remove) { view.remove(); } + + this.triggerMethod('empty', view); + + delete this.currentView; + }, + + // Attach an existing view to the region. This + // will not call `render` or `onShow` for the new view, + // and will not replace the current HTML for the `el` + // of the region. + attachView: function(view) { + this.currentView = view; + }, + + // Reset the region by destroying any existing view and + // clearing out the cached `$el`. The next time a view + // is shown via this region, the region will re-query the + // DOM for the region's `el`. + reset: function() { + this.empty(); + + if (this.$el) { + this.el = this.$el.selector; + } + + delete this.$el; + }, + + // Proxy `getOption` to enable getting options from this or this.options by name. + getOption: Marionette.proxyGetOption, + + // import the `triggerMethod` to trigger events with corresponding + // methods if the method exists + triggerMethod: Marionette.triggerMethod + }); + + // Copy the `extend` function used by Backbone's classes + Marionette.Region.extend = Marionette.extend; + + // Marionette.RegionManager + // ------------------------ + // + // Manage one or more related `Marionette.Region` objects. + Marionette.RegionManager = (function(Marionette) { + + var RegionManager = Marionette.Controller.extend({ + constructor: function(options) { + this._regions = {}; + Marionette.Controller.call(this, options); + }, + + // Add multiple regions using an object literal, where + // each key becomes the region name, and each value is + // the region definition. + addRegions: function(regionDefinitions, defaults) { + var regions = {}; + + _.each(regionDefinitions, function(definition, name) { + if (_.isString(definition)) { + definition = {selector: definition}; + } + + if (definition.selector) { + definition = _.defaults({}, definition, defaults); + } + + var region = this.addRegion(name, definition); + regions[name] = region; + }, this); + + return regions; + }, + + // Add an individual region to the region manager, + // and return the region instance + addRegion: function(name, definition) { + var region; + + var isObject = _.isObject(definition); + var isString = _.isString(definition); + var hasSelector = !!definition.selector; + + if (isString || (isObject && hasSelector)) { + region = Marionette.Region.buildRegion(definition, Marionette.Region); + } else if (_.isFunction(definition)) { + region = Marionette.Region.buildRegion(definition, Marionette.Region); + } else { + region = definition; + } + + this.triggerMethod('before:add:region', name, region); + + this._store(name, region); + + this.triggerMethod('add:region', name, region); + return region; + }, + + // Get a region by name + get: function(name) { + return this._regions[name]; + }, + + // Gets all the regions contained within + // the `regionManager` instance. + getRegions: function(){ + return _.clone(this._regions); + }, + + // Remove a region by name + removeRegion: function(name) { + var region = this._regions[name]; + this._remove(name, region); + }, + + // Empty all regions in the region manager, and + // remove them + removeRegions: function() { + _.each(this._regions, function(region, name) { + this._remove(name, region); + }, this); + }, + + // Empty all regions in the region manager, but + // leave them attached + emptyRegions: function() { + _.each(this._regions, function(region) { + region.empty(); + }, this); + }, + + // Destroy all regions and shut down the region + // manager entirely + destroy: function() { + this.removeRegions(); + Marionette.Controller.prototype.destroy.apply(this, arguments); + }, + + // internal method to store regions + _store: function(name, region) { + this._regions[name] = region; + this._setLength(); + }, + + // internal method to remove a region + _remove: function(name, region) { + this.triggerMethod('before:remove:region', name, region); + region.empty(); + region.stopListening(); + delete this._regions[name]; + this._setLength(); + this.triggerMethod('remove:region', name, region); + }, + + // set the number of regions current held + _setLength: function() { + this.length = _.size(this._regions); + } + + }); + + Marionette.actAsCollection(RegionManager.prototype, '_regions'); + + return RegionManager; + })(Marionette); + + + // Template Cache + // -------------- + + // Manage templates stored in `<script>` blocks, + // caching them for faster access. + Marionette.TemplateCache = function(templateId) { + this.templateId = templateId; + }; + + // TemplateCache object-level methods. Manage the template + // caches from these method calls instead of creating + // your own TemplateCache instances + _.extend(Marionette.TemplateCache, { + templateCaches: {}, + + // Get the specified template by id. Either + // retrieves the cached version, or loads it + // from the DOM. + get: function(templateId) { + var cachedTemplate = this.templateCaches[templateId]; + + if (!cachedTemplate) { + cachedTemplate = new Marionette.TemplateCache(templateId); + this.templateCaches[templateId] = cachedTemplate; + } + + return cachedTemplate.load(); + }, + + // Clear templates from the cache. If no arguments + // are specified, clears all templates: + // `clear()` + // + // If arguments are specified, clears each of the + // specified templates from the cache: + // `clear("#t1", "#t2", "...")` + clear: function() { + var i; + var args = slice.call(arguments); + var length = args.length; + + if (length > 0) { + for (i = 0; i < length; i++) { + delete this.templateCaches[args[i]]; + } + } else { + this.templateCaches = {}; + } + } + }); + + // TemplateCache instance methods, allowing each + // template cache object to manage its own state + // and know whether or not it has been loaded + _.extend(Marionette.TemplateCache.prototype, { + + // Internal method to load the template + load: function() { + // Guard clause to prevent loading this template more than once + if (this.compiledTemplate) { + return this.compiledTemplate; + } + + // Load the template and compile it + var template = this.loadTemplate(this.templateId); + this.compiledTemplate = this.compileTemplate(template); + + return this.compiledTemplate; + }, + + // Load a template from the DOM, by default. Override + // this method to provide your own template retrieval + // For asynchronous loading with AMD/RequireJS, consider + // using a template-loader plugin as described here: + // https://github.com/marionettejs/backbone.marionette/wiki/Using-marionette-with-requirejs + loadTemplate: function(templateId) { + var template = Backbone.$(templateId).html(); + + if (!template || template.length === 0) { + throwError('Could not find template: "' + templateId + '"', 'NoTemplateError'); + } + + return template; + }, + + // Pre-compile the template before caching it. Override + // this method if you do not need to pre-compile a template + // (JST / RequireJS for example) or if you want to change + // the template engine used (Handebars, etc). + compileTemplate: function(rawTemplate) { + return _.template(rawTemplate); + } + }); + + // Renderer + // -------- + + // Render a template with data by passing in the template + // selector and the data to render. + Marionette.Renderer = { + + // Render a template with data. The `template` parameter is + // passed to the `TemplateCache` object to retrieve the + // template function. Override this method to provide your own + // custom rendering and template handling for all of Marionette. + render: function(template, data) { + if (!template) { + throwError('Cannot render the template since its false, null or undefined.', + 'TemplateNotFoundError'); + } + + var templateFunc; + if (typeof template === 'function') { + templateFunc = template; + } else { + templateFunc = Marionette.TemplateCache.get(template); + } + + return templateFunc(data); + } + }; + + + /* jshint maxlen: 114, nonew: false */ + // Marionette.View + // --------------- + + // The core view class that other Marionette views extend from. + Marionette.View = Backbone.View.extend({ + + constructor: function(options) { + _.bindAll(this, 'render'); + + // this exposes view options to the view initializer + // this is a backfill since backbone removed the assignment + // of this.options + // at some point however this may be removed + this.options = _.extend({}, _.result(this, 'options'), _.isFunction(options) ? options.call(this) : options); + // parses out the @ui DSL for events + this.events = this.normalizeUIKeys(_.result(this, 'events')); + + if (_.isObject(this.behaviors)) { + new Marionette.Behaviors(this); + } + + Backbone.View.apply(this, arguments); + + Marionette.MonitorDOMRefresh(this); + this.listenTo(this, 'show', this.onShowCalled); + }, + + // Get the template for this view + // instance. You can set a `template` attribute in the view + // definition or pass a `template: "whatever"` parameter in + // to the constructor options. + getTemplate: function() { + return this.getOption('template'); + }, + + // Mix in template helper methods. Looks for a + // `templateHelpers` attribute, which can either be an + // object literal, or a function that returns an object + // literal. All methods and attributes from this object + // are copies to the object passed in. + mixinTemplateHelpers: function(target) { + target = target || {}; + var templateHelpers = this.getOption('templateHelpers'); + if (_.isFunction(templateHelpers)) { + templateHelpers = templateHelpers.call(this); + } + return _.extend(target, templateHelpers); + }, + + + normalizeUIKeys: function(hash) { + var ui = _.result(this, 'ui'); + var uiBindings = _.result(this, '_uiBindings'); + return Marionette.normalizeUIKeys(hash, uiBindings || ui); + }, + + // Configure `triggers` to forward DOM events to view + // events. `triggers: {"click .foo": "do:foo"}` + configureTriggers: function() { + if (!this.triggers) { return; } + + var triggerEvents = {}; + + // Allow `triggers` to be configured as a function + var triggers = this.normalizeUIKeys(_.result(this, 'triggers')); + + // Configure the triggers, prevent default + // action and stop propagation of DOM events + _.each(triggers, function(value, key) { + + var hasOptions = _.isObject(value); + var eventName = hasOptions ? value.event : value; + + // build the event handler function for the DOM event + triggerEvents[key] = function(e) { + + // stop the event in its tracks + if (e) { + var prevent = e.preventDefault; + var stop = e.stopPropagation; + + var shouldPrevent = hasOptions ? value.preventDefault : prevent; + var shouldStop = hasOptions ? value.stopPropagation : stop; + + if (shouldPrevent && prevent) { prevent.apply(e); } + if (shouldStop && stop) { stop.apply(e); } + } + + // build the args for the event + var args = { + view: this, + model: this.model, + collection: this.collection + }; + + // trigger the event + this.triggerMethod(eventName, args); + }; + + }, this); + + return triggerEvents; + }, + + // Overriding Backbone.View's delegateEvents to handle + // the `triggers`, `modelEvents`, and `collectionEvents` configuration + delegateEvents: function(events) { + this._delegateDOMEvents(events); + this.bindEntityEvents(this.model, this.getOption('modelEvents')); + this.bindEntityEvents(this.collection, this.getOption('collectionEvents')); + }, + + // internal method to delegate DOM events and triggers + _delegateDOMEvents: function(events) { + events = events || this.events; + if (_.isFunction(events)) { events = events.call(this); } + + // normalize ui keys + events = this.normalizeUIKeys(events); + + var combinedEvents = {}; + + // look up if this view has behavior events + var behaviorEvents = _.result(this, 'behaviorEvents') || {}; + var triggers = this.configureTriggers(); + + // behavior events will be overriden by view events and or triggers + _.extend(combinedEvents, behaviorEvents, events, triggers); + + Backbone.View.prototype.delegateEvents.call(this, combinedEvents); + }, + + // Overriding Backbone.View's undelegateEvents to handle unbinding + // the `triggers`, `modelEvents`, and `collectionEvents` config + undelegateEvents: function() { + var args = Array.prototype.slice.call(arguments); + Backbone.View.prototype.undelegateEvents.apply(this, args); + this.unbindEntityEvents(this.model, this.getOption('modelEvents')); + this.unbindEntityEvents(this.collection, this.getOption('collectionEvents')); + }, + + // Internal method, handles the `show` event. + onShowCalled: function() {}, + + // Internal helper method to verify whether the view hasn't been destroyed + _ensureViewIsIntact: function() { + if (this.isDestroyed) { + var err = new Error('Cannot use a view thats already been destroyed.'); + err.name = 'ViewDestroyedError'; + throw err; + } + }, + + // Default `destroy` implementation, for removing a view from the + // DOM and unbinding it. Regions will call this method + // for you. You can specify an `onDestroy` method in your view to + // add custom code that is called after the view is destroyed. + destroy: function() { + if (this.isDestroyed) { return; } + + var args = Array.prototype.slice.call(arguments); + + this.triggerMethod.apply(this, ['before:destroy'].concat(args)); + + // mark as destroyed before doing the actual destroy, to + // prevent infinite loops within "destroy" event handlers + // that are trying to destroy other views + this.isDestroyed = true; + this.triggerMethod.apply(this, ['destroy'].concat(args)); + + // unbind UI elements + this.unbindUIElements(); + + // remove the view from the DOM + this.remove(); + }, + + // This method binds the elements specified in the "ui" hash inside the view's code with + // the associated jQuery selectors. + bindUIElements: function() { + if (!this.ui) { return; } + + // store the ui hash in _uiBindings so they can be reset later + // and so re-rendering the view will be able to find the bindings + if (!this._uiBindings) { + this._uiBindings = this.ui; + } + + // get the bindings result, as a function or otherwise + var bindings = _.result(this, '_uiBindings'); + + // empty the ui so we don't have anything to start with + this.ui = {}; + + // bind each of the selectors + _.each(_.keys(bindings), function(key) { + var selector = bindings[key]; + this.ui[key] = this.$(selector); + }, this); + }, + + // This method unbinds the elements specified in the "ui" hash + unbindUIElements: function() { + if (!this.ui || !this._uiBindings) { return; } + + // delete all of the existing ui bindings + _.each(this.ui, function($el, name) { + delete this.ui[name]; + }, this); + + // reset the ui element to the original bindings configuration + this.ui = this._uiBindings; + delete this._uiBindings; + }, + + // import the `triggerMethod` to trigger events with corresponding + // methods if the method exists + triggerMethod: Marionette.triggerMethod, + + // Imports the "normalizeMethods" to transform hashes of + // events=>function references/names to a hash of events=>function references + normalizeMethods: Marionette.normalizeMethods, + + // Proxy `getOption` to enable getting options from this or this.options by name. + getOption: Marionette.proxyGetOption, + + // Proxy `unbindEntityEvents` to enable binding view's events from another entity. + bindEntityEvents: Marionette.proxyBindEntityEvents, + + // Proxy `unbindEntityEvents` to enable unbinding view's events from another entity. + unbindEntityEvents: Marionette.proxyUnbindEntityEvents + }); + + // Item View + // --------- + + // A single item view implementation that contains code for rendering + // with underscore.js templates, serializing the view's model or collection, + // and calling several methods on extended views, such as `onRender`. + Marionette.ItemView = Marionette.View.extend({ + + // Setting up the inheritance chain which allows changes to + // Marionette.View.prototype.constructor which allows overriding + constructor: function() { + Marionette.View.apply(this, arguments); + }, + + // Serialize the model or collection for the view. If a model is + // found, `.toJSON()` is called. If a collection is found, `.toJSON()` + // is also called, but is used to populate an `items` array in the + // resulting data. If both are found, defaults to the model. + // You can override the `serializeData` method in your own view + // definition, to provide custom serialization for your view's data. + serializeData: function() { + var data = {}; + + if (this.model) { + data = this.model.toJSON(); + } + else if (this.collection) { + data = {items: this.collection.toJSON()}; + } + + return data; + }, + + // Render the view, defaulting to underscore.js templates. + // You can override this in your view definition to provide + // a very specific rendering for your view. In general, though, + // you should override the `Marionette.Renderer` object to + // change how Marionette renders views. + render: function() { + this._ensureViewIsIntact(); + + this.triggerMethod('before:render', this); + + var data = this.serializeData(); + data = this.mixinTemplateHelpers(data); + + var template = this.getTemplate(); + var html = Marionette.Renderer.render(template, data); + this.attachElContent(html); + this.bindUIElements(); + + this.triggerMethod('render', this); + + return this; + }, + + // Attaches the content of a given view. + // This method can be overriden to optimize rendering, + // or to render in a non standard way. + // + // For example, using `innerHTML` instead of `$el.html` + // + // ```js + // attachElContent: function(html) { + // this.el.innerHTML = html; + // return this; + // } + // ``` + attachElContent: function(html) { + this.$el.html(html); + + return this; + }, + + // Override the default destroy event to add a few + // more events that are triggered. + destroy: function() { + if (this.isDestroyed) { return; } + + Marionette.View.prototype.destroy.apply(this, arguments); + } + }); + + /* jshint maxstatements: 14 */ + + // Collection View + // --------------- + + // A view that iterates over a Backbone.Collection + // and renders an individual child view for each model. + Marionette.CollectionView = Marionette.View.extend({ + + // used as the prefix for child view events + // that are forwarded through the collectionview + childViewEventPrefix: 'childview', + + // constructor + // option to pass `{sort: false}` to prevent the `CollectionView` from + // maintaining the sorted order of the collection. + // This will fallback onto appending childView's to the end. + constructor: function(options){ + var initOptions = options || {}; + this.sort = _.isUndefined(initOptions.sort) ? true : initOptions.sort; + + this._initChildViewStorage(); + + Marionette.View.apply(this, arguments); + + this._initialEvents(); + this.initRenderBuffer(); + }, + + // Instead of inserting elements one by one into the page, + // it's much more performant to insert elements into a document + // fragment and then insert that document fragment into the page + initRenderBuffer: function() { + this.elBuffer = document.createDocumentFragment(); + this._bufferedChildren = []; + }, + + startBuffering: function() { + this.initRenderBuffer(); + this.isBuffering = true; + }, + + endBuffering: function() { + this.isBuffering = false; + this._triggerBeforeShowBufferedChildren(); + this.attachBuffer(this, this.elBuffer); + this._triggerShowBufferedChildren(); + this.initRenderBuffer(); + }, + + _triggerBeforeShowBufferedChildren: function() { + if (this._isShown) { + _.invoke(this._bufferedChildren, 'triggerMethod', 'before:show'); + } + }, + + _triggerShowBufferedChildren: function() { + if (this._isShown) { + _.each(this._bufferedChildren, function (child) { + if (_.isFunction(child.triggerMethod)) { + child.triggerMethod('show'); + } else { + Marionette.triggerMethod.call(child, 'show'); + } + }); + this._bufferedChildren = []; + } + }, + + // Configured the initial events that the collection view + // binds to. + _initialEvents: function() { + if (this.collection) { + this.listenTo(this.collection, 'add', this._onCollectionAdd); + this.listenTo(this.collection, 'remove', this._onCollectionRemove); + this.listenTo(this.collection, 'reset', this.render); + + if (this.sort) { + this.listenTo(this.collection, 'sort', this._sortViews); + } + } + }, + + // Handle a child added to the collection + _onCollectionAdd: function(child, collection, options) { + this.destroyEmptyView(); + var ChildView = this.getChildView(child); + var index = this.collection.indexOf(child); + this.addChild(child, ChildView, index); + }, + + // get the child view by model it holds, and remove it + _onCollectionRemove: function(model) { + var view = this.children.findByModel(model); + this.removeChildView(view); + this.checkEmpty(); + }, + + // Override from `Marionette.View` to trigger show on child views + onShowCalled: function(){ + this.children.each(function(child){ + if (_.isFunction(child.triggerMethod)) { + child.triggerMethod('show'); + } else { + Marionette.triggerMethod.call(child, 'show'); + } + }); + }, + + // Render children views. Override this method to + // provide your own implementation of a render function for + // the collection view. + render: function() { + this._ensureViewIsIntact(); + this.triggerMethod('before:render', this); + this._renderChildren(); + this.triggerMethod('render', this); + return this; + }, + + // Internal method. This checks for any changes in the order of the collection. + // If the index of any view doesn't match, it will render. + _sortViews: function(){ + // check for any changes in sort order of views + var orderChanged = this.collection.find(function(item, index){ + var view = this.children.findByModel(item); + return view && view._index !== index; + }, this); + + if (orderChanged) { + this.render(); + } + }, + + // Internal method. Separated so that CompositeView can have + // more control over events being triggered, around the rendering + // process + _renderChildren: function() { + this.startBuffering(); + + this.destroyEmptyView(); + this.destroyChildren(); + + if (!this.isEmpty(this.collection)) { + this.triggerMethod('before:render:collection', this); + this.showCollection(); + this.triggerMethod('render:collection', this); + } else { + this.showEmptyView(); + } + + this.endBuffering(); + }, + + // Internal method to loop through collection and show each child view. + showCollection: function() { + var ChildView; + this.collection.each(function(child, index) { + ChildView = this.getChildView(child); + this.addChild(child, ChildView, index); + }, this); + }, + + // Internal method to show an empty view in place of + // a collection of child views, when the collection is empty + showEmptyView: function() { + var EmptyView = this.getEmptyView(); + + if (EmptyView && !this._showingEmptyView) { + this.triggerMethod('before:render:empty'); + + this._showingEmptyView = true; + var model = new Backbone.Model(); + this.addEmptyView(model, EmptyView); + + this.triggerMethod('render:empty'); + } + }, + + // Internal method to destroy an existing emptyView instance + // if one exists. Called when a collection view has been + // rendered empty, and then a child is added to the collection. + destroyEmptyView: function() { + if (this._showingEmptyView) { + this.destroyChildren(); + delete this._showingEmptyView; + } + }, + + // Retrieve the empty view class + getEmptyView: function() { + return this.getOption('emptyView'); + }, + + // Render and show the emptyView. Similar to addChild method + // but "child:added" events are not fired, and the event from + // emptyView are not forwarded + addEmptyView: function(child, EmptyView){ + + // get the emptyViewOptions, falling back to childViewOptions + var emptyViewOptions = this.getOption('emptyViewOptions') || + this.getOption('childViewOptions'); + + if (_.isFunction(emptyViewOptions)){ + emptyViewOptions = emptyViewOptions.call(this); + } + + // build the empty view + var view = this.buildChildView(child, EmptyView, emptyViewOptions); + + // trigger the 'before:show' event on `view` if the collection view + // has already been shown + if (this._isShown){ + this.triggerMethod.call(view, 'before:show'); + } + + // Store the `emptyView` like a `childView` so we can properly + // remove and/or close it later + this.children.add(view); + + // Render it and show it + this.renderChildView(view, -1); + + // call the 'show' method if the collection view + // has already been shown + if (this._isShown){ + this.triggerMethod.call(view, 'show'); + } + }, + + // Retrieve the childView class, either from `this.options.childView` + // or from the `childView` in the object definition. The "options" + // takes precedence. + getChildView: function(child) { + var childView = this.getOption('childView'); + + if (!childView) { + throwError('A "childView" must be specified', 'NoChildViewError'); + } + + return childView; + }, + + // Render the child's view and add it to the + // HTML for the collection view at a given index. + // This will also update the indices of later views in the collection + // in order to keep the children in sync with the collection. + addChild: function(child, ChildView, index) { + var childViewOptions = this.getOption('childViewOptions'); + if (_.isFunction(childViewOptions)) { + childViewOptions = childViewOptions.call(this, child, index); + } + + var view = this.buildChildView(child, ChildView, childViewOptions); + + // increment indices of views after this one + this._updateIndices(view, true, index); + + this._addChildView(view, index); + + return view; + }, + + // Internal method. This decrements or increments the indices of views after the + // added/removed view to keep in sync with the collection. + _updateIndices: function(view, increment, index) { + if (!this.sort) { + return; + } + + if (increment) { + // assign the index to the view + view._index = index; + + // increment the index of views after this one + this.children.each(function (laterView) { + if (laterView._index >= view._index) { + laterView._index++; + } + }); + } + else { + // decrement the index of views after this one + this.children.each(function (laterView) { + if (laterView._index >= view._index) { + laterView._index--; + } + }); + } + }, + + + // Internal Method. Add the view to children and render it at + // the given index. + _addChildView: function(view, index) { + // set up the child view event forwarding + this.proxyChildEvents(view); + + this.triggerMethod('before:add:child', view); + + // Store the child view itself so we can properly + // remove and/or destroy it later + this.children.add(view); + this.renderChildView(view, index); + + if (this._isShown && !this.isBuffering){ + if (_.isFunction(view.triggerMethod)) { + view.triggerMethod('show'); + } else { + Marionette.triggerMethod.call(view, 'show'); + } + } + + this.triggerMethod('add:child', view); + }, + + // render the child view + renderChildView: function(view, index) { + view.render(); + this.attachHtml(this, view, index); + }, + + // Build a `childView` for a model in the collection. + buildChildView: function(child, ChildViewClass, childViewOptions) { + var options = _.extend({model: child}, childViewOptions); + return new ChildViewClass(options); + }, + + // Remove the child view and destroy it. + // This function also updates the indices of + // later views in the collection in order to keep + // the children in sync with the collection. + removeChildView: function(view) { + + if (view) { + this.triggerMethod('before:remove:child', view); + // call 'destroy' or 'remove', depending on which is found + if (view.destroy) { view.destroy(); } + else if (view.remove) { view.remove(); } + + this.stopListening(view); + this.children.remove(view); + this.triggerMethod('remove:child', view); + + // decrement the index of views after this one + this._updateIndices(view, false); + } + + }, + + // check if the collection is empty + isEmpty: function(collection) { + return !this.collection || this.collection.length === 0; + }, + + // If empty, show the empty view + checkEmpty: function() { + if (this.isEmpty(this.collection)) { + this.showEmptyView(); + } + }, + + // You might need to override this if you've overridden attachHtml + attachBuffer: function(collectionView, buffer) { + collectionView.$el.append(buffer); + }, + + // Append the HTML to the collection's `el`. + // Override this method to do something other + // than `.append`. + attachHtml: function(collectionView, childView, index) { + if (collectionView.isBuffering) { + // buffering happens on reset events and initial renders + // in order to reduce the number of inserts into the + // document, which are expensive. + collectionView.elBuffer.appendChild(childView.el); + collectionView._bufferedChildren.push(childView); + } + else { + // If we've already rendered the main collection, append + // the new child into the correct order if we need to. Otherwise + // append to the end. + if (!collectionView._insertBefore(childView, index)){ + collectionView._insertAfter(childView); + } + } + }, + + // Internal method. Check whether we need to insert the view into + // the correct position. + _insertBefore: function(childView, index) { + var currentView; + var findPosition = this.sort && (index < this.children.length - 1); + if (findPosition) { + // Find the view after this one + currentView = this.children.find(function (view) { + return view._index === index + 1; + }); + } + + if (currentView) { + currentView.$el.before(childView.el); + return true; + } + + return false; + }, + + // Internal method. Append a view to the end of the $el + _insertAfter: function(childView) { + this.$el.append(childView.el); + }, + + // Internal method to set up the `children` object for + // storing all of the child views + _initChildViewStorage: function() { + this.children = new Backbone.ChildViewContainer(); + }, + + // Handle cleanup and other destroying needs for the collection of views + destroy: function() { + if (this.isDestroyed) { return; } + + this.triggerMethod('before:destroy:collection'); + this.destroyChildren(); + this.triggerMethod('destroy:collection'); + + Marionette.View.prototype.destroy.apply(this, arguments); + }, + + // Destroy the child views that this collection view + // is holding on to, if any + destroyChildren: function() { + this.children.each(this.removeChildView, this); + this.checkEmpty(); + }, + + // Set up the child view event forwarding. Uses a "childview:" + // prefix in front of all forwarded events. + proxyChildEvents: function(view) { + var prefix = this.getOption('childViewEventPrefix'); + + // Forward all child view events through the parent, + // prepending "childview:" to the event name + this.listenTo(view, 'all', function() { + var args = Array.prototype.slice.call(arguments); + var rootEvent = args[0]; + var childEvents = this.normalizeMethods(_.result(this, 'childEvents')); + + args[0] = prefix + ':' + rootEvent; + args.splice(1, 0, view); + + // call collectionView childEvent if defined + if (typeof childEvents !== 'undefined' && _.isFunction(childEvents[rootEvent])) { + childEvents[rootEvent].apply(this, args.slice(1)); + } + + this.triggerMethod.apply(this, args); + }, this); + } + }); + + /* jshint maxstatements: 17, maxlen: 117 */ + + // Composite View + // -------------- + + // Used for rendering a branch-leaf, hierarchical structure. + // Extends directly from CollectionView and also renders an + // a child view as `modelView`, for the top leaf + Marionette.CompositeView = Marionette.CollectionView.extend({ + + // Setting up the inheritance chain which allows changes to + // Marionette.CollectionView.prototype.constructor which allows overriding + // option to pass '{sort: false}' to prevent the CompositeView from + // maintaining the sorted order of the collection. + // This will fallback onto appending childView's to the end. + constructor: function() { + Marionette.CollectionView.apply(this, arguments); + }, + + // Configured the initial events that the composite view + // binds to. Override this method to prevent the initial + // events, or to add your own initial events. + _initialEvents: function() { + + // Bind only after composite view is rendered to avoid adding child views + // to nonexistent childViewContainer + this.once('render', function() { + if (this.collection) { + this.listenTo(this.collection, 'add', this._onCollectionAdd); + this.listenTo(this.collection, 'remove', this._onCollectionRemove); + this.listenTo(this.collection, 'reset', this._renderChildren); + + if (this.sort) { + this.listenTo(this.collection, 'sort', this._sortViews); + } + } + }); + + }, + + // Retrieve the `childView` to be used when rendering each of + // the items in the collection. The default is to return + // `this.childView` or Marionette.CompositeView if no `childView` + // has been defined + getChildView: function(child) { + var childView = this.getOption('childView') || this.constructor; + + if (!childView) { + throwError('A "childView" must be specified', 'NoChildViewError'); + } + + return childView; + }, + + // Serialize the collection for the view. + // You can override the `serializeData` method in your own view + // definition, to provide custom serialization for your view's data. + serializeData: function() { + var data = {}; + + if (this.model) { + data = this.model.toJSON(); + } + + return data; + }, + + // Renders the model once, and the collection once. Calling + // this again will tell the model's view to re-render itself + // but the collection will not re-render. + render: function() { + this._ensureViewIsIntact(); + this.isRendered = true; + this.resetChildViewContainer(); + + this.triggerMethod('before:render', this); + + this._renderRoot(); + this._renderChildren(); + + this.triggerMethod('render', this); + return this; + }, + + _renderChildren: function() { + if (this.isRendered) { + Marionette.CollectionView.prototype._renderChildren.call(this); + } + }, + + // Render the root template that the children + // views are appended to + _renderRoot: function() { + var data = {}; + data = this.serializeData(); + data = this.mixinTemplateHelpers(data); + + this.triggerMethod('before:render:template'); + + var template = this.getTemplate(); + var html = Marionette.Renderer.render(template, data); + this.attachElContent(html); + + // the ui bindings is done here and not at the end of render since they + // will not be available until after the model is rendered, but should be + // available before the collection is rendered. + this.bindUIElements(); + this.triggerMethod('render:template'); + }, + + // Attaches the content of the root. + // This method can be overriden to optimize rendering, + // or to render in a non standard way. + // + // For example, using `innerHTML` instead of `$el.html` + // + // ```js + // attachElContent: function(html) { + // this.el.innerHTML = html; + // return this; + // } + // ``` + attachElContent: function(html) { + this.$el.html(html); + + return this; + }, + + // You might need to override this if you've overridden attachHtml + attachBuffer: function(compositeView, buffer) { + var $container = this.getChildViewContainer(compositeView); + $container.append(buffer); + }, + + // Internal method. Append a view to the end of the $el. + // Overidden from CollectionView to ensure view is appended to + // childViewContainer + _insertAfter: function (childView) { + var $container = this.getChildViewContainer(this); + $container.append(childView.el); + }, + + // Internal method to ensure an `$childViewContainer` exists, for the + // `attachHtml` method to use. + getChildViewContainer: function(containerView) { + if ('$childViewContainer' in containerView) { + return containerView.$childViewContainer; + } + + var container; + var childViewContainer = Marionette.getOption(containerView, 'childViewContainer'); + if (childViewContainer) { + + var selector = _.isFunction(childViewContainer) ? childViewContainer.call(containerView) : childViewContainer; + + if (selector.charAt(0) === '@' && containerView.ui) { + container = containerView.ui[selector.substr(4)]; + } else { + container = containerView.$(selector); + } + + if (container.length <= 0) { + throwError('The specified "childViewContainer" was not found: ' + + containerView.childViewContainer, 'ChildViewContainerMissingError'); + } + + } else { + container = containerView.$el; + } + + containerView.$childViewContainer = container; + return container; + }, + + // Internal method to reset the `$childViewContainer` on render + resetChildViewContainer: function() { + if (this.$childViewContainer) { + delete this.$childViewContainer; + } + } + }); + + // LayoutView + // ---------- + + // Used for managing application layoutViews, nested layoutViews and + // multiple regions within an application or sub-application. + // + // A specialized view class that renders an area of HTML and then + // attaches `Region` instances to the specified `regions`. + // Used for composite view management and sub-application areas. + Marionette.LayoutView = Marionette.ItemView.extend({ + regionClass: Marionette.Region, + + // Ensure the regions are available when the `initialize` method + // is called. + constructor: function(options) { + options = options || {}; + + this._firstRender = true; + this._initializeRegions(options); + + Marionette.ItemView.call(this, options); + }, + + // LayoutView's render will use the existing region objects the + // first time it is called. Subsequent calls will destroy the + // views that the regions are showing and then reset the `el` + // for the regions to the newly rendered DOM elements. + render: function() { + this._ensureViewIsIntact(); + + if (this._firstRender) { + // if this is the first render, don't do anything to + // reset the regions + this._firstRender = false; + } else { + // If this is not the first render call, then we need to + // re-initialize the `el` for each region + this._reInitializeRegions(); + } + + return Marionette.ItemView.prototype.render.apply(this, arguments); + }, + + // Handle destroying regions, and then destroy the view itself. + destroy: function() { + if (this.isDestroyed) { return; } + + this.regionManager.destroy(); + Marionette.ItemView.prototype.destroy.apply(this, arguments); + }, + + // Add a single region, by name, to the layoutView + addRegion: function(name, definition) { + this.triggerMethod('before:region:add', name); + var regions = {}; + regions[name] = definition; + return this._buildRegions(regions)[name]; + }, + + // Add multiple regions as a {name: definition, name2: def2} object literal + addRegions: function(regions) { + this.regions = _.extend({}, this.regions, regions); + return this._buildRegions(regions); + }, + + // Remove a single region from the LayoutView, by name + removeRegion: function(name) { + this.triggerMethod('before:region:remove', name); + delete this.regions[name]; + return this.regionManager.removeRegion(name); + }, + + // Provides alternative access to regions + // Accepts the region name + // getRegion('main') + getRegion: function(region) { + return this.regionManager.get(region); + }, + + // Get all regions + getRegions: function(){ + return this.regionManager.getRegions(); + }, + + // internal method to build regions + _buildRegions: function(regions) { + var that = this; + + var defaults = { + regionClass: this.getOption('regionClass'), + parentEl: function() { return that.$el; } + }; + + return this.regionManager.addRegions(regions, defaults); + }, + + // Internal method to initialize the regions that have been defined in a + // `regions` attribute on this layoutView. + _initializeRegions: function(options) { + var regions; + this._initRegionManager(); + + if (_.isFunction(this.regions)) { + regions = this.regions(options); + } else { + regions = this.regions || {}; + } + + // Enable users to define `regions` as instance options. + var regionOptions = this.getOption.call(options, 'regions'); + + // enable region options to be a function + if (_.isFunction(regionOptions)) { + regionOptions = regionOptions.call(this, options); + } + + _.extend(regions, regionOptions); + + this.addRegions(regions); + }, + + // Internal method to re-initialize all of the regions by updating the `el` that + // they point to + _reInitializeRegions: function() { + this.regionManager.emptyRegions(); + this.regionManager.each(function(region) { + region.reset(); + }); + }, + + // Enable easy overiding of the default `RegionManager` + // for customized region interactions and buisness specific + // view logic for better control over single regions. + getRegionManager: function() { + return new Marionette.RegionManager(); + }, + + // Internal method to initialize the region manager + // and all regions in it + _initRegionManager: function() { + this.regionManager = this.getRegionManager(); + + this.listenTo(this.regionManager, 'before:add:region', function(name) { + this.triggerMethod('before:add:region', name); + }); + + this.listenTo(this.regionManager, 'add:region', function(name, region) { + this[name] = region; + this.triggerMethod('add:region', name, region); + }); + + this.listenTo(this.regionManager, 'before:remove:region', function(name) { + this.triggerMethod('before:remove:region', name); + }); + + this.listenTo(this.regionManager, 'remove:region', function(name, region) { + delete this[name]; + this.triggerMethod('remove:region', name, region); + }); + } + }); + + + // Behavior + // ----------- + + // A Behavior is an isolated set of DOM / + // user interactions that can be mixed into any View. + // Behaviors allow you to blackbox View specific interactions + // into portable logical chunks, keeping your views simple and your code DRY. + + Marionette.Behavior = (function(_, Backbone) { + function Behavior(options, view) { + // Setup reference to the view. + // this comes in handle when a behavior + // wants to directly talk up the chain + // to the view. + this.view = view; + this.defaults = _.result(this, 'defaults') || {}; + this.options = _.extend({}, this.defaults, options); + + // proxy behavior $ method to the view + // this is useful for doing jquery DOM lookups + // scoped to behaviors view. + this.$ = function() { + return this.view.$.apply(this.view, arguments); + }; + + // Call the initialize method passing + // the arguments from the instance constructor + this.initialize.apply(this, arguments); + } + + _.extend(Behavior.prototype, Backbone.Events, { + initialize: function() {}, + + // stopListening to behavior `onListen` events. + destroy: function() { + this.stopListening(); + }, + + // import the `triggerMethod` to trigger events with corresponding + // methods if the method exists + triggerMethod: Marionette.triggerMethod, + + // Proxy `getOption` to enable getting options from this or this.options by name. + getOption: Marionette.proxyGetOption, + + // Proxy `unbindEntityEvents` to enable binding view's events from another entity. + bindEntityEvents: Marionette.proxyBindEntityEvents, + + // Proxy `unbindEntityEvents` to enable unbinding view's events from another entity. + unbindEntityEvents: Marionette.proxyUnbindEntityEvents + }); + + // Borrow Backbones extend implementation + // this allows us to setup a proper + // inheritence pattern that follow in suite + // with the rest of Marionette views. + Behavior.extend = Marionette.extend; + + return Behavior; + })(_, Backbone); + + /* jshint maxlen: 143, nonew: false */ + // Marionette.Behaviors + // -------- + + // Behaviors is a utility class that takes care of + // glueing your behavior instances to their given View. + // The most important part of this class is that you + // **MUST** override the class level behaviorsLookup + // method for things to work properly. + + Marionette.Behaviors = (function(Marionette, _) { + + function Behaviors(view, behaviors) { + // Behaviors defined on a view can be a flat object literal + // or it can be a function that returns an object. + behaviors = Behaviors.parseBehaviors(view, behaviors || _.result(view, 'behaviors')); + + // Wraps several of the view's methods + // calling the methods first on each behavior + // and then eventually calling the method on the view. + Behaviors.wrap(view, behaviors, [ + 'bindUIElements', 'unbindUIElements', + 'delegateEvents', 'undelegateEvents', + 'behaviorEvents', 'triggerMethod', + 'setElement', 'destroy' + ]); + } + + var methods = { + setElement: function(setElement, behaviors) { + setElement.apply(this, _.tail(arguments, 2)); + + // proxy behavior $el to the view's $el. + // This is needed because a view's $el proxy + // is not set until after setElement is called. + _.each(behaviors, function(b) { + b.$el = this.$el; + }, this); + }, + + destroy: function(destroy, behaviors) { + var args = _.tail(arguments, 2); + destroy.apply(this, args); + + // Call destroy on each behavior after + // destroying the view. + // This unbinds event listeners + // that behaviors have registerd for. + _.invoke(behaviors, 'destroy', args); + }, + + bindUIElements: function(bindUIElements, behaviors) { + bindUIElements.apply(this); + _.invoke(behaviors, bindUIElements); + }, + + unbindUIElements: function(unbindUIElements, behaviors) { + unbindUIElements.apply(this); + _.invoke(behaviors, unbindUIElements); + }, + + triggerMethod: function(triggerMethod, behaviors) { + var args = _.tail(arguments, 2); + triggerMethod.apply(this, args); + + _.each(behaviors, function(b) { + triggerMethod.apply(b, args); + }); + }, + + delegateEvents: function(delegateEvents, behaviors) { + var args = _.tail(arguments, 2); + delegateEvents.apply(this, args); + + _.each(behaviors, function(b) { + Marionette.bindEntityEvents(b, this.model, Marionette.getOption(b, 'modelEvents')); + Marionette.bindEntityEvents(b, this.collection, Marionette.getOption(b, 'collectionEvents')); + }, this); + }, + + undelegateEvents: function(undelegateEvents, behaviors) { + var args = _.tail(arguments, 2); + undelegateEvents.apply(this, args); + + _.each(behaviors, function(b) { + Marionette.unbindEntityEvents(b, this.model, Marionette.getOption(b, 'modelEvents')); + Marionette.unbindEntityEvents(b, this.collection, Marionette.getOption(b, 'collectionEvents')); + }, this); + }, + + behaviorEvents: function(behaviorEvents, behaviors) { + var _behaviorsEvents = {}; + var viewUI = _.result(this, 'ui'); + + _.each(behaviors, function(b, i) { + var _events = {}; + var behaviorEvents = _.clone(_.result(b, 'events')) || {}; + var behaviorUI = _.result(b, 'ui'); + + // Construct an internal UI hash first using + // the views UI hash and then the behaviors UI hash. + // This allows the user to use UI hash elements + // defined in the parent view as well as those + // defined in the given behavior. + var ui = _.extend({}, viewUI, behaviorUI); + + // Normalize behavior events hash to allow + // a user to use the @ui. syntax. + behaviorEvents = Marionette.normalizeUIKeys(behaviorEvents, ui); + + _.each(_.keys(behaviorEvents), function(key) { + // Append white-space at the end of each key to prevent behavior key collisions. + // This is relying on the fact that backbone events considers "click .foo" the same as + // "click .foo ". + + // +2 is used because new Array(1) or 0 is "" and not " " + var whitespace = (new Array(i + 2)).join(' '); + var eventKey = key + whitespace; + var handler = _.isFunction(behaviorEvents[key]) ? behaviorEvents[key] : b[behaviorEvents[key]]; + + _events[eventKey] = _.bind(handler, b); + }); + + _behaviorsEvents = _.extend(_behaviorsEvents, _events); + }); + + return _behaviorsEvents; + } + }; + + _.extend(Behaviors, { + + // Placeholder method to be extended by the user. + // The method should define the object that stores the behaviors. + // i.e. + // + // ```js + // Marionette.Behaviors.behaviorsLookup: function() { + // return App.Behaviors + // } + // ``` + behaviorsLookup: function() { + 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'); + }, + + // Takes care of getting the behavior class + // given options and a key. + // If a user passes in options.behaviorClass + // default to using that. Otherwise delegate + // the lookup to the users `behaviorsLookup` implementation. + getBehaviorClass: function(options, key) { + if (options.behaviorClass) { + return options.behaviorClass; + } + + // Get behavior class can be either a flat object or a method + return _.isFunction(Behaviors.behaviorsLookup) ? Behaviors.behaviorsLookup.apply(this, arguments)[key] : Behaviors.behaviorsLookup[key]; + }, + + // Iterate over the behaviors object, for each behavior + // instantiate it and get its grouped behaviors. + parseBehaviors: function(view, behaviors) { + return _.chain(behaviors).map(function(options, key) { + var BehaviorClass = Behaviors.getBehaviorClass(options, key); + + var behavior = new BehaviorClass(options, view); + var nestedBehaviors = Behaviors.parseBehaviors(view, _.result(behavior, 'behaviors')); + + return [behavior].concat(nestedBehaviors); + }).flatten().value(); + }, + + // Wrap view internal methods so that they delegate to behaviors. For example, + // `onDestroy` should trigger destroy on all of the behaviors and then destroy itself. + // i.e. + // + // `view.delegateEvents = _.partial(methods.delegateEvents, view.delegateEvents, behaviors);` + wrap: function(view, behaviors, methodNames) { + _.each(methodNames, function(methodName) { + view[methodName] = _.partial(methods[methodName], view[methodName], behaviors); + }); + } + }); + + return Behaviors; + + })(Marionette, _); + + + // AppRouter + // --------- + + // Reduce the boilerplate code of handling route events + // and then calling a single method on another object. + // Have your routers configured to call the method on + // your object, directly. + // + // Configure an AppRouter with `appRoutes`. + // + // App routers can only take one `controller` object. + // It is recommended that you divide your controller + // objects in to smaller pieces of related functionality + // and have multiple routers / controllers, instead of + // just one giant router and controller. + // + // You can also add standard routes to an AppRouter. + + Marionette.AppRouter = Backbone.Router.extend({ + + constructor: function(options) { + Backbone.Router.apply(this, arguments); + + this.options = options || {}; + + var appRoutes = this.getOption('appRoutes'); + var controller = this._getController(); + this.processAppRoutes(controller, appRoutes); + this.on('route', this._processOnRoute, this); + }, + + // Similar to route method on a Backbone Router but + // method is called on the controller + appRoute: function(route, methodName) { + var controller = this._getController(); + this._addAppRoute(controller, route, methodName); + }, + + // process the route event and trigger the onRoute + // method call, if it exists + _processOnRoute: function(routeName, routeArgs) { + // find the path that matched + var routePath = _.invert(this.appRoutes)[routeName]; + + // make sure an onRoute is there, and call it + if (_.isFunction(this.onRoute)) { + this.onRoute(routeName, routePath, routeArgs); + } + }, + + // Internal method to process the `appRoutes` for the + // router, and turn them in to routes that trigger the + // specified method on the specified `controller`. + processAppRoutes: function(controller, appRoutes) { + if (!appRoutes) { return; } + + var routeNames = _.keys(appRoutes).reverse(); // Backbone requires reverted order of routes + + _.each(routeNames, function(route) { + this._addAppRoute(controller, route, appRoutes[route]); + }, this); + }, + + _getController: function() { + return this.getOption('controller'); + }, + + _addAppRoute: function(controller, route, methodName) { + var method = controller[methodName]; + + if (!method) { + throwError('Method "' + methodName + '" was not found on the controller'); + } + + this.route(route, methodName, _.bind(method, controller)); + }, + + // Proxy `getOption` to enable getting options from this or this.options by name. + getOption: Marionette.proxyGetOption + }); + + // Application + // ----------- + + // Contain and manage the composite application as a whole. + // Stores and starts up `Region` objects, includes an + // event aggregator as `app.vent` + Marionette.Application = function(options) { + this._initRegionManager(); + this._initCallbacks = new Marionette.Callbacks(); + var globalCh = Backbone.Wreqr.radio.channel('global'); + this.vent = globalCh.vent; + this.commands = globalCh.commands; + this.reqres = globalCh.reqres; + this.submodules = {}; + + _.extend(this, options); + }; + + _.extend(Marionette.Application.prototype, Backbone.Events, { + // Command execution, facilitated by Backbone.Wreqr.Commands + execute: function() { + this.commands.execute.apply(this.commands, arguments); + }, + + // Request/response, facilitated by Backbone.Wreqr.RequestResponse + request: function() { + return this.reqres.request.apply(this.reqres, arguments); + }, + + // Add an initializer that is either run at when the `start` + // method is called, or run immediately if added after `start` + // has already been called. + addInitializer: function(initializer) { + this._initCallbacks.add(initializer); + }, + + // kick off all of the application's processes. + // initializes all of the regions that have been added + // to the app, and runs all of the initializer functions + start: function(options) { + this.triggerMethod('before:start', options); + this._initCallbacks.run(options, this); + this.triggerMethod('start', options); + }, + + // Add regions to your app. + // Accepts a hash of named strings or Region objects + // addRegions({something: "#someRegion"}) + // addRegions({something: Region.extend({el: "#someRegion"}) }); + addRegions: function(regions) { + return this._regionManager.addRegions(regions); + }, + + // Empty all regions in the app, without removing them + emptyRegions: function() { + this._regionManager.emptyRegions(); + }, + + // Removes a region from your app, by name + // Accepts the regions name + // removeRegion('myRegion') + removeRegion: function(region) { + this._regionManager.removeRegion(region); + }, + + // Provides alternative access to regions + // Accepts the region name + // getRegion('main') + getRegion: function(region) { + return this._regionManager.get(region); + }, + + // Get all the regions from the region manager + getRegions: function(){ + return this._regionManager.getRegions(); + }, + + // Create a module, attached to the application + module: function(moduleNames, moduleDefinition) { + + // Overwrite the module class if the user specifies one + var ModuleClass = Marionette.Module.getClass(moduleDefinition); + + // slice the args, and add this application object as the + // first argument of the array + var args = slice.call(arguments); + args.unshift(this); + + // see the Marionette.Module object for more information + return ModuleClass.create.apply(ModuleClass, args); + }, + + // Internal method to set up the region manager + _initRegionManager: function() { + this._regionManager = new Marionette.RegionManager(); + + this.listenTo(this._regionManager, 'before:add:region', function(name) { + this.triggerMethod('before:add:region', name); + }); + + this.listenTo(this._regionManager, 'add:region', function(name, region) { + this[name] = region; + this.triggerMethod('add:region', name, region); + }); + + this.listenTo(this._regionManager, 'before:remove:region', function(name) { + this.triggerMethod('before:remove:region', name); + }); + + this.listenTo(this._regionManager, 'remove:region', function(name, region) { + delete this[name]; + this.triggerMethod('remove:region', name, region); + }); + }, + + // import the `triggerMethod` to trigger events with corresponding + // methods if the method exists + triggerMethod: Marionette.triggerMethod + }); + + // Copy the `extend` function used by Backbone's classes + Marionette.Application.extend = Marionette.extend; + + /* jshint maxparams: 9 */ + + // Module + // ------ + + // A simple module system, used to create privacy and encapsulation in + // Marionette applications + Marionette.Module = function(moduleName, app, options) { + this.moduleName = moduleName; + this.options = _.extend({}, this.options, options); + // Allow for a user to overide the initialize + // for a given module instance. + this.initialize = options.initialize || this.initialize; + + // Set up an internal store for sub-modules. + this.submodules = {}; + + this._setupInitializersAndFinalizers(); + + // Set an internal reference to the app + // within a module. + this.app = app; + + // By default modules start with their parents. + this.startWithParent = true; + + if (_.isFunction(this.initialize)) { + this.initialize(moduleName, app, this.options); + } + }; + + Marionette.Module.extend = Marionette.extend; + + // Extend the Module prototype with events / listenTo, so that the module + // can be used as an event aggregator or pub/sub. + _.extend(Marionette.Module.prototype, Backbone.Events, { + + // Initialize is an empty function by default. Override it with your own + // initialization logic when extending Marionette.Module. + initialize: function() {}, + + // Initializer for a specific module. Initializers are run when the + // module's `start` method is called. + addInitializer: function(callback) { + this._initializerCallbacks.add(callback); + }, + + // Finalizers are run when a module is stopped. They are used to teardown + // and finalize any variables, references, events and other code that the + // module had set up. + addFinalizer: function(callback) { + this._finalizerCallbacks.add(callback); + }, + + // Start the module, and run all of its initializers + start: function(options) { + // Prevent re-starting a module that is already started + if (this._isInitialized) { return; } + + // start the sub-modules (depth-first hierarchy) + _.each(this.submodules, function(mod) { + // check to see if we should start the sub-module with this parent + if (mod.startWithParent) { + mod.start(options); + } + }); + + // run the callbacks to "start" the current module + this.triggerMethod('before:start', options); + + this._initializerCallbacks.run(options, this); + this._isInitialized = true; + + this.triggerMethod('start', options); + }, + + // Stop this module by running its finalizers and then stop all of + // the sub-modules for this module + stop: function() { + // if we are not initialized, don't bother finalizing + if (!this._isInitialized) { return; } + this._isInitialized = false; + + this.triggerMethod('before:stop'); + + // stop the sub-modules; depth-first, to make sure the + // sub-modules are stopped / finalized before parents + _.each(this.submodules, function(mod) { mod.stop(); }); + + // run the finalizers + this._finalizerCallbacks.run(undefined, this); + + // reset the initializers and finalizers + this._initializerCallbacks.reset(); + this._finalizerCallbacks.reset(); + + this.triggerMethod('stop'); + }, + + // Configure the module with a definition function and any custom args + // that are to be passed in to the definition function + addDefinition: function(moduleDefinition, customArgs) { + this._runModuleDefinition(moduleDefinition, customArgs); + }, + + // Internal method: run the module definition function with the correct + // arguments + _runModuleDefinition: function(definition, customArgs) { + // If there is no definition short circut the method. + if (!definition) { return; } + + // build the correct list of arguments for the module definition + var args = _.flatten([ + this, + this.app, + Backbone, + Marionette, + Backbone.$, _, + customArgs + ]); + + definition.apply(this, args); + }, + + // Internal method: set up new copies of initializers and finalizers. + // Calling this method will wipe out all existing initializers and + // finalizers. + _setupInitializersAndFinalizers: function() { + this._initializerCallbacks = new Marionette.Callbacks(); + this._finalizerCallbacks = new Marionette.Callbacks(); + }, + + // import the `triggerMethod` to trigger events with corresponding + // methods if the method exists + triggerMethod: Marionette.triggerMethod + }); + + // Class methods to create modules + _.extend(Marionette.Module, { + + // Create a module, hanging off the app parameter as the parent object. + create: function(app, moduleNames, moduleDefinition) { + var module = app; + + // get the custom args passed in after the module definition and + // get rid of the module name and definition function + var customArgs = slice.call(arguments); + customArgs.splice(0, 3); + + // Split the module names and get the number of submodules. + // i.e. an example module name of `Doge.Wow.Amaze` would + // then have the potential for 3 module definitions. + moduleNames = moduleNames.split('.'); + var length = moduleNames.length; + + // store the module definition for the last module in the chain + var moduleDefinitions = []; + moduleDefinitions[length - 1] = moduleDefinition; + + // Loop through all the parts of the module definition + _.each(moduleNames, function(moduleName, i) { + var parentModule = module; + module = this._getModule(parentModule, moduleName, app, moduleDefinition); + this._addModuleDefinition(parentModule, module, moduleDefinitions[i], customArgs); + }, this); + + // Return the last module in the definition chain + return module; + }, + + _getModule: function(parentModule, moduleName, app, def, args) { + var options = _.extend({}, def); + var ModuleClass = this.getClass(def); + + // Get an existing module of this name if we have one + var module = parentModule[moduleName]; + + if (!module) { + // Create a new module if we don't have one + module = new ModuleClass(moduleName, app, options); + parentModule[moduleName] = module; + // store the module on the parent + parentModule.submodules[moduleName] = module; + } + + return module; + }, + + // ## Module Classes + // + // Module classes can be used as an alternative to the define pattern. + // The extend function of a Module is identical to the extend functions + // on other Backbone and Marionette classes. + // This allows module lifecyle events like `onStart` and `onStop` to be called directly. + getClass: function(moduleDefinition) { + var ModuleClass = Marionette.Module; + + if (!moduleDefinition) { + return ModuleClass; + } + + // If all of the module's functionality is defined inside its class, + // then the class can be passed in directly. `MyApp.module("Foo", FooModule)`. + if (moduleDefinition.prototype instanceof ModuleClass) { + return moduleDefinition; + } + + return moduleDefinition.moduleClass || ModuleClass; + }, + + // Add the module definition and add a startWithParent initializer function. + // This is complicated because module definitions are heavily overloaded + // and support an anonymous function, module class, or options object + _addModuleDefinition: function(parentModule, module, def, args) { + var fn = this._getDefine(def); + var startWithParent = this._getStartWithParent(def, module); + + if (fn) { + module.addDefinition(fn, args); + } + + this._addStartWithParent(parentModule, module, startWithParent); + }, + + _getStartWithParent: function(def, module) { + var swp; + + if (_.isFunction(def) && (def.prototype instanceof Marionette.Module)) { + swp = module.constructor.prototype.startWithParent; + return _.isUndefined(swp) ? true : swp; + } + + if (_.isObject(def)) { + swp = def.startWithParent; + return _.isUndefined(swp) ? true : swp; + } + + return true; + }, + + _getDefine: function(def) { + if (_.isFunction(def) && !(def.prototype instanceof Marionette.Module)) { + return def; + } + + if (_.isObject(def)) { + return def.define; + } + + return null; + }, + + _addStartWithParent: function(parentModule, module, startWithParent) { + module.startWithParent = module.startWithParent && startWithParent; + + if (!module.startWithParent || !!module.startWithParentIsConfigured) { + return; + } + + module.startWithParentIsConfigured = true; + + parentModule.addInitializer(function(options) { + if (module.startWithParent) { + module.start(options); + } + }); + } + }); + + + return Marionette; +})); |
