From 32381d8e66a230b61151590e6252dd7a04f97a62 Mon Sep 17 00:00:00 2001 From: SupportKit Date: Mon, 13 Jul 2015 17:23:40 +0000 Subject: [PATCH] release 0.2.13 --- dist/supportkit.js | 18159 +++++++++++++++++++++++++++++++++++++++ dist/supportkit.min.js | 11 + 2 files changed, 18170 insertions(+) create mode 100644 dist/supportkit.js create mode 100644 dist/supportkit.min.js diff --git a/dist/supportkit.js b/dist/supportkit.js new file mode 100644 index 00000000..f52e5214 --- /dev/null +++ b/dist/supportkit.js @@ -0,0 +1,18159 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.SupportKit = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 && original != updated) { + original.parents = _.difference(original.parents, [this]); + // Don't bubble to this parent anymore + original._proxyCallback && original.off("all", original._proxyCallback, this); + } + }, + + // Returns new `collection` (or derivatives) of type `options.model`. + _createCollection: function (type, options) { + options = _.defaults(options, {model: type.model}); + var c = new type([], _.isFunction(options) ? options.call(this) : options); + c.parents = [this]; + return c; + }, + + // Process all pending events after the entire object graph has been updated + _processPendingEvents:function () { + if (!this._processedEvents) { + this._processedEvents = true; + + this._deferEvents = false; + + // Trigger all pending events + _.each(this._pendingEvents, function (e) { + e.c.trigger.apply(e.c, e.a); + }); + + this._pendingEvents = []; + + // Traverse down the object graph and call process pending events on sub-trees + _.each(this.relations, function (relation) { + var val = this.attributes[relation.key]; + val && val._processPendingEvents && val._processPendingEvents(); + }, this); + + delete this._processedEvents; + } + }, + + // Process the raw `relatedModel` value set in a relation + _transformRelatedModel: function (relation, attributes) { + + var relatedModel = relation.relatedModel; + var activationContext = relation.scope || root; + + // Call function if relatedModel is implemented as a function + if (relatedModel && !(relatedModel.prototype instanceof BackboneModel)) + relatedModel = _.isFunction(relatedModel) ? + relatedModel.call(this, relation, attributes) : + relatedModel; + + // Get class if relation and map is stored as a string. + if (relatedModel && _.isString(relatedModel)) { + relatedModel = (relatedModel === Backbone.Self) ? + this.constructor : + map2Scope(relatedModel, activationContext); + } + + // Error checking + if (relation.type === Backbone.One) { + + if (!relatedModel) + throw new Error('specify a relatedModel for Backbone.One type'); + + if (!(relatedModel.prototype instanceof Backbone.Model)) + throw new Error('specify an AssociatedModel or Backbone.Model for Backbone.One type'); + } + + return relatedModel; + }, + + // Process the raw `collectionType` value set in a relation + _transformCollectionType: function (relation, relatedModel, attributes) { + + var collectionType = relation.collectionType; + var activationContext = relation.scope || root; + + if (collectionType && _.isFunction(collectionType) && + (collectionType.prototype instanceof BackboneModel)) + throw new Error('type is of Backbone.Model. Specify derivatives of Backbone.Collection'); + + // Call function if collectionType is implemented as a function + if (collectionType && !(collectionType.prototype instanceof BackboneCollection)) + collectionType = _.isFunction(collectionType) ? + collectionType.call(this, relation, attributes) : collectionType; + + collectionType && _.isString(collectionType) && + (collectionType = map2Scope(collectionType, activationContext)); + + // `collectionType` of defined `relation` should be instance of `Backbone.Collection`. + if (collectionType && !collectionType.prototype instanceof BackboneCollection) { + throw new Error('collectionType must inherit from Backbone.Collection'); + } + + // Error checking + if (relation.type === Backbone.Many) { + if ((!relatedModel) && (!collectionType)) + throw new Error('specify either a relatedModel or collectionType'); + } + + return collectionType; + + }, + + // Override trigger to defer events in the object graph. + trigger:function (name) { + // Defer event processing + if (this._deferEvents) { + this._pendingEvents = this._pendingEvents || []; + // Maintain a queue of pending events to trigger after the entire object graph is updated. + this._pendingEvents.push({c:this, a:arguments}); + } else { + ModelProto.trigger.apply(this, arguments); + } + }, + + // The JSON representation of the model. + toJSON:function (options) { + var json = {}, aJson; + json[this.idAttribute] = this.id; + if (!this.visited) { + this.visited = true; + // Get json representation from `BackboneModel.toJSON`. + json = ModelProto.toJSON.apply(this, arguments); + + // Pick up only the keys you want to serialize + if (options && options.serialize_keys) { + json = _.pick(json, options.serialize_keys); + } + // If `this.relations` is defined, iterate through each `relation` + // and added it's json representation to parents' json representation. + if (this.relations) { + _.each(this.relations, function (relation) { + var key = relation.key, + remoteKey = relation.remoteKey, + attr = this.attributes[key], + serialize = !relation.isTransient, + serialize_keys = relation.serialize || [], + _options = _.clone(options); + + // Remove default Backbone serialization for associations. + delete json[key]; + + //Assign to remoteKey if specified. Otherwise use the default key. + //Only for non-transient relationships + if (serialize) { + + // Pass the keys to serialize as options to the toJSON method. + if (serialize_keys.length) { + _options ? + (_options.serialize_keys = serialize_keys) : + (_options = {serialize_keys: serialize_keys}) + } + + aJson = attr && attr.toJSON ? attr.toJSON(_options) : attr; + json[remoteKey || key] = _.isArray(aJson) ? _.compact(aJson) : aJson; + } + + }, this); + } + + delete this.visited; + } + return json; + }, + + // Create a new model with identical attributes to this one. + clone: function (options) { + return new this.constructor(this.toJSON(options)); + }, + + // Call this if you want to set an `AssociatedModel` to a falsy value like undefined/null directly. + // Not calling this will leak memory and have wrong parents. + // See test case "parent relations" + cleanup:function (options) { + options = options || {}; + + _.each(this.relations, function (relation) { + var val = this.attributes[relation.key]; + if(val) { + val._proxyCallback && val.off("all", val._proxyCallback, this); + val.parents = _.difference(val.parents, [this]); + } + }, this); + + (!options.listen) && this.off(); + }, + + // Override destroy to perform house-keeping on `parents` collection + destroy: function(options) { + options = options ? _.clone(options) : {}; + options = _.defaults(options, {remove_references: true, listen: true}); + var model = this; + + if(options.remove_references && options.wait) { + // Proxy success implementation + var success = options.success; + + // Substitute with an implementation which will remove references to `model` + options.success = function (resp) { + if (success) success(model, resp, options); + model.cleanup(options); + } + } + // Call the base implementation + var xhr = ModelProto.destroy.apply(this, [options]); + + if(options.remove_references && !options.wait) { + model.cleanup(options); + } + + return xhr; + }, + + // Navigate the path to the leaf object in the path to query for the attribute value + _getAttr:function (path) { + + var result = this, + cache = this.__attributes__, + //Tokenize the path + attrs = getPathArray(path), + key, + i; + if (_.size(attrs) < 1) return; + for (i = 0; i < attrs.length; i++) { + key = attrs[i]; + if (!result) break; + //Navigate the path to get to the result + result = result instanceof BackboneCollection + ? (isNaN(key) ? undefined : result.at(key)) + : (cache ? + (isValuePresent(result.attributes[key]) ? result.attributes[key] : cache[key]) : + result.attributes[key] + ); + } + return result; + } + }); + + // Tokenize the fully qualified event path + var getPathArray = function (path) { + if (path === '') return ['']; + return _.isString(path) ? (path.match(delimiters)) : path || []; + }; + + // Get the end point of the path. + var getPathEndPoint = function (path) { + + if (!path) return path; + + // event_type: + var tokens = path.split(":"); + + if (tokens.length > 1) { + path = tokens[tokens.length - 1]; + tokens = path.split(pathSeparator); + return tokens.length > 1 ? tokens[tokens.length - 1].split('[')[0] : tokens[0].split('[')[0]; + } else { + //path of 0 depth + return ""; + } + + }; + + var map2Scope = function (path, context) { + var target, + scopes = [context]; + + // Check global scopes after passed-in context + scopes.push.apply(scopes, Backbone.Associations.scopes); + + for (var ctx, i = 0, l = scopes.length; i < l; ++i) { + if (ctx = scopes[i]) { + target = _.reduce(path.split(pathSeparator), function (memo, elem) { + return memo[elem]; + }, ctx); + if (target) break; + } + } + return target; + }; + + + // Infer the relation from the collection's parents and find the appropriate map for the passed in `models` + var map2models = function (parents, target, models) { + var relation, surrogate; + //Iterate over collection's parents + _.find(parents, function (parent) { + //Iterate over relations + relation = _.find(parent.relations, function (rel) { + return parent.get(rel.key) === target; + }, this); + if (relation) { + surrogate = parent;//surrogate for transformation + return true;//break; + } + }, this); + + //If we found a relation and it has a mapping function + if (relation && relation.map) { + return relation.map.call(surrogate, models, target); + } + return models; + }; + + // Utility method to check for null or undefined value + var isValuePresent = function (value) { + return (!_.isUndefined(value)) && (!_.isNull(value)); + }; + + var proxies = {}; + // Proxy Backbone collection methods + _.each(['set', 'remove', 'reset'], function (method) { + proxies[method] = BackboneCollection.prototype[method]; + + CollectionProto[method] = function (models, options) { + //Short-circuit if this collection doesn't hold `AssociatedModels` + if (this.model.prototype instanceof AssociatedModel && this.parents) { + //Find a map function if available and perform a transformation + arguments[0] = map2models(this.parents, this, models); + } + return proxies[method].apply(this, arguments); + } + }); + + // Override trigger to defer events in the object graph. + proxies['trigger'] = CollectionProto['trigger']; + CollectionProto['trigger'] = function (name) { + if (this._deferEvents) { + this._pendingEvents = this._pendingEvents || []; + // Maintain a queue of pending events to trigger after the entire object graph is updated. + this._pendingEvents.push({c:this, a:arguments}); + } else { + proxies['trigger'].apply(this, arguments); + } + }; + + // Attach process pending event functionality on collections as well. Re-use from `AssociatedModel` + CollectionProto._processPendingEvents = AssociatedModel.prototype._processPendingEvents; + CollectionProto.on = AssociatedModel.prototype.on; + CollectionProto.off = AssociatedModel.prototype.off; + + return Backbone; +})); + +},{"backbone":6,"underscore":33}],2:[function(require,module,exports){ +// MarionetteJS (Backbone.Marionette) +// ---------------------------------- +// v2.4.2 +// +// Copyright (c)2015 Derick Bailey, Muted Solutions, LLC. +// Distributed under MIT license +// +// http://marionettejs.com + +(function(root, factory) { + + if (typeof define === 'function' && define.amd) { + define(['backbone', 'underscore', 'backbone.wreqr', 'backbone.babysitter'], function(Backbone, _) { + return (root.Marionette = root.Mn = factory(root, Backbone, _)); + }); + } else if (typeof exports !== 'undefined') { + var Backbone = require('backbone'); + var _ = require('underscore'); + var Wreqr = require('backbone.wreqr'); + var BabySitter = require('backbone.babysitter'); + module.exports = factory(root, Backbone, _); + } else { + root.Marionette = root.Mn = factory(root, root.Backbone, root._); + } + +}(this, function(root, Backbone, _) { + 'use strict'; + + var previousMarionette = root.Marionette; + var previousMn = root.Mn; + + var Marionette = Backbone.Marionette = {}; + + Marionette.VERSION = '2.4.2'; + + Marionette.noConflict = function() { + root.Marionette = previousMarionette; + root.Mn = previousMn; + return this; + }; + + // Get the Deferred creator for later use + Marionette.Deferred = Backbone.$.Deferred; + + Marionette.FEATURES = { + }; + + Marionette.isEnabled = function(name) { + return !!Marionette.FEATURES[name]; + }; + + /* jshint unused: false *//* global console */ + + // Helpers + // ------- + + // Marionette.extend + // ----------------- + + // Borrow the Backbone `extend` method so we can use it as needed + Marionette.extend = Backbone.Model.extend; + + // Marionette.isNodeAttached + // ------------------------- + + // Determine if `el` is a child of the document + Marionette.isNodeAttached = function(el) { + return Backbone.$.contains(document.documentElement, el); + }; + + // Merge `keys` from `options` onto `this` + Marionette.mergeOptions = function(options, keys) { + if (!options) { return; } + _.extend(this, _.pick(options, keys)); + }; + + // Marionette.getOption + // -------------------- + + // Retrieve an object, function or other value from a target + // object or its `options`, with `options` taking precedence. + Marionette.getOption = function(target, optionName) { + if (!target || !optionName) { return; } + if (target.options && (target.options[optionName] !== undefined)) { + return target.options[optionName]; + } else { + return target[optionName]; + } + }; + + // Proxy `Marionette.getOption` + Marionette.proxyGetOption = function(optionName) { + return Marionette.getOption(this, optionName); + }; + + // Similar to `_.result`, this is a simple helper + // If a function is provided we call it with context + // otherwise just return the value. If the value is + // undefined return a default value + Marionette._getValue = function(value, context, params) { + if (_.isFunction(value)) { + value = params ? value.apply(context, params) : value.call(context); + } + return value; + }; + + // Marionette.normalizeMethods + // ---------------------- + + // Pass in a mapping of events => functions or function names + // and return a mapping of events => functions + Marionette.normalizeMethods = function(hash) { + return _.reduce(hash, function(normalizedHash, method, name) { + if (!_.isFunction(method)) { + method = this[method]; + } + if (method) { + normalizedHash[name] = method; + } + return normalizedHash; + }, {}, this); + }; + + // utility method for parsing @ui. syntax strings + // into associated selector + Marionette.normalizeUIString = function(uiString, ui) { + return uiString.replace(/@ui\.[a-zA-Z_$0-9]*/g, function(r) { + return ui[r.slice(4)]; + }); + }; + + // allows for the use of the @ui. syntax within + // a given key for triggers and events + // swaps the @ui with the associated selector. + // Returns a new, non-mutated, parsed events hash. + Marionette.normalizeUIKeys = function(hash, ui) { + return _.reduce(hash, function(memo, val, key) { + var normalizedKey = Marionette.normalizeUIString(key, ui); + memo[normalizedKey] = val; + return memo; + }, {}); + }; + + // allows for the use of the @ui. syntax within + // a given value for regions + // swaps the @ui with the associated selector + Marionette.normalizeUIValues = function(hash, ui, properties) { + _.each(hash, function(val, key) { + if (_.isString(val)) { + hash[key] = Marionette.normalizeUIString(val, ui); + } else if (_.isObject(val) && _.isArray(properties)) { + _.extend(val, Marionette.normalizeUIValues(_.pick(val, properties), ui)); + /* Value is an object, and we got an array of embedded property names to normalize. */ + _.each(properties, function(property) { + var propertyVal = val[property]; + if (_.isString(propertyVal)) { + val[property] = Marionette.normalizeUIString(propertyVal, ui); + } + }); + } + }); + return hash; + }; + + // Mix in methods from Underscore, for iteration, and other + // collection related features. + // Borrowing this code from Backbone.Collection: + // http://backbonejs.org/docs/backbone.html#section-121 + Marionette.actAsCollection = function(object, listProperty) { + var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', + 'select', 'reject', 'every', 'all', 'some', 'any', 'include', + 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', + 'last', 'without', 'isEmpty', 'pluck']; + + _.each(methods, function(method) { + object[method] = function() { + var list = _.values(_.result(this, listProperty)); + var args = [list].concat(_.toArray(arguments)); + return _[method].apply(_, args); + }; + }); + }; + + var deprecate = Marionette.deprecate = function(message, test) { + if (_.isObject(message)) { + message = ( + message.prev + ' is going to be removed in the future. ' + + 'Please use ' + message.next + ' instead.' + + (message.url ? ' See: ' + message.url : '') + ); + } + + if ((test === undefined || !test) && !deprecate._cache[message]) { + deprecate._warn('Deprecation warning: ' + message); + deprecate._cache[message] = true; + } + }; + + deprecate._warn = typeof console !== 'undefined' && (console.warn || console.log) || function() {}; + deprecate._cache = {}; + + /* jshint maxstatements: 14, maxcomplexity: 7 */ + + // Trigger Method + // -------------- + + Marionette._triggerMethod = (function() { + // split the event name on the ":" + var splitter = /(^|:)(\w)/gi; + + // take the event section ("section1:section2:section3") + // and turn it in to uppercase name + function getEventName(match, prefix, eventName) { + return eventName.toUpperCase(); + } + + return function(context, event, args) { + var noEventArg = arguments.length < 3; + if (noEventArg) { + args = event; + event = args[0]; + } + + // get the method name from the event name + var methodName = 'on' + event.replace(splitter, getEventName); + var method = context[methodName]; + var result; + + // call the onMethodName if it exists + if (_.isFunction(method)) { + // pass all args, except the event name + result = method.apply(context, noEventArg ? _.rest(args) : args); + } + + // trigger the event, if a trigger method exists + if (_.isFunction(context.trigger)) { + if (noEventArg + args.length > 1) { + context.trigger.apply(context, noEventArg ? args : [event].concat(_.drop(args, 0))); + } else { + context.trigger(event); + } + } + + return result; + }; + })(); + + // Trigger an event and/or a corresponding method name. Examples: + // + // `this.triggerMethod("foo")` will trigger the "foo" event and + // call the "onFoo" method. + // + // `this.triggerMethod("foo:bar")` will trigger the "foo:bar" event and + // call the "onFooBar" method. + Marionette.triggerMethod = function(event) { + return Marionette._triggerMethod(this, arguments); + }; + + // triggerMethodOn invokes triggerMethod on a specific context + // + // e.g. `Marionette.triggerMethodOn(view, 'show')` + // will trigger a "show" event or invoke onShow the view. + Marionette.triggerMethodOn = function(context) { + var fnc = _.isFunction(context.triggerMethod) ? + context.triggerMethod : + Marionette.triggerMethod; + + return fnc.apply(context, _.rest(arguments)); + }; + + // DOM Refresh + // ----------- + + // Monitor a view's state, and after it has been rendered and shown + // in the DOM, trigger a "dom:refresh" event every time it is + // re-rendered. + + Marionette.MonitorDOMRefresh = function(view) { + + // track when the view has been shown in the DOM, + // using a Marionette.Region (or by other means of triggering "show") + function handleShow() { + view._isShown = true; + triggerDOMRefresh(); + } + + // track when the view has been rendered + function handleRender() { + view._isRendered = true; + triggerDOMRefresh(); + } + + // Trigger the "dom:refresh" event and corresponding "onDomRefresh" method + function triggerDOMRefresh() { + if (view._isShown && view._isRendered && Marionette.isNodeAttached(view.el)) { + if (_.isFunction(view.triggerMethod)) { + view.triggerMethod('dom:refresh'); + } + } + } + + view.on({ + show: handleShow, + render: handleRender + }); + }; + + /* jshint maxparams: 5 */ + + // Bind Entity Events & Unbind Entity Events + // ----------------------------------------- + // + // These methods are used to bind/unbind a backbone "entity" (e.g. collection/model) + // to methods on a target object. + // + // The first parameter, `target`, must have the Backbone.Events module mixed in. + // + // The second parameter is the `entity` (Backbone.Model, Backbone.Collection or + // any object that has Backbone.Events mixed in) to bind the events from. + // + // The third parameter is a hash of { "event:name": "eventHandler" } + // configuration. Multiple handlers can be separated by a space. A + // function can be supplied instead of a string handler name. + + (function(Marionette) { + 'use strict'; + + // Bind the event to handlers specified as a string of + // handler names on the target object + function bindFromStrings(target, entity, evt, methods) { + var methodNames = methods.split(/\s+/); + + _.each(methodNames, function(methodName) { + + var method = target[methodName]; + if (!method) { + throw new Marionette.Error('Method "' + methodName + + '" was configured as an event handler, but does not exist.'); + } + + target.listenTo(entity, evt, method); + }); + } + + // Bind the event to a supplied callback function + function bindToFunction(target, entity, evt, method) { + target.listenTo(entity, evt, method); + } + + // Bind the event to handlers specified as a string of + // handler names on the target object + function unbindFromStrings(target, entity, evt, methods) { + var methodNames = methods.split(/\s+/); + + _.each(methodNames, function(methodName) { + var method = target[methodName]; + target.stopListening(entity, evt, method); + }); + } + + // Bind the event to a supplied callback function + function unbindToFunction(target, entity, evt, method) { + target.stopListening(entity, evt, method); + } + + // generic looping function + function iterateEvents(target, entity, bindings, functionCallback, stringCallback) { + if (!entity || !bindings) { return; } + + // type-check bindings + if (!_.isObject(bindings)) { + throw new Marionette.Error({ + message: 'Bindings must be an object or function.', + url: 'marionette.functions.html#marionettebindentityevents' + }); + } + + // allow the bindings to be a function + bindings = Marionette._getValue(bindings, target); + + // iterate the bindings and bind them + _.each(bindings, function(methods, evt) { + + // allow for a function as the handler, + // or a list of event names as a string + if (_.isFunction(methods)) { + functionCallback(target, entity, evt, methods); + } else { + stringCallback(target, entity, evt, methods); + } + + }); + } + + // Export Public API + Marionette.bindEntityEvents = function(target, entity, bindings) { + iterateEvents(target, entity, bindings, bindToFunction, bindFromStrings); + }; + + Marionette.unbindEntityEvents = function(target, entity, bindings) { + iterateEvents(target, entity, bindings, unbindToFunction, unbindFromStrings); + }; + + // Proxy `bindEntityEvents` + Marionette.proxyBindEntityEvents = function(entity, bindings) { + return Marionette.bindEntityEvents(this, entity, bindings); + }; + + // Proxy `unbindEntityEvents` + Marionette.proxyUnbindEntityEvents = function(entity, bindings) { + return Marionette.unbindEntityEvents(this, entity, bindings); + }; + })(Marionette); + + + // Error + // ----- + + var errorProps = ['description', 'fileName', 'lineNumber', 'name', 'message', 'number']; + + Marionette.Error = Marionette.extend.call(Error, { + urlRoot: 'http://marionettejs.com/docs/v' + Marionette.VERSION + '/', + + constructor: function(message, options) { + if (_.isObject(message)) { + options = message; + message = options.message; + } else if (!options) { + options = {}; + } + + var error = Error.call(this, message); + _.extend(this, _.pick(error, errorProps), _.pick(options, errorProps)); + + this.captureStackTrace(); + + if (options.url) { + this.url = this.urlRoot + options.url; + } + }, + + captureStackTrace: function() { + if (Error.captureStackTrace) { + Error.captureStackTrace(this, Marionette.Error); + } + }, + + toString: function() { + return this.name + ': ' + this.message + (this.url ? ' See: ' + this.url : ''); + } + }); + + Marionette.Error.extend = Marionette.extend; + + // Callbacks + // --------- + + // A simple way of managing a collection of callbacks + // and executing them at a later point in time, using jQuery's + // `Deferred` object. + Marionette.Callbacks = function() { + this._deferred = Marionette.Deferred(); + this._callbacks = []; + }; + + _.extend(Marionette.Callbacks.prototype, { + + // Add a callback to be executed. Callbacks added here are + // guaranteed to execute, even if they are added after the + // `run` method is called. + add: function(callback, contextOverride) { + var promise = _.result(this._deferred, 'promise'); + + this._callbacks.push({cb: callback, ctx: contextOverride}); + + promise.then(function(args) { + if (contextOverride) { args.context = contextOverride; } + callback.call(args.context, args.options); + }); + }, + + // Run all registered callbacks with the context specified. + // Additional callbacks can be added after this has been run + // and they will still be executed. + run: function(options, context) { + this._deferred.resolve({ + options: options, + context: context + }); + }, + + // Resets the list of callbacks to be run, allowing the same list + // to be run multiple times - whenever the `run` method is called. + reset: function() { + var callbacks = this._callbacks; + this._deferred = Marionette.Deferred(); + this._callbacks = []; + + _.each(callbacks, function(cb) { + this.add(cb.cb, cb.ctx); + }, this); + } + }); + + // Controller + // ---------- + + // A multi-purpose object to use as a controller for + // modules and routers, and as a mediator for workflow + // and coordination of other objects, views, and more. + Marionette.Controller = function(options) { + this.options = options || {}; + + if (_.isFunction(this.initialize)) { + this.initialize(this.options); + } + }; + + Marionette.Controller.extend = Marionette.extend; + + // Controller Methods + // -------------- + + // Ensure it can trigger events with Backbone.Events + _.extend(Marionette.Controller.prototype, Backbone.Events, { + destroy: function() { + Marionette._triggerMethod(this, 'before:destroy', arguments); + Marionette._triggerMethod(this, 'destroy', arguments); + + this.stopListening(); + this.off(); + return this; + }, + + // import the `triggerMethod` to trigger events with corresponding + // methods if the method exists + triggerMethod: Marionette.triggerMethod, + + // A handy way to merge options onto the instance + mergeOptions: Marionette.mergeOptions, + + // Proxy `getOption` to enable getting options from this or this.options by name. + getOption: Marionette.proxyGetOption + + }); + + // Object + // ------ + + // A Base Class that other Classes should descend from. + // Object borrows many conventions and utilities from Backbone. + Marionette.Object = function(options) { + this.options = _.extend({}, _.result(this, 'options'), options); + + this.initialize.apply(this, arguments); + }; + + Marionette.Object.extend = Marionette.extend; + + // Object Methods + // -------------- + + // Ensure it can trigger events with Backbone.Events + _.extend(Marionette.Object.prototype, Backbone.Events, { + + //this is a noop method intended to be overridden by classes that extend from this base + initialize: function() {}, + + destroy: function() { + this.triggerMethod('before:destroy'); + this.triggerMethod('destroy'); + this.stopListening(); + + return this; + }, + + // Import the `triggerMethod` to trigger events with corresponding + // methods if the method exists + triggerMethod: Marionette.triggerMethod, + + // A handy way to merge options onto the instance + mergeOptions: Marionette.mergeOptions, + + // Proxy `getOption` to enable getting options from this or this.options by name. + getOption: Marionette.proxyGetOption, + + // Proxy `bindEntityEvents` to enable binding view's events from another entity. + bindEntityEvents: Marionette.proxyBindEntityEvents, + + // Proxy `unbindEntityEvents` to enable unbinding view's events from another entity. + unbindEntityEvents: Marionette.proxyUnbindEntityEvents + }); + + /* jshint maxcomplexity: 16, maxstatements: 45, maxlen: 120 */ + + // Region + // ------ + + // Manage the visual regions of your composite application. See + // http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/ + + Marionette.Region = Marionette.Object.extend({ + constructor: function(options) { + + // set options temporarily so that we can get `el`. + // options will be overriden by Object.constructor + this.options = options || {}; + this.el = this.getOption('el'); + + // Handle when this.el is passed in as a $ wrapped element. + this.el = this.el instanceof Backbone.$ ? this.el[0] : this.el; + + if (!this.el) { + throw new Marionette.Error({ + name: 'NoElError', + message: 'An "el" must be specified for a region.' + }); + } + + this.$el = this.getEl(this.el); + Marionette.Object.call(this, options); + }, + + // Displays a backbone view instance inside of the region. + // Handles calling the `render` method for you. Reads content + // directly from the `el` attribute. Also calls an optional + // `onShow` and `onDestroy` method on your view, just after showing + // or just before destroying the view, respectively. + // The `preventDestroy` option can be used to prevent a view from + // the old view being destroyed on show. + // The `forceShow` option can be used to force a view to be + // re-rendered if it's already shown in the region. + show: function(view, options) { + if (!this._ensureElement()) { + return; + } + + this._ensureViewIsIntact(view); + + var showOptions = options || {}; + var isDifferentView = view !== this.currentView; + var preventDestroy = !!showOptions.preventDestroy; + var forceShow = !!showOptions.forceShow; + + // We are only changing the view if there is a current view to change to begin with + var isChangingView = !!this.currentView; + + // Only destroy the current view if we don't want to `preventDestroy` and if + // the view given in the first argument is different than `currentView` + var _shouldDestroyView = isDifferentView && !preventDestroy; + + // Only show the view given in the first argument if it is different than + // the current view or if we want to re-show the view. Note that if + // `_shouldDestroyView` is true, then `_shouldShowView` is also necessarily true. + var _shouldShowView = isDifferentView || forceShow; + + if (isChangingView) { + this.triggerMethod('before:swapOut', this.currentView, this, options); + } + + if (this.currentView) { + delete this.currentView._parent; + } + + if (_shouldDestroyView) { + this.empty(); + + // A `destroy` event is attached to the clean up manually removed views. + // We need to detach this event when a new view is going to be shown as it + // is no longer relevant. + } else if (isChangingView && _shouldShowView) { + this.currentView.off('destroy', this.empty, this); + } + + if (_shouldShowView) { + + // We need to listen for if a view is destroyed + // in a way other than through the region. + // If this happens we need to remove the reference + // to the currentView since once a view has been destroyed + // we can not reuse it. + view.once('destroy', this.empty, this); + view.render(); + + view._parent = this; + + if (isChangingView) { + this.triggerMethod('before:swap', view, this, options); + } + + this.triggerMethod('before:show', view, this, options); + Marionette.triggerMethodOn(view, 'before:show', view, this, options); + + if (isChangingView) { + this.triggerMethod('swapOut', this.currentView, this, options); + } + + // An array of views that we're about to display + var attachedRegion = Marionette.isNodeAttached(this.el); + + // The views that we're about to attach to the document + // It's important that we prevent _getNestedViews from being executed unnecessarily + // as it's a potentially-slow method + var displayedViews = []; + + var attachOptions = _.extend({ + triggerBeforeAttach: this.triggerBeforeAttach, + triggerAttach: this.triggerAttach + }, showOptions); + + if (attachedRegion && attachOptions.triggerBeforeAttach) { + displayedViews = this._displayedViews(view); + this._triggerAttach(displayedViews, 'before:'); + } + + this.attachHtml(view); + this.currentView = view; + + if (attachedRegion && attachOptions.triggerAttach) { + displayedViews = this._displayedViews(view); + this._triggerAttach(displayedViews); + } + + if (isChangingView) { + this.triggerMethod('swap', view, this, options); + } + + this.triggerMethod('show', view, this, options); + Marionette.triggerMethodOn(view, 'show', view, this, options); + + return this; + } + + return this; + }, + + triggerBeforeAttach: true, + triggerAttach: true, + + _triggerAttach: function(views, prefix) { + var eventName = (prefix || '') + 'attach'; + _.each(views, function(view) { + Marionette.triggerMethodOn(view, eventName, view, this); + }, this); + }, + + _displayedViews: function(view) { + return _.union([view], _.result(view, '_getNestedViews') || []); + }, + + _ensureElement: function() { + if (!_.isObject(this.el)) { + this.$el = this.getEl(this.el); + this.el = this.$el[0]; + } + + if (!this.$el || this.$el.length === 0) { + if (this.getOption('allowMissingEl')) { + return false; + } else { + throw new Marionette.Error('An "el" ' + this.$el.selector + ' must exist in DOM'); + } + } + return true; + }, + + _ensureViewIsIntact: function(view) { + if (!view) { + throw new Marionette.Error({ + name: 'ViewNotValid', + message: 'The view passed is undefined and therefore invalid. You must pass a view instance to show.' + }); + } + + if (view.isDestroyed) { + throw new Marionette.Error({ + name: 'ViewDestroyedError', + message: 'View (cid: "' + view.cid + '") has already been destroyed and cannot be used.' + }); + } + }, + + // Override this method to change how the region finds the DOM + // element that it manages. Return a jQuery selector object scoped + // to a provided parent el or the document if none exists. + getEl: function(el) { + return Backbone.$(el, Marionette._getValue(this.options.parentEl, this)); + }, + + // Override this method to change how the new view is + // appended to the `$el` that the region is managing + attachHtml: function(view) { + this.$el.contents().detach(); + + this.el.appendChild(view.el); + }, + + // Destroy the current view, if there is one. If there is no + // current view, it does nothing and returns immediately. + empty: function(options) { + var view = this.currentView; + + var preventDestroy = Marionette._getValue(options, 'preventDestroy', this); + // If there is no view in the region + // we should not remove anything + if (!view) { return; } + + view.off('destroy', this.empty, this); + this.triggerMethod('before:empty', view); + if (!preventDestroy) { + this._destroyView(); + } + this.triggerMethod('empty', view); + + // Remove region pointer to the currentView + delete this.currentView; + + if (preventDestroy) { + this.$el.contents().detach(); + } + + return this; + }, + + // call 'destroy' or 'remove', depending on which is found + // on the view (if showing a raw Backbone view or a Marionette View) + _destroyView: function() { + var view = this.currentView; + + if (view.destroy && !view.isDestroyed) { + view.destroy(); + } else if (view.remove) { + view.remove(); + + // appending isDestroyed to raw Backbone View allows regions + // to throw a ViewDestroyedError for this view + view.isDestroyed = true; + } + }, + + // Attach an existing view to the region. This + // will not call `render` or `onShow` for the new view, + // and will not replace the current HTML for the `el` + // of the region. + attachView: function(view) { + this.currentView = view; + return this; + }, + + // Checks whether a view is currently present within + // the region. Returns `true` if there is and `false` if + // no view is present. + hasView: function() { + return !!this.currentView; + }, + + // Reset the region by destroying any existing view and + // clearing out the cached `$el`. The next time a view + // is shown via this region, the region will re-query the + // DOM for the region's `el`. + reset: function() { + this.empty(); + + if (this.$el) { + this.el = this.$el.selector; + } + + delete this.$el; + return this; + } + + }, + + // Static Methods + { + + // Build an instance of a region by passing in a configuration object + // and a default region class to use if none is specified in the config. + // + // The config object should either be a string as a jQuery DOM selector, + // a Region class directly, or an object literal that specifies a selector, + // a custom regionClass, and any options to be supplied to the region: + // + // ```js + // { + // selector: "#foo", + // regionClass: MyCustomRegion, + // allowMissingEl: false + // } + // ``` + // + buildRegion: function(regionConfig, DefaultRegionClass) { + if (_.isString(regionConfig)) { + return this._buildRegionFromSelector(regionConfig, DefaultRegionClass); + } + + if (regionConfig.selector || regionConfig.el || regionConfig.regionClass) { + return this._buildRegionFromObject(regionConfig, DefaultRegionClass); + } + + if (_.isFunction(regionConfig)) { + return this._buildRegionFromRegionClass(regionConfig); + } + + throw new Marionette.Error({ + message: 'Improper region configuration type.', + url: 'marionette.region.html#region-configuration-types' + }); + }, + + // Build the region from a string selector like '#foo-region' + _buildRegionFromSelector: function(selector, DefaultRegionClass) { + return new DefaultRegionClass({el: selector}); + }, + + // Build the region from a configuration object + // ```js + // { selector: '#foo', regionClass: FooRegion, allowMissingEl: false } + // ``` + _buildRegionFromObject: function(regionConfig, DefaultRegionClass) { + var RegionClass = regionConfig.regionClass || DefaultRegionClass; + var options = _.omit(regionConfig, 'selector', 'regionClass'); + + if (regionConfig.selector && !options.el) { + options.el = regionConfig.selector; + } + + return new RegionClass(options); + }, + + // Build the region directly from a given `RegionClass` + _buildRegionFromRegionClass: function(RegionClass) { + return new RegionClass(); + } + }); + + // Region Manager + // -------------- + + // Manage one or more related `Marionette.Region` objects. + Marionette.RegionManager = Marionette.Controller.extend({ + constructor: function(options) { + this._regions = {}; + this.length = 0; + + Marionette.Controller.call(this, options); + + this.addRegions(this.getOption('regions')); + }, + + // Add multiple regions using an object literal or a + // function that returns an object literal, where + // each key becomes the region name, and each value is + // the region definition. + addRegions: function(regionDefinitions, defaults) { + regionDefinitions = Marionette._getValue(regionDefinitions, this, arguments); + + return _.reduce(regionDefinitions, function(regions, definition, name) { + if (_.isString(definition)) { + definition = {selector: definition}; + } + if (definition.selector) { + definition = _.defaults({}, definition, defaults); + } + + regions[name] = this.addRegion(name, definition); + return regions; + }, {}, this); + }, + + // Add an individual region to the region manager, + // and return the region instance + addRegion: function(name, definition) { + var region; + + if (definition instanceof Marionette.Region) { + region = definition; + } else { + region = Marionette.Region.buildRegion(definition, Marionette.Region); + } + + this.triggerMethod('before:add:region', name, region); + + region._parent = this; + this._store(name, region); + + this.triggerMethod('add:region', name, region); + return region; + }, + + // Get a region by name + get: function(name) { + return this._regions[name]; + }, + + // Gets all the regions contained within + // the `regionManager` instance. + getRegions: function() { + return _.clone(this._regions); + }, + + // Remove a region by name + removeRegion: function(name) { + var region = this._regions[name]; + this._remove(name, region); + + return region; + }, + + // Empty all regions in the region manager, and + // remove them + removeRegions: function() { + var regions = this.getRegions(); + _.each(this._regions, function(region, name) { + this._remove(name, region); + }, this); + + return regions; + }, + + // Empty all regions in the region manager, but + // leave them attached + emptyRegions: function() { + var regions = this.getRegions(); + _.invoke(regions, 'empty'); + return regions; + }, + + // Destroy all regions and shut down the region + // manager entirely + destroy: function() { + this.removeRegions(); + return Marionette.Controller.prototype.destroy.apply(this, arguments); + }, + + // internal method to store regions + _store: function(name, region) { + if (!this._regions[name]) { + this.length++; + } + + this._regions[name] = region; + }, + + // internal method to remove a region + _remove: function(name, region) { + this.triggerMethod('before:remove:region', name, region); + region.empty(); + region.stopListening(); + + delete region._parent; + delete this._regions[name]; + this.length--; + this.triggerMethod('remove:region', name, region); + } + }); + + Marionette.actAsCollection(Marionette.RegionManager.prototype, '_regions'); + + + // Template Cache + // -------------- + + // Manage templates stored in `