Skip to content

Instantly share code, notes, and snippets.

@paulfalgout
Created June 20, 2017 06:26
Show Gist options
  • Save paulfalgout/3d0bbe1e2a4a82e15efc428636b18506 to your computer and use it in GitHub Desktop.
Save paulfalgout/3d0bbe1e2a4a82e15efc428636b18506 to your computer and use it in GitHub Desktop.
Example of rollup 0.43 build
// MarionetteJS (Backbone.Marionette)
// ----------------------------------
// v3.3.1
//
// Copyright (c)2017 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 = factory(global.Backbone,global._,global.Backbone.Radio));
}(this, (function (Backbone,_,Radio) { 'use strict';
Backbone = Backbone && 'default' in Backbone ? Backbone['default'] : Backbone;
_ = _ && 'default' in _ ? _['default'] : _;
Radio = Radio && 'default' in Radio ? Radio['default'] : Radio;
var version = "3.3.1";
//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);
};
};
// Marionette.extend
// -----------------
// Borrow the Backbone `extend` method so we can use it as needed
var extend = Backbone.Model.extend;
/* global console */
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 = {};
// Marionette.isNodeAttached
// -------------------------
// Determine if `el` is a child of the document
var isNodeAttached = function isNodeAttached(el) {
return document.documentElement.contains(el && el.parentNode);
};
// 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;
}, {});
};
// Trigger Method
// --------------
// 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);
}
// DOM Refresh
// -----------
// 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 triggerDOMRemove(view) {
if (view._isAttached && view._isRendered) {
triggerMethodOn(view, '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) {
return;
}
view._areViewEventsMonitored = true;
view.on({
'before:attach': handleBeforeAttach,
'attach': handleAttach,
'before:detach': handleBeforeDetach,
'detach': handleDetach,
'before:render': handleBeforeRender,
'render': handleRender
});
}
// Error
// -----
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 Entity Events & Unbind Entity Events
// -----------------------------------------
//
// These methods are used to bind/unbind a backbone "entity" (e.g. collection/model)
// to methods on a target object.
//
// The first parameter, `target`, must have the Backbone.Events module mixed in.
//
// The second parameter is the `entity` (Backbone.Model, Backbone.Collection or
// any object that has Backbone.Events mixed in) to bind the events from.
//
// The third parameter is a hash of { "event:name": "eventHandler" }
// configuration. Multiple handlers can be separated by a space. A
// function can be supplied instead of a string handler name.
// 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;
}
// Bind/Unbind Radio Requests
// -----------------------------------------
//
// These methods are used to bind/unbind a backbone.radio request
// to methods on a target object.
//
// The first parameter, `target`, will set the context of the reply method
//
// The second parameter is the `Radio.channel` to bind the reply to.
//
// The third parameter is a hash of { "request:name": "replyHandler" }
// configuration. A function can be supplied instead of a string handler name.
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(options) {
this.options = _.extend({}, _.result(this, 'options'), options);
};
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
};
// Object
// ------
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) {
if (!this.hasOwnProperty('options')) {
this._setOptions(options);
}
this.mergeOptions(options, ClassOptions);
this._setCid();
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() {},
_setCid: function _setCid() {
if (this.cid) {
return;
}
this.cid = _.uniqueId(this.cidPrefix);
},
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
});
// Template Cache
// --------------
// 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);
}
});
// Implementation of the invoke method (http://underscorejs.org/#invoke) with support for
// lodash v3, v4, and underscore.js
var _invoke = _.invokeMap || _.invoke;
// 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() {
this._behaviors = this._getBehaviors();
},
_getBehaviors: function _getBehaviors() {
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.
return _.isObject(behaviors) ? parseBehaviors(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() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
// 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(args));
},
// Remove a behavior
_removeBehavior: function _removeBehavior(behavior) {
// Don't worry about the clean up if the view is destroyed
if (this._isDestroyed) {
return;
}
this._behaviors = _.without(this._behaviors, behavior);
},
_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]);
};
// Add Feature flags here
// e.g. 'class' => false
var FEATURES = {
childViewEventPrefix: true,
triggersStopPropagation: true,
triggersPreventDefault: true
};
function isEnabled(name) {
return !!FEATURES[name];
}
function setEnabled(name, state) {
return FEATURES[name] = state;
}
// 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;
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();
}
view.triggerMethod(eventName, view, event);
};
}
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];
}
};
// DomApi
// ---------
// Performant method for returning the jQuery instance
function _getEl(el) {
return el instanceof Backbone.$ ? el : Backbone.$(el);
}
// Static setter
function setDomApi(mixin) {
this.prototype.Dom = _.extend({}, this.prototype.Dom, mixin);
}
var DomApi = {
// Returns a new HTML DOM node instance
createBuffer: function createBuffer() {
return document.createDocumentFragment();
},
// 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) {
var _$el = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : _getEl(el);
return _$el.find(selector);
},
// 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);
},
// 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 === undefined ? _getEl(el) : _ref$_$el,
_ref$_$contents = _ref._$contents,
_$contents = _ref$_$contents === undefined ? _getEl(contents) : _ref$_$contents;
_$el.append(_$contents);
},
// 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
// ---------
// MixinOptions
// - behaviors
// - childViewEventPrefix
// - childViewEvents
// - childViewTriggers
// - collectionEvents
// - modelEvents
// - triggers
// - ui
var ViewMixin = {
Dom: DomApi,
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;
},
// 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
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.apply(this, args);
this.triggerMethod.apply(this, ['destroy', this].concat(args));
this.stopListening();
return this;
},
// Equates to this.$el.remove
_removeElement: function _removeElement() {
this.$el.off().removeData();
this.Dom.detachEl(this.el, this.$el);
},
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);
},
// used as the prefix for child view events
// that are forwarded through the layoutview
childViewEventPrefix: function childViewEventPrefix() {
return isEnabled('childViewEventPrefix') ? 'childview' : false;
},
// import the `triggerMethod` to trigger events with corresponding
// methods if the method exists
triggerMethod: function triggerMethod$$1() {
var ret = triggerMethod.apply(this, arguments);
this._triggerEventOnBehaviors.apply(this, arguments);
return ret;
},
// Cache `childViewEvents` and `childViewTriggers`
_buildEventProxies: function _buildEventProxies() {
this._childViewEvents = _.result(this, 'childViewEvents');
this._childViewTriggers = _.result(this, 'childViewTriggers');
},
_proxyChildViewEvents: function _proxyChildViewEvents(view) {
this.listenTo(view, 'all', this._childViewEventHandler);
},
_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 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);
}
}
function destroyView(view) {
if (view.destroy) {
view.destroy();
return;
}
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);
}
}
// Region
// ------
var ClassOptions$2 = ['allowMissingEl', 'parentEl', 'replaceElement'];
var Region = MarionetteObject.extend({
Dom: DomApi,
cidPrefix: 'mnr',
replaceElement: false,
_isReplaced: false,
_isSwappingView: 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;
}
view = this._getView(view, options);
if (view === this.currentView) {
return this;
}
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 (!view._isAttached) {
this.empty(options);
}
this._setupChildView(view);
renderView(view);
this._attachView(view, options);
this.currentView = view;
this.triggerMethod('show', this, view, options);
this._isSwappingView = false;
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);
},
_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);
}
},
_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;
},
_getView: function _getView(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.'
});
}
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)) {
var $el = this.Dom.findEl(context, el);
if ($el.length) {
return $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 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) {
if (shouldDestroy) {
this.removeView(view);
} else {
this._detachView(view);
}
this._stopChildViewEvents(view);
}
this.triggerMethod('empty', this, view);
},
_stopChildViewEvents: function _stopChildViewEvents(view) {
var parentView = this._parentView;
if (!parentView) {
return;
}
this._parentView.stopListening(view);
},
destroyView: function destroyView$$1(view) {
if (view._isDestroyed) {
return view;
}
destroyView(view);
return view;
},
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;
var shouldRestoreEl = this._isReplaced;
if (shouldTriggerDetach) {
triggerMethodOn(view, 'before:detach', view);
}
if (shouldRestoreEl) {
this.Dom.replaceEl(this.el, view.el);
} else {
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.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);
if (this.$el) {
this.el = this._initEl;
}
delete this.$el;
return this;
},
destroy: function destroy(options) {
if (this._isDestroyed) {
return this;
}
this.reset(options);
if (this._name) {
this._parentView._removeReferences(this._name);
}
delete this._parentView;
delete this._name;
return MarionetteObject.prototype.destroy.apply(this, arguments);
}
}, {
setDomApi: setDomApi
});
// return the region instance from the definition
var buildRegion = function (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._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, _.bind(this._removeRegion, 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];
},
// Get all regions
_getRegions: function _getRegions() {
return _.clone(this._regions);
},
getRegions: function getRegions() {
if (!this._isRendered) {
this.render();
}
return this._getRegions();
},
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;
}
};
// Renderer
// --------
// 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);
}
};
// View
// ---------
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();
this._triggerEventOnBehaviors('initialize', this);
},
// 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() {
if (this._isDestroyed) {
return this;
}
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) {
deprecate('template:false is deprecated. Use _.noop.');
return;
}
// Add in entity data and template context
var data = this.mixinTemplateContext(this.serializeData());
// Render and add to el
var html = this._renderHtml(template, data);
this.attachElContent(html);
},
// Renders the data into the template
_renderHtml: function _renderHtml(template, data) {
return Renderer.render(template, data, this);
},
// 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.Dom.setContents(this.el, html, this.$el);
return this;
},
// called by ViewMixin destroy
_removeChildren: function _removeChildren() {
this.removeRegions();
},
_getImmediateChildren: function _getImmediateChildren() {
return _.chain(this._getRegions()).map('currentView').compact().value();
}
}, {
// Sets the renderer for the Marionette.View class
setRenderer: function setRenderer(renderer) {
this.prototype._renderHtml = renderer;
},
setDomApi: setDomApi
});
_.extend(View.prototype, ViewMixin, RegionsMixin);
// Mix in methods from Underscore, for iteration, and other
// collection related features.
// Borrowing this code from Backbone.Collection:
// https://github.com/jashkenas/backbone/blob/1.1.2/backbone.js#L962
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', 'partition'];
var emulateCollection = function emulateCollection(object, listProperty) {
_.each(methods, function (method) {
object[method] = function () {
var list = _.result(this, listProperty);
var args = Array.prototype.slice.call(arguments);
return _[method].apply(_, [list].concat(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, '_getViews');
// Container Methods
// -----------------
_.extend(Container.prototype, {
_getViews: function _getViews() {
return _.values(this._views);
},
// 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;
}
});
// Collection View
// ---------------
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();
this._triggerEventOnBehaviors('initialize', this);
},
// 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) {
// 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 (this.isEmpty()) {
this._showEmptyView();
}
},
// 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 = model && _this.children.findByModel(model);
if (!view || view._isDestroyed) {
return removingViews;
}
_this._removeChildView(view);
removingViews.push(view);
return removingViews;
}, []);
},
_removeChildView: function _removeChildView(view) {
this.triggerMethod('before:remove:child', this, view);
this.children._remove(view);
destroyView(view);
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() {
if (this._isDestroyed) {
return this;
}
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 _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
preventRender = _ref.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 {
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 = _.reduce(this.children._views, 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));
this.triggerMethod('before:reorder', this);
var elBuffer = this.Dom.createBuffer();
_.each(elsToReorder, function (el) {
_this3.Dom.appendContents(elBuffer, el);
});
// Since append moves elements that are already in the DOM, appending the elements
// will effectively reorder them.
this._appendReorderedChildren(elBuffer);
// remove any views that have been filtered out
this._removeChildModels(filteredOutModels);
this.triggerMethod('reorder', this);
}
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.Dom.appendContents(this.el, children, { _$el: this.$el });
},
// 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();
}
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) {
monitorViewEvents(view);
// set up the child view event forwarding
this._proxyChildViewEvents(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 _.clone(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);
}
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;
}
if (!increment) {
_.each(_.sortBy(this.children._views, '_index'), function (view, index) {
view._index = index;
});
return;
}
var view = _.isArray(views) ? _.max(views, '_index') : views;
if (_.isObject(view)) {
// update the indexes of views after this one
_.each(this.children._views, function (laterView) {
if (laterView._index >= view._index) {
laterView._index += 1;
}
});
}
},
_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;
},
// You might need to override this if you've overridden attachHtml
attachBuffer: function attachBuffer(collectionView, buffer) {
this.Dom.appendContents(collectionView.el, buffer, { _$el: collectionView.$el });
},
// Create a fragment buffer from the currently buffered children
_createBuffer: function _createBuffer() {
var _this6 = this;
var elBuffer = this.Dom.createBuffer();
_.each(this._bufferedChildren, function (b) {
_this6.Dom.appendContents(elBuffer, b.el, { _$contents: 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 = _.find(this.children._views, function (view) {
return view._index === index + 1;
});
}
if (currentView) {
this.beforeEl(currentView.el, childView.el);
return true;
}
return false;
},
// Override to handle DOM inserting differently
beforeEl: function beforeEl(el, siblings) {
this.$(el).before(siblings);
},
// Internal method. Append a view to the end of the $el
_insertAfter: function _insertAfter(childView) {
this.Dom.appendContents(this.el, childView.el, { _$el: this.$el, _$contents: 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();
},
// 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);
_.each(this.children._views, _.bind(this._removeChildView, this));
this.children._updateLength();
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);
}
}, {
setDomApi: setDomApi
});
_.extend(CollectionView.prototype, ViewMixin);
// Provide a container to store, retrieve and
// shut down child views.
var Container$1 = function Container() {
this._init();
};
emulateCollection(Container$1.prototype, '_views');
function stringComparator(comparator, view) {
return view.model && view.model.get(comparator);
}
// Container Methods
// -----------------
_.extend(Container$1.prototype, {
// Initializes an empty container
_init: function _init() {
this._views = [];
this._viewsByCid = {};
this._indexByModel = {};
this._updateLength();
},
// Add a view to this container. Stores the view
// by `cid` and makes it searchable by the model
// cid (and model itself). Additionally it stores
// the view by index in the _views array
_add: function _add(view) {
var index = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this._views.length;
var viewCid = view.cid;
// store the view
this._viewsByCid[viewCid] = view;
// index it by model
if (view.model) {
this._indexByModel[view.model.cid] = viewCid;
}
// add to end by default
this._views.splice(index, 0, view);
this._updateLength();
},
// Sort (mutate) and return the array of the child views.
_sort: function _sort(comparator) {
if (typeof comparator === 'string') {
comparator = _.partial(stringComparator, comparator);
return this._sortBy(comparator);
}
if (comparator.length === 1) {
return this._sortBy(comparator);
}
return this._views.sort(comparator);
},
// Makes `_.sortBy` mutate the array to match `this._views.sort`
_sortBy: function _sortBy(comparator) {
var sortedViews = _.sortBy(this._views, comparator);
this._set(sortedViews);
return sortedViews;
},
// Replace array contents without overwriting the reference.
_set: function _set(views) {
this._views.length = 0;
this._views.push.apply(this._views, views.slice(0));
this._updateLength();
},
// 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 index.
findByIndex: function findByIndex(index) {
return this._views[index];
},
// Find the index of a view instance
findIndexByView: function findIndexByView(view) {
return this._views.indexOf(view);
},
// Retrieve a view by its `cid` directly
findByCid: function findByCid(cid) {
return this._viewsByCid[cid];
},
// Remove a view and clean up index references.
_remove: function _remove(view) {
if (!this._viewsByCid[view.cid]) {
return;
}
// delete model index
if (view.model) {
delete this._indexByModel[view.model.cid];
}
// remove the view from the container
delete this._viewsByCid[view.cid];
var index = this.findIndexByView(view);
this._views.splice(index, 1);
this._updateLength();
},
// Update the `.length` attribute on this container
_updateLength: function _updateLength() {
this.length = this._views.length;
}
});
// Next Collection View
// ---------------
var ClassOptions$4 = ['behaviors', 'childView', 'childViewEventPrefix', 'childViewEvents', 'childViewOptions', 'childViewTriggers', 'collectionEvents', 'emptyView', 'emptyViewOptions', 'events', 'modelEvents', 'sortWithCollection', 'triggers', 'ui', 'viewComparator', 'viewFilter'];
// A view that iterates over a Backbone.Collection
// and renders an individual child view for each model.
var CollectionView$2 = Backbone.View.extend({
// flag for maintaining the sorted order of the collection
sortWithCollection: true,
// constructor
constructor: function constructor(options) {
this._setOptions(options);
this.mergeOptions(options, ClassOptions$4);
monitorViewEvents(this);
this.once('render', this._initialEvents);
// This children container isn't really used by a render, but it provides
// the ability to check `this.children.length` prior to rendering
// It also allows for cases where only addChildView is used
this._initChildViewStorage();
this._initBehaviors();
var args = Array.prototype.slice.call(arguments);
args[0] = this.options;
Backbone.View.prototype.constructor.apply(this, args);
this._initEmptyRegion();
this.delegateEntityEvents();
this._triggerEventOnBehaviors('initialize', this);
},
// Internal method to set up the `children` object for storing all of the child views
_initChildViewStorage: function _initChildViewStorage() {
this.children = new Container$1();
},
// Create an region to show the emptyView
_initEmptyRegion: function _initEmptyRegion() {
this.emptyRegion = new Region({ el: this.el });
this.emptyRegion._parentView = this;
},
// Configured the initial events that the collection view binds to.
_initialEvents: function _initialEvents() {
this.listenTo(this.collection, {
'sort': this._onCollectionSort,
'reset': this._onCollectionReset,
'update': this._onCollectionUpdate
});
},
// Internal method. This checks for any changes in the order of the collection.
// If the index of any view doesn't match, it will re-sort.
_onCollectionSort: function _onCollectionSort() {
var _this = this;
if (!this.sortWithCollection) {
return;
}
// If the data is changing we will handle the sort later
if (this.collection.length !== this.children.length) {
return;
}
// Additional check if the data is changing
var hasAddedModel = this.collection.some(function (model) {
return !_this.children.findByModel(model);
});
if (hasAddedModel) {
return;
}
// If the only thing happening here is sorting, sort.
this.sort();
},
_onCollectionReset: function _onCollectionReset() {
this.render();
},
// Handle collection update model additions and removals
_onCollectionUpdate: function _onCollectionUpdate(collection, options) {
var changes = options.changes;
// Remove first since it'll be a shorter array lookup.
var removedViews = this._removeChildModels(changes.removed);
this._addChildModels(changes.added);
this._detachChildren(removedViews);
this._showChildren();
// Destroy removed child views after all of the render is complete
this._removeChildViews(removedViews);
},
_removeChildModels: function _removeChildModels(models) {
return _.map(models, _.bind(this._removeChildModel, this));
},
_removeChildModel: function _removeChildModel(model) {
var view = this.children.findByModel(model);
this._removeChild(view);
return view;
},
_removeChild: function _removeChild(view) {
this.triggerMethod('before:remove:child', this, view);
this.children._remove(view);
this.triggerMethod('remove:child', this, view);
},
// Added views are returned for consistency with _removeChildModels
_addChildModels: function _addChildModels(models) {
return _.map(models, _.bind(this._addChildModel, this));
},
_addChildModel: function _addChildModel(model) {
var view = this._createChildView(model);
this._addChild(view);
return view;
},
_createChildView: function _createChildView(model) {
var ChildView = this._getChildView(model);
var childViewOptions = this._getChildViewOptions(model);
var view = this.buildChildView(model, ChildView, childViewOptions);
return view;
},
_addChild: function _addChild(view, index) {
this.triggerMethod('before:add:child', this, view);
this._setupChildView(view);
this.children._add(view, index);
this.triggerMethod('add:child', this, view);
},
// 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);
}
},
_getChildViewOptions: function _getChildViewOptions(child) {
if (_.isFunction(this.childViewOptions)) {
return this.childViewOptions(child);
}
return this.childViewOptions;
},
// Build a `childView` for a model in the collection.
// Override to customize the build
buildChildView: function buildChildView(child, ChildViewClass, childViewOptions) {
var options = _.extend({ model: child }, childViewOptions);
return new ChildViewClass(options);
},
_setupChildView: function _setupChildView(view) {
monitorViewEvents(view);
// We need to listen for if a view is destroyed in a way other
// than through the CollectionView.
// If this happens we need to remove the reference to the view
// since once a view has been destroyed we can not reuse it.
view.on('destroy', this.removeChildView, this);
// set up the child view event forwarding
this._proxyChildViewEvents(view);
},
// used by ViewMixin's `_childViewEventHandler`
_getImmediateChildren: function _getImmediateChildren() {
return this.children._views;
},
// 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.
render: function render() {
if (this._isDestroyed) {
return this;
}
this.triggerMethod('before:render', this);
this._destroyChildren();
// After all children have been destroyed re-init the container
this.children._init();
if (this.collection) {
this._addChildModels(this.collection.models);
}
this._showChildren();
this._isRendered = true;
this.triggerMethod('render', this);
return this;
},
// Sorts the children then filters and renders the results.
sort: function sort() {
if (this._isDestroyed) {
return this;
}
if (!this.children.length) {
return this;
}
this._showChildren();
return this;
},
_showChildren: function _showChildren() {
if (this.isEmpty()) {
this._showEmptyView();
return;
}
this._sortChildren();
this.filter();
},
// Returns true if the collectionView is considered empty.
// This is called twice during a render. Once to check the data,
// and again when views are filtered. Override this function to
// customize what empty means.
isEmpty: function isEmpty(allViewsFiltered) {
return allViewsFiltered || !this.children.length;
},
_showEmptyView: function _showEmptyView() {
var EmptyView = this._getEmptyView();
if (!EmptyView) {
return;
}
var options = this._getEmptyViewOptions();
this.emptyRegion.show(new EmptyView(options));
},
// Retrieve the empty view class
_getEmptyView: function _getEmptyView() {
var emptyView = this.emptyView;
if (!emptyView) {
return;
}
return this._getView(emptyView);
},
// Remove the emptyView
_destroyEmptyView: function _destroyEmptyView() {
// Only empty if a view is show so the region
// doesn't detach any other unrelated HTML
if (this.emptyRegion.hasView()) {
this.emptyRegion.empty();
}
},
//
_getEmptyViewOptions: function _getEmptyViewOptions() {
var emptyViewOptions = this.emptyViewOptions || this.childViewOptions;
if (_.isFunction(emptyViewOptions)) {
return emptyViewOptions.call(this);
}
return emptyViewOptions;
},
// Sorts views by viewComparator and sets the children to the new order
_sortChildren: function _sortChildren() {
this.triggerMethod('before:sort', this);
var viewComparator = this.getComparator();
if (_.isFunction(viewComparator)) {
// Must use native bind to preserve length
viewComparator = viewComparator.bind(this);
}
this.children._sort(viewComparator);
this.triggerMethod('sort', this);
},
// Sets the view's `viewComparator` and applies the sort if the view is ready.
// To prevent the render pass `{ preventRender: true }` as the 2nd argument.
setComparator: function setComparator(comparator) {
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
preventRender = _ref.preventRender;
var comparatorChanged = this.viewComparator !== comparator;
var shouldSort = comparatorChanged && !preventRender;
this.viewComparator = comparator;
if (shouldSort) {
this.sort();
}
return this;
},
// Clears the `viewComparator` and follows the same rules for rendering as `setComparator`.
removeComparator: function removeComparator(options) {
return this.setComparator(null, options);
},
// If viewComparator is overriden it will be returned here.
// Additionally override this function to provide custom
// viewComparator logic
getComparator: function getComparator() {
return this.viewComparator || this._viewComparator;
},
// Default internal view comparator that order the views by
// the order of the collection
_viewComparator: function _viewComparator(view) {
if (!this.collection) {
return;
}
return this.collection.indexOf(view.model);
},
// This method re-filters the children views and re-renders the results
filter: function filter() {
if (this._isDestroyed) {
return this;
}
if (!this.children.length) {
return this;
}
var filteredViews = this._filterChildren();
this._renderChildren(filteredViews);
return this;
},
_filterChildren: function _filterChildren() {
var viewFilter = this._getFilter();
if (!viewFilter) {
return this.children._views;
}
this.triggerMethod('before:filter', this);
var filteredViews = _.partition(this.children._views, _.bind(viewFilter, this));
this._detachChildren(filteredViews[1]);
this.triggerMethod('filter', this);
return filteredViews[0];
},
// This method returns a function for the viewFilter
_getFilter: function _getFilter() {
var viewFilter = this.getFilter();
if (!viewFilter) {
return false;
}
if (_.isFunction(viewFilter)) {
return viewFilter;
}
// Support filter predicates `{ fooFlag: true }`
if (_.isObject(viewFilter)) {
var matcher = _.matches(viewFilter);
return function (view) {
return matcher(view.model && view.model.attributes);
};
}
// Filter by model attribute
if (_.isString(viewFilter)) {
return function (view) {
return view.model && view.model.get(viewFilter);
};
}
throw new MarionetteError({
name: 'InvalidViewFilterError',
message: '"viewFilter" must be a function, predicate object literal, a string indicating a model attribute, or falsy'
});
},
// Override this function to provide custom
// viewFilter logic
getFilter: function getFilter() {
return this.viewFilter;
},
// Sets the view's `viewFilter` and applies the filter if the view is ready.
// To prevent the render pass `{ preventRender: true }` as the 2nd argument.
setFilter: function setFilter(filter) {
var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
preventRender = _ref2.preventRender;
var filterChanged = this.viewFilter !== filter;
var shouldRender = filterChanged && !preventRender;
this.viewFilter = filter;
if (shouldRender) {
this.filter();
}
return this;
},
// Clears the `viewFilter` and follows the same rules for rendering as `setFilter`.
removeFilter: function removeFilter(options) {
return this.setFilter(null, options);
},
_detachChildren: function _detachChildren(detachingViews) {
_.each(detachingViews, _.bind(this._detachChildView, this));
},
_detachChildView: function _detachChildView(view) {
var shouldTriggerDetach = !!view._isAttached;
if (shouldTriggerDetach) {
triggerMethodOn(view, 'before:detach', view);
}
this.detachHtml(view);
if (shouldTriggerDetach) {
view._isAttached = false;
triggerMethodOn(view, 'detach', view);
}
},
// Override this method to change how the collectionView detaches a child view
detachHtml: function detachHtml(view) {
this.Dom.detachEl(view.el, view.$el);
},
_renderChildren: function _renderChildren(views) {
if (this.isEmpty(!views.length)) {
this._showEmptyView();
return;
}
this._destroyEmptyView();
this.triggerMethod('before:render:children', this, views);
var els = this._getBuffer(views);
this._attachChildren(els, views);
this.triggerMethod('render:children', this, views);
},
_attachChildren: function _attachChildren(els, views) {
var shouldTriggerAttach = !!this._isAttached;
views = shouldTriggerAttach ? views : [];
_.each(views, function (view) {
if (view._isAttached) {
return;
}
triggerMethodOn(view, 'before:attach', view);
});
this.attachHtml(els);
_.each(views, function (view) {
if (view._isAttached) {
return;
}
view._isAttached = true;
triggerMethodOn(view, 'attach', view);
});
},
// Renders each view in children and creates a fragment buffer from them
_getBuffer: function _getBuffer(views) {
var _this2 = this;
var elBuffer = this.Dom.createBuffer();
_.each(views, function (view) {
renderView(view);
_this2.Dom.appendContents(elBuffer, view.el, { _$contents: view.$el });
});
return elBuffer;
},
// Override this method to do something other than `.append`.
// You can attach any HTML at this point including the els.
attachHtml: function attachHtml(els) {
this.Dom.appendContents(this.el, els, { _$el: this.$el });
},
// Render the child's view and add it to the HTML for the collection view at a given index, based on the current sort
addChildView: function addChildView(view, index) {
if (!view || view._isDestroyed) {
return view;
}
this._addChild(view, index);
this._showChildren();
return view;
},
// Detach a view from the children. Best used when adding a
// childView from `addChildView`
detachChildView: function detachChildView(view) {
this.removeChildView(view, { shouldDetach: true });
return view;
},
// Remove the child view and destroy it. Best used when adding a
// childView from `addChildView`
// The options argument is for internal use only
removeChildView: function removeChildView(view, options) {
if (!view) {
return view;
}
this._removeChildView(view, options);
this._removeChild(view);
if (this.isEmpty()) {
this._showEmptyView();
}
return view;
},
_removeChildViews: function _removeChildViews(views) {
_.each(views, _.bind(this._removeChildView, this));
},
_removeChildView: function _removeChildView(view) {
var _ref3 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
shouldDetach = _ref3.shouldDetach;
view.off('destroy', this.removeChildView, this);
if (shouldDetach) {
this._detachChildView(view);
} else {
this._destroyChildView(view);
}
this.stopListening(view);
},
_destroyChildView: function _destroyChildView(view) {
if (view._isDestroyed) {
return;
}
destroyView(view);
},
// called by ViewMixin destroy
_removeChildren: function _removeChildren() {
this._destroyChildren();
this.emptyRegion.destroy();
},
// Destroy the child views that this collection view is holding on to, if any
_destroyChildren: function _destroyChildren() {
if (!this.children || !this.children.length) {
return;
}
this.triggerMethod('before:destroy:children', this);
_.each(this.children._views, _.bind(this._removeChildView, this));
this.triggerMethod('destroy:children', this);
}
}, {
setDomApi: setDomApi
});
_.extend(CollectionView$2.prototype, ViewMixin);
// Composite View
// --------------
var ClassOptions$5 = ['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$5);
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() {
if (this._isDestroyed) {
return this;
}
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);
this.Dom.appendContents($container[0], buffer, { _$el: $container });
},
// 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);
this.Dom.appendContents($container[0], childView.el, { _$el: $container, _$contents: 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);
this.Dom.appendContents($container[0], children, { _$el: $container });
},
// 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 = this.$(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', '_renderHtml', 'mixinTemplateContext', 'attachElContent');
_.extend(CompositeView.prototype, MixinFromView);
// Behavior
// --------
// A Behavior is an isolated set of DOM /
// user interactions that can be mixed into any View.
// Behaviors allow you to blackbox View specific interactions
// into portable logical chunks, keeping your views simple and your code DRY.
var ClassOptions$6 = ['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;
if (this.defaults) {
deprecate('Behavior defaults are deprecated. For similar functionality set options on the Behavior class.');
}
this.defaults = _.clone(_.result(this, 'defaults', {}));
this._setOptions(this.defaults, options);
this.mergeOptions(this.options, ClassOptions$6);
// 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();
this.view._removeBehavior(this);
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) {
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);
// Application
// -----------
var ClassOptions$7 = ['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$7);
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;
}
});
// App Router
// ----------
// Reduce the boilerplate code of handling route events
// and then calling a single method on another object,
// called a controller.
// Have your routers configured to call the method on
// your controller, directly.
//
// Configure an AppRouter with `appRoutes`.
//
// App routers can only take one `controller` object.
// It is recommended that you divide your controller
// objects in to smaller pieces of related functionality
// and have multiple routers / controllers, instead of
// just one giant router and controller.
//
// You can also add standard routes to an AppRouter.
var ClassOptions$8 = ['appRoutes', 'controller'];
var AppRouter = Backbone.Router.extend({
constructor: function constructor(options) {
this._setOptions(options);
this.mergeOptions(options, ClassOptions$8);
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'
});
}
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.NextCollectionView = CollectionView$2;
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;
Marionette.DomApi = DomApi;
Marionette.setDomApi = function (mixin) {
CollectionView.setDomApi(mixin);
CompositeView.setDomApi(mixin);
CollectionView$2.setDomApi(mixin);
Region.setDomApi(mixin);
View.setDomApi(mixin);
};
return Marionette;
})));
this.Mn = this.Marionette;
//# sourceMappingURL=backbone.marionette.js.map
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment