diff --git a/lib/backbone.marionette.esm.js b/lib/backbone.marionette.esm.js index 4597b85aa2..efc5ea73ff 100644 --- a/lib/backbone.marionette.esm.js +++ b/lib/backbone.marionette.esm.js @@ -1,5 +1,5 @@ +import _, { has, extend as extend$1, create, reduce, keys, once, each, uniqueId, result, isFunction, isString, isObject, map, matches } from 'underscore'; import Backbone from 'backbone'; -import _ from 'underscore'; import Radio from 'backbone.radio'; var version = "4.1.2"; @@ -17,7 +17,31 @@ var proxy = function proxy(method) { // Marionette.extend -var extend = Backbone.Model.extend; +function extend (protoProps, staticProps) { + var parent = this; + var child; // The constructor function for the new subclass is either defined by you + // (the "constructor" property in your `extend` definition), or defaulted + // by us to simply call the parent constructor. + + if (protoProps && has(protoProps, 'constructor')) { + child = protoProps.constructor; + } else { + child = function child() { + return parent.apply(this, arguments); + }; + } // Add static properties to the constructor function, if supplied. + + + extend$1(child, parent, staticProps); // Set the prototype chain to inherit from `parent`, without calling + // `parent`'s constructor function and add the prototype properties. + + child.prototype = create(parent.prototype, protoProps); + child.prototype.constructor = child; // Set a convenience property in case the parent's prototype is needed + // later. + + child.__super__ = parent.prototype; + return child; +} // ---------------------- // Pass in a mapping of events => functions or function names @@ -307,7 +331,454 @@ function triggerMethod(event) { return result; } +function _typeof(obj) { + if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { + _typeof = function (obj) { + return typeof obj; + }; + } else { + _typeof = function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + } + + return _typeof(obj); +} + +var eventSplitter = /\s+/; // Iterates over the standard `event, callback` (as well as the fancy multiple +// space-separated events `"change blur", callback` and jQuery-style event +// maps `{event: callback}`). + +function buildEventArgs(name, callback, context, listener) { + if (name && _typeof(name) === 'object') { + return reduce(keys(name), function (eventArgs, key) { + return eventArgs.concat(buildEventArgs(key, name[key], context || callback, listener)); + }, []); + } + + if (name && eventSplitter.test(name)) { + return reduce(name.split(eventSplitter), function (eventArgs, n) { + eventArgs.push({ + name: n, + callback: callback, + context: context, + listener: listener + }); + return eventArgs; + }, []); + } + + return [{ + name: name, + callback: callback, + context: context, + listener: listener + }]; +} + +// An optimized way to execute callbacks. +function callHandler(callback, context) { + var args = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; + + switch (args.length) { + case 0: + return callback.call(context); + + case 1: + return callback.call(context, args[0]); + + case 2: + return callback.call(context, args[0], args[1]); + + case 3: + return callback.call(context, args[0], args[1], args[2]); + + default: + return callback.apply(context, args); + } +} + +// `offCallback` unbinds the `onceWrapper` after it has been called. + +function onceWrap(callback, offCallback) { + var onceCallback = once(function () { + offCallback(onceCallback); + return callback.apply(this, arguments); + }); + onceCallback._callback = callback; + return onceCallback; +} + +// a custom event channel. You may bind a callback to an event with `on` or +// remove with `off`; `trigger`-ing an event fires all callbacks in +// succession. +// +// var object = {}; +// _.extend(object, Events); +// object.on('expand', function(){ alert('expanded'); }); +// object.trigger('expand'); +// +// The reducing API that adds a callback to the `events` object. + +var onApi = function onApi(_ref) { + var events = _ref.events, + name = _ref.name, + callback = _ref.callback, + context = _ref.context, + ctx = _ref.ctx, + listener = _ref.listener; + var handlers = events[name] || (events[name] = []); + handlers.push({ + callback: callback, + context: context, + ctx: context || ctx, + listener: listener + }); + return events; +}; + +var onReducer = function onReducer(events, _ref2) { + var name = _ref2.name, + callback = _ref2.callback, + context = _ref2.context; + + if (!callback) { + return events; + } + + return onApi({ + events: events, + name: name, + callback: callback, + context: context, + ctx: this + }); +}; + +var onceReducer = function onceReducer(events, _ref3) { + var name = _ref3.name, + callback = _ref3.callback, + context = _ref3.context; + + if (!callback) { + return events; + } + + var onceCallback = onceWrap(callback, this.off.bind(this, name)); + return onApi({ + events: events, + name: name, + callback: onceCallback, + context: context, + ctx: this + }); +}; + +var cleanupListener = function cleanupListener(_ref4) { + var obj = _ref4.obj, + listeneeId = _ref4.listeneeId, + listenerId = _ref4.listenerId, + listeningTo = _ref4.listeningTo; + delete listeningTo[listeneeId]; + delete obj._rdListeners[listenerId]; +}; // The reducing API that removes a callback from the `events` object. + + +var offReducer = function offReducer(events, _ref5) { + var name = _ref5.name, + callback = _ref5.callback, + context = _ref5.context; + var names = name ? [name] : keys(events); + each(names, function (key) { + var handlers = events[key]; // Bail out if there are no events stored. + + if (!handlers) { + return; + } // Find any remaining events. + + + events[key] = reduce(handlers, function (remaining, handler) { + if (callback && callback !== handler.callback && callback !== handler.callback._callback || context && context !== handler.context) { + remaining.push(handler); + return remaining; + } // If not including event, clean up any related listener + + + if (handler.listener) { + var listener = handler.listener; + listener.count--; + + if (!listener.count) { + cleanupListener(listener); + } + } + + return remaining; + }, []); + + if (!events[key].length) { + delete events[key]; + } + }); + return events; +}; + +var getListener = function getListener(obj, listenerObj) { + var listeneeId = obj._rdListenId || (obj._rdListenId = uniqueId('l')); + obj._rdEvents = obj._rdEvents || {}; + var listeningTo = listenerObj._rdListeningTo || (listenerObj._rdListeningTo = {}); + var listener = listeningTo[listeneeId]; // This listenerObj is not listening to any other events on `obj` yet. + // Setup the necessary references to track the listening callbacks. + + if (!listener) { + var listenerId = listenerObj._rdListenId || (listenerObj._rdListenId = uniqueId('l')); + listeningTo[listeneeId] = { + obj: obj, + listeneeId: listeneeId, + listenerId: listenerId, + listeningTo: listeningTo, + count: 0 + }; + return listeningTo[listeneeId]; + } + + return listener; +}; + +var listenToApi = function listenToApi(_ref6) { + var name = _ref6.name, + callback = _ref6.callback, + context = _ref6.context, + listener = _ref6.listener; + + if (!callback) { + return; + } + + var obj = listener.obj, + listenerId = listener.listenerId; + var listeners = obj._rdListeners || (obj._rdListeners = {}); + obj._rdEvents = onApi({ + events: obj._rdEvents, + name: name, + callback: callback, + context: context, + listener: listener + }); + listeners[listenerId] = listener; + listener.count++; // Call `on` for interop + + obj.on(name, callback, context, { + _rdInternal: true + }); +}; + +var listenToOnceApi = function listenToOnceApi(_ref7) { + var name = _ref7.name, + callback = _ref7.callback, + context = _ref7.context, + listener = _ref7.listener; + + if (!callback) { + return; + } + + var offCallback = this.stopListening.bind(this, listener.obj, name); + var onceCallback = onceWrap(callback, offCallback); + listenToApi({ + name: name, + callback: onceCallback, + context: context, + listener: listener + }); +}; // Handles triggering the appropriate event callbacks. + + +var triggerApi = function triggerApi(_ref8) { + var events = _ref8.events, + name = _ref8.name, + args = _ref8.args; + var objEvents = events[name]; + var allEvents = objEvents && events.all ? events.all.slice() : events.all; + + if (objEvents) { + triggerEvents(objEvents, args); + } + + if (allEvents) { + triggerEvents(allEvents, [name].concat(args)); + } +}; + +var triggerEvents = function triggerEvents(events, args) { + each(events, function (_ref9) { + var callback = _ref9.callback, + ctx = _ref9.ctx; + callHandler(callback, ctx, args); + }); +}; + var Events = { + // Bind an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. + on: function on(name, callback, context, opts) { + if (opts && opts._rdInternal) { + return; + } + + var eventArgs = buildEventArgs(name, callback, context); + this._rdEvents = reduce(eventArgs, onReducer.bind(this), this._rdEvents || {}); + return this; + }, + // Remove one or many callbacks. If `context` is null, removes all + // callbacks with that function. If `callback` is null, removes all + // callbacks for the event. If `name` is null, removes all bound + // callbacks for all events. + off: function off(name, callback, context, opts) { + if (!this._rdEvents) { + return this; + } + + if (opts && opts._rdInternal) { + return; + } // Delete all event listeners and "drop" events. + + + if (!name && !context && !callback) { + this._rdEvents = void 0; + var listeners = this._rdListeners; + each(keys(listeners), function (listenerId) { + cleanupListener(listeners[listenerId]); + }); + return this; + } + + var eventArgs = buildEventArgs(name, callback, context); + this._rdEvents = reduce(eventArgs, offReducer, this._rdEvents); + return this; + }, + // Bind an event to only be triggered a single time. After the first time + // the callback is invoked, its listener will be removed. If multiple events + // are passed in using the space-separated syntax, the handler will fire + // once for each event, not once for a combination of all events. + once: function once(name, callback, context) { + var eventArgs = buildEventArgs(name, callback, context); + this._rdEvents = reduce(eventArgs, onceReducer.bind(this), this._rdEvents || {}); + return this; + }, + // Inversion-of-control versions of `on`. Tell *this* object to listen to + // an event in another object... keeping track of what it's listening to + // for easier unbinding later. + listenTo: function listenTo(obj, name, callback) { + if (!obj) { + return this; + } + + var listener = getListener(obj, this); + var eventArgs = buildEventArgs(name, callback, this, listener); + each(eventArgs, listenToApi); + return this; + }, + // Inversion-of-control versions of `once`. + listenToOnce: function listenToOnce(obj, name, callback) { + if (!obj) { + return this; + } + + var listener = getListener(obj, this); + var eventArgs = buildEventArgs(name, callback, this, listener); + each(eventArgs, listenToOnceApi.bind(this)); + return this; + }, + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + stopListening: function stopListening(obj, name, callback) { + var _this = this; + + var listeningTo = this._rdListeningTo; + + if (!listeningTo) { + return this; + } + + var eventArgs = buildEventArgs(name, callback, this); + var listenerIds = obj ? [obj._rdListenId] : keys(listeningTo); + + var _loop = function _loop(i) { + var listener = listeningTo[listenerIds[i]]; // If listening doesn't exist, this object is not currently + // listening to obj. Break out early. + + if (!listener) { + return "break"; + } + + each(eventArgs, function (args) { + var listenToObj = listener.obj; + var events = listenToObj._rdEvents; + + if (!events) { + return; + } + + listenToObj._rdEvents = offReducer(events, args); // Call `off` for interop + + listenToObj.off(args.name, args.callback, _this, { + _reInternal: true + }); + }); + }; + + for (var i = 0; i < listenerIds.length; i++) { + var _ret = _loop(i); + + if (_ret === "break") break; + } + + return this; + }, + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + trigger: function trigger(name) { + var _this2 = this; + + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + if (!this._rdEvents) { + return this; + } + + if (name && _typeof(name) === 'object') { + each(keys(name), function (key) { + triggerApi({ + events: _this2._rdEvents, + name: key, + args: [name[key]] + }); + }); + } + + if (name && eventSplitter.test(name)) { + each(name.split(eventSplitter), function (n) { + triggerApi({ + events: _this2._rdEvents, + name: n, + args: args + }); + }); + return this; + } + + triggerApi({ + events: this._rdEvents, + name: name, + args: args + }); + return this; + }, triggerMethod: triggerMethod }; @@ -647,88 +1118,6 @@ var TemplateRenderMixin = { } }; -// Borrow event splitter from Backbone -var delegateEventSplitter = /^(\S+)\s*(.*)$/; // Set event name to be namespaced using a unique index -// to generate a non colliding event namespace -// http://api.jquery.com/event.namespace/ - -var getNamespacedEventName = function getNamespacedEventName(eventName, namespace) { - var match = eventName.match(delegateEventSplitter); - return "".concat(match[1], ".").concat(namespace, " ").concat(match[2]); -}; - -// Add Feature flags here -// e.g. 'class' => false -var FEATURES = { - childViewEventPrefix: false, - triggersStopPropagation: true, - triggersPreventDefault: true, - DEV_MODE: false -}; - -function isEnabled(name) { - return !!FEATURES[name]; -} - -function setEnabled(name, state) { - return FEATURES[name] = state; -} - -// 'click:foo' - -function buildViewTrigger(view, triggerDef) { - if (_.isString(triggerDef)) { - triggerDef = { - event: triggerDef - }; - } - - var eventName = triggerDef.event; - var shouldPreventDefault = !!triggerDef.preventDefault; - - if (isEnabled('triggersPreventDefault')) { - shouldPreventDefault = triggerDef.preventDefault !== false; - } - - var shouldStopPropagation = !!triggerDef.stopPropagation; - - if (isEnabled('triggersStopPropagation')) { - shouldStopPropagation = triggerDef.stopPropagation !== false; - } - - return function (event) { - if (shouldPreventDefault) { - event.preventDefault(); - } - - if (shouldStopPropagation) { - event.stopPropagation(); - } - - for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - args[_key - 1] = arguments[_key]; - } - - view.triggerMethod.apply(view, [eventName, view, event].concat(args)); - }; -} - -var TriggersMixin = { - // Configure `triggers` to forward DOM events to view - // events. `triggers: {"click .foo": "do:foo"}` - _getViewTriggers: function _getViewTriggers(view, triggers) { - var _this = this; - - // Configure the triggers, prevent default - // action and stop propagation of DOM events - return _.reduce(triggers, function (events, value, key) { - key = getNamespacedEventName(key, "trig".concat(_this.cid)); - events[key] = buildViewTrigger(view, value); - return events; - }, {}); - } -}; - // a given key for triggers and events // swaps the @ui with the associated selector. // Returns a new, non-mutated, parsed events hash. @@ -846,18 +1235,204 @@ var UIMixin = { } }; -// DomApi +// Add Feature flags here +// e.g. 'class' => false +var FEATURES = { + childViewEventPrefix: false, + triggersStopPropagation: true, + triggersPreventDefault: true, + DEV_MODE: false +}; -function _getEl(el) { - return el instanceof Backbone.$ ? el : Backbone.$(el); -} // Static setter +function isEnabled(name) { + return !!FEATURES[name]; +} +function setEnabled(name, state) { + return FEATURES[name] = state; +} + +// Event Delegator + +function setEventDelegator(mixin) { + this.prototype.EventDelegator = extend$1({}, this.prototype.EventDelegator, mixin); + return this; +} +var EventDelegator = { + shouldCapture: function shouldCapture(eventName) { + return ['focus', 'blur'].indexOf(eventName) !== -1; + }, + // this.$el.on(eventName + '.delegateEvents' + this.cid, selector, handler); + delegate: function delegate(_ref) { + var eventName = _ref.eventName, + selector = _ref.selector, + handler = _ref.handler, + events = _ref.events, + rootEl = _ref.rootEl; + var shouldCapture = this.shouldCapture(eventName); + + if (selector) { + var delegateHandler = function delegateHandler(evt) { + var node = evt.target; + + for (; node && node !== rootEl; node = node.parentNode) { + if (Element.prototype.matches.call(node, selector)) { + evt.delegateTarget = node; + handler(evt); + } + } + }; + + events.push({ + eventName: eventName, + handler: delegateHandler + }); + Element.prototype.addEventListener.call(rootEl, eventName, delegateHandler, shouldCapture); + return; + } + + events.push({ + eventName: eventName, + handler: handler + }); + Element.prototype.addEventListener.call(rootEl, eventName, handler, shouldCapture); + }, + // this.$el.off('.delegateEvents' + this.cid); + undelegateAll: function undelegateAll(_ref2) { + var _this = this; + + var events = _ref2.events, + rootEl = _ref2.rootEl; + + if (!rootEl) { + return; + } + + each(events, function (_ref3) { + var eventName = _ref3.eventName, + handler = _ref3.handler; + + var shouldCapture = _this.shouldCapture(eventName); + + Element.prototype.removeEventListener.call(rootEl, eventName, handler, shouldCapture); + }); + events.length = 0; + } +}; + +var delegateEventSplitter = /^(\S+)\s*(.*)$/; // 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(); + } + + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + view.triggerMethod.apply(view, [eventName, view, event].concat(args)); + }; +} + +var ViewEventsMixin = { + EventDelegator: EventDelegator, + _initViewEvents: function _initViewEvents() { + this._domEvents = []; + }, + _undelegateViewEvents: function _undelegateViewEvents() { + this.EventDelegator.undelegateAll({ + events: this._domEvents, + rootEl: this.el + }); + }, + _delegateViewEvents: function _delegateViewEvents() { + var view = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this; + + var uiBindings = this._getUIBindings(); + + this._delegateEvents(uiBindings); + + this._delegateTriggers(uiBindings, view); + }, + _delegateEvents: function _delegateEvents(uiBindings) { + var _this = this; + + if (!this.events) { + return; + } + + each(result(this, 'events'), function (handler, key) { + if (!isFunction(handler)) { + handler = _this[handler]; + } + + if (!handler) { + return; + } + + _this._delegate(handler.bind(_this), _this.normalizeUIString(key, uiBindings)); + }); + }, + _delegateTriggers: function _delegateTriggers(uiBindings, view) { + var _this2 = this; + + if (!this.triggers) { + return; + } + + each(result(this, 'triggers'), function (value, key) { + _this2._delegate(buildViewTrigger(view, value), _this2.normalizeUIString(key, uiBindings)); + }); + }, + _delegate: function _delegate(handler, key) { + var match = key.match(delegateEventSplitter); + this.EventDelegator.delegate({ + eventName: match[1], + selector: match[2], + handler: handler, + events: this._domEvents, + rootEl: this.el + }); + } +}; + +// DomApi function setDomApi(mixin) { - this.prototype.Dom = _.extend({}, this.prototype.Dom, mixin); + this.prototype.Dom = extend$1({}, this.prototype.Dom, mixin); return this; } var DomApi = { + // Returns a new HTML DOM node of tagName + createElement: function createElement(tagName) { + return document.createElement(tagName); + }, // Returns a new HTML DOM node instance createBuffer: function createBuffer() { return document.createDocumentFragment(); @@ -866,16 +1441,10 @@ var DomApi = { getDocumentEl: function getDocumentEl(el) { return el.ownerDocument.documentElement; }, - // Lookup the `selector` string - // Selector may also be a DOM element - // Returns an array-like object of nodes - getEl: function getEl(selector) { - return _getEl(selector); - }, // Finds the `selector` string with the el // Returns an array-like object of nodes findEl: function findEl(el, selector) { - return _getEl(el).find(selector); + return el.querySelectorAll(selector); }, // Returns true if the el contains the node childEl hasEl: function hasEl(el, childEl) { @@ -883,9 +1452,9 @@ var DomApi = { }, // 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(); + if (el.parentNode) { + el.parentNode.removeChild(el); + } }, // Remove `oldEl` from the DOM and put `newEl` in its place replaceEl: function replaceEl(newEl, oldEl) { @@ -919,22 +1488,20 @@ var DomApi = { parent1.insertBefore(el2, next1); parent2.insertBefore(el1, next2); }, - // Replace the contents of `el` with the HTML string of `html` + // Replace the contents of `el` with the `html` setContents: function setContents(el, html) { - var _$el = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : _getEl(el); - - _$el.html(html); + el.innerHTML = html; + }, + // Sets attributes on a DOM node + setAttributes: function setAttributes(el, attrs) { + each(keys(attrs), function (attr) { + attr in el ? el[attr] = attrs[attr] : el.setAttribute(attr, attrs[attr]); + }); }, // Takes the DOM node `el` and appends the DOM node `contents` // to the end of the element's contents. appendContents: function appendContents(el, contents) { - var _ref = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, - _ref$_$el = _ref._$el, - _$el = _ref$_$el === void 0 ? _getEl(el) : _ref$_$el, - _ref$_$contents = _ref._$contents, - _$contents = _ref$_$contents === void 0 ? _getEl(contents) : _ref$_$contents; - - _$el.append(_$contents); + el.appendChild(contents); }, // Does the el have child nodes hasContents: function hasContents(el) { @@ -943,24 +1510,56 @@ var DomApi = { // 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(); + el.textContent = ''; } }; // ViewMixin +// - attributes // - behaviors // - childViewEventPrefix // - childViewEvents // - childViewTriggers +// - className +// - collection // - collectionEvents +// - el +// - events +// - id +// - model // - modelEvents +// - tagName // - triggers // - ui var ViewMixin = { + tagName: 'div', + // This is a noop method intended to be overridden + preinitialize: function preinitialize() {}, Dom: DomApi, + // Create an element from the `id`, `className` and `tagName` properties. + _getEl: function _getEl() { + if (!this.el) { + var el = this.Dom.createElement(result(this, 'tagName')); + var attrs = extend$1({}, result(this, 'attributes')); + + if (this.id) { + attrs.id = result(this, 'id'); + } + + if (this.className) { + attrs.class = result(this, 'className'); + } + + this.Dom.setAttributes(el, attrs); + return el; + } + + return result(this, 'el'); + }, + $: function $(selector) { + return this.Dom.findEl(this.el, selector); + }, _isElAttached: function _isElAttached() { return !!this.el && this.Dom.hasEl(this.Dom.getDocumentEl(this.el), this.el); }, @@ -978,43 +1577,6 @@ var ViewMixin = { isAttached: function isAttached() { return !!this._isAttached; }, - // Overriding Backbone.View's `delegateEvents` to handle - // `events` and `triggers` - delegateEvents: function delegateEvents(events) { - this._proxyBehaviorViewProperties(); - - this._buildEventProxies(); - - var combinedEvents = _.extend({}, this._getBehaviorEvents(), this._getEvents(events), this._getBehaviorTriggers(), this._getTriggers()); - - Backbone.View.prototype.delegateEvents.call(this, combinedEvents); - return this; - }, - // Allows Backbone.View events to utilize `@ui.` selectors - _getEvents: function _getEvents(events) { - if (events) { - return this.normalizeUIKeys(events); - } - - if (!this.events) { - return; - } - - return this.normalizeUIKeys(_.result(this, 'events')); - }, - // Configure `triggers` to forward DOM events to view - // events. `triggers: {"click .foo": "do:foo"}` - _getTriggers: function _getTriggers() { - if (!this.triggers) { - return; - } // Allow `triggers` to be configured as a function - - - var triggers = this.normalizeUIKeys(_.result(this, 'triggers')); // Configure the triggers, prevent default - // action and stop propagation of DOM events - - return this._getViewTriggers(this, triggers); - }, // Handle `modelEvents`, and `collectionEvents` configuration delegateEntityEvents: function delegateEntityEvents() { this._delegateEntityEvents(this.model, this.collection); // bind each behaviors model and collection events @@ -1048,9 +1610,12 @@ var ViewMixin = { } // unbind UI elements - this.unbindUIElements(); // remove the view from the DOM + this.unbindUIElements(); + + this._undelegateViewEvents(); // remove the view from the DOM + - this._removeElement(); + this.Dom.detachEl(this.el); if (shouldTriggerDetach) { this._isAttached = false; @@ -1074,11 +1639,6 @@ var ViewMixin = { this.stopListening(); return this; }, - // Equates to this.$el.remove - _removeElement: function _removeElement() { - this.$el.off().removeData(); - this.Dom.detachEl(this.el, this.$el); - }, // This method binds the elements specified in the "ui" hash bindUIElements: function bindUIElements() { this._bindUIElements(); @@ -1100,15 +1660,13 @@ var ViewMixin = { }, // Cache `childViewEvents` and `childViewTriggers` _buildEventProxies: function _buildEventProxies() { - this._childViewEvents = this.normalizeMethods(_.result(this, 'childViewEvents')); - this._childViewTriggers = _.result(this, 'childViewTriggers'); + this._childViewEvents = this.normalizeMethods(result(this, 'childViewEvents')); + this._childViewTriggers = result(this, 'childViewTriggers'); this._eventPrefix = this._getEventPrefix(); }, _getEventPrefix: function _getEventPrefix() { var defaultPrefix = isEnabled('childViewEventPrefix') ? 'childview' : false; - - var prefix = _.result(this, 'childViewEventPrefix', defaultPrefix); - + var prefix = result(this, 'childViewEventPrefix', defaultPrefix); return prefix === false ? prefix : prefix + ':'; }, _proxyChildViewEvents: function _proxyChildViewEvents(view) { @@ -1139,9 +1697,14 @@ var ViewMixin = { } } }; +extend$1(ViewMixin, BehaviorsMixin, CommonMixin, DelegateEntityEventsMixin, TemplateRenderMixin, UIMixin, ViewEventsMixin); -_.extend(ViewMixin, BehaviorsMixin, CommonMixin, DelegateEntityEventsMixin, TemplateRenderMixin, TriggersMixin, UIMixin); - +function isView(view) { + return view.render && (view.destroy || view.remove); +} +function isViewClass(ViewClass) { + return ViewClass.prototype.render && (ViewClass.prototype.destroy || ViewClass.prototype.remove); +} function renderView(view) { if (view._isRendered) { return; @@ -1198,12 +1761,9 @@ var ClassOptions$1 = ['allowMissingEl', 'parentEl', 'replaceElement']; var Region = function Region(options) { this._setOptions(options, ClassOptions$1); - this.cid = _.uniqueId(this.cidPrefix); // getOption necessary because options.el may be passed as undefined + this.cid = uniqueId(this.cidPrefix); // getOption necessary because options.el may be passed as undefined - this._initEl = this.el = this.getOption('el'); // Handle when this.el is passed in as a $ wrapped element. - - this.el = this.el instanceof Backbone.$ ? this.el[0] : this.el; - this.$el = this._getEl(this.el); + this._initEl = this.el = this.getOption('el'); this.initialize.apply(this, arguments); }; @@ -1211,14 +1771,12 @@ Region.extend = extend; Region.setDomApi = setDomApi; // Region Methods // -------------- -_.extend(Region.prototype, CommonMixin, { +extend$1(Region.prototype, CommonMixin, { Dom: DomApi, cidPrefix: 'mnr', replaceElement: false, _isReplaced: false, _isSwappingView: false, - // This is a noop method intended to be overridden - initialize: function initialize() {}, // Displays a view instance inside of the region. If necessary handles calling the `render` // method for you. Reads content directly from the `el` attribute. show: function show(view, options) { @@ -1258,7 +1816,12 @@ _.extend(Region.prototype, CommonMixin, { this._isSwappingView = false; return this; }, - _getEl: function _getEl(el) { + _setEl: function _setEl(el) { + if (isObject(el)) { + this.el = el; + return; + } + if (!el) { throw new MarionetteError({ name: classErrorName, @@ -1267,19 +1830,7 @@ _.extend(Region.prototype, CommonMixin, { }); } - return this.getEl(el); - }, - _setEl: function _setEl() { - this.$el = this._getEl(this.el); - - if (this.$el.length) { - this.el = this.$el[0]; - } // Make sure the $el contains only the el - - - if (this.$el.length > 1) { - this.$el = this.Dom.getEl(this.el); - } + this.el = this.getEl(el); }, // Set the `el` of the region and move any current view to the new `el`. _setElement: function _setElement(el) { @@ -1291,9 +1842,7 @@ _.extend(Region.prototype, CommonMixin, { this._restoreEl(); - this.el = el; - - this._setEl(); + this._setEl(el); if (this.currentView) { var view = this.currentView; @@ -1338,7 +1887,7 @@ _.extend(Region.prototype, CommonMixin, { replaceElement = _ref.replaceElement; var shouldTriggerAttach = !view._isAttached && this._isElAttached() && !this._shouldDisableMonitoring(); - var shouldReplaceEl = typeof replaceElement === 'undefined' ? !!_.result(this, 'replaceElement') : !!replaceElement; + var shouldReplaceEl = typeof replaceElement === 'undefined' ? !!result(this, 'replaceElement') : !!replaceElement; if (shouldTriggerAttach) { view.triggerMethod('before:attach', view); @@ -1361,12 +1910,10 @@ _.extend(Region.prototype, CommonMixin, { _ensureElement: function _ensureElement() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - if (!_.isObject(this.el)) { - this._setEl(); - } + this._setEl(this.el); - if (!this.$el || this.$el.length === 0) { - var allowMissingEl = typeof options.allowMissingEl === 'undefined' ? !!_.result(this, 'allowMissingEl') : !!options.allowMissingEl; + if (!this.el) { + var allowMissingEl = typeof options.allowMissingEl === 'undefined' ? !!result(this, 'allowMissingEl') : !!options.allowMissingEl; if (allowMissingEl) { return false; @@ -1398,7 +1945,7 @@ _.extend(Region.prototype, CommonMixin, { }); } - if (view instanceof Backbone.View) { + if (isView(view)) { return view; } @@ -1409,13 +1956,13 @@ _.extend(Region.prototype, CommonMixin, { // This allows for a template or a static string to be // used as a template _getViewOptions: function _getViewOptions(viewOptions) { - if (_.isFunction(viewOptions)) { + if (isFunction(viewOptions)) { return { template: viewOptions }; } - if (_.isObject(viewOptions)) { + if (isObject(viewOptions)) { return viewOptions; } @@ -1430,13 +1977,8 @@ _.extend(Region.prototype, CommonMixin, { // Override this method to change how the region finds the DOM element that it manages. Return // a jQuery selector object scoped to a provided parent el or the document if none exists. getEl: function getEl(el) { - var context = _.result(this, 'parentEl'); - - if (context && _.isString(el)) { - return this.Dom.findEl(context, el); - } - - return this.Dom.getEl(el); + var context = result(this, 'parentEl'); + return this.Dom.findEl(context || document, el)[0]; }, _replaceEl: function _replaceEl(view) { // Always restore the el to ensure the regions el is present before replacing @@ -1474,10 +2016,7 @@ _.extend(Region.prototype, CommonMixin, { // 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 - }); + this.Dom.appendContents(this.el, view.el); }, // Destroy the current view, if there is one. If there is no current view, // it will detach any html inside the region's `el`. @@ -1579,7 +2118,7 @@ _.extend(Region.prototype, CommonMixin, { }, // Override this method to change how the region detaches current content detachHtml: function detachHtml() { - this.Dom.detachContents(this.el, this.$el); + this.Dom.detachContents(this.el); }, // Checks whether a view is currently present within the region. Returns `true` if there is // and `false` if no view is present. @@ -1800,7 +2339,7 @@ function setRenderer(renderer) { } // View -var ClassOptions$2 = ['behaviors', 'childViewEventPrefix', 'childViewEvents', 'childViewTriggers', 'collectionEvents', 'events', 'modelEvents', 'regionClass', 'regions', 'template', 'templateContext', 'triggers', 'ui']; // Used by _getImmediateChildren +var ClassOptions$2 = ['attributes', 'behaviors', 'childViewEventPrefix', 'childViewEvents', 'childViewTriggers', 'className', 'collection', 'collectionEvents', 'el', 'events', 'id', 'model', 'modelEvents', 'regionClass', 'regions', 'tagName', 'template', 'templateContext', 'triggers', 'ui']; // Used by _getImmediateChildren function childReducer(children, region) { if (region.currentView) { @@ -1812,26 +2351,46 @@ function childReducer(children, region) { // templates, nested views, and more. -var View = Backbone.View.extend({ - constructor: function constructor(options) { - this._setOptions(options, ClassOptions$2); +var View = function View(options) { + this.cid = uniqueId(this.cidPrefix); - monitorViewEvents(this); + this._setOptions(options, ClassOptions$2); - this._initBehaviors(); + this.preinitialize.apply(this, arguments); - this._initRegions(); + this._initViewEvents(); - Backbone.View.prototype.constructor.apply(this, arguments); - this.delegateEntityEvents(); + this.setElement(this._getEl()); + monitorViewEvents(this); + + this._initBehaviors(); + + this._initRegions(); + + this._buildEventProxies(); + + this.initialize.apply(this, arguments); + this.delegateEntityEvents(); + + this._triggerEventOnBehaviors('initialize', this, options); +}; + +extend$1(View, { + extend: extend, + setRenderer: setRenderer, + setDomApi: setDomApi, + setEventDelegator: setEventDelegator +}); + +extend$1(View.prototype, ViewMixin, RegionsMixin, { + cidPrefix: 'mnv', + setElement: function setElement(element) { + this._undelegateViewEvents(); + + this.el = element; + + this._setBehaviorElements(); - this._triggerEventOnBehaviors('initialize', this, options); - }, - // 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() { - Backbone.View.prototype.setElement.apply(this, arguments); this._isRendered = this.Dom.hasContents(this.el); this._isAttached = this._isElAttached(); @@ -1839,6 +2398,8 @@ var View = Backbone.View.extend({ this.bindUIElements(); } + this._delegateViewEvents(); + return this; }, // If a template is available, renders it into the view's `el` @@ -1869,15 +2430,10 @@ var View = Backbone.View.extend({ this.removeRegions(); }, _getImmediateChildren: function _getImmediateChildren() { - return _.reduce(this._regions, childReducer, []); + return reduce(this._regions, childReducer, []); } -}, { - setRenderer: setRenderer, - setDomApi: setDomApi }); -_.extend(View.prototype, ViewMixin, RegionsMixin); - // shut down child views. var Container = function Container() { @@ -2038,29 +2594,46 @@ _.extend(Container.prototype, { // Collection View var classErrorName$1 = 'CollectionViewError'; -var ClassOptions$3 = ['behaviors', 'childView', 'childViewContainer', 'childViewEventPrefix', 'childViewEvents', 'childViewOptions', 'childViewTriggers', 'collectionEvents', 'emptyView', 'emptyViewOptions', 'events', 'modelEvents', 'sortWithCollection', 'template', 'templateContext', 'triggers', 'ui', 'viewComparator', 'viewFilter']; // A view that iterates over a Backbone.Collection +var ClassOptions$3 = ['attributes', 'behaviors', 'childView', 'childViewContainer', 'childViewEventPrefix', 'childViewEvents', 'childViewOptions', 'childViewTriggers', 'className', 'collection', 'collectionEvents', 'el', 'emptyView', 'emptyViewOptions', 'events', 'id', 'model', 'modelEvents', 'sortWithCollection', 'tagName', 'template', 'templateContext', 'triggers', 'ui', 'viewComparator', 'viewFilter']; // 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 - sortWithCollection: true, - // constructor - constructor: function constructor(options) { - this._setOptions(options, ClassOptions$3); +var CollectionView = function CollectionView(options) { + this.cid = uniqueId(this.cidPrefix); - monitorViewEvents(this); + this._setOptions(options, ClassOptions$3); - this._initChildViewStorage(); + this.preinitialize.apply(this, arguments); - this._initBehaviors(); + this._initViewEvents(); - Backbone.View.prototype.constructor.apply(this, arguments); // Init empty region + this.setElement(this._getEl()); + monitorViewEvents(this); - this.getEmptyRegion(); - this.delegateEntityEvents(); + this._initChildViewStorage(); - this._triggerEventOnBehaviors('initialize', this, options); - }, + this._initBehaviors(); + + this._buildEventProxies(); // Init empty region + + + this.getEmptyRegion(); + this.initialize.apply(this, arguments); + this.delegateEntityEvents(); + + this._triggerEventOnBehaviors('initialize', this, options); +}; + +extend$1(CollectionView, { + extend: extend, + setRenderer: setRenderer, + setDomApi: setDomApi, + setEventDelegator: setEventDelegator +}); + +extend$1(CollectionView.prototype, ViewMixin, { + cidPrefix: 'mncv', + // flag for maintaining the sorted order of the collection + sortWithCollection: true, // Internal method to set up the `children` object for storing all of the child views // `_children` represents all child views // `children` represents only views filtered to be shown @@ -2070,16 +2643,16 @@ var CollectionView = Backbone.View.extend({ }, // Create an region to show the emptyView getEmptyRegion: function getEmptyRegion() { - var $emptyEl = this.$container || this.$el; + var emptyEl = this.container || this.el; if (this._emptyRegion && !this._emptyRegion.isDestroyed()) { - this._emptyRegion._setElement($emptyEl[0]); + this._emptyRegion._setElement(emptyEl); return this._emptyRegion; } this._emptyRegion = new Region({ - el: $emptyEl[0], + el: emptyEl, replaceElement: false }); this._emptyRegion._parentView = this; @@ -2140,7 +2713,7 @@ var CollectionView = Backbone.View.extend({ _removeChildModels: function _removeChildModels(models) { var _this = this; - return _.reduce(models, function (views, model) { + return reduce(models, function (views, model) { var removeView = _this._removeChildModel(model); if (removeView) { @@ -2170,7 +2743,7 @@ var CollectionView = Backbone.View.extend({ }, // Added views are returned for consistency with _removeChildModels _addChildModels: function _addChildModels(models) { - return _.map(models, this._addChildModel.bind(this)); + return map(models, this._addChildModel.bind(this)); }, _addChildModel: function _addChildModel(model) { var view = this._createChildView(model); @@ -2228,14 +2801,14 @@ var CollectionView = Backbone.View.extend({ // 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) { + if (isViewClass(view)) { return view; - } else if (_.isFunction(view)) { + } else if (isFunction(view)) { return view.call(this, child); } }, _getChildViewOptions: function _getChildViewOptions(child) { - if (_.isFunction(this.childViewOptions)) { + if (isFunction(this.childViewOptions)) { return this.childViewOptions(child); } @@ -2244,7 +2817,7 @@ var CollectionView = Backbone.View.extend({ // Build a `childView` for a model in the collection. // Override to customize the build buildChildView: function buildChildView(child, ChildViewClass, childViewOptions) { - var options = _.extend({ + var options = extend$1({ model: child }, childViewOptions); @@ -2267,9 +2840,17 @@ var CollectionView = Backbone.View.extend({ // 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() { - Backbone.View.prototype.setElement.apply(this, arguments); + setElement: function setElement(element) { + this._undelegateViewEvents(); + + this.el = element; + + this._setBehaviorElements(); + this._isAttached = this._isElAttached(); + + this._delegateViewEvents(); + return this; }, // Render children views. @@ -2305,11 +2886,10 @@ var CollectionView = Backbone.View.extend({ }, // Get a container within the template to add the children within _getChildViewContainer: function _getChildViewContainer() { - var childViewContainer = _.result(this, 'childViewContainer'); + var childViewContainer = result(this, 'childViewContainer'); + this.container = childViewContainer ? this.$(childViewContainer)[0] : this.el; - this.$container = childViewContainer ? this.$(childViewContainer) : this.$el; - - if (!this.$container.length) { + if (!this.container) { throw new MarionetteError({ name: classErrorName$1, message: "The specified \"childViewContainer\" was not found: ".concat(childViewContainer), @@ -2364,7 +2944,7 @@ var CollectionView = Backbone.View.extend({ removeComparator: function removeComparator(options) { return this.setComparator(null, options); }, - // If viewComparator is overriden it will be returned here. + // If viewComparator is overridden it will be returned here. // Additionally override this function to provide custom // viewComparator logic getComparator: function getComparator() { @@ -2417,8 +2997,7 @@ var CollectionView = Backbone.View.extend({ this.triggerMethod('before:filter', this); var attachViews = []; var detachViews = []; - - _.each(this._children._views, function (view, key, children) { + each(this._children._views, function (view, key, children) { (viewFilter.call(_this2, view, key, children) ? attachViews : detachViews).push(view); }); @@ -2437,21 +3016,20 @@ var CollectionView = Backbone.View.extend({ return false; } - if (_.isFunction(viewFilter)) { + if (isFunction(viewFilter)) { return viewFilter; } // Support filter predicates `{ fooFlag: true }` - if (_.isObject(viewFilter)) { - var matcher = _.matches(viewFilter); - + if (isObject(viewFilter)) { + var matcher = matches(viewFilter); return function (view) { return matcher(view.model && view.model.attributes); }; } // Filter by model attribute - if (_.isString(viewFilter)) { + if (isString(viewFilter)) { return function (view) { return view.model && view.model.get(viewFilter); }; @@ -2489,7 +3067,7 @@ var CollectionView = Backbone.View.extend({ return this.setFilter(null, options); }, _detachChildren: function _detachChildren(detachingViews) { - _.each(detachingViews, this._detachChildView.bind(this)); + each(detachingViews, this._detachChildView.bind(this)); }, _detachChildView: function _detachChildView(view) { var shouldTriggerDetach = view._isAttached && this.monitorViewEvents !== false; @@ -2509,7 +3087,7 @@ var CollectionView = Backbone.View.extend({ }, // Override this method to change how the collectionView detaches a child view detachHtml: function detachHtml(view) { - this.Dom.detachEl(view.el, view.$el); + this.Dom.detachEl(view.el); }, _renderChildren: function _renderChildren() { // If there are unrendered views prevent add to end perf @@ -2539,34 +3117,27 @@ var CollectionView = Backbone.View.extend({ var _this3 = this; var elBuffer = this.Dom.createBuffer(); - - _.each(views, function (view) { + each(views, function (view) { renderView(view); // corresponds that view is shown in a Region or CollectionView view._isShown = true; - _this3.Dom.appendContents(elBuffer, view.el, { - _$contents: view.$el - }); + _this3.Dom.appendContents(elBuffer, view.el); }); - return elBuffer; }, _attachChildren: function _attachChildren(els, views) { var shouldTriggerAttach = this._isAttached && this.monitorViewEvents !== false; views = shouldTriggerAttach ? views : []; - - _.each(views, function (view) { + each(views, function (view) { if (view._isAttached) { return; } view.triggerMethod('before:attach', view); }); - - this.attachHtml(els, this.$container); - - _.each(views, function (view) { + this.attachHtml(els, this.container); + each(views, function (view) { if (view._isAttached) { return; } @@ -2577,10 +3148,8 @@ var CollectionView = Backbone.View.extend({ }, // Override this method to do something other than `.append`. // You can attach any HTML at this point including the els. - attachHtml: function attachHtml(els, $container) { - this.Dom.appendContents($container[0], els, { - _$el: $container - }); + attachHtml: function attachHtml(els, container) { + this.Dom.appendContents(container, els); }, isEmpty: function isEmpty() { return !this.children.length; @@ -2620,7 +3189,7 @@ var CollectionView = Backbone.View.extend({ _getEmptyViewOptions: function _getEmptyViewOptions() { var emptyViewOptions = this.emptyViewOptions || this.childViewOptions; - if (_.isFunction(emptyViewOptions)) { + if (isFunction(emptyViewOptions)) { return emptyViewOptions.call(this); } @@ -2663,7 +3232,7 @@ var CollectionView = Backbone.View.extend({ }); } - if (_.isObject(index)) { + if (isObject(index)) { options = index; } // If options has defined index we should use it @@ -2725,7 +3294,7 @@ var CollectionView = Backbone.View.extend({ return view; }, _removeChildViews: function _removeChildViews(views) { - _.each(views, this._removeChildView.bind(this)); + each(views, this._removeChildView.bind(this)); }, _removeChildView: function _removeChildView(view) { var _ref4 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, @@ -2766,7 +3335,7 @@ var CollectionView = Backbone.View.extend({ this.triggerMethod('before:destroy:children', this); if (this.monitorViewEvents === false) { - this.Dom.detachContents(this.el, this.$el); + this.Dom.detachContents(this.el); } this._removeChildViews(this._children._views); // After all children have been destroyed re-init the container @@ -2778,13 +3347,8 @@ var CollectionView = Backbone.View.extend({ this.triggerMethod('destroy:children', this); } -}, { - setDomApi: setDomApi, - setRenderer: setRenderer }); -_.extend(CollectionView.prototype, ViewMixin); - // Behavior var ClassOptions$4 = ['collectionEvents', 'events', 'modelEvents', 'triggers', 'ui']; @@ -2797,7 +3361,11 @@ var Behavior = function Behavior(options, view) { this._setOptions(options, ClassOptions$4); - this.cid = _.uniqueId(this.cidPrefix); // Construct an internal UI hash using the behaviors UI + this.cid = uniqueId(this.cidPrefix); + + this._initViewEvents(); + + this.setElement(); // Construct an internal UI hash using the behaviors UI // hash combined and overridden by 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 behavior. @@ -2805,7 +3373,7 @@ var Behavior = function Behavior(options, view) { // between multiple views, while letting a view override // a selector under an UI key. - this.ui = _.extend({}, _.result(this, 'ui'), _.result(view, 'ui')); // Proxy view triggers + this.ui = extend$1({}, result(this, 'ui'), result(view, 'ui')); // Proxy view triggers this.listenTo(view, 'all', this.triggerMethod); this.initialize.apply(this, arguments); @@ -2814,10 +3382,8 @@ var Behavior = function Behavior(options, view) { Behavior.extend = extend; // Behavior Methods // -------------- -_.extend(Behavior.prototype, CommonMixin, DelegateEntityEventsMixin, TriggersMixin, UIMixin, { +extend$1(Behavior.prototype, CommonMixin, DelegateEntityEventsMixin, UIMixin, ViewEventsMixin, { cidPrefix: 'mnb', - // This is a noop method intended to be overridden - initialize: function initialize() {}, // proxy behavior $ method to the view // this is useful for doing jquery DOM lookups // scoped to behaviors view. @@ -2826,6 +3392,8 @@ _.extend(Behavior.prototype, CommonMixin, DelegateEntityEventsMixin, TriggersMix }, // Stops the behavior from listening to events. destroy: function destroy() { + this._undelegateViewEvents(); + this.stopListening(); this.view._removeBehavior(this); @@ -2834,9 +3402,13 @@ _.extend(Behavior.prototype, CommonMixin, DelegateEntityEventsMixin, TriggersMix return this; }, - proxyViewProperties: function proxyViewProperties() { - this.$el = this.view.$el; + setElement: function setElement() { + this._undelegateViewEvents(); + this.el = this.view.el; + + this._delegateViewEvents(this.view); + return this; }, bindUIElements: function bindUIElements() { @@ -2862,42 +3434,6 @@ _.extend(Behavior.prototype, CommonMixin, DelegateEntityEventsMixin, TriggersMix this._undelegateEntityEvents(this.view.model, this.view.collection); return this; - }, - _getEvents: function _getEvents() { - var _this = this; - - if (!this.events) { - return; - } // 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 events; - } - - key = getNamespacedEventName(key, _this.cid); - events[key] = behaviorHandler.bind(_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); } }); diff --git a/src/behavior.js b/src/behavior.js index 5102a19237..436d7212b2 100644 --- a/src/behavior.js +++ b/src/behavior.js @@ -6,13 +6,12 @@ // Behaviors allow you to blackbox View specific interactions // into portable logical chunks, keeping your views simple and your code DRY. -import _ from 'underscore'; +import { extend as _extend, uniqueId, result } from 'underscore'; import extend from './utils/extend'; -import getNamespacedEventName from './utils/get-namespaced-event-name'; import CommonMixin from './mixins/common'; import DelegateEntityEventsMixin from './mixins/delegate-entity-events'; -import TriggersMixin from './mixins/triggers'; import UIMixin from './mixins/ui'; +import ViewEventsMixin from './mixins/view-events'; const ClassOptions = [ 'collectionEvents', @@ -29,8 +28,12 @@ const Behavior = function(options, view) { // to the view. this.view = view; + this._setOptions(options, ClassOptions); - this.cid = _.uniqueId(this.cidPrefix); + this.cid = uniqueId(this.cidPrefix); + + this._initViewEvents(); + this.setElement(); // Construct an internal UI hash using the behaviors UI // hash combined and overridden by the view UI hash. @@ -39,7 +42,7 @@ const Behavior = function(options, view) { // 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')); + this.ui = _extend({}, result(this, 'ui'), result(view, 'ui')); // Proxy view triggers this.listenTo(view, 'all', this.triggerMethod); @@ -52,12 +55,9 @@ Behavior.extend = extend; // Behavior Methods // -------------- -_.extend(Behavior.prototype, CommonMixin, DelegateEntityEventsMixin, TriggersMixin, UIMixin, { +_extend(Behavior.prototype, CommonMixin, DelegateEntityEventsMixin, UIMixin, ViewEventsMixin, { cidPrefix: 'mnb', - // This is a noop method intended to be overridden - initialize() {}, - // proxy behavior $ method to the view // this is useful for doing jquery DOM lookups // scoped to behaviors view. @@ -67,6 +67,8 @@ _.extend(Behavior.prototype, CommonMixin, DelegateEntityEventsMixin, TriggersMix // Stops the behavior from listening to events. destroy() { + this._undelegateViewEvents(); + this.stopListening(); this.view._removeBehavior(this); @@ -76,10 +78,13 @@ _.extend(Behavior.prototype, CommonMixin, DelegateEntityEventsMixin, TriggersMix return this; }, - proxyViewProperties() { - this.$el = this.view.$el; + setElement() { + this._undelegateViewEvents(); + this.el = this.view.el; + this._delegateViewEvents(this.view); + return this; }, @@ -110,36 +115,6 @@ _.extend(Behavior.prototype, CommonMixin, DelegateEntityEventsMixin, TriggersMix this._undelegateEntityEvents(this.view.model, this.view.collection); return this; - }, - - _getEvents() { - if (!this.events) { return; } - - // Normalize behavior events hash to allow - // a user to use the @ui. syntax. - const behaviorEvents = this.normalizeUIKeys(_.result(this, 'events')); - - // binds the handler to the behavior and builds a unique eventName - return _.reduce(behaviorEvents, (events, behaviorHandler, key) => { - if (!_.isFunction(behaviorHandler)) { - behaviorHandler = this[behaviorHandler]; - } - if (!behaviorHandler) { return events; } - key = getNamespacedEventName(key, this.cid); - events[key] = behaviorHandler.bind(this); - return events; - }, {}); - }, - - // Internal method to build all trigger handlers for a given behavior - _getTriggers() { - if (!this.triggers) { return; } - - // Normalize behavior triggers hash to allow - // a user to use the @ui. syntax. - const behaviorTriggers = this.normalizeUIKeys(_.result(this, 'triggers')); - - return this._getViewTriggers(this.view, behaviorTriggers); } }); diff --git a/src/collection-view.js b/src/collection-view.js index 1bd64c15f8..1cef07c333 100644 --- a/src/collection-view.js +++ b/src/collection-view.js @@ -1,20 +1,22 @@ // Collection View // --------------- -import _ from 'underscore'; -import Backbone from 'backbone'; +import { extend as _extend, uniqueId, result, map, isFunction, isObject, isString, matches, each, reduce } from 'underscore'; +import extend from './utils/extend'; import MarionetteError from './utils/error'; -import { renderView, destroyView } from './common/view'; +import { renderView, destroyView, isViewClass } from './common/view'; import monitorViewEvents from './common/monitor-view-events'; import ChildViewContainer from './child-view-container'; import Region from './region'; import ViewMixin from './mixins/view'; import { setDomApi } from './config/dom'; +import { setEventDelegator } from './config/event-delegator'; import { setRenderer } from './config/renderer'; const classErrorName = 'CollectionViewError'; const ClassOptions = [ + 'attributes', 'behaviors', 'childView', 'childViewContainer', @@ -22,12 +24,18 @@ const ClassOptions = [ 'childViewEvents', 'childViewOptions', 'childViewTriggers', + 'className', + 'collection', 'collectionEvents', + 'el', 'emptyView', 'emptyViewOptions', 'events', + 'id', + 'model', 'modelEvents', 'sortWithCollection', + 'tagName', 'template', 'templateContext', 'triggers', @@ -38,28 +46,38 @@ const ClassOptions = [ // A view that iterates over a Backbone.Collection // and renders an individual child view for each model. -const CollectionView = Backbone.View.extend({ - // flag for maintaining the sorted order of the collection - sortWithCollection: true, +const CollectionView = function(options) { + this.cid = uniqueId(this.cidPrefix); + this._setOptions(options, ClassOptions); - // constructor - constructor(options) { - this._setOptions(options, ClassOptions); + this.preinitialize.apply(this, arguments); - monitorViewEvents(this); + this._initViewEvents(); + this.setElement(this._getEl()); - this._initChildViewStorage(); - this._initBehaviors(); + monitorViewEvents(this); - Backbone.View.prototype.constructor.apply(this, arguments); + this._initChildViewStorage(); + this._initBehaviors(); + this._buildEventProxies(); - // Init empty region - this.getEmptyRegion(); + // Init empty region + this.getEmptyRegion(); - this.delegateEntityEvents(); + this.initialize.apply(this, arguments); - this._triggerEventOnBehaviors('initialize', this, options); - }, + this.delegateEntityEvents(); + + this._triggerEventOnBehaviors('initialize', this, options); +}; + +_extend(CollectionView, { extend, setRenderer, setDomApi, setEventDelegator }); + +_extend(CollectionView.prototype, ViewMixin, { + cidPrefix: 'mncv', + + // flag for maintaining the sorted order of the collection + sortWithCollection: true, // Internal method to set up the `children` object for storing all of the child views // `_children` represents all child views @@ -71,14 +89,14 @@ const CollectionView = Backbone.View.extend({ // Create an region to show the emptyView getEmptyRegion() { - const $emptyEl = this.$container || this.$el; + const emptyEl = this.container || this.el; if (this._emptyRegion && !this._emptyRegion.isDestroyed()) { - this._emptyRegion._setElement($emptyEl[0]); + this._emptyRegion._setElement(emptyEl); return this._emptyRegion; } - this._emptyRegion = new Region({ el: $emptyEl[0], replaceElement: false }); + this._emptyRegion = new Region({ el: emptyEl, replaceElement: false }); this._emptyRegion._parentView = this; @@ -138,7 +156,7 @@ const CollectionView = Backbone.View.extend({ }, _removeChildModels(models) { - return _.reduce(models, (views, model) => { + return reduce(models, (views, model) => { const removeView = this._removeChildModel(model); if (removeView) { views.push(removeView); } @@ -166,7 +184,7 @@ const CollectionView = Backbone.View.extend({ // Added views are returned for consistency with _removeChildModels _addChildModels(models) { - return _.map(models, this._addChildModel.bind(this)); + return map(models, this._addChildModel.bind(this)); }, _addChildModel(model) { @@ -226,15 +244,15 @@ const CollectionView = Backbone.View.extend({ // 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(view, child) { - if (view.prototype instanceof Backbone.View || view === Backbone.View) { + if (isViewClass(view)) { return view; - } else if (_.isFunction(view)) { + } else if (isFunction(view)) { return view.call(this, child); } }, _getChildViewOptions(child) { - if (_.isFunction(this.childViewOptions)) { + if (isFunction(this.childViewOptions)) { return this.childViewOptions(child); } @@ -244,7 +262,7 @@ const CollectionView = Backbone.View.extend({ // Build a `childView` for a model in the collection. // Override to customize the build buildChildView(child, ChildViewClass, childViewOptions) { - const options = _.extend({model: child}, childViewOptions); + const options = _extend({model: child}, childViewOptions); return new ChildViewClass(options); }, @@ -269,11 +287,15 @@ const CollectionView = Backbone.View.extend({ // Overriding Backbone.View's `setElement` to handle // if an el was previously defined. If so, the view might be // attached on setElement. - setElement() { - Backbone.View.prototype.setElement.apply(this, arguments); + setElement(element) { + this._undelegateViewEvents(); + this.el = element; + this._setBehaviorElements(); this._isAttached = this._isElAttached(); + this._delegateViewEvents(); + return this; }, @@ -306,10 +328,10 @@ const CollectionView = Backbone.View.extend({ // Get a container within the template to add the children within _getChildViewContainer() { - const childViewContainer = _.result(this, 'childViewContainer'); - this.$container = childViewContainer ? this.$(childViewContainer) : this.$el; + const childViewContainer = result(this, 'childViewContainer'); + this.container = childViewContainer ? this.$(childViewContainer)[0] : this.el; - if (!this.$container.length) { + if (!this.container) { throw new MarionetteError({ name: classErrorName, message: `The specified "childViewContainer" was not found: ${childViewContainer}`, @@ -416,7 +438,7 @@ const CollectionView = Backbone.View.extend({ const attachViews = []; const detachViews = []; - _.each(this._children._views, (view, key, children) => { + each(this._children._views, (view, key, children) => { (viewFilter.call(this, view, key, children) ? attachViews : detachViews).push(view); }); @@ -434,20 +456,20 @@ const CollectionView = Backbone.View.extend({ if (!viewFilter) { return false; } - if (_.isFunction(viewFilter)) { + if (isFunction(viewFilter)) { return viewFilter; } // Support filter predicates `{ fooFlag: true }` - if (_.isObject(viewFilter)) { - const matcher = _.matches(viewFilter); + if (isObject(viewFilter)) { + const matcher = matches(viewFilter); return function(view) { return matcher(view.model && view.model.attributes); }; } // Filter by model attribute - if (_.isString(viewFilter)) { + if (isString(viewFilter)) { return function(view) { return view.model && view.model.get(viewFilter); }; @@ -487,7 +509,7 @@ const CollectionView = Backbone.View.extend({ }, _detachChildren(detachingViews) { - _.each(detachingViews, this._detachChildView.bind(this)); + each(detachingViews, this._detachChildView.bind(this)); }, _detachChildView(view) { @@ -508,7 +530,7 @@ const CollectionView = Backbone.View.extend({ // Override this method to change how the collectionView detaches a child view detachHtml(view) { - this.Dom.detachEl(view.el, view.$el); + this.Dom.detachEl(view.el); }, _renderChildren() { @@ -541,11 +563,11 @@ const CollectionView = Backbone.View.extend({ _getBuffer(views) { const elBuffer = this.Dom.createBuffer(); - _.each(views, view => { + each(views, view => { renderView(view); // corresponds that view is shown in a Region or CollectionView view._isShown = true; - this.Dom.appendContents(elBuffer, view.el, {_$contents: view.$el}); + this.Dom.appendContents(elBuffer, view.el); }); return elBuffer; @@ -556,14 +578,14 @@ const CollectionView = Backbone.View.extend({ views = shouldTriggerAttach ? views : []; - _.each(views, view => { + each(views, view => { if (view._isAttached) { return; } view.triggerMethod('before:attach', view); }); - this.attachHtml(els, this.$container); + this.attachHtml(els, this.container); - _.each(views, view => { + each(views, view => { if (view._isAttached) { return; } view._isAttached = true; view.triggerMethod('attach', view); @@ -572,8 +594,8 @@ const CollectionView = Backbone.View.extend({ // Override this method to do something other than `.append`. // You can attach any HTML at this point including the els. - attachHtml(els, $container) { - this.Dom.appendContents($container[0], els, {_$el: $container}); + attachHtml(els, container) { + this.Dom.appendContents(container, els); }, isEmpty() { @@ -617,7 +639,7 @@ const CollectionView = Backbone.View.extend({ _getEmptyViewOptions() { const emptyViewOptions = this.emptyViewOptions || this.childViewOptions; - if (_.isFunction(emptyViewOptions)) { + if (isFunction(emptyViewOptions)) { return emptyViewOptions.call(this); } @@ -660,7 +682,7 @@ const CollectionView = Backbone.View.extend({ }); } - if (_.isObject(index)) { + if (isObject(index)) { options = index; } @@ -725,7 +747,7 @@ const CollectionView = Backbone.View.extend({ }, _removeChildViews(views) { - _.each(views, this._removeChildView.bind(this)); + each(views, this._removeChildView.bind(this)); }, _removeChildView(view, {shouldDetach} = {}) { @@ -765,7 +787,7 @@ const CollectionView = Backbone.View.extend({ this.triggerMethod('before:destroy:children', this); if (this.monitorViewEvents === false) { - this.Dom.detachContents(this.el, this.$el); + this.Dom.detachContents(this.el); } this._removeChildViews(this._children._views); @@ -776,11 +798,7 @@ const CollectionView = Backbone.View.extend({ this.triggerMethod('destroy:children', this); } -}, { - setDomApi, - setRenderer }); -_.extend(CollectionView.prototype, ViewMixin); export default CollectionView; diff --git a/src/common/view.js b/src/common/view.js index 60fa33d541..ce9ddeffa7 100644 --- a/src/common/view.js +++ b/src/common/view.js @@ -1,3 +1,11 @@ +export function isView(view) { + return view.render && (view.destroy || view.remove); +} + +export function isViewClass(ViewClass) { + return ViewClass.prototype.render && (ViewClass.prototype.destroy || ViewClass.prototype.remove); +} + export function renderView(view) { if (view._isRendered) { return; diff --git a/src/config/dom.js b/src/config/dom.js index 503e25a7e4..d90ffb7dbc 100644 --- a/src/config/dom.js +++ b/src/config/dom.js @@ -1,20 +1,18 @@ // DomApi // --------- -import _ from 'underscore'; -import Backbone from 'backbone'; - -// Performant method for returning the jQuery instance -function getEl(el) { - return el instanceof Backbone.$ ? el : Backbone.$(el); -} +import { extend, each, keys } from 'underscore'; // Static setter export function setDomApi(mixin) { - this.prototype.Dom = _.extend({}, this.prototype.Dom, mixin); + this.prototype.Dom = extend({}, this.prototype.Dom, mixin); return this; } export default { + // Returns a new HTML DOM node of tagName + createElement(tagName) { + return document.createElement(tagName); + }, // Returns a new HTML DOM node instance createBuffer() { @@ -26,17 +24,10 @@ export default { return el.ownerDocument.documentElement; }, - // Lookup the `selector` string - // Selector may also be a DOM element - // Returns an array-like object of nodes - getEl(selector) { - return getEl(selector); - }, - // Finds the `selector` string with the el // Returns an array-like object of nodes findEl(el, selector) { - return getEl(el).find(selector); + return el.querySelectorAll(selector); }, // Returns true if the el contains the node childEl @@ -45,8 +36,8 @@ export default { }, // Detach `el` from the DOM without removing listeners - detachEl(el, _$el = getEl(el)) { - _$el.detach(); + detachEl(el) { + if (el.parentNode) {el.parentNode.removeChild(el);} }, // Remove `oldEl` from the DOM and put `newEl` in its place @@ -84,15 +75,22 @@ export default { parent2.insertBefore(el1, next2); }, - // Replace the contents of `el` with the HTML string of `html` - setContents(el, html, _$el = getEl(el)) { - _$el.html(html); + // Replace the contents of `el` with the `html` + setContents(el, html) { + el.innerHTML = html + }, + + // Sets attributes on a DOM node + setAttributes(el, attrs) { + each(keys(attrs), attr => { + attr in el ? el[attr] = attrs[attr] : el.setAttribute(attr, attrs[attr]); + }); }, // Takes the DOM node `el` and appends the DOM node `contents` // to the end of the element's contents. - appendContents(el, contents, {_$el = getEl(el), _$contents = getEl(contents)} = {}) { - _$el.append(_$contents); + appendContents(el, contents) { + el.appendChild(contents); }, // Does the el have child nodes @@ -102,7 +100,7 @@ export default { // Remove the inner contents of `el` from the DOM while leaving // `el` itself in the DOM. - detachContents(el, _$el = getEl(el)) { - _$el.contents().detach(); + detachContents(el) { + el.textContent = ''; } }; diff --git a/src/config/event-delegator.js b/src/config/event-delegator.js new file mode 100644 index 0000000000..c52e70f2d2 --- /dev/null +++ b/src/config/event-delegator.js @@ -0,0 +1,53 @@ +// Event Delegator +// --------- +import { each, extend } from 'underscore'; + +// Static setter +export function setEventDelegator(mixin) { + this.prototype.EventDelegator = extend({}, this.prototype.EventDelegator, mixin); + return this; +} + +export default { + + shouldCapture(eventName) { + return ['focus', 'blur'].indexOf(eventName) !== -1; + }, + + // this.$el.on(eventName + '.delegateEvents' + this.cid, selector, handler); + delegate({ eventName, selector, handler, events, rootEl }) { + const shouldCapture = this.shouldCapture(eventName); + + if (selector) { + const delegateHandler = function(evt) { + let node = evt.target; + for (; node && node !== rootEl; node = node.parentNode) { + if (Element.prototype.matches.call(node, selector)) { + evt.delegateTarget = node; + handler(evt); + } + } + }; + + events.push({ eventName, handler: delegateHandler }); + Element.prototype.addEventListener.call(rootEl, eventName, delegateHandler, shouldCapture); + + return; + } + + events.push({ eventName, handler }); + Element.prototype.addEventListener.call(rootEl, eventName, handler, shouldCapture); + }, + + // this.$el.off('.delegateEvents' + this.cid); + undelegateAll({ events, rootEl }) { + if (!rootEl) { return; } + + each(events, ({ eventName, handler }) => { + const shouldCapture = this.shouldCapture(eventName); + Element.prototype.removeEventListener.call(rootEl, eventName, handler, shouldCapture); + }); + + events.length = 0; + } +}; diff --git a/src/mixins/behaviors.js b/src/mixins/behaviors.js index b471148b0c..3a7abca711 100644 --- a/src/mixins/behaviors.js +++ b/src/mixins/behaviors.js @@ -1,6 +1,5 @@ -import _ from 'underscore'; +import { isFunction, extend, reduce, result, without, map } from 'underscore'; import MarionetteError from '../utils/error'; -import _invoke from '../utils/invoke'; // MixinOptions // - behaviors @@ -17,7 +16,7 @@ function getBehaviorClass(options) { } //treat functions as a Behavior constructor - if (_.isFunction(options)) { + if (isFunction(options)) { return { BehaviorClass: options, options: {} }; } @@ -31,47 +30,47 @@ function getBehaviorClass(options) { // instantiate it and get its grouped behaviors. // This accepts a list of behaviors in either an object or array form function parseBehaviors(view, behaviors, allBehaviors) { - return _.reduce(behaviors, (reducedBehaviors, behaviorDefiniton) => { + return reduce(behaviors, (reducedBehaviors, behaviorDefiniton) => { const { BehaviorClass, options } = getBehaviorClass(behaviorDefiniton); const behavior = new BehaviorClass(options, view); reducedBehaviors.push(behavior); - return parseBehaviors(view, _.result(behavior, 'behaviors'), reducedBehaviors); + return parseBehaviors(view, result(behavior, 'behaviors'), reducedBehaviors); }, allBehaviors); } export default { _initBehaviors() { - this._behaviors = parseBehaviors(this, _.result(this, 'behaviors'), []); + this._behaviors = parseBehaviors(this, result(this, 'behaviors'), []); }, _getBehaviorTriggers() { - const triggers = _invoke(this._behaviors, '_getTriggers'); - return _.reduce(triggers, function(memo, _triggers) { - return _.extend(memo, _triggers); + const triggers = map(this._behaviors, behavior => behavior._getTriggers()); + return reduce(triggers, function(memo, _triggers) { + return extend(memo, _triggers); }, {}); }, _getBehaviorEvents() { - const events = _invoke(this._behaviors, '_getEvents'); - return _.reduce(events, function(memo, _events) { - return _.extend(memo, _events); + const events = map(this._behaviors, behavior => behavior._getEvents()); + return reduce(events, function(memo, _events) { + return extend(memo, _events); }, {}); }, - // proxy behavior $el to the view's $el. - _proxyBehaviorViewProperties() { - _invoke(this._behaviors, 'proxyViewProperties'); + // proxy behavior el to the view's el. + _setBehaviorElements() { + map(this._behaviors, behavior => behavior.setElement()); }, // delegate modelEvents and collectionEvents _delegateBehaviorEntityEvents() { - _invoke(this._behaviors, 'delegateEntityEvents'); + map(this._behaviors, behavior => behavior.delegateEntityEvents()); }, // undelegate modelEvents and collectionEvents _undelegateBehaviorEntityEvents() { - _invoke(this._behaviors, 'undelegateEntityEvents'); + map(this._behaviors, behavior => behavior.undelegateEntityEvents()); }, _destroyBehaviors(options) { @@ -79,7 +78,7 @@ export default { // destroying the view. // This unbinds event listeners // that behaviors have registered for. - _invoke(this._behaviors, 'destroy', options); + map(this._behaviors, behavior => behavior.destroy(options)); }, // Remove a behavior @@ -90,18 +89,18 @@ export default { // Remove behavior-only triggers and events this.undelegate(`.trig${ behavior.cid } .${ behavior.cid }`); - this._behaviors = _.without(this._behaviors, behavior); + this._behaviors = without(this._behaviors, behavior); }, _bindBehaviorUIElements() { - _invoke(this._behaviors, 'bindUIElements'); + map(this._behaviors, behavior => behavior.bindUIElements()); }, _unbindBehaviorUIElements() { - _invoke(this._behaviors, 'unbindUIElements'); + map(this._behaviors, behavior => behavior.unbindUIElements()); }, _triggerEventOnBehaviors(eventName, view, options) { - _invoke(this._behaviors, 'triggerMethod', eventName, view, options); + map(this._behaviors, behavior => behavior.triggerMethod(eventName, view, options)); } }; diff --git a/src/mixins/common.js b/src/mixins/common.js index 9ad146c967..1702f48091 100644 --- a/src/mixins/common.js +++ b/src/mixins/common.js @@ -1,6 +1,6 @@ -import _ from 'underscore'; -import Backbone from 'backbone'; +import { extend, result } from 'underscore'; +import EventsMixin from './events'; import getOption from '../common/get-option'; import mergeOptions from '../common/merge-options'; import normalizeMethods from '../common/normalize-methods'; @@ -16,12 +16,15 @@ import { const CommonMixin = { + // This is a noop method intended to be overridden + initialize() {}, + // Imports the "normalizeMethods" to transform hashes of // events=>function references/names to a hash of events=>function references normalizeMethods, _setOptions(options, classOptions) { - this.options = _.extend({}, _.result(this, 'options'), options); + this.options = extend({}, result(this, 'options'), options); this.mergeOptions(options, classOptions); }, @@ -46,6 +49,6 @@ const CommonMixin = { triggerMethod }; -_.extend(CommonMixin, Backbone.Events); +extend(CommonMixin, EventsMixin); export default CommonMixin; diff --git a/src/mixins/events.js b/src/mixins/events.js index 03daff20ed..57e322d12d 100644 --- a/src/mixins/events.js +++ b/src/mixins/events.js @@ -1,5 +1,276 @@ +import { reduce, each, keys, uniqueId } from 'underscore'; + +import buildEventArgs, { eventSplitter } from '../utils/build-event-args'; +import callHandler from '../utils/call-handler'; +import onceWrap from '../utils/once-wrap'; + import triggerMethod from '../common/trigger-method'; -export default { - triggerMethod +// A module that can be mixed in to *any object* in order to provide it with +// a custom event channel. You may bind a callback to an event with `on` or +// remove with `off`; `trigger`-ing an event fires all callbacks in +// succession. +// +// var object = {}; +// _.extend(object, Events); +// object.on('expand', function(){ alert('expanded'); }); +// object.trigger('expand'); +// + +// The reducing API that adds a callback to the `events` object. +const onApi = function({ events, name, callback, context, ctx, listener }) { + const handlers = events[name] || (events[name] = []); + handlers.push({ callback, context, ctx: context || ctx, listener }); + return events; +}; + +const onReducer = function(events, { name, callback, context }) { + if (!callback) {return events;} + return onApi({ events, name, callback, context, ctx: this }); +} + +const onceReducer = function(events, { name, callback, context }) { + if (!callback) {return events;} + const onceCallback = onceWrap(callback, this.off.bind(this, name)); + return onApi({ events, name, callback: onceCallback, context, ctx: this }); } + +const cleanupListener = function({ obj, listeneeId, listenerId, listeningTo }) { + delete listeningTo[listeneeId]; + delete obj._rdListeners[listenerId]; +}; + +// The reducing API that removes a callback from the `events` object. +const offReducer = function(events , { name, callback, context }) { + const names = name ? [name] : keys(events); + + each(names, key => { + const handlers = events[key]; + + // Bail out if there are no events stored. + if (!handlers) {return;} + + // Find any remaining events. + events[key] = reduce(handlers, (remaining, handler) => { + if ( + callback && callback !== handler.callback && + callback !== handler.callback._callback || + context && context !== handler.context + ) { + remaining.push(handler); + return remaining; + } + + // If not including event, clean up any related listener + if (handler.listener) { + const listener = handler.listener; + listener.count--; + if (!listener.count) {cleanupListener(listener);} + } + + return remaining; + }, []); + + if (!events[key].length) {delete events[key];} + }); + + return events; +}; + +const getListener = function(obj, listenerObj) { + const listeneeId = obj._rdListenId || (obj._rdListenId = uniqueId('l')); + obj._rdEvents = obj._rdEvents || {}; + const listeningTo = listenerObj._rdListeningTo || (listenerObj._rdListeningTo = {}); + const listener = listeningTo[listeneeId]; + + // This listenerObj is not listening to any other events on `obj` yet. + // Setup the necessary references to track the listening callbacks. + if (!listener) { + const listenerId = listenerObj._rdListenId || (listenerObj._rdListenId = uniqueId('l')); + listeningTo[listeneeId] = {obj, listeneeId, listenerId, listeningTo, count: 0}; + + return listeningTo[listeneeId]; + } + + return listener; +} + +const listenToApi = function({ name, callback, context, listener }) { + if (!callback) {return;} + + const { obj, listenerId } = listener; + const listeners = obj._rdListeners || (obj._rdListeners = {}); + obj._rdEvents = onApi({ events: obj._rdEvents, name, callback, context, listener }); + listeners[listenerId] = listener; + listener.count++; + + // Call `on` for interop + obj.on(name, callback, context, { _rdInternal: true }); +}; + +const listenToOnceApi = function({ name, callback, context, listener }) { + if (!callback) {return;} + const offCallback = this.stopListening.bind(this, listener.obj, name); + const onceCallback = onceWrap(callback, offCallback); + listenToApi({ name, callback: onceCallback, context, listener }); +}; + +// Handles triggering the appropriate event callbacks. +const triggerApi = function({ events, name, args }) { + const objEvents = events[name]; + const allEvents = (objEvents && events.all) ? events.all.slice() : events.all; + if (objEvents) {triggerEvents(objEvents, args);} + if (allEvents) {triggerEvents(allEvents, [name].concat(args));} +}; + +const triggerEvents = function(events, args) { + each(events, ({ callback, ctx }) => { + callHandler(callback, ctx, args); + }); +}; + +export default { + + // Bind an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. + on(name, callback, context, opts) { + if (opts && opts._rdInternal) {return;} + + const eventArgs = buildEventArgs(name, callback, context); + this._rdEvents = reduce(eventArgs, onReducer.bind(this), this._rdEvents || {}); + + return this; + }, + + // Remove one or many callbacks. If `context` is null, removes all + // callbacks with that function. If `callback` is null, removes all + // callbacks for the event. If `name` is null, removes all bound + // callbacks for all events. + off(name, callback, context, opts) { + if (!this._rdEvents) {return this;} + if (opts && opts._rdInternal) {return;} + + // Delete all event listeners and "drop" events. + if (!name && !context && !callback) { + this._rdEvents = void 0; + const listeners = this._rdListeners; + each(keys(listeners), listenerId => { + cleanupListener(listeners[listenerId]); + }); + return this; + } + + const eventArgs = buildEventArgs(name, callback, context); + + this._rdEvents = reduce(eventArgs, offReducer, this._rdEvents); + + return this; + }, + + // Bind an event to only be triggered a single time. After the first time + // the callback is invoked, its listener will be removed. If multiple events + // are passed in using the space-separated syntax, the handler will fire + // once for each event, not once for a combination of all events. + once(name, callback, context) { + const eventArgs = buildEventArgs(name, callback, context); + + this._rdEvents = reduce(eventArgs, onceReducer.bind(this), this._rdEvents || {}) + + return this; + }, + + // Inversion-of-control versions of `on`. Tell *this* object to listen to + // an event in another object... keeping track of what it's listening to + // for easier unbinding later. + listenTo(obj, name, callback) { + if (!obj) {return this;} + + const listener = getListener(obj, this); + const eventArgs = buildEventArgs(name, callback, this, listener); + each(eventArgs, listenToApi); + + return this; + }, + + // Inversion-of-control versions of `once`. + listenToOnce(obj, name, callback) { + if (!obj) {return this;} + + const listener = getListener(obj, this); + const eventArgs = buildEventArgs(name, callback, this, listener); + each(eventArgs, listenToOnceApi.bind(this)); + + return this; + }, + + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + stopListening(obj, name, callback) { + const listeningTo = this._rdListeningTo; + if (!listeningTo) {return this;} + + const eventArgs = buildEventArgs(name, callback, this); + + const listenerIds = obj ? [obj._rdListenId] : keys(listeningTo); + for (let i = 0; i < listenerIds.length; i++) { + const listener = listeningTo[listenerIds[i]]; + + // If listening doesn't exist, this object is not currently + // listening to obj. Break out early. + if (!listener) {break;} + + each(eventArgs, args => { + const listenToObj = listener.obj; + const events = listenToObj._rdEvents; + + if (!events) {return;} + + listenToObj._rdEvents = offReducer(events, args); + + // Call `off` for interop + listenToObj.off(args.name, args.callback, this, { _reInternal: true }); + }); + } + + return this; + }, + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + trigger(name, ...args) { + if (!this._rdEvents) {return this;} + + if (name && typeof name === 'object') { + each(keys(name), key => { + triggerApi({ + events: this._rdEvents, + name: key, + args: [name[key]], + }); + }); + } + + if (name && eventSplitter.test(name)) { + each(name.split(eventSplitter), n => { + triggerApi({ + events: this._rdEvents, + name: n, + args, + }); + }); + return this; + } + + triggerApi({ + events: this._rdEvents, + name, + args, + }); + + return this; + }, + + triggerMethod, +}; diff --git a/src/mixins/view-events.js b/src/mixins/view-events.js new file mode 100644 index 0000000000..9472bcd138 --- /dev/null +++ b/src/mixins/view-events.js @@ -0,0 +1,92 @@ +import { isString, isFunction, result, each } from 'underscore'; +import { isEnabled } from '../config/features'; +import EventDelegator from '../config/event-delegator'; + +const delegateEventSplitter = /^(\S+)\s*(.*)$/; + +// Internal method to create an event handler for a given `triggerDef` like +// 'click:foo' +function buildViewTrigger(view, triggerDef) { + if (isString(triggerDef)) { + triggerDef = {event: triggerDef}; + } + + const eventName = triggerDef.event; + + let shouldPreventDefault = !!triggerDef.preventDefault; + + if (isEnabled('triggersPreventDefault')) { + shouldPreventDefault = triggerDef.preventDefault !== false; + } + + let shouldStopPropagation = !!triggerDef.stopPropagation; + + if (isEnabled('triggersStopPropagation')) { + shouldStopPropagation = triggerDef.stopPropagation !== false; + } + + return function(event, ...args) { + if (shouldPreventDefault) { + event.preventDefault(); + } + + if (shouldStopPropagation) { + event.stopPropagation(); + } + + view.triggerMethod(eventName, view, event, ...args); + }; +} + +export default { + + EventDelegator, + + _initViewEvents() { + this._domEvents = []; + }, + + _undelegateViewEvents() { + this.EventDelegator.undelegateAll({ + events: this._domEvents, + rootEl: this.el + }); + }, + + _delegateViewEvents(view = this) { + const uiBindings = this._getUIBindings(); + this._delegateEvents(uiBindings); + this._delegateTriggers(uiBindings, view); + }, + + _delegateEvents(uiBindings) { + if (!this.events) { return; } + + each(result(this, 'events'), (handler, key) => { + if (!isFunction(handler)) { + handler = this[handler]; + } + if (!handler) { return; } + this._delegate(handler.bind(this), this.normalizeUIString(key, uiBindings)); + }); + }, + + _delegateTriggers(uiBindings, view) { + if (!this.triggers) { return; } + + each(result(this, 'triggers'), (value, key) => { + this._delegate(buildViewTrigger(view, value), this.normalizeUIString(key, uiBindings)); + }); + }, + + _delegate(handler, key) { + const match = key.match(delegateEventSplitter); + this.EventDelegator.delegate({ + eventName: match[1], + selector: match[2], + handler, + events: this._domEvents, + rootEl: this.el + }); + } +}; diff --git a/src/mixins/view.js b/src/mixins/view.js index 5120d9050d..28c9b10074 100644 --- a/src/mixins/view.js +++ b/src/mixins/view.js @@ -1,31 +1,62 @@ // ViewMixin // --------- -import Backbone from 'backbone'; -import _ from 'underscore'; +import { extend, result } from 'underscore'; import BehaviorsMixin from './behaviors'; import CommonMixin from './common'; import DelegateEntityEventsMixin from './delegate-entity-events'; import TemplateRenderMixin from './template-render'; -import TriggersMixin from './triggers'; import UIMixin from './ui'; +import ViewEvents from './view-events'; import { isEnabled } from '../config/features'; import DomApi from '../config/dom'; + // MixinOptions +// - attributes // - behaviors // - childViewEventPrefix // - childViewEvents // - childViewTriggers +// - className +// - collection // - collectionEvents +// - el +// - events +// - id +// - model // - modelEvents +// - tagName // - triggers // - ui const ViewMixin = { + tagName: 'div', + + // This is a noop method intended to be overridden + preinitialize() {}, + Dom: DomApi, + // Create an element from the `id`, `className` and `tagName` properties. + _getEl() { + if (!this.el) { + const el = this.Dom.createElement(result(this, 'tagName')); + const attrs = extend({}, result(this, 'attributes')); + if (this.id) {attrs.id = result(this, 'id');} + if (this.className) {attrs.class = result(this, 'className');} + this.Dom.setAttributes(el, attrs); + return el; + } + + return result(this, 'el'); + }, + + $(selector) { + return this.Dom.findEl(this.el, selector) + }, + _isElAttached() { return !!this.el && this.Dom.hasEl(this.Dom.getDocumentEl(this.el), this.el); }, @@ -51,48 +82,6 @@ const ViewMixin = { return !!this._isAttached; }, - // Overriding Backbone.View's `delegateEvents` to handle - // `events` and `triggers` - delegateEvents(events) { - this._proxyBehaviorViewProperties(); - this._buildEventProxies(); - - const combinedEvents = _.extend({}, - this._getBehaviorEvents(), - this._getEvents(events), - this._getBehaviorTriggers(), - this._getTriggers() - ); - - Backbone.View.prototype.delegateEvents.call(this, combinedEvents); - - return this; - }, - - // Allows Backbone.View events to utilize `@ui.` selectors - _getEvents(events) { - if (events) { - return this.normalizeUIKeys(events); - } - - if (!this.events) { return; } - - return this.normalizeUIKeys(_.result(this, 'events')); - }, - - // Configure `triggers` to forward DOM events to view - // events. `triggers: {"click .foo": "do:foo"}` - _getTriggers() { - if (!this.triggers) { return; } - - // Allow `triggers` to be configured as a function - const 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() { this._delegateEntityEvents(this.model, this.collection); @@ -126,9 +115,10 @@ const ViewMixin = { // unbind UI elements this.unbindUIElements(); + this._undelegateViewEvents(); // remove the view from the DOM - this._removeElement(); + this.Dom.detachEl(this.el); if (shouldTriggerDetach) { this._isAttached = false; @@ -154,12 +144,6 @@ const ViewMixin = { return this; }, - // Equates to this.$el.remove - _removeElement() { - this.$el.off().removeData(); - this.Dom.detachEl(this.el, this.$el); - }, - // This method binds the elements specified in the "ui" hash bindUIElements() { this._bindUIElements(); @@ -182,14 +166,14 @@ const ViewMixin = { // Cache `childViewEvents` and `childViewTriggers` _buildEventProxies() { - this._childViewEvents = this.normalizeMethods(_.result(this, 'childViewEvents')); - this._childViewTriggers = _.result(this, 'childViewTriggers'); + this._childViewEvents = this.normalizeMethods(result(this, 'childViewEvents')); + this._childViewTriggers = result(this, 'childViewTriggers'); this._eventPrefix = this._getEventPrefix(); }, _getEventPrefix() { const defaultPrefix = isEnabled('childViewEventPrefix') ? 'childview' : false; - const prefix = _.result(this, 'childViewEventPrefix', defaultPrefix); + const prefix = result(this, 'childViewEventPrefix', defaultPrefix); return (prefix === false) ? prefix : prefix + ':'; }, @@ -222,6 +206,6 @@ const ViewMixin = { } }; -_.extend(ViewMixin, BehaviorsMixin, CommonMixin, DelegateEntityEventsMixin, TemplateRenderMixin, TriggersMixin, UIMixin); +extend(ViewMixin, BehaviorsMixin, CommonMixin, DelegateEntityEventsMixin, TemplateRenderMixin, UIMixin, ViewEvents); export default ViewMixin; diff --git a/src/region.js b/src/region.js index 131199ef3a..54d75dc8dd 100644 --- a/src/region.js +++ b/src/region.js @@ -1,12 +1,11 @@ // Region // ------ -import _ from 'underscore'; -import Backbone from 'backbone'; +import { extend as _extend, uniqueId, isObject, isFunction, result } from 'underscore'; import MarionetteError from './utils/error'; import extend from './utils/extend'; import monitorViewEvents from './common/monitor-view-events'; -import { renderView, destroyView } from './common/view'; +import { renderView, destroyView, isView } from './common/view'; import CommonMixin from './mixins/common'; import View from './view'; import DomApi, { setDomApi } from './config/dom'; @@ -22,16 +21,11 @@ const ClassOptions = [ const Region = function(options) { this._setOptions(options, ClassOptions); - this.cid = _.uniqueId(this.cidPrefix); + this.cid = uniqueId(this.cidPrefix); // getOption necessary because options.el may be passed as undefined this._initEl = this.el = this.getOption('el'); - // Handle when this.el is passed in as a $ wrapped element. - this.el = this.el instanceof Backbone.$ ? this.el[0] : this.el; - - this.$el = this._getEl(this.el); - this.initialize.apply(this, arguments); }; @@ -41,7 +35,7 @@ Region.setDomApi = setDomApi; // Region Methods // -------------- -_.extend(Region.prototype, CommonMixin, { +_extend(Region.prototype, CommonMixin, { Dom: DomApi, cidPrefix: 'mnr', @@ -49,9 +43,6 @@ _.extend(Region.prototype, CommonMixin, { _isReplaced: false, _isSwappingView: false, - // This is a noop method intended to be overridden - initialize() {}, - // Displays a view instance inside of the region. If necessary handles calling the `render` // method for you. Reads content directly from the `el` attribute. show(view, options) { @@ -95,7 +86,12 @@ _.extend(Region.prototype, CommonMixin, { return this; }, - _getEl(el) { + _setEl(el) { + if (isObject(el)) { + this.el = el; + return; + } + if (!el) { throw new MarionetteError({ name: classErrorName, @@ -104,20 +100,7 @@ _.extend(Region.prototype, CommonMixin, { }); } - return this.getEl(el); - }, - - _setEl() { - this.$el = this._getEl(this.el); - - if (this.$el.length) { - this.el = this.$el[0]; - } - - // Make sure the $el contains only the el - if (this.$el.length > 1) { - this.$el = this.Dom.getEl(this.el); - } + this.el = this.getEl(el); }, // Set the `el` of the region and move any current view to the new `el`. @@ -128,9 +111,7 @@ _.extend(Region.prototype, CommonMixin, { this._restoreEl(); - this.el = el; - - this._setEl(); + this._setEl(el); if (this.currentView) { const view = this.currentView; @@ -175,7 +156,7 @@ _.extend(Region.prototype, CommonMixin, { _attachView(view, { replaceElement } = {}) { const shouldTriggerAttach = !view._isAttached && this._isElAttached() && !this._shouldDisableMonitoring(); - const shouldReplaceEl = typeof replaceElement === 'undefined' ? !!_.result(this, 'replaceElement') : !!replaceElement; + const shouldReplaceEl = typeof replaceElement === 'undefined' ? !!result(this, 'replaceElement') : !!replaceElement; if (shouldTriggerAttach) { view.triggerMethod('before:attach', view); @@ -197,12 +178,10 @@ _.extend(Region.prototype, CommonMixin, { }, _ensureElement(options = {}) { - if (!_.isObject(this.el)) { - this._setEl(); - } + this._setEl(this.el); - if (!this.$el || this.$el.length === 0) { - const allowMissingEl = typeof options.allowMissingEl === 'undefined' ? !!_.result(this, 'allowMissingEl') : !!options.allowMissingEl; + if (!this.el) { + const allowMissingEl = typeof options.allowMissingEl === 'undefined' ? !!result(this, 'allowMissingEl') : !!options.allowMissingEl; if (allowMissingEl) { return false; @@ -234,7 +213,7 @@ _.extend(Region.prototype, CommonMixin, { }); } - if (view instanceof Backbone.View) { + if (isView(view)) { return view; } @@ -246,11 +225,11 @@ _.extend(Region.prototype, CommonMixin, { // This allows for a template or a static string to be // used as a template _getViewOptions(viewOptions) { - if (_.isFunction(viewOptions)) { + if (isFunction(viewOptions)) { return { template: viewOptions }; } - if (_.isObject(viewOptions)) { + if (isObject(viewOptions)) { return viewOptions; } @@ -262,13 +241,9 @@ _.extend(Region.prototype, CommonMixin, { // 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(el) { - const context = _.result(this, 'parentEl'); - - if (context && _.isString(el)) { - return this.Dom.findEl(context, el); - } + const context = result(this, 'parentEl'); - return this.Dom.getEl(el); + return this.Dom.findEl(context || document, el)[0]; }, _replaceEl(view) { @@ -313,7 +288,7 @@ _.extend(Region.prototype, CommonMixin, { // Override this method to change how the new view is appended to the `$el` that the // region is managing attachHtml(view) { - this.Dom.appendContents(this.el, view.el, {_$el: this.$el, _$contents: view.$el}); + this.Dom.appendContents(this.el, view.el); }, // Destroy the current view, if there is one. If there is no current view, @@ -413,7 +388,7 @@ _.extend(Region.prototype, CommonMixin, { // Override this method to change how the region detaches current content detachHtml() { - this.Dom.detachContents(this.el, this.$el); + this.Dom.detachContents(this.el); }, // Checks whether a view is currently present within the region. Returns `true` if there is diff --git a/src/utils/build-event-args.js b/src/utils/build-event-args.js new file mode 100644 index 0000000000..054009be3f --- /dev/null +++ b/src/utils/build-event-args.js @@ -0,0 +1,24 @@ +import { reduce, keys } from 'underscore'; + +// Regular expression used to split event strings. +export const eventSplitter = /\s+/; + +// Iterates over the standard `event, callback` (as well as the fancy multiple +// space-separated events `"change blur", callback` and jQuery-style event +// maps `{event: callback}`). +export default function buildEventArgs(name, callback, context, listener) { + if (name && typeof name === 'object') { + return reduce(keys(name), (eventArgs, key) => { + return eventArgs.concat(buildEventArgs(key, name[key], context || callback, listener)); + }, []); + } + + if (name && eventSplitter.test(name)) { + return reduce(name.split(eventSplitter), (eventArgs, n) => { + eventArgs.push({ name: n, callback, context, listener }); + return eventArgs; + }, []); + } + + return [{ name, callback, context, listener }]; +} diff --git a/src/utils/call-handler.js b/src/utils/call-handler.js new file mode 100644 index 0000000000..390887a4c2 --- /dev/null +++ b/src/utils/call-handler.js @@ -0,0 +1,10 @@ +// An optimized way to execute callbacks. +export default function callHandler(callback, context, args = []) { + switch (args.length) { + case 0: return callback.call(context); + case 1: return callback.call(context, args[0]); + case 2: return callback.call(context, args[0], args[1]); + case 3: return callback.call(context, args[0], args[1], args[2]); + default: return callback.apply(context, args); + } +} diff --git a/src/utils/extend.js b/src/utils/extend.js index d5b73d473b..ce41ea35f3 100644 --- a/src/utils/extend.js +++ b/src/utils/extend.js @@ -1,9 +1,33 @@ // Marionette.extend // ----------------- -import Backbone from 'backbone'; +import { has, extend, create } from 'underscore'; -// Borrow the Backbone `extend` method so we can use it as needed -const extend = Backbone.Model.extend; +// Borrowed from backbone.js +export default function(protoProps, staticProps) { + const parent = this; + let child; -export default extend; + // The constructor function for the new subclass is either defined by you + // (the "constructor" property in your `extend` definition), or defaulted + // by us to simply call the parent constructor. + if (protoProps && has(protoProps, 'constructor')) { + child = protoProps.constructor; + } else { + child = function() { return parent.apply(this, arguments); }; + } + + // Add static properties to the constructor function, if supplied. + extend(child, parent, staticProps); + + // Set the prototype chain to inherit from `parent`, without calling + // `parent`'s constructor function and add the prototype properties. + child.prototype = create(parent.prototype, protoProps); + child.prototype.constructor = child; + + // Set a convenience property in case the parent's prototype is needed + // later. + child.__super__ = parent.prototype; + + return child; +} diff --git a/src/utils/make-callback.js b/src/utils/make-callback.js new file mode 100644 index 0000000000..d2d971dd7a --- /dev/null +++ b/src/utils/make-callback.js @@ -0,0 +1,9 @@ +// If callback is not a function return the callback and flag it for removal +export default function makeCallback(callback) { + if (typeof callback === 'function') { + return callback; + } + const result = function() { return callback; }; + result._callback = callback; + return result; +} diff --git a/src/utils/once-wrap.js b/src/utils/once-wrap.js new file mode 100644 index 0000000000..d47ea31376 --- /dev/null +++ b/src/utils/once-wrap.js @@ -0,0 +1,14 @@ +import { once } from 'underscore'; + +// Wrap callback in a once. Returns for requests +// `offCallback` unbinds the `onceWrapper` after it has been called. +export default function onceWrap(callback, offCallback) { + const onceCallback = once(function() { + offCallback(onceCallback); + return callback.apply(this, arguments); + }); + + onceCallback._callback = callback; + + return onceCallback; +} diff --git a/src/view.js b/src/view.js index 7cb1c3c546..a8acd207a3 100644 --- a/src/view.js +++ b/src/view.js @@ -1,24 +1,32 @@ // View // --------- -import _ from 'underscore'; -import Backbone from 'backbone'; +import { extend as _extend, uniqueId, reduce } from 'underscore'; +import extend from './utils/extend'; import monitorViewEvents from './common/monitor-view-events'; import ViewMixin from './mixins/view'; import RegionsMixin from './mixins/regions'; import { setDomApi } from './config/dom'; +import { setEventDelegator } from './config/event-delegator'; import { setRenderer } from './config/renderer'; const ClassOptions = [ + 'attributes', 'behaviors', 'childViewEventPrefix', 'childViewEvents', 'childViewTriggers', + 'className', + 'collection', 'collectionEvents', + 'el', 'events', + 'id', + 'model', 'modelEvents', 'regionClass', 'regions', + 'tagName', 'template', 'templateContext', 'triggers', @@ -36,28 +44,37 @@ function childReducer(children, region) { // The standard view. Includes view events, automatic rendering // templates, nested views, and more. -const View = Backbone.View.extend({ +const View = function(options) { + this.cid = uniqueId(this.cidPrefix); + this._setOptions(options, ClassOptions); - constructor(options) { - this._setOptions(options, ClassOptions); + this.preinitialize.apply(this, arguments); - monitorViewEvents(this); + this._initViewEvents(); + this.setElement(this._getEl()); - this._initBehaviors(); - this._initRegions(); + monitorViewEvents(this); - Backbone.View.prototype.constructor.apply(this, arguments); + this._initBehaviors(); + this._initRegions(); + this._buildEventProxies(); - this.delegateEntityEvents(); + this.initialize.apply(this, arguments); - this._triggerEventOnBehaviors('initialize', this, options); - }, + this.delegateEntityEvents(); + + this._triggerEventOnBehaviors('initialize', this, options); +}; + +_extend(View, { extend, setRenderer, setDomApi, setEventDelegator }); - // 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() { - Backbone.View.prototype.setElement.apply(this, arguments); +_extend(View.prototype, ViewMixin, RegionsMixin, { + cidPrefix: 'mnv', + + setElement(element) { + this._undelegateViewEvents(); + this.el = element; + this._setBehaviorElements(); this._isRendered = this.Dom.hasContents(this.el); this._isAttached = this._isElAttached(); @@ -66,6 +83,8 @@ const View = Backbone.View.extend({ this.bindUIElements(); } + this._delegateViewEvents(); + return this; }, @@ -99,13 +118,8 @@ const View = Backbone.View.extend({ }, _getImmediateChildren() { - return _.reduce(this._regions, childReducer, []); + return reduce(this._regions, childReducer, []); } -}, { - setRenderer, - setDomApi }); -_.extend(View.prototype, ViewMixin, RegionsMixin); - export default View;