// @tag core/** * Represents single event type that an Observable object listens to. * All actual listeners are tracked inside here. When the event fires, * it calls all the registered listener functions. * * @private */Ext.define('Ext.util.Event', function() { var arraySlice = Array.prototype.slice, arrayInsert = Ext.Array.insert, toArray = Ext.Array.toArray, fireArgs = {}; return { requires: 'Ext.util.DelayedTask', /** * @property {Boolean} isEvent * `true` in this class to identify an object as an instantiated Event, or subclass thereof. */ isEvent: true, // Private. Event suspend count suspended: 0, noOptions: {}, constructor: function(observable, name) { this.name = name; this.observable = observable; this.listeners = []; }, addListener: function(fn, scope, options, caller, manager) { var me = this, added = false, observable = me.observable, eventName = me.name, listeners, listener, priority, isNegativePriority, highestNegativePriorityIndex, hasNegativePriorityIndex, length, index, i, listenerPriority, managedListeners; //<debug> if (scope && !Ext._namedScopes[scope] && (typeof fn === 'string') && (typeof scope[fn] !== 'function')) { Ext.raise("No method named '" + fn + "' found on scope object"); } //</debug> if (me.findListener(fn, scope) === -1) { listener = me.createListener(fn, scope, options, caller, manager); if (me.firing) { // if we are currently firing this event, don't disturb the listener loop me.listeners = me.listeners.slice(0); } listeners = me.listeners; index = length = listeners.length; priority = options && options.priority; highestNegativePriorityIndex = me._highestNegativePriorityIndex; hasNegativePriorityIndex = highestNegativePriorityIndex !== undefined; if (priority) { // Find the index at which to insert the listener into the listeners array, // sorted by priority highest to lowest. isNegativePriority = (priority < 0); if (!isNegativePriority || hasNegativePriorityIndex) { // If the priority is a positive number, or if it is a negative number // and there are other existing negative priority listenrs, then we // need to calcuate the listeners priority-order index. // If the priority is a negative number, begin the search for priority // order index at the index of the highest existing negative priority // listener, otherwise begin at 0 // eslint-disable-next-line max-len for (i = (isNegativePriority ? highestNegativePriorityIndex : 0); i < length; i++) { // Listeners created without options will have no "o" property listenerPriority = listeners[i].o ? listeners[i].o.priority || 0 : 0; if (listenerPriority < priority) { index = i; break; } } } else { // if the priority is a negative number, and there are no other negative // priority listeners, then no calculation is needed - the negative // priority listener gets appended to the end of the listeners array. me._highestNegativePriorityIndex = index; } } else if (hasNegativePriorityIndex) { // listeners with a priority of 0 or undefined are appended to the end of // the listeners array unless there are negative priority listeners in the // listeners array, then they are inserted before the highest negative // priority listener. index = highestNegativePriorityIndex; } if (!isNegativePriority && index <= highestNegativePriorityIndex) { me._highestNegativePriorityIndex ++; } if (index === length) { listeners[length] = listener; } else { arrayInsert(listeners, index, [listener]); } if (observable.isElement) { // It is the role of Ext.util.Event (vs Ext.Element) to handle subscribe/ // unsubscribe because it is the lowest level place to intercept the // listener before it is added/removed. For addListener this could easily // be done in Ext.Element's doAddListener override, but since there are // multiple paths for listener removal (un, clearListeners), it is best // to keep all subscribe/unsubscribe logic here. observable._getPublisher(eventName, options.translate === false).subscribe( observable, eventName, options.delegated !== false, options.capture ); } // If the listener was passed with a manager, add it to the manager's list. if (manager) { // if scope is an observable, the listener will be automatically managed // this eliminates the need to call mon() in a majority of cases managedListeners = manager.managedListeners || (manager.managedListeners = []); managedListeners.push({ item: me.observable, ename: (options && options.managedName) || me.name, fn: fn, scope: scope, options: options }); } added = true; } return added; }, createListener: function(fn, scope, o, caller, manager) { var me = this, namedScopes = Ext._namedScopes, namedScope = namedScopes[scope], listener = { fn: fn, scope: scope, ev: me, caller: caller, manager: manager, namedScope: namedScope, defaultScope: namedScope ? (scope || me.observable) : undefined, lateBound: typeof fn === 'string' }, handler = fn, wrapped = false, type; if (listener.lateBound && fn[2] === '.') { //<debug> if (fn.substr(0, 2) !== 'up') { Ext.raise('Invalid listener method: ' + fn); } //</debug> listener.defaultScope = null; listener.namedScope = namedScopes[listener.scope = scope = 'up']; listener.fn = handler = fn.substr(3); } // The order is important. The 'single' wrapper must be wrapped by the 'buffer' // and 'delayed' wrapper because the event removal that the single listener // does destroys the listener's DelayedTask(s) if (o) { listener.o = o; if (o.single) { handler = me.createSingle(handler, listener, o, scope); wrapped = true; } if (o.target) { handler = me.createTargeted(handler, listener, o, scope, wrapped); wrapped = true; } if (o.onFrame) { handler = me.createAnimFrame(handler, listener, o, scope, wrapped); wrapped = true; } if (o.delay) { handler = me.createDelayed(handler, listener, o, scope, wrapped); wrapped = true; } if (o.buffer) { handler = me.createBuffered(handler, listener, o, scope, wrapped); wrapped = true; } if (me.observable.isElement) { // If the event type was translated, e.g. mousedown -> touchstart, we need // to save the original type in the listener object so that the Ext.event.Event // object can reflect the correct type at firing time type = o.type; if (type) { listener.type = type; } } } listener.fireFn = handler; listener.wrapped = wrapped; return listener; }, findListener: function(fn, scope) { var listeners = this.listeners, i = listeners.length, listener; while (i--) { listener = listeners[i]; if (listener) { // use ==, not === for scope comparison, so that undefined and null are equal // eslint-disable-next-line eqeqeq if (listener.fn === fn && listener.scope == scope) { return i; } } } return - 1; }, removeListener: function(fn, scope, index) { var me = this, removed = false, observable = me.observable, eventName = me.name, listener, options, manager, managedListeners, managedListener, i; index = index != null ? index : me.findListener(fn, scope); if (index !== -1) { listener = me.listeners[index]; if (me.firing) { me.listeners = me.listeners.slice(0); } // Remove this listener from the listeners array. We can use splice directly here. // The IE8 bug which Ext.Array works around only affects *insertion* // http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/6e946d03-e09f-4b22-a4dd-cd5e276bf05a/ me.listeners.splice(index, 1); // if the listeners array contains negative priority listeners, adjust the // internal index if needed. if (me._highestNegativePriorityIndex) { if (index < me._highestNegativePriorityIndex) { me._highestNegativePriorityIndex--; } else if (index === me._highestNegativePriorityIndex && index === me.listeners.length) { delete me._highestNegativePriorityIndex; } } if (listener) { options = listener.o; // cancel and remove a buffered handler that hasn't fired yet. // When the buffered listener is invoked, it must check whether // it still has a task. if (listener.task) { listener.task.cancel(); delete listener.task; } // cancel and remove all delayed handlers that haven't fired yet i = listener.tasks && listener.tasks.length; if (i) { while (i--) { listener.tasks[i].cancel(); } delete listener.tasks; } // Cancel the timer that could have been set if the event has already fired listener.fireFn.timerId = Ext.undefer(listener.fireFn.timerId); manager = listener.manager; if (manager) { // If this is a managed listener we need to remove it from the manager's // managedListeners array. This ensures that if we listen using mon // and then remove without using mun, the managedListeners array is updated // accordingly, for example // // manager.on(target, 'foo', fn); // // target.un('foo', fn); managedListeners = manager.managedListeners; if (managedListeners) { for (i = managedListeners.length; i--;) { managedListener = managedListeners[i]; if (managedListener.item === me.observable && managedListener.ename === eventName && managedListener.fn === fn && managedListener.scope === scope) { managedListeners.splice(i, 1); } } } } if (observable.isElement) { // eslint-disable-next-line max-len observable._getPublisher(eventName, options.translate === false).unsubscribe( observable, eventName, options.delegated !== false, options.capture ); } } removed = true; } return removed; }, // Iterate to stop any buffered/delayed events clearListeners: function() { var listeners = this.listeners, i = listeners.length, listener; while (i--) { listener = listeners[i]; this.removeListener(listener.fn, listener.scope); } }, suspend: function() { ++this.suspended; }, resume: function() { if (this.suspended) { --this.suspended; } }, isSuspended: function() { return this.suspended > 0; }, fireDelegated: function(firingObservable, args) { this.firingObservable = firingObservable; return this.fire.apply(this, args); }, fire: function() { var me = this, CQ = Ext.ComponentQuery, listeners = me.listeners, count = listeners.length, observable = me.observable, isElement = observable.isElement, isComponent = observable.isComponent, firingObservable = me.firingObservable, options, delegate, fireInfo, i, args, listener, len, delegateEl, currentTarget, type, chained, firingArgs, e, fireFn, fireScope; if (!me.suspended && count > 0) { me.firing = true; args = arguments.length ? arraySlice.call(arguments, 0) : []; len = args.length; if (isElement) { e = args[0]; } for (i = 0; i < count; i++) { listener = listeners[i]; // Listener may be undefined if one of the previous listeners // destroyed the observable that was listening to these events. // We'd be still in the middle of the loop here, unawares. if (!listener) { continue; } options = listener.o; if (isElement) { if (currentTarget) { // restore the previous currentTarget if we changed it last time // around the loop while processing the delegate option. e.setCurrentTarget(currentTarget); } // For events that have been translated to provide device compatibility, // e.g. mousedown -> touchstart, we want the event object to reflect the // type that was originally listened for, not the type of the actual event // that fired. The listener's "type" property reflects the original type. type = listener.type; if (type) { // chain a new object to the event object before changing the type. // This is more efficient than creating a new event object, and we // don't want to change the type of the original event because it may // be used asynchronously by other handlers // Translated events are not gestures. They must appear to be // atomic events, so that they can be stopped. chained = e; e = args[0] = chained.chain({ type: type, isGesture: false }); } // In Ext4 Ext.EventObject was a singleton event object that was reused // as events were fired. Set Ext.EventObject to the last fired event // for compatibility. Ext.EventObject = e; } firingArgs = args; if (options) { delegate = options.delegate; if (delegate) { if (isElement) { // prepending the currentTarget.id to the delegate selector // allows us to match selectors such as "> div" // eslint-disable-next-line max-len delegateEl = e.getTarget(typeof delegate === 'function' ? delegate : '#' + e.currentTarget.id + ' ' + delegate); if (delegateEl) { args[1] = delegateEl; // save the current target before changing it to the delegateEl // so that we can restore it next time around currentTarget = e.currentTarget; e.setCurrentTarget(delegateEl); } else { continue; } } // eslint-disable-next-line max-len else if (isComponent && !CQ.is(firingObservable, delegate, observable)) { continue; } } if (isElement) { if (options.preventDefault) { e.preventDefault(); } if (options.stopPropagation) { e.stopPropagation(); } if (options.stopEvent) { e.stopEvent(); } } args[len] = options; if (options.args) { firingArgs = options.args.concat(args); } } fireInfo = me.getFireInfo(listener); fireFn = fireInfo.fn; fireScope = fireInfo.scope; // We don't want to keep closure and scope on the Event prototype! fireInfo.fn = fireInfo.scope = null; // If the scope is already destroyed, we absolutely cannot deliver events to it. // We also need to clean up the listener to avoid it hanging around forever // like a zombie. Scope can be null/undefined, that's normal. if (fireScope && fireScope.destroyed) { me.removeListener(fireFn, fireScope, i); fireFn = null; //<debug> // Skip warnings for Ext.container.Monitor // It is to be deprecated and removed shortly. if (fireScope.$className !== 'Ext.container.Monitor') { (Ext.raiseOnDestroyed ? Ext.raise : Ext.log.warn)({ msg: 'Attempting to fire "' + me.name + '" event on destroyed ' + (fireScope.$className || 'object') + ' instance with id: ' + (fireScope.id || 'unknown'), instance: fireScope }); } //</debug> } // N.B. This is where actual listener code is called. Step boldly into! if (fireFn && fireFn.apply(fireScope, firingArgs) === false) { Ext.EventObject = null; return (me.firing = false); } // We should remove the last item here to avoid future listeners // in the Array to inherit these options by mistake if (options) { args.length--; } if (chained) { // if we chained the event object for type translation we need to // un-chain it before proceeding to process the next listener, which // may not be a translated event. e = args[0] = chained; chained = null; } // We don't guarantee Ext.EventObject existence outside of the immediate // event propagation scope Ext.EventObject = null; } } me.firing = false; return true; }, getFireInfo: function(listener, fromWrapped) { var observable = this.observable, fireFn = listener.fireFn, scope = listener.scope, namedScope = listener.namedScope, fn, origin; // If we are called with a wrapped listener, only attempt to do scope // resolution if we are explicitly called by the last wrapped function if (!fromWrapped && listener.wrapped) { fireArgs.fn = fireFn; return fireArgs; } fn = fromWrapped ? listener.fn : fireFn; //<debug> var name = fn; // eslint-disable-line vars-on-top //</debug> if (listener.lateBound) { // handler is a function name - need to resolve it to a function reference origin = listener.caller || observable; if (namedScope && namedScope.isUp) { scope = Ext.lookUpFn(origin, fn); } else if (!scope || namedScope) { // Only invoke resolveListenerScope if the user did not specify a scope, // or if the user specified a named scope. Named function handlers that // use an arbitrary object as the scope just skip this part, and just // use the given scope object to resolve the method. scope = origin.resolveListenerScope(listener.defaultScope); } //<debug> if (!scope) { Ext.raise('Unable to dynamically resolve scope for "' + listener.ev.name + '" listener on ' + this.observable.id); } if (!Ext.isFunction(scope[fn])) { Ext.raise('No method named "' + fn + '" on ' + (scope.$className || 'scope object.')); } //</debug> fn = scope[fn]; } else if (namedScope && namedScope.isController) { // If handler is a function reference and scope:'controller' was requested // we'll do our best to look up a controller. scope = (listener.caller || observable).resolveListenerScope(listener.defaultScope); //<debug> if (!scope) { Ext.raise('Unable to dynamically resolve scope for "' + listener.ev.name + '" listener on ' + this.observable.id); } //</debug> } else if (!scope || namedScope) { // If handler is a function reference we use the observable instance as // the default scope scope = observable; } // We can only ever be firing one event at a time, so just keep // overwriting the object we've got in our closure, otherwise we'll be // creating a whole bunch of garbage objects fireArgs.fn = fn; fireArgs.scope = scope; //<debug> if (!fn) { Ext.raise('Unable to dynamically resolve method "' + name + '" on ' + this.observable.$className); } //</debug> return fireArgs; }, createAnimFrame: function(handler, listener, o, scope, wrapped) { var fireInfo; if (!wrapped) { fireInfo = listener.ev.getFireInfo(listener, true); handler = fireInfo.fn; scope = fireInfo.scope; // We don't want to keep closure and scope references on the Event prototype! fireInfo.fn = fireInfo.scope = null; } return Ext.Function.createAnimationFrame(handler, scope, o.args); }, createTargeted: function(handler, listener, o, scope, wrapped) { return function() { var fireInfo; if (o.target === arguments[0]) { if (!wrapped) { fireInfo = listener.ev.getFireInfo(listener, true); handler = fireInfo.fn; scope = fireInfo.scope; // We don't want to keep closure and scope references // on the Event prototype! fireInfo.fn = fireInfo.scope = null; } return handler.apply(scope, arguments); } }; }, createBuffered: function(handler, listener, o, scope, wrapped) { listener.task = new Ext.util.DelayedTask(); return function() { var fireInfo; // If the listener is removed during the event call, the listener stays in the // list of listeners to be invoked in the fire method, but the task is deleted // So if we get here with no task, it's because the listener has been removed. if (listener.task) { //<debug> if (Ext._unitTesting) { o.$delayedTask = listener.task; // for unit test access } //</debug> if (!wrapped) { fireInfo = listener.ev.getFireInfo(listener, true); handler = fireInfo.fn; scope = fireInfo.scope; // We don't want to keep closure and scope references // on the Event prototype! fireInfo.fn = fireInfo.scope = null; } listener.task.delay(o.buffer, handler, scope, toArray(arguments)); } }; }, createDelayed: function(handler, listener, o, scope, wrapped) { return function() { var task = new Ext.util.DelayedTask(), fireInfo; if (!wrapped) { fireInfo = listener.ev.getFireInfo(listener, true); handler = fireInfo.fn; scope = fireInfo.scope; // We don't want to keep closure and scope references on the Event prototype! fireInfo.fn = fireInfo.scope = null; } if (!listener.tasks) { listener.tasks = []; } listener.tasks.push(task); //<debug> if (Ext._unitTesting) { o.$delayedTask = task; // for unit test access } //</debug> task.delay(o.delay || 10, handler, scope, toArray(arguments)); }; }, createSingle: function(handler, listener, o, scope, wrapped) { return function() { var event = listener.ev, observable = event.observable, fn = listener.fn, fireInfo; // If we have an observable, use that to clean up because there // can be special cases that need handling. For example element // listeners may bind multiple events (mousemove+touchmove) and they // need to act in tandem. if (observable) { if (!observable.destroyed) { observable.removeListener(event.name, fn, scope); } } else { event.removeListener(fn, scope); } if (!wrapped) { fireInfo = event.getFireInfo(listener, true); handler = fireInfo.fn; scope = fireInfo.scope; // We don't want to keep closure and scope references on the Event prototype! fireInfo.fn = fireInfo.scope = null; } return handler.apply(scope, arguments); }; } };});