http://git-wip-us.apache.org/repos/asf/ambari/blob/cd6398a1/contrib/views/storm/src/main/resources/libs/bower/backbone.marionette/js/backbone.marionette.js ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/libs/bower/backbone.marionette/js/backbone.marionette.js b/contrib/views/storm/src/main/resources/libs/bower/backbone.marionette/js/backbone.marionette.js new file mode 100644 index 0000000..8714cd7 --- /dev/null +++ b/contrib/views/storm/src/main/resources/libs/bower/backbone.marionette/js/backbone.marionette.js @@ -0,0 +1,3128 @@ +// MarionetteJS (Backbone.Marionette) +// ---------------------------------- +// v2.3.2 +// +// Copyright (c)2015 Derick Bailey, Muted Solutions, LLC. +// Distributed under MIT license +// +// http://marionettejs.com + +(function(root, factory) { + + if (typeof define === 'function' && define.amd) { + define(['backbone', 'underscore', 'backbone.wreqr', 'backbone.babysitter'], function(Backbone, _) { + return (root.Marionette = root.Mn = factory(root, Backbone, _)); + }); + } else if (typeof exports !== 'undefined') { + var Backbone = require('backbone'); + var _ = require('underscore'); + var Wreqr = require('backbone.wreqr'); + var BabySitter = require('backbone.babysitter'); + module.exports = factory(root, Backbone, _); + } else { + root.Marionette = root.Mn = factory(root, root.Backbone, root._); + } + +}(this, function(root, Backbone, _) { + 'use strict'; + + var previousMarionette = root.Marionette; + var previousMn = root.Mn; + + var Marionette = Backbone.Marionette = {}; + + Marionette.VERSION = '2.3.2'; + + Marionette.noConflict = function() { + root.Marionette = previousMarionette; + root.Mn = previousMn; + return this; + }; + + // Get the Deferred creator for later use + Marionette.Deferred = Backbone.$.Deferred; + + /* jshint unused: false *//* global console */ + + // Helpers + // ------- + + // Marionette.extend + // ----------------- + + // Borrow the Backbone `extend` method so we can use it as needed + Marionette.extend = Backbone.Model.extend; + + // Marionette.isNodeAttached + // ------------------------- + + // Determine if `el` is a child of the document + Marionette.isNodeAttached = function(el) { + return Backbone.$.contains(document.documentElement, el); + }; + + + // Marionette.getOption + // -------------------- + + // Retrieve an object, function or other value from a target + // object or its `options`, with `options` taking precedence. + Marionette.getOption = function(target, optionName) { + if (!target || !optionName) { return; } + if (target.options && (target.options[optionName] !== undefined)) { + return target.options[optionName]; + } else { + return target[optionName]; + } + }; + + // Proxy `Marionette.getOption` + Marionette.proxyGetOption = function(optionName) { + return Marionette.getOption(this, optionName); + }; + + // Similar to `_.result`, this is a simple helper + // If a function is provided we call it with context + // otherwise just return the value. If the value is + // undefined return a default value + Marionette._getValue = function(value, context, params) { + if (_.isFunction(value)) { + // We need to ensure that params is not undefined + // to prevent `apply` from failing in ie8 + params = params || []; + + value = value.apply(context, params); + } + return value; + }; + + // Marionette.normalizeMethods + // ---------------------- + + // Pass in a mapping of events => functions or function names + // and return a mapping of events => functions + Marionette.normalizeMethods = function(hash) { + return _.reduce(hash, function(normalizedHash, method, name) { + if (!_.isFunction(method)) { + method = this[method]; + } + if (method) { + normalizedHash[name] = method; + } + return normalizedHash; + }, {}, this); + }; + + // utility method for parsing @ui. syntax strings + // into associated selector + Marionette.normalizeUIString = function(uiString, ui) { + return uiString.replace(/@ui\.[a-zA-Z_$0-9]*/g, function(r) { + return ui[r.slice(4)]; + }); + }; + + // allows for the use of the @ui. syntax within + // a given key for triggers and events + // swaps the @ui with the associated selector. + // Returns a new, non-mutated, parsed events hash. + Marionette.normalizeUIKeys = function(hash, ui) { + return _.reduce(hash, function(memo, val, key) { + var normalizedKey = Marionette.normalizeUIString(key, ui); + memo[normalizedKey] = val; + return memo; + }, {}); + }; + + // allows for the use of the @ui. syntax within + // a given value for regions + // swaps the @ui with the associated selector + Marionette.normalizeUIValues = function(hash, ui) { + _.each(hash, function(val, key) { + if (_.isString(val)) { + hash[key] = Marionette.normalizeUIString(val, ui); + } + }); + return hash; + }; + + // Mix in methods from Underscore, for iteration, and other + // collection related features. + // Borrowing this code from Backbone.Collection: + // http://backbonejs.org/docs/backbone.html#section-121 + Marionette.actAsCollection = function(object, listProperty) { + var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', + 'select', 'reject', 'every', 'all', 'some', 'any', 'include', + 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', + 'last', 'without', 'isEmpty', 'pluck']; + + _.each(methods, function(method) { + object[method] = function() { + var list = _.values(_.result(this, listProperty)); + var args = [list].concat(_.toArray(arguments)); + return _[method].apply(_, args); + }; + }); + }; + + var deprecate = Marionette.deprecate = function(message, test) { + if (_.isObject(message)) { + message = ( + message.prev + ' is going to be removed in the future. ' + + 'Please use ' + message.next + ' instead.' + + (message.url ? ' See: ' + message.url : '') + ); + } + + if ((test === undefined || !test) && !deprecate._cache[message]) { + deprecate._warn('Deprecation warning: ' + message); + deprecate._cache[message] = true; + } + }; + + deprecate._warn = typeof console !== 'undefined' && (console.warn || console.log) || function() {}; + deprecate._cache = {}; + + /* jshint maxstatements: 14, maxcomplexity: 7 */ + + // Trigger Method + // -------------- + + + Marionette._triggerMethod = (function() { + // split the event name on the ":" + var splitter = /(^|:)(\w)/gi; + + // take the event section ("section1:section2:section3") + // and turn it in to uppercase name + function getEventName(match, prefix, eventName) { + return eventName.toUpperCase(); + } + + return function(context, event, args) { + var noEventArg = arguments.length < 3; + if (noEventArg) { + args = event; + event = args[0]; + } + + // get the method name from the event name + var methodName = 'on' + event.replace(splitter, getEventName); + var method = context[methodName]; + var result; + + // call the onMethodName if it exists + if (_.isFunction(method)) { + // pass all args, except the event name + result = method.apply(context, noEventArg ? _.rest(args) : args); + } + + // trigger the event, if a trigger method exists + if (_.isFunction(context.trigger)) { + if (noEventArg + args.length > 1) { + context.trigger.apply(context, noEventArg ? args : [event].concat(_.rest(args, 0))); + } else { + context.trigger(event); + } + } + + return result; + }; + })(); + + // Trigger an event and/or a corresponding method name. Examples: + // + // `this.triggerMethod("foo")` will trigger the "foo" event and + // call the "onFoo" method. + // + // `this.triggerMethod("foo:bar")` will trigger the "foo:bar" event and + // call the "onFooBar" method. + Marionette.triggerMethod = function(event) { + return Marionette._triggerMethod(this, arguments); + }; + + // triggerMethodOn invokes triggerMethod on a specific context + // + // e.g. `Marionette.triggerMethodOn(view, 'show')` + // will trigger a "show" event or invoke onShow the view. + Marionette.triggerMethodOn = function(context) { + var fnc = _.isFunction(context.triggerMethod) ? + context.triggerMethod : + Marionette.triggerMethod; + + return fnc.apply(context, _.rest(arguments)); + }; + + // DOM Refresh + // ----------- + + // Monitor a view's state, and after it has been rendered and shown + // in the DOM, trigger a "dom:refresh" event every time it is + // re-rendered. + + Marionette.MonitorDOMRefresh = function(view) { + + // track when the view has been shown in the DOM, + // using a Marionette.Region (or by other means of triggering "show") + function handleShow() { + view._isShown = true; + triggerDOMRefresh(); + } + + // track when the view has been rendered + function handleRender() { + view._isRendered = true; + triggerDOMRefresh(); + } + + // Trigger the "dom:refresh" event and corresponding "onDomRefresh" method + function triggerDOMRefresh() { + if (view._isShown && view._isRendered && Marionette.isNodeAttached(view.el)) { + if (_.isFunction(view.triggerMethod)) { + view.triggerMethod('dom:refresh'); + } + } + } + + view.on({ + show: handleShow, + render: handleRender + }); + }; + + /* jshint maxparams: 5 */ + + // Bind Entity Events & Unbind Entity Events + // ----------------------------------------- + // + // These methods are used to bind/unbind a backbone "entity" (e.g. collection/model) + // to methods on a target object. + // + // The first parameter, `target`, must have the Backbone.Events module mixed in. + // + // The second parameter is the `entity` (Backbone.Model, Backbone.Collection or + // any object that has Backbone.Events mixed in) to bind the events from. + // + // The third parameter is a hash of { "event:name": "eventHandler" } + // configuration. Multiple handlers can be separated by a space. A + // function can be supplied instead of a string handler name. + + (function(Marionette) { + 'use strict'; + + // Bind the event to handlers specified as a string of + // handler names on the target object + function bindFromStrings(target, entity, evt, methods) { + var methodNames = methods.split(/\s+/); + + _.each(methodNames, function(methodName) { + + var method = target[methodName]; + if (!method) { + throw new Marionette.Error('Method "' + methodName + + '" was configured as an event handler, but does not exist.'); + } + + target.listenTo(entity, evt, method); + }); + } + + // Bind the event to a supplied callback function + function bindToFunction(target, entity, evt, method) { + target.listenTo(entity, evt, method); + } + + // Bind the event to handlers specified as a string of + // handler names on the target object + function unbindFromStrings(target, entity, evt, methods) { + var methodNames = methods.split(/\s+/); + + _.each(methodNames, function(methodName) { + var method = target[methodName]; + target.stopListening(entity, evt, method); + }); + } + + // Bind the event to a supplied callback function + function unbindToFunction(target, entity, evt, method) { + target.stopListening(entity, evt, method); + } + + + // generic looping function + function iterateEvents(target, entity, bindings, functionCallback, stringCallback) { + if (!entity || !bindings) { return; } + + // type-check bindings + if (!_.isObject(bindings)) { + throw new Marionette.Error({ + message: 'Bindings must be an object or function.', + url: 'marionette.functions.html#marionettebindentityevents' + }); + } + + // allow the bindings to be a function + bindings = Marionette._getValue(bindings, target); + + // iterate the bindings and bind them + _.each(bindings, function(methods, evt) { + + // allow for a function as the handler, + // or a list of event names as a string + if (_.isFunction(methods)) { + functionCallback(target, entity, evt, methods); + } else { + stringCallback(target, entity, evt, methods); + } + + }); + } + + // Export Public API + Marionette.bindEntityEvents = function(target, entity, bindings) { + iterateEvents(target, entity, bindings, bindToFunction, bindFromStrings); + }; + + Marionette.unbindEntityEvents = function(target, entity, bindings) { + iterateEvents(target, entity, bindings, unbindToFunction, unbindFromStrings); + }; + + // Proxy `bindEntityEvents` + Marionette.proxyBindEntityEvents = function(entity, bindings) { + return Marionette.bindEntityEvents(this, entity, bindings); + }; + + // Proxy `unbindEntityEvents` + Marionette.proxyUnbindEntityEvents = function(entity, bindings) { + return Marionette.unbindEntityEvents(this, entity, bindings); + }; + })(Marionette); + + + // Error + // ----- + + var errorProps = ['description', 'fileName', 'lineNumber', 'name', 'message', 'number']; + + Marionette.Error = Marionette.extend.call(Error, { + urlRoot: 'http://marionettejs.com/docs/v' + Marionette.VERSION + '/', + + constructor: function(message, options) { + if (_.isObject(message)) { + options = message; + message = options.message; + } else if (!options) { + options = {}; + } + + var error = Error.call(this, message); + _.extend(this, _.pick(error, errorProps), _.pick(options, errorProps)); + + this.captureStackTrace(); + + if (options.url) { + this.url = this.urlRoot + options.url; + } + }, + + captureStackTrace: function() { + if (Error.captureStackTrace) { + Error.captureStackTrace(this, Marionette.Error); + } + }, + + toString: function() { + return this.name + ': ' + this.message + (this.url ? ' See: ' + this.url : ''); + } + }); + + Marionette.Error.extend = Marionette.extend; + + // Callbacks + // --------- + + // A simple way of managing a collection of callbacks + // and executing them at a later point in time, using jQuery's + // `Deferred` object. + Marionette.Callbacks = function() { + this._deferred = Marionette.Deferred(); + this._callbacks = []; + }; + + _.extend(Marionette.Callbacks.prototype, { + + // Add a callback to be executed. Callbacks added here are + // guaranteed to execute, even if they are added after the + // `run` method is called. + add: function(callback, contextOverride) { + var promise = _.result(this._deferred, 'promise'); + + this._callbacks.push({cb: callback, ctx: contextOverride}); + + promise.then(function(args) { + if (contextOverride){ args.context = contextOverride; } + callback.call(args.context, args.options); + }); + }, + + // Run all registered callbacks with the context specified. + // Additional callbacks can be added after this has been run + // and they will still be executed. + run: function(options, context) { + this._deferred.resolve({ + options: options, + context: context + }); + }, + + // Resets the list of callbacks to be run, allowing the same list + // to be run multiple times - whenever the `run` method is called. + reset: function() { + var callbacks = this._callbacks; + this._deferred = Marionette.Deferred(); + this._callbacks = []; + + _.each(callbacks, function(cb) { + this.add(cb.cb, cb.ctx); + }, this); + } + }); + + // Controller + // ---------- + + // A multi-purpose object to use as a controller for + // modules and routers, and as a mediator for workflow + // and coordination of other objects, views, and more. + Marionette.Controller = function(options) { + this.options = options || {}; + + if (_.isFunction(this.initialize)) { + this.initialize(this.options); + } + }; + + Marionette.Controller.extend = Marionette.extend; + + // Controller Methods + // -------------- + + // Ensure it can trigger events with Backbone.Events + _.extend(Marionette.Controller.prototype, Backbone.Events, { + destroy: function() { + Marionette._triggerMethod(this, 'before:destroy', arguments); + Marionette._triggerMethod(this, 'destroy', arguments); + + this.stopListening(); + this.off(); + return this; + }, + + // import the `triggerMethod` to trigger events with corresponding + // methods if the method exists + triggerMethod: Marionette.triggerMethod, + + // Proxy `getOption` to enable getting options from this or this.options by name. + getOption: Marionette.proxyGetOption + + }); + + // Object + // ------ + + // A Base Class that other Classes should descend from. + // Object borrows many conventions and utilities from Backbone. + Marionette.Object = function(options) { + this.options = _.extend({}, _.result(this, 'options'), options); + + this.initialize.apply(this, arguments); + }; + + Marionette.Object.extend = Marionette.extend; + + // Object Methods + // -------------- + + // Ensure it can trigger events with Backbone.Events + _.extend(Marionette.Object.prototype, Backbone.Events, { + + //this is a noop method intended to be overridden by classes that extend from this base + initialize: function() {}, + + destroy: function() { + this.triggerMethod('before:destroy'); + this.triggerMethod('destroy'); + this.stopListening(); + }, + + // Import the `triggerMethod` to trigger events with corresponding + // methods if the method exists + triggerMethod: Marionette.triggerMethod, + + // Proxy `getOption` to enable getting options from this or this.options by name. + getOption: Marionette.proxyGetOption, + + // Proxy `bindEntityEvents` to enable binding view's events from another entity. + bindEntityEvents: Marionette.proxyBindEntityEvents, + + // Proxy `unbindEntityEvents` to enable unbinding view's events from another entity. + unbindEntityEvents: Marionette.proxyUnbindEntityEvents + }); + + /* jshint maxcomplexity: 16, maxstatements: 45, maxlen: 120 */ + + // Region + // ------ + + // Manage the visual regions of your composite application. See + // http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/ + + Marionette.Region = Marionette.Object.extend({ + constructor: function (options) { + + // set options temporarily so that we can get `el`. + // options will be overriden by Object.constructor + this.options = options || {}; + this.el = this.getOption('el'); + + // Handle when this.el is passed in as a $ wrapped element. + this.el = this.el instanceof Backbone.$ ? this.el[0] : this.el; + + if (!this.el) { + throw new Marionette.Error({ + name: 'NoElError', + message: 'An "el" must be specified for a region.' + }); + } + + this.$el = this.getEl(this.el); + Marionette.Object.call(this, options); + }, + + // Displays a backbone view instance inside of the region. + // Handles calling the `render` method for you. Reads content + // directly from the `el` attribute. Also calls an optional + // `onShow` and `onDestroy` method on your view, just after showing + // or just before destroying the view, respectively. + // The `preventDestroy` option can be used to prevent a view from + // the old view being destroyed on show. + // The `forceShow` option can be used to force a view to be + // re-rendered if it's already shown in the region. + show: function(view, options){ + if (!this._ensureElement()) { + return; + } + + this._ensureViewIsIntact(view); + + var showOptions = options || {}; + var isDifferentView = view !== this.currentView; + var preventDestroy = !!showOptions.preventDestroy; + var forceShow = !!showOptions.forceShow; + + // We are only changing the view if there is a current view to change to begin with + var isChangingView = !!this.currentView; + + // Only destroy the current view if we don't want to `preventDestroy` and if + // the view given in the first argument is different than `currentView` + var _shouldDestroyView = isDifferentView && !preventDestroy; + + // Only show the view given in the first argument if it is different than + // the current view or if we want to re-show the view. Note that if + // `_shouldDestroyView` is true, then `_shouldShowView` is also necessarily true. + var _shouldShowView = isDifferentView || forceShow; + + if (isChangingView) { + this.triggerMethod('before:swapOut', this.currentView, this, options); + } + + if (this.currentView) { + delete this.currentView._parent; + } + + if (_shouldDestroyView) { + this.empty(); + + // A `destroy` event is attached to the clean up manually removed views. + // We need to detach this event when a new view is going to be shown as it + // is no longer relevant. + } else if (isChangingView && _shouldShowView) { + this.currentView.off('destroy', this.empty, this); + } + + if (_shouldShowView) { + + // We need to listen for if a view is destroyed + // in a way other than through the region. + // If this happens we need to remove the reference + // to the currentView since once a view has been destroyed + // we can not reuse it. + view.once('destroy', this.empty, this); + view.render(); + + view._parent = this; + + if (isChangingView) { + this.triggerMethod('before:swap', view, this, options); + } + + this.triggerMethod('before:show', view, this, options); + Marionette.triggerMethodOn(view, 'before:show', view, this, options); + + if (isChangingView) { + this.triggerMethod('swapOut', this.currentView, this, options); + } + + // An array of views that we're about to display + var attachedRegion = Marionette.isNodeAttached(this.el); + + // The views that we're about to attach to the document + // It's important that we prevent _getNestedViews from being executed unnecessarily + // as it's a potentially-slow method + var displayedViews = []; + + var triggerBeforeAttach = showOptions.triggerBeforeAttach || this.triggerBeforeAttach; + var triggerAttach = showOptions.triggerAttach || this.triggerAttach; + + if (attachedRegion && triggerBeforeAttach) { + displayedViews = this._displayedViews(view); + this._triggerAttach(displayedViews, 'before:'); + } + + this.attachHtml(view); + this.currentView = view; + + if (attachedRegion && triggerAttach) { + displayedViews = this._displayedViews(view); + this._triggerAttach(displayedViews); + } + + if (isChangingView) { + this.triggerMethod('swap', view, this, options); + } + + this.triggerMethod('show', view, this, options); + Marionette.triggerMethodOn(view, 'show', view, this, options); + + return this; + } + + return this; + }, + + triggerBeforeAttach: true, + triggerAttach: true, + + _triggerAttach: function(views, prefix) { + var eventName = (prefix || '') + 'attach'; + _.each(views, function(view) { + Marionette.triggerMethodOn(view, eventName, view, this); + }, this); + }, + + _displayedViews: function(view) { + return _.union([view], _.result(view, '_getNestedViews') || []); + }, + + _ensureElement: function(){ + if (!_.isObject(this.el)) { + this.$el = this.getEl(this.el); + this.el = this.$el[0]; + } + + if (!this.$el || this.$el.length === 0) { + if (this.getOption('allowMissingEl')) { + return false; + } else { + throw new Marionette.Error('An "el" ' + this.$el.selector + ' must exist in DOM'); + } + } + return true; + }, + + _ensureViewIsIntact: function(view) { + if (!view) { + throw new Marionette.Error({ + name: 'ViewNotValid', + message: 'The view passed is undefined and therefore invalid. You must pass a view instance to show.' + }); + } + + if (view.isDestroyed) { + throw new Marionette.Error({ + name: 'ViewDestroyedError', + message: 'View (cid: "' + view.cid + '") has already been destroyed and cannot be used.' + }); + } + }, + + // Override this method to change how the region finds the DOM + // element that it manages. Return a jQuery selector object scoped + // to a provided parent el or the document if none exists. + getEl: function(el) { + return Backbone.$(el, Marionette._getValue(this.options.parentEl, this)); + }, + + // Override this method to change how the new view is + // appended to the `$el` that the region is managing + attachHtml: function(view) { + this.$el.contents().detach(); + + this.el.appendChild(view.el); + }, + + // Destroy the current view, if there is one. If there is no + // current view, it does nothing and returns immediately. + empty: function() { + var view = this.currentView; + + // If there is no view in the region + // we should not remove anything + if (!view) { return; } + + view.off('destroy', this.empty, this); + this.triggerMethod('before:empty', view); + this._destroyView(); + this.triggerMethod('empty', view); + + // Remove region pointer to the currentView + delete this.currentView; + return this; + }, + + // call 'destroy' or 'remove', depending on which is found + // on the view (if showing a raw Backbone view or a Marionette View) + _destroyView: function() { + var view = this.currentView; + + if (view.destroy && !view.isDestroyed) { + view.destroy(); + } else if (view.remove) { + view.remove(); + + // appending isDestroyed to raw Backbone View allows regions + // to throw a ViewDestroyedError for this view + view.isDestroyed = true; + } + }, + + // Attach an existing view to the region. This + // will not call `render` or `onShow` for the new view, + // and will not replace the current HTML for the `el` + // of the region. + attachView: function(view) { + this.currentView = view; + return this; + }, + + // Checks whether a view is currently present within + // the region. Returns `true` if there is and `false` if + // no view is present. + hasView: function() { + return !!this.currentView; + }, + + // Reset the region by destroying any existing view and + // clearing out the cached `$el`. The next time a view + // is shown via this region, the region will re-query the + // DOM for the region's `el`. + reset: function() { + this.empty(); + + if (this.$el) { + this.el = this.$el.selector; + } + + delete this.$el; + return this; + } + + }, + + // Static Methods + { + + // Build an instance of a region by passing in a configuration object + // and a default region class to use if none is specified in the config. + // + // The config object should either be a string as a jQuery DOM selector, + // a Region class directly, or an object literal that specifies a selector, + // a custom regionClass, and any options to be supplied to the region: + // + // ```js + // { + // selector: "#foo", + // regionClass: MyCustomRegion, + // allowMissingEl: false + // } + // ``` + // + buildRegion: function(regionConfig, DefaultRegionClass) { + if (_.isString(regionConfig)) { + return this._buildRegionFromSelector(regionConfig, DefaultRegionClass); + } + + if (regionConfig.selector || regionConfig.el || regionConfig.regionClass) { + return this._buildRegionFromObject(regionConfig, DefaultRegionClass); + } + + if (_.isFunction(regionConfig)) { + return this._buildRegionFromRegionClass(regionConfig); + } + + throw new Marionette.Error({ + message: 'Improper region configuration type.', + url: 'marionette.region.html#region-configuration-types' + }); + }, + + // Build the region from a string selector like '#foo-region' + _buildRegionFromSelector: function(selector, DefaultRegionClass) { + return new DefaultRegionClass({ el: selector }); + }, + + // Build the region from a configuration object + // ```js + // { selector: '#foo', regionClass: FooRegion, allowMissingEl: false } + // ``` + _buildRegionFromObject: function(regionConfig, DefaultRegionClass) { + var RegionClass = regionConfig.regionClass || DefaultRegionClass; + var options = _.omit(regionConfig, 'selector', 'regionClass'); + + if (regionConfig.selector && !options.el) { + options.el = regionConfig.selector; + } + + return new RegionClass(options); + }, + + // Build the region directly from a given `RegionClass` + _buildRegionFromRegionClass: function(RegionClass) { + return new RegionClass(); + } + }); + + // Region Manager + // -------------- + + // Manage one or more related `Marionette.Region` objects. + Marionette.RegionManager = Marionette.Controller.extend({ + constructor: function(options) { + this._regions = {}; + + Marionette.Controller.call(this, options); + + this.addRegions(this.getOption('regions')); + }, + + // Add multiple regions using an object literal or a + // function that returns an object literal, where + // each key becomes the region name, and each value is + // the region definition. + addRegions: function(regionDefinitions, defaults) { + regionDefinitions = Marionette._getValue(regionDefinitions, this, arguments); + + return _.reduce(regionDefinitions, function(regions, definition, name) { + if (_.isString(definition)) { + definition = {selector: definition}; + } + if (definition.selector) { + definition = _.defaults({}, definition, defaults); + } + + regions[name] = this.addRegion(name, definition); + return regions; + }, {}, this); + }, + + // Add an individual region to the region manager, + // and return the region instance + addRegion: function(name, definition) { + var region; + + if (definition instanceof Marionette.Region) { + region = definition; + } else { + region = Marionette.Region.buildRegion(definition, Marionette.Region); + } + + this.triggerMethod('before:add:region', name, region); + + region._parent = this; + this._store(name, region); + + this.triggerMethod('add:region', name, region); + return region; + }, + + // Get a region by name + get: function(name) { + return this._regions[name]; + }, + + // Gets all the regions contained within + // the `regionManager` instance. + getRegions: function(){ + return _.clone(this._regions); + }, + + // Remove a region by name + removeRegion: function(name) { + var region = this._regions[name]; + this._remove(name, region); + + return region; + }, + + // Empty all regions in the region manager, and + // remove them + removeRegions: function() { + var regions = this.getRegions(); + _.each(this._regions, function(region, name) { + this._remove(name, region); + }, this); + + return regions; + }, + + // Empty all regions in the region manager, but + // leave them attached + emptyRegions: function() { + var regions = this.getRegions(); + _.invoke(regions, 'empty'); + return regions; + }, + + // Destroy all regions and shut down the region + // manager entirely + destroy: function() { + this.removeRegions(); + return Marionette.Controller.prototype.destroy.apply(this, arguments); + }, + + // internal method to store regions + _store: function(name, region) { + this._regions[name] = region; + this._setLength(); + }, + + // internal method to remove a region + _remove: function(name, region) { + this.triggerMethod('before:remove:region', name, region); + region.empty(); + region.stopListening(); + + delete region._parent; + delete this._regions[name]; + this._setLength(); + this.triggerMethod('remove:region', name, region); + }, + + // set the number of regions current held + _setLength: function() { + this.length = _.size(this._regions); + } + }); + + Marionette.actAsCollection(Marionette.RegionManager.prototype, '_regions'); + + + // Template Cache + // -------------- + + // Manage templates stored in `<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 = _.toArray(arguments); + var length = args.length; + + if (length > 0) { + for (i = 0; i < length; i++) { + delete this.templateCaches[args[i]]; + } + } else { + this.templateCaches = {}; + } + } + }); + + // TemplateCache instance methods, allowing each + // template cache object to manage its own state + // and know whether or not it has been loaded + _.extend(Marionette.TemplateCache.prototype, { + + // Internal method to load the template + load: function() { + // Guard clause to prevent loading this template more than once + if (this.compiledTemplate) { + return this.compiledTemplate; + } + + // Load the template and compile it + var template = this.loadTemplate(this.templateId); + this.compiledTemplate = this.compileTemplate(template); + + return this.compiledTemplate; + }, + + // Load a template from the DOM, by default. Override + // this method to provide your own template retrieval + // For asynchronous loading with AMD/RequireJS, consider + // using a template-loader plugin as described here: + // https://github.com/marionettejs/backbone.marionette/wiki/Using-marionette-with-requirejs + loadTemplate: function(templateId) { + var template = Backbone.$(templateId).html(); + + if (!template || template.length === 0) { + throw new Marionette.Error({ + name: 'NoTemplateError', + message: 'Could not find template: "' + templateId + '"' + }); + } + + 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) { + throw new Marionette.Error({ + name: 'TemplateNotFoundError', + message: 'Cannot render the template since its false, null or undefined.' + }); + } + + var templateFunc = _.isFunction(template) ? template : Marionette.TemplateCache.get(template); + + return templateFunc(data); + } + }; + + + /* jshint maxlen: 114, nonew: false */ + // View + // ---- + + // The core view class that other Marionette views extend from. + Marionette.View = Backbone.View.extend({ + isDestroyed: false, + + constructor: function(options) { + _.bindAll(this, 'render'); + + options = Marionette._getValue(options, this); + + // 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'), options); + + this._behaviors = Marionette.Behaviors(this); + + Backbone.View.apply(this, arguments); + + Marionette.MonitorDOMRefresh(this); + this.on('show', this.onShowCalled); + }, + + // Get the template for this view + // instance. You can set a `template` attribute in the view + // definition or pass a `template: "whatever"` parameter in + // to the constructor options. + getTemplate: function() { + return this.getOption('template'); + }, + + // Serialize a model by returning its attributes. Clones + // the attributes to allow modification. + serializeModel: function(model){ + return model.toJSON.apply(model, _.rest(arguments)); + }, + + // Mix in template helper methods. Looks for a + // `templateHelpers` attribute, which can either be an + // object literal, or a function that returns an object + // literal. All methods and attributes from this object + // are copies to the object passed in. + mixinTemplateHelpers: function(target) { + target = target || {}; + var templateHelpers = this.getOption('templateHelpers'); + templateHelpers = Marionette._getValue(templateHelpers, this); + return _.extend(target, templateHelpers); + }, + + // normalize the keys of passed hash with the views `ui` selectors. + // `{"@ui.foo": "bar"}` + normalizeUIKeys: function(hash) { + var uiBindings = _.result(this, '_uiBindings'); + return Marionette.normalizeUIKeys(hash, uiBindings || _.result(this, 'ui')); + }, + + // normalize the values of passed hash with the views `ui` selectors. + // `{foo: "@ui.bar"}` + normalizeUIValues: function(hash) { + var ui = _.result(this, 'ui'); + var uiBindings = _.result(this, '_uiBindings'); + return Marionette.normalizeUIValues(hash, uiBindings || ui); + }, + + // Configure `triggers` to forward DOM events to view + // events. `triggers: {"click .foo": "do:foo"}` + configureTriggers: function() { + if (!this.triggers) { return; } + + // 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 + return _.reduce(triggers, function(events, value, key) { + events[key] = this._buildViewTrigger(value); + return events; + }, {}, this); + }, + + // Overriding Backbone.View's delegateEvents to handle + // the `triggers`, `modelEvents`, and `collectionEvents` configuration + delegateEvents: function(events) { + this._delegateDOMEvents(events); + this.bindEntityEvents(this.model, this.getOption('modelEvents')); + this.bindEntityEvents(this.collection, this.getOption('collectionEvents')); + + _.each(this._behaviors, function(behavior) { + behavior.bindEntityEvents(this.model, behavior.getOption('modelEvents')); + behavior.bindEntityEvents(this.collection, behavior.getOption('collectionEvents')); + }, this); + + return this; + }, + + // internal method to delegate DOM events and triggers + _delegateDOMEvents: function(eventsArg) { + var events = Marionette._getValue(eventsArg || this.events, this); + + // normalize ui keys + events = this.normalizeUIKeys(events); + if(_.isUndefined(eventsArg)) {this.events = events;} + + var combinedEvents = {}; + + // look up if this view has behavior events + var behaviorEvents = _.result(this, 'behaviorEvents') || {}; + var triggers = this.configureTriggers(); + var behaviorTriggers = _.result(this, 'behaviorTriggers') || {}; + + // behavior events will be overriden by view events and or triggers + _.extend(combinedEvents, behaviorEvents, events, triggers, behaviorTriggers); + + Backbone.View.prototype.delegateEvents.call(this, combinedEvents); + }, + + // Overriding Backbone.View's undelegateEvents to handle unbinding + // the `triggers`, `modelEvents`, and `collectionEvents` config + undelegateEvents: function() { + Backbone.View.prototype.undelegateEvents.apply(this, arguments); + + this.unbindEntityEvents(this.model, this.getOption('modelEvents')); + this.unbindEntityEvents(this.collection, this.getOption('collectionEvents')); + + _.each(this._behaviors, function(behavior) { + behavior.unbindEntityEvents(this.model, behavior.getOption('modelEvents')); + behavior.unbindEntityEvents(this.collection, behavior.getOption('collectionEvents')); + }, this); + + return this; + }, + + // Internal method, handles the `show` event. + onShowCalled: function() {}, + + // Internal helper method to verify whether the view hasn't been destroyed + _ensureViewIsIntact: function() { + if (this.isDestroyed) { + throw new Marionette.Error({ + name: 'ViewDestroyedError', + message: 'View (cid: "' + this.cid + '") has already been destroyed and cannot be used.' + }); + } + }, + + // Default `destroy` implementation, for removing a view from the + // DOM and unbinding it. Regions will call this method + // for you. You can specify an `onDestroy` method in your view to + // add custom code that is called after the view is destroyed. + destroy: function() { + if (this.isDestroyed) { return; } + + var args = _.toArray(arguments); + + this.triggerMethod.apply(this, ['before:destroy'].concat(args)); + + // mark as destroyed before doing the actual destroy, to + // prevent infinite loops within "destroy" event handlers + // that are trying to destroy other views + this.isDestroyed = true; + this.triggerMethod.apply(this, ['destroy'].concat(args)); + + // unbind UI elements + this.unbindUIElements(); + + // remove the view from the DOM + this.remove(); + + // Call destroy on each behavior after + // destroying the view. + // This unbinds event listeners + // that behaviors have registered for. + _.invoke(this._behaviors, 'destroy', args); + + return this; + }, + + bindUIElements: function() { + this._bindUIElements(); + _.invoke(this._behaviors, this._bindUIElements); + }, + + // 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(bindings, function(selector, key) { + this.ui[key] = this.$(selector); + }, this); + }, + + // This method unbinds the elements specified in the "ui" hash + unbindUIElements: function() { + this._unbindUIElements(); + _.invoke(this._behaviors, this._unbindUIElements); + }, + + _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; + }, + + // Internal method to create an event handler for a given `triggerDef` like + // 'click:foo' + _buildViewTrigger: function(triggerDef) { + var hasOptions = _.isObject(triggerDef); + + var options = _.defaults({}, (hasOptions ? triggerDef : {}), { + preventDefault: true, + stopPropagation: true + }); + + var eventName = hasOptions ? options.event : triggerDef; + + return function(e) { + if (e) { + if (e.preventDefault && options.preventDefault) { + e.preventDefault(); + } + + if (e.stopPropagation && options.stopPropagation) { + e.stopPropagation(); + } + } + + var args = { + view: this, + model: this.model, + collection: this.collection + }; + + this.triggerMethod(eventName, args); + }; + }, + + setElement: function() { + var ret = Backbone.View.prototype.setElement.apply(this, arguments); + + // proxy behavior $el to the view's $el. + // This is needed because a view's $el proxy + // is not set until after setElement is called. + _.invoke(this._behaviors, 'proxyViewProperties', this); + + return ret; + }, + + // import the `triggerMethod` to trigger events with corresponding + // methods if the method exists + triggerMethod: function() { + var triggerMethod = Marionette._triggerMethod; + var ret = triggerMethod(this, arguments); + var behaviors = this._behaviors; + // Use good ol' for as this is a very hot function + for (var i = 0, length = behaviors && behaviors.length; i < length; i++) { + triggerMethod(behaviors[i], arguments); + } + + return ret; + }, + + // This method returns any views that are immediate + // children of this view + _getImmediateChildren: function() { + return []; + }, + + // Returns an array of every nested view within this view + _getNestedViews: function() { + var children = this._getImmediateChildren(); + + if (!children.length) { return children; } + + return _.reduce(children, function(memo, view) { + if (!view._getNestedViews) { return memo; } + return memo.concat(view._getNestedViews()); + }, children); + }, + + // Imports the "normalizeMethods" to transform hashes of + // events=>function references/names to a hash of events=>function references + normalizeMethods: Marionette.normalizeMethods, + + // Proxy `getOption` to enable getting options from this or this.options by name. + getOption: Marionette.proxyGetOption, + + // Proxy `bindEntityEvents` to enable binding view's events from another entity. + bindEntityEvents: Marionette.proxyBindEntityEvents, + + // Proxy `unbindEntityEvents` to enable unbinding view's events from another entity. + unbindEntityEvents: Marionette.proxyUnbindEntityEvents + }); + + // Item View + // --------- + + // A single item view implementation that contains code for rendering + // with underscore.js templates, serializing the view's model or collection, + // and calling several methods on extended views, such as `onRender`. + Marionette.ItemView = Marionette.View.extend({ + + // Setting up the inheritance chain which allows changes to + // Marionette.View.prototype.constructor which allows overriding + constructor: function() { + Marionette.View.apply(this, arguments); + }, + + // Serialize the model or collection for the view. If a model is + // found, the view's `serializeModel` is called. If a collection is found, + // each model in the collection is serialized by calling + // the view's `serializeCollection` and put into 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(){ + if (!this.model && !this.collection) { + return {}; + } + + var args = [this.model || this.collection]; + if (arguments.length) { + args.push.apply(args, arguments); + } + + if (this.model) { + return this.serializeModel.apply(this, args); + } else { + return { + items: this.serializeCollection.apply(this, args) + }; + } + }, + + // Serialize a collection by serializing each of its models. + serializeCollection: function(collection){ + return collection.toJSON.apply(collection, _.rest(arguments)); + }, + + // Render the view, defaulting to underscore.js templates. + // You can override this in your view definition to provide + // a very specific rendering for your view. In general, though, + // you should override the `Marionette.Renderer` object to + // change how Marionette renders views. + render: function() { + this._ensureViewIsIntact(); + + this.triggerMethod('before:render', this); + + this._renderTemplate(); + this.bindUIElements(); + + this.triggerMethod('render', this); + + return this; + }, + + // Internal method to render the template with the serialized data + // and template helpers via the `Marionette.Renderer` object. + // Throws an `UndefinedTemplateError` error if the template is + // any falsely value but literal `false`. + _renderTemplate: function() { + var template = this.getTemplate(); + + // Allow template-less item views + if (template === false) { + return; + } + + if (!template) { + throw new Marionette.Error({ + name: 'UndefinedTemplateError', + message: 'Cannot render the template since it is null or undefined.' + }); + } + + // Add in entity data and template helpers + var data = this.serializeData(); + data = this.mixinTemplateHelpers(data); + + // Render and add to el + var html = Marionette.Renderer.render(template, data, this); + this.attachElContent(html); + + return this; + }, + + // Attaches the content of a given view. + // This method can be overridden to optimize rendering, + // or to render in a non standard way. + // + // For example, using `innerHTML` instead of `$el.html` + // + // ```js + // attachElContent: function(html) { + // this.el.innerHTML = html; + // return this; + // } + // ``` + attachElContent: function(html) { + this.$el.html(html); + + return this; + } + }); + + /* jshint maxstatements: 14 */ + + // Collection View + // --------------- + + // A view that iterates over a Backbone.Collection + // and renders an individual child view for each model. + Marionette.CollectionView = Marionette.View.extend({ + + // used as the prefix for child view events + // that are forwarded through the collectionview + childViewEventPrefix: 'childview', + + // constructor + // option to pass `{sort: false}` to prevent the `CollectionView` from + // maintaining the sorted order of the collection. + // This will fallback onto appending childView's to the end. + constructor: function(options){ + var initOptions = options || {}; + if (_.isUndefined(this.sort)){ + this.sort = _.isUndefined(initOptions.sort) ? true : initOptions.sort; + } + + this.once('render', this._initialEvents); + this._initChildViewStorage(); + + Marionette.View.apply(this, arguments); + + this.initRenderBuffer(); + }, + + // Instead of inserting elements one by one into the page, + // it's much more performant to insert elements into a document + // fragment and then insert that document fragment into the page + initRenderBuffer: function() { + this.elBuffer = document.createDocumentFragment(); + this._bufferedChildren = []; + }, + + startBuffering: function() { + this.initRenderBuffer(); + this.isBuffering = true; + }, + + endBuffering: function() { + this.isBuffering = false; + this._triggerBeforeShowBufferedChildren(); + this.attachBuffer(this, this.elBuffer); + this._triggerShowBufferedChildren(); + this.initRenderBuffer(); + }, + + _triggerBeforeShowBufferedChildren: function() { + if (this._isShown) { + _.each(this._bufferedChildren, _.partial(this._triggerMethodOnChild, 'before:show')); + } + }, + + _triggerShowBufferedChildren: function() { + if (this._isShown) { + _.each(this._bufferedChildren, _.partial(this._triggerMethodOnChild, 'show')); + + this._bufferedChildren = []; + } + }, + + // Internal method for _.each loops to call `Marionette.triggerMethodOn` on + // a child view + _triggerMethodOnChild: function(event, childView) { + Marionette.triggerMethodOn(childView, event); + }, + + // Configured the initial events that the collection view + // binds to. + _initialEvents: function() { + if (this.collection) { + this.listenTo(this.collection, 'add', this._onCollectionAdd); + this.listenTo(this.collection, 'remove', this._onCollectionRemove); + this.listenTo(this.collection, 'reset', this.render); + + if (this.sort) { + this.listenTo(this.collection, 'sort', this._sortViews); + } + } + }, + + // Handle a child added to the collection + _onCollectionAdd: function(child) { + this.destroyEmptyView(); + var ChildView = this.getChildView(child); + var index = this.collection.indexOf(child); + this.addChild(child, ChildView, index); + }, + + // get the child view by model it holds, and remove it + _onCollectionRemove: function(model) { + var view = this.children.findByModel(model); + this.removeChildView(view); + this.checkEmpty(); + }, + + // Override from `Marionette.View` to trigger show on child views + onShowCalled: function() { + this.children.each(_.partial(this._triggerMethodOnChild, 'show')); + }, + + // Render children views. Override this method to + // provide your own implementation of a render function for + // the collection view. + render: function() { + this._ensureViewIsIntact(); + this.triggerMethod('before:render', this); + this._renderChildren(); + this.triggerMethod('render', this); + return this; + }, + + // Render view after sorting. Override this method to + // change how the view renders after a `sort` on the collection. + // An example of this would be to only `renderChildren` in a `CompositeView` + // rather than the full view. + resortView: function() { + this.render(); + }, + + // Internal method. This checks for any changes in the order of the collection. + // If the index of any view doesn't match, it will render. + _sortViews: function() { + // check for any changes in sort order of views + var orderChanged = this.collection.find(function(item, index){ + var view = this.children.findByModel(item); + return !view || view._index !== index; + }, this); + + if (orderChanged) { + this.resortView(); + } + }, + + // Internal reference to what index a `emptyView` is. + _emptyViewIndex: -1, + + // Internal method. Separated so that CompositeView can have + // more control over events being triggered, around the rendering + // process + _renderChildren: function() { + this.destroyEmptyView(); + this.destroyChildren(); + + if (this.isEmpty(this.collection)) { + this.showEmptyView(); + } else { + this.triggerMethod('before:render:collection', this); + this.startBuffering(); + this.showCollection(); + this.endBuffering(); + this.triggerMethod('render:collection', this); + } + }, + + // Internal method to loop through collection and show each child view. + showCollection: function() { + var ChildView; + this.collection.each(function(child, index) { + ChildView = this.getChildView(child); + this.addChild(child, ChildView, index); + }, this); + }, + + // Internal method to show an empty view in place of + // a collection of child views, when the collection is empty + showEmptyView: function() { + var EmptyView = this.getEmptyView(); + + if (EmptyView && !this._showingEmptyView) { + this.triggerMethod('before:render:empty'); + + this._showingEmptyView = true; + var model = new Backbone.Model(); + this.addEmptyView(model, EmptyView); + + this.triggerMethod('render:empty'); + } + }, + + // Internal method to destroy an existing emptyView instance + // if one exists. Called when a collection view has been + // rendered empty, and then a child is added to the collection. + destroyEmptyView: function() { + if (this._showingEmptyView) { + this.triggerMethod('before:remove:empty'); + + this.destroyChildren(); + delete this._showingEmptyView; + + this.triggerMethod('remove:empty'); + } + }, + + // Retrieve the empty view class + getEmptyView: function() { + return this.getOption('emptyView'); + }, + + // Render and show the emptyView. Similar to addChild method + // but "add:child" events are not fired, and the event from + // emptyView are not forwarded + addEmptyView: function(child, EmptyView) { + + // get the emptyViewOptions, falling back to childViewOptions + var emptyViewOptions = this.getOption('emptyViewOptions') || + this.getOption('childViewOptions'); + + if (_.isFunction(emptyViewOptions)){ + emptyViewOptions = emptyViewOptions.call(this, child, this._emptyViewIndex); + } + + // build the empty view + var view = this.buildChildView(child, EmptyView, emptyViewOptions); + + view._parent = this; + + // Proxy emptyView events + this.proxyChildEvents(view); + + // trigger the 'before:show' event on `view` if the collection view + // has already been shown + if (this._isShown) { + Marionette.triggerMethodOn(view, 'before:show'); + } + + // Store the `emptyView` like a `childView` so we can properly + // remove and/or close it later + this.children.add(view); + + // Render it and show it + this.renderChildView(view, this._emptyViewIndex); + + // call the 'show' method if the collection view + // has already been shown + if (this._isShown) { + Marionette.triggerMethodOn(view, 'show'); + } + }, + + // Retrieve the `childView` class, either from `this.options.childView` + // or from the `childView` in the object definition. The "options" + // takes precedence. + // This method receives the model that will be passed to the instance + // created from this `childView`. Overriding methods may use the child + // to determine what `childView` class to return. + getChildView: function(child) { + var childView = this.getOption('childView'); + + if (!childView) { + throw new Marionette.Error({ + name: 'NoChildViewError', + message: 'A "childView" must be specified' + }); + } + + return childView; + }, + + // Render the child's view and add it to the + // HTML for the collection view at a given index. + // This will also update the indices of later views in the collection + // in order to keep the children in sync with the collection. + addChild: function(child, ChildView, index) { + var childViewOptions = this.getOption('childViewOptions'); + childViewOptions = Marionette._getValue(childViewOptions, this, [child, index]); + + var view = this.buildChildView(child, ChildView, childViewOptions); + + // increment indices of views after this one + this._updateIndices(view, true, index); + + this._addChildView(view, index); + + view._parent = this; + + return view; + }, + + // Internal method. This decrements or increments the indices of views after the + // added/removed view to keep in sync with the collection. + _updateIndices: function(view, increment, index) { + if (!this.sort) { + return; + } + + if (increment) { + // assign the index to the view + view._index = index; + } + + // update the indexes of views after this one + this.children.each(function (laterView) { + if (laterView._index >= view._index) { + laterView._index += increment ? 1 : -1; + } + }); + }, + + + // Internal Method. Add the view to children and render it at + // the given index. + _addChildView: function(view, index) { + // set up the child view event forwarding + this.proxyChildEvents(view); + + this.triggerMethod('before:add:child', view); + + // Store the child view itself so we can properly + // remove and/or destroy it later + this.children.add(view); + this.renderChildView(view, index); + + if (this._isShown && !this.isBuffering) { + Marionette.triggerMethodOn(view, 'show'); + } + + this.triggerMethod('add:child', view); + }, + + // render the child view + renderChildView: function(view, index) { + view.render(); + this.attachHtml(this, view, index); + return view; + }, + + // Build a `childView` for a model in the collection. + buildChildView: function(child, ChildViewClass, childViewOptions) { + var options = _.extend({model: child}, childViewOptions); + return new ChildViewClass(options); + }, + + // Remove the child view and destroy it. + // This function also updates the indices of + // later views in the collection in order to keep + // the children in sync with the collection. + removeChildView: function(view) { + + if (view) { + this.triggerMethod('before:remove:child', view); + // call 'destroy' or 'remove', depending on which is found + if (view.destroy) { view.destroy(); } + else if (view.remove) { view.remove(); } + + delete view._parent; + this.stopListening(view); + this.children.remove(view); + this.triggerMethod('remove:child', view); + + // decrement the index of views after this one + this._updateIndices(view, false); + } + + return view; + }, + + // check if the collection is empty + isEmpty: function() { + return !this.collection || this.collection.length === 0; + }, + + // If empty, show the empty view + checkEmpty: function() { + if (this.isEmpty(this.collection)) { + this.showEmptyView(); + } + }, + + // You might need to override this if you've overridden attachHtml + attachBuffer: function(collectionView, buffer) { + collectionView.$el.append(buffer); + }, + + // Append the HTML to the collection's `el`. + // Override this method to do something other + // than `.append`. + attachHtml: function(collectionView, childView, index) { + if (collectionView.isBuffering) { + // buffering happens on reset events and initial renders + // in order to reduce the number of inserts into the + // document, which are expensive. + collectionView.elBuffer.appendChild(childView.el); + collectionView._bufferedChildren.push(childView); + } + else { + // If we've already rendered the main collection, append + // the new child into the correct order if we need to. Otherwise + // append to the end. + if (!collectionView._insertBefore(childView, index)){ + collectionView._insertAfter(childView); + } + } + }, + + // Internal method. Check whether we need to insert the view into + // the correct position. + _insertBefore: function(childView, index) { + var currentView; + var findPosition = this.sort && (index < this.children.length - 1); + if (findPosition) { + // Find the view after this one + currentView = this.children.find(function (view) { + return view._index === index + 1; + }); + } + + if (currentView) { + currentView.$el.before(childView.el); + return true; + } + + return false; + }, + + // Internal method. Append a view to the end of the $el + _insertAfter: function(childView) { + this.$el.append(childView.el); + }, + + // Internal method to set up the `children` object for + // storing all of the child views + _initChildViewStorage: function() { + this.children = new Backbone.ChildViewContainer(); + }, + + // Handle cleanup and other destroying needs for the collection of views + destroy: function() { + if (this.isDestroyed) { return; } + + this.triggerMethod('before:destroy:collection'); + this.destroyChildren(); + this.triggerMethod('destroy:collection'); + + return Marionette.View.prototype.destroy.apply(this, arguments); + }, + + // Destroy the child views that this collection view + // is holding on to, if any + destroyChildren: function() { + var childViews = this.children.map(_.identity); + this.children.each(this.removeChildView, this); + this.checkEmpty(); + return childViews; + }, + + // Set up the child view event forwarding. Uses a "childview:" + // prefix in front of all forwarded events. + proxyChildEvents: function(view) { + var prefix = this.getOption('childViewEventPrefix'); + + // Forward all child view events through the parent, + // prepending "childview:" to the event name + this.listenTo(view, 'all', function() { + var args = _.toArray(arguments); + var rootEvent = args[0]; + var childEvents = this.normalizeMethods(_.result(this, 'childEvents')); + + args[0] = prefix + ':' + rootEvent; + args.splice(1, 0, view); + + // call collectionView childEvent if defined + if (typeof childEvents !== 'undefined' && _.isFunction(childEvents[rootEvent])) { + childEvents[rootEvent].apply(this, args.slice(1)); + } + + this.triggerMethod.apply(this, args); + }, this); + }, + + _getImmediateChildren: function() { + return _.values(this.children._views); + } + }); + + /* jshint maxstatements: 17, maxlen: 117 */ + + // Composite View + // -------------- + + // Used for rendering a branch-leaf, hierarchical structure. + // Extends directly from CollectionView and also renders an + // a child view as `modelView`, for the top leaf + Marionette.CompositeView = Marionette.CollectionView.extend({ + + // Setting up the inheritance chain which allows changes to + // Marionette.CollectionView.prototype.constructor which allows overriding + // option to pass '{sort: false}' to prevent the CompositeView from + // maintaining the sorted order of the collection. + // This will fallback onto appending childView's to the end. + constructor: function() { + Marionette.CollectionView.apply(this, arguments); + }, + + // Configured the initial events that the composite view + // binds to. Override this method to prevent the initial + // events, or to add your own initial events. + _initialEvents: function() { + + // Bind only after composite view is rendered to avoid adding child views + // to nonexistent childViewContainer + + if (this.collection) { + this.listenTo(this.collection, 'add', this._onCollectionAdd); + this.listenTo(this.collection, 'remove', this._onCollectionRemove); + this.listenTo(this.collection, 'reset', this._renderChildren); + + if (this.sort) { + this.listenTo(this.collection, 'sort', this._sortViews); + } + } + }, + + // Retrieve the `childView` to be used when rendering each of + // the items in the collection. The default is to return + // `this.childView` or Marionette.CompositeView if no `childView` + // has been defined + getChildView: function(child) { + var childView = this.getOption('childView') || this.constructor; + + return childView; + }, + + // Serialize the model 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 = _.partial(this.serializeModel, this.model).apply(this, arguments); + } + + return data; + }, + + // Renders the model and the collection. + render: function() { + this._ensureViewIsIntact(); + this.isRendered = true; + this.resetChildViewContainer(); + + this.triggerMethod('before:render', this); + + this._renderTemplate(); + this._renderChildren(); + + this.triggerMethod('render', this); + return this; + }, + + _renderChildren: function() { + if (this.isRendered) { + Marionette.CollectionView.prototype._renderChildren.call(this); + } + }, + + // Render the root template that the children + // views are appended to + _renderTemplate: function() { + var data = {}; + data = this.serializeData(); + data = this.mixinTemplateHelpers(data); + + this.triggerMethod('before:render:template'); + + var template = this.getTemplate(); + var html = Marionette.Renderer.render(template, data, this); + this.attachElContent(html); + + // the ui bindings is done here and not at the end of render since they + // will not be available until after the model is rendered, but should be + // available before the collection is rendered. + this.bindUIElements(); + this.triggerMethod('render:template'); + }, + + // Attaches the content of the root. + // This method can be overridden to optimize rendering, + // or to render in a non standard way. + // + // For example, using `innerHTML` instead of `$el.html` + // + // ```js + // attachElContent: function(html) { + // this.el.innerHTML = html; + // return this; + // } + // ``` + attachElContent: function(html) { + this.$el.html(html); + + return this; + }, + + // You might need to override this if you've overridden attachHtml + attachBuffer: function(compositeView, buffer) { + var $container = this.getChildViewContainer(compositeView); + $container.append(buffer); + }, + + // Internal method. Append a view to the end of the $el. + // Overidden from CollectionView to ensure view is appended to + // childViewContainer + _insertAfter: function (childView) { + var $container = this.getChildViewContainer(this, childView); + $container.append(childView.el); + }, + + // Internal method to ensure an `$childViewContainer` exists, for the + // `attachHtml` method to use. + getChildViewContainer: function(containerView, childView) { + if ('$childViewContainer' in containerView) { + return containerView.$childViewContainer; + } + + var container; + var childViewContainer = Marionette.getOption(containerView, 'childViewContainer'); + if (childViewContainer) { + + var selector = Marionette._getValue(childViewContainer, containerView); + + if (selector.charAt(0) === '@' && containerView.ui) { + container = containerView.ui[selector.substr(4)]; + } else { + container = containerView.$(selector); + } + + if (container.length <= 0) { + throw new Marionette.Error({ + name: 'ChildViewContainerMissingError', + message: 'The specified "childViewContainer" was not found: ' + containerView.childViewContainer + }); + } + + } else { + container = containerView.$el; + } + + containerView.$childViewContainer = container; + return container; + }, + + // Internal method to reset the `$childViewContainer` on render + resetChildViewContainer: function() { + if (this.$childViewContainer) { + delete this.$childViewContainer; + } + } + }); + + // Layout View + // ----------- + + // Used for managing application layoutViews, nested layoutViews and + // multiple regions within an application or sub-application. + // + // A specialized view class that renders an area of HTML and then + // attaches `Region` instances to the specified `regions`. + // Used for composite view management and sub-application areas. + Marionette.LayoutView = Marionette.ItemView.extend({ + regionClass: Marionette.Region, + + // Ensure the regions are available when the `initialize` method + // is called. + constructor: function(options) { + options = options || {}; + + this._firstRender = true; + this._initializeRegions(options); + + Marionette.ItemView.call(this, options); + }, + + // LayoutView's render will use the existing region objects the + // first time it is called. Subsequent calls will destroy the + // views that the regions are showing and then reset the `el` + // for the regions to the newly rendered DOM elements. + render: function() { + this._ensureViewIsIntact(); + + if (this._firstRender) { + // if this is the first render, don't do anything to + // reset the regions + this._firstRender = false; + } else { + // If this is not the first render call, then we need to + // re-initialize the `el` for each region + this._reInitializeRegions(); + } + + return Marionette.ItemView.prototype.render.apply(this, arguments); + }, + + // Handle destroying regions, and then destroy the view itself. + destroy: function() { + if (this.isDestroyed) { return this; } + + this.regionManager.destroy(); + return Marionette.ItemView.prototype.destroy.apply(this, arguments); + }, + + // Add a single region, by name, to the layoutView + 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 LayoutView, by name + removeRegion: function(name) { + delete this.regions[name]; + return this.regionManager.removeRegion(name); + }, + + // Provides alternative access to regions + // Accepts the region name + // getRegion('main') + getRegion: function(region) { + return this.regionManager.get(region); + }, + + // Get all regions + getRegions: function(){ + return this.regionManager.getRegions(); + }, + + // internal method to build regions + _buildRegions: function(regions) { + var defaults = { + regionClass: this.getOption('regionClass'), + parentEl: _.partial(_.result, this, 'el') + }; + + return this.regionManager.addRegions(regions, defaults); + }, + + // Internal method to initialize the regions that have been defined in a + // `regions` attribute on this layoutView. + _initializeRegions: function(options) { + var regions; + this._initRegionManager(); + + regions = Marionette._getValue(this.regions, this, [options]) || {}; + + // Enable users to define `regions` as instance options. + var regionOptions = this.getOption.call(options, 'regions'); + + // enable region options to be a function + regionOptions = Marionette._getValue(regionOptions, this, [options]); + + _.extend(regions, regionOptions); + + // Normalize region selectors hash to allow + // a user to use the @ui. syntax. + regions = this.normalizeUIValues(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.invoke('reset'); + }, + + // Enable easy overriding of the default `RegionManager` + // for customized region interactions and business specific + // view logic for better control over single regions. + getRegionManager: function() { + return new Marionette.RegionManager(); + }, + + // Internal method to initialize the region manager + // and all regions in it + _initRegionManager: function() { + this.regionManager = this.getRegionManager(); + this.regionManager._parent = this; + + this.listenTo(this.regionManager, 'before:add:region', function(name) { + this.triggerMethod('before:add:region', name); + }); + + this.listenTo(this.regionManager, 'add:region', function(name, region) { + this[name] = region; + this.triggerMethod('add:region', name, region); + }); + + this.listenTo(this.regionManager, 'before:remove:region', function(name) { + this.triggerMethod('before:remove:region', name); + }); + + this.listenTo(this.regionManager, 'remove:region', function(name, region) { + delete this[name]; + this.triggerMethod('remove:region', name, region); + }); + }, + + _getImmediateChildren: function() { + return _.chain(this.regionManager.getRegions()) + .pluck('currentView') + .compact() + .value(); + } + }); + + + // Behavior + // -------- + + // A Behavior is an isolated set of DOM / + // user interactions that can be mixed into any View. + // Behaviors allow you to blackbox View specific interactions + // into portable logical chunks, keeping your views simple and your code DRY. + + Marionette.Behavior = Marionette.Object.extend({ + constructor: function(options, view) { + // Setup reference to the view. + // this comes in handle when a behavior + // wants to directly talk up the chain + // to the view. + this.view = view; + this.defaults = _.result(this, 'defaults') || {}; + this.options = _.extend({}, this.defaults, options); + + Marionette.Object.apply(this, arguments); + }, + + // proxy behavior $ method to the view + // this is useful for doing jquery DOM lookups + // scoped to behaviors view. + $: function() { + return this.view.$.apply(this.view, arguments); + }, + + // Stops the behavior from listening to events. + // Overrides Object#destroy to prevent additional events from being triggered. + destroy: function() { + this.stopListening(); + }, + + proxyViewProperties: function (view) { + this.$el = view.$el; + this.el = view.el; + } + }); + + /* jshint maxlen: 143 */ + // Behaviors + // --------- + + // Behaviors is a utility class that takes care of + // gluing your behavior instances to their given View. + // The most important part of this class is that you + // **MUST** override the class level behaviorsLookup + // method for things to work properly. + + Marionette.Behaviors = (function(Marionette, _) { + // Borrow event splitter from Backbone + var delegateEventSplitter = /^(\S+)\s*(.*)$/; + + function Behaviors(view, behaviors) { + + if (!_.isObject(view.behaviors)) { + return {}; + } + + // Behaviors defined on a view can be a flat object literal + // or it can be a function that returns an object. + behaviors = Behaviors.parseBehaviors(view, behaviors || _.result(view, 'behaviors')); + + // Wraps several of the view's methods + // calling the methods first on each behavior + // and then eventually calling the method on the view. + Behaviors.wrap(view, behaviors, _.keys(methods)); + return behaviors; + } + + var methods = { + behaviorTriggers: function(behaviorTriggers, behaviors) { + var triggerBuilder = new BehaviorTriggersBuilder(this, behaviors); + return triggerBuilder.buildBehaviorTriggers(); + }, + + behaviorEvents: function(behaviorEvents, behaviors) { + var _behaviorsEvents = {}; + var viewUI = this._uiBindings || _.result(this, 'ui'); + + _.each(behaviors, function(b, i) { + var _events = {}; + var behaviorEvents = _.clone(_.result(b, 'events')) || {}; + var behaviorUI = b._uiBindings || _.result(b, 'ui'); + + // Construct an internal UI hash first using + // the views UI hash and then the behaviors UI hash. + // This allows the user to use UI hash elements + // defined in the parent view as well as those + // defined in the given behavior. + var ui = _.extend({}, viewUI, behaviorUI); + + // Normalize behavior events hash to allow + // a user to use the @ui. syntax. + behaviorEvents = Marionette.normalizeUIKeys(behaviorEvents, ui); + + var j = 0; + _.each(behaviorEvents, function(behaviour, key) { + var match = key.match(delegateEventSplitter); + + // Set event name to be namespaced using the view cid, + // the behavior index, and the behavior event index + // to generate a non colliding event namespace + // http://api.jquery.com/event.namespace/ + var eventName = match[1] + '.' + [this.cid, i, j++, ' '].join(''), + selector = match[2]; + + var eventKey = eventName + selector; + var handler = _.isFunction(behaviour) ? behaviour : b[behaviour]; + + _events[eventKey] = _.bind(handler, b); + }, this); + + _behaviorsEvents = _.extend(_behaviorsEvents, _events); + }, this); + + return _behaviorsEvents; + } + }; + + _.extend(Behaviors, { + + // Placeholder method to be extended by the user. + // The method should define the object that stores the behaviors. + // i.e. + // + // ```js + // Marionette.Behaviors.behaviorsLookup: function() { + // return App.Behaviors + // } + // ``` + behaviorsLookup: function() { + throw new Marionette.Error({ + message: 'You must define where your behaviors are stored.', + url: 'marionette.behaviors.html#behaviorslookup' + }); + }, + + // Takes care of getting the behavior class + // given options and a key. + // If a user passes in options.behaviorClass + // default to using that. Otherwise delegate + // the lookup to the users `behaviorsLookup` implementation. + getBehaviorClass: function(options, key) { + if (options.behaviorClass) { + return options.behaviorClass; + } + + // Get behavior class can be either a flat object or a method + return Marionette._getValue(Behaviors.behaviorsLookup, this, [options, key])[key]; + }, + + // Iterate over the behaviors object, for each behavior + // instantiate it and get its grouped behaviors. + parseBehaviors: function(view, behaviors) { + return _.chain(behaviors).map(function(options, key) { + var BehaviorClass = Behaviors.getBehaviorClass(options, key); + + var behavior = new BehaviorClass(options, view); + var nestedBehaviors = Behaviors.parseBehaviors(view, _.result(behavior, 'behaviors')); + + return [behavior].concat(nestedBehaviors); + }).flatten().value(); + }, + + // Wrap view internal methods so that they delegate to behaviors. For example, + // `onDestroy` should trigger destroy on all of the behaviors and then destroy itself. + // i.e. + // + // `view.delegateEvents = _.partial(methods.delegateEvents, view.delegateEvents, behaviors);` + wrap: function(view, behaviors, methodNames) { + _.each(methodNames, function(methodName) { + view[methodName] = _.partial(methods[methodName], view[methodName], behaviors); + }); + } + }); + + // Class to build handlers for `triggers` on behaviors + // for views + function BehaviorTriggersBuilder(view, behaviors) { + this._view = view; + this._viewUI = _.result(view, 'ui'); + this._behaviors = behaviors; + this._triggers = {}; + } + + _.extend(BehaviorTriggersBuilder.prototype, { + // Main method to build the triggers hash with event keys and handlers + buildBehaviorTriggers: function() { + _.each(this._behaviors, this._buildTriggerHandlersForBehavior, this); + return this._triggers; + }, + + // Internal method to build all trigger handlers for a given behavior + _buildTriggerHandlersForBehavior: function(behavior, i) { + var ui = _.extend({}, this._viewUI, _.result(behavior, 'ui')); + var triggersHash = _.clone(_.result(behavior, 'triggers')) || {}; + + triggersHash = Marionette.normalizeUIKeys(triggersHash, ui); + + _.each(triggersHash, _.bind(this._setHandlerForBehavior, this, behavior, i)); + }, + + // Internal method to create and assign the trigger handler for a given + // behavior + _setHandlerForBehavior: function(behavior, i, eventName, trigger) { + // Unique identifier for the `this._triggers` hash + var triggerKey = trigger.replace(/^\S+/, function(triggerName) { + return triggerName + '.' + 'behaviortriggers' + i; + }); + + this._triggers[triggerKey] = this._view._buildViewTrigger(eventName); + } + }); + + return Behaviors; + + })(Marionette, _); + + + // App Router + // ---------- + + // 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) { + this.options = options || {}; + + Backbone.Router.apply(this, arguments); + + var appRoutes = this.getOption('appRoutes'); + var controller = this._getController(); + this.processAppRoutes(controller, appRoutes); + this.on('route', this._processOnRoute, this); + }, + + // Similar to route method on a Backbone Router but + // method is called on the controller + appRoute: function(route, methodName) { + var controller = this._getController(); + this._addAppRoute(controller, route, methodName); + }, + + // process the route event and trigger the onRoute + // method call, if it exists + _processOnRoute: function(routeName, routeArgs) { + // make sure an onRoute before trying to call it + if (_.isFunction(this.onRoute)) { + // find the path that matches the current route + var routePath = _.invert(this.getOption('appRoutes'))[routeName]; + this.onRoute(routeName, routePath, routeArgs); + } + }, + + // Internal method to process th
<TRUNCATED>
