diff options
Diffstat (limited to 'server/sonar-web/src/main/js/libs/third-party/backbone.marionette.js')
-rw-r--r-- | server/sonar-web/src/main/js/libs/third-party/backbone.marionette.js | 2572 |
1 files changed, 2572 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/libs/third-party/backbone.marionette.js b/server/sonar-web/src/main/js/libs/third-party/backbone.marionette.js new file mode 100644 index 00000000000..feb9eb4c48f --- /dev/null +++ b/server/sonar-web/src/main/js/libs/third-party/backbone.marionette.js @@ -0,0 +1,2572 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// MarionetteJS (Backbone.Marionette) +// ---------------------------------- +// v1.6.3 +// +// 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; +}; + +// 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(); + 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){ + 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(); + + 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 `<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 = Marionette.$(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 it's false, null or undefined.", "TemplateNotFoundError"); + } + + var templateFunc; + if (typeof template === "function"){ + templateFunc = template; + } else { + templateFunc = Marionette.TemplateCache.get(template); + } + + return templateFunc(data); + } +}; + + + +// Marionette.View +// --------------- + +// The core view type 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')); + Backbone.View.prototype.constructor.apply(this, arguments); + + Marionette.MonitorDOMRefresh(this); + this.listenTo(this, "show", this.onShowCalled); + }, + + // 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, + + // 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 Marionette.getOption(this, "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 = Marionette.getOption(this, "templateHelpers"); + if (_.isFunction(templateHelpers)){ + templateHelpers = templateHelpers.call(this); + } + return _.extend(target, templateHelpers); + }, + + // allows for the use of the @ui. syntax within + // a given key for triggers and events + // swaps the @ui with the associated selector + normalizeUIKeys: function(hash) { + var _this = this; + 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 _.result(_this, "ui")[r.slice(4)]; + })] = hash[v]; + delete hash[v]; + } + }); + + return hash; + }, + + // 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); + Marionette.bindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents")); + Marionette.bindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents")); + }, + + // internal method to delegate DOM events and triggers + _delegateDOMEvents: function(events){ + events = events || this.events; + if (_.isFunction(events)){ events = events.call(this); } + + var combinedEvents = {}; + var triggers = this.configureTriggers(); + _.extend(combinedEvents, 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); + + Marionette.unbindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents")); + Marionette.unbindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents")); + }, + + // Internal method, handles the `show` event. + onShowCalled: function(){}, + + // Default `close` implementation, for removing a view from the + // DOM and unbinding it. Regions will call this method + // for you. You can specify an `onClose` method in your view to + // add custom code that is called after the view is closed. + close: function(){ + if (this.isClosed) { return; } + + // allow the close to be stopped by returning `false` + // from the `onBeforeClose` method + var shouldClose = this.triggerMethod("before:close"); + if (shouldClose === false){ + return; + } + + // mark as closed before doing the actual close, to + // prevent infinite loops within "close" event handlers + // that are trying to close other views + this.isClosed = true; + this.triggerMethod("close"); + + // 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; + } +}); + +// 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.prototype.constructor.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.isClosed = false; + + this.triggerMethod("before:render", this); + this.triggerMethod("item:before:render", this); + + var data = this.serializeData(); + data = this.mixinTemplateHelpers(data); + + var template = this.getTemplate(); + var html = Marionette.Renderer.render(template, data); + + this.$el.html(html); + this.bindUIElements(); + + this.triggerMethod("render", this); + this.triggerMethod("item:rendered", this); + + return this; + }, + + // Override the default close event to add a few + // more events that are triggered. + close: function(){ + if (this.isClosed){ return; } + + this.triggerMethod('item:before:close'); + + Marionette.View.prototype.close.apply(this, arguments); + + this.triggerMethod('item:closed'); + } +}); + +// Collection View +// --------------- + +// A view that iterates over a Backbone.Collection +// and renders an individual ItemView for each model. +Marionette.CollectionView = Marionette.View.extend({ + // used as the prefix for item view events + // that are forwarded through the collectionview + itemViewEventPrefix: "itemview", + + // constructor + constructor: function(options){ + this._initChildViewStorage(); + + Marionette.View.prototype.constructor.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.appendBuffer(this, this.elBuffer); + this._triggerShowBufferedChildren(); + this.initRenderBuffer(); + }, + + _triggerShowBufferedChildren: function () { + if (this._isShown) { + _.each(this._bufferedChildren, function (child) { + 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.addChildView); + this.listenTo(this.collection, "remove", this.removeItemView); + this.listenTo(this.collection, "reset", this.render); + } + }, + + // Handle a child item added to the collection + addChildView: function(item, collection, options){ + this.closeEmptyView(); + var ItemView = this.getItemView(item); + var index = this.collection.indexOf(item); + this.addItemView(item, ItemView, index); + }, + + // Override from `Marionette.View` to guarantee the `onShow` method + // of child views is called. + onShowCalled: function(){ + this.children.each(function(child){ + Marionette.triggerMethod.call(child, "show"); + }); + }, + + // Internal method to trigger the before render callbacks + // and events + triggerBeforeRender: function(){ + this.triggerMethod("before:render", this); + this.triggerMethod("collection:before:render", this); + }, + + // Internal method to trigger the rendered callbacks and + // events + triggerRendered: function(){ + this.triggerMethod("render", this); + this.triggerMethod("collection:rendered", this); + }, + + // Render the collection of items. Override this method to + // provide your own implementation of a render function for + // the collection view. + render: function(){ + this.isClosed = false; + this.triggerBeforeRender(); + this._renderChildren(); + this.triggerRendered(); + return this; + }, + + // Internal method. Separated so that CompositeView can have + // more control over events being triggered, around the rendering + // process + _renderChildren: function(){ + this.startBuffering(); + + this.closeEmptyView(); + this.closeChildren(); + + if (!this.isEmpty(this.collection)) { + this.showCollection(); + } else { + this.showEmptyView(); + } + + this.endBuffering(); + }, + + // Internal method to loop through each item in the + // collection view and show it + showCollection: function(){ + var ItemView; + this.collection.each(function(item, index){ + ItemView = this.getItemView(item); + this.addItemView(item, ItemView, index); + }, this); + }, + + // Internal method to show an empty view in place of + // a collection of item views, when the collection is + // empty + showEmptyView: function(){ + var EmptyView = this.getEmptyView(); + + if (EmptyView && !this._showingEmptyView){ + this._showingEmptyView = true; + var model = new Backbone.Model(); + this.addItemView(model, EmptyView, 0); + } + }, + + // Internal method to close an existing emptyView instance + // if one exists. Called when a collection view has been + // rendered empty, and then an item is added to the collection. + closeEmptyView: function(){ + if (this._showingEmptyView){ + this.closeChildren(); + delete this._showingEmptyView; + } + }, + + // Retrieve the empty view type + getEmptyView: function(){ + return Marionette.getOption(this, "emptyView"); + }, + + // Retrieve the itemView type, either from `this.options.itemView` + // or from the `itemView` in the object definition. The "options" + // takes precedence. + getItemView: function(item){ + var itemView = Marionette.getOption(this, "itemView"); + + if (!itemView){ + throwError("An `itemView` must be specified", "NoItemViewError"); + } + + return itemView; + }, + + // Render the child item's view and add it to the + // HTML for the collection view. + addItemView: function(item, ItemView, index){ + // get the itemViewOptions if any were specified + var itemViewOptions = Marionette.getOption(this, "itemViewOptions"); + if (_.isFunction(itemViewOptions)){ + itemViewOptions = itemViewOptions.call(this, item, index); + } + + // build the view + var view = this.buildItemView(item, ItemView, itemViewOptions); + + // set up the child view event forwarding + this.addChildViewEventForwarding(view); + + // this view is about to be added + this.triggerMethod("before:item:added", view); + + // Store the child view itself so we can properly + // remove and/or close it later + this.children.add(view); + + // Render it and show it + this.renderItemView(view, index); + + // call the "show" method if the collection view + // has already been shown + if (this._isShown && !this.isBuffering){ + Marionette.triggerMethod.call(view, "show"); + } + + // this view was added + this.triggerMethod("after:item:added", view); + + return view; + }, + + // Set up the child view event forwarding. Uses an "itemview:" + // prefix in front of all forwarded events. + addChildViewEventForwarding: function(view){ + var prefix = Marionette.getOption(this, "itemViewEventPrefix"); + + // Forward all child item view events through the parent, + // prepending "itemview:" to the event name + this.listenTo(view, "all", function(){ + var args = slice.call(arguments); + var rootEvent = args[0]; + var itemEvents = this.normalizeMethods(this.getItemEvents()); + + args[0] = prefix + ":" + rootEvent; + args.splice(1, 0, view); + + // call collectionView itemEvent if defined + if (typeof itemEvents !== "undefined" && _.isFunction(itemEvents[rootEvent])) { + itemEvents[rootEvent].apply(this, args); + } + + Marionette.triggerMethod.apply(this, args); + }, this); + }, + + // returns the value of itemEvents depending on if a function + getItemEvents: function() { + if (_.isFunction(this.itemEvents)) { + return this.itemEvents.call(this); + } + + return this.itemEvents; + }, + + // render the item view + renderItemView: function(view, index) { + view.render(); + this.appendHtml(this, view, index); + }, + + // Build an `itemView` for every model in the collection. + buildItemView: function(item, ItemViewType, itemViewOptions){ + var options = _.extend({model: item}, itemViewOptions); + return new ItemViewType(options); + }, + + // get the child view by item it holds, and remove it + removeItemView: function(item){ + var view = this.children.findByModel(item); + this.removeChildView(view); + this.checkEmpty(); + }, + + // Remove the child view and close it + removeChildView: function(view){ + + // shut down the child view properly, + // including events that the collection has from it + if (view){ + this.stopListening(view); + + // call 'close' or 'remove', depending on which is found + if (view.close) { view.close(); } + else if (view.remove) { view.remove(); } + + this.children.remove(view); + } + + this.triggerMethod("item:removed", view); + }, + + // helper to check if the collection is empty + isEmpty: function(collection){ + // check if we're empty now + 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 appendHtml + appendBuffer: function(collectionView, buffer) { + collectionView.$el.append(buffer); + }, + + // Append the HTML to the collection's `el`. + // Override this method to do something other + // than `.append`. + appendHtml: function(collectionView, itemView, 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(itemView.el); + collectionView._bufferedChildren.push(itemView); + } + else { + // If we've already rendered the main collection, just + // append the new items directly into the element. + collectionView.$el.append(itemView.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 closing needs for + // the collection of views. + close: function(){ + if (this.isClosed){ return; } + + this.triggerMethod("collection:before:close"); + this.closeChildren(); + this.triggerMethod("collection:closed"); + + Marionette.View.prototype.close.apply(this, arguments); + }, + + // Close the child views that this collection view + // is holding on to, if any + closeChildren: function(){ + this.children.each(function(child){ + this.removeChildView(child); + }, this); + this.checkEmpty(); + } +}); + + +// Composite View +// -------------- + +// Used for rendering a branch-leaf, hierarchical structure. +// Extends directly from CollectionView and also renders an +// an item 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 + constructor: function(){ + Marionette.CollectionView.prototype.constructor.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 itemViewContainer + this.once('render', function () { + if (this.collection){ + this.listenTo(this.collection, "add", this.addChildView); + this.listenTo(this.collection, "remove", this.removeItemView); + this.listenTo(this.collection, "reset", this._renderChildren); + } + }); + + }, + + // Retrieve the `itemView` to be used when rendering each of + // the items in the collection. The default is to return + // `this.itemView` or Marionette.CompositeView if no `itemView` + // has been defined + getItemView: function(item){ + var itemView = Marionette.getOption(this, "itemView") || this.constructor; + + if (!itemView){ + throwError("An `itemView` must be specified", "NoItemViewError"); + } + + return itemView; + }, + + // 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.isRendered = true; + this.isClosed = false; + this.resetItemViewContainer(); + + this.triggerBeforeRender(); + var html = this.renderModel(); + this.$el.html(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("composite:model:rendered"); + + this._renderChildren(); + + this.triggerMethod("composite:rendered"); + this.triggerRendered(); + return this; + }, + + _renderChildren: function(){ + if (this.isRendered){ + this.triggerMethod("composite:collection:before:render"); + Marionette.CollectionView.prototype._renderChildren.call(this); + this.triggerMethod("composite:collection:rendered"); + } + }, + + // Render an individual model, if we have one, as + // part of a composite view (branch / leaf). For example: + // a treeview. + renderModel: function(){ + var data = {}; + data = this.serializeData(); + data = this.mixinTemplateHelpers(data); + + var template = this.getTemplate(); + return Marionette.Renderer.render(template, data); + }, + + + // You might need to override this if you've overridden appendHtml + appendBuffer: function(compositeView, buffer) { + var $container = this.getItemViewContainer(compositeView); + $container.append(buffer); + }, + + // Appends the `el` of itemView instances to the specified + // `itemViewContainer` (a jQuery selector). Override this method to + // provide custom logic of how the child item view instances have their + // HTML appended to the composite view instance. + appendHtml: function(compositeView, itemView, index){ + if (compositeView.isBuffering) { + compositeView.elBuffer.appendChild(itemView.el); + compositeView._bufferedChildren.push(itemView); + } + else { + // If we've already rendered the main collection, just + // append the new items directly into the element. + var $container = this.getItemViewContainer(compositeView); + $container.append(itemView.el); + } + }, + + + // Internal method to ensure an `$itemViewContainer` exists, for the + // `appendHtml` method to use. + getItemViewContainer: function(containerView){ + if ("$itemViewContainer" in containerView){ + return containerView.$itemViewContainer; + } + + var container; + var itemViewContainer = Marionette.getOption(containerView, "itemViewContainer"); + if (itemViewContainer){ + + var selector = _.isFunction(itemViewContainer) ? itemViewContainer.call(this) : itemViewContainer; + container = containerView.$(selector); + if (container.length <= 0) { + throwError("The specified `itemViewContainer` was not found: " + containerView.itemViewContainer, "ItemViewContainerMissingError"); + } + + } else { + container = containerView.$el; + } + + containerView.$itemViewContainer = container; + return container; + }, + + // Internal method to reset the `$itemViewContainer` on render + resetItemViewContainer: function(){ + if (this.$itemViewContainer){ + delete this.$itemViewContainer; + } + } +}); + + +// Layout +// ------ + +// Used for managing application layouts, nested layouts and +// multiple regions within an application or sub-application. +// +// A specialized view type 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.Layout = Marionette.ItemView.extend({ + regionType: 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.prototype.constructor.call(this, options); + }, + + // Layout's render will use the existing region objects the + // first time it is called. Subsequent calls will close the + // views that the regions are showing and then reset the `el` + // for the regions to the newly rendered DOM elements. + render: function(){ + + if (this.isClosed){ + // a previously closed layout means we need to + // completely re-initialize the regions + this._initializeRegions(); + } + if (this._firstRender) { + // if this is the first render, don't do anything to + // reset the regions + this._firstRender = false; + } else if (!this.isClosed){ + // If this is not the first render call, then we need to + // re-initializing the `el` for each region + this._reInitializeRegions(); + } + + return Marionette.ItemView.prototype.render.apply(this, arguments); + }, + + // Handle closing regions, and then close the view itself. + close: function () { + if (this.isClosed){ return; } + this.regionManager.close(); + Marionette.ItemView.prototype.close.apply(this, arguments); + }, + + // Add a single region, by name, to the layout + addRegion: function(name, definition){ + 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 Layout, by name + removeRegion: function(name){ + delete this.regions[name]; + return this.regionManager.removeRegion(name); + }, + + // internal method to build regions + _buildRegions: function(regions){ + var that = this; + + var defaults = { + regionType: Marionette.getOption(this, "regionType"), + 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 layout. + _initializeRegions: function (options) { + var regions; + this._initRegionManager(); + + if (_.isFunction(this.regions)) { + regions = this.regions(options); + } else { + regions = this.regions || {}; + } + + this.addRegions(regions); + }, + + // Internal method to re-initialize all of the regions by updating the `el` that + // they point to + _reInitializeRegions: function(){ + this.regionManager.closeRegions(); + this.regionManager.each(function(region){ + region.reset(); + }); + }, + + // Internal method to initialize the region manager + // and all regions in it + _initRegionManager: function(){ + this.regionManager = new Marionette.RegionManager(); + + this.listenTo(this.regionManager, "region:add", function(name, region){ + this[name] = region; + this.trigger("region:add", name, region); + }); + + this.listenTo(this.regionManager, "region:remove", function(name, region){ + delete this[name]; + this.trigger("region:remove", name, region); + }); + } +}); + + +// 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.prototype.constructor.apply(this, arguments); + + this.options = options || {}; + + var appRoutes = Marionette.getOption(this, "appRoutes"); + var controller = this._getController(); + this.processAppRoutes(controller, appRoutes); + }, + + // 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); + }, + + // 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 Marionette.getOption(this, "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)); + } +}); + + +// 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(); + this.vent = new Backbone.Wreqr.EventAggregator(); + this.commands = new Backbone.Wreqr.Commands(); + this.reqres = new Backbone.Wreqr.RequestResponse(); + this.submodules = {}; + + _.extend(this, options); + + this.triggerMethod = Marionette.triggerMethod; +}; + +_.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("initialize:before", options); + this._initCallbacks.run(options, this); + this.triggerMethod("initialize:after", options); + + 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); + }, + + // Close all regions in the app, without removing them + closeRegions: function(){ + this._regionManager.closeRegions(); + }, + + // 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); + }, + + // 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, "region:add", function(name, region){ + this[name] = region; + }); + + this.listenTo(this._regionManager, "region:remove", function(name, region){ + delete this[name]; + }); + } +}); + +// Copy the `extend` function used by Backbone's classes +Marionette.Application.extend = Marionette.extend; + +// 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); + this.initialize = options.initialize || this.initialize; + + // store sub-modules + this.submodules = {}; + + this._setupInitializersAndFinalizers(); + + // store the configuration for this module + this.app = app; + this.startWithParent = true; + + this.triggerMethod = Marionette.triggerMethod; + + if (_.isFunction(this.initialize)){ + this.initialize(this.options, moduleName, app); + } +}; + +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; + + Marionette.triggerMethod.call(this, "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(); + + Marionette.triggerMethod.call(this, "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 (!definition){ return; } + + // build the correct list of arguments for the module definition + var args = _.flatten([ + this, + this.app, + Backbone, + Marionette, + Marionette.$, _, + 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(); + } +}); + +// Type 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 length + 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; + }, + + getClass: function(moduleDefinition) { + var ModuleClass = Marionette.Module; + + if (!moduleDefinition) { + return ModuleClass; + } + + if (moduleDefinition.prototype instanceof ModuleClass) { + return moduleDefinition; + } + + return moduleDefinition.moduleClass || ModuleClass; + }, + + _addModuleDefinition: function(parentModule, module, def, args){ + var fn; + var startWithParent; + + if (_.isFunction(def)){ + // if a function is supplied for the module definition + fn = def; + startWithParent = true; + + } else if (_.isObject(def)){ + // if an object is supplied + fn = def.define; + startWithParent = !_.isUndefined(def.startWithParent) ? def.startWithParent : true; + + } else { + // if nothing is supplied + startWithParent = true; + } + + // add module definition if needed + if (fn){ + module.addDefinition(fn, args); + } + + // `and` the two together, ensuring a single `false` will prevent it + // from starting with the parent + module.startWithParent = module.startWithParent && startWithParent; + + // setup auto-start if needed + if (module.startWithParent && !module.startWithParentIsConfigured){ + + // only configure this once + module.startWithParentIsConfigured = true; + + // add the module initializer config + parentModule.addInitializer(function(options){ + if (module.startWithParent){ + module.start(options); + } + }); + + } + + } +}); + + + + return Marionette; +})(this, Backbone, _); |