| // MarionetteJS (Backbone.Marionette) |
| // ---------------------------------- |
| // v2.0.1 |
| // |
| // Copyright (c)2014 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 = 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 = factory(root, root.Backbone, root._); |
| } |
| |
| }(this, function(root, Backbone, _) { |
| 'use strict'; |
| |
| var previousMarionette = root.Marionette; |
| |
| var Marionette = Backbone.Marionette = {}; |
| |
| Marionette.VERSION = '2.0.1'; |
| |
| Marionette.noConflict = function() { |
| root.Marionette = previousMarionette; |
| return this; |
| }; |
| |
| // Get the Deferred creator for later use |
| Marionette.Deferred = Backbone.$.Deferred; |
| |
| /* jshint unused: false */ |
| |
| // 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 && (target.options[optionName] !== undefined)) { |
| value = target.options[optionName]; |
| } else { |
| value = target[optionName]; |
| } |
| |
| return value; |
| }; |
| |
| // Proxy `Marionette.getOption` |
| Marionette.proxyGetOption = function(optionName) { |
| return Marionette.getOption(this, optionName); |
| }; |
| |
| // Marionette.normalizeMethods |
| // ---------------------- |
| |
| // Pass in a mapping of events => functions or function names |
| // and return a mapping of events => functions |
| Marionette.normalizeMethods = function(hash) { |
| var normalizedHash = {}, method; |
| _.each(hash, function(fn, name) { |
| method = fn; |
| if (!_.isFunction(method)) { |
| method = this[method]; |
| } |
| if (!method) { |
| return; |
| } |
| normalizedHash[name] = method; |
| }, this); |
| return normalizedHash; |
| }; |
| |
| |
| // allows for the use of the @ui. syntax within |
| // a given key for triggers and events |
| // swaps the @ui with the associated selector |
| Marionette.normalizeUIKeys = function(hash, ui) { |
| if (typeof(hash) === 'undefined') { |
| return; |
| } |
| |
| _.each(_.keys(hash), function(v) { |
| var pattern = /@ui.[a-zA-Z_$0-9]*/g; |
| if (v.match(pattern)) { |
| hash[v.replace(pattern, function(r) { |
| return ui[r.slice(4)]; |
| })] = hash[v]; |
| delete hash[v]; |
| } |
| }); |
| |
| return hash; |
| }; |
| |
| // 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-106 |
| 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); |
| }; |
| }); |
| }; |
| |
| // 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]; |
| var result; |
| |
| // call the onMethodName if it exists |
| if (_.isFunction(method)) { |
| // pass all arguments, except the event name |
| result = method.apply(this, _.tail(arguments)); |
| } |
| |
| // trigger the event, if a trigger method exists |
| if (_.isFunction(this.trigger)) { |
| this.trigger.apply(this, arguments); |
| } |
| |
| return result; |
| }; |
| |
| 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); |
| |
| |
| /* jshint maxparams: 5 */ |
| |
| // 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); |
| }; |
| |
| // 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); |
| |
| |
| // 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); |
| } |
| }); |
| |
| // 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, { |
| destroy: function() { |
| var args = Array.prototype.slice.call(arguments); |
| this.triggerMethod.apply(this, ['before:destroy'].concat(args)); |
| this.triggerMethod.apply(this, ['destroy'].concat(args)); |
| |
| this.stopListening(); |
| this.off(); |
| }, |
| |
| // 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 |
| |
| }); |
| |
| /* jshint maxcomplexity: 10, maxstatements: 27 */ |
| |
| // 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 = 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) { |
| throwError('An "el" must be specified for a region.', 'NoElError'); |
| } |
| |
| this.$el = this.getEl(this.el); |
| |
| if (this.initialize) { |
| var args = Array.prototype.slice.apply(arguments); |
| this.initialize.apply(this, args); |
| } |
| }; |
| |
| |
| // Region Class methods |
| // ------------------- |
| |
| _.extend(Marionette.Region, { |
| |
| // 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 both |
| // a selector and regionClass: |
| // |
| // ```js |
| // { |
| // selector: "#foo", |
| // regionClass: MyCustomRegion |
| // } |
| // ``` |
| // |
| buildRegion: function(regionConfig, defaultRegionClass) { |
| var regionIsString = _.isString(regionConfig); |
| var regionSelectorIsString = _.isString(regionConfig.selector); |
| var regionClassIsUndefined = _.isUndefined(regionConfig.regionClass); |
| var regionIsClass = _.isFunction(regionConfig); |
| |
| if (!regionIsClass && !regionIsString && !regionSelectorIsString) { |
| throwError('Region must be specified as a Region class,' + |
| 'a selector string or an object with selector property'); |
| } |
| |
| var selector, RegionClass; |
| |
| // get the selector for the region |
| |
| if (regionIsString) { |
| selector = regionConfig; |
| } |
| |
| if (regionConfig.selector) { |
| selector = regionConfig.selector; |
| delete regionConfig.selector; |
| } |
| |
| // get the class for the region |
| |
| if (regionIsClass) { |
| RegionClass = regionConfig; |
| } |
| |
| if (!regionIsClass && regionClassIsUndefined) { |
| RegionClass = defaultRegionClass; |
| } |
| |
| if (regionConfig.regionClass) { |
| RegionClass = regionConfig.regionClass; |
| delete regionConfig.regionClass; |
| } |
| |
| if (regionIsString || regionIsClass) { |
| regionConfig = {}; |
| } |
| |
| regionConfig.el = selector; |
| |
| // build the region instance |
| var region = new RegionClass(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(el) { |
| if (_.isObject(el)) { |
| return Backbone.$(el); |
| } |
| var parentEl = regionConfig.parentEl; |
| if (_.isFunction(parentEl)) { |
| parentEl = parentEl(); |
| } |
| return parentEl.find(el); |
| }; |
| } |
| |
| 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 `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){ |
| this._ensureElement(); |
| |
| 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 view to change to begin with |
| var isChangingView = !!this.currentView; |
| |
| // only destroy the view if we don't want to preventDestroy and the view is different |
| var _shouldDestroyView = !preventDestroy && isDifferentView; |
| |
| if (_shouldDestroyView) { |
| this.empty(); |
| } |
| |
| // show the view if the view is different or if you want to re-show the view |
| var _shouldShowView = isDifferentView || forceShow; |
| |
| if (_shouldShowView) { |
| view.render(); |
| |
| if (isChangingView) { |
| this.triggerMethod('before:swap', view); |
| } |
| |
| this.triggerMethod('before:show', view); |
| this.triggerMethod.call(view, 'before:show'); |
| |
| this.attachHtml(view); |
| this.currentView = view; |
| |
| if (isChangingView) { |
| this.triggerMethod('swap', view); |
| } |
| |
| this.triggerMethod('show', view); |
| |
| if (_.isFunction(view.triggerMethod)) { |
| view.triggerMethod('show'); |
| } else { |
| this.triggerMethod.call(view, 'show'); |
| } |
| |
| return this; |
| } |
| |
| return this; |
| }, |
| |
| _ensureElement: function(){ |
| if (!_.isObject(this.el)) { |
| this.$el = this.getEl(this.el); |
| this.el = this.$el[0]; |
| } |
| |
| if (!this.$el || this.$el.length === 0) { |
| throwError('An "el" ' + this.$el.selector + ' must exist in DOM'); |
| } |
| }, |
| |
| // Override this method to change how the region finds the |
| // DOM element that it manages. Return a jQuery selector object. |
| getEl: function(el) { |
| return Backbone.$(el); |
| }, |
| |
| // Override this method to change how the new view is |
| // appended to the `$el` that the region is managing |
| attachHtml: function(view) { |
| // empty the node and append new view |
| this.el.innerHTML=''; |
| 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 (!view || view.isDestroyed) { return; } |
| |
| this.triggerMethod('before:empty', view); |
| |
| // call 'destroy' or 'remove', depending on which is found |
| if (view.destroy) { view.destroy(); } |
| else if (view.remove) { view.remove(); } |
| |
| this.triggerMethod('empty', 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 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; |
| }, |
| |
| // Proxy `getOption` to enable getting options from this or this.options by name. |
| getOption: Marionette.proxyGetOption, |
| |
| // import the `triggerMethod` to trigger events with corresponding |
| // methods if the method exists |
| triggerMethod: Marionette.triggerMethod |
| }); |
| |
| // 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.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.triggerMethod('before:add:region', name, region); |
| |
| 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); |
| }, |
| |
| // Empty all regions in the region manager, and |
| // remove them |
| removeRegions: function() { |
| _.each(this._regions, function(region, name) { |
| this._remove(name, region); |
| }, this); |
| }, |
| |
| // Empty all regions in the region manager, but |
| // leave them attached |
| emptyRegions: function() { |
| _.each(this._regions, function(region) { |
| region.empty(); |
| }, this); |
| }, |
| |
| // Destroy all regions and shut down the region |
| // manager entirely |
| destroy: function() { |
| this.removeRegions(); |
| 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 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(RegionManager.prototype, '_regions'); |
| |
| 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 = Backbone.$(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 its false, null or undefined.', |
| 'TemplateNotFoundError'); |
| } |
| |
| var templateFunc; |
| if (typeof template === 'function') { |
| templateFunc = template; |
| } else { |
| templateFunc = Marionette.TemplateCache.get(template); |
| } |
| |
| return templateFunc(data); |
| } |
| }; |
| |
| |
| /* jshint maxlen: 114, nonew: false */ |
| // Marionette.View |
| // --------------- |
| |
| // The core view class 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')); |
| |
| if (_.isObject(this.behaviors)) { |
| new Marionette.Behaviors(this); |
| } |
| |
| Backbone.View.apply(this, arguments); |
| |
| Marionette.MonitorDOMRefresh(this); |
| this.listenTo(this, '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'); |
| }, |
| |
| // 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'); |
| if (_.isFunction(templateHelpers)) { |
| templateHelpers = templateHelpers.call(this); |
| } |
| return _.extend(target, templateHelpers); |
| }, |
| |
| |
| normalizeUIKeys: function(hash) { |
| var ui = _.result(this, 'ui'); |
| var uiBindings = _.result(this, '_uiBindings'); |
| return Marionette.normalizeUIKeys(hash, uiBindings || ui); |
| }, |
| |
| // 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); |
| this.bindEntityEvents(this.model, this.getOption('modelEvents')); |
| this.bindEntityEvents(this.collection, this.getOption('collectionEvents')); |
| }, |
| |
| // internal method to delegate DOM events and triggers |
| _delegateDOMEvents: function(events) { |
| events = events || this.events; |
| if (_.isFunction(events)) { events = events.call(this); } |
| |
| // normalize ui keys |
| events = this.normalizeUIKeys(events); |
| |
| var combinedEvents = {}; |
| |
| // look up if this view has behavior events |
| var behaviorEvents = _.result(this, 'behaviorEvents') || {}; |
| var triggers = this.configureTriggers(); |
| |
| // behavior events will be overriden by view events and or triggers |
| _.extend(combinedEvents, behaviorEvents, 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); |
| this.unbindEntityEvents(this.model, this.getOption('modelEvents')); |
| this.unbindEntityEvents(this.collection, this.getOption('collectionEvents')); |
| }, |
| |
| // 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) { |
| var err = new Error('Cannot use a view thats already been destroyed.'); |
| err.name = 'ViewDestroyedError'; |
| throw err; |
| } |
| }, |
| |
| // 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 = Array.prototype.slice.call(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(); |
| }, |
| |
| // 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; |
| }, |
| |
| // 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, |
| |
| // Proxy `getOption` to enable getting options from this or this.options by name. |
| getOption: Marionette.proxyGetOption, |
| |
| // Proxy `unbindEntityEvents` 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, `.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._ensureViewIsIntact(); |
| |
| this.triggerMethod('before:render', this); |
| |
| var data = this.serializeData(); |
| data = this.mixinTemplateHelpers(data); |
| |
| var template = this.getTemplate(); |
| var html = Marionette.Renderer.render(template, data); |
| this.attachElContent(html); |
| this.bindUIElements(); |
| |
| this.triggerMethod('render', this); |
| |
| return this; |
| }, |
| |
| // Attaches the content of a given view. |
| // This method can be overriden 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; |
| }, |
| |
| // Override the default destroy event to add a few |
| // more events that are triggered. |
| destroy: function() { |
| if (this.isDestroyed) { return; } |
| |
| Marionette.View.prototype.destroy.apply(this, arguments); |
| } |
| }); |
| |
| /* 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 || {}; |
| this.sort = _.isUndefined(initOptions.sort) ? true : initOptions.sort; |
| |
| this._initChildViewStorage(); |
| |
| Marionette.View.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._triggerBeforeShowBufferedChildren(); |
| this.attachBuffer(this, this.elBuffer); |
| this._triggerShowBufferedChildren(); |
| this.initRenderBuffer(); |
| }, |
| |
| _triggerBeforeShowBufferedChildren: function() { |
| if (this._isShown) { |
| _.invoke(this._bufferedChildren, 'triggerMethod', 'before:show'); |
| } |
| }, |
| |
| _triggerShowBufferedChildren: function() { |
| if (this._isShown) { |
| _.each(this._bufferedChildren, function (child) { |
| if (_.isFunction(child.triggerMethod)) { |
| child.triggerMethod('show'); |
| } else { |
| 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._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, collection, options) { |
| 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(function(child){ |
| if (_.isFunction(child.triggerMethod)) { |
| child.triggerMethod('show'); |
| } else { |
| Marionette.triggerMethod.call(child, '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; |
| }, |
| |
| // 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.render(); |
| } |
| }, |
| |
| // Internal method. Separated so that CompositeView can have |
| // more control over events being triggered, around the rendering |
| // process |
| _renderChildren: function() { |
| this.startBuffering(); |
| |
| this.destroyEmptyView(); |
| this.destroyChildren(); |
| |
| if (!this.isEmpty(this.collection)) { |
| this.triggerMethod('before:render:collection', this); |
| this.showCollection(); |
| this.triggerMethod('render:collection', this); |
| } else { |
| this.showEmptyView(); |
| } |
| |
| this.endBuffering(); |
| }, |
| |
| // 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.destroyChildren(); |
| delete this._showingEmptyView; |
| } |
| }, |
| |
| // Retrieve the empty view class |
| getEmptyView: function() { |
| return this.getOption('emptyView'); |
| }, |
| |
| // Render and show the emptyView. Similar to addChild method |
| // but "child:added" 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); |
| } |
| |
| // build the empty view |
| var view = this.buildChildView(child, EmptyView, emptyViewOptions); |
| |
| // trigger the 'before:show' event on `view` if the collection view |
| // has already been shown |
| if (this._isShown){ |
| this.triggerMethod.call(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, -1); |
| |
| // call the 'show' method if the collection view |
| // has already been shown |
| if (this._isShown){ |
| this.triggerMethod.call(view, 'show'); |
| } |
| }, |
| |
| // Retrieve the childView class, either from `this.options.childView` |
| // or from the `childView` in the object definition. The "options" |
| // takes precedence. |
| getChildView: function(child) { |
| var childView = this.getOption('childView'); |
| |
| if (!childView) { |
| throwError('A "childView" must be specified', 'NoChildViewError'); |
| } |
| |
| 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'); |
| if (_.isFunction(childViewOptions)) { |
| childViewOptions = childViewOptions.call(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); |
| |
| 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; |
| |
| // increment the index of views after this one |
| this.children.each(function (laterView) { |
| if (laterView._index >= view._index) { |
| laterView._index++; |
| } |
| }); |
| } |
| else { |
| // decrement the index of views after this one |
| this.children.each(function (laterView) { |
| if (laterView._index >= view._index) { |
| laterView._index--; |
| } |
| }); |
| } |
| }, |
| |
| |
| // 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){ |
| if (_.isFunction(view.triggerMethod)) { |
| view.triggerMethod('show'); |
| } else { |
| Marionette.triggerMethod.call(view, 'show'); |
| } |
| } |
| |
| this.triggerMethod('add:child', view); |
| }, |
| |
| // render the child view |
| renderChildView: function(view, index) { |
| view.render(); |
| this.attachHtml(this, view, index); |
| }, |
| |
| // 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(); } |
| |
| this.stopListening(view); |
| this.children.remove(view); |
| this.triggerMethod('remove:child', view); |
| |
| // decrement the index of views after this one |
| this._updateIndices(view, false); |
| } |
| |
| }, |
| |
| // check if the collection is empty |
| isEmpty: function(collection) { |
| 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'); |
| |
| Marionette.View.prototype.destroy.apply(this, arguments); |
| }, |
| |
| // Destroy the child views that this collection view |
| // is holding on to, if any |
| destroyChildren: function() { |
| this.children.each(this.removeChildView, this); |
| this.checkEmpty(); |
| }, |
| |
| // 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 = Array.prototype.slice.call(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); |
| } |
| }); |
| |
| /* 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 |
| this.once('render', function() { |
| 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; |
| |
| if (!childView) { |
| throwError('A "childView" must be specified', 'NoChildViewError'); |
| } |
| |
| return childView; |
| }, |
| |
| // 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._ensureViewIsIntact(); |
| this.isRendered = true; |
| this.resetChildViewContainer(); |
| |
| this.triggerMethod('before:render', this); |
| |
| this._renderRoot(); |
| 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 |
| _renderRoot: 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.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 overriden 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); |
| $container.append(childView.el); |
| }, |
| |
| // Internal method to ensure an `$childViewContainer` exists, for the |
| // `attachHtml` method to use. |
| getChildViewContainer: function(containerView) { |
| if ('$childViewContainer' in containerView) { |
| return containerView.$childViewContainer; |
| } |
| |
| var container; |
| var childViewContainer = Marionette.getOption(containerView, 'childViewContainer'); |
| if (childViewContainer) { |
| |
| var selector = _.isFunction(childViewContainer) ? childViewContainer.call(containerView) : childViewContainer; |
| |
| if (selector.charAt(0) === '@' && containerView.ui) { |
| container = containerView.ui[selector.substr(4)]; |
| } else { |
| container = containerView.$(selector); |
| } |
| |
| if (container.length <= 0) { |
| throwError('The specified "childViewContainer" was not found: ' + |
| containerView.childViewContainer, 'ChildViewContainerMissingError'); |
| } |
| |
| } 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; |
| } |
| } |
| }); |
| |
| // LayoutView |
| // ---------- |
| |
| // 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.regionManager.destroy(); |
| Marionette.ItemView.prototype.destroy.apply(this, arguments); |
| }, |
| |
| // Add a single region, by name, to the layoutView |
| addRegion: function(name, definition) { |
| this.triggerMethod('before:region:add', name); |
| 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) { |
| this.triggerMethod('before:region:remove', 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 that = this; |
| |
| var defaults = { |
| regionClass: this.getOption('regionClass'), |
| 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 layoutView. |
| _initializeRegions: function(options) { |
| var regions; |
| this._initRegionManager(); |
| |
| if (_.isFunction(this.regions)) { |
| regions = this.regions(options); |
| } else { |
| regions = this.regions || {}; |
| } |
| |
| // Enable users to define `regions` as instance options. |
| var regionOptions = this.getOption.call(options, 'regions'); |
| |
| // enable region options to be a function |
| if (_.isFunction(regionOptions)) { |
| regionOptions = regionOptions.call(this, options); |
| } |
| |
| _.extend(regions, regionOptions); |
| |
| this.addRegions(regions); |
| }, |
| |
| // Internal method to re-initialize all of the regions by updating the `el` that |
| // they point to |
| _reInitializeRegions: function() { |
| this.regionManager.emptyRegions(); |
| this.regionManager.each(function(region) { |
| region.reset(); |
| }); |
| }, |
| |
| // Enable easy overiding of the default `RegionManager` |
| // for customized region interactions and buisness 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.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); |
| }); |
| } |
| }); |
| |
| |
| // 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 = (function(_, Backbone) { |
| function Behavior(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); |
| |
| // proxy behavior $ method to the view |
| // this is useful for doing jquery DOM lookups |
| // scoped to behaviors view. |
| this.$ = function() { |
| return this.view.$.apply(this.view, arguments); |
| }; |
| |
| // Call the initialize method passing |
| // the arguments from the instance constructor |
| this.initialize.apply(this, arguments); |
| } |
| |
| _.extend(Behavior.prototype, Backbone.Events, { |
| initialize: function() {}, |
| |
| // stopListening to behavior `onListen` events. |
| destroy: function() { |
| 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 `unbindEntityEvents` 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 |
| }); |
| |
| // Borrow Backbones extend implementation |
| // this allows us to setup a proper |
| // inheritence pattern that follow in suite |
| // with the rest of Marionette views. |
| Behavior.extend = Marionette.extend; |
| |
| return Behavior; |
| })(_, Backbone); |
| |
| /* jshint maxlen: 143, nonew: false */ |
| // Marionette.Behaviors |
| // -------- |
| |
| // Behaviors is a utility class that takes care of |
| // glueing 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, _) { |
| |
| function Behaviors(view, behaviors) { |
| // 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, [ |
| 'bindUIElements', 'unbindUIElements', |
| 'delegateEvents', 'undelegateEvents', |
| 'behaviorEvents', 'triggerMethod', |
| 'setElement', 'destroy' |
| ]); |
| } |
| |
| var methods = { |
| setElement: function(setElement, behaviors) { |
| setElement.apply(this, _.tail(arguments, 2)); |
| |
| // 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. |
| _.each(behaviors, function(b) { |
| b.$el = this.$el; |
| }, this); |
| }, |
| |
| destroy: function(destroy, behaviors) { |
| var args = _.tail(arguments, 2); |
| destroy.apply(this, args); |
| |
| // Call destroy on each behavior after |
| // destroying the view. |
| // This unbinds event listeners |
| // that behaviors have registerd for. |
| _.invoke(behaviors, 'destroy', args); |
| }, |
| |
| bindUIElements: function(bindUIElements, behaviors) { |
| bindUIElements.apply(this); |
| _.invoke(behaviors, bindUIElements); |
| }, |
| |
| unbindUIElements: function(unbindUIElements, behaviors) { |
| unbindUIElements.apply(this); |
| _.invoke(behaviors, unbindUIElements); |
| }, |
| |
| triggerMethod: function(triggerMethod, behaviors) { |
| var args = _.tail(arguments, 2); |
| triggerMethod.apply(this, args); |
| |
| _.each(behaviors, function(b) { |
| triggerMethod.apply(b, args); |
| }); |
| }, |
| |
| delegateEvents: function(delegateEvents, behaviors) { |
| var args = _.tail(arguments, 2); |
| delegateEvents.apply(this, args); |
| |
| _.each(behaviors, function(b) { |
| Marionette.bindEntityEvents(b, this.model, Marionette.getOption(b, 'modelEvents')); |
| Marionette.bindEntityEvents(b, this.collection, Marionette.getOption(b, 'collectionEvents')); |
| }, this); |
| }, |
| |
| undelegateEvents: function(undelegateEvents, behaviors) { |
| var args = _.tail(arguments, 2); |
| undelegateEvents.apply(this, args); |
| |
| _.each(behaviors, function(b) { |
| Marionette.unbindEntityEvents(b, this.model, Marionette.getOption(b, 'modelEvents')); |
| Marionette.unbindEntityEvents(b, this.collection, Marionette.getOption(b, 'collectionEvents')); |
| }, this); |
| }, |
| |
| behaviorEvents: function(behaviorEvents, behaviors) { |
| var _behaviorsEvents = {}; |
| var viewUI = _.result(this, 'ui'); |
| |
| _.each(behaviors, function(b, i) { |
| var _events = {}; |
| var behaviorEvents = _.clone(_.result(b, 'events')) || {}; |
| var behaviorUI = _.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); |
| |
| _.each(_.keys(behaviorEvents), function(key) { |
| // Append white-space at the end of each key to prevent behavior key collisions. |
| // This is relying on the fact that backbone events considers "click .foo" the same as |
| // "click .foo ". |
| |
| // +2 is used because new Array(1) or 0 is "" and not " " |
| var whitespace = (new Array(i + 2)).join(' '); |
| var eventKey = key + whitespace; |
| var handler = _.isFunction(behaviorEvents[key]) ? behaviorEvents[key] : b[behaviorEvents[key]]; |
| |
| _events[eventKey] = _.bind(handler, b); |
| }); |
| |
| _behaviorsEvents = _.extend(_behaviorsEvents, _events); |
| }); |
| |
| 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 Error('You must define where your behaviors are stored.' + |
| 'See https://github.com/marionettejs/backbone.marionette' + |
| '/blob/master/docs/marionette.behaviors.md#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 _.isFunction(Behaviors.behaviorsLookup) ? Behaviors.behaviorsLookup.apply(this, arguments)[key] : Behaviors.behaviorsLookup[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); |
| }); |
| } |
| }); |
| |
| return Behaviors; |
| |
| })(Marionette, _); |
| |
| |
| // 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.apply(this, arguments); |
| |
| this.options = options || {}; |
| |
| 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) { |
| // find the path that matched |
| var routePath = _.invert(this.appRoutes)[routeName]; |
| |
| // make sure an onRoute is there, and call it |
| if (_.isFunction(this.onRoute)) { |
| this.onRoute(routeName, routePath, routeArgs); |
| } |
| }, |
| |
| // 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 this.getOption('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)); |
| }, |
| |
| // Proxy `getOption` to enable getting options from this or this.options by name. |
| getOption: Marionette.proxyGetOption |
| }); |
| |
| // 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(); |
| var globalCh = Backbone.Wreqr.radio.channel('global'); |
| this.vent = globalCh.vent; |
| this.commands = globalCh.commands; |
| this.reqres = globalCh.reqres; |
| this.submodules = {}; |
| |
| _.extend(this, options); |
| }; |
| |
| _.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('before:start', options); |
| this._initCallbacks.run(options, this); |
| 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); |
| }, |
| |
| // Empty all regions in the app, without removing them |
| emptyRegions: function() { |
| this._regionManager.emptyRegions(); |
| }, |
| |
| // 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); |
| }, |
| |
| // Get all the regions from the region manager |
| getRegions: function(){ |
| return this._regionManager.getRegions(); |
| }, |
| |
| // 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, '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); |
| }); |
| }, |
| |
| // import the `triggerMethod` to trigger events with corresponding |
| // methods if the method exists |
| triggerMethod: Marionette.triggerMethod |
| }); |
| |
| // Copy the `extend` function used by Backbone's classes |
| Marionette.Application.extend = Marionette.extend; |
| |
| /* jshint maxparams: 9 */ |
| |
| // 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); |
| // Allow for a user to overide the initialize |
| // for a given module instance. |
| this.initialize = options.initialize || this.initialize; |
| |
| // Set up an internal store for sub-modules. |
| this.submodules = {}; |
| |
| this._setupInitializersAndFinalizers(); |
| |
| // Set an internal reference to the app |
| // within a module. |
| this.app = app; |
| |
| // By default modules start with their parents. |
| this.startWithParent = true; |
| |
| if (_.isFunction(this.initialize)) { |
| this.initialize(moduleName, app, this.options); |
| } |
| }; |
| |
| 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; |
| |
| this.triggerMethod('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(); |
| |
| this.triggerMethod('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 there is no definition short circut the method. |
| if (!definition) { return; } |
| |
| // build the correct list of arguments for the module definition |
| var args = _.flatten([ |
| this, |
| this.app, |
| Backbone, |
| Marionette, |
| Backbone.$, _, |
| 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(); |
| }, |
| |
| // import the `triggerMethod` to trigger events with corresponding |
| // methods if the method exists |
| triggerMethod: Marionette.triggerMethod |
| }); |
| |
| // Class 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 number of submodules. |
| // i.e. an example module name of `Doge.Wow.Amaze` would |
| // then have the potential for 3 module definitions. |
| 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; |
| }, |
| |
| // ## Module Classes |
| // |
| // Module classes can be used as an alternative to the define pattern. |
| // The extend function of a Module is identical to the extend functions |
| // on other Backbone and Marionette classes. |
| // This allows module lifecyle events like `onStart` and `onStop` to be called directly. |
| getClass: function(moduleDefinition) { |
| var ModuleClass = Marionette.Module; |
| |
| if (!moduleDefinition) { |
| return ModuleClass; |
| } |
| |
| // If all of the module's functionality is defined inside its class, |
| // then the class can be passed in directly. `MyApp.module("Foo", FooModule)`. |
| if (moduleDefinition.prototype instanceof ModuleClass) { |
| return moduleDefinition; |
| } |
| |
| return moduleDefinition.moduleClass || ModuleClass; |
| }, |
| |
| // Add the module definition and add a startWithParent initializer function. |
| // This is complicated because module definitions are heavily overloaded |
| // and support an anonymous function, module class, or options object |
| _addModuleDefinition: function(parentModule, module, def, args) { |
| var fn = this._getDefine(def); |
| var startWithParent = this._getStartWithParent(def, module); |
| |
| if (fn) { |
| module.addDefinition(fn, args); |
| } |
| |
| this._addStartWithParent(parentModule, module, startWithParent); |
| }, |
| |
| _getStartWithParent: function(def, module) { |
| var swp; |
| |
| if (_.isFunction(def) && (def.prototype instanceof Marionette.Module)) { |
| swp = module.constructor.prototype.startWithParent; |
| return _.isUndefined(swp) ? true : swp; |
| } |
| |
| if (_.isObject(def)) { |
| swp = def.startWithParent; |
| return _.isUndefined(swp) ? true : swp; |
| } |
| |
| return true; |
| }, |
| |
| _getDefine: function(def) { |
| if (_.isFunction(def) && !(def.prototype instanceof Marionette.Module)) { |
| return def; |
| } |
| |
| if (_.isObject(def)) { |
| return def.define; |
| } |
| |
| return null; |
| }, |
| |
| _addStartWithParent: function(parentModule, module, startWithParent) { |
| module.startWithParent = module.startWithParent && startWithParent; |
| |
| if (!module.startWithParent || !!module.startWithParentIsConfigured) { |
| return; |
| } |
| |
| module.startWithParentIsConfigured = true; |
| |
| parentModule.addInitializer(function(options) { |
| if (module.startWithParent) { |
| module.start(options); |
| } |
| }); |
| } |
| }); |
| |
| |
| return Marionette; |
| })); |