feat(core): implement EventListenerOptions for addEventListener and removeEventListener

This commit is contained in:
shirakaba
2024-05-08 12:19:49 +09:00
parent 5d08e44b79
commit 05a6be2e3d
9 changed files with 83 additions and 58 deletions

View File

@@ -286,8 +286,8 @@ export var test_Observable_identity = function () {
// If you try to add the same callback for a given event name twice, without
// distinguishing by its thisArg, the second addition will no-op.
obj.addEventListener(eventName, callback);
obj.addEventListener(eventName, callback);
obj.addEventListener(eventName, callback, false);
obj.addEventListener(eventName, callback, false);
obj.set('testName', 1);
TKUnit.assert(receivedCount === 1, 'Expected Observable to fire exactly once upon a property change, having passed the same callback into addEventListener() twice');
obj.removeEventListener(eventName, callback);
@@ -296,9 +296,9 @@ export var test_Observable_identity = function () {
// All truthy thisArgs are distinct, so we have three distinct identities here
// and they should all get added.
obj.addEventListener(eventName, callback);
obj.addEventListener(eventName, callback, 1);
obj.addEventListener(eventName, callback, 2);
obj.addEventListener(eventName, callback, false);
obj.addEventListener(eventName, callback, false, 1);
obj.addEventListener(eventName, callback, false, 2);
obj.set('testName', 2);
TKUnit.assert(receivedCount === 3, 'Expected Observable to fire exactly three times upon a property change, having passed the same callback into addEventListener() three times, with the latter two distinguished by each having a different truthy thisArg');
obj.removeEventListener(eventName, callback);
@@ -307,24 +307,24 @@ export var test_Observable_identity = function () {
// If you specify thisArg when removing an event listener, it should remove
// just the event listener with the corresponding thisArg.
obj.addEventListener(eventName, callback, 1);
obj.addEventListener(eventName, callback, 2);
obj.addEventListener(eventName, callback, false, 1);
obj.addEventListener(eventName, callback, false, 2);
obj.set('testName', 3);
TKUnit.assert(receivedCount === 2, 'Expected Observable to fire exactly three times upon a property change, having passed the same callback into addEventListener() three times, with the latter two distinguished by each having a different truthy thisArg');
obj.removeEventListener(eventName, callback, 2);
obj.removeEventListener(eventName, callback, false, 2);
TKUnit.assert(obj.hasListeners(eventName), 'Expected removeEventListener(eventName, callback, thisArg) to remove just the event listener that matched the callback and thisArg');
obj.removeEventListener(eventName, callback, 1);
obj.removeEventListener(eventName, callback, false, 1);
TKUnit.assert(!obj.hasListeners(eventName), 'Expected removeEventListener(eventName, callback, thisArg) to remove the remaining event listener that matched the callback and thisArg');
receivedCount = 0;
// All falsy thisArgs are treated alike, so these all have the same identity
// and only the first should get added.
obj.addEventListener(eventName, callback);
obj.addEventListener(eventName, callback, 0);
obj.addEventListener(eventName, callback, false);
obj.addEventListener(eventName, callback, null);
obj.addEventListener(eventName, callback, undefined);
obj.addEventListener(eventName, callback, '');
obj.addEventListener(eventName, callback, false, 0);
obj.addEventListener(eventName, callback, false, false);
obj.addEventListener(eventName, callback, false, null);
obj.addEventListener(eventName, callback, false, undefined);
obj.addEventListener(eventName, callback, false, '');
obj.set('testName', 4);
TKUnit.assert(receivedCount === 1, 'Expected Observable to fire exactly once upon a property change, having passed the same callback into addEventListener() multiple times, each time with a different falsy (and therefore indistinct) thisArg');
obj.removeEventListener(eventName, callback);
@@ -376,7 +376,10 @@ export var test_Observable_removeEventListener_SingleEvent_NoCallbackSpecified =
obj.addEventListener(Observable.propertyChangeEvent, callback2);
obj.set('testName', 1);
obj.removeEventListener(Observable.propertyChangeEvent);
// @ts-expect-error the callback is no longer an optional argument on
// removeEventListener(), but is still optional for off().
TKUnit.assertThrows(() => obj.removeEventListener(Observable.propertyChangeEvent));
obj.off(Observable.propertyChangeEvent);
TKUnit.assert(!obj.hasListeners(Observable.propertyChangeEvent), 'Expected result for hasObservers is false.');

View File

@@ -111,7 +111,7 @@ function ensureStateListener(): SharedA11YObservable {
touchExplorationStateChangeListener = null;
if (sharedA11YObservable) {
sharedA11YObservable.removeEventListener(Observable.propertyChangeEvent);
sharedA11YObservable.off(Observable.propertyChangeEvent);
sharedA11YObservable = null;
}

View File

@@ -56,7 +56,7 @@ function getSharedA11YObservable(): SharedA11YObservable {
nativeObserver = null;
if (sharedA11YObservable) {
sharedA11YObservable.removeEventListener(Observable.propertyChangeEvent);
sharedA11YObservable.off(Observable.propertyChangeEvent);
sharedA11YObservable = null;
}

View File

@@ -89,6 +89,12 @@ const _globalEventHandlers: {
};
} = {};
/**
* A reusable ready-configured options object for internal use to avoid
* unnecessary allocations.
*/
const onceOption = { once: true } as const satisfies AddEventListenerOptions;
/**
* Observable is used when you want to be notified when a change occurs. Use on/off methods to add/remove listener.
* Please note that should you be using the `new Observable({})` constructor, it is **obsolete** since v3.0,
@@ -160,7 +166,7 @@ export class Observable {
* bound.
*/
public on(eventName: string, callback: (data: EventData) => void, thisArg?: any): void {
this.addEventListener(eventName, callback, thisArg);
this.addEventListener(eventName, callback, onceOption, thisArg);
}
/**
@@ -175,7 +181,7 @@ export class Observable {
* bound.
*/
public once(eventName: string, callback: (data: EventData) => void, thisArg?: any): void {
this.addEventListener(eventName, callback, thisArg, true);
this.addEventListener(eventName, callback, onceOption, thisArg);
}
/**
@@ -188,17 +194,26 @@ export class Observable {
* refine search of the correct event listener to be removed.
*/
public off(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void {
this.removeEventListener(eventName, callback, thisArg);
this.removeEventListener(eventName, callback, false, thisArg);
}
/**
* Adds a listener for the specified event name.
* @param eventName Name of the event to attach to.
* @param callback A function to be called when some of the specified event(s) is raised.
* @param thisArg An optional parameter which when set will be used as "this" in callback method call.
*
* @param eventName The name of the event.
* @param callback The event listener to add. Will be called when an event of
* the given name is raised.
* @param options An object or boolean indicating the EventListenerOptions.
* - If true, is interpreted as { capture: true }.
* - If false, is interpreted as { capture: false }.
* Note that 'capture' is not implemented at this time anyway, however, so the
* value is ignored. It's purely for API-compatibility with DOM Events.
* @param thisArg An optional parameter which, when set, will be bound as the
* `this` context when the callback is called. Falsy values will be not be
* bound.
*/
public addEventListener(eventName: string, callback: (data: EventData) => void, thisArg?: any, once?: boolean): void {
once = once || undefined;
public addEventListener(eventName: string, callback: (data: EventData) => void, options?: AddEventListenerOptions | boolean, thisArg?: any): void {
const once = (typeof options === 'object' && options.once) || undefined;
thisArg = thisArg || undefined;
if (typeof eventName !== 'string') {
@@ -223,20 +238,27 @@ export class Observable {
}
/**
* Removes listener(s) for the specified event name.
* @param eventName Name of the event to attach to.
* @param callback An optional parameter pointing to a specific listener. If not defined, all listeners for the event names will be removed.
* @param thisArg An optional parameter which when set will be used to refine search of the correct callback which will be removed as event listener.
* Removes the listener for the specified event name.
*
* @param eventName The name of the event.
* @param callback The event listener to remove.
* @param _options An object or boolean indicating the EventListenerOptions.
* - If true, is interpreted as { capture: true }.
* - If false, is interpreted as { capture: false }.
* Note that 'capture' is not implemented at this time anyway, however, so the
* value is ignored. It's purely for API-compatibility with DOM Events.
* @param thisArg An optional parameter which, when set, will be used to
* refine search of the correct event listener to be removed.
*/
public removeEventListener(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void {
public removeEventListener(eventName: string, callback: (data: EventData) => void, _options?: EventListenerOptions | boolean, thisArg?: any): void {
thisArg = thisArg || undefined;
if (typeof eventName !== 'string') {
throw new TypeError('Events name(s) must be string.');
}
if (callback && typeof callback !== 'function') {
throw new TypeError('callback must be function.');
if (typeof callback !== 'function') {
throw new TypeError('Callback must be a function.');
}
const entries = this._observers[eventName];
@@ -257,8 +279,8 @@ export class Observable {
* in future.
* @deprecated
*/
public static on(eventName: string, callback: (data: EventData) => void, thisArg?: any, once?: boolean): void {
this.addEventListener(eventName, callback, thisArg, once);
public static on(eventName: string, callback: (data: EventData) => void, thisArg?: any): void {
this.addEventListener(eventName, callback, false, thisArg);
}
/**
@@ -267,7 +289,7 @@ export class Observable {
* @deprecated
*/
public static once(eventName: string, callback: (data: EventData) => void, thisArg?: any): void {
this.addEventListener(eventName, callback, thisArg, true);
this.addEventListener(eventName, callback, onceOption, thisArg);
}
/**
@@ -276,7 +298,7 @@ export class Observable {
* @deprecated
*/
public static off(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void {
this.removeEventListener(eventName, callback, thisArg);
this.removeEventListener(eventName, callback, false, thisArg);
}
private static innerRemoveEventListener(entries: Array<ListenerEntry>, callback?: (data: EventData) => void, thisArg?: any): void {
@@ -306,15 +328,15 @@ export class Observable {
* in future.
* @deprecated
*/
public static removeEventListener(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void {
public static removeEventListener(eventName: string, callback: (data: EventData) => void, _options?: EventListenerOptions | boolean, thisArg?: any): void {
thisArg = thisArg || undefined;
if (typeof eventName !== 'string') {
throw new TypeError('Event name must be a string.');
}
if (callback && typeof callback !== 'function') {
throw new TypeError('Callback, if provided, must be function.');
if (typeof callback !== 'function') {
throw new TypeError('Callback must be a function.');
}
const eventClass = this.name === 'Observable' ? '*' : this.name;
@@ -343,8 +365,8 @@ export class Observable {
* in future.
* @deprecated
*/
public static addEventListener(eventName: string, callback: (data: EventData) => void, thisArg?: any, once?: boolean): void {
once = once || undefined;
public static addEventListener(eventName: string, callback: (data: EventData) => void, options?: AddEventListenerOptions | boolean, thisArg?: any): void {
const once = (typeof options === 'object' && options.once) || undefined;
thisArg = thisArg || undefined;
if (typeof eventName !== 'string') {

View File

@@ -297,11 +297,11 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
return this._gestureObservers[type];
}
public addEventListener(eventNames: string, callback: (data: EventData) => void, thisArg?: any) {
public addEventListener(eventName: string, callback: (data: EventData) => void, options?: AddEventListenerOptions | boolean, thisArg?: any) {
thisArg = thisArg || undefined;
// Normalize "ontap" -> "tap"
const normalizedName = getEventOrGestureName(eventNames);
const normalizedName = getEventOrGestureName(eventName);
// Coerce "tap" -> GestureTypes.tap
// Coerce "loaded" -> undefined
@@ -313,14 +313,14 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
return;
}
super.addEventListener(normalizedName, callback, thisArg);
super.addEventListener(normalizedName, callback, options, thisArg);
}
public removeEventListener(eventNames: string, callback?: (data: EventData) => void, thisArg?: any) {
public removeEventListener(eventName: string, callback?: (data: EventData) => void, options?: EventListenerOptions | boolean, thisArg?: any) {
thisArg = thisArg || undefined;
// Normalize "ontap" -> "tap"
const normalizedName = getEventOrGestureName(eventNames);
const normalizedName = getEventOrGestureName(eventName);
// Coerce "tap" -> GestureTypes.tap
// Coerce "loaded" -> undefined
@@ -332,7 +332,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
return;
}
super.removeEventListener(normalizedName, callback, thisArg);
super.removeEventListener(normalizedName, callback, options, thisArg);
}
public onBackPressed(): boolean {

View File

@@ -21,7 +21,7 @@ function getHandlerForEventName(eventName: string): (eventData: EventData) => vo
const sourceEventMap = sourcesMap.get(source);
if (!sourceEventMap) {
// There is no event map for this source - it is safe to detach the listener;
source.removeEventListener(eventName, handlersForEventName.get(eventName));
source.off(eventName, handlersForEventName.get(eventName));
return;
}
@@ -46,7 +46,7 @@ function getHandlerForEventName(eventName: string): (eventData: EventData) => vo
if (deadPairsIndexes.length === targetHandlerPairList.length) {
// There are no alive targets for this event - unsubscribe
source.removeEventListener(eventName, handlersForEventName.get(eventName));
source.off(eventName, handlersForEventName.get(eventName));
sourceEventMap.delete(eventName);
} else {
for (let j = deadPairsIndexes.length - 1; j >= 0; j--) {

View File

@@ -17,10 +17,10 @@ export abstract class ScrollViewBase extends ContentView implements ScrollViewDe
private _addedScrollEvent = false;
public addEventListener(arg: string, callback: (data: EventData) => void, thisArg?: any): void {
public addEventListener(arg: string, callback: (data: EventData) => void, options?: AddEventListenerOptions | boolean, thisArg?: any): void {
const hasExistingScrollListeners: boolean = this.hasListeners(ScrollViewBase.scrollEvent);
super.addEventListener(arg, callback, thisArg);
super.addEventListener(arg, callback, options, thisArg);
// This indicates that a scroll listener was added for first time
if (!hasExistingScrollListeners && this.hasListeners(ScrollViewBase.scrollEvent)) {
@@ -32,10 +32,10 @@ export abstract class ScrollViewBase extends ContentView implements ScrollViewDe
}
}
public removeEventListener(arg: string, callback?: (data: EventData) => void, thisArg?: any): void {
public removeEventListener(arg: string, callback?: (data: EventData) => void, options?: EventListenerOptions | boolean, thisArg?: any): void {
const hasExistingScrollListeners: boolean = this.hasListeners(ScrollViewBase.scrollEvent);
super.removeEventListener(arg, callback, thisArg);
super.removeEventListener(arg, callback, options, thisArg);
// This indicates that the final scroll listener was removed
if (hasExistingScrollListeners && !this.hasListeners(ScrollViewBase.scrollEvent)) {

View File

@@ -14,7 +14,7 @@ export class FormattedString extends ViewBase implements FormattedStringDefiniti
constructor() {
super();
this._spans = new ObservableArray<Span>();
this._spans.addEventListener(ObservableArray.changeEvent, this.onSpansCollectionChanged, this);
this._spans.addEventListener(ObservableArray.changeEvent, this.onSpansCollectionChanged, false, this);
}
get fontFamily(): string {

View File

@@ -109,13 +109,13 @@ export class Span extends ViewBase implements SpanDefinition {
return this._tappable;
}
addEventListener(arg: string, callback: (data: EventData) => void, thisArg?: any): void {
super.addEventListener(arg, callback, thisArg);
addEventListener(arg: string, callback: (data: EventData) => void, options?: AddEventListenerOptions | boolean, thisArg?: any): void {
super.addEventListener(arg, callback, options, thisArg);
this._setTappable(this.hasListeners(Span.linkTapEvent));
}
removeEventListener(arg: string, callback?: (data: EventData) => void, thisArg?: any): void {
super.removeEventListener(arg, callback, thisArg);
removeEventListener(arg: string, callback?: (data: EventData) => void, options?: EventListenerOptions | boolean, thisArg?: any): void {
super.removeEventListener(arg, callback, options, thisArg);
this._setTappable(this.hasListeners(Span.linkTapEvent));
}