// Backbone.Marionette, v1.0.0-rc5 // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. // Distributed under MIT license // http://github.com/marionettejs/backbone.marionette /*! * Includes BabySitter * https://github.com/marionettejs/backbone.babysitter/ * * Includes Wreqr * https://github.com/marionettejs/backbone.wreqr/ */ // Backbone.BabySitter, v0.0.4 // Copyright (c)2012 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 (initialViews) { this._views = {}; this._indexByModel = {}; this._indexByCollection = {}; this._indexByCustom = {}; this._updateLength(); this._addInitialViews(initialViews); }; // Container Methods // ----------------- _.extend(Container.prototype, { // Add a view to this container. Stores the view // by `cid` and makes it searchable by the model // and/or collection of the view. 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 it by collection if (view.collection) { this._indexByCollection[view.collection.cid] = viewCid; } // index by custom if (customIndex) { this._indexByCustom[customIndex] = viewCid; } this._updateLength(); }, // Find a view by the model that was attached to // it. Uses the model's `cid` to find it, and // retrieves the view by it's `cid` from the result findByModel: function (model) { var viewCid = this._indexByModel[model.cid]; return this.findByCid(viewCid); }, // Find a view by the collection that was attached to // it. Uses the collection's `cid` to find it, and // retrieves the view by it's `cid` from the result findByCollection: function (col) { var viewCid = this._indexByCollection[col.cid]; 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 it's `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 collection index if (view.collection) { delete this._indexByCollection[view.collection.cid]; } // delete custom index var cust; for (var key in this._indexByCustom) { if (this._indexByCustom.hasOwnProperty(key)) { if (this._indexByCustom[key] === viewCid) { cust = key; break; } } } if (cust) { delete this._indexByCustom[cust]; } // remove the view from the container delete this._views[viewCid]; // update the length this._updateLength(); }, // 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, args) { args = Array.prototype.slice.call(arguments, 1); this.apply(method, args); }, // 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) { var view; // fix for IE < 9 args = args || []; _.each(this._views, function (view, key) { if (_.isFunction(view[method])) { view[method].apply(view, args); } }); }, // Update the `.length` attribute on this container _updateLength: function () { this.length = _.size(this._views); }, // set up an initial list of views _addInitialViews: function (views) { if (!views) { return; } var view, i, length = views.length; for (i = 0; i < length; i++) { view = views[i]; this.add(view); } } }); // 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, v0.1.1 // Copyright (c)2013 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 () { this._handlers = {}; }; Handlers.extend = Backbone.Model.extend; // Instance Members // ---------------- _.extend(Handlers.prototype, { // Add a handler for the given name, with an // optional context to run the handler within addHandler: function (name, handler, context) { var config = { callback: handler, context: context }; this._handlers[name] = config; }, // Get the currently registered handler for // the specified name. Throws an exception if // no handler is found. getHandler: function (name) { var config = this._handlers[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._handlers[name]; }, // Remove all handlers from this registry removeAllHandlers: function () { this._handlers = {}; } }); return Handlers; })(Backbone, _); // Wreqr.Commands // -------------- // // A simple command pattern implementation. Register a command // handler and execute it. Wreqr.Commands = (function (Wreqr) { "use strict"; return Wreqr.Handlers.extend({ execute: function () { var name = arguments[0]; var args = Array.prototype.slice.call(arguments, 1); this.getHandler(name).apply(this, args); } }); })(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 (Backbone, _, $) { "use strict"; var Marionette = {}; Backbone.Marionette = Marionette; // Helpers // ------- // For slicing `arguments` in functions var slice = Array.prototype.slice; // 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 it's `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; }; // Mairionette.createObject // ------------------------ // A wrapper / shim for `Object.create`. Uses native `Object.create` // if available, otherwise shims it in place for Marionette to use. Marionette.createObject = (function () { var createObject; // Define this once, and just replace the .prototype on it as needed, // to improve performance in older / less optimized JS engines function F() { } // Check for existing native / shimmed Object.create if (typeof Object.create === "function") { // found native/shim, so use it createObject = Object.create; } else { // An implementation of the Boodman/Crockford delegation // w/ Cornford optimization, as suggested by @unscriptable // https://gist.github.com/3959151 // native/shim not found, so shim it ourself createObject = function (o) { // set the prototype of the function // so we will get `o` as the prototype // of the new object instance F.prototype = o; // create a new object that inherits from // the `o` parameter var child = new F(); // clean up just in case o is really large F.prototype = null; // send it back return child; }; } return createObject; })(); // Trigger an event and 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 () { var args = Array.prototype.slice.apply(arguments); var eventName = args[0]; var segments = eventName.split(":"); var segment, capLetter, methodName = "on"; for (var i = 0; i < segments.length; i++) { segment = segments[i]; capLetter = segment.charAt(0).toUpperCase(); methodName += capLetter + segment.slice(1); } this.trigger.apply(this, args); if (_.isFunction(this[methodName])) { args.shift(); return this[methodName].apply(this, args); } }; // 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 () { // track when the view has been rendered function handleShow(view) { view._isShown = true; triggerDOMRefresh(view); } // track when the view has been shown in the DOM, // using a Marionette.Region (or by other means of triggering "show") 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) { if (_.isFunction(view.triggerMethod)) { view.triggerMethod("dom:refresh"); } } } // Export public API return function (view) { view.listenTo(view, "show", function () { handleShow(view); }); view.listenTo(view, "render", function () { handleRender(view); }); }; })(); // Marionette.bindEntityEvents & unbindEntityEvents // --------------------------- // // These methods are used to bind/unbind a backbone "entity" (collection/model) // to methods on a target object. // // The first paremter, `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) { throw new Error("Method '" + methodName + "' was configured as an event handler, but does not exist."); } target.listenTo(entity, evt, method, target); }); } // Bind the event to a supplied callback function function bindToFunction(target, entity, evt, method) { target.listenTo(entity, evt, method, target); } // 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[method]; target.stopListening(entity, evt, method, target); }); } // Bind the event to a supplied callback function function unbindToFunction(target, entity, evt, method) { target.stopListening(entity, evt, method, target); } // 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 = $.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 that = this; var callbacks = this._callbacks; this._deferred = $.Deferred(); this._callbacks = []; _.each(callbacks, function (cb) { that.add(cb.cb, cb.ctx); }); } }); // 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(); this.triggerMethod("close"); 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) { var err = new Error("An 'el' must be specified for a region."); err.name = "NoElError"; throw err; } 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 = (typeof regionConfig === "string"); var regionSelectorIsString = (typeof regionConfig.selector === "string"); var regionTypeIsUndefined = (typeof regionConfig.regionType === "undefined"); var regionIsType = (typeof regionConfig === "function"); if (!regionIsType && !regionIsString && !regionSelectorIsString) { throw new Error("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; } // get the type for the region if (regionIsType) { RegionType = regionConfig; } if (!regionIsType && regionTypeIsUndefined) { RegionType = defaultRegionType; } if (regionConfig.regionType) { RegionType = regionConfig.regionType; } // build the region instance var regionManager = new RegionType({ el: selector }); return regionManager; } }); // 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(); this.close(); view.render(); this.open(view); Marionette.triggerMethod.call(view, "show"); Marionette.triggerMethod.call(this, "show", view); this.currentView = view; }, 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 $(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; } if (view.close) { view.close(); } Marionette.triggerMethod.call(this, "close"); 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; // Template Cache // -------------- // Manage templates stored in `