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 // 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. // distinguishing by its thisArg, the second addition will no-op.
obj.addEventListener(eventName, callback); obj.addEventListener(eventName, callback, false);
obj.addEventListener(eventName, callback); obj.addEventListener(eventName, callback, false);
obj.set('testName', 1); 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'); 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); 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 // All truthy thisArgs are distinct, so we have three distinct identities here
// and they should all get added. // and they should all get added.
obj.addEventListener(eventName, callback); obj.addEventListener(eventName, callback, false);
obj.addEventListener(eventName, callback, 1); obj.addEventListener(eventName, callback, false, 1);
obj.addEventListener(eventName, callback, 2); obj.addEventListener(eventName, callback, false, 2);
obj.set('testName', 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'); 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); 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 // If you specify thisArg when removing an event listener, it should remove
// just the event listener with the corresponding thisArg. // just the event listener with the corresponding thisArg.
obj.addEventListener(eventName, callback, 1); obj.addEventListener(eventName, callback, false, 1);
obj.addEventListener(eventName, callback, 2); obj.addEventListener(eventName, callback, false, 2);
obj.set('testName', 3); 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'); 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'); 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'); TKUnit.assert(!obj.hasListeners(eventName), 'Expected removeEventListener(eventName, callback, thisArg) to remove the remaining event listener that matched the callback and thisArg');
receivedCount = 0; receivedCount = 0;
// All falsy thisArgs are treated alike, so these all have the same identity // All falsy thisArgs are treated alike, so these all have the same identity
// and only the first should get added. // 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, false);
obj.addEventListener(eventName, callback, null); obj.addEventListener(eventName, callback, false, 0);
obj.addEventListener(eventName, callback, undefined); obj.addEventListener(eventName, callback, false, false);
obj.addEventListener(eventName, callback, ''); obj.addEventListener(eventName, callback, false, null);
obj.addEventListener(eventName, callback, false, undefined);
obj.addEventListener(eventName, callback, false, '');
obj.set('testName', 4); 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'); 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); obj.removeEventListener(eventName, callback);
@@ -376,7 +376,10 @@ export var test_Observable_removeEventListener_SingleEvent_NoCallbackSpecified =
obj.addEventListener(Observable.propertyChangeEvent, callback2); obj.addEventListener(Observable.propertyChangeEvent, callback2);
obj.set('testName', 1); 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.'); TKUnit.assert(!obj.hasListeners(Observable.propertyChangeEvent), 'Expected result for hasObservers is false.');

View File

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

View File

@@ -56,7 +56,7 @@ function getSharedA11YObservable(): SharedA11YObservable {
nativeObserver = null; nativeObserver = null;
if (sharedA11YObservable) { if (sharedA11YObservable) {
sharedA11YObservable.removeEventListener(Observable.propertyChangeEvent); sharedA11YObservable.off(Observable.propertyChangeEvent);
sharedA11YObservable = null; 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. * 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, * 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. * bound.
*/ */
public on(eventName: string, callback: (data: EventData) => void, thisArg?: any): void { 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. * bound.
*/ */
public once(eventName: string, callback: (data: EventData) => void, thisArg?: any): void { 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. * refine search of the correct event listener to be removed.
*/ */
public off(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void { 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. * 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 eventName The name of the event.
* @param thisArg An optional parameter which when set will be used as "this" in callback method call. * @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 { public addEventListener(eventName: string, callback: (data: EventData) => void, options?: AddEventListenerOptions | boolean, thisArg?: any): void {
once = once || undefined; const once = (typeof options === 'object' && options.once) || undefined;
thisArg = thisArg || undefined; thisArg = thisArg || undefined;
if (typeof eventName !== 'string') { if (typeof eventName !== 'string') {
@@ -223,20 +238,27 @@ export class Observable {
} }
/** /**
* Removes listener(s) for the specified event name. * Removes the listener 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 eventName The name of the event.
* @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. * @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; thisArg = thisArg || undefined;
if (typeof eventName !== 'string') { if (typeof eventName !== 'string') {
throw new TypeError('Events name(s) must be string.'); throw new TypeError('Events name(s) must be string.');
} }
if (callback && typeof callback !== 'function') { if (typeof callback !== 'function') {
throw new TypeError('callback must be function.'); throw new TypeError('Callback must be a function.');
} }
const entries = this._observers[eventName]; const entries = this._observers[eventName];
@@ -257,8 +279,8 @@ export class Observable {
* in future. * in future.
* @deprecated * @deprecated
*/ */
public static on(eventName: string, callback: (data: EventData) => void, thisArg?: any, once?: boolean): void { public static on(eventName: string, callback: (data: EventData) => void, thisArg?: any): void {
this.addEventListener(eventName, callback, thisArg, once); this.addEventListener(eventName, callback, false, thisArg);
} }
/** /**
@@ -267,7 +289,7 @@ export class Observable {
* @deprecated * @deprecated
*/ */
public static once(eventName: string, callback: (data: EventData) => void, thisArg?: any): void { 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 * @deprecated
*/ */
public static off(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void { 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 { private static innerRemoveEventListener(entries: Array<ListenerEntry>, callback?: (data: EventData) => void, thisArg?: any): void {
@@ -306,15 +328,15 @@ export class Observable {
* in future. * in future.
* @deprecated * @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; thisArg = thisArg || undefined;
if (typeof eventName !== 'string') { if (typeof eventName !== 'string') {
throw new TypeError('Event name must be a string.'); throw new TypeError('Event name must be a string.');
} }
if (callback && typeof callback !== 'function') { if (typeof callback !== 'function') {
throw new TypeError('Callback, if provided, must be function.'); throw new TypeError('Callback must be a function.');
} }
const eventClass = this.name === 'Observable' ? '*' : this.name; const eventClass = this.name === 'Observable' ? '*' : this.name;
@@ -343,8 +365,8 @@ export class Observable {
* in future. * in future.
* @deprecated * @deprecated
*/ */
public static addEventListener(eventName: string, callback: (data: EventData) => void, thisArg?: any, once?: boolean): void { public static addEventListener(eventName: string, callback: (data: EventData) => void, options?: AddEventListenerOptions | boolean, thisArg?: any): void {
once = once || undefined; const once = (typeof options === 'object' && options.once) || undefined;
thisArg = thisArg || undefined; thisArg = thisArg || undefined;
if (typeof eventName !== 'string') { if (typeof eventName !== 'string') {

View File

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

View File

@@ -21,7 +21,7 @@ function getHandlerForEventName(eventName: string): (eventData: EventData) => vo
const sourceEventMap = sourcesMap.get(source); const sourceEventMap = sourcesMap.get(source);
if (!sourceEventMap) { if (!sourceEventMap) {
// There is no event map for this source - it is safe to detach the listener; // 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; return;
} }
@@ -46,7 +46,7 @@ function getHandlerForEventName(eventName: string): (eventData: EventData) => vo
if (deadPairsIndexes.length === targetHandlerPairList.length) { if (deadPairsIndexes.length === targetHandlerPairList.length) {
// There are no alive targets for this event - unsubscribe // There are no alive targets for this event - unsubscribe
source.removeEventListener(eventName, handlersForEventName.get(eventName)); source.off(eventName, handlersForEventName.get(eventName));
sourceEventMap.delete(eventName); sourceEventMap.delete(eventName);
} else { } else {
for (let j = deadPairsIndexes.length - 1; j >= 0; j--) { 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; 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); 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 // This indicates that a scroll listener was added for first time
if (!hasExistingScrollListeners && this.hasListeners(ScrollViewBase.scrollEvent)) { 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); 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 // This indicates that the final scroll listener was removed
if (hasExistingScrollListeners && !this.hasListeners(ScrollViewBase.scrollEvent)) { if (hasExistingScrollListeners && !this.hasListeners(ScrollViewBase.scrollEvent)) {

View File

@@ -14,7 +14,7 @@ export class FormattedString extends ViewBase implements FormattedStringDefiniti
constructor() { constructor() {
super(); super();
this._spans = new ObservableArray<Span>(); 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 { get fontFamily(): string {

View File

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