// MarionetteJS (Backbone.Marionette) // ---------------------------------- // v2.3.2 // // Copyright (c)2015 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) { /* istanbul ignore next */ if (typeof define === 'function' && define.amd) { define(['backbone', 'underscore'], function(Backbone, _) { return (root.Marionette = root.Mn = factory(root, Backbone, _)); }); } else if (typeof exports !== 'undefined') { var Backbone = require('backbone'); var _ = require('underscore'); module.exports = factory(root, Backbone, _); } else { root.Marionette = root.Mn = factory(root, root.Backbone, root._); } }(this, function(root, Backbone, _) { 'use strict'; /* istanbul ignore next */ // Backbone.BabySitter // ------------------- // v0.1.5 // // 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.5"; Backbone.ChildViewContainer.noConflict = function() { Backbone.ChildViewContainer = previousChildViewContainer; return this; }; return Backbone.ChildViewContainer; })(Backbone, _); /* istanbul ignore next */ // 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.3.2'; 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 *//* global console */ // Helpers // ------- // Marionette.extend // ----------------- // Borrow the Backbone `extend` method so we can use it as needed Marionette.extend = Backbone.Model.extend; // Marionette.isNodeAttached // ------------------------- // Determine if `el` is a child of the document Marionette.isNodeAttached = function(el) { return Backbone.$.contains(document.documentElement, el); }; // 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; } if (target.options && (target.options[optionName] !== undefined)) { return target.options[optionName]; } else { return target[optionName]; } }; // Proxy `Marionette.getOption` Marionette.proxyGetOption = function(optionName) { return Marionette.getOption(this, optionName); }; // Similar to `_.result`, this is a simple helper // If a function is provided we call it with context // otherwise just return the value. If the value is // undefined return a default value Marionette._getValue = function(value, context, params) { if (_.isFunction(value)) { // We need to ensure that params is not undefined // to prevent `apply` from failing in ie8 params = params || []; value = value.apply(context, params); } return value; }; // Marionette.normalizeMethods // ---------------------- // Pass in a mapping of events => functions or function names // and return a mapping of events => functions Marionette.normalizeMethods = function(hash) { return _.reduce(hash, function(normalizedHash, method, name) { if (!_.isFunction(method)) { method = this[method]; } if (method) { normalizedHash[name] = method; } return normalizedHash; }, {}, this); }; // utility method for parsing @ui. syntax strings // into associated selector Marionette.normalizeUIString = function(uiString, ui) { return uiString.replace(/@ui\.[a-zA-Z_$0-9]*/g, function(r) { return ui[r.slice(4)]; }); }; // allows for the use of the @ui. syntax within // a given key for triggers and events // swaps the @ui with the associated selector. // Returns a new, non-mutated, parsed events hash. Marionette.normalizeUIKeys = function(hash, ui) { return _.reduce(hash, function(memo, val, key) { var normalizedKey = Marionette.normalizeUIString(key, ui); memo[normalizedKey] = val; return memo; }, {}); }; // allows for the use of the @ui. syntax within // a given value for regions // swaps the @ui with the associated selector Marionette.normalizeUIValues = function(hash, ui) { _.each(hash, function(val, key) { if (_.isString(val)) { hash[key] = Marionette.normalizeUIString(val, ui); } }); 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-121 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); }; }); }; var deprecate = Marionette.deprecate = function(message, test) { if (_.isObject(message)) { message = ( message.prev + ' is going to be removed in the future. ' + 'Please use ' + message.next + ' instead.' + (message.url ? ' See: ' + message.url : '') ); } if ((test === undefined || !test) && !deprecate._cache[message]) { deprecate._warn('Deprecation warning: ' + message); deprecate._cache[message] = true; } }; deprecate._warn = typeof console !== 'undefined' && (console.warn || console.log) || function() {}; deprecate._cache = {}; /* jshint maxstatements: 14, maxcomplexity: 7 */ // Trigger 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(); } return function(context, event, args) { var noEventArg = arguments.length < 3; if (noEventArg) { args = event; event = args[0]; } // get the method name from the event name var methodName = 'on' + event.replace(splitter, getEventName); var method = context[methodName]; var result; // call the onMethodName if it exists if (_.isFunction(method)) { // pass all args, except the event name result = method.apply(context, noEventArg ? _.rest(args) : args); } // trigger the event, if a trigger method exists if (_.isFunction(context.trigger)) { if (noEventArg + args.length > 1) { context.trigger.apply(context, noEventArg ? args : [event].concat(_.rest(args, 0))); } else { context.trigger(event); } } return result; }; })(); // 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(event) { return Marionette._triggerMethod(this, arguments); }; // triggerMethodOn invokes triggerMethod on a specific context // // e.g. `Marionette.triggerMethodOn(view, 'show')` // will trigger a "show" event or invoke onShow the view. Marionette.triggerMethodOn = function(context) { var fnc = _.isFunction(context.triggerMethod) ? context.triggerMethod : Marionette.triggerMethod; return fnc.apply(context, _.rest(arguments)); }; // DOM Refresh // ----------- // 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(view) { // track when the view has been shown in the DOM, // using a Marionette.Region (or by other means of triggering "show") function handleShow() { view._isShown = true; triggerDOMRefresh(); } // track when the view has been rendered function handleRender() { view._isRendered = true; triggerDOMRefresh(); } // Trigger the "dom:refresh" event and corresponding "onDomRefresh" method function triggerDOMRefresh() { if (view._isShown && view._isRendered && Marionette.isNodeAttached(view.el)) { if (_.isFunction(view.triggerMethod)) { view.triggerMethod('dom:refresh'); } } } view.on({ show: handleShow, render: handleRender }); }; /* jshint maxparams: 5 */ // Bind Entity Events & Unbind Entity Events // ----------------------------------------- // // These methods are used to bind/unbind a backbone "entity" (e.g. collection/model) // to methods on a target object. // // The first parameter, `target`, must have the Backbone.Events module mixed in. // // The second parameter is the `entity` (Backbone.Model, Backbone.Collection or // any object that has Backbone.Events mixed in) 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) { throw new Marionette.Error('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; } // type-check bindings if (!_.isObject(bindings)) { throw new Marionette.Error({ message: 'Bindings must be an object or function.', url: 'marionette.functions.html#marionettebindentityevents' }); } // allow the bindings to be a function bindings = Marionette._getValue(bindings, 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); // Error // ----- var errorProps = ['description', 'fileName', 'lineNumber', 'name', 'message', 'number']; Marionette.Error = Marionette.extend.call(Error, { urlRoot: 'http://marionettejs.com/docs/v' + Marionette.VERSION + '/', constructor: function(message, options) { if (_.isObject(message)) { options = message; message = options.message; } else if (!options) { options = {}; } var error = Error.call(this, message); _.extend(this, _.pick(error, errorProps), _.pick(options, errorProps)); this.captureStackTrace(); if (options.url) { this.url = this.urlRoot + options.url; } }, captureStackTrace: function() { if (Error.captureStackTrace) { Error.captureStackTrace(this, Marionette.Error); } }, toString: function() { return this.name + ': ' + this.message + (this.url ? ' See: ' + this.url : ''); } }); Marionette.Error.extend = Marionette.extend; // 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); } }); // 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.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() { Marionette._triggerMethod(this, 'before:destroy', arguments); Marionette._triggerMethod(this, 'destroy', arguments); this.stopListening(); this.off(); return this; }, // 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 }); // Object // ------ // A Base Class that other Classes should descend from. // Object borrows many conventions and utilities from Backbone. Marionette.Object = function(options) { this.options = _.extend({}, _.result(this, 'options'), options); this.initialize.apply(this, arguments); }; Marionette.Object.extend = Marionette.extend; // Object Methods // -------------- // Ensure it can trigger events with Backbone.Events _.extend(Marionette.Object.prototype, Backbone.Events, { //this is a noop method intended to be overridden by classes that extend from this base initialize: function() {}, destroy: function() { this.triggerMethod('before:destroy'); this.triggerMethod('destroy'); 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 `bindEntityEvents` 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 }); /* jshint maxcomplexity: 16, maxstatements: 45, maxlen: 120 */ // 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 = Marionette.Object.extend({ constructor: function (options) { // set options temporarily so that we can get `el`. // options will be overriden by Object.constructor 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) { throw new Marionette.Error({ name: 'NoElError', message: 'An "el" must be specified for a region.' }); } this.$el = this.getEl(this.el); Marionette.Object.call(this, options); }, // 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){ if (!this._ensureElement()) { return; } this._ensureViewIsIntact(view); 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 current view to change to begin with var isChangingView = !!this.currentView; // Only destroy the current view if we don't want to `preventDestroy` and if // the view given in the first argument is different than `currentView` var _shouldDestroyView = isDifferentView && !preventDestroy; // Only show the view given in the first argument if it is different than // the current view or if we want to re-show the view. Note that if // `_shouldDestroyView` is true, then `_shouldShowView` is also necessarily true. var _shouldShowView = isDifferentView || forceShow; if (isChangingView) { this.triggerMethod('before:swapOut', this.currentView, this, options); } if (this.currentView) { delete this.currentView._parent; } if (_shouldDestroyView) { this.empty(); // A `destroy` event is attached to the clean up manually removed views. // We need to detach this event when a new view is going to be shown as it // is no longer relevant. } else if (isChangingView && _shouldShowView) { this.currentView.off('destroy', this.empty, this); } if (_shouldShowView) { // We need to listen for if a view is destroyed // in a way other than through the region. // If this happens we need to remove the reference // to the currentView since once a view has been destroyed // we can not reuse it. view.once('destroy', this.empty, this); view.render(); view._parent = this; if (isChangingView) { this.triggerMethod('before:swap', view, this, options); } this.triggerMethod('before:show', view, this, options); Marionette.triggerMethodOn(view, 'before:show', view, this, options); if (isChangingView) { this.triggerMethod('swapOut', this.currentView, this, options); } // An array of views that we're about to display var attachedRegion = Marionette.isNodeAttached(this.el); // The views that we're about to attach to the document // It's important that we prevent _getNestedViews from being executed unnecessarily // as it's a potentially-slow method var displayedViews = []; var triggerBeforeAttach = showOptions.triggerBeforeAttach || this.triggerBeforeAttach; var triggerAttach = showOptions.triggerAttach || this.triggerAttach; if (attachedRegion && triggerBeforeAttach) { displayedViews = this._displayedViews(view); this._triggerAttach(displayedViews, 'before:'); } this.attachHtml(view); this.currentView = view; if (attachedRegion && triggerAttach) { displayedViews = this._displayedViews(view); this._triggerAttach(displayedViews); } if (isChangingView) { this.triggerMethod('swap', view, this, options); } this.triggerMethod('show', view, this, options); Marionette.triggerMethodOn(view, 'show', view, this, options); return this; } return this; }, triggerBeforeAttach: true, triggerAttach: true, _triggerAttach: function(views, prefix) { var eventName = (prefix || '') + 'attach'; _.each(views, function(view) { Marionette.triggerMethodOn(view, eventName, view, this); }, this); }, _displayedViews: function(view) { return _.union([view], _.result(view, '_getNestedViews') || []); }, _ensureElement: function(){ if (!_.isObject(this.el)) { this.$el = this.getEl(this.el); this.el = this.$el[0]; } if (!this.$el || this.$el.length === 0) { if (this.getOption('allowMissingEl')) { return false; } else { throw new Marionette.Error('An "el" ' + this.$el.selector + ' must exist in DOM'); } } return true; }, _ensureViewIsIntact: function(view) { if (!view) { throw new Marionette.Error({ name: 'ViewNotValid', message: 'The view passed is undefined and therefore invalid. You must pass a view instance to show.' }); } if (view.isDestroyed) { throw new Marionette.Error({ name: 'ViewDestroyedError', message: 'View (cid: "' + view.cid + '") has already been destroyed and cannot be used.' }); } }, // Override this method to change how the region finds the DOM // element that it manages. Return a jQuery selector object scoped // to a provided parent el or the document if none exists. getEl: function(el) { return Backbone.$(el, Marionette._getValue(this.options.parentEl, this)); }, // Override this method to change how the new view is // appended to the `$el` that the region is managing attachHtml: function(view) { this.$el.contents().detach(); 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 there is no view in the region // we should not remove anything if (!view) { return; } view.off('destroy', this.empty, this); this.triggerMethod('before:empty', view); this._destroyView(); this.triggerMethod('empty', view); // Remove region pointer to the currentView delete this.currentView; return this; }, // call 'destroy' or 'remove', depending on which is found // on the view (if showing a raw Backbone view or a Marionette View) _destroyView: function() { var view = this.currentView; if (view.destroy && !view.isDestroyed) { view.destroy(); } else if (view.remove) { view.remove(); // appending isDestroyed to raw Backbone View allows regions // to throw a ViewDestroyedError for this view view.isDestroyed = true; } }, // 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; return this; }, // Checks whether a view is currently present within // the region. Returns `true` if there is and `false` if // no view is present. hasView: function() { return !!this.currentView; }, // 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; return this; } }, // Static Methods { // 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 a selector, // a custom regionClass, and any options to be supplied to the region: // // ```js // { // selector: "#foo", // regionClass: MyCustomRegion, // allowMissingEl: false // } // ``` // buildRegion: function(regionConfig, DefaultRegionClass) { if (_.isString(regionConfig)) { return this._buildRegionFromSelector(regionConfig, DefaultRegionClass); } if (regionConfig.selector || regionConfig.el || regionConfig.regionClass) { return this._buildRegionFromObject(regionConfig, DefaultRegionClass); } if (_.isFunction(regionConfig)) { return this._buildRegionFromRegionClass(regionConfig); } throw new Marionette.Error({ message: 'Improper region configuration type.', url: 'marionette.region.html#region-configuration-types' }); }, // Build the region from a string selector like '#foo-region' _buildRegionFromSelector: function(selector, DefaultRegionClass) { return new DefaultRegionClass({ el: selector }); }, // Build the region from a configuration object // ```js // { selector: '#foo', regionClass: FooRegion, allowMissingEl: false } // ``` _buildRegionFromObject: function(regionConfig, DefaultRegionClass) { var RegionClass = regionConfig.regionClass || DefaultRegionClass; var options = _.omit(regionConfig, 'selector', 'regionClass'); if (regionConfig.selector && !options.el) { options.el = regionConfig.selector; } return new RegionClass(options); }, // Build the region directly from a given `RegionClass` _buildRegionFromRegionClass: function(RegionClass) { return new RegionClass(); } }); // Region Manager // -------------- // Manage one or more related `Marionette.Region` objects. Marionette.RegionManager = Marionette.Controller.extend({ constructor: function(options) { this._regions = {}; Marionette.Controller.call(this, options); this.addRegions(this.getOption('regions')); }, // Add multiple regions using an object literal or a // function that returns an object literal, where // each key becomes the region name, and each value is // the region definition. addRegions: function(regionDefinitions, defaults) { regionDefinitions = Marionette._getValue(regionDefinitions, this, arguments); return _.reduce(regionDefinitions, function(regions, definition, name) { if (_.isString(definition)) { definition = {selector: definition}; } if (definition.selector) { definition = _.defaults({}, definition, defaults); } regions[name] = this.addRegion(name, definition); return regions; }, {}, this); }, // Add an individual region to the region manager, // and return the region instance addRegion: function(name, definition) { var region; if (definition instanceof Marionette.Region) { region = definition; } else { region = Marionette.Region.buildRegion(definition, Marionette.Region); } this.triggerMethod('before:add:region', name, region); region._parent = this; 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); return region; }, // Empty all regions in the region manager, and // remove them removeRegions: function() { var regions = this.getRegions(); _.each(this._regions, function(region, name) { this._remove(name, region); }, this); return regions; }, // Empty all regions in the region manager, but // leave them attached emptyRegions: function() { var regions = this.getRegions(); _.invoke(regions, 'empty'); return regions; }, // Destroy all regions and shut down the region // manager entirely destroy: function() { this.removeRegions(); return 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 region._parent; 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(Marionette.RegionManager.prototype, '_regions'); // Template Cache // -------------- // Manage templates stored in `