UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

411 lines (375 loc) 14.6 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2011 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Martin Wittemann (martinwittemann) ************************************************************************ */ /** * This mixin offers basic event handling capabilities. It includes the * commonly known methods for managing event listeners and firing events. * * @use(qx.event.dispatch.Direct) * @use(qx.event.handler.Object) */ qx.Mixin.define("qx.core.MEvent", { members: { /** @type {Class} Pointer to the regular event registration class */ __Registration: qx.event.Registration, /** * Add event listener to this object. * * @param type {String} name of the event type * @param listener {Function} event callback function * @param self {Object ? null} Reference to the 'this' variable inside * the event listener. When not given, the corresponding dispatcher * usually falls back to a default, which is the target * by convention. Note this is not a strict requirement, i.e. * custom dispatchers can follow a different strategy. * @param capture {Boolean ? false} Whether to attach the event to the * capturing phase or the bubbling phase of the event. The default is * to attach the event handler to the bubbling phase. * @return {String} An opaque id, which can be used to remove the event listener * using the {@link #removeListenerById} method. */ addListener(type, listener, self, capture) { if (!this.$$disposed) { return this.__Registration.addListener( this, type, listener, self, capture ); } return null; }, /** * Add event listener to this object, which is only called once. After the * listener is called the event listener gets removed. * * @param type {String} name of the event type * @param listener {Function} event callback function * @param context {Object ? window} reference to the 'this' variable inside the callback * @param capture {Boolean ? false} Whether to attach the event to the * capturing phase or the bubbling phase of the event. The default is * to attach the event handler to the bubbling phase. * @return {String} An opaque id, which can be used to remove the event listener * using the {@link #removeListenerById} method. */ addListenerOnce(type, listener, context, capture) { var self = this; // self is needed to remove the listener inside the callback if (!context) { context = this; } var id; // store id in closure context var callback = function (e) { self.removeListenerById(id); listener.call(context, e); }; // check for wrapped callback storage if (!listener.$$wrapped_callback) { listener.$$wrapped_callback = {}; } // store the call for each type in case the listener is // used for more than one type [BUG #8038] listener.$$wrapped_callback[type + this.toHashCode()] = callback; id = this.addListener(type, callback, context, capture); return id; }, /** * Remove event listener from this object * * @param type {String} name of the event type * @param listener {Function} event callback function * @param self {Object ? null} reference to the 'this' variable inside the callback * @param capture {Boolean} Whether to remove the event listener of * the bubbling or of the capturing phase. * @return {Boolean} Whether the event was removed successfully (has existed) */ removeListener(type, listener, self, capture) { if (!this.$$disposed) { // special handling for wrapped once listener if ( listener.$$wrapped_callback && listener.$$wrapped_callback[type + this.$$hash] ) { var callback = listener.$$wrapped_callback[type + this.$$hash]; delete listener.$$wrapped_callback[type + this.$$hash]; listener = callback; } return this.__Registration.removeListener( this, type, listener, self, capture ); } return false; }, /** * Removes an event listener from an event target by an id returned by * {@link #addListener} * * @param id {String} The id returned by {@link #addListener} * @return {Boolean} Whether the event was removed successfully (has existed) */ removeListenerById(id) { if (!this.$$disposed) { return this.__Registration.removeListenerById(this, id); } return false; }, /** * Check if there are one or more listeners for an event type. * * @param type {String} name of the event type * @param capture {Boolean ? false} Whether to check for listeners of * the bubbling or of the capturing phase. * @return {Boolean} Whether the object has a listener of the given type. */ hasListener(type, capture) { return this.__Registration.hasListener(this, type, capture); }, /** * Dispatch an event on this object * * @param evt {qx.event.type.Event} event to dispatch * @return {Boolean} Whether the event default was prevented or not. * Returns true, when the event was NOT prevented. */ dispatchEvent(evt) { if (!this.$$disposed) { return this.__Registration.dispatchEvent(this, evt); } return true; }, /** @type{Object<String,qx.Promise>} list of pending events, indexed by hash code */ __pendingEvents: null, /** @type{qx.Promise} promise that callers are waiting on, ready for when all events are finished */ __promiseWaitForPendingEvents: null, /** * Internal helper method to track promises returned from event handlers * * @param {var} result the result from the event handler * @returns {qx.Promise|var} the value to return */ __trackPendingEvent(result) { if (qx.core.Environment.get("qx.promise")) { if (!qx.Promise.isPromise(result)) { return result; } if (!this.__pendingEvents) { this.__pendingEvents = {}; } if (!(result instanceof qx.Promise)) { result = qx.Promise.resolve(result); } let hashCode = result.toHashCode(); let newPromise = result .then(result => { delete this.__pendingEvents[hashCode]; let promise = this.__promiseWaitForPendingEvents; if (promise && Object.keys(this.__pendingEvents).length == 0) { this.__pendingEvents = null; this.__promiseWaitForPendingEvents = null; promise.resolve(); } return result; }) .catch(err => { delete this.__pendingEvents[hashCode]; let promise = this.__promiseWaitForPendingEvents; if (promise && Object.keys(this.__pendingEvents).length == 0) { this.__pendingEvents = null; this.__promiseWaitForPendingEvents = null; promise.reject(err); } throw err; }); this.__pendingEvents[hashCode] = newPromise; return newPromise; } else { return result; } }, /** * Waits for all pending events to be resolved */ async waitForPendingEvents() { if (qx.core.Environment.get("qx.promise")) { if (!this.__pendingEvents) { return; } if (!this.__promiseWaitForPendingEvents) { this.__promiseWaitForPendingEvents = new qx.Promise(); } let promise = this.__promiseWaitForPendingEvents; await promise; } }, /** * Creates and dispatches an event on this object. * * @param type {String} Event type to fire * @param clazz {Class?qx.event.type.Event} The event class * @param args {Array?null} Arguments, which will be passed to * the event's init method. * @return {Boolean|qx.Promise} whether the event default was prevented or not. * Returns true, when the event was NOT prevented. */ fireEvent(type, clazz, args) { if (!this.$$disposed) { return this.__trackPendingEvent( this.__Registration.fireEvent(this, type, clazz, args) ); } return true; }, /** * Creates and dispatches an event on this object; equivalent to fireEvent, except that it * always returns a promise * * @param type {String} Event type to fire * @param clazz {Class?qx.event.type.Event} The event class * @param args {Array?null} Arguments, which will be passed to * the event's init method. * @return {qx.Promise} a promise aggregated from the event handlers; * if the default was prevented, the promise is rejected */ fireEventAsync(type, clazz, args) { if (!qx.core.Environment.get("qx.promise")) { throw new Error( this.classname + ".fireEventAsync not supported because qx.promise==false" ); } if (!this.$$disposed) { return this.__trackPendingEvent( this.__Registration.fireEventAsync(this, type, clazz, args) ); } return qx.Promise.resolve(true); }, /** * Create an event object and dispatch it on this object. * The event dispatched with this method does never bubble! Use only if you * are sure that bubbling is not required. * * @param type {String} Event type to fire * @param clazz {Class?qx.event.type.Event} The event class * @param args {Array?null} Arguments, which will be passed to * the event's init method. * @return {Boolean} Whether the event default was prevented or not. * Returns true, when the event was NOT prevented. */ fireNonBubblingEvent(type, clazz, args) { if (!this.$$disposed) { return this.__trackPendingEvent( this.__Registration.fireNonBubblingEvent(this, type, clazz, args) ); } return true; }, /** * Create an event object and dispatch it on this object; equivalent to fireNonBubblingEvent, * except that it always returns a promise. * * The event dispatched with this method does never bubble! Use only if you * are sure that bubbling is not required. * * @param type {String} Event type to fire * @param clazz {Class?qx.event.type.Event} The event class * @param args {Array?null} Arguments, which will be passed to * the event's init method. * @return {qx.Promise} a promise aggregated from the event handlers; * if the default was prevented, the promise is rejected */ fireNonBubblingEventAsync(type, clazz, args) { if (!qx.core.Environment.get("qx.promise")) { throw new Error( this.classname + ".fireNonBubblingEventAsync not supported because qx.promise==false" ); } if (!this.$$disposed) { return this.__trackPendingEvent( this.__Registration.fireNonBubblingEventAsync(this, type, clazz, args) ); } return qx.Promise.resolve(true); }, /** * Creates and dispatches an non-bubbling data event on this object. * * @param type {String} Event type to fire * @param data {var} User defined data attached to the event object * @param oldData {var?null} The event's old data (optional) * @param cancelable {Boolean?false} Whether or not an event can have its default * action prevented. The default action can either be the browser's * default action of a native event (e.g. open the context menu on a * right click) or the default action of a qooxdoo class (e.g. close * the window widget). The default action can be prevented by calling * {@link qx.event.type.Event#preventDefault} * @return {Boolean|qx.Promise} whether the event default was prevented or not. * Returns true, when the event was NOT prevented. */ fireDataEvent(type, data, oldData, cancelable) { if (!this.$$disposed) { if (oldData === undefined) { oldData = null; } return this.__trackPendingEvent( this.__Registration.fireEvent(this, type, qx.event.type.Data, [ data, oldData, !!cancelable ]) ); } return true; }, /** * Creates and dispatches an non-bubbling data event on this object; equivalent to * fireEvent, except that it always returns a promise. * * @param type {String} Event type to fire * @param data {var} User defined data attached to the event object * @param oldData {var?null} The event's old data (optional) * @param cancelable {Boolean?false} Whether or not an event can have its default * action prevented. The default action can either be the browser's * default action of a native event (e.g. open the context menu on a * right click) or the default action of a qooxdoo class (e.g. close * the window widget). The default action can be prevented by calling * {@link qx.event.type.Event#preventDefault} * @return {qx.Promise} a promise aggregated from the event handlers; * if the default was prevented, the promise is rejected */ fireDataEventAsync(type, data, oldData, cancelable) { if (!qx.core.Environment.get("qx.promise")) { throw new Error( this.classname + ".fireDataEventAsync not supported because qx.promise==false" ); } if (!this.$$disposed) { if (oldData === undefined) { oldData = null; } return this.__trackPendingEvent( this.__Registration.fireEventAsync(this, type, qx.event.type.Data, [ data, oldData, !!cancelable ]) ); } return qx.Promise.resolve(true); } } });