// MarionetteJS (Backbone.Marionette) // ---------------------------------- // v1.7.4 // // 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/ */ // Backbone.BabySitter // ------------------- // v0.1.0 // // Copyright (c)2014 Derick Bailey, Muted Solutions, LLC. // Distributed under MIT license // // http://github.com/marionettejs/backbone.babysitter // Backbone.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.Wreqr (Backbone.Marionette) // ---------------------------------- // v1.0.0 // // Copyright (c)2014 Derick Bailey, Muted Solutions, LLC. // Distributed under MIT license // // http://github.com/marionettejs/backbone.wreqr Backbone.Wreqr = (function(Backbone, Marionette, _){ "use strict"; var Wreqr = {}; // 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){ throw new Error("Handler not found for '" + name + "'"); } 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); 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, _); return Wreqr; })(Backbone, Backbone.Marionette, _); var Marionette = (function(global, Backbone, _){ "use strict"; // Define and export the Marionette namespace var Marionette = {}; Backbone.Marionette = Marionette; // Get the DOM manipulator for later use Marionette.$ = Backbone.$; // 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 && (optionName in target.options) && (target.options[optionName] !== undefined)){ value = target.options[optionName]; } else { value = target[optionName]; } 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) { 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; }; // 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]; // trigger the event, if a trigger method exists if(_.isFunction(this.trigger)) { this.trigger.apply(this, arguments); } // call the onMethodName if it exists if (_.isFunction(method)) { // pass all arguments, except the event name return method.apply(this, _.tail(arguments)); } }; 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); // 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); }; })(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){ this._callbacks.push({cb: callback, ctx: contextOverride}); this._deferred.done(function(context, options){ if (contextOverride){ context = contextOverride; } callback.call(context, 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(context, options); }, // 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, { close: function(){ this.stopListening(); var args = Array.prototype.slice.call(arguments); this.triggerMethod.apply(this, ["close"].concat(args)); this.unbind(); } }); // 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 = Marionette.getOption(this, "el"); if (!this.el){ throwError("An 'el' must be specified for a region.", "NoElError"); } if (this.initialize){ var args = Array.prototype.slice.apply(arguments); this.initialize.apply(this, args); } }; // Region Type methods // ------------------- _.extend(Marionette.Region, { // Build an instance of a region by passing in a configuration object // and a default region type to use if none is specified in the config. // // The config object should either be a string as a jQuery DOM selector, // a Region type directly, or an object literal that specifies both // a selector and regionType: // // ```js // { // selector: "#foo", // regionType: MyCustomRegion // } // ``` // buildRegion: function(regionConfig, defaultRegionType){ var regionIsString = _.isString(regionConfig); var regionSelectorIsString = _.isString(regionConfig.selector); var regionTypeIsUndefined = _.isUndefined(regionConfig.regionType); var regionIsType = _.isFunction(regionConfig); if (!regionIsType && !regionIsString && !regionSelectorIsString) { throwError("Region must be specified as a Region type, a selector string or an object with selector property"); } var selector, RegionType; // get the selector for the region if (regionIsString) { selector = regionConfig; } if (regionConfig.selector) { selector = regionConfig.selector; delete regionConfig.selector; } // get the type for the region if (regionIsType){ RegionType = regionConfig; } if (!regionIsType && regionTypeIsUndefined) { RegionType = defaultRegionType; } if (regionConfig.regionType) { RegionType = regionConfig.regionType; delete regionConfig.regionType; } if (regionIsString || regionIsType) { regionConfig = {}; } regionConfig.el = selector; // build the region instance var region = new RegionType(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(selector) { var parentEl = regionConfig.parentEl; if (_.isFunction(parentEl)){ parentEl = parentEl(); } return parentEl.find(selector); }; } 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 `close` method on your view, just after showing // or just before closing the view, respectively. show: function(view){ this.ensureEl(); var isViewClosed = view.isClosed || _.isUndefined(view.$el); var isDifferentView = view !== this.currentView; if (isDifferentView) { this.close(); } view.render(); Marionette.triggerMethod.call(this, "before:show", view); Marionette.triggerMethod.call(view, "before:show"); if (isDifferentView || isViewClosed) { this.open(view); } this.currentView = view; Marionette.triggerMethod.call(this, "show", view); Marionette.triggerMethod.call(view, "show"); }, ensureEl: function(){ if (!this.$el || this.$el.length === 0){ this.$el = this.getEl(this.el); } }, // Override this method to change how the region finds the // DOM element that it manages. Return a jQuery selector object. getEl: function(selector){ return Marionette.$(selector); }, // Override this method to change how the new view is // appended to the `$el` that the region is managing open: function(view){ this.$el.empty().append(view.el); }, // Close the current view, if there is one. If there is no // current view, it does nothing and returns immediately. close: function(){ var view = this.currentView; if (!view || view.isClosed){ return; } // call 'close' or 'remove', depending on which is found if (view.close) { view.close(); } else if (view.remove) { view.remove(); } Marionette.triggerMethod.call(this, "close", 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 closing 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.close(); delete this.$el; } }); // 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.prototype.constructor.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._store(name, region); this.triggerMethod("region:add", name, region); return region; }, // Get a region by name get: function(name){ return this._regions[name]; }, // Remove a region by name removeRegion: function(name){ var region = this._regions[name]; this._remove(name, region); }, // Close all regions in the region manager, and // remove them removeRegions: function(){ _.each(this._regions, function(region, name){ this._remove(name, region); }, this); }, // Close all regions in the region manager, but // leave them attached closeRegions: function(){ _.each(this._regions, function(region, name){ region.close(); }, this); }, // Close all regions and shut down the region // manager entirely close: function(){ this.removeRegions(); Marionette.Controller.prototype.close.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){ region.close(); delete this._regions[name]; this._setLength(); this.triggerMethod("region:remove", name, region); }, // set the number of regions current held _setLength: function(){ this.length = _.size(this._regions); } }); // 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) { RegionManager.prototype[method] = function() { var regions = _.values(this._regions); var args = [regions].concat(_.toArray(arguments)); return _[method].apply(_, args); }; }); return RegionManager; })(Marionette); // Template Cache // -------------- // Manage templates stored in `