From 5e07ba86a0801e0b3b9315cc5b36e1388e93bfb4 Mon Sep 17 00:00:00 2001 From: Paul Falgout Date: Sat, 5 Aug 2017 14:56:18 +0900 Subject: [PATCH] V3.4 (#3411) * Update build related dependencies * Bump and build v3.4 Includes changelog --- bower.json | 2 +- changelog.md | 21 + lib/backbone.marionette.esm.js | 4603 ++++++++++++++++++++++++++++ lib/backbone.marionette.esm.js.map | 1 + lib/backbone.marionette.js | 547 ++-- lib/backbone.marionette.js.map | 2 +- lib/backbone.marionette.min.js | 6 +- lib/backbone.marionette.min.js.map | 2 +- package.json | 14 +- yarn.lock | 229 +- 10 files changed, 5141 insertions(+), 286 deletions(-) create mode 100644 lib/backbone.marionette.esm.js create mode 100644 lib/backbone.marionette.esm.js.map diff --git a/bower.json b/bower.json index 55799f752b..d61ae88437 100644 --- a/bower.json +++ b/bower.json @@ -3,7 +3,7 @@ "description": "The Backbone Framework", "homepage": "https://marionettejs.com/", "main": "./lib/backbone.marionette.js", - "version": "3.3.1", + "version": "3.4.0", "license": "MIT", "keywords": [ "backbone", diff --git a/changelog.md b/changelog.md index be0db395f4..43b78457b6 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,24 @@ +### v3.4.0 [view commit logs](https://github.com/marionettejs/backbone.marionette/compare/v3.3.1...v3.4.0) + +#### Features +* A new build of Marionette supporting ES6 modules was added +* Added DOM API to encapsulate DOM interactions in the views and region +* `monitorViewEvents` was added as an option to all Views to disable DOM lifecycle events +* Added `swapChildViews` to `NextCollectionView` +* Added `viewComparator: false` option to `NextCollectionView` for disabling the default sort + +#### Experimental API Breaking Changes +* DOM Mixin was removed (replaced with DOM API) +* `NextCollectionView` `attachHtml` no longer receives the view as the first argument + +#### Fixes +* A region's currentView will now be set during that view's initial `dom:refresh` event +* A view will now be considered rendered if its `el` has contents and not only if it has an `el` + +#### Misc +* While `Backbone.Radio` is still a dependency, it will no longer cause Marionette to error if nonexistent +* Various performance improvements + ### v3.3.1 [view commit logs](https://github.com/marionettejs/backbone.marionette/compare/v3.3.0...v3.3.1) #### Fixes diff --git a/lib/backbone.marionette.esm.js b/lib/backbone.marionette.esm.js new file mode 100644 index 0000000000..b6c21bc7de --- /dev/null +++ b/lib/backbone.marionette.esm.js @@ -0,0 +1,4603 @@ +// MarionetteJS (Backbone.Marionette) +// ---------------------------------- +// v3.4.0 +// +// Copyright (c)2017 Derick Bailey, Muted Solutions, LLC. +// Distributed under MIT license +// +// http://marionettejs.com + + +import Backbone from 'backbone'; +import _ from 'underscore'; +import Radio from 'backbone.radio'; + +var version = "3.4.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); + }; +}; + +// 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; + } +}; + +/* istanbul ignore next: can't clear console */ +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 || view.monitorViewEvents === false) { + 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 `