Files
slide_in_demo/lib/backbone.marionette.js

3331 lines
96 KiB
JavaScript

// MarionetteJS (Backbone.Marionette)
// ----------------------------------
// v3.1.0
//
// Copyright (c)2016 Derick Bailey, Muted Solutions, LLC.
// Distributed under MIT license
//
// http://marionettejs.com
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('backbone'), require('underscore'), require('backbone.radio')) :
typeof define === 'function' && define.amd ? define(['backbone', 'underscore', 'backbone.radio'], factory) :
(global.Marionette = global['Mn'] = factory(global.Backbone,global._,global.Backbone.Radio));
}(this, function (Backbone,_,Radio) { 'use strict';
Backbone = 'default' in Backbone ? Backbone['default'] : Backbone;
_ = 'default' in _ ? _['default'] : _;
Radio = 'default' in Radio ? Radio['default'] : Radio;
var version = "3.1.0";
//Internal utility for creating context style global utils
var proxy = function proxy(method) {
return function (context) {
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
return method.apply(context, args);
};
};
// Borrow the Backbone `extend` method so we can use it as needed
var extend = Backbone.Model.extend;
var deprecate = function deprecate(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 (!Marionette.DEV_MODE) {
return;
}
if ((test === undefined || !test) && !deprecate._cache[message]) {
deprecate._warn('Deprecation warning: ' + message);
deprecate._cache[message] = true;
}
};
deprecate._console = typeof console !== 'undefined' ? console : {};
deprecate._warn = function () {
var warn = deprecate._console.warn || deprecate._console.log || _.noop;
return warn.apply(deprecate._console, arguments);
};
deprecate._cache = {};
// Determine if `el` is a child of the document
var isNodeAttached = function isNodeAttached(el) {
return Backbone.$.contains(document.documentElement, el);
};
// Merge `keys` from `options` onto `this`
var mergeOptions = function mergeOptions(options, keys) {
var _this = this;
if (!options) {
return;
}
_.each(keys, function (key) {
var option = options[key];
if (option !== undefined) {
_this[key] = option;
}
});
};
// Marionette.getOption
// --------------------
// Retrieve an object, function or other value from the
// object or its `options`, with `options` taking precedence.
var getOption = function getOption(optionName) {
if (!optionName) {
return;
}
if (this.options && this.options[optionName] !== undefined) {
return this.options[optionName];
} else {
return this[optionName];
}
};
// Marionette.normalizeMethods
// ----------------------
// Pass in a mapping of events => functions or function names
// and return a mapping of events => functions
var normalizeMethods = function normalizeMethods(hash) {
var _this = this;
return _.reduce(hash, function (normalizedHash, method, name) {
if (!_.isFunction(method)) {
method = _this[method];
}
if (method) {
normalizedHash[name] = method;
}
return normalizedHash;
}, {});
};
// split the event name on the ":"
var splitter = /(^|:)(\w)/gi;
// take the event section ("section1:section2:section3")
// and turn it in to uppercase name onSection1Section2Section3
function getEventName(match, prefix, eventName) {
return eventName.toUpperCase();
}
var getOnMethodName = _.memoize(function (event) {
return 'on' + event.replace(splitter, getEventName);
});
// 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.
function triggerMethod(event) {
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
// get the method name from the event name
var methodName = getOnMethodName(event);
var method = getOption.call(this, methodName);
var result = void 0;
// call the onMethodName if it exists
if (_.isFunction(method)) {
// pass all args, except the event name
result = method.apply(this, args);
}
// trigger the event
this.trigger.apply(this, arguments);
return result;
}
// triggerMethodOn invokes triggerMethod on a specific context
//
// e.g. `Marionette.triggerMethodOn(view, 'show')`
// will trigger a "show" event or invoke onShow the view.
function triggerMethodOn(context) {
for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
if (_.isFunction(context.triggerMethod)) {
return context.triggerMethod.apply(context, args);
}
return triggerMethod.apply(context, args);
}
// Trigger method on children unless a pure Backbone.View
function triggerMethodChildren(view, event, shouldTrigger) {
if (!view._getImmediateChildren) {
return;
}
_.each(view._getImmediateChildren(), function (child) {
if (!shouldTrigger(child)) {
return;
}
triggerMethodOn(child, event, child);
});
}
function shouldTriggerAttach(view) {
return !view._isAttached;
}
function shouldAttach(view) {
if (!shouldTriggerAttach(view)) {
return false;
}
view._isAttached = true;
return true;
}
function shouldTriggerDetach(view) {
return view._isAttached;
}
function shouldDetach(view) {
if (!shouldTriggerDetach(view)) {
return false;
}
view._isAttached = false;
return true;
}
function triggerDOMRefresh(view) {
if (view._isAttached && view._isRendered) {
triggerMethodOn(view, 'dom:refresh', view);
}
}
function handleBeforeAttach() {
triggerMethodChildren(this, 'before:attach', shouldTriggerAttach);
}
function handleAttach() {
triggerMethodChildren(this, 'attach', shouldAttach);
triggerDOMRefresh(this);
}
function handleBeforeDetach() {
triggerMethodChildren(this, 'before:detach', shouldTriggerDetach);
}
function handleDetach() {
triggerMethodChildren(this, 'detach', shouldDetach);
}
function handleRender() {
triggerDOMRefresh(this);
}
// Monitor a view's state, propagating attach/detach events to children and firing dom:refresh
// whenever a rendered view is attached or an attached view is rendered.
function monitorViewEvents(view) {
if (view._areViewEventsMonitored) {
return;
}
view._areViewEventsMonitored = true;
view.on({
'before:attach': handleBeforeAttach,
'attach': handleAttach,
'before:detach': handleBeforeDetach,
'detach': handleDetach,
'render': handleRender
});
}
var errorProps = ['description', 'fileName', 'lineNumber', 'name', 'message', 'number'];
var MarionetteError = extend.call(Error, {
urlRoot: 'http://marionettejs.com/docs/v' + version + '/',
constructor: function constructor(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 captureStackTrace() {
if (Error.captureStackTrace) {
Error.captureStackTrace(this, MarionetteError);
}
},
toString: function toString() {
return this.name + ': ' + this.message + (this.url ? ' See: ' + this.url : '');
}
});
MarionetteError.extend = extend;
// Bind/unbind the event to handlers specified as a string of
// handler names on the target object
function bindFromStrings(target, entity, evt, methods, actionName) {
var methodNames = methods.split(/\s+/);
_.each(methodNames, function (methodName) {
var method = target[methodName];
if (!method) {
throw new MarionetteError('Method "' + methodName + '" was configured as an event handler, but does not exist.');
}
target[actionName](entity, evt, method);
});
}
// generic looping function
function iterateEvents(target, entity, bindings, actionName) {
if (!entity || !bindings) {
return;
}
// type-check bindings
if (!_.isObject(bindings)) {
throw new MarionetteError({
message: 'Bindings must be an object.',
url: 'marionette.functions.html#marionettebindevents'
});
}
// iterate the bindings and bind/unbind them
_.each(bindings, function (method, evt) {
// allow for a list of method names as a string
if (_.isString(method)) {
bindFromStrings(target, entity, evt, method, actionName);
return;
}
target[actionName](entity, evt, method);
});
}
function bindEvents(entity, bindings) {
iterateEvents(this, entity, bindings, 'listenTo');
return this;
}
function unbindEvents(entity, bindings) {
iterateEvents(this, entity, bindings, 'stopListening');
return this;
}
function iterateReplies(target, channel, bindings, actionName) {
if (!channel || !bindings) {
return;
}
// type-check bindings
if (!_.isObject(bindings)) {
throw new MarionetteError({
message: 'Bindings must be an object.',
url: 'marionette.functions.html#marionettebindrequests'
});
}
var normalizedRadioRequests = normalizeMethods.call(target, bindings);
channel[actionName](normalizedRadioRequests, target);
}
function bindRequests(channel, bindings) {
iterateReplies(this, channel, bindings, 'reply');
return this;
}
function unbindRequests(channel, bindings) {
iterateReplies(this, channel, bindings, 'stopReplying');
return this;
}
// Internal utility for setting options consistently across Mn
var setOptions = function setOptions() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
this.options = _.extend.apply(_, [{}, _.result(this, 'options')].concat(args));
};
var CommonMixin = {
// Imports the "normalizeMethods" to transform hashes of
// events=>function references/names to a hash of events=>function references
normalizeMethods: normalizeMethods,
_setOptions: setOptions,
// A handy way to merge passed-in options onto the instance
mergeOptions: mergeOptions,
// Enable getting options from this or this.options by name.
getOption: getOption,
// Enable binding view's events from another entity.
bindEvents: bindEvents,
// Enable unbinding view's events from another entity.
unbindEvents: unbindEvents
};
// MixinOptions
// - channelName
// - radioEvents
// - radioRequests
var RadioMixin = {
_initRadio: function _initRadio() {
var channelName = _.result(this, 'channelName');
if (!channelName) {
return;
}
/* istanbul ignore next */
if (!Radio) {
throw new MarionetteError({
name: 'BackboneRadioMissing',
message: 'The dependency "backbone.radio" is missing.'
});
}
var channel = this._channel = Radio.channel(channelName);
var radioEvents = _.result(this, 'radioEvents');
this.bindEvents(channel, radioEvents);
var radioRequests = _.result(this, 'radioRequests');
this.bindRequests(channel, radioRequests);
this.on('destroy', this._destroyRadio);
},
_destroyRadio: function _destroyRadio() {
this._channel.stopReplying(null, null, this);
},
getChannel: function getChannel() {
return this._channel;
},
// Proxy `bindEvents`
bindEvents: bindEvents,
// Proxy `unbindEvents`
unbindEvents: unbindEvents,
// Proxy `bindRequests`
bindRequests: bindRequests,
// Proxy `unbindRequests`
unbindRequests: unbindRequests
};
var ClassOptions = ['channelName', 'radioEvents', 'radioRequests'];
// A Base Class that other Classes should descend from.
// Object borrows many conventions and utilities from Backbone.
var MarionetteObject = function MarionetteObject(options) {
this._setOptions(options);
this.mergeOptions(options, ClassOptions);
this.cid = _.uniqueId(this.cidPrefix);
this._initRadio();
this.initialize.apply(this, arguments);
};
MarionetteObject.extend = extend;
// Object Methods
// --------------
// Ensure it can trigger events with Backbone.Events
_.extend(MarionetteObject.prototype, Backbone.Events, CommonMixin, RadioMixin, {
cidPrefix: 'mno',
// for parity with Marionette.AbstractView lifecyle
_isDestroyed: false,
isDestroyed: function isDestroyed() {
return this._isDestroyed;
},
//this is a noop method intended to be overridden by classes that extend from this base
initialize: function initialize() {},
destroy: function destroy() {
if (this._isDestroyed) {
return this;
}
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
this.triggerMethod.apply(this, ['before:destroy', this].concat(args));
this._isDestroyed = true;
this.triggerMethod.apply(this, ['destroy', this].concat(args));
this.stopListening();
return this;
},
triggerMethod: triggerMethod
});
// Manage templates stored in `<script>` blocks,
// caching them for faster access.
var TemplateCache = function TemplateCache(templateId) {
this.templateId = templateId;
};
// TemplateCache object-level methods. Manage the template
// caches from these method calls instead of creating
// your own TemplateCache instances
_.extend(TemplateCache, {
templateCaches: {},
// Get the specified template by id. Either
// retrieves the cached version, or loads it
// from the DOM.
get: function get(templateId, options) {
var cachedTemplate = this.templateCaches[templateId];
if (!cachedTemplate) {
cachedTemplate = new TemplateCache(templateId);
this.templateCaches[templateId] = cachedTemplate;
}
return cachedTemplate.load(options);
},
// 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 clear() {
var i = void 0;
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
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(TemplateCache.prototype, {
// Internal method to load the template
load: function load(options) {
// 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, options);
this.compiledTemplate = this.compileTemplate(template, options);
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 loadTemplate(templateId, options) {
var $template = Backbone.$(templateId);
if (!$template.length) {
throw new MarionetteError({
name: 'NoTemplateError',
message: 'Could not find template: "' + templateId + '"'
});
}
return $template.html();
},
// 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 compileTemplate(rawTemplate, options) {
return _.template(rawTemplate, options);
}
});
var _invoke = _.invokeMap || _.invoke;
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
// MixinOptions
// - behaviors
// Takes care of getting the behavior class
// given options and a key.
// If a user passes in options.behaviorClass
// default to using that.
// If a user passes in a Behavior Class directly, use that
// Otherwise delegate the lookup to the users `behaviorsLookup` implementation.
function getBehaviorClass(options, key) {
if (options.behaviorClass) {
return options.behaviorClass;
//treat functions as a Behavior constructor
} else if (_.isFunction(options)) {
return options;
}
// behaviorsLookup can be either a flat object or a method
if (_.isFunction(Marionette.Behaviors.behaviorsLookup)) {
return Marionette.Behaviors.behaviorsLookup(options, key)[key];
}
return Marionette.Behaviors.behaviorsLookup[key];
}
// Iterate over the behaviors object, for each behavior
// instantiate it and get its grouped behaviors.
// This accepts a list of behaviors in either an object or array form
function parseBehaviors(view, behaviors) {
return _.chain(behaviors).map(function (options, key) {
var BehaviorClass = getBehaviorClass(options, key);
//if we're passed a class directly instead of an object
var _options = options === BehaviorClass ? {} : options;
var behavior = new BehaviorClass(_options, view);
var nestedBehaviors = parseBehaviors(view, _.result(behavior, 'behaviors'));
return [behavior].concat(nestedBehaviors);
}).flatten().value();
}
var BehaviorsMixin = {
_initBehaviors: function _initBehaviors() {
var behaviors = _.result(this, 'behaviors');
// Behaviors defined on a view can be a flat object literal
// or it can be a function that returns an object.
this._behaviors = _.isObject(behaviors) ? parseBehaviors(this, behaviors) : {};
},
_getBehaviorTriggers: function _getBehaviorTriggers() {
var triggers = _invoke(this._behaviors, 'getTriggers');
return _.extend.apply(_, [{}].concat(_toConsumableArray(triggers)));
},
_getBehaviorEvents: function _getBehaviorEvents() {
var events = _invoke(this._behaviors, 'getEvents');
return _.extend.apply(_, [{}].concat(_toConsumableArray(events)));
},
// proxy behavior $el to the view's $el.
_proxyBehaviorViewProperties: function _proxyBehaviorViewProperties() {
_invoke(this._behaviors, 'proxyViewProperties');
},
// delegate modelEvents and collectionEvents
_delegateBehaviorEntityEvents: function _delegateBehaviorEntityEvents() {
_invoke(this._behaviors, 'delegateEntityEvents');
},
// undelegate modelEvents and collectionEvents
_undelegateBehaviorEntityEvents: function _undelegateBehaviorEntityEvents() {
_invoke(this._behaviors, 'undelegateEntityEvents');
},
_destroyBehaviors: function _destroyBehaviors(args) {
// Call destroy on each behavior after
// destroying the view.
// This unbinds event listeners
// that behaviors have registered for.
_invoke.apply(undefined, [this._behaviors, 'destroy'].concat(_toConsumableArray(args)));
},
_bindBehaviorUIElements: function _bindBehaviorUIElements() {
_invoke(this._behaviors, 'bindUIElements');
},
_unbindBehaviorUIElements: function _unbindBehaviorUIElements() {
_invoke(this._behaviors, 'unbindUIElements');
},
_triggerEventOnBehaviors: function _triggerEventOnBehaviors() {
var behaviors = this._behaviors;
// Use good ol' for as this is a very hot function
for (var i = 0, length = behaviors && behaviors.length; i < length; i++) {
triggerMethod.apply(behaviors[i], arguments);
}
}
};
// MixinOptions
// - collectionEvents
// - modelEvents
var DelegateEntityEventsMixin = {
// Handle `modelEvents`, and `collectionEvents` configuration
_delegateEntityEvents: function _delegateEntityEvents(model, collection) {
this._undelegateEntityEvents(model, collection);
var modelEvents = _.result(this, 'modelEvents');
bindEvents.call(this, model, modelEvents);
var collectionEvents = _.result(this, 'collectionEvents');
bindEvents.call(this, collection, collectionEvents);
},
_undelegateEntityEvents: function _undelegateEntityEvents(model, collection) {
var modelEvents = _.result(this, 'modelEvents');
unbindEvents.call(this, model, modelEvents);
var collectionEvents = _.result(this, 'collectionEvents');
unbindEvents.call(this, collection, collectionEvents);
}
};
// Borrow event splitter from Backbone
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
function uniqueName(eventName, selector) {
return [eventName + _.uniqueId('.evt'), selector].join(' ');
}
// Set event name to be namespaced using a unique index
// to generate a non colliding event namespace
// http://api.jquery.com/event.namespace/
var getUniqueEventName = function getUniqueEventName(eventName) {
var match = eventName.match(delegateEventSplitter);
return uniqueName(match[1], match[2]);
};
// Internal method to create an event handler for a given `triggerDef` like
// 'click:foo'
function buildViewTrigger(view, triggerDef) {
if (_.isString(triggerDef)) {
triggerDef = { event: triggerDef };
}
var eventName = triggerDef.event;
var shouldPreventDefault = triggerDef.preventDefault !== false;
var shouldStopPropagation = triggerDef.stopPropagation !== false;
return function (e) {
if (shouldPreventDefault) {
e.preventDefault();
}
if (shouldStopPropagation) {
e.stopPropagation();
}
view.triggerMethod(eventName, view);
};
}
var TriggersMixin = {
// Configure `triggers` to forward DOM events to view
// events. `triggers: {"click .foo": "do:foo"}`
_getViewTriggers: function _getViewTriggers(view, triggers) {
// Configure the triggers, prevent default
// action and stop propagation of DOM events
return _.reduce(triggers, function (events, value, key) {
key = getUniqueEventName(key);
events[key] = buildViewTrigger(view, value);
return events;
}, {});
}
};
// 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.
var _normalizeUIKeys = function _normalizeUIKeys(hash, ui) {
return _.reduce(hash, function (memo, val, key) {
var normalizedKey = _normalizeUIString(key, ui);
memo[normalizedKey] = val;
return memo;
}, {});
};
// utility method for parsing @ui. syntax strings
// into associated selector
var _normalizeUIString = function _normalizeUIString(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 value for regions
// swaps the @ui with the associated selector
var _normalizeUIValues = function _normalizeUIValues(hash, ui, properties) {
_.each(hash, function (val, key) {
if (_.isString(val)) {
hash[key] = _normalizeUIString(val, ui);
} else if (_.isObject(val) && _.isArray(properties)) {
_.extend(val, _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] = _normalizeUIString(propertyVal, ui);
}
});
}
});
return hash;
};
var UIMixin = {
// normalize the keys of passed hash with the views `ui` selectors.
// `{"@ui.foo": "bar"}`
normalizeUIKeys: function normalizeUIKeys(hash) {
var uiBindings = this._getUIBindings();
return _normalizeUIKeys(hash, uiBindings);
},
// normalize the passed string with the views `ui` selectors.
// `"@ui.bar"`
normalizeUIString: function normalizeUIString(uiString) {
var uiBindings = this._getUIBindings();
return _normalizeUIString(uiString, uiBindings);
},
// normalize the values of passed hash with the views `ui` selectors.
// `{foo: "@ui.bar"}`
normalizeUIValues: function normalizeUIValues(hash, properties) {
var uiBindings = this._getUIBindings();
return _normalizeUIValues(hash, uiBindings, properties);
},
_getUIBindings: function _getUIBindings() {
var uiBindings = _.result(this, '_uiBindings');
var ui = _.result(this, 'ui');
return uiBindings || ui;
},
// This method binds the elements specified in the "ui" hash inside the view's code with
// the associated jQuery selectors.
_bindUIElements: function _bindUIElements() {
var _this = this;
if (!this.ui) {
return;
}
// store the ui hash in _uiBindings so they can be reset later
// and so re-rendering the view will be able to find the bindings
if (!this._uiBindings) {
this._uiBindings = this.ui;
}
// get the bindings result, as a function or otherwise
var bindings = _.result(this, '_uiBindings');
// empty the ui so we don't have anything to start with
this._ui = {};
// bind each of the selectors
_.each(bindings, function (selector, key) {
_this._ui[key] = _this.$(selector);
});
this.ui = this._ui;
},
_unbindUIElements: function _unbindUIElements() {
var _this2 = this;
if (!this.ui || !this._uiBindings) {
return;
}
// delete all of the existing ui bindings
_.each(this.ui, function ($el, name) {
delete _this2.ui[name];
});
// reset the ui element to the original bindings configuration
this.ui = this._uiBindings;
delete this._uiBindings;
delete this._ui;
},
_getUI: function _getUI(name) {
return this._ui[name];
}
};
// MixinOptions
// - behaviors
// - childViewEventPrefix
// - childViewEvents
// - childViewTriggers
// - collectionEvents
// - modelEvents
// - triggers
// - ui
var ViewMixin = {
supportsRenderLifecycle: true,
supportsDestroyLifecycle: true,
_isDestroyed: false,
isDestroyed: function isDestroyed() {
return !!this._isDestroyed;
},
_isRendered: false,
isRendered: function isRendered() {
return !!this._isRendered;
},
_isAttached: false,
isAttached: function isAttached() {
return !!this._isAttached;
},
// Overriding Backbone.View's `delegateEvents` to handle
// `events` and `triggers`
delegateEvents: function delegateEvents(eventsArg) {
this._proxyBehaviorViewProperties();
this._buildEventProxies();
var viewEvents = this._getEvents(eventsArg);
if (typeof eventsArg === 'undefined') {
this.events = viewEvents;
}
var combinedEvents = _.extend({}, this._getBehaviorEvents(), viewEvents, this._getBehaviorTriggers(), this.getTriggers());
Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
return this;
},
_getEvents: function _getEvents(eventsArg) {
var events = eventsArg || this.events;
if (_.isFunction(events)) {
return this.normalizeUIKeys(events.call(this));
}
return this.normalizeUIKeys(events);
},
// Configure `triggers` to forward DOM events to view
// events. `triggers: {"click .foo": "do:foo"}`
getTriggers: function getTriggers() {
if (!this.triggers) {
return;
}
// Allow `triggers` to be configured as a function
var triggers = this.normalizeUIKeys(_.result(this, 'triggers'));
// Configure the triggers, prevent default
// action and stop propagation of DOM events
return this._getViewTriggers(this, triggers);
},
// Handle `modelEvents`, and `collectionEvents` configuration
delegateEntityEvents: function delegateEntityEvents() {
this._delegateEntityEvents(this.model, this.collection);
// bind each behaviors model and collection events
this._delegateBehaviorEntityEvents();
return this;
},
// Handle unbinding `modelEvents`, and `collectionEvents` configuration
undelegateEntityEvents: function undelegateEntityEvents() {
this._undelegateEntityEvents(this.model, this.collection);
// unbind each behaviors model and collection events
this._undelegateBehaviorEntityEvents();
return this;
},
// Internal helper method to verify whether the view hasn't been destroyed
_ensureViewIsIntact: function _ensureViewIsIntact() {
if (this._isDestroyed) {
throw new MarionetteError({
name: 'ViewDestroyedError',
message: 'View (cid: "' + this.cid + '") has already been destroyed and cannot be used.'
});
}
},
// Handle destroying the view and its children.
destroy: function destroy() {
if (this._isDestroyed) {
return this;
}
var shouldTriggerDetach = !!this._isAttached;
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
this.triggerMethod.apply(this, ['before:destroy', this].concat(args));
if (shouldTriggerDetach) {
this.triggerMethod('before:detach', this);
}
// unbind UI elements
this.unbindUIElements();
// remove the view from the DOM
// https://github.com/jashkenas/backbone/blob/1.2.3/backbone.js#L1235
this._removeElement();
if (shouldTriggerDetach) {
this._isAttached = false;
this.triggerMethod('detach', this);
}
// remove children after the remove to prevent extra paints
this._removeChildren();
this._destroyBehaviors(args);
this._isDestroyed = true;
this._isRendered = false;
this.triggerMethod.apply(this, ['destroy', this].concat(args));
this.stopListening();
return this;
},
bindUIElements: function bindUIElements() {
this._bindUIElements();
this._bindBehaviorUIElements();
return this;
},
// This method unbinds the elements specified in the "ui" hash
unbindUIElements: function unbindUIElements() {
this._unbindUIElements();
this._unbindBehaviorUIElements();
return this;
},
getUI: function getUI(name) {
this._ensureViewIsIntact();
return this._getUI(name);
},
// used as the prefix for child view events
// that are forwarded through the layoutview
childViewEventPrefix: 'childview',
// import the `triggerMethod` to trigger events with corresponding
// methods if the method exists
triggerMethod: function triggerMethod$$() {
var ret = triggerMethod.apply(this, arguments);
this._triggerEventOnBehaviors.apply(this, arguments);
this._triggerEventOnParentLayout.apply(this, arguments);
return ret;
},
// Cache `childViewEvents` and `childViewTriggers`
_buildEventProxies: function _buildEventProxies() {
this._childViewEvents = _.result(this, 'childViewEvents');
this._childViewTriggers = _.result(this, 'childViewTriggers');
},
_triggerEventOnParentLayout: function _triggerEventOnParentLayout() {
var layoutView = this._parentView();
if (!layoutView) {
return;
}
layoutView._childViewEventHandler.apply(layoutView, arguments);
},
// Walk the _parent tree until we find a view (if one exists).
// Returns the parent view hierarchically closest to this view.
_parentView: function _parentView() {
var parent = this._parent;
while (parent) {
if (parent instanceof View) {
return parent;
}
parent = parent._parent;
}
},
_childViewEventHandler: function _childViewEventHandler(eventName) {
var childViewEvents = this.normalizeMethods(this._childViewEvents);
// call collectionView childViewEvent if defined
for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
if (typeof childViewEvents !== 'undefined' && _.isFunction(childViewEvents[eventName])) {
childViewEvents[eventName].apply(this, args);
}
// use the parent view's proxyEvent handlers
var childViewTriggers = this._childViewTriggers;
// Call the event with the proxy name on the parent layout
if (childViewTriggers && _.isString(childViewTriggers[eventName])) {
this.triggerMethod.apply(this, [childViewTriggers[eventName]].concat(args));
}
var prefix = _.result(this, 'childViewEventPrefix');
if (prefix !== false) {
var childEventName = prefix + ':' + eventName;
this.triggerMethod.apply(this, [childEventName].concat(args));
}
}
};
_.extend(ViewMixin, BehaviorsMixin, CommonMixin, DelegateEntityEventsMixin, TriggersMixin, UIMixin);
function destroyBackboneView(view) {
if (!view.supportsDestroyLifecycle) {
triggerMethodOn(view, 'before:destroy', view);
}
var shouldTriggerDetach = !!view._isAttached;
if (shouldTriggerDetach) {
triggerMethodOn(view, 'before:detach', view);
}
view.remove();
if (shouldTriggerDetach) {
view._isAttached = false;
triggerMethodOn(view, 'detach', view);
}
view._isDestroyed = true;
if (!view.supportsDestroyLifecycle) {
triggerMethodOn(view, 'destroy', view);
}
}
var ClassOptions$2 = ['allowMissingEl', 'parentEl', 'replaceElement'];
var Region = MarionetteObject.extend({
cidPrefix: 'mnr',
replaceElement: false,
_isReplaced: false,
constructor: function constructor(options) {
this._setOptions(options);
this.mergeOptions(options, ClassOptions$2);
// getOption necessary because options.el may be passed as undefined
this._initEl = 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 MarionetteError({
name: 'NoElError',
message: 'An "el" must be specified for a region.'
});
}
this.$el = this.getEl(this.el);
MarionetteObject.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. The `preventDestroy`
// option can be used to prevent a view from the old view being destroyed on show.
show: function show(view, options) {
if (!this._ensureElement(options)) {
return;
}
this._ensureView(view);
if (view === this.currentView) {
return this;
}
this.triggerMethod('before:show', this, view, options);
monitorViewEvents(view);
this.empty(options);
// 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.on('destroy', this._empty, this);
// Make this region the view's parent.
// It's important that this parent binding happens before rendering so that any events
// the child may trigger during render can also be triggered on the child's ancestor views.
view._parent = this;
this._renderView(view);
this._attachView(view, options);
this.triggerMethod('show', this, view, options);
return this;
},
_renderView: function _renderView(view) {
if (view._isRendered) {
return;
}
if (!view.supportsRenderLifecycle) {
triggerMethodOn(view, 'before:render', view);
}
view.render();
if (!view.supportsRenderLifecycle) {
view._isRendered = true;
triggerMethodOn(view, 'render', view);
}
},
_attachView: function _attachView(view) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var shouldTriggerAttach = !view._isAttached && isNodeAttached(this.el);
var shouldReplaceEl = typeof options.replaceElement === 'undefined' ? !!_.result(this, 'replaceElement') : !!options.replaceElement;
if (shouldTriggerAttach) {
triggerMethodOn(view, 'before:attach', view);
}
if (shouldReplaceEl) {
this._replaceEl(view);
} else {
this.attachHtml(view);
}
if (shouldTriggerAttach) {
view._isAttached = true;
triggerMethodOn(view, 'attach', view);
}
this.currentView = view;
},
_ensureElement: function _ensureElement() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
if (!_.isObject(this.el)) {
this.$el = this.getEl(this.el);
this.el = this.$el[0];
}
if (!this.$el || this.$el.length === 0) {
var allowMissingEl = typeof options.allowMissingEl === 'undefined' ? !!_.result(this, 'allowMissingEl') : !!options.allowMissingEl;
if (allowMissingEl) {
return false;
} else {
throw new MarionetteError('An "el" must exist in DOM for this region ' + this.cid);
}
}
return true;
},
_ensureView: function _ensureView(view) {
if (!view) {
throw new MarionetteError({
name: 'ViewNotValid',
message: 'The view passed is undefined and therefore invalid. You must pass a view instance to show.'
});
}
if (view._isDestroyed) {
throw new MarionetteError({
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 getEl(el) {
return Backbone.$(el, _.result(this, 'parentEl'));
},
_replaceEl: function _replaceEl(view) {
// always restore the el to ensure the regions el is present before replacing
this._restoreEl();
var parent = this.el.parentNode;
parent.replaceChild(view.el, this.el);
this._isReplaced = true;
},
// Restore the region's element in the DOM.
_restoreEl: function _restoreEl() {
// There is nothing to replace
if (!this._isReplaced) {
return;
}
var view = this.currentView;
if (!view) {
return;
}
var parent = view.el.parentNode;
if (!parent) {
return;
}
parent.replaceChild(this.el, view.el);
this._isReplaced = false;
},
// Check to see if the region's el was replaced.
isReplaced: function isReplaced() {
return !!this._isReplaced;
},
// Override this method to change how the new view is appended to the `$el` that the
// region is managing
attachHtml: function attachHtml(view) {
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 empty() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { allowMissingEl: true };
var view = this.currentView;
// If there is no view in the region we should only detach current html
if (!view) {
if (this._ensureElement(options)) {
this.detachHtml();
}
return this;
}
var shouldDestroy = !options.preventDestroy;
if (!shouldDestroy) {
deprecate('The preventDestroy option is deprecated. Use Region#detachView');
}
this._empty(view, shouldDestroy);
return this;
},
_empty: function _empty(view, shouldDestroy) {
view.off('destroy', this._empty, this);
this.triggerMethod('before:empty', this, view);
this._restoreEl();
delete this.currentView;
if (!view._isDestroyed) {
this._removeView(view, shouldDestroy);
delete view._parent;
}
this.triggerMethod('empty', this, view);
},
_removeView: function _removeView(view, shouldDestroy) {
if (!shouldDestroy) {
this._detachView(view);
return;
}
if (view.destroy) {
view.destroy();
} else {
destroyBackboneView(view);
}
},
detachView: function detachView() {
var view = this.currentView;
if (!view) {
return;
}
this._empty(view);
return view;
},
_detachView: function _detachView(view) {
var shouldTriggerDetach = !!view._isAttached;
if (shouldTriggerDetach) {
triggerMethodOn(view, 'before:detach', view);
}
this.detachHtml();
if (shouldTriggerDetach) {
view._isAttached = false;
triggerMethodOn(view, 'detach', view);
}
},
// Override this method to change how the region detaches current content
detachHtml: function detachHtml() {
this.$el.contents().detach();
},
// Checks whether a view is currently present within the region. Returns `true` if there is
// and `false` if no view is present.
hasView: function hasView() {
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 reset(options) {
this.empty(options);
if (this.$el) {
this.el = this._initEl;
}
delete this.$el;
return this;
},
destroy: function destroy(options) {
this.reset(options);
return MarionetteObject.prototype.destroy.apply(this, arguments);
}
});
// return the region instance from the definition
function buildRegion (definition, defaults) {
if (definition instanceof Region) {
return definition;
}
return buildRegionFromDefinition(definition, defaults);
}
function buildRegionFromDefinition(definition, defaults) {
var opts = _.extend({}, defaults);
if (_.isString(definition)) {
_.extend(opts, { el: definition });
return buildRegionFromObject(opts);
}
if (_.isFunction(definition)) {
_.extend(opts, { regionClass: definition });
return buildRegionFromObject(opts);
}
if (_.isObject(definition)) {
if (definition.selector) {
deprecate('The selector option on a Region definition object is deprecated. Use el to pass a selector string');
}
_.extend(opts, { el: definition.selector }, definition);
return buildRegionFromObject(opts);
}
throw new MarionetteError({
message: 'Improper region configuration type.',
url: 'marionette.region.html#region-configuration-types'
});
}
function buildRegionFromObject(definition) {
var RegionClass = definition.regionClass;
var options = _.omit(definition, 'regionClass');
return new RegionClass(options);
}
// MixinOptions
// - regions
// - regionClass
var RegionsMixin = {
regionClass: Region,
// Internal method to initialize the regions that have been defined in a
// `regions` attribute on this View.
_initRegions: function _initRegions() {
// init regions hash
this.regions = this.regions || {};
this._regions = {};
this.addRegions(_.result(this, 'regions'));
},
// Internal method to re-initialize all of the regions by updating
// the `el` that they point to
_reInitRegions: function _reInitRegions() {
_invoke(this._regions, 'reset');
},
// Add a single region, by name, to the View
addRegion: function addRegion(name, definition) {
var regions = {};
regions[name] = definition;
return this.addRegions(regions)[name];
},
// Add multiple regions as a {name: definition, name2: def2} object literal
addRegions: function addRegions(regions) {
// If there's nothing to add, stop here.
if (_.isEmpty(regions)) {
return;
}
// Normalize region selectors hash to allow
// a user to use the @ui. syntax.
regions = this.normalizeUIValues(regions, ['selector', 'el']);
// Add the regions definitions to the regions property
this.regions = _.extend({}, this.regions, regions);
return this._addRegions(regions);
},
// internal method to build and add regions
_addRegions: function _addRegions(regionDefinitions) {
var _this = this;
var defaults = {
regionClass: this.regionClass,
parentEl: _.partial(_.result, this, 'el')
};
return _.reduce(regionDefinitions, function (regions, definition, name) {
regions[name] = buildRegion(definition, defaults);
_this._addRegion(regions[name], name);
return regions;
}, {});
},
_addRegion: function _addRegion(region, name) {
this.triggerMethod('before:add:region', this, name, region);
region._parent = this;
this._regions[name] = region;
this.triggerMethod('add:region', this, name, region);
},
// Remove a single region from the View, by name
removeRegion: function removeRegion(name) {
var region = this._regions[name];
this._removeRegion(region, name);
return region;
},
// Remove all regions from the View
removeRegions: function removeRegions() {
var regions = this.getRegions();
_.each(this._regions, _.bind(this._removeRegion, this));
return regions;
},
_removeRegion: function _removeRegion(region, name) {
this.triggerMethod('before:remove:region', this, name, region);
region.destroy();
delete this.regions[name];
delete this._regions[name];
this.triggerMethod('remove:region', this, name, region);
},
// Empty all regions in the region manager, but
// leave them attached
emptyRegions: function emptyRegions() {
var regions = this.getRegions();
_invoke(regions, 'empty');
return regions;
},
// Checks to see if view contains region
// Accepts the region name
// hasRegion('main')
hasRegion: function hasRegion(name) {
return !!this.getRegion(name);
},
// Provides access to regions
// Accepts the region name
// getRegion('main')
getRegion: function getRegion(name) {
return this._regions[name];
},
// Get all regions
getRegions: function getRegions() {
return _.clone(this._regions);
},
showChildView: function showChildView(name, view) {
var region = this.getRegion(name);
for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
args[_key - 2] = arguments[_key];
}
return region.show.apply(region, [view].concat(args));
},
detachChildView: function detachChildView(name) {
return this.getRegion(name).detachView();
},
getChildView: function getChildView(name) {
return this.getRegion(name).currentView;
}
};
// Render a template with data by passing in the template
// selector and the data to render.
var 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 render(template, data) {
if (!template) {
throw new MarionetteError({
name: 'TemplateNotFoundError',
message: 'Cannot render the template since its false, null or undefined.'
});
}
var templateFunc = _.isFunction(template) ? template : TemplateCache.get(template);
return templateFunc(data);
}
};
var ClassOptions$1 = ['behaviors', 'childViewEventPrefix', 'childViewEvents', 'childViewTriggers', 'collectionEvents', 'events', 'modelEvents', 'regionClass', 'regions', 'template', 'templateContext', 'triggers', 'ui'];
// The standard view. Includes view events, automatic rendering
// of Underscore templates, nested views, and more.
var View = Backbone.View.extend({
constructor: function constructor(options) {
this.render = _.bind(this.render, this);
this._setOptions(options);
this.mergeOptions(options, ClassOptions$1);
monitorViewEvents(this);
this._initBehaviors();
this._initRegions();
var args = Array.prototype.slice.call(arguments);
args[0] = this.options;
Backbone.View.prototype.constructor.apply(this, args);
this.delegateEntityEvents();
},
// Serialize the view's model *or* collection, if
// it exists, for the template
serializeData: function serializeData() {
if (!this.model && !this.collection) {
return {};
}
// If we have a model, we serialize that
if (this.model) {
return this.serializeModel();
}
// Otherwise, we serialize the collection,
// making it available under the `items` property
return {
items: this.serializeCollection()
};
},
// Prepares the special `model` property of a view
// for being displayed in the template. By default
// we simply clone the attributes. Override this if
// you need a custom transformation for your view's model
serializeModel: function serializeModel() {
if (!this.model) {
return {};
}
return _.clone(this.model.attributes);
},
// Serialize a collection by cloning each of
// its model's attributes
serializeCollection: function serializeCollection() {
if (!this.collection) {
return {};
}
return this.collection.map(function (model) {
return _.clone(model.attributes);
});
},
// Overriding Backbone.View's `setElement` to handle
// if an el was previously defined. If so, the view might be
// rendered or attached on setElement.
setElement: function setElement() {
var hasEl = !!this.el;
Backbone.View.prototype.setElement.apply(this, arguments);
if (hasEl) {
this._isRendered = !!this.$el.length;
this._isAttached = isNodeAttached(this.el);
}
if (this._isRendered) {
this.bindUIElements();
}
return this;
},
// 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.
// Subsequent renders after the first will re-render all nested
// views.
render: function render() {
this._ensureViewIsIntact();
this.triggerMethod('before:render', this);
// If this is not the first render call, then we need to
// re-initialize the `el` for each region
if (this._isRendered) {
this._reInitRegions();
}
this._renderTemplate();
this.bindUIElements();
this._isRendered = true;
this.triggerMethod('render', this);
return this;
},
// Internal method to render the template with the serialized data
// and template context via the `Marionette.Renderer` object.
_renderTemplate: function _renderTemplate() {
var template = this.getTemplate();
// Allow template-less views
if (template === false) {
return;
}
// Add in entity data and template context
var data = this.mixinTemplateContext(this.serializeData());
// Render and add to el
var html = Renderer.render(template, data, this);
this.attachElContent(html);
},
// 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 getTemplate() {
return this.template;
},
// Mix in template context methods. Looks for a
// `templateContext` 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.
mixinTemplateContext: function mixinTemplateContext() {
var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var templateContext = _.result(this, 'templateContext');
return _.extend(target, templateContext);
},
// Attaches the content of a given view.
// This method can be overridden to optimize rendering,
// or to render in a non standard way.
//
// For example, using `innerHTML` instead of `$el.html`
//
// ```js
// attachElContent(html) {
// this.el.innerHTML = html;
// return this;
// }
// ```
attachElContent: function attachElContent(html) {
this.$el.html(html);
return this;
},
// called by ViewMixin destroy
_removeChildren: function _removeChildren() {
this.removeRegions();
},
_getImmediateChildren: function _getImmediateChildren() {
return _.chain(this.getRegions()).map('currentView').compact().value();
}
});
_.extend(View.prototype, ViewMixin, RegionsMixin);
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', 'reduce'];
var emulateCollection = function emulateCollection(object, listProperty) {
_.each(methods, function (method) {
object[method] = function () {
var list = _.values(_.result(this, listProperty));
var args = [list].concat(_.toArray(arguments));
return _[method].apply(_, args);
};
});
};
// Provide a container to store, retrieve and
// shut down child views.
var Container = function Container(views) {
this._views = {};
this._indexByModel = {};
this._indexByCustom = {};
this._updateLength();
_.each(views, _.bind(this.add, this));
};
emulateCollection(Container.prototype, '_views');
// Container Methods
// -----------------
_.extend(Container.prototype, {
// Add a view to this container. Stores the view
// by `cid` and makes it searchable by the model
// cid (and model itself). Optionally specify
// a custom key to store an retrieve the view.
add: function add(view, customIndex) {
return this._add(view, customIndex)._updateLength();
},
// To be used when avoiding call _updateLength
// When you are done adding all your new views
// call _updateLength
_add: function _add(view, customIndex) {
var viewCid = view.cid;
// store the view
this._views[viewCid] = view;
// index it by model
if (view.model) {
this._indexByModel[view.model.cid] = viewCid;
}
// index by custom
if (customIndex) {
this._indexByCustom[customIndex] = viewCid;
}
return this;
},
// Find a view by the model that was attached to
// it. Uses the model's `cid` to find it.
findByModel: function findByModel(model) {
return this.findByModelCid(model.cid);
},
// Find a view by the `cid` of the model that was attached to
// it. Uses the model's `cid` to find the view `cid` and
// retrieve the view using it.
findByModelCid: function findByModelCid(modelCid) {
var viewCid = this._indexByModel[modelCid];
return this.findByCid(viewCid);
},
// Find a view by a custom indexer.
findByCustom: function findByCustom(index) {
var viewCid = this._indexByCustom[index];
return this.findByCid(viewCid);
},
// Find by index. This is not guaranteed to be a
// stable index.
findByIndex: function findByIndex(index) {
return _.values(this._views)[index];
},
// retrieve a view by its `cid` directly
findByCid: function findByCid(cid) {
return this._views[cid];
},
// Remove a view
remove: function remove(view) {
return this._remove(view)._updateLength();
},
// To be used when avoiding call _updateLength
// When you are done adding all your new views
// call _updateLength
_remove: function _remove(view) {
var viewCid = view.cid;
// delete model index
if (view.model) {
delete this._indexByModel[view.model.cid];
}
// delete custom index
_.some(this._indexByCustom, _.bind(function (cid, key) {
if (cid === viewCid) {
delete this._indexByCustom[key];
return true;
}
}, this));
// remove the view from the container
delete this._views[viewCid];
return this;
},
// Update the `.length` attribute on this container
_updateLength: function _updateLength() {
this.length = _.size(this._views);
return this;
}
});
var ClassOptions$3 = ['behaviors', 'childView', 'childViewEventPrefix', 'childViewEvents', 'childViewOptions', 'childViewTriggers', 'collectionEvents', 'events', 'filter', 'emptyView', 'emptyViewOptions', 'modelEvents', 'reorderOnSort', 'sort', 'triggers', 'ui', 'viewComparator'];
// A view that iterates over a Backbone.Collection
// and renders an individual child view for each model.
var CollectionView = Backbone.View.extend({
// flag for maintaining the sorted order of the collection
sort: true,
// constructor
// option to pass `{sort: false}` to prevent the `CollectionView` from
// maintaining the sorted order of the collection.
// This will fallback onto appending childView's to the end.
//
// option to pass `{viewComparator: compFunction()}` to allow the `CollectionView`
// to use a custom sort order for the collection.
constructor: function constructor(options) {
this.render = _.bind(this.render, this);
this._setOptions(options);
this.mergeOptions(options, ClassOptions$3);
monitorViewEvents(this);
this._initBehaviors();
this.once('render', this._initialEvents);
this._initChildViewStorage();
this._bufferedChildren = [];
var args = Array.prototype.slice.call(arguments);
args[0] = this.options;
Backbone.View.prototype.constructor.apply(this, args);
this.delegateEntityEvents();
},
// Instead of inserting elements one by one into the page, it's much more performant to insert
// elements into a document fragment and then insert that document fragment into the page
_startBuffering: function _startBuffering() {
this._isBuffering = true;
},
_endBuffering: function _endBuffering() {
var shouldTriggerAttach = !!this._isAttached;
var triggerOnChildren = shouldTriggerAttach ? this._getImmediateChildren() : [];
this._isBuffering = false;
_.each(triggerOnChildren, function (child) {
triggerMethodOn(child, 'before:attach', child);
});
this.attachBuffer(this, this._createBuffer());
_.each(triggerOnChildren, function (child) {
child._isAttached = true;
triggerMethodOn(child, 'attach', child);
});
this._bufferedChildren = [];
},
_getImmediateChildren: function _getImmediateChildren() {
return _.values(this.children._views);
},
// Configured the initial events that the collection view binds to.
_initialEvents: function _initialEvents() {
if (this.collection) {
this.listenTo(this.collection, 'add', this._onCollectionAdd);
this.listenTo(this.collection, 'update', this._onCollectionUpdate);
this.listenTo(this.collection, 'reset', this.render);
if (this.sort) {
this.listenTo(this.collection, 'sort', this._sortViews);
}
}
},
// Handle a child added to the collection
_onCollectionAdd: function _onCollectionAdd(child, collection, opts) {
// `index` is present when adding with `at` since BB 1.2; indexOf fallback for < 1.2
var index = opts.at !== undefined && (opts.index || collection.indexOf(child));
// When filtered or when there is no initial index, calculate index.
if (this.filter || index === false) {
index = _.indexOf(this._filteredSortedModels(index), child);
}
if (this._shouldAddChild(child, index)) {
this._destroyEmptyView();
this._addChild(child, index);
}
},
// Handle collection update model removals
_onCollectionUpdate: function _onCollectionUpdate(collection, options) {
var changes = options.changes;
this._removeChildModels(changes.removed);
},
// Remove the child views and destroy them.
// This function also updates the indices of later views
// in the collection in order to keep the children in sync with the collection.
// "models" is an array of models and the corresponding views
// will be removed and destroyed from the CollectionView
_removeChildModels: function _removeChildModels(models) {
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var checkEmpty = _ref.checkEmpty;
var shouldCheckEmpty = checkEmpty !== false;
// Used to determine where to update the remaining
// sibling view indices after these views are removed.
var removedViews = this._getRemovedViews(models);
if (!removedViews.length) {
return;
}
this.children._updateLength();
// decrement the index of views after this one
this._updateIndices(removedViews, false);
if (shouldCheckEmpty) {
this._checkEmpty();
}
},
// Returns the views that will be used for re-indexing
// through CollectionView#_updateIndices.
_getRemovedViews: function _getRemovedViews(models) {
var _this = this;
// Returning a view means something was removed.
return _.reduce(models, function (removingViews, model) {
var view = _this.children.findByModel(model);
if (!view || view._isDestroyed) {
return removingViews;
}
_this._removeChildView(view);
removingViews.push(view);
return removingViews;
}, []);
},
_findGreatestIndexedView: function _findGreatestIndexedView(views) {
return _.reduce(views, function (greatestIndexedView, view) {
// Even if the index is `undefined`, a view will get returned.
if (!greatestIndexedView || greatestIndexedView._index < view._index) {
return view;
}
return greatestIndexedView;
}, undefined);
},
_removeChildView: function _removeChildView(view) {
this.triggerMethod('before:remove:child', this, view);
this.children._remove(view);
if (view.destroy) {
view.destroy();
} else {
destroyBackboneView(view);
}
delete view._parent;
this.stopListening(view);
this.triggerMethod('remove:child', this, view);
},
// Overriding Backbone.View's `setElement` to handle
// if an el was previously defined. If so, the view might be
// attached on setElement.
setElement: function setElement() {
var hasEl = !!this.el;
Backbone.View.prototype.setElement.apply(this, arguments);
if (hasEl) {
this._isAttached = isNodeAttached(this.el);
}
return this;
},
// Render children views. Override this method to provide your own implementation of a
// render function for the collection view.
render: function render() {
this._ensureViewIsIntact();
this.triggerMethod('before:render', this);
this._renderChildren();
this._isRendered = true;
this.triggerMethod('render', this);
return this;
},
// An efficient rendering used for filtering. Instead of modifying the whole DOM for the
// collection view, we are only adding or removing the related childrenViews.
setFilter: function setFilter(filter) {
var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var preventRender = _ref2.preventRender;
var canBeRendered = this._isRendered && !this._isDestroyed;
var filterChanged = this.filter !== filter;
var shouldRender = canBeRendered && filterChanged && !preventRender;
if (shouldRender) {
var previousModels = this._filteredSortedModels();
this.filter = filter;
var models = this._filteredSortedModels();
this._applyModelDeltas(models, previousModels);
} else {
this.filter = filter;
}
return this;
},
// `removeFilter` is actually an alias for removing filters.
removeFilter: function removeFilter(options) {
return this.setFilter(null, options);
},
// Calculate and apply difference by cid between `models` and `previousModels`.
_applyModelDeltas: function _applyModelDeltas(models, previousModels) {
var _this2 = this;
var currentIds = {};
_.each(models, function (model, index) {
var addedChildNotExists = !_this2.children.findByModel(model);
if (addedChildNotExists) {
_this2._onCollectionAdd(model, _this2.collection, { at: index });
}
currentIds[model.cid] = true;
});
var removeModels = _.filter(previousModels, function (prevModel) {
return !currentIds[prevModel.cid] && _this2.children.findByModel(prevModel);
});
this._removeChildModels(removeModels);
},
// Reorder DOM after sorting. When your element's rendering do not use their index,
// you can pass reorderOnSort: true to only reorder the DOM after a sort instead of
// rendering all the collectionView.
reorder: function reorder() {
var _this3 = this;
var children = this.children;
var models = this._filteredSortedModels();
if (!models.length && this._showingEmptyView) {
return this;
}
var anyModelsAdded = _.some(models, function (model) {
return !children.findByModel(model);
});
// If there are any new models added due to filtering we need to add child views,
// so render as normal.
if (anyModelsAdded) {
this.render();
} else {
(function () {
var filteredOutModels = [];
// Get the DOM nodes in the same order as the models and
// find the model that were children before but aren't in this new order.
var elsToReorder = children.reduce(function (viewEls, view) {
var index = _.indexOf(models, view.model);
if (index === -1) {
filteredOutModels.push(view.model);
return viewEls;
}
view._index = index;
viewEls[index] = view.el;
return viewEls;
}, new Array(models.length));
_this3.triggerMethod('before:reorder', _this3);
// Since append moves elements that are already in the DOM, appending the elements
// will effectively reorder them.
_this3._appendReorderedChildren(elsToReorder);
// remove any views that have been filtered out
_this3._removeChildModels(filteredOutModels);
_this3.triggerMethod('reorder', _this3);
})();
}
return this;
},
// Render view after sorting. Override this method to change how the view renders
// after a `sort` on the collection.
resortView: function resortView() {
if (this.reorderOnSort) {
this.reorder();
} else {
this._renderChildren();
}
return this;
},
// Internal method. This checks for any changes in the order of the collection.
// If the index of any view doesn't match, it will render.
_sortViews: function _sortViews() {
var _this4 = this;
var models = this._filteredSortedModels();
// check for any changes in sort order of views
var orderChanged = _.find(models, function (item, index) {
var view = _this4.children.findByModel(item);
return !view || view._index !== index;
});
if (orderChanged) {
this.resortView();
}
},
// Internal reference to what index a `emptyView` is.
_emptyViewIndex: -1,
// Internal method. Separated so that CompositeView can append to the childViewContainer
// if necessary
_appendReorderedChildren: function _appendReorderedChildren(children) {
this.$el.append(children);
},
// Internal method. Separated so that CompositeView can have more control over events
// being triggered, around the rendering process
_renderChildren: function _renderChildren() {
if (this._isRendered) {
this._destroyEmptyView();
this._destroyChildren({ checkEmpty: false });
}
var models = this._filteredSortedModels();
if (this.isEmpty({ processedModels: models })) {
this._showEmptyView();
} else {
this.triggerMethod('before:render:children', this);
this._startBuffering();
this._showCollection(models);
this._endBuffering();
this.triggerMethod('render:children', this);
}
},
_createView: function _createView(model, index) {
var ChildView = this._getChildView(model);
var childViewOptions = this._getChildViewOptions(model, index);
var view = this.buildChildView(model, ChildView, childViewOptions);
return view;
},
_setupChildView: function _setupChildView(view, index) {
view._parent = this;
monitorViewEvents(view);
// set up the child view event forwarding
this._proxyChildEvents(view);
if (this.sort) {
view._index = index;
}
},
// Internal method to loop through collection and show each child view.
_showCollection: function _showCollection(models) {
_.each(models, _.bind(this._addChild, this));
this.children._updateLength();
},
// Allow the collection to be sorted by a custom view comparator
_filteredSortedModels: function _filteredSortedModels(addedAt) {
if (!this.collection || !this.collection.length) {
return [];
}
var viewComparator = this.getViewComparator();
var models = this.collection.models;
addedAt = Math.min(Math.max(addedAt, 0), models.length - 1);
if (viewComparator) {
var addedModel = void 0;
// Preserve `at` location, even for a sorted view
if (addedAt) {
addedModel = models[addedAt];
models = models.slice(0, addedAt).concat(models.slice(addedAt + 1));
}
models = this._sortModelsBy(models, viewComparator);
if (addedModel) {
models.splice(addedAt, 0, addedModel);
}
}
// Filter after sorting in case the filter uses the index
models = this._filterModels(models);
return models;
},
getViewComparator: function getViewComparator() {
return this.viewComparator;
},
// Filter an array of models, if a filter exists
_filterModels: function _filterModels(models) {
var _this5 = this;
if (this.filter) {
models = _.filter(models, function (model, index) {
return _this5._shouldAddChild(model, index);
});
}
return models;
},
_sortModelsBy: function _sortModelsBy(models, comparator) {
if (typeof comparator === 'string') {
return _.sortBy(models, function (model) {
return model.get(comparator);
});
} else if (comparator.length === 1) {
return _.sortBy(models, _.bind(comparator, this));
} else {
return models.sort(_.bind(comparator, this));
}
},
// Internal method to show an empty view in place of a collection of child views,
// when the collection is empty
_showEmptyView: function _showEmptyView() {
var EmptyView = this._getEmptyView();
if (EmptyView && !this._showingEmptyView) {
this._showingEmptyView = true;
var model = new Backbone.Model();
var emptyViewOptions = this.emptyViewOptions || this.childViewOptions;
if (_.isFunction(emptyViewOptions)) {
emptyViewOptions = emptyViewOptions.call(this, model, this._emptyViewIndex);
}
var view = this.buildChildView(model, EmptyView, emptyViewOptions);
this.triggerMethod('before:render:empty', this, view);
this.addChildView(view, 0);
this.triggerMethod('render:empty', this, view);
}
},
// Internal method to destroy an existing emptyView instance if one exists. Called when
// a collection view has been rendered empty, and then a child is added to the collection.
_destroyEmptyView: function _destroyEmptyView() {
if (this._showingEmptyView) {
this.triggerMethod('before:remove:empty', this);
this._destroyChildren();
delete this._showingEmptyView;
this.triggerMethod('remove:empty', this);
}
},
// Retrieve the empty view class
_getEmptyView: function _getEmptyView() {
var emptyView = this.emptyView;
if (!emptyView) {
return;
}
return this._getView(emptyView);
},
// Retrieve the `childView` class
// The `childView` property can be either a view class or a function that
// returns a view class. If it is a function, it will receive the model that
// will be passed to the view instance (created from the returned view class)
_getChildView: function _getChildView(child) {
var childView = this.childView;
if (!childView) {
throw new MarionetteError({
name: 'NoChildViewError',
message: 'A "childView" must be specified'
});
}
childView = this._getView(childView, child);
if (!childView) {
throw new MarionetteError({
name: 'InvalidChildViewError',
message: '"childView" must be a view class or a function that returns a view class'
});
}
return childView;
},
// First check if the `view` is a view class (the common case)
// Then check if it's a function (which we assume that returns a view class)
_getView: function _getView(view, child) {
if (view.prototype instanceof Backbone.View || view === Backbone.View) {
return view;
} else if (_.isFunction(view)) {
return view.call(this, child);
}
},
// Internal method for building and adding a child view
_addChild: function _addChild(child, index) {
var view = this._createView(child, index);
this.addChildView(view, index);
return view;
},
_getChildViewOptions: function _getChildViewOptions(child, index) {
if (_.isFunction(this.childViewOptions)) {
return this.childViewOptions(child, index);
}
return this.childViewOptions;
},
// Render the child's view and add it to the HTML for the collection view at a given index.
// This will also update the indices of later views in the collection in order to keep the
// children in sync with the collection.
addChildView: function addChildView(view, index) {
this.triggerMethod('before:add:child', this, view);
this._setupChildView(view, index);
// Store the child view itself so we can properly remove and/or destroy it later
if (this._isBuffering) {
// Add to children, but don't update children's length.
this.children._add(view);
} else {
// increment indices of views after this one
this._updateIndices(view, true);
this.children.add(view);
}
this._renderView(view);
this._attachView(view, index);
this.triggerMethod('add:child', this, view);
return view;
},
// Internal method. This decrements or increments the indices of views after the added/removed
// view to keep in sync with the collection.
_updateIndices: function _updateIndices(views, increment) {
if (!this.sort) {
return;
}
var view = _.isArray(views) ? this._findGreatestIndexedView(views) : views;
// update the indexes of views after this one
this.children.each(function (laterView) {
if (laterView._index >= view._index) {
laterView._index += increment ? 1 : -1;
}
});
},
_renderView: function _renderView(view) {
if (view._isRendered) {
return;
}
if (!view.supportsRenderLifecycle) {
triggerMethodOn(view, 'before:render', view);
}
view.render();
if (!view.supportsRenderLifecycle) {
view._isRendered = true;
triggerMethodOn(view, 'render', view);
}
},
_attachView: function _attachView(view, index) {
// Only trigger attach if already attached and not buffering,
// otherwise _endBuffering() or Region#show() handles this.
var shouldTriggerAttach = !view._isAttached && !this._isBuffering && this._isAttached;
if (shouldTriggerAttach) {
triggerMethodOn(view, 'before:attach', view);
}
this.attachHtml(this, view, index);
if (shouldTriggerAttach) {
view._isAttached = true;
triggerMethodOn(view, 'attach', view);
}
},
// Build a `childView` for a model in the collection.
buildChildView: function buildChildView(child, ChildViewClass, childViewOptions) {
var options = _.extend({ model: child }, childViewOptions);
return new ChildViewClass(options);
},
// Remove the child view and destroy it. This function also updates the indices of later views
// in the collection in order to keep the children in sync with the collection.
removeChildView: function removeChildView(view) {
if (!view || view._isDestroyed) {
return view;
}
this._removeChildView(view);
this.children._updateLength();
// decrement the index of views after this one
this._updateIndices(view, false);
return view;
},
// check if the collection is empty or optionally whether an array of pre-processed models is empty
isEmpty: function isEmpty(options) {
var models = void 0;
if (_.result(options, 'processedModels')) {
models = options.processedModels;
} else {
models = this.collection ? this.collection.models : [];
models = this._filterModels(models);
}
return models.length === 0;
},
// If empty, show the empty view
_checkEmpty: function _checkEmpty() {
if (this.isEmpty()) {
this._showEmptyView();
}
},
// You might need to override this if you've overridden attachHtml
attachBuffer: function attachBuffer(collectionView, buffer) {
collectionView.$el.append(buffer);
},
// Create a fragment buffer from the currently buffered children
_createBuffer: function _createBuffer() {
var elBuffer = document.createDocumentFragment();
_.each(this._bufferedChildren, function (b) {
elBuffer.appendChild(b.el);
});
return elBuffer;
},
// Append the HTML to the collection's `el`. Override this method to do something other
// than `.append`.
attachHtml: function attachHtml(collectionView, childView, index) {
if (collectionView._isBuffering) {
// buffering happens on reset events and initial renders
// in order to reduce the number of inserts into the
// document, which are expensive.
collectionView._bufferedChildren.splice(index, 0, childView);
} else {
// If we've already rendered the main collection, append
// the new child into the correct order if we need to. Otherwise
// append to the end.
if (!collectionView._insertBefore(childView, index)) {
collectionView._insertAfter(childView);
}
}
},
// Internal method. Check whether we need to insert the view into the correct position.
_insertBefore: function _insertBefore(childView, index) {
var currentView = void 0;
var findPosition = this.sort && index < this.children.length - 1;
if (findPosition) {
// Find the view after this one
currentView = this.children.find(function (view) {
return view._index === index + 1;
});
}
if (currentView) {
currentView.$el.before(childView.el);
return true;
}
return false;
},
// Internal method. Append a view to the end of the $el
_insertAfter: function _insertAfter(childView) {
this.$el.append(childView.el);
},
// Internal method to set up the `children` object for storing all of the child views
_initChildViewStorage: function _initChildViewStorage() {
this.children = new Container();
},
// called by ViewMixin destroy
_removeChildren: function _removeChildren() {
this._destroyChildren({ checkEmpty: false });
},
// Destroy the child views that this collection view is holding on to, if any
_destroyChildren: function _destroyChildren(options) {
if (!this.children.length) {
return;
}
this.triggerMethod('before:destroy:children', this);
var childModels = this.children.map('model');
this._removeChildModels(childModels, options);
this.triggerMethod('destroy:children', this);
},
// Return true if the given child should be shown. Return false otherwise.
// The filter will be passed (child, index, collection), where
// 'child' is the given model
// 'index' is the index of that model in the collection
// 'collection' is the collection referenced by this CollectionView
_shouldAddChild: function _shouldAddChild(child, index) {
var filter = this.filter;
return !_.isFunction(filter) || filter.call(this, child, index, this.collection);
},
// Set up the child view event forwarding. Uses a "childview:" prefix in front of all forwarded events.
_proxyChildEvents: function _proxyChildEvents(view) {
this.listenTo(view, 'all', this._childViewEventHandler);
}
});
_.extend(CollectionView.prototype, ViewMixin);
var ClassOptions$4 = ['childViewContainer', 'template', 'templateContext'];
// Used for rendering a branch-leaf, hierarchical structure.
// Extends directly from CollectionView
// @deprecated
var CompositeView = CollectionView.extend({
// Setting up the inheritance chain which allows changes to
// Marionette.CollectionView.prototype.constructor which allows overriding
// option to pass '{sort: false}' to prevent the CompositeView from
// maintaining the sorted order of the collection.
// This will fallback onto appending childView's to the end.
constructor: function constructor(options) {
deprecate('CompositeView is deprecated. Convert to View at your earliest convenience');
this.mergeOptions(options, ClassOptions$4);
CollectionView.prototype.constructor.apply(this, arguments);
},
// Configured the initial events that the composite view
// binds to. Override this method to prevent the initial
// events, or to add your own initial events.
_initialEvents: function _initialEvents() {
// Bind only after composite view is rendered to avoid adding child views
// to nonexistent childViewContainer
if (this.collection) {
this.listenTo(this.collection, 'add', this._onCollectionAdd);
this.listenTo(this.collection, 'update', this._onCollectionUpdate);
this.listenTo(this.collection, 'reset', this.renderChildren);
if (this.sort) {
this.listenTo(this.collection, 'sort', this._sortViews);
}
}
},
// Retrieve the `childView` to be used when rendering each of
// the items in the collection. The default is to return
// `this.childView` or Marionette.CompositeView if no `childView`
// has been defined. As happens in CollectionView, `childView` can
// be a function (which should return a view class).
_getChildView: function _getChildView(child) {
var childView = this.childView;
// for CompositeView, if `childView` is not specified, we'll get the same
// composite view class rendered for each child in the collection
// then check if the `childView` is a view class (the common case)
// finally check if it's a function (which we assume that returns a view class)
if (!childView) {
return this.constructor;
}
childView = this._getView(childView, child);
if (!childView) {
throw new MarionetteError({
name: 'InvalidChildViewError',
message: '"childView" must be a view class or a function that returns a view class'
});
}
return childView;
},
// Return the serialized model
serializeData: function serializeData() {
return this.serializeModel();
},
// Renders the model and the collection.
render: function render() {
this._ensureViewIsIntact();
this._isRendering = true;
this.resetChildViewContainer();
this.triggerMethod('before:render', this);
this._renderTemplate();
this.bindUIElements();
this.renderChildren();
this._isRendering = false;
this._isRendered = true;
this.triggerMethod('render', this);
return this;
},
renderChildren: function renderChildren() {
if (this._isRendered || this._isRendering) {
CollectionView.prototype._renderChildren.call(this);
}
},
// You might need to override this if you've overridden attachHtml
attachBuffer: function attachBuffer(compositeView, buffer) {
var $container = this.getChildViewContainer(compositeView);
$container.append(buffer);
},
// Internal method. Append a view to the end of the $el.
// Overidden from CollectionView to ensure view is appended to
// childViewContainer
_insertAfter: function _insertAfter(childView) {
var $container = this.getChildViewContainer(this, childView);
$container.append(childView.el);
},
// Internal method. Append reordered childView'.
// Overidden from CollectionView to ensure reordered views
// are appended to childViewContainer
_appendReorderedChildren: function _appendReorderedChildren(children) {
var $container = this.getChildViewContainer(this);
$container.append(children);
},
// Internal method to ensure an `$childViewContainer` exists, for the
// `attachHtml` method to use.
getChildViewContainer: function getChildViewContainer(containerView, childView) {
if (!!containerView.$childViewContainer) {
return containerView.$childViewContainer;
}
var container = void 0;
var childViewContainer = containerView.childViewContainer;
if (childViewContainer) {
var selector = _.result(containerView, 'childViewContainer');
if (selector.charAt(0) === '@' && containerView.ui) {
container = containerView.ui[selector.substr(4)];
} else {
container = containerView.$(selector);
}
if (container.length <= 0) {
throw new MarionetteError({
name: 'ChildViewContainerMissingError',
message: 'The specified "childViewContainer" was not found: ' + containerView.childViewContainer
});
}
} else {
container = containerView.$el;
}
containerView.$childViewContainer = container;
return container;
},
// Internal method to reset the `$childViewContainer` on render
resetChildViewContainer: function resetChildViewContainer() {
if (this.$childViewContainer) {
this.$childViewContainer = undefined;
}
}
});
// To prevent duplication but allow the best View organization
// Certain View methods are mixed directly into the deprecated CompositeView
var MixinFromView = _.pick(View.prototype, 'serializeModel', 'getTemplate', '_renderTemplate', 'mixinTemplateContext', 'attachElContent');
_.extend(CompositeView.prototype, MixinFromView);
var ClassOptions$5 = ['collectionEvents', 'events', 'modelEvents', 'triggers', 'ui'];
var Behavior = MarionetteObject.extend({
cidPrefix: 'mnb',
constructor: function constructor(options, view) {
// Setup reference to the view.
// this comes in handle when a behavior
// wants to directly talk up the chain
// to the view.
this.view = view;
this.defaults = _.clone(_.result(this, 'defaults', {}));
this._setOptions(this.defaults, options);
this.mergeOptions(this.options, ClassOptions$5);
// Construct an internal UI hash using
// the behaviors UI hash and then the view UI hash.
// This allows the user to use UI hash elements
// defined in the parent view as well as those
// defined in the given behavior.
// This order will help the reuse and share of a behavior
// between multiple views, while letting a view override a
// selector under an UI key.
this.ui = _.extend({}, _.result(this, 'ui'), _.result(view, 'ui'));
MarionetteObject.apply(this, arguments);
},
// proxy behavior $ method to the view
// this is useful for doing jquery DOM lookups
// scoped to behaviors view.
$: function $() {
return this.view.$.apply(this.view, arguments);
},
// Stops the behavior from listening to events.
// Overrides Object#destroy to prevent additional events from being triggered.
destroy: function destroy() {
this.stopListening();
return this;
},
proxyViewProperties: function proxyViewProperties() {
this.$el = this.view.$el;
this.el = this.view.el;
return this;
},
bindUIElements: function bindUIElements() {
this._bindUIElements();
return this;
},
unbindUIElements: function unbindUIElements() {
this._unbindUIElements();
return this;
},
getUI: function getUI(name) {
this.view._ensureViewIsIntact();
return this._getUI(name);
},
// Handle `modelEvents`, and `collectionEvents` configuration
delegateEntityEvents: function delegateEntityEvents() {
this._delegateEntityEvents(this.view.model, this.view.collection);
return this;
},
undelegateEntityEvents: function undelegateEntityEvents() {
this._undelegateEntityEvents(this.view.model, this.view.collection);
return this;
},
getEvents: function getEvents() {
var _this = this;
// Normalize behavior events hash to allow
// a user to use the @ui. syntax.
var behaviorEvents = this.normalizeUIKeys(_.result(this, 'events'));
// binds the handler to the behavior and builds a unique eventName
return _.reduce(behaviorEvents, function (events, behaviorHandler, key) {
if (!_.isFunction(behaviorHandler)) {
behaviorHandler = _this[behaviorHandler];
}
if (!behaviorHandler) {
return;
}
key = getUniqueEventName(key);
events[key] = _.bind(behaviorHandler, _this);
return events;
}, {});
},
// Internal method to build all trigger handlers for a given behavior
getTriggers: function getTriggers() {
if (!this.triggers) {
return;
}
// Normalize behavior triggers hash to allow
// a user to use the @ui. syntax.
var behaviorTriggers = this.normalizeUIKeys(_.result(this, 'triggers'));
return this._getViewTriggers(this.view, behaviorTriggers);
}
});
_.extend(Behavior.prototype, DelegateEntityEventsMixin, TriggersMixin, UIMixin);
var ClassOptions$6 = ['region', 'regionClass'];
// A container for a Marionette application.
var Application = MarionetteObject.extend({
cidPrefix: 'mna',
constructor: function constructor(options) {
this._setOptions(options);
this.mergeOptions(options, ClassOptions$6);
this._initRegion();
MarionetteObject.prototype.constructor.apply(this, arguments);
},
regionClass: Region,
_initRegion: function _initRegion() {
var region = this.region;
if (!region) {
return;
}
var defaults = {
regionClass: this.regionClass
};
this._region = buildRegion(region, defaults);
},
getRegion: function getRegion() {
return this._region;
},
showView: function showView(view) {
var region = this.getRegion();
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
return region.show.apply(region, [view].concat(args));
},
getView: function getView() {
return this.getRegion().currentView;
},
// kick off all of the application's processes.
start: function start(options) {
this.triggerMethod('before:start', this, options);
this.triggerMethod('start', this, options);
return this;
}
});
var ClassOptions$7 = ['appRoutes', 'controller'];
var AppRouter = Backbone.Router.extend({
constructor: function constructor(options) {
this._setOptions(options);
this.mergeOptions(options, ClassOptions$7);
Backbone.Router.apply(this, arguments);
var appRoutes = this.appRoutes;
var controller = this._getController();
this.processAppRoutes(controller, appRoutes);
this.on('route', this._processOnRoute, this);
},
// Similar to route method on a Backbone Router but
// method is called on the controller
appRoute: function appRoute(route, methodName) {
var controller = this._getController();
this._addAppRoute(controller, route, methodName);
return this;
},
// process the route event and trigger the onRoute
// method call, if it exists
_processOnRoute: function _processOnRoute(routeName, routeArgs) {
// make sure an onRoute before trying to call it
if (_.isFunction(this.onRoute)) {
// find the path that matches the current route
var routePath = _.invert(this.appRoutes)[routeName];
this.onRoute(routeName, routePath, routeArgs);
}
},
// Internal method to process the `appRoutes` for the
// router, and turn them in to routes that trigger the
// specified method on the specified `controller`.
processAppRoutes: function processAppRoutes(controller, appRoutes) {
var _this = this;
if (!appRoutes) {
return this;
}
var routeNames = _.keys(appRoutes).reverse(); // Backbone requires reverted order of routes
_.each(routeNames, function (route) {
_this._addAppRoute(controller, route, appRoutes[route]);
});
return this;
},
_getController: function _getController() {
return this.controller;
},
_addAppRoute: function _addAppRoute(controller, route, methodName) {
var method = controller[methodName];
if (!method) {
throw new MarionetteError('Method "' + methodName + '" was not found on the controller');
}
this.route(route, methodName, _.bind(method, controller));
},
triggerMethod: triggerMethod
});
_.extend(AppRouter.prototype, CommonMixin);
// Placeholder method to be extended by the user.
// The method should define the object that stores the behaviors.
// i.e.
//
// ```js
// Marionette.Behaviors.behaviorsLookup: function() {
// return App.Behaviors
// }
// ```
function behaviorsLookup() {
throw new MarionetteError({
message: 'You must define where your behaviors are stored.',
url: 'marionette.behaviors.md#behaviorslookup'
});
}
// Add Feature flags here
// e.g. 'class' => false
var FEATURES = {};
function isEnabled(name) {
return !!FEATURES[name];
}
function setEnabled(name, state) {
return FEATURES[name] = state;
}
var previousMarionette = Backbone.Marionette;
var Marionette = Backbone.Marionette = {};
// This allows you to run multiple instances of Marionette on the same
// webapp. After loading the new version, call `noConflict()` to
// get a reference to it. At the same time the old version will be
// returned to Backbone.Marionette.
Marionette.noConflict = function () {
Backbone.Marionette = previousMarionette;
return this;
};
// Utilities
Marionette.bindEvents = proxy(bindEvents);
Marionette.unbindEvents = proxy(unbindEvents);
Marionette.bindRequests = proxy(bindRequests);
Marionette.unbindRequests = proxy(unbindRequests);
Marionette.mergeOptions = proxy(mergeOptions);
Marionette.getOption = proxy(getOption);
Marionette.normalizeMethods = proxy(normalizeMethods);
Marionette.extend = extend;
Marionette.isNodeAttached = isNodeAttached;
Marionette.deprecate = deprecate;
Marionette.triggerMethod = proxy(triggerMethod);
Marionette.triggerMethodOn = triggerMethodOn;
Marionette.isEnabled = isEnabled;
Marionette.setEnabled = setEnabled;
Marionette.monitorViewEvents = monitorViewEvents;
Marionette.Behaviors = {};
Marionette.Behaviors.behaviorsLookup = behaviorsLookup;
// Classes
Marionette.Application = Application;
Marionette.AppRouter = AppRouter;
Marionette.Renderer = Renderer;
Marionette.TemplateCache = TemplateCache;
Marionette.View = View;
Marionette.CollectionView = CollectionView;
Marionette.CompositeView = CompositeView;
Marionette.Behavior = Behavior;
Marionette.Region = Region;
Marionette.Error = MarionetteError;
Marionette.Object = MarionetteObject;
// Configuration
Marionette.DEV_MODE = false;
Marionette.FEATURES = FEATURES;
Marionette.VERSION = version;
return Marionette;
}));
//# sourceMappingURL=backbone.marionette.js.map