backbone.marionette
Version:
The Backbone Framework
1,792 lines (1,459 loc) • 85.6 kB
JavaScript
import Backbone from 'backbone';
import _ from 'underscore';
import Radio from 'backbone.radio';
var version = "4.1.3";
//Internal utility for creating context style global utils
var proxy = function proxy(method) {
return function (context) {
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
return method.apply(context, args);
};
};
// Marionette.extend
var extend = Backbone.Model.extend;
// ----------------------
// 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;
if (!hash) {
return;
}
return _.reduce(hash, function (normalizedHash, method, name) {
if (!_.isFunction(method)) {
method = _this[method];
}
if (method) {
normalizedHash[name] = method;
}
return normalizedHash;
}, {});
};
// Error
var errorProps = ['description', 'fileName', 'lineNumber', 'name', 'message', 'number', 'url'];
var MarionetteError = extend.call(Error, {
urlRoot: "http://marionettejs.com/docs/v".concat(version, "/"),
url: '',
constructor: function constructor(options) {
var error = Error.call(this, options.message);
_.extend(this, _.pick(error, errorProps), _.pick(options, errorProps));
if (Error.captureStackTrace) {
this.captureStackTrace();
}
this.url = this.urlRoot + this.url;
},
captureStackTrace: function captureStackTrace() {
Error.captureStackTrace(this, MarionetteError);
},
toString: function toString() {
return "".concat(this.name, ": ").concat(this.message, " See: ").concat(this.url);
}
});
// Bind Entity Events & Unbind Entity Events
function normalizeBindings(context, bindings) {
if (!_.isObject(bindings)) {
throw new MarionetteError({
message: 'Bindings must be an object.',
url: 'common.html#bindevents'
});
}
return normalizeMethods.call(context, bindings);
}
function bindEvents(entity, bindings) {
if (!entity || !bindings) {
return this;
}
this.listenTo(entity, normalizeBindings(this, bindings));
return this;
}
function unbindEvents(entity, bindings) {
if (!entity) {
return this;
}
if (!bindings) {
this.stopListening(entity);
return this;
}
this.stopListening(entity, normalizeBindings(this, bindings));
return this;
} // Export Public API
// Bind/Unbind Radio Requests
function normalizeBindings$1(context, bindings) {
if (!_.isObject(bindings)) {
throw new MarionetteError({
message: 'Bindings must be an object.',
url: 'common.html#bindrequests'
});
}
return normalizeMethods.call(context, bindings);
}
function bindRequests(channel, bindings) {
if (!channel || !bindings) {
return this;
}
channel.reply(normalizeBindings$1(this, bindings), this);
return this;
}
function unbindRequests(channel, bindings) {
if (!channel) {
return this;
}
if (!bindings) {
channel.stopReplying(null, null, this);
return this;
}
channel.stopReplying(normalizeBindings$1(this, bindings), this);
return this;
}
// 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];
}
};
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;
}
});
};
// DOM Refresh
function triggerMethodChildren(view, event, shouldTrigger) {
if (!view._getImmediateChildren) {
return;
}
_.each(view._getImmediateChildren(), function (child) {
if (!shouldTrigger(child)) {
return;
}
child.triggerMethod(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) {
view._isAttached = false;
return true;
}
function triggerDOMRefresh(view) {
if (view._isAttached && view._isRendered) {
view.triggerMethod('dom:refresh', view);
}
}
function triggerDOMRemove(view) {
if (view._isAttached && view._isRendered) {
view.triggerMethod('dom:remove', view);
}
}
function handleBeforeAttach() {
triggerMethodChildren(this, 'before:attach', shouldTriggerAttach);
}
function handleAttach() {
triggerMethodChildren(this, 'attach', shouldAttach);
triggerDOMRefresh(this);
}
function handleBeforeDetach() {
triggerMethodChildren(this, 'before:detach', shouldTriggerDetach);
triggerDOMRemove(this);
}
function handleDetach() {
triggerMethodChildren(this, 'detach', shouldDetach);
}
function handleBeforeRender() {
triggerDOMRemove(this);
}
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 || view.monitorViewEvents === false) {
return;
}
view._areViewEventsMonitored = true;
view.on({
'before:attach': handleBeforeAttach,
'attach': handleAttach,
'before:detach': handleBeforeDetach,
'detach': handleDetach,
'before:render': handleBeforeRender,
'render': handleRender
});
}
// Trigger Method
var splitter = /(^|:)(\w)/gi; // Only calc getOnMethodName once
var methodCache = {}; // 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 = function getOnMethodName(event) {
if (!methodCache[event]) {
methodCache[event] = 'on' + event.replace(splitter, getEventName);
}
return methodCache[event];
}; // 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) {
// get the method name from the event name
var methodName = getOnMethodName(event);
var method = getOption.call(this, methodName);
var result; // call the onMethodName if it exists
if (_.isFunction(method)) {
// pass all args, except the event name
result = method.apply(this, _.drop(arguments));
} // trigger the event
this.trigger.apply(this, arguments);
return result;
}
var Events = {
triggerMethod: triggerMethod
};
var CommonMixin = {
// Imports the "normalizeMethods" to transform hashes of
// events=>function references/names to a hash of events=>function references
normalizeMethods: normalizeMethods,
_setOptions: function _setOptions(options, classOptions) {
this.options = _.extend({}, _.result(this, 'options'), options);
this.mergeOptions(options, classOptions);
},
// 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,
// Enable binding view's requests.
bindRequests: bindRequests,
// Enable unbinding view's requests.
unbindRequests: unbindRequests,
triggerMethod: triggerMethod
};
_.extend(CommonMixin, Backbone.Events);
var DestroyMixin = {
_isDestroyed: false,
isDestroyed: function isDestroyed() {
return this._isDestroyed;
},
destroy: function destroy(options) {
if (this._isDestroyed) {
return this;
}
this.triggerMethod('before:destroy', this, options);
this._isDestroyed = true;
this.triggerMethod('destroy', this, options);
this.stopListening();
return this;
}
};
// - channelName
// - radioEvents
// - radioRequests
var RadioMixin = {
_initRadio: function _initRadio() {
var channelName = _.result(this, 'channelName');
if (!channelName) {
return;
}
/* istanbul ignore next */
if (!Radio) {
throw new MarionetteError({
message: 'The dependency "backbone.radio" is missing.',
url: 'backbone.radio.html#marionette-integration'
});
}
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;
}
};
// Object
var ClassOptions = ['channelName', 'radioEvents', 'radioRequests']; // Object borrows many conventions and utilities from Backbone.
var MarionetteObject = function MarionetteObject(options) {
this._setOptions(options, ClassOptions);
this.cid = _.uniqueId(this.cidPrefix);
this._initRadio();
this.initialize.apply(this, arguments);
};
MarionetteObject.extend = extend; // Object Methods
// --------------
_.extend(MarionetteObject.prototype, CommonMixin, DestroyMixin, RadioMixin, {
cidPrefix: 'mno',
// This is a noop method intended to be overridden
initialize: function initialize() {}
});
// Implementation of the invoke method (http://underscorejs.org/#invoke) with support for
var _invoke = _.invokeMap || _.invoke;
// - 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 an error is thrown
function getBehaviorClass(options) {
if (options.behaviorClass) {
return {
BehaviorClass: options.behaviorClass,
options: options
};
} //treat functions as a Behavior constructor
if (_.isFunction(options)) {
return {
BehaviorClass: options,
options: {}
};
}
throw new MarionetteError({
message: 'Unable to get behavior class. A Behavior constructor should be passed directly or as behaviorClass property of options',
url: 'marionette.behavior.html#defining-and-attaching-behaviors'
});
} // 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, allBehaviors) {
return _.reduce(behaviors, function (reducedBehaviors, behaviorDefiniton) {
var _getBehaviorClass = getBehaviorClass(behaviorDefiniton),
BehaviorClass = _getBehaviorClass.BehaviorClass,
options = _getBehaviorClass.options;
var behavior = new BehaviorClass(options, view);
reducedBehaviors.push(behavior);
return parseBehaviors(view, _.result(behavior, 'behaviors'), reducedBehaviors);
}, allBehaviors);
}
var BehaviorsMixin = {
_initBehaviors: function _initBehaviors() {
this._behaviors = parseBehaviors(this, _.result(this, 'behaviors'), []);
},
_getBehaviorTriggers: function _getBehaviorTriggers() {
var triggers = _invoke(this._behaviors, '_getTriggers');
return _.reduce(triggers, function (memo, _triggers) {
return _.extend(memo, _triggers);
}, {});
},
_getBehaviorEvents: function _getBehaviorEvents() {
var events = _invoke(this._behaviors, '_getEvents');
return _.reduce(events, function (memo, _events) {
return _.extend(memo, _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(options) {
// Call destroy on each behavior after
// destroying the view.
// This unbinds event listeners
// that behaviors have registered for.
_invoke(this._behaviors, 'destroy', options);
},
// Remove a behavior
_removeBehavior: function _removeBehavior(behavior) {
// Don't worry about the clean up if the view is destroyed
if (this._isDestroyed) {
return;
} // Remove behavior-only triggers and events
this.undelegate(".trig".concat(behavior.cid, " .").concat(behavior.cid));
this._behaviors = _.without(this._behaviors, behavior);
},
_bindBehaviorUIElements: function _bindBehaviorUIElements() {
_invoke(this._behaviors, 'bindUIElements');
},
_unbindBehaviorUIElements: function _unbindBehaviorUIElements() {
_invoke(this._behaviors, 'unbindUIElements');
},
_triggerEventOnBehaviors: function _triggerEventOnBehaviors(eventName, view, options) {
_invoke(this._behaviors, 'triggerMethod', eventName, view, options);
}
};
// - collectionEvents
// - modelEvents
var DelegateEntityEventsMixin = {
// Handle `modelEvents`, and `collectionEvents` configuration
_delegateEntityEvents: function _delegateEntityEvents(model, collection) {
if (model) {
this._modelEvents = _.result(this, 'modelEvents');
this.bindEvents(model, this._modelEvents);
}
if (collection) {
this._collectionEvents = _.result(this, 'collectionEvents');
this.bindEvents(collection, this._collectionEvents);
}
},
// Remove any previously delegate entity events
_undelegateEntityEvents: function _undelegateEntityEvents(model, collection) {
if (this._modelEvents) {
this.unbindEvents(model, this._modelEvents);
delete this._modelEvents;
}
if (this._collectionEvents) {
this.unbindEvents(collection, this._collectionEvents);
delete this._collectionEvents;
}
},
// Remove cached event handlers
_deleteEntityEventHandlers: function _deleteEntityEventHandlers() {
delete this._modelEvents;
delete this._collectionEvents;
}
};
// - template
// - templateContext
var TemplateRenderMixin = {
// Internal method to render the template with the serialized data
// and template context
_renderTemplate: function _renderTemplate(template) {
// Add in entity data and template context
var data = this.mixinTemplateContext(this.serializeData()) || {}; // Render and add to el
var html = this._renderHtml(template, data);
if (typeof html !== 'undefined') {
this.attachElContent(html);
}
},
// Get the template for this view instance.
// You can set a `template` attribute in the view definition
// or pass a `template: TemplateFunction` 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(serializedData) {
var templateContext = _.result(this, 'templateContext');
if (!templateContext) {
return serializedData;
}
if (!serializedData) {
return templateContext;
}
return _.extend({}, serializedData, templateContext);
},
// Serialize the view's model *or* collection, if
// it exists, for the template
serializeData: function serializeData() {
// 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
if (this.collection) {
return {
items: this.serializeCollection()
};
}
},
// Prepares the special `model` property of a view
// for being displayed in the template. Override this if
// you need a custom transformation for your view's model
serializeModel: function serializeModel() {
return this.model.attributes;
},
// Serialize a collection
serializeCollection: function serializeCollection() {
return _.map(this.collection.models, function (model) {
return model.attributes;
});
},
// Renders the data into the template
_renderHtml: function _renderHtml(template, data) {
return template(data);
},
// 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;
// }
// ```
attachElContent: function attachElContent(html) {
this.Dom.setContents(this.el, html, this.$el);
}
};
// Borrow event splitter from Backbone
var delegateEventSplitter = /^(\S+)\s*(.*)$/; // Set event name to be namespaced using a unique index
// to generate a non colliding event namespace
// http://api.jquery.com/event.namespace/
var getNamespacedEventName = function getNamespacedEventName(eventName, namespace) {
var match = eventName.match(delegateEventSplitter);
return "".concat(match[1], ".").concat(namespace, " ").concat(match[2]);
};
// Add Feature flags here
// e.g. 'class' => false
var FEATURES = {
childViewEventPrefix: false,
triggersStopPropagation: true,
triggersPreventDefault: true,
DEV_MODE: false
};
function isEnabled(name) {
return !!FEATURES[name];
}
function setEnabled(name, state) {
return FEATURES[name] = state;
}
// 'click:foo'
function buildViewTrigger(view, triggerDef) {
if (_.isString(triggerDef)) {
triggerDef = {
event: triggerDef
};
}
var eventName = triggerDef.event;
var shouldPreventDefault = !!triggerDef.preventDefault;
if (isEnabled('triggersPreventDefault')) {
shouldPreventDefault = triggerDef.preventDefault !== false;
}
var shouldStopPropagation = !!triggerDef.stopPropagation;
if (isEnabled('triggersStopPropagation')) {
shouldStopPropagation = triggerDef.stopPropagation !== false;
}
return function (event) {
if (shouldPreventDefault) {
event.preventDefault();
}
if (shouldStopPropagation) {
event.stopPropagation();
}
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
view.triggerMethod.apply(view, [eventName, view, event].concat(args));
};
}
var TriggersMixin = {
// Configure `triggers` to forward DOM events to view
// events. `triggers: {"click .foo": "do:foo"}`
_getViewTriggers: function _getViewTriggers(view, triggers) {
var _this = this;
// Configure the triggers, prevent default
// action and stop propagation of DOM events
return _.reduce(triggers, function (events, value, key) {
key = getNamespacedEventName(key, "trig".concat(_this.cid));
events[key] = buildViewTrigger(view, value);
return events;
}, {});
}
};
// 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;
}, {});
};
var uiRegEx = /@ui\.[a-zA-Z-_$0-9]*/g; // utility method for parsing @ui. syntax strings
// into associated selector
var _normalizeUIString = function normalizeUIString(uiString, ui) {
return uiString.replace(uiRegEx, 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, property) {
_.each(hash, function (val, key) {
if (_.isString(val)) {
hash[key] = _normalizeUIString(val, ui);
} else if (val) {
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, property) {
var uiBindings = this._getUIBindings();
return _normalizeUIValues(hash, uiBindings, property);
},
_getUIBindings: function _getUIBindings() {
var uiBindings = _.result(this, '_uiBindings');
return uiBindings || _.result(this, '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];
}
};
// DomApi
function _getEl(el) {
return el instanceof Backbone.$ ? el : Backbone.$(el);
} // Static setter
function setDomApi(mixin) {
this.prototype.Dom = _.extend({}, this.prototype.Dom, mixin);
return this;
}
var DomApi = {
// Returns a new HTML DOM node instance
createBuffer: function createBuffer() {
return document.createDocumentFragment();
},
// Returns the document element for a given DOM element
getDocumentEl: function getDocumentEl(el) {
return el.ownerDocument.documentElement;
},
// Lookup the `selector` string
// Selector may also be a DOM element
// Returns an array-like object of nodes
getEl: function getEl(selector) {
return _getEl(selector);
},
// Finds the `selector` string with the el
// Returns an array-like object of nodes
findEl: function findEl(el, selector) {
return _getEl(el).find(selector);
},
// Returns true if the el contains the node childEl
hasEl: function hasEl(el, childEl) {
return el.contains(childEl && childEl.parentNode);
},
// Detach `el` from the DOM without removing listeners
detachEl: function detachEl(el) {
var _$el = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _getEl(el);
_$el.detach();
},
// Remove `oldEl` from the DOM and put `newEl` in its place
replaceEl: function replaceEl(newEl, oldEl) {
if (newEl === oldEl) {
return;
}
var parent = oldEl.parentNode;
if (!parent) {
return;
}
parent.replaceChild(newEl, oldEl);
},
// Swaps the location of `el1` and `el2` in the DOM
swapEl: function swapEl(el1, el2) {
if (el1 === el2) {
return;
}
var parent1 = el1.parentNode;
var parent2 = el2.parentNode;
if (!parent1 || !parent2) {
return;
}
var next1 = el1.nextSibling;
var next2 = el2.nextSibling;
parent1.insertBefore(el2, next1);
parent2.insertBefore(el1, next2);
},
// Replace the contents of `el` with the HTML string of `html`
setContents: function setContents(el, html) {
var _$el = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : _getEl(el);
_$el.html(html);
},
// Takes the DOM node `el` and appends the DOM node `contents`
// to the end of the element's contents.
appendContents: function appendContents(el, contents) {
var _ref = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
_ref$_$el = _ref._$el,
_$el = _ref$_$el === void 0 ? _getEl(el) : _ref$_$el,
_ref$_$contents = _ref._$contents,
_$contents = _ref$_$contents === void 0 ? _getEl(contents) : _ref$_$contents;
_$el.append(_$contents);
},
// Does the el have child nodes
hasContents: function hasContents(el) {
return !!el && el.hasChildNodes();
},
// Remove the inner contents of `el` from the DOM while leaving
// `el` itself in the DOM.
detachContents: function detachContents(el) {
var _$el = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _getEl(el);
_$el.contents().detach();
}
};
// ViewMixin
// - behaviors
// - childViewEventPrefix
// - childViewEvents
// - childViewTriggers
// - collectionEvents
// - modelEvents
// - triggers
// - ui
var ViewMixin = {
Dom: DomApi,
_isElAttached: function _isElAttached() {
return !!this.el && this.Dom.hasEl(this.Dom.getDocumentEl(this.el), this.el);
},
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(events) {
this._proxyBehaviorViewProperties();
this._buildEventProxies();
var combinedEvents = _.extend({}, this._getBehaviorEvents(), this._getEvents(events), this._getBehaviorTriggers(), this._getTriggers());
Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
return this;
},
// Allows Backbone.View events to utilize `@ui.` selectors
_getEvents: function _getEvents(events) {
if (events) {
return this.normalizeUIKeys(events);
}
if (!this.events) {
return;
}
return this.normalizeUIKeys(_.result(this, '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;
},
// Handle destroying the view and its children.
destroy: function destroy(options) {
if (this._isDestroyed || this._isDestroying) {
return this;
}
this._isDestroying = true;
var shouldTriggerDetach = this._isAttached && !this._disableDetachEvents;
this.triggerMethod('before:destroy', this, options);
if (shouldTriggerDetach) {
this.triggerMethod('before:detach', this);
} // unbind UI elements
this.unbindUIElements(); // remove the view from the DOM
this._removeElement();
if (shouldTriggerDetach) {
this._isAttached = false;
this.triggerMethod('detach', this);
} // remove children after the remove to prevent extra paints
this._removeChildren();
this._isDestroyed = true;
this._isRendered = false; // Destroy behaviors after _isDestroyed flag
this._destroyBehaviors(options);
this._deleteEntityEventHandlers();
this.triggerMethod('destroy', this, options);
this._triggerEventOnBehaviors('destroy', this, options);
this.stopListening();
return this;
},
// Equates to this.$el.remove
_removeElement: function _removeElement() {
this.$el.off().removeData();
this.Dom.detachEl(this.el, this.$el);
},
// This method binds the elements specified in the "ui" hash
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) {
return this._getUI(name);
},
// Cache `childViewEvents` and `childViewTriggers`
_buildEventProxies: function _buildEventProxies() {
this._childViewEvents = this.normalizeMethods(_.result(this, 'childViewEvents'));
this._childViewTriggers = _.result(this, 'childViewTriggers');
this._eventPrefix = this._getEventPrefix();
},
_getEventPrefix: function _getEventPrefix() {
var defaultPrefix = isEnabled('childViewEventPrefix') ? 'childview' : false;
var prefix = _.result(this, 'childViewEventPrefix', defaultPrefix);
return prefix === false ? prefix : prefix + ':';
},
_proxyChildViewEvents: function _proxyChildViewEvents(view) {
if (this._childViewEvents || this._childViewTriggers || this._eventPrefix) {
this.listenTo(view, 'all', this._childViewEventHandler);
}
},
_childViewEventHandler: function _childViewEventHandler(eventName) {
var childViewEvents = this._childViewEvents; // call collectionView childViewEvent if defined
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
if (childViewEvents && 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 && childViewTriggers[eventName]) {
this.triggerMethod.apply(this, [childViewTriggers[eventName]].concat(args));
}
if (this._eventPrefix) {
this.triggerMethod.apply(this, [this._eventPrefix + eventName].concat(args));
}
}
};
_.extend(ViewMixin, BehaviorsMixin, CommonMixin, DelegateEntityEventsMixin, TemplateRenderMixin, TriggersMixin, UIMixin);
function renderView(view) {
if (view._isRendered) {
return;
}
if (!view.supportsRenderLifecycle) {
view.triggerMethod('before:render', view);
}
view.render();
view._isRendered = true;
if (!view.supportsRenderLifecycle) {
view.triggerMethod('render', view);
}
}
function destroyView(view, disableDetachEvents) {
if (view.destroy) {
// Attach flag for public destroy function internal check
view._disableDetachEvents = disableDetachEvents;
view.destroy();
return;
} // Destroy for non-Marionette Views
if (!view.supportsDestroyLifecycle) {
view.triggerMethod('before:destroy', view);
}
var shouldTriggerDetach = view._isAttached && !disableDetachEvents;
if (shouldTriggerDetach) {
view.triggerMethod('before:detach', view);
}
view.remove();
if (shouldTriggerDetach) {
view._isAttached = false;
view.triggerMethod('detach', view);
}
view._isDestroyed = true;
if (!view.supportsDestroyLifecycle) {
view.triggerMethod('destroy', view);
}
}
// Region
var classErrorName = 'RegionError';
var ClassOptions$1 = ['allowMissingEl', 'parentEl', 'replaceElement'];
var Region = function Region(options) {
this._setOptions(options, ClassOptions$1);
this.cid = _.uniqueId(this.cidPrefix); // 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;
this.$el = this._getEl(this.el);
this.initialize.apply(this, arguments);
};
Region.extend = extend;
Region.setDomApi = setDomApi; // Region Methods
// --------------
_.extend(Region.prototype, CommonMixin, {
Dom: DomApi,
cidPrefix: 'mnr',
replaceElement: false,
_isReplaced: false,
_isSwappingView: false,
// This is a noop method intended to be overridden
initialize: function initialize() {},
// Displays a view instance inside of the region. If necessary handles calling the `render`
// method for you. Reads content directly from the `el` attribute.
show: function show(view, options) {
if (!this._ensureElement(options)) {
return;
}
view = this._getView(view, options);
if (view === this.currentView) {
return this;
}
if (view._isShown) {
throw new MarionetteError({
name: classErrorName,
message: 'View is already shown in a Region or CollectionView',
url: 'marionette.region.html#showing-a-view'
});
}
this._isSwappingView = !!this.currentView;
this.triggerMethod('before:show', this, view, options); // Assume an attached view is already in the region for pre-existing DOM
if (this.currentView || !view._isAttached) {
this.empty(options);
}
this._setupChildView(view);
this.currentView = view;
renderView(view);
this._attachView(view, options);
this.triggerMethod('show', this, view, options);
this._isSwappingView = false;
return this;
},
_getEl: function _getEl(el) {
if (!el) {
throw new MarionetteError({
name: classErrorName,
message: 'An "el" must be specified for a region.',
url: 'marionette.region.html#additional-options'
});
}
return this.getEl(el);
},
_setEl: function _setEl() {
this.$el = this._getEl(this.el);
if (this.$el.length) {
this.el = this.$el[0];
} // Make sure the $el contains only the el
if (this.$el.length > 1) {
this.$el = this.Dom.getEl(this.el);
}
},
// Set the `el` of the region and move any current view to the new `el`.
_setElement: function _setElement(el) {
if (el === this.el) {
return this;
}
var shouldReplace = this._isReplaced;
this._restoreEl();
this.el = el;
this._setEl();
if (this.currentView) {
var view = this.currentView;
if (shouldReplace) {
this._replaceEl(view);
} else {
this.attachHtml(view);
}
}
return this;
},
_setupChildView: function _setupChildView(view) {
monitorViewEvents(view);
this._proxyChildViewEvents(view); // 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);
},
_proxyChildViewEvents: function _proxyChildViewEvents(view) {
var parentView = this._parentView;
if (!parentView) {
return;
}
parentView._proxyChildViewEvents(view);
},
// If the regions parent view is not monitoring its attach/detach events
_shouldDisableMonitoring: function _shouldDisableMonitoring() {
return this._parentView && this._parentView.monitorViewEvents === false;
},
_isElAttached: function _isElAttached() {
return this.Dom.hasEl(this.Dom.getDocumentEl(this.el), this.el);
},
_attachView: function _attachView(view) {
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
replaceElement = _ref.replaceElement;
var shouldTriggerAttach = !view._isAttached && this._isElAttached() && !this._shouldDisableMonitoring();
var shouldReplaceEl = typeof replaceElement === 'undefined' ? !!_.result(this, 'replaceElement') : !!replaceElement;
if (shouldTriggerAttach) {
view.triggerMethod('before:attach', view);
}
if (shouldReplaceEl) {
this._replaceEl(view);
} else {
this.attachHtml(view);
}
if (shouldTriggerAttach) {
view._isAttached = true;
view.triggerMethod('attach', view);
} // Corresponds that view is shown in a marionette Region or CollectionView
view._isShown = true;
},
_ensureElement: function _ensureElement() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
if (!_.isObject(this.el)) {
this._setEl();
}
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({
name: classErrorName,
message: "An \"el\" must exist in DOM for this region ".concat(this.cid),
url: 'marionette.region.html#additional-options'
});
}
}
return true;
},
_getView: function _getView(view) {
if (!view) {
throw new MarionetteError({
name: classErrorName,
message: 'The view passed is undefined and therefore invalid. You must pass a view instance to show.',
url: 'marionette.region.html#showing-a-view'
});
}
if (view._isDestroyed) {
throw new MarionetteError({
name: classErrorName,
message: "View (cid: \"".concat(view.cid, "\") has already been destroyed and cannot be used."),
url: 'marionette.region.html#showing-a-view'
});
}
if (view instanceof Backbone.View) {
return view;
}
var viewOptions = this._getViewOptions(view);
return new View(viewOptions);
},
// This allows for a template or a static string to be
// used as a template
_getViewOptions: function _getViewOptions(viewOptions) {
if (_.isFunction(viewOptions)) {
return {
template: viewOptions
};
}
if (_.isObject(viewOptions)) {
return viewOptions;
}
var template = function template() {
return viewOptions;
};
return {
template: template
};
},
// 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) {
var context = _.result(this, 'parentEl');
if (context && _.isString(el)) {
return this.Dom.findEl(context, el);
}
return this.Dom.getEl(el);
},
_replaceEl: function _replaceEl(view) {
// Always restore the el to ensure the regions el is present before replacing
this._restoreEl();
view.on('before:destroy', this._restoreEl, this);
this.Dom.replaceEl(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;
}
this._detachView(view);
this._isReplaced = false;
},
// Check to see if the region's el was replaced.
isReplaced: function isReplaced() {
return !!this._isReplaced;
},
// Check to see if a view is being swapped by another
isSwappingView: function isSwappingView() {
return !!this._isSwappingView;
},
// Override this method to change how the new view is appended to the `$el` that the
// region is managing
attachHtml: function attachHtml(view) {
this.Dom.appendContents(this.el, view.el, {
_$el: this.$el,
_$contents: view.$el
});
},
// Destroy the current view, if there is one. If there is no current view,
// it will detach any html inside the region's `el`.
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;
}
this._empty(view, true);
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) {
if (shouldDestroy) {
this.removeView(view);
} else {
this._detachView(view);
}
view._isShown = false;
this._stopChildViewEvents(view);
}
this.triggerMethod('empty', this, view);
},
_stopChildViewEvents: function _stopChildViewEvents(view) {
var parentView = this._parentView;
if (!parentView) {
return;
}
this._parentView.stopListening(view);
},
// Non-Marionette safe view.destroy
destroyView: function destroyView$1(view) {
if (view._isDestroyed) {
return view;
}
destroyView(view, this._shouldDisableMonitoring());
return view;
},
// Override this method to determine what happens when the view
// is removed from the region when the view is not being detached
removeView: function removeView(view) {
this.destroyView(view);
},
// Empties the Region without destroying the view
// Returns the detached view
detachView: function detachView() {
var view = this.currentView;
if (!view) {
return;
}
this._empty(view);
return view;
},
_detachView: function _detachView(view) {
var shouldTriggerDetach = view._isAttached && !this._shouldDisableMonitoring();
var shouldRestoreEl = this._isReplaced;
if (shouldTriggerDetach) {
view.triggerMethod('before:detach', view);
}
if (shouldRestoreEl) {
this.Dom.replaceEl(this.el, view.el);
} else {
this.detachHtml();
}
if (shouldTriggerDetach) {
view._isAttached = false;
view.triggerMethod('detach', view);
}
},
// Override this method to change how the region detaches current content
detachHtml: function detachHtml() {
this.Dom.detachContents(this.el, this.$el);
},
// 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);
this.el = this._initEl;
delete this.$el;
return this;
},
_isDestroyed: false,
isDestroyed: function isDestroyed() {
return this._isDestroyed;
},
// Destroy the region, remove any child view
// and remove the region from any associated view
destroy: function destroy(options) {
if (this._isDestroyed) {
return this;
}
this.triggerMethod('before:destroy', this, options);
this._isDestroyed = true;
this.reset(options);
if (this._name) {
this._parentView._removeReferences(this._name);
}
delete this._parentView;
delete this._name;
this.triggerMethod('destroy', this, options);
this.stopListening();
return this;
}
});
function buildRegion (definition, defaults) {
if (definition instanceof Region) {
return definition;
}
if (_.isString(definition)) {
return buildRegionFromObject(defaults, {
el: definition
});
}
if (_.isFunction(definition)) {
return buildRegionFromObject(defaults, {
regionClass: definition
});
}
if (_.isObject(definition)) {
return buildRegionFromObject(defaults, definition);
}
throw new MarionetteError({
message: 'Improper region configuration type.',
url: 'marionette.region.html#defining-regions'
});
}
function buildRegionFromObject(defaults, definition) {
var options = _.extend({}, defaults, definition);
var RegionClass = options.regionClass;
delete options.regionClass;
return new RegionClass(options);
}
// - 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, '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._parentView = this;
region._name = name;
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, this._removeRegion.bind(this));
return regions;
},
_removeRegion: function _removeRegion(region, name) {
this.triggerMethod('before:remove:region', this, name, region);
region.destroy();
this.triggerMethod('remove:region', this, name, region);
},
// Called in a region's destroy
_removeReferences: function _removeReferences(name) {
delete this.regions[name];
delete this._regions[name];
},
// 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) {
if (!this._isRendered) {
this.render();
}
return this._regions[name];
},
_getRegions: function _getRegions() {
return _.clone(this._regions);
},
// Get all regions
getRegions: function getRegions() {
if (!this._isRendered) {
this.render();
}
return this._getRegions();
},
showChildView: function showChildView(name, view, options) {
var region = this.getRegion(name);
region.show(view, options);
return view;
},
detachChildView: function detachChildView(name) {
return this.getRegion(name).detachView();
},
getChildView: function getChildView(name) {
return this.getRegion