// MarionetteJS (Backbone.Marionette) // ---------------------------------- // v3.1.0 // // Copyright (c)2016 Derick Bailey, Muted Solutions, LLC. // Distributed under MIT license // // http://marionettejs.com (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('backbone'), require('underscore'), require('backbone.radio')) : typeof define === 'function' && define.amd ? define(['backbone', 'underscore', 'backbone.radio'], factory) : (global.Marionette = global['Mn'] = factory(global.Backbone,global._,global.Backbone.Radio)); }(this, function (Backbone,_,Radio) { 'use strict'; Backbone = 'default' in Backbone ? Backbone['default'] : Backbone; _ = 'default' in _ ? _['default'] : _; Radio = 'default' in Radio ? Radio['default'] : Radio; var version = "3.1.0"; //Internal utility for creating context style global utils var proxy = function proxy(method) { return function (context) { for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } return method.apply(context, args); }; }; // Borrow the Backbone `extend` method so we can use it as needed var extend = Backbone.Model.extend; var deprecate = function deprecate(message, test) { if (_.isObject(message)) { message = message.prev + ' is going to be removed in the future. ' + 'Please use ' + message.next + ' instead.' + (message.url ? ' See: ' + message.url : ''); } if (!Marionette.DEV_MODE) { return; } if ((test === undefined || !test) && !deprecate._cache[message]) { deprecate._warn('Deprecation warning: ' + message); deprecate._cache[message] = true; } }; deprecate._console = typeof console !== 'undefined' ? console : {}; deprecate._warn = function () { var warn = deprecate._console.warn || deprecate._console.log || _.noop; return warn.apply(deprecate._console, arguments); }; deprecate._cache = {}; // Determine if `el` is a child of the document var isNodeAttached = function isNodeAttached(el) { return Backbone.$.contains(document.documentElement, el); }; // Merge `keys` from `options` onto `this` var mergeOptions = function mergeOptions(options, keys) { var _this = this; if (!options) { return; } _.each(keys, function (key) { var option = options[key]; if (option !== undefined) { _this[key] = option; } }); }; // Marionette.getOption // -------------------- // Retrieve an object, function or other value from the // object or its `options`, with `options` taking precedence. var getOption = function getOption(optionName) { if (!optionName) { return; } if (this.options && this.options[optionName] !== undefined) { return this.options[optionName]; } else { return this[optionName]; } }; // Marionette.normalizeMethods // ---------------------- // Pass in a mapping of events => functions or function names // and return a mapping of events => functions var normalizeMethods = function normalizeMethods(hash) { var _this = this; return _.reduce(hash, function (normalizedHash, method, name) { if (!_.isFunction(method)) { method = _this[method]; } if (method) { normalizedHash[name] = method; } return normalizedHash; }, {}); }; // split the event name on the ":" var splitter = /(^|:)(\w)/gi; // take the event section ("section1:section2:section3") // and turn it in to uppercase name onSection1Section2Section3 function getEventName(match, prefix, eventName) { return eventName.toUpperCase(); } var getOnMethodName = _.memoize(function (event) { return 'on' + event.replace(splitter, getEventName); }); // Trigger an event and/or a corresponding method name. Examples: // // `this.triggerMethod("foo")` will trigger the "foo" event and // call the "onFoo" method. // // `this.triggerMethod("foo:bar")` will trigger the "foo:bar" event and // call the "onFooBar" method. function triggerMethod(event) { for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } // get the method name from the event name var methodName = getOnMethodName(event); var method = getOption.call(this, methodName); var result = void 0; // call the onMethodName if it exists if (_.isFunction(method)) { // pass all args, except the event name result = method.apply(this, args); } // trigger the event this.trigger.apply(this, arguments); return result; } // triggerMethodOn invokes triggerMethod on a specific context // // e.g. `Marionette.triggerMethodOn(view, 'show')` // will trigger a "show" event or invoke onShow the view. function triggerMethodOn(context) { for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { args[_key2 - 1] = arguments[_key2]; } if (_.isFunction(context.triggerMethod)) { return context.triggerMethod.apply(context, args); } return triggerMethod.apply(context, args); } // Trigger method on children unless a pure Backbone.View function triggerMethodChildren(view, event, shouldTrigger) { if (!view._getImmediateChildren) { return; } _.each(view._getImmediateChildren(), function (child) { if (!shouldTrigger(child)) { return; } triggerMethodOn(child, event, child); }); } function shouldTriggerAttach(view) { return !view._isAttached; } function shouldAttach(view) { if (!shouldTriggerAttach(view)) { return false; } view._isAttached = true; return true; } function shouldTriggerDetach(view) { return view._isAttached; } function shouldDetach(view) { if (!shouldTriggerDetach(view)) { return false; } view._isAttached = false; return true; } function triggerDOMRefresh(view) { if (view._isAttached && view._isRendered) { triggerMethodOn(view, 'dom:refresh', view); } } function handleBeforeAttach() { triggerMethodChildren(this, 'before:attach', shouldTriggerAttach); } function handleAttach() { triggerMethodChildren(this, 'attach', shouldAttach); triggerDOMRefresh(this); } function handleBeforeDetach() { triggerMethodChildren(this, 'before:detach', shouldTriggerDetach); } function handleDetach() { triggerMethodChildren(this, 'detach', shouldDetach); } function handleRender() { triggerDOMRefresh(this); } // Monitor a view's state, propagating attach/detach events to children and firing dom:refresh // whenever a rendered view is attached or an attached view is rendered. function monitorViewEvents(view) { if (view._areViewEventsMonitored) { return; } view._areViewEventsMonitored = true; view.on({ 'before:attach': handleBeforeAttach, 'attach': handleAttach, 'before:detach': handleBeforeDetach, 'detach': handleDetach, 'render': handleRender }); } var errorProps = ['description', 'fileName', 'lineNumber', 'name', 'message', 'number']; var MarionetteError = extend.call(Error, { urlRoot: 'http://marionettejs.com/docs/v' + version + '/', constructor: function constructor(message, options) { if (_.isObject(message)) { options = message; message = options.message; } else if (!options) { options = {}; } var error = Error.call(this, message); _.extend(this, _.pick(error, errorProps), _.pick(options, errorProps)); this.captureStackTrace(); if (options.url) { this.url = this.urlRoot + options.url; } }, captureStackTrace: function captureStackTrace() { if (Error.captureStackTrace) { Error.captureStackTrace(this, MarionetteError); } }, toString: function toString() { return this.name + ': ' + this.message + (this.url ? ' See: ' + this.url : ''); } }); MarionetteError.extend = extend; // Bind/unbind the event to handlers specified as a string of // handler names on the target object function bindFromStrings(target, entity, evt, methods, actionName) { var methodNames = methods.split(/\s+/); _.each(methodNames, function (methodName) { var method = target[methodName]; if (!method) { throw new MarionetteError('Method "' + methodName + '" was configured as an event handler, but does not exist.'); } target[actionName](entity, evt, method); }); } // generic looping function function iterateEvents(target, entity, bindings, actionName) { if (!entity || !bindings) { return; } // type-check bindings if (!_.isObject(bindings)) { throw new MarionetteError({ message: 'Bindings must be an object.', url: 'marionette.functions.html#marionettebindevents' }); } // iterate the bindings and bind/unbind them _.each(bindings, function (method, evt) { // allow for a list of method names as a string if (_.isString(method)) { bindFromStrings(target, entity, evt, method, actionName); return; } target[actionName](entity, evt, method); }); } function bindEvents(entity, bindings) { iterateEvents(this, entity, bindings, 'listenTo'); return this; } function unbindEvents(entity, bindings) { iterateEvents(this, entity, bindings, 'stopListening'); return this; } function iterateReplies(target, channel, bindings, actionName) { if (!channel || !bindings) { return; } // type-check bindings if (!_.isObject(bindings)) { throw new MarionetteError({ message: 'Bindings must be an object.', url: 'marionette.functions.html#marionettebindrequests' }); } var normalizedRadioRequests = normalizeMethods.call(target, bindings); channel[actionName](normalizedRadioRequests, target); } function bindRequests(channel, bindings) { iterateReplies(this, channel, bindings, 'reply'); return this; } function unbindRequests(channel, bindings) { iterateReplies(this, channel, bindings, 'stopReplying'); return this; } // Internal utility for setting options consistently across Mn var setOptions = function setOptions() { for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } this.options = _.extend.apply(_, [{}, _.result(this, 'options')].concat(args)); }; var CommonMixin = { // Imports the "normalizeMethods" to transform hashes of // events=>function references/names to a hash of events=>function references normalizeMethods: normalizeMethods, _setOptions: setOptions, // A handy way to merge passed-in options onto the instance mergeOptions: mergeOptions, // Enable getting options from this or this.options by name. getOption: getOption, // Enable binding view's events from another entity. bindEvents: bindEvents, // Enable unbinding view's events from another entity. unbindEvents: unbindEvents }; // MixinOptions // - channelName // - radioEvents // - radioRequests var RadioMixin = { _initRadio: function _initRadio() { var channelName = _.result(this, 'channelName'); if (!channelName) { return; } /* istanbul ignore next */ if (!Radio) { throw new MarionetteError({ name: 'BackboneRadioMissing', message: 'The dependency "backbone.radio" is missing.' }); } var channel = this._channel = Radio.channel(channelName); var radioEvents = _.result(this, 'radioEvents'); this.bindEvents(channel, radioEvents); var radioRequests = _.result(this, 'radioRequests'); this.bindRequests(channel, radioRequests); this.on('destroy', this._destroyRadio); }, _destroyRadio: function _destroyRadio() { this._channel.stopReplying(null, null, this); }, getChannel: function getChannel() { return this._channel; }, // Proxy `bindEvents` bindEvents: bindEvents, // Proxy `unbindEvents` unbindEvents: unbindEvents, // Proxy `bindRequests` bindRequests: bindRequests, // Proxy `unbindRequests` unbindRequests: unbindRequests }; var ClassOptions = ['channelName', 'radioEvents', 'radioRequests']; // A Base Class that other Classes should descend from. // Object borrows many conventions and utilities from Backbone. var MarionetteObject = function MarionetteObject(options) { this._setOptions(options); this.mergeOptions(options, ClassOptions); this.cid = _.uniqueId(this.cidPrefix); this._initRadio(); this.initialize.apply(this, arguments); }; MarionetteObject.extend = extend; // Object Methods // -------------- // Ensure it can trigger events with Backbone.Events _.extend(MarionetteObject.prototype, Backbone.Events, CommonMixin, RadioMixin, { cidPrefix: 'mno', // for parity with Marionette.AbstractView lifecyle _isDestroyed: false, isDestroyed: function isDestroyed() { return this._isDestroyed; }, //this is a noop method intended to be overridden by classes that extend from this base initialize: function initialize() {}, destroy: function destroy() { if (this._isDestroyed) { return this; } for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } this.triggerMethod.apply(this, ['before:destroy', this].concat(args)); this._isDestroyed = true; this.triggerMethod.apply(this, ['destroy', this].concat(args)); this.stopListening(); return this; }, triggerMethod: triggerMethod }); // Manage templates stored in `