| // MarionetteJS (Backbone.Marionette) |
| // ---------------------------------- |
| // v1.1.0 |
| // |
| // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. |
| // Distributed under MIT license |
| // |
| // http://marionettejs.com |
| |
| |
| |
| /*! |
| * Includes BabySitter |
| * https://github.com/marionettejs/backbone.babysitter/ |
| * |
| * Includes Wreqr |
| * https://github.com/marionettejs/backbone.wreqr/ |
| */ |
| |
| (function (root, factory) { |
| if (typeof exports === 'object') { |
| |
| var underscore = require('underscore'); |
| var backbone = require('backbone'); |
| var wreqr = require('backbone.wreqr'); |
| var babysitter = require('backbone.babysitter'); |
| |
| module.exports = factory(underscore, backbone, wreqr, babysitter); |
| |
| } else if (typeof define === 'function' && define.amd) { |
| |
| define(['underscore', 'backbone', 'backbone.wreqr', 'backbone.babysitter'], factory); |
| |
| } |
| }(this, function (_, Backbone) { |
| |
| var Marionette = (function(global, Backbone, _){ |
| "use strict"; |
| |
| // Define and export the Marionette namespace |
| var Marionette = {}; |
| Backbone.Marionette = Marionette; |
| |
| // Get the DOM manipulator for later use |
| Marionette.$ = Backbone.$; |
| |
| // Helpers |
| // ------- |
| |
| // For slicing `arguments` in functions |
| var protoSlice = Array.prototype.slice; |
| function slice(args) { |
| return protoSlice.call(args); |
| } |
| |
| function throwError(message, name) { |
| var error = new Error(message); |
| error.name = name || 'Error'; |
| throw error; |
| } |
| |
| // Marionette.extend |
| // ----------------- |
| |
| // Borrow the Backbone `extend` method so we can use it as needed |
| Marionette.extend = Backbone.Model.extend; |
| |
| // Marionette.getOption |
| // -------------------- |
| |
| // Retrieve an object, function or other value from a target |
| // object or its `options`, with `options` taking precedence. |
| Marionette.getOption = function(target, optionName){ |
| if (!target || !optionName){ return; } |
| var value; |
| |
| if (target.options && (optionName in target.options) && (target.options[optionName] !== undefined)){ |
| value = target.options[optionName]; |
| } else { |
| value = target[optionName]; |
| } |
| |
| return value; |
| }; |
| |
| // 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 name |
| var triggerMethod = function(event) { |
| // get the method name from the event name |
| var methodName = 'on' + event.replace(splitter, getEventName); |
| var method = this[methodName]; |
| |
| // trigger the event, if a trigger method exists |
| if(_.isFunction(this.trigger)) { |
| this.trigger.apply(this, arguments); |
| } |
| |
| // call the onMethodName if it exists |
| if (_.isFunction(method)) { |
| // pass all arguments, except the event name |
| return method.apply(this, _.tail(arguments)); |
| } |
| }; |
| |
| return triggerMethod; |
| })(); |
| |
| // DOMRefresh |
| // ---------- |
| // |
| // Monitor a view's state, and after it has been rendered and shown |
| // in the DOM, trigger a "dom:refresh" event every time it is |
| // re-rendered. |
| |
| Marionette.MonitorDOMRefresh = (function(){ |
| // 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){ |
| if (_.isFunction(view.triggerMethod)){ |
| view.triggerMethod("dom:refresh"); |
| } |
| } |
| } |
| |
| // Export public API |
| return function(view){ |
| view.listenTo(view, "show", function(){ |
| handleShow(view); |
| }); |
| |
| view.listenTo(view, "render", function(){ |
| handleRender(view); |
| }); |
| }; |
| })(); |
| |
| |
| // Marionette.bindEntityEvents & unbindEntityEvents |
| // --------------------------- |
| // |
| // These methods are used to bind/unbind a backbone "entity" (collection/model) |
| // to methods on a target object. |
| // |
| // The first 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, target); |
| }); |
| } |
| |
| // Bind the event to a supplied callback function |
| function bindToFunction(target, entity, evt, method){ |
| target.listenTo(entity, evt, method, target); |
| } |
| |
| // Bind the event to handlers specified as a string of |
| // handler names on the target object |
| function unbindFromStrings(target, entity, evt, methods){ |
| var methodNames = methods.split(/\s+/); |
| |
| _.each(methodNames,function(methodName) { |
| var method = target[methodName]; |
| target.stopListening(entity, evt, method, target); |
| }); |
| } |
| |
| // Bind the event to a supplied callback function |
| function unbindToFunction(target, entity, evt, method){ |
| target.stopListening(entity, evt, method, target); |
| } |
| |
| |
| // generic looping function |
| function iterateEvents(target, entity, bindings, functionCallback, stringCallback){ |
| if (!entity || !bindings) { return; } |
| |
| // allow the bindings to be a function |
| if (_.isFunction(bindings)){ |
| bindings = bindings.call(target); |
| } |
| |
| // iterate the bindings and bind them |
| _.each(bindings, function(methods, evt){ |
| |
| // allow for a function as the handler, |
| // or a list of event names as a string |
| if (_.isFunction(methods)){ |
| functionCallback(target, entity, evt, methods); |
| } else { |
| stringCallback(target, entity, evt, methods); |
| } |
| |
| }); |
| } |
| |
| // Export Public API |
| Marionette.bindEntityEvents = function(target, entity, bindings){ |
| iterateEvents(target, entity, bindings, bindToFunction, bindFromStrings); |
| }; |
| |
| Marionette.unbindEntityEvents = function(target, entity, bindings){ |
| iterateEvents(target, entity, bindings, unbindToFunction, unbindFromStrings); |
| }; |
| |
| })(Marionette); |
| |
| |
| // Callbacks |
| // --------- |
| |
| // A simple way of managing a collection of callbacks |
| // and executing them at a later point in time, using jQuery's |
| // `Deferred` object. |
| Marionette.Callbacks = function(){ |
| this._deferred = Marionette.$.Deferred(); |
| this._callbacks = []; |
| }; |
| |
| _.extend(Marionette.Callbacks.prototype, { |
| |
| // Add a callback to be executed. Callbacks added here are |
| // guaranteed to execute, even if they are added after the |
| // `run` method is called. |
| add: function(callback, contextOverride){ |
| this._callbacks.push({cb: callback, ctx: contextOverride}); |
| |
| this._deferred.done(function(context, options){ |
| if (contextOverride){ context = contextOverride; } |
| callback.call(context, options); |
| }); |
| }, |
| |
| // Run all registered callbacks with the context specified. |
| // Additional callbacks can be added after this has been run |
| // and they will still be executed. |
| run: function(options, context){ |
| this._deferred.resolve(context, options); |
| }, |
| |
| // Resets the list of callbacks to be run, allowing the same list |
| // to be run multiple times - whenever the `run` method is called. |
| reset: function(){ |
| var callbacks = this._callbacks; |
| this._deferred = Marionette.$.Deferred(); |
| this._callbacks = []; |
| |
| _.each(callbacks, function(cb){ |
| this.add(cb.cb, cb.ctx); |
| }, this); |
| } |
| }); |
| |
| |
| // Marionette Controller |
| // --------------------- |
| // |
| // A multi-purpose object to use as a controller for |
| // modules and routers, and as a mediator for workflow |
| // and coordination of other objects, views, and more. |
| Marionette.Controller = function(options){ |
| this.triggerMethod = Marionette.triggerMethod; |
| this.options = options || {}; |
| |
| if (_.isFunction(this.initialize)){ |
| this.initialize(this.options); |
| } |
| }; |
| |
| Marionette.Controller.extend = Marionette.extend; |
| |
| // Controller Methods |
| // -------------- |
| |
| // Ensure it can trigger events with Backbone.Events |
| _.extend(Marionette.Controller.prototype, Backbone.Events, { |
| close: function(){ |
| this.stopListening(); |
| this.triggerMethod("close"); |
| this.unbind(); |
| } |
| }); |
| |
| // Region |
| // ------ |
| // |
| // Manage the visual regions of your composite application. See |
| // http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/ |
| |
| Marionette.Region = function(options){ |
| this.options = options || {}; |
| |
| this.el = Marionette.getOption(this, "el"); |
| |
| if (!this.el){ |
| var err = new Error("An 'el' must be specified for a region."); |
| err.name = "NoElError"; |
| throw err; |
| } |
| |
| if (this.initialize){ |
| var args = Array.prototype.slice.apply(arguments); |
| this.initialize.apply(this, args); |
| } |
| }; |
| |
| |
| // Region Type methods |
| // ------------------- |
| |
| _.extend(Marionette.Region, { |
| |
| // Build an instance of a region by passing in a configuration object |
| // and a default region type to use if none is specified in the config. |
| // |
| // The config object should either be a string as a jQuery DOM selector, |
| // a Region type directly, or an object literal that specifies both |
| // a selector and regionType: |
| // |
| // ```js |
| // { |
| // selector: "#foo", |
| // regionType: MyCustomRegion |
| // } |
| // ``` |
| // |
| buildRegion: function(regionConfig, defaultRegionType){ |
| |
| var regionIsString = (typeof regionConfig === "string"); |
| var regionSelectorIsString = (typeof regionConfig.selector === "string"); |
| var regionTypeIsUndefined = (typeof regionConfig.regionType === "undefined"); |
| var regionIsType = (typeof regionConfig === "function"); |
| |
| if (!regionIsType && !regionIsString && !regionSelectorIsString) { |
| throw new Error("Region must be specified as a Region type, a selector string or an object with selector property"); |
| } |
| |
| var selector, RegionType; |
| |
| // get the selector for the region |
| |
| if (regionIsString) { |
| selector = regionConfig; |
| } |
| |
| if (regionConfig.selector) { |
| selector = regionConfig.selector; |
| } |
| |
| // get the type for the region |
| |
| if (regionIsType){ |
| RegionType = regionConfig; |
| } |
| |
| if (!regionIsType && regionTypeIsUndefined) { |
| RegionType = defaultRegionType; |
| } |
| |
| if (regionConfig.regionType) { |
| RegionType = regionConfig.regionType; |
| } |
| |
| // build the region instance |
| var region = new RegionType({ |
| el: selector |
| }); |
| |
| // override the `getEl` function if we have a parentEl |
| // this must be overridden to ensure the selector is found |
| // on the first use of the region. if we try to assign the |
| // region's `el` to `parentEl.find(selector)` in the object |
| // literal to build the region, the element will not be |
| // guaranteed to be in the DOM already, and will cause problems |
| if (regionConfig.parentEl){ |
| |
| region.getEl = function(selector) { |
| var parentEl = regionConfig.parentEl; |
| if (_.isFunction(parentEl)){ |
| parentEl = parentEl(); |
| } |
| return parentEl.find(selector); |
| }; |
| } |
| |
| return region; |
| } |
| |
| }); |
| |
| // Region Instance Methods |
| // ----------------------- |
| |
| _.extend(Marionette.Region.prototype, Backbone.Events, { |
| |
| // Displays a backbone view instance inside of the region. |
| // Handles calling the `render` method for you. Reads content |
| // directly from the `el` attribute. Also calls an optional |
| // `onShow` and `close` method on your view, just after showing |
| // or just before closing the view, respectively. |
| show: function(view){ |
| |
| this.ensureEl(); |
| |
| var isViewClosed = view.isClosed || _.isUndefined(view.$el); |
| |
| var isDifferentView = view !== this.currentView; |
| |
| if (isDifferentView) { |
| this.close(); |
| } |
| |
| view.render(); |
| |
| if (isDifferentView || isViewClosed) { |
| this.open(view); |
| } |
| |
| this.currentView = view; |
| |
| Marionette.triggerMethod.call(this, "show", view); |
| Marionette.triggerMethod.call(view, "show"); |
| }, |
| |
| ensureEl: function(){ |
| if (!this.$el || this.$el.length === 0){ |
| this.$el = this.getEl(this.el); |
| } |
| }, |
| |
| // Override this method to change how the region finds the |
| // DOM element that it manages. Return a jQuery selector object. |
| getEl: function(selector){ |
| return Marionette.$(selector); |
| }, |
| |
| // Override this method to change how the new view is |
| // appended to the `$el` that the region is managing |
| open: function(view){ |
| this.$el.empty().append(view.el); |
| }, |
| |
| // Close the current view, if there is one. If there is no |
| // current view, it does nothing and returns immediately. |
| close: function(){ |
| var view = this.currentView; |
| if (!view || view.isClosed){ return; } |
| |
| // call 'close' or 'remove', depending on which is found |
| if (view.close) { view.close(); } |
| else if (view.remove) { view.remove(); } |
| |
| Marionette.triggerMethod.call(this, "close"); |
| |
| delete this.currentView; |
| }, |
| |
| // Attach an existing view to the region. This |
| // will not call `render` or `onShow` for the new view, |
| // and will not replace the current HTML for the `el` |
| // of the region. |
| attachView: function(view){ |
| this.currentView = view; |
| }, |
| |
| // Reset the region by closing any existing view and |
| // clearing out the cached `$el`. The next time a view |
| // is shown via this region, the region will re-query the |
| // DOM for the region's `el`. |
| reset: function(){ |
| this.close(); |
| delete this.$el; |
| } |
| }); |
| |
| // Copy the `extend` function used by Backbone's classes |
| Marionette.Region.extend = Marionette.extend; |
| |
| // Marionette.RegionManager |
| // ------------------------ |
| // |
| // Manage one or more related `Marionette.Region` objects. |
| Marionette.RegionManager = (function(Marionette){ |
| |
| var RegionManager = Marionette.Controller.extend({ |
| constructor: function(options){ |
| this._regions = {}; |
| Marionette.Controller.prototype.constructor.call(this, options); |
| }, |
| |
| // Add multiple regions using an object literal, where |
| // each key becomes the region name, and each value is |
| // the region definition. |
| addRegions: function(regionDefinitions, defaults){ |
| var regions = {}; |
| |
| _.each(regionDefinitions, function(definition, name){ |
| if (typeof definition === "string"){ |
| definition = { selector: definition }; |
| } |
| |
| if (definition.selector){ |
| definition = _.defaults({}, definition, defaults); |
| } |
| |
| var region = this.addRegion(name, definition); |
| regions[name] = region; |
| }, this); |
| |
| return regions; |
| }, |
| |
| // Add an individual region to the region manager, |
| // and return the region instance |
| addRegion: function(name, definition){ |
| var region; |
| |
| var isObject = _.isObject(definition); |
| var isString = _.isString(definition); |
| var hasSelector = !!definition.selector; |
| |
| if (isString || (isObject && hasSelector)){ |
| region = Marionette.Region.buildRegion(definition, Marionette.Region); |
| } else if (_.isFunction(definition)){ |
| region = Marionette.Region.buildRegion(definition, Marionette.Region); |
| } else { |
| region = definition; |
| } |
| |
| this._store(name, region); |
| this.triggerMethod("region:add", name, region); |
| return region; |
| }, |
| |
| // Get a region by name |
| get: function(name){ |
| return this._regions[name]; |
| }, |
| |
| // Remove a region by name |
| removeRegion: function(name){ |
| var region = this._regions[name]; |
| this._remove(name, region); |
| }, |
| |
| // Close all regions in the region manager, and |
| // remove them |
| removeRegions: function(){ |
| _.each(this._regions, function(region, name){ |
| this._remove(name, region); |
| }, this); |
| }, |
| |
| // Close all regions in the region manager, but |
| // leave them attached |
| closeRegions: function(){ |
| _.each(this._regions, function(region, name){ |
| region.close(); |
| }, this); |
| }, |
| |
| // Close all regions and shut down the region |
| // manager entirely |
| close: function(){ |
| this.removeRegions(); |
| var args = Array.prototype.slice.call(arguments); |
| Marionette.Controller.prototype.close.apply(this, args); |
| }, |
| |
| // internal method to store regions |
| _store: function(name, region){ |
| this._regions[name] = region; |
| this._setLength(); |
| }, |
| |
| // internal method to remove a region |
| _remove: function(name, region){ |
| region.close(); |
| delete this._regions[name]; |
| this._setLength(); |
| this.triggerMethod("region:remove", name, region); |
| }, |
| |
| // set the number of regions current held |
| _setLength: function(){ |
| this.length = _.size(this._regions); |
| } |
| |
| }); |
| |
| // Borrowing this code from Backbone.Collection: |
| // http://backbonejs.org/docs/backbone.html#section-106 |
| // |
| // Mix in methods from Underscore, for iteration, and other |
| // collection related features. |
| var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', |
| 'select', 'reject', 'every', 'all', 'some', 'any', 'include', |
| 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', |
| 'last', 'without', 'isEmpty', 'pluck']; |
| |
| _.each(methods, function(method) { |
| RegionManager.prototype[method] = function() { |
| var regions = _.values(this._regions); |
| var args = [regions].concat(_.toArray(arguments)); |
| return _[method].apply(_, args); |
| }; |
| }); |
| |
| return RegionManager; |
| })(Marionette); |
| |
| |
| // Template Cache |
| // -------------- |
| |
| // Manage templates stored in `<script>` blocks, |
| // caching them for faster access. |
| Marionette.TemplateCache = function(templateId){ |
| this.templateId = templateId; |
| }; |
| |
| // TemplateCache object-level methods. Manage the template |
| // caches from these method calls instead of creating |
| // your own TemplateCache instances |
| _.extend(Marionette.TemplateCache, { |
| templateCaches: {}, |
| |
| // Get the specified template by id. Either |
| // retrieves the cached version, or loads it |
| // from the DOM. |
| get: function(templateId){ |
| var cachedTemplate = this.templateCaches[templateId]; |
| |
| if (!cachedTemplate){ |
| cachedTemplate = new Marionette.TemplateCache(templateId); |
| this.templateCaches[templateId] = cachedTemplate; |
| } |
| |
| return cachedTemplate.load(); |
| }, |
| |
| // Clear templates from the cache. If no arguments |
| // are specified, clears all templates: |
| // `clear()` |
| // |
| // If arguments are specified, clears each of the |
| // specified templates from the cache: |
| // `clear("#t1", "#t2", "...")` |
| clear: function(){ |
| var i; |
| var args = slice(arguments); |
| var length = args.length; |
| |
| if (length > 0){ |
| for(i=0; i<length; i++){ |
| delete this.templateCaches[args[i]]; |
| } |
| } else { |
| this.templateCaches = {}; |
| } |
| } |
| }); |
| |
| // TemplateCache instance methods, allowing each |
| // template cache object to manage its own state |
| // and know whether or not it has been loaded |
| _.extend(Marionette.TemplateCache.prototype, { |
| |
| // Internal method to load the template |
| load: function(){ |
| // Guard clause to prevent loading this template more than once |
| if (this.compiledTemplate){ |
| return this.compiledTemplate; |
| } |
| |
| // Load the template and compile it |
| var template = this.loadTemplate(this.templateId); |
| this.compiledTemplate = this.compileTemplate(template); |
| |
| return this.compiledTemplate; |
| }, |
| |
| // Load a template from the DOM, by default. Override |
| // this method to provide your own template retrieval |
| // For asynchronous loading with AMD/RequireJS, consider |
| // using a template-loader plugin as described here: |
| // https://github.com/marionettejs/backbone.marionette/wiki/Using-marionette-with-requirejs |
| loadTemplate: function(templateId){ |
| var template = Marionette.$(templateId).html(); |
| |
| if (!template || template.length === 0){ |
| throwError("Could not find template: '" + templateId + "'", "NoTemplateError"); |
| } |
| |
| return template; |
| }, |
| |
| // Pre-compile the template before caching it. Override |
| // this method if you do not need to pre-compile a template |
| // (JST / RequireJS for example) or if you want to change |
| // the template engine used (Handebars, etc). |
| compileTemplate: function(rawTemplate){ |
| return _.template(rawTemplate); |
| } |
| }); |
| |
| |
| // Renderer |
| // -------- |
| |
| // Render a template with data by passing in the template |
| // selector and the data to render. |
| Marionette.Renderer = { |
| |
| // Render a template with data. The `template` parameter is |
| // passed to the `TemplateCache` object to retrieve the |
| // template function. Override this method to provide your own |
| // custom rendering and template handling for all of Marionette. |
| render: function(template, data){ |
| |
| if (!template) { |
| var error = new Error("Cannot render the template since it's false, null or undefined."); |
| error.name = "TemplateNotFoundError"; |
| throw error; |
| } |
| |
| var templateFunc; |
| if (typeof template === "function"){ |
| templateFunc = template; |
| } else { |
| templateFunc = Marionette.TemplateCache.get(template); |
| } |
| |
| return templateFunc(data); |
| } |
| }; |
| |
| |
| |
| // Marionette.View |
| // --------------- |
| |
| // The core view type that other Marionette views extend from. |
| Marionette.View = Backbone.View.extend({ |
| |
| constructor: function(){ |
| _.bindAll(this, "render"); |
| |
| var args = Array.prototype.slice.apply(arguments); |
| Backbone.View.prototype.constructor.apply(this, args); |
| |
| Marionette.MonitorDOMRefresh(this); |
| this.listenTo(this, "show", this.onShowCalled, this); |
| }, |
| |
| // import the "triggerMethod" to trigger events with corresponding |
| // methods if the method exists |
| triggerMethod: Marionette.triggerMethod, |
| |
| // Get the template for this view |
| // instance. You can set a `template` attribute in the view |
| // definition or pass a `template: "whatever"` parameter in |
| // to the constructor options. |
| getTemplate: function(){ |
| return Marionette.getOption(this, "template"); |
| }, |
| |
| // Mix in template helper methods. Looks for a |
| // `templateHelpers` attribute, which can either be an |
| // object literal, or a function that returns an object |
| // literal. All methods and attributes from this object |
| // are copies to the object passed in. |
| mixinTemplateHelpers: function(target){ |
| target = target || {}; |
| var templateHelpers = Marionette.getOption(this, "templateHelpers"); |
| if (_.isFunction(templateHelpers)){ |
| templateHelpers = templateHelpers.call(this); |
| } |
| return _.extend(target, templateHelpers); |
| }, |
| |
| // 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 = _.result(this, "triggers"); |
| |
| // Configure the triggers, prevent default |
| // action and stop propagation of DOM events |
| _.each(triggers, function(value, key){ |
| |
| // build the event handler function for the DOM event |
| triggerEvents[key] = function(e){ |
| |
| // stop the event in its tracks |
| if (e && e.preventDefault){ e.preventDefault(); } |
| if (e && e.stopPropagation){ e.stopPropagation(); } |
| |
| // build the args for the event |
| var args = { |
| view: this, |
| model: this.model, |
| collection: this.collection |
| }; |
| |
| // trigger the event |
| this.triggerMethod(value, args); |
| }; |
| |
| }, this); |
| |
| return triggerEvents; |
| }, |
| |
| // Overriding Backbone.View's delegateEvents to handle |
| // the `triggers`, `modelEvents`, and `collectionEvents` configuration |
| delegateEvents: function(events){ |
| this._delegateDOMEvents(events); |
| Marionette.bindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents")); |
| Marionette.bindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents")); |
| }, |
| |
| // internal method to delegate DOM events and triggers |
| _delegateDOMEvents: function(events){ |
| events = events || this.events; |
| if (_.isFunction(events)){ events = events.call(this); } |
| |
| var combinedEvents = {}; |
| var triggers = this.configureTriggers(); |
| _.extend(combinedEvents, events, triggers); |
| |
| Backbone.View.prototype.delegateEvents.call(this, combinedEvents); |
| }, |
| |
| // Overriding Backbone.View's undelegateEvents to handle unbinding |
| // the `triggers`, `modelEvents`, and `collectionEvents` config |
| undelegateEvents: function(){ |
| var args = Array.prototype.slice.call(arguments); |
| Backbone.View.prototype.undelegateEvents.apply(this, args); |
| |
| Marionette.unbindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents")); |
| Marionette.unbindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents")); |
| }, |
| |
| // Internal method, handles the `show` event. |
| onShowCalled: function(){}, |
| |
| // Default `close` implementation, for removing a view from the |
| // DOM and unbinding it. Regions will call this method |
| // for you. You can specify an `onClose` method in your view to |
| // add custom code that is called after the view is closed. |
| close: function(){ |
| if (this.isClosed) { return; } |
| |
| // allow the close to be stopped by returning `false` |
| // from the `onBeforeClose` method |
| var shouldClose = this.triggerMethod("before:close"); |
| if (shouldClose === false){ |
| return; |
| } |
| |
| // mark as closed before doing the actual close, to |
| // prevent infinite loops within "close" event handlers |
| // that are trying to close other views |
| this.isClosed = true; |
| this.triggerMethod("close"); |
| |
| // unbind UI elements |
| this.unbindUIElements(); |
| |
| // remove the view from the DOM |
| this.remove(); |
| }, |
| |
| // This method binds the elements specified in the "ui" hash inside the view's code with |
| // the associated jQuery selectors. |
| bindUIElements: function(){ |
| if (!this.ui) { return; } |
| |
| // store the ui hash in _uiBindings so they can be reset later |
| // and so re-rendering the view will be able to find the bindings |
| if (!this._uiBindings){ |
| this._uiBindings = this.ui; |
| } |
| |
| // get the bindings result, as a function or otherwise |
| var bindings = _.result(this, "_uiBindings"); |
| |
| // empty the ui so we don't have anything to start with |
| this.ui = {}; |
| |
| // bind each of the selectors |
| _.each(_.keys(bindings), function(key) { |
| var selector = bindings[key]; |
| this.ui[key] = this.$(selector); |
| }, this); |
| }, |
| |
| // This method unbinds the elements specified in the "ui" hash |
| unbindUIElements: function(){ |
| if (!this.ui || !this._uiBindings){ return; } |
| |
| // delete all of the existing ui bindings |
| _.each(this.ui, function($el, name){ |
| delete this.ui[name]; |
| }, this); |
| |
| // reset the ui element to the original bindings configuration |
| this.ui = this._uiBindings; |
| delete this._uiBindings; |
| } |
| }); |
| |
| // Item View |
| // --------- |
| |
| // A single item view implementation that contains code for rendering |
| // with underscore.js templates, serializing the view's model or collection, |
| // and calling several methods on extended views, such as `onRender`. |
| Marionette.ItemView = Marionette.View.extend({ |
| |
| // Setting up the inheritance chain which allows changes to |
| // Marionette.View.prototype.constructor which allows overriding |
| constructor: function(){ |
| Marionette.View.prototype.constructor.apply(this, slice(arguments)); |
| }, |
| |
| // Serialize the model or collection for the view. If a model is |
| // found, `.toJSON()` is called. If a collection is found, `.toJSON()` |
| // is also called, but is used to populate an `items` array in the |
| // resulting data. If both are found, defaults to the model. |
| // You can override the `serializeData` method in your own view |
| // definition, to provide custom serialization for your view's data. |
| serializeData: function(){ |
| var data = {}; |
| |
| if (this.model) { |
| data = this.model.toJSON(); |
| } |
| else if (this.collection) { |
| data = { items: this.collection.toJSON() }; |
| } |
| |
| return data; |
| }, |
| |
| // Render the view, defaulting to underscore.js templates. |
| // You can override this in your view definition to provide |
| // a very specific rendering for your view. In general, though, |
| // you should override the `Marionette.Renderer` object to |
| // change how Marionette renders views. |
| render: function(){ |
| this.isClosed = false; |
| |
| this.triggerMethod("before:render", this); |
| this.triggerMethod("item:before:render", this); |
| |
| var data = this.serializeData(); |
| data = this.mixinTemplateHelpers(data); |
| |
| var template = this.getTemplate(); |
| var html = Marionette.Renderer.render(template, data); |
| |
| this.$el.html(html); |
| this.bindUIElements(); |
| |
| this.triggerMethod("render", this); |
| this.triggerMethod("item:rendered", this); |
| |
| return this; |
| }, |
| |
| // Override the default close event to add a few |
| // more events that are triggered. |
| close: function(){ |
| if (this.isClosed){ return; } |
| |
| this.triggerMethod('item:before:close'); |
| |
| Marionette.View.prototype.close.apply(this, slice(arguments)); |
| |
| this.triggerMethod('item:closed'); |
| } |
| }); |
| |
| // Collection View |
| // --------------- |
| |
| // A view that iterates over a Backbone.Collection |
| // and renders an individual ItemView for each model. |
| Marionette.CollectionView = Marionette.View.extend({ |
| // used as the prefix for item view events |
| // that are forwarded through the collectionview |
| itemViewEventPrefix: "itemview", |
| |
| // constructor |
| constructor: function(options){ |
| this._initChildViewStorage(); |
| |
| Marionette.View.prototype.constructor.apply(this, slice(arguments)); |
| |
| this._initialEvents(); |
| }, |
| |
| // Configured the initial events that the collection view |
| // binds to. Override this method to prevent the initial |
| // events, or to add your own initial events. |
| _initialEvents: function(){ |
| if (this.collection){ |
| this.listenTo(this.collection, "add", this.addChildView, this); |
| this.listenTo(this.collection, "remove", this.removeItemView, this); |
| this.listenTo(this.collection, "reset", this.render, this); |
| } |
| }, |
| |
| // Handle a child item added to the collection |
| addChildView: function(item, collection, options){ |
| this.closeEmptyView(); |
| var ItemView = this.getItemView(item); |
| var index = this.collection.indexOf(item); |
| this.addItemView(item, ItemView, index); |
| }, |
| |
| // Override from `Marionette.View` to guarantee the `onShow` method |
| // of child views is called. |
| onShowCalled: function(){ |
| this.children.each(function(child){ |
| Marionette.triggerMethod.call(child, "show"); |
| }); |
| }, |
| |
| // Internal method to trigger the before render callbacks |
| // and events |
| triggerBeforeRender: function(){ |
| this.triggerMethod("before:render", this); |
| this.triggerMethod("collection:before:render", this); |
| }, |
| |
| // Internal method to trigger the rendered callbacks and |
| // events |
| triggerRendered: function(){ |
| this.triggerMethod("render", this); |
| this.triggerMethod("collection:rendered", this); |
| }, |
| |
| // Render the collection of items. Override this method to |
| // provide your own implementation of a render function for |
| // the collection view. |
| render: function(){ |
| this.isClosed = false; |
| this.triggerBeforeRender(); |
| this._renderChildren(); |
| this.triggerRendered(); |
| return this; |
| }, |
| |
| // Internal method. Separated so that CompositeView can have |
| // more control over events being triggered, around the rendering |
| // process |
| _renderChildren: function(){ |
| this.closeEmptyView(); |
| this.closeChildren(); |
| |
| if (this.collection && this.collection.length > 0) { |
| this.showCollection(); |
| } else { |
| this.showEmptyView(); |
| } |
| }, |
| |
| // Internal method to loop through each item in the |
| // collection view and show it |
| showCollection: function(){ |
| var ItemView; |
| this.collection.each(function(item, index){ |
| ItemView = this.getItemView(item); |
| this.addItemView(item, ItemView, index); |
| }, this); |
| }, |
| |
| // Internal method to show an empty view in place of |
| // a collection of item views, when the collection is |
| // empty |
| showEmptyView: function(){ |
| var EmptyView = Marionette.getOption(this, "emptyView"); |
| |
| if (EmptyView && !this._showingEmptyView){ |
| this._showingEmptyView = true; |
| var model = new Backbone.Model(); |
| this.addItemView(model, EmptyView, 0); |
| } |
| }, |
| |
| // Internal method to close an existing emptyView instance |
| // if one exists. Called when a collection view has been |
| // rendered empty, and then an item is added to the collection. |
| closeEmptyView: function(){ |
| if (this._showingEmptyView){ |
| this.closeChildren(); |
| delete this._showingEmptyView; |
| } |
| }, |
| |
| // Retrieve the itemView type, either from `this.options.itemView` |
| // or from the `itemView` in the object definition. The "options" |
| // takes precedence. |
| getItemView: function(item){ |
| var itemView = Marionette.getOption(this, "itemView"); |
| |
| if (!itemView){ |
| throwError("An `itemView` must be specified", "NoItemViewError"); |
| } |
| |
| return itemView; |
| }, |
| |
| // Render the child item's view and add it to the |
| // HTML for the collection view. |
| addItemView: function(item, ItemView, index){ |
| // get the itemViewOptions if any were specified |
| var itemViewOptions = Marionette.getOption(this, "itemViewOptions"); |
| if (_.isFunction(itemViewOptions)){ |
| itemViewOptions = itemViewOptions.call(this, item, index); |
| } |
| |
| // build the view |
| var view = this.buildItemView(item, ItemView, itemViewOptions); |
| |
| // set up the child view event forwarding |
| this.addChildViewEventForwarding(view); |
| |
| // this view is about to be added |
| this.triggerMethod("before:item:added", view); |
| |
| // Store the child view itself so we can properly |
| // remove and/or close it later |
| this.children.add(view); |
| |
| // Render it and show it |
| this.renderItemView(view, index); |
| |
| // call the "show" method if the collection view |
| // has already been shown |
| if (this._isShown){ |
| Marionette.triggerMethod.call(view, "show"); |
| } |
| |
| // this view was added |
| this.triggerMethod("after:item:added", view); |
| }, |
| |
| // Set up the child view event forwarding. Uses an "itemview:" |
| // prefix in front of all forwarded events. |
| addChildViewEventForwarding: function(view){ |
| var prefix = Marionette.getOption(this, "itemViewEventPrefix"); |
| |
| // Forward all child item view events through the parent, |
| // prepending "itemview:" to the event name |
| this.listenTo(view, "all", function(){ |
| var args = slice(arguments); |
| args[0] = prefix + ":" + args[0]; |
| args.splice(1, 0, view); |
| |
| Marionette.triggerMethod.apply(this, args); |
| }, this); |
| }, |
| |
| // render the item view |
| renderItemView: function(view, index) { |
| view.render(); |
| this.appendHtml(this, view, index); |
| }, |
| |
| // Build an `itemView` for every model in the collection. |
| buildItemView: function(item, ItemViewType, itemViewOptions){ |
| var options = _.extend({model: item}, itemViewOptions); |
| return new ItemViewType(options); |
| }, |
| |
| // get the child view by item it holds, and remove it |
| removeItemView: function(item){ |
| var view = this.children.findByModel(item); |
| this.removeChildView(view); |
| this.checkEmpty(); |
| }, |
| |
| // Remove the child view and close it |
| removeChildView: function(view){ |
| |
| // shut down the child view properly, |
| // including events that the collection has from it |
| if (view){ |
| this.stopListening(view); |
| |
| // call 'close' or 'remove', depending on which is found |
| if (view.close) { view.close(); } |
| else if (view.remove) { view.remove(); } |
| |
| this.children.remove(view); |
| } |
| |
| this.triggerMethod("item:removed", view); |
| }, |
| |
| // helper to show the empty view if the collection is empty |
| checkEmpty: function() { |
| // check if we're empty now, and if we are, show the |
| // empty view |
| if (!this.collection || this.collection.length === 0){ |
| this.showEmptyView(); |
| } |
| }, |
| |
| // Append the HTML to the collection's `el`. |
| // Override this method to do something other |
| // then `.append`. |
| appendHtml: function(collectionView, itemView, index){ |
| collectionView.$el.append(itemView.el); |
| }, |
| |
| // Internal method to set up the `children` object for |
| // storing all of the child views |
| _initChildViewStorage: function(){ |
| this.children = new Backbone.ChildViewContainer(); |
| }, |
| |
| // Handle cleanup and other closing needs for |
| // the collection of views. |
| close: function(){ |
| if (this.isClosed){ return; } |
| |
| this.triggerMethod("collection:before:close"); |
| this.closeChildren(); |
| this.triggerMethod("collection:closed"); |
| |
| Marionette.View.prototype.close.apply(this, slice(arguments)); |
| }, |
| |
| // Close the child views that this collection view |
| // is holding on to, if any |
| closeChildren: function(){ |
| this.children.each(function(child){ |
| this.removeChildView(child); |
| }, this); |
| this.checkEmpty(); |
| } |
| }); |
| |
| |
| // Composite View |
| // -------------- |
| |
| // Used for rendering a branch-leaf, hierarchical structure. |
| // Extends directly from CollectionView and also renders an |
| // an item view as `modelView`, for the top leaf |
| Marionette.CompositeView = Marionette.CollectionView.extend({ |
| |
| // Setting up the inheritance chain which allows changes to |
| // Marionette.CollectionView.prototype.constructor which allows overriding |
| constructor: function(){ |
| Marionette.CollectionView.prototype.constructor.apply(this, slice(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(){ |
| if (this.collection){ |
| this.listenTo(this.collection, "add", this.addChildView, this); |
| this.listenTo(this.collection, "remove", this.removeItemView, this); |
| this.listenTo(this.collection, "reset", this._renderChildren, this); |
| } |
| }, |
| |
| // Retrieve the `itemView` to be used when rendering each of |
| // the items in the collection. The default is to return |
| // `this.itemView` or Marionette.CompositeView if no `itemView` |
| // has been defined |
| getItemView: function(item){ |
| var itemView = Marionette.getOption(this, "itemView") || this.constructor; |
| |
| if (!itemView){ |
| throwError("An `itemView` must be specified", "NoItemViewError"); |
| } |
| |
| return itemView; |
| }, |
| |
| // Serialize the collection for the view. |
| // You can override the `serializeData` method in your own view |
| // definition, to provide custom serialization for your view's data. |
| serializeData: function(){ |
| var data = {}; |
| |
| if (this.model){ |
| data = this.model.toJSON(); |
| } |
| |
| return data; |
| }, |
| |
| // Renders the model once, and the collection once. Calling |
| // this again will tell the model's view to re-render itself |
| // but the collection will not re-render. |
| render: function(){ |
| this.isRendered = true; |
| this.isClosed = false; |
| this.resetItemViewContainer(); |
| |
| this.triggerBeforeRender(); |
| var html = this.renderModel(); |
| this.$el.html(html); |
| // the ui bindings is done here and not at the end of render since they |
| // will not be available until after the model is rendered, but should be |
| // available before the collection is rendered. |
| this.bindUIElements(); |
| this.triggerMethod("composite:model:rendered"); |
| |
| this._renderChildren(); |
| |
| this.triggerMethod("composite:rendered"); |
| this.triggerRendered(); |
| return this; |
| }, |
| |
| _renderChildren: function(){ |
| if (this.isRendered){ |
| Marionette.CollectionView.prototype._renderChildren.call(this); |
| this.triggerMethod("composite:collection:rendered"); |
| } |
| }, |
| |
| // Render an individual model, if we have one, as |
| // part of a composite view (branch / leaf). For example: |
| // a treeview. |
| renderModel: function(){ |
| var data = {}; |
| data = this.serializeData(); |
| data = this.mixinTemplateHelpers(data); |
| |
| var template = this.getTemplate(); |
| return Marionette.Renderer.render(template, data); |
| }, |
| |
| // Appends the `el` of itemView instances to the specified |
| // `itemViewContainer` (a jQuery selector). Override this method to |
| // provide custom logic of how the child item view instances have their |
| // HTML appended to the composite view instance. |
| appendHtml: function(cv, iv, index){ |
| var $container = this.getItemViewContainer(cv); |
| $container.append(iv.el); |
| }, |
| |
| // Internal method to ensure an `$itemViewContainer` exists, for the |
| // `appendHtml` method to use. |
| getItemViewContainer: function(containerView){ |
| if ("$itemViewContainer" in containerView){ |
| return containerView.$itemViewContainer; |
| } |
| |
| var container; |
| var itemViewContainer = Marionette.getOption(containerView, "itemViewContainer"); |
| if (itemViewContainer){ |
| |
| var selector = _.isFunction(itemViewContainer) ? itemViewContainer() : itemViewContainer; |
| container = containerView.$(selector); |
| if (container.length <= 0) { |
| throwError("The specified `itemViewContainer` was not found: " + containerView.itemViewContainer, "ItemViewContainerMissingError"); |
| } |
| |
| } else { |
| container = containerView.$el; |
| } |
| |
| containerView.$itemViewContainer = container; |
| return container; |
| }, |
| |
| // Internal method to reset the `$itemViewContainer` on render |
| resetItemViewContainer: function(){ |
| if (this.$itemViewContainer){ |
| delete this.$itemViewContainer; |
| } |
| } |
| }); |
| |
| |
| // Layout |
| // ------ |
| |
| // Used for managing application layouts, nested layouts and |
| // multiple regions within an application or sub-application. |
| // |
| // A specialized view type that renders an area of HTML and then |
| // attaches `Region` instances to the specified `regions`. |
| // Used for composite view management and sub-application areas. |
| Marionette.Layout = Marionette.ItemView.extend({ |
| regionType: Marionette.Region, |
| |
| // Ensure the regions are available when the `initialize` method |
| // is called. |
| constructor: function (options) { |
| options = options || {}; |
| |
| this._firstRender = true; |
| this._initializeRegions(options); |
| |
| Marionette.ItemView.prototype.constructor.call(this, options); |
| }, |
| |
| // Layout's render will use the existing region objects the |
| // first time it is called. Subsequent calls will close the |
| // views that the regions are showing and then reset the `el` |
| // for the regions to the newly rendered DOM elements. |
| render: function(){ |
| |
| if (this.isClosed){ |
| // a previously closed layout means we need to |
| // completely re-initialize the regions |
| this._initializeRegions(); |
| } |
| if (this._firstRender) { |
| // if this is the first render, don't do anything to |
| // reset the regions |
| this._firstRender = false; |
| } else if (!this.isClosed){ |
| // If this is not the first render call, then we need to |
| // re-initializing the `el` for each region |
| this._reInitializeRegions(); |
| } |
| |
| var args = Array.prototype.slice.apply(arguments); |
| var result = Marionette.ItemView.prototype.render.apply(this, args); |
| |
| return result; |
| }, |
| |
| // Handle closing regions, and then close the view itself. |
| close: function () { |
| if (this.isClosed){ return; } |
| this.regionManager.close(); |
| var args = Array.prototype.slice.apply(arguments); |
| Marionette.ItemView.prototype.close.apply(this, args); |
| }, |
| |
| // Add a single region, by name, to the layout |
| addRegion: function(name, definition){ |
| var regions = {}; |
| regions[name] = definition; |
| return this._buildRegions(regions)[name]; |
| }, |
| |
| // Add multiple regions as a {name: definition, name2: def2} object literal |
| addRegions: function(regions){ |
| this.regions = _.extend({}, this.regions, regions); |
| return this._buildRegions(regions); |
| }, |
| |
| // Remove a single region from the Layout, by name |
| removeRegion: function(name){ |
| delete this.regions[name]; |
| return this.regionManager.removeRegion(name); |
| }, |
| |
| // internal method to build regions |
| _buildRegions: function(regions){ |
| var that = this; |
| |
| var defaults = { |
| regionType: Marionette.getOption(this, "regionType"), |
| parentEl: function(){ return that.$el; } |
| }; |
| |
| return this.regionManager.addRegions(regions, defaults); |
| }, |
| |
| // Internal method to initialize the regions that have been defined in a |
| // `regions` attribute on this layout. |
| _initializeRegions: function (options) { |
| var regions; |
| this._initRegionManager(); |
| |
| if (_.isFunction(this.regions)) { |
| regions = this.regions(options); |
| } else { |
| regions = this.regions || {}; |
| } |
| |
| this.addRegions(regions); |
| }, |
| |
| // Internal method to re-initialize all of the regions by updating the `el` that |
| // they point to |
| _reInitializeRegions: function(){ |
| this.regionManager.closeRegions(); |
| this.regionManager.each(function(region){ |
| region.reset(); |
| }); |
| }, |
| |
| // Internal method to initialize the region manager |
| // and all regions in it |
| _initRegionManager: function(){ |
| this.regionManager = new Marionette.RegionManager(); |
| |
| this.listenTo(this.regionManager, "region:add", function(name, region){ |
| this[name] = region; |
| this.trigger("region:add", name, region); |
| }); |
| |
| this.listenTo(this.regionManager, "region:remove", function(name, region){ |
| delete this[name]; |
| this.trigger("region:remove", name, region); |
| }); |
| } |
| }); |
| |
| |
| // AppRouter |
| // --------- |
| |
| // Reduce the boilerplate code of handling route events |
| // and then calling a single method on another object. |
| // Have your routers configured to call the method on |
| // your object, directly. |
| // |
| // Configure an AppRouter with `appRoutes`. |
| // |
| // App routers can only take one `controller` object. |
| // It is recommended that you divide your controller |
| // objects in to smaller pieces of related functionality |
| // and have multiple routers / controllers, instead of |
| // just one giant router and controller. |
| // |
| // You can also add standard routes to an AppRouter. |
| |
| Marionette.AppRouter = Backbone.Router.extend({ |
| |
| constructor: function(options){ |
| Backbone.Router.prototype.constructor.apply(this, slice(arguments)); |
| |
| this.options = options || {}; |
| |
| var appRoutes = Marionette.getOption(this, "appRoutes"); |
| var controller = this._getController(); |
| this.processAppRoutes(controller, appRoutes); |
| }, |
| |
| // Similar to route method on a Backbone Router but |
| // method is called on the controller |
| appRoute: function(route, methodName) { |
| var controller = this._getController(); |
| this._addAppRoute(controller, route, methodName); |
| }, |
| |
| // Internal method to process the `appRoutes` for the |
| // router, and turn them in to routes that trigger the |
| // specified method on the specified `controller`. |
| processAppRoutes: function(controller, appRoutes) { |
| if (!appRoutes){ return; } |
| |
| var routeNames = _.keys(appRoutes).reverse(); // Backbone requires reverted order of routes |
| |
| _.each(routeNames, function(route) { |
| this._addAppRoute(controller, route, appRoutes[route]); |
| }, this); |
| }, |
| |
| _getController: function(){ |
| return Marionette.getOption(this, "controller"); |
| }, |
| |
| _addAppRoute: function(controller, route, methodName){ |
| var method = controller[methodName]; |
| |
| if (!method) { |
| throw new Error("Method '" + methodName + "' was not found on the controller"); |
| } |
| |
| this.route(route, methodName, _.bind(method, controller)); |
| } |
| }); |
| |
| |
| // Application |
| // ----------- |
| |
| // Contain and manage the composite application as a whole. |
| // Stores and starts up `Region` objects, includes an |
| // event aggregator as `app.vent` |
| Marionette.Application = function(options){ |
| this._initRegionManager(); |
| this._initCallbacks = new Marionette.Callbacks(); |
| this.vent = new Backbone.Wreqr.EventAggregator(); |
| this.commands = new Backbone.Wreqr.Commands(); |
| this.reqres = new Backbone.Wreqr.RequestResponse(); |
| this.submodules = {}; |
| |
| _.extend(this, options); |
| |
| this.triggerMethod = Marionette.triggerMethod; |
| }; |
| |
| _.extend(Marionette.Application.prototype, Backbone.Events, { |
| // Command execution, facilitated by Backbone.Wreqr.Commands |
| execute: function(){ |
| var args = Array.prototype.slice.apply(arguments); |
| this.commands.execute.apply(this.commands, args); |
| }, |
| |
| // Request/response, facilitated by Backbone.Wreqr.RequestResponse |
| request: function(){ |
| var args = Array.prototype.slice.apply(arguments); |
| return this.reqres.request.apply(this.reqres, args); |
| }, |
| |
| // Add an initializer that is either run at when the `start` |
| // method is called, or run immediately if added after `start` |
| // has already been called. |
| addInitializer: function(initializer){ |
| this._initCallbacks.add(initializer); |
| }, |
| |
| // kick off all of the application's processes. |
| // initializes all of the regions that have been added |
| // to the app, and runs all of the initializer functions |
| start: function(options){ |
| this.triggerMethod("initialize:before", options); |
| this._initCallbacks.run(options, this); |
| this.triggerMethod("initialize:after", options); |
| |
| this.triggerMethod("start", options); |
| }, |
| |
| // Add regions to your app. |
| // Accepts a hash of named strings or Region objects |
| // addRegions({something: "#someRegion"}) |
| // addRegions({something: Region.extend({el: "#someRegion"}) }); |
| addRegions: function(regions){ |
| return this._regionManager.addRegions(regions); |
| }, |
| |
| // Close all regions in the app, without removing them |
| closeRegions: function(){ |
| this._regionManager.closeRegions(); |
| }, |
| |
| // Removes a region from your app, by name |
| // Accepts the regions name |
| // removeRegion('myRegion') |
| removeRegion: function(region) { |
| this._regionManager.removeRegion(region); |
| }, |
| |
| // Provides alternative access to regions |
| // Accepts the region name |
| // getRegion('main') |
| getRegion: function(region) { |
| return this._regionManager.get(region); |
| }, |
| |
| // Create a module, attached to the application |
| module: function(moduleNames, moduleDefinition){ |
| // slice the args, and add this application object as the |
| // first argument of the array |
| var args = slice(arguments); |
| args.unshift(this); |
| |
| // see the Marionette.Module object for more information |
| return Marionette.Module.create.apply(Marionette.Module, args); |
| }, |
| |
| // Internal method to set up the region manager |
| _initRegionManager: function(){ |
| this._regionManager = new Marionette.RegionManager(); |
| |
| this.listenTo(this._regionManager, "region:add", function(name, region){ |
| this[name] = region; |
| }); |
| |
| this.listenTo(this._regionManager, "region:remove", function(name, region){ |
| delete this[name]; |
| }); |
| } |
| }); |
| |
| // Copy the `extend` function used by Backbone's classes |
| Marionette.Application.extend = Marionette.extend; |
| |
| // Module |
| // ------ |
| |
| // A simple module system, used to create privacy and encapsulation in |
| // Marionette applications |
| Marionette.Module = function(moduleName, app){ |
| this.moduleName = moduleName; |
| |
| // store sub-modules |
| this.submodules = {}; |
| |
| this._setupInitializersAndFinalizers(); |
| |
| // store the configuration for this module |
| this.app = app; |
| this.startWithParent = true; |
| |
| this.triggerMethod = Marionette.triggerMethod; |
| }; |
| |
| // 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, { |
| |
| // Initializer for a specific module. Initializers are run when the |
| // module's `start` method is called. |
| addInitializer: function(callback){ |
| this._initializerCallbacks.add(callback); |
| }, |
| |
| // Finalizers are run when a module is stopped. They are used to teardown |
| // and finalize any variables, references, events and other code that the |
| // module had set up. |
| addFinalizer: function(callback){ |
| this._finalizerCallbacks.add(callback); |
| }, |
| |
| // Start the module, and run all of its initializers |
| start: function(options){ |
| // Prevent re-starting a module that is already started |
| if (this._isInitialized){ return; } |
| |
| // start the sub-modules (depth-first hierarchy) |
| _.each(this.submodules, function(mod){ |
| // check to see if we should start the sub-module with this parent |
| if (mod.startWithParent){ |
| mod.start(options); |
| } |
| }); |
| |
| // run the callbacks to "start" the current module |
| this.triggerMethod("before:start", options); |
| |
| this._initializerCallbacks.run(options, this); |
| this._isInitialized = true; |
| |
| this.triggerMethod("start", options); |
| }, |
| |
| // Stop this module by running its finalizers and then stop all of |
| // the sub-modules for this module |
| stop: function(){ |
| // if we are not initialized, don't bother finalizing |
| if (!this._isInitialized){ return; } |
| this._isInitialized = false; |
| |
| Marionette.triggerMethod.call(this, "before:stop"); |
| |
| // stop the sub-modules; depth-first, to make sure the |
| // sub-modules are stopped / finalized before parents |
| _.each(this.submodules, function(mod){ mod.stop(); }); |
| |
| // run the finalizers |
| this._finalizerCallbacks.run(undefined,this); |
| |
| // reset the initializers and finalizers |
| this._initializerCallbacks.reset(); |
| this._finalizerCallbacks.reset(); |
| |
| Marionette.triggerMethod.call(this, "stop"); |
| }, |
| |
| // Configure the module with a definition function and any custom args |
| // that are to be passed in to the definition function |
| addDefinition: function(moduleDefinition, customArgs){ |
| this._runModuleDefinition(moduleDefinition, customArgs); |
| }, |
| |
| // Internal method: run the module definition function with the correct |
| // arguments |
| _runModuleDefinition: function(definition, customArgs){ |
| if (!definition){ return; } |
| |
| // build the correct list of arguments for the module definition |
| var args = _.flatten([ |
| this, |
| this.app, |
| Backbone, |
| Marionette, |
| Marionette.$, _, |
| customArgs |
| ]); |
| |
| definition.apply(this, args); |
| }, |
| |
| // Internal method: set up new copies of initializers and finalizers. |
| // Calling this method will wipe out all existing initializers and |
| // finalizers. |
| _setupInitializersAndFinalizers: function(){ |
| this._initializerCallbacks = new Marionette.Callbacks(); |
| this._finalizerCallbacks = new Marionette.Callbacks(); |
| } |
| }); |
| |
| // Type methods to create modules |
| _.extend(Marionette.Module, { |
| |
| // Create a module, hanging off the app parameter as the parent object. |
| create: function(app, moduleNames, moduleDefinition){ |
| var module = app; |
| |
| // get the custom args passed in after the module definition and |
| // get rid of the module name and definition function |
| var customArgs = slice(arguments); |
| customArgs.splice(0, 3); |
| |
| // split the module names and get the length |
| moduleNames = moduleNames.split("."); |
| var length = moduleNames.length; |
| |
| // store the module definition for the last module in the chain |
| var moduleDefinitions = []; |
| moduleDefinitions[length-1] = moduleDefinition; |
| |
| // Loop through all the parts of the module definition |
| _.each(moduleNames, function(moduleName, i){ |
| var parentModule = module; |
| module = this._getModule(parentModule, moduleName, app); |
| 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){ |
| // 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 Marionette.Module(moduleName, app); |
| parentModule[moduleName] = module; |
| // store the module on the parent |
| parentModule.submodules[moduleName] = module; |
| } |
| |
| return module; |
| }, |
| |
| _addModuleDefinition: function(parentModule, module, def, args){ |
| var fn; |
| var startWithParent; |
| |
| if (_.isFunction(def)){ |
| // if a function is supplied for the module definition |
| fn = def; |
| startWithParent = true; |
| |
| } else if (_.isObject(def)){ |
| // if an object is supplied |
| fn = def.define; |
| startWithParent = def.startWithParent; |
| |
| } else { |
| // if nothing is supplied |
| startWithParent = true; |
| } |
| |
| // add module definition if needed |
| if (fn){ |
| module.addDefinition(fn, args); |
| } |
| |
| // `and` the two together, ensuring a single `false` will prevent it |
| // from starting with the parent |
| module.startWithParent = module.startWithParent && startWithParent; |
| |
| // setup auto-start if needed |
| if (module.startWithParent && !module.startWithParentIsConfigured){ |
| |
| // only configure this once |
| module.startWithParentIsConfigured = true; |
| |
| // add the module initializer config |
| parentModule.addInitializer(function(options){ |
| if (module.startWithParent){ |
| module.start(options); |
| } |
| }); |
| |
| } |
| |
| } |
| }); |
| |
| |
| |
| return Marionette; |
| })(this, Backbone, _); |
| |
| return Backbone.Marionette; |
| |
| })); |