fix(core): drop support for plural event/gesture names (#10539)

This commit is contained in:
Jamie Birch
2024-05-07 10:20:28 +09:00
committed by GitHub
parent d323672b29
commit 9be392fbb0
8 changed files with 329 additions and 275 deletions

View File

@ -163,7 +163,7 @@ export var test_Observable_addEventListener_MultipleEvents = function () {
obj.addEventListener(events, callback); obj.addEventListener(events, callback);
obj.set('testName', 1); obj.set('testName', 1);
obj.test(); obj.test();
TKUnit.assert(receivedCount === 2, 'Callbacks not raised properly.'); TKUnit.assert(receivedCount === 0, "Expected no event handlers to fire upon the 'propertyChange' event when listening for event name 'propertyChange,tested', as we have dropped support for listening to plural event names.");
}; };
export var test_Observable_addEventListener_MultipleEvents_ShouldTrim = function () { export var test_Observable_addEventListener_MultipleEvents_ShouldTrim = function () {
@ -176,13 +176,14 @@ export var test_Observable_addEventListener_MultipleEvents_ShouldTrim = function
var events = Observable.propertyChangeEvent + ' , ' + TESTED_NAME; var events = Observable.propertyChangeEvent + ' , ' + TESTED_NAME;
obj.addEventListener(events, callback); obj.addEventListener(events, callback);
TKUnit.assert(obj.hasListeners(Observable.propertyChangeEvent), 'Observable.addEventListener for multiple events should trim each event name.'); TKUnit.assert(obj.hasListeners(events), "Expected a listener to be present for event name 'propertyChange , tested', as we have dropped support for splitting plural event names.");
TKUnit.assert(obj.hasListeners(TESTED_NAME), 'Observable.addEventListener for multiple events should trim each event name.'); TKUnit.assert(!obj.hasListeners(Observable.propertyChangeEvent), "Expected no listeners to be present for event name 'propertyChange', as we have dropped support for splitting plural event names.");
TKUnit.assert(!obj.hasListeners(TESTED_NAME), "Expected no listeners to be present for event name 'tested', as we have dropped support for splitting plural event names.");
obj.set('testName', 1); obj.set('testName', 1);
obj.test(); obj.test();
TKUnit.assert(receivedCount === 2, 'Callbacks not raised properly.'); TKUnit.assert(receivedCount === 0, "Expected no event handlers to fire upon the 'propertyChange' event when listening for event name 'propertyChange , tested', as we have dropped support for listening to plural event names (and trimming whitespace in event names).");
}; };
export var test_Observable_addEventListener_MultipleCallbacks = function () { export var test_Observable_addEventListener_MultipleCallbacks = function () {
@ -223,7 +224,7 @@ export var test_Observable_addEventListener_MultipleCallbacks_MultipleEvents = f
obj.set('testName', 1); obj.set('testName', 1);
obj.test(); obj.test();
TKUnit.assert(receivedCount === 4, 'The propertyChanged notification should be raised twice.'); TKUnit.assert(receivedCount === 0, "Expected no event handlers to fire upon the 'propertyChange' event when listening for event name 'propertyChange , tested' with two different callbacks, as we have dropped support for listening to plural event names (and trimming whitespace in event names).");
}; };
export var test_Observable_removeEventListener_SingleEvent_SingleCallback = function () { export var test_Observable_removeEventListener_SingleEvent_SingleCallback = function () {
@ -341,19 +342,22 @@ export var test_Observable_removeEventListener_MultipleEvents_SingleCallback = f
var events = Observable.propertyChangeEvent + ' , ' + TESTED_NAME; var events = Observable.propertyChangeEvent + ' , ' + TESTED_NAME;
obj.addEventListener(events, callback); obj.addEventListener(events, callback);
TKUnit.assert(obj.hasListeners(events), "Expected a listener to be present for event name 'propertyChange , tested', as we have dropped support for splitting plural event names.");
TKUnit.assert(!obj.hasListeners(Observable.propertyChangeEvent), "Expected no listeners to be present for event name 'propertyChange', as we have dropped support for splitting plural event names.");
TKUnit.assert(!obj.hasListeners(TESTED_NAME), "Expected no listeners to be present for event name 'tested', as we have dropped support for splitting plural event names.");
TKUnit.assert(receivedCount === 0, "Expected no event handlers to fire upon the 'propertyChange' event when listening for event name 'propertyChange , tested', as we have dropped support for listening to plural event names (and trimming whitespace in event names).");
obj.set('testName', 1); obj.set('testName', 1);
obj.test(); obj.test();
obj.removeEventListener(events, callback); obj.removeEventListener(events, callback);
TKUnit.assert(!obj.hasListeners(Observable.propertyChangeEvent), 'Expected result for hasObservers is false'); TKUnit.assert(!obj.hasListeners(events), "Expected the listener for event name 'propertyChange , tested' to have been removed, as we have dropped support for splitting plural event names.");
TKUnit.assert(!obj.hasListeners(TESTED_NAME), 'Expected result for hasObservers is false.');
obj.set('testName', 2); obj.set('testName', 2);
obj.test(); obj.test();
TKUnit.assert(receivedCount === 2, 'Expected receive count is 2'); TKUnit.assert(receivedCount === 0, "Expected no event handlers to fire upon the 'propertyChange' event when listening for event name 'propertyChange , tested', as we have dropped support for listening to plural event names (and trimming whitespace in event names).");
}; };
export var test_Observable_removeEventListener_SingleEvent_NoCallbackSpecified = function () { export var test_Observable_removeEventListener_SingleEvent_NoCallbackSpecified = function () {

View File

@ -89,8 +89,6 @@ const _globalEventHandlers: {
}; };
} = {}; } = {};
const eventNamesRegex = /\s*,\s*/;
/** /**
* 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,
@ -153,54 +151,53 @@ export class Observable {
/** /**
* A basic method signature to hook an event listener (shortcut alias to the addEventListener method). * A basic method signature to hook an event listener (shortcut alias to the addEventListener method).
* @param eventNames - String corresponding to events (e.g. "propertyChange"). Optionally could be used more events separated by `,` (e.g. "propertyChange", "change"). * @param eventName Name of the event to attach to.
* @param callback - Callback function which will be executed when event is raised. * @param callback - Callback function which will be executed when event is raised.
* @param thisArg - An optional parameter which will be used as `this` context for callback execution. * @param thisArg - An optional parameter which will be used as `this` context for callback execution.
*/ */
public on(eventNames: string, callback: (data: EventData) => void, thisArg?: any): void { public on(eventName: string, callback: (data: EventData) => void, thisArg?: any): void {
this.addEventListener(eventNames, callback, thisArg); this.addEventListener(eventName, callback, thisArg);
} }
/** /**
* Adds one-time listener function for the event named `event`. * Adds one-time listener function for the event named `event`.
* @param event Name of the event to attach to. * @param eventName Name of the event to attach to.
* @param callback A function to be called when the specified event is raised. * @param callback A function to be called when the specified event is raised.
* @param thisArg An optional parameter which when set will be used as "this" in callback method call. * @param thisArg An optional parameter which when set will be used as "this" in callback method call.
*/ */
public once(event: string, callback: (data: EventData) => void, thisArg?: any): void { public once(eventName: string, callback: (data: EventData) => void, thisArg?: any): void {
this.addEventListener(event, callback, thisArg, true); this.addEventListener(eventName, callback, thisArg, true);
} }
/** /**
* Shortcut alias to the removeEventListener method. * Shortcut alias to the removeEventListener method.
*/ */
public off(eventNames: string, callback?: (data: EventData) => void, thisArg?: any): void { public off(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void {
this.removeEventListener(eventNames, callback, thisArg); this.removeEventListener(eventName, callback, thisArg);
} }
/** /**
* Adds a listener for the specified event name. * Adds a listener for the specified event name.
* @param eventNames Comma delimited names of the events to attach the listener to. * @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 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 thisArg An optional parameter which when set will be used as "this" in callback method call.
*/ */
public addEventListener(eventNames: string, callback: (data: EventData) => void, thisArg?: any, once?: boolean): void { public addEventListener(eventName: string, callback: (data: EventData) => void, thisArg?: any, once?: boolean): void {
once = once || undefined; once = once || undefined;
thisArg = thisArg || undefined; thisArg = thisArg || undefined;
if (typeof eventNames !== 'string') { if (typeof eventName !== 'string') {
throw new TypeError('Event name(s) must be a string.'); throw new TypeError('Event name must be a string.');
} }
if (typeof callback !== 'function') { if (typeof callback !== 'function') {
throw new TypeError('Callback, if provided, must be a function.'); throw new TypeError('Callback, if provided, must be a function.');
} }
for (const eventName of eventNames.trim().split(eventNamesRegex)) {
const list = this._getEventList(eventName, true); const list = this._getEventList(eventName, true);
if (Observable._indexOfListener(list, callback, thisArg) !== -1) { if (Observable._indexOfListener(list, callback, thisArg) !== -1) {
// Already added. // Already added.
continue; return;
} }
list.push({ list.push({
@ -209,18 +206,17 @@ export class Observable {
once, once,
}); });
} }
}
/** /**
* Removes listener(s) for the specified event name. * Removes listener(s) for the specified event name.
* @param eventNames Comma delimited names of the events the specified listener is associated with. * @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 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. * @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.
*/ */
public removeEventListener(eventNames: string, callback?: (data: EventData) => void, thisArg?: any): void { public removeEventListener(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void {
thisArg = thisArg || undefined; thisArg = thisArg || undefined;
if (typeof eventNames !== 'string') { if (typeof eventName !== 'string') {
throw new TypeError('Events name(s) must be string.'); throw new TypeError('Events name(s) must be string.');
} }
@ -228,10 +224,9 @@ export class Observable {
throw new TypeError('callback must be function.'); throw new TypeError('callback must be function.');
} }
for (const eventName of eventNames.trim().split(eventNamesRegex)) {
const entries = this._observers[eventName]; const entries = this._observers[eventName];
if (!entries) { if (!entries) {
continue; return;
} }
Observable.innerRemoveEventListener(entries, callback, thisArg); Observable.innerRemoveEventListener(entries, callback, thisArg);
@ -241,7 +236,6 @@ export class Observable {
delete this._observers[eventName]; delete this._observers[eventName];
} }
} }
}
/** /**
* Please avoid using the static event-handling APIs as they will be removed * Please avoid using the static event-handling APIs as they will be removed
@ -297,11 +291,11 @@ export class Observable {
* in future. * in future.
* @deprecated * @deprecated
*/ */
public static removeEventListener(eventNames: string, callback?: (data: EventData) => void, thisArg?: any): void { public static removeEventListener(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void {
thisArg = thisArg || undefined; thisArg = thisArg || undefined;
if (typeof eventNames !== 'string') { if (typeof eventName !== 'string') {
throw new TypeError('Event name(s) must be a string.'); throw new TypeError('Event name must be a string.');
} }
if (callback && typeof callback !== 'function') { if (callback && typeof callback !== 'function') {
@ -310,10 +304,9 @@ export class Observable {
const eventClass = this.name === 'Observable' ? '*' : this.name; const eventClass = this.name === 'Observable' ? '*' : this.name;
for (const eventName of eventNames.trim().split(eventNamesRegex)) {
const entries = _globalEventHandlers?.[eventClass]?.[eventName]; const entries = _globalEventHandlers?.[eventClass]?.[eventName];
if (!entries) { if (!entries) {
continue; return;
} }
Observable.innerRemoveEventListener(entries, callback, thisArg); Observable.innerRemoveEventListener(entries, callback, thisArg);
@ -329,19 +322,18 @@ export class Observable {
delete _globalEventHandlers[eventClass]; delete _globalEventHandlers[eventClass];
} }
} }
}
/** /**
* Please avoid using the static event-handling APIs as they will be removed * Please avoid using the static event-handling APIs as they will be removed
* in future. * in future.
* @deprecated * @deprecated
*/ */
public static addEventListener(eventNames: string, callback: (data: EventData) => void, thisArg?: any, once?: boolean): void { public static addEventListener(eventName: string, callback: (data: EventData) => void, thisArg?: any, once?: boolean): void {
once = once || undefined; once = once || undefined;
thisArg = thisArg || undefined; thisArg = thisArg || undefined;
if (typeof eventNames !== 'string') { if (typeof eventName !== 'string') {
throw new TypeError('Event name(s) must be a string.'); throw new TypeError('Event name must be a string.');
} }
if (typeof callback !== 'function') { if (typeof callback !== 'function') {
@ -353,7 +345,6 @@ export class Observable {
_globalEventHandlers[eventClass] = {}; _globalEventHandlers[eventClass] = {};
} }
for (const eventName of eventNames.trim().split(eventNamesRegex)) {
if (!_globalEventHandlers[eventClass][eventName]) { if (!_globalEventHandlers[eventClass][eventName]) {
_globalEventHandlers[eventClass][eventName] = []; _globalEventHandlers[eventClass][eventName] = [];
} }
@ -364,7 +355,6 @@ export class Observable {
_globalEventHandlers[eventClass][eventName].push({ callback, thisArg, once }); _globalEventHandlers[eventClass][eventName].push({ callback, thisArg, once });
} }
}
private _globalNotify<T extends EventData>(eventClass: string, eventType: string, data: T): void { private _globalNotify<T extends EventData>(eventClass: string, eventType: string, data: T): void {
// Check for the Global handlers for JUST this class // Check for the Global handlers for JUST this class
@ -464,11 +454,9 @@ export class Observable {
}; };
} }
public _emit(eventNames: string): void { public _emit(eventName: string): void {
for (const eventName of eventNames.trim().split(eventNamesRegex)) {
this.notify({ eventName, object: this }); this.notify({ eventName, object: this });
} }
}
private _getEventList(eventName: string, createIfNeeded?: boolean): Array<ListenerEntry> | undefined { private _getEventList(eventName: string, createIfNeeded?: boolean): Array<ListenerEntry> | undefined {
if (!eventName) { if (!eventName) {

View File

@ -4,6 +4,7 @@ import { ViewBase } from '../view-base';
// Requires // Requires
import { unsetValue } from '../properties'; import { unsetValue } from '../properties';
import { Observable, PropertyChangeData } from '../../../data/observable'; import { Observable, PropertyChangeData } from '../../../data/observable';
import { fromString as gestureFromString } from '../../../ui/gestures/gestures-common';
import { addWeakEventListener, removeWeakEventListener } from '../weak-event-listener'; import { addWeakEventListener, removeWeakEventListener } from '../weak-event-listener';
import { bindingConstants, parentsRegex } from '../../builder/binding-builder'; import { bindingConstants, parentsRegex } from '../../builder/binding-builder';
import { escapeRegexSymbols } from '../../../utils'; import { escapeRegexSymbols } from '../../../utils';
@ -87,25 +88,45 @@ export interface ValueConverter {
toView: (...params: any[]) => any; toView: (...params: any[]) => any;
} }
/**
* Normalizes "ontap" to "tap", and "ondoubletap" to "ondoubletap".
*
* Removes the leading "on" from an event gesture name, for example:
* - "ontap" -> "tap"
* - "ondoubletap" -> "doubletap"
* - "onTap" -> "Tap"
*
* Be warned that, as event/gesture names in NativeScript are case-sensitive,
* this may produce an invalid event/gesture name (i.e. "doubletap" would fail
* to match the "doubleTap" gesture name), and so it is up to the consumer to
* handle the output properly.
*/
export function getEventOrGestureName(name: string): string { export function getEventOrGestureName(name: string): string {
return name.indexOf('on') === 0 ? name.substr(2, name.length - 2) : name; return name.indexOf('on') === 0 ? name.slice(2) : name;
} }
// NOTE: method fromString from "ui/gestures";
export function isGesture(eventOrGestureName: string): boolean { export function isGesture(eventOrGestureName: string): boolean {
// Not sure whether this trimming and lowercasing is still needed in practice
// (all Core tests pass without it), so worth revisiting in future. I think
// this is used exclusively by the XML flavour, and my best guess is that
// maybe it's to handle how getEventOrGestureName("onTap") might pass "Tap"
// into this.
const t = eventOrGestureName.trim().toLowerCase(); const t = eventOrGestureName.trim().toLowerCase();
// Would be nice to have a convenience function for getting all GestureState
// names in `gestures-common.ts`, but when I tried introducing it, it created
// a circular dependency that crashed the automated tests app.
return t === 'tap' || t === 'doubletap' || t === 'pinch' || t === 'pan' || t === 'swipe' || t === 'rotation' || t === 'longpress' || t === 'touch'; return t === 'tap' || t === 'doubletap' || t === 'pinch' || t === 'pan' || t === 'swipe' || t === 'rotation' || t === 'longpress' || t === 'touch';
} }
// TODO: Make this instance function so that we dont need public statc tapEvent = "tap" // TODO: Make this instance function so that we dont need public static tapEvent = "tap"
// in controls. They will just override this one and provide their own event support. // in controls. They will just override this one and provide their own event support.
export function isEventOrGesture(name: string, view: ViewBase): boolean { export function isEventOrGesture(name: string, view: ViewBase): boolean {
if (typeof name === 'string') { if (typeof name === 'string') {
const eventOrGestureName = getEventOrGestureName(name); const eventOrGestureName = getEventOrGestureName(name);
const evt = `${eventOrGestureName}Event`; const evt = `${eventOrGestureName}Event`;
return (view.constructor && evt in view.constructor) || isGesture(eventOrGestureName.toLowerCase()); return (view.constructor && evt in view.constructor) || isGesture(eventOrGestureName);
} }
return false; return false;

View File

@ -305,11 +305,11 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
// Coerce "tap" -> GestureTypes.tap // Coerce "tap" -> GestureTypes.tap
// Coerce "loaded" -> undefined // Coerce "loaded" -> undefined
const gesture: GestureTypes | undefined = gestureFromString(normalizedName); const gestureType: GestureTypes | undefined = gestureFromString(normalizedName);
// If it's a gesture (and this Observable declares e.g. `static tapEvent`) // If it's a gesture (and this Observable declares e.g. `static tapEvent`)
if (gesture && !this._isEvent(normalizedName)) { if (gestureType && !this._isEvent(normalizedName)) {
this._observe(gesture, callback, thisArg); this._observe(gestureType, callback, thisArg);
return; return;
} }
@ -324,11 +324,11 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
// Coerce "tap" -> GestureTypes.tap // Coerce "tap" -> GestureTypes.tap
// Coerce "loaded" -> undefined // Coerce "loaded" -> undefined
const gesture: GestureTypes | undefined = gestureFromString(normalizedName); const gestureType: GestureTypes | undefined = gestureFromString(normalizedName);
// If it's a gesture (and this Observable declares e.g. `static tapEvent`) // If it's a gesture (and this Observable declares e.g. `static tapEvent`)
if (gesture && !this._isEvent(normalizedName)) { if (gestureType && !this._isEvent(normalizedName)) {
this._disconnectGestureObservers(gesture, callback, thisArg); this._disconnectGestureObservers(gestureType, callback, thisArg);
return; return;
} }

View File

@ -280,59 +280,45 @@ export interface RotationGestureEventData extends GestureEventDataWithState {
/** /**
* Returns a string representation of a gesture type. * Returns a string representation of a gesture type.
* @param type - Type of the gesture. * @param type - The singular type of the gesture. Looks for an exact match, so
* @param separator(optional) - Text separator between gesture type strings. * passing plural types like `GestureTypes.tap & GestureTypes.doubleTap` will
* simply return undefined.
*/ */
export function toString(type: GestureTypes, separator?: string): string { export function toString(type: GestureTypes): (typeof GestureTypes)[GestureTypes] | undefined {
// We can get stronger typings with `keyof typeof GestureTypes`, but sadly switch (type) {
// indexing into an enum simply returns `string`, so we'd have to type-assert case GestureTypes.tap:
// all of the below anyway. Even this `(typeof GestureTypes)[GestureTypes]` is return GestureTypes[GestureTypes.tap];
// more for documentation than for type-safety (it resolves to `string`, too).
const types = new Array<(typeof GestureTypes)[GestureTypes]>();
if (type & GestureTypes.tap) { case GestureTypes.doubleTap:
types.push(GestureTypes[GestureTypes.tap]); return GestureTypes[GestureTypes.doubleTap];
case GestureTypes.pinch:
return GestureTypes[GestureTypes.pinch];
case GestureTypes.pan:
return GestureTypes[GestureTypes.pan];
case GestureTypes.swipe:
return GestureTypes[GestureTypes.swipe];
case GestureTypes.rotation:
return GestureTypes[GestureTypes.rotation];
case GestureTypes.longPress:
return GestureTypes[GestureTypes.longPress];
case GestureTypes.touch:
return GestureTypes[GestureTypes.touch];
} }
if (type & GestureTypes.doubleTap) {
types.push(GestureTypes[GestureTypes.doubleTap]);
}
if (type & GestureTypes.pinch) {
types.push(GestureTypes[GestureTypes.pinch]);
}
if (type & GestureTypes.pan) {
types.push(GestureTypes[GestureTypes.pan]);
}
if (type & GestureTypes.swipe) {
types.push(GestureTypes[GestureTypes.swipe]);
}
if (type & GestureTypes.rotation) {
types.push(GestureTypes[GestureTypes.rotation]);
}
if (type & GestureTypes.longPress) {
types.push(GestureTypes[GestureTypes.longPress]);
}
if (type & GestureTypes.touch) {
types.push(GestureTypes[GestureTypes.touch]);
}
return types.join(separator);
} }
// NOTE: toString could return the text of multiple GestureTypes.
// Souldn't fromString do split on separator and return multiple GestureTypes?
/** /**
* Returns a gesture type enum value from a string (case insensitive). * Returns a gesture type enum value from a string (case insensitive).
* @param type - A string representation of a gesture type (e.g. Tap). *
* @param type - A string representation of a single gesture type (e.g. "tap").
*/ */
export function fromString(type: string): GestureTypes | undefined { export function fromString(type: (typeof GestureTypes)[GestureTypes]): GestureTypes | undefined {
return GestureTypes[type.trim()]; return GestureTypes[type];
} }
export abstract class GesturesObserverBase implements GesturesObserverDefinition { export abstract class GesturesObserverBase implements GesturesObserverDefinition {
@ -340,7 +326,8 @@ export abstract class GesturesObserverBase implements GesturesObserverDefinition
private _target: View; private _target: View;
private _context?: any; private _context?: any;
public type: GestureTypes; /** This is populated on the first call to observe(). */
type: GestureTypes;
public get callback(): (args: GestureEventData) => void { public get callback(): (args: GestureEventData) => void {
return this._callback; return this._callback;

View File

@ -27,14 +27,14 @@ function initializeTapAndDoubleTapGestureListener() {
class TapAndDoubleTapGestureListenerImpl extends android.view.GestureDetector.SimpleOnGestureListener { class TapAndDoubleTapGestureListenerImpl extends android.view.GestureDetector.SimpleOnGestureListener {
private _observer: GesturesObserver; private _observer: GesturesObserver;
private _target: View; private _target: View;
private _type: number; private _type: GestureTypes;
private _lastUpTime = 0; private _lastUpTime = 0;
private _tapTimeoutId: number; private _tapTimeoutId: number;
private static DoubleTapTimeout = android.view.ViewConfiguration.getDoubleTapTimeout(); private static DoubleTapTimeout = android.view.ViewConfiguration.getDoubleTapTimeout();
constructor(observer: GesturesObserver, target: View, type: number) { constructor(observer: GesturesObserver, target: View, type: GestureTypes) {
super(); super();
this._observer = observer; this._observer = observer;
@ -61,7 +61,7 @@ function initializeTapAndDoubleTapGestureListener() {
} }
public onLongPress(motionEvent: android.view.MotionEvent): void { public onLongPress(motionEvent: android.view.MotionEvent): void {
if (this._type & GestureTypes.longPress) { if (this._type === GestureTypes.longPress) {
const args = _getLongPressArgs(GestureTypes.longPress, this._target, GestureStateTypes.began, motionEvent); const args = _getLongPressArgs(GestureTypes.longPress, this._target, GestureStateTypes.began, motionEvent);
_executeCallback(this._observer, args); _executeCallback(this._observer, args);
} }
@ -70,14 +70,14 @@ function initializeTapAndDoubleTapGestureListener() {
private _handleSingleTap(motionEvent: android.view.MotionEvent): void { private _handleSingleTap(motionEvent: android.view.MotionEvent): void {
if (this._target.getGestureObservers(GestureTypes.doubleTap)) { if (this._target.getGestureObservers(GestureTypes.doubleTap)) {
this._tapTimeoutId = timer.setTimeout(() => { this._tapTimeoutId = timer.setTimeout(() => {
if (this._type & GestureTypes.tap) { if (this._type === GestureTypes.tap) {
const args = _getTapArgs(GestureTypes.tap, this._target, motionEvent); const args = _getTapArgs(GestureTypes.tap, this._target, motionEvent);
_executeCallback(this._observer, args); _executeCallback(this._observer, args);
} }
timer.clearTimeout(this._tapTimeoutId); timer.clearTimeout(this._tapTimeoutId);
}, TapAndDoubleTapGestureListenerImpl.DoubleTapTimeout); }, TapAndDoubleTapGestureListenerImpl.DoubleTapTimeout);
} else { } else {
if (this._type & GestureTypes.tap) { if (this._type === GestureTypes.tap) {
const args = _getTapArgs(GestureTypes.tap, this._target, motionEvent); const args = _getTapArgs(GestureTypes.tap, this._target, motionEvent);
_executeCallback(this._observer, args); _executeCallback(this._observer, args);
} }
@ -88,7 +88,7 @@ function initializeTapAndDoubleTapGestureListener() {
if (this._tapTimeoutId) { if (this._tapTimeoutId) {
timer.clearTimeout(this._tapTimeoutId); timer.clearTimeout(this._tapTimeoutId);
} }
if (this._type & GestureTypes.doubleTap) { if (this._type === GestureTypes.doubleTap) {
const args = _getTapArgs(GestureTypes.doubleTap, this._target, motionEvent); const args = _getTapArgs(GestureTypes.doubleTap, this._target, motionEvent);
_executeCallback(this._observer, args); _executeCallback(this._observer, args);
} }
@ -252,12 +252,16 @@ export class GesturesObserver extends GesturesObserverBase {
private _onTargetUnloaded: (data: EventData) => void; private _onTargetUnloaded: (data: EventData) => void;
public observe(type: GestureTypes) { public observe(type: GestureTypes) {
if (this.target) {
this.type = type; this.type = type;
this._onTargetLoaded = (args) => {
if (!this.target) {
return;
}
this._onTargetLoaded = () => {
this._attach(this.target, type); this._attach(this.target, type);
}; };
this._onTargetUnloaded = (args) => { this._onTargetUnloaded = () => {
this._detach(); this._detach();
}; };
@ -268,7 +272,6 @@ export class GesturesObserver extends GesturesObserverBase {
this._attach(this.target, type); this._attach(this.target, type);
} }
} }
}
public disconnect() { public disconnect() {
this._detach(); this._detach();
@ -280,6 +283,7 @@ export class GesturesObserver extends GesturesObserverBase {
this._onTargetLoaded = null; this._onTargetLoaded = null;
this._onTargetUnloaded = null; this._onTargetUnloaded = null;
} }
// clears target, context and callback references // clears target, context and callback references
super.disconnect(); super.disconnect();
} }
@ -297,43 +301,57 @@ export class GesturesObserver extends GesturesObserverBase {
private _attach(target: View, type: GestureTypes) { private _attach(target: View, type: GestureTypes) {
this._detach(); this._detach();
let recognizer; let recognizer: unknown;
if (type & GestureTypes.tap || type & GestureTypes.doubleTap || type & GestureTypes.longPress) { switch (type) {
// Whether it's a tap, doubleTap, or longPress, we handle with the same
// listener. It'll listen for all three of these gesture types, but only
// notify if the type it was registered with matched the relevant gesture.
case GestureTypes.tap:
case GestureTypes.doubleTap:
case GestureTypes.longPress: {
initializeTapAndDoubleTapGestureListener(); initializeTapAndDoubleTapGestureListener();
recognizer = this._simpleGestureDetector = <any>new androidx.core.view.GestureDetectorCompat(target._context, new TapAndDoubleTapGestureListener(this, this.target, type)); recognizer = this._simpleGestureDetector = <any>new androidx.core.view.GestureDetectorCompat(target._context, new TapAndDoubleTapGestureListener(this, this.target, type));
break;
} }
case GestureTypes.pinch: {
if (type & GestureTypes.pinch) {
initializePinchGestureListener(); initializePinchGestureListener();
recognizer = this._scaleGestureDetector = new android.view.ScaleGestureDetector(target._context, new PinchGestureListener(this, this.target)); recognizer = this._scaleGestureDetector = new android.view.ScaleGestureDetector(target._context, new PinchGestureListener(this, this.target));
break;
} }
if (type & GestureTypes.swipe) { case GestureTypes.swipe: {
initializeSwipeGestureListener(); initializeSwipeGestureListener();
recognizer = this._swipeGestureDetector = <any>new androidx.core.view.GestureDetectorCompat(target._context, new SwipeGestureListener(this, this.target)); recognizer = this._swipeGestureDetector = <any>new androidx.core.view.GestureDetectorCompat(target._context, new SwipeGestureListener(this, this.target));
break;
} }
if (type & GestureTypes.pan) { case GestureTypes.pan: {
recognizer = this._panGestureDetector = new CustomPanGestureDetector(this, this.target); recognizer = this._panGestureDetector = new CustomPanGestureDetector(this, this.target);
break;
} }
if (type & GestureTypes.rotation) { case GestureTypes.rotation: {
recognizer = this._rotateGestureDetector = new CustomRotateGestureDetector(this, this.target); recognizer = this._rotateGestureDetector = new CustomRotateGestureDetector(this, this.target);
break;
} }
if (type & GestureTypes.touch) { case GestureTypes.touch: {
this._notifyTouch = true; this._notifyTouch = true;
} else { // For touch events, return early rather than breaking from the switch
// statement.
return;
}
}
this.target.notify({ this.target.notify({
eventName: GestureEvents.gestureAttached, eventName: GestureEvents.gestureAttached,
object: this.target, object: this.target,
type, type: type,
view: this.target, view: this.target,
android: recognizer, android: recognizer,
}); });
} }
}
public androidOnTouchEvent(motionEvent: android.view.MotionEvent) { public androidOnTouchEvent(motionEvent: android.view.MotionEvent) {
if (this._notifyTouch) { if (this._notifyTouch) {
@ -430,7 +448,13 @@ class PinchGestureEventData implements PinchGestureEventData {
public eventName = toString(GestureTypes.pinch); public eventName = toString(GestureTypes.pinch);
public ios; public ios;
constructor(public view: View, public android: android.view.ScaleGestureDetector, public scale: number, public object: any, public state: GestureStateTypes) {} constructor(
public view: View,
public android: android.view.ScaleGestureDetector,
public scale: number,
public object: any,
public state: GestureStateTypes,
) {}
getFocusX(): number { getFocusX(): number {
return this.android.getFocusX() / layout.getDisplayDensity(); return this.android.getFocusX() / layout.getDisplayDensity();
@ -669,7 +693,10 @@ class Pointer implements Pointer {
public android: number; public android: number;
public ios: any = undefined; public ios: any = undefined;
constructor(id: number, private event: android.view.MotionEvent) { constructor(
id: number,
private event: android.view.MotionEvent,
) {
this.android = id; this.android = id;
} }

View File

@ -27,7 +27,9 @@ export class GesturesObserver {
disconnect(); disconnect();
/** /**
* Gesture type attached to the observer. * Singular gesture type (e.g. GestureTypes.tap) attached to the observer.
* Does not support plural gesture types (e.g.
* GestureTypes.tap & GestureTypes.doubleTap).
*/ */
type: GestureTypes; type: GestureTypes;

View File

@ -91,27 +91,32 @@ class UIGestureRecognizerImpl extends NSObject {
} }
export class GesturesObserver extends GesturesObserverBase { export class GesturesObserver extends GesturesObserverBase {
private _recognizers: {}; private readonly _recognizers: { [type: string]: RecognizerCache } = {};
private _onTargetLoaded: (data: EventData) => void; private _onTargetLoaded: (data: EventData) => void;
private _onTargetUnloaded: (data: EventData) => void; private _onTargetUnloaded: (data: EventData) => void;
constructor(target: View, callback: (args: GestureEventData) => void, context: any) {
super(target, callback, context);
this._recognizers = {};
}
public androidOnTouchEvent(motionEvent: android.view.MotionEvent): void { public androidOnTouchEvent(motionEvent: android.view.MotionEvent): void {
// //
} }
/**
* Observes a singular GestureTypes value (e.g. GestureTypes.tap).
*
* Does not support observing plural GestureTypes values, e.g.
* GestureTypes.tap & GestureTypes.doubleTap.
*/
public observe(type: GestureTypes) { public observe(type: GestureTypes) {
if (this.target) {
this.type = type; this.type = type;
this._onTargetLoaded = (args) => {
if (!this.target) {
return;
}
this._onTargetLoaded = () => {
this._attach(this.target, type); this._attach(this.target, type);
}; };
this._onTargetUnloaded = (args) => { this._onTargetUnloaded = () => {
this._detach(); this._detach();
}; };
@ -122,55 +127,69 @@ export class GesturesObserver extends GesturesObserverBase {
this._attach(this.target, type); this._attach(this.target, type);
} }
} }
}
/**
* Given a singular GestureTypes value (e.g. GestureTypes.tap), adds a
* UIGestureRecognizer for it and populates a RecognizerCache entry in
* this._recognizers.
*
* Does not support attaching plural GestureTypes values, e.g.
* GestureTypes.tap & GestureTypes.doubleTap.
*/
private _attach(target: View, type: GestureTypes) { private _attach(target: View, type: GestureTypes) {
this._detach(); this._detach();
if (target && target.nativeViewProtected && target.nativeViewProtected.addGestureRecognizer) { const nativeView = target?.nativeViewProtected as UIView | undefined;
const nativeView = <UIView>target.nativeViewProtected; if (!nativeView?.addGestureRecognizer) {
return;
}
if (type & GestureTypes.tap) { switch (type) {
case GestureTypes.tap: {
nativeView.addGestureRecognizer( nativeView.addGestureRecognizer(
this._createRecognizer(GestureTypes.tap, (args) => { this._createRecognizer(GestureTypes.tap, (args) => {
if (args.view) { if (args.view) {
this._executeCallback(_getTapData(args)); this._executeCallback(_getTapData(args));
} }
}) }),
); );
break;
} }
if (type & GestureTypes.doubleTap) { case GestureTypes.doubleTap: {
nativeView.addGestureRecognizer( nativeView.addGestureRecognizer(
this._createRecognizer(GestureTypes.doubleTap, (args) => { this._createRecognizer(GestureTypes.doubleTap, (args) => {
if (args.view) { if (args.view) {
this._executeCallback(_getTapData(args)); this._executeCallback(_getTapData(args));
} }
}) }),
); );
break;
} }
if (type & GestureTypes.pinch) { case GestureTypes.pinch: {
nativeView.addGestureRecognizer( nativeView.addGestureRecognizer(
this._createRecognizer(GestureTypes.pinch, (args) => { this._createRecognizer(GestureTypes.pinch, (args) => {
if (args.view) { if (args.view) {
this._executeCallback(_getPinchData(args)); this._executeCallback(_getPinchData(args));
} }
}) }),
); );
break;
} }
if (type & GestureTypes.pan) { case GestureTypes.pan: {
nativeView.addGestureRecognizer( nativeView.addGestureRecognizer(
this._createRecognizer(GestureTypes.pan, (args) => { this._createRecognizer(GestureTypes.pan, (args) => {
if (args.view) { if (args.view) {
this._executeCallback(_getPanData(args, target.nativeViewProtected)); this._executeCallback(_getPanData(args, target.nativeViewProtected));
} }
}) }),
); );
break;
} }
if (type & GestureTypes.swipe) { case GestureTypes.swipe: {
nativeView.addGestureRecognizer( nativeView.addGestureRecognizer(
this._createRecognizer( this._createRecognizer(
GestureTypes.swipe, GestureTypes.swipe,
@ -179,8 +198,8 @@ export class GesturesObserver extends GesturesObserverBase {
this._executeCallback(_getSwipeData(args)); this._executeCallback(_getSwipeData(args));
} }
}, },
UISwipeGestureRecognizerDirection.Down UISwipeGestureRecognizerDirection.Down,
) ),
); );
nativeView.addGestureRecognizer( nativeView.addGestureRecognizer(
@ -191,8 +210,8 @@ export class GesturesObserver extends GesturesObserverBase {
this._executeCallback(_getSwipeData(args)); this._executeCallback(_getSwipeData(args));
} }
}, },
UISwipeGestureRecognizerDirection.Left UISwipeGestureRecognizerDirection.Left,
) ),
); );
nativeView.addGestureRecognizer( nativeView.addGestureRecognizer(
@ -203,8 +222,8 @@ export class GesturesObserver extends GesturesObserverBase {
this._executeCallback(_getSwipeData(args)); this._executeCallback(_getSwipeData(args));
} }
}, },
UISwipeGestureRecognizerDirection.Right UISwipeGestureRecognizerDirection.Right,
) ),
); );
nativeView.addGestureRecognizer( nativeView.addGestureRecognizer(
@ -215,49 +234,49 @@ export class GesturesObserver extends GesturesObserverBase {
this._executeCallback(_getSwipeData(args)); this._executeCallback(_getSwipeData(args));
} }
}, },
UISwipeGestureRecognizerDirection.Up UISwipeGestureRecognizerDirection.Up,
) ),
); );
break;
} }
if (type & GestureTypes.rotation) { case GestureTypes.rotation: {
nativeView.addGestureRecognizer( nativeView.addGestureRecognizer(
this._createRecognizer(GestureTypes.rotation, (args) => { this._createRecognizer(GestureTypes.rotation, (args) => {
if (args.view) { if (args.view) {
this._executeCallback(_getRotationData(args)); this._executeCallback(_getRotationData(args));
} }
}) }),
); );
break;
} }
if (type & GestureTypes.longPress) { case GestureTypes.longPress: {
nativeView.addGestureRecognizer( nativeView.addGestureRecognizer(
this._createRecognizer(GestureTypes.longPress, (args) => { this._createRecognizer(GestureTypes.longPress, (args) => {
if (args.view) { if (args.view) {
this._executeCallback(_getLongPressData(args)); this._executeCallback(_getLongPressData(args));
} }
}) }),
); );
break;
} }
if (type & GestureTypes.touch) { case GestureTypes.touch: {
nativeView.addGestureRecognizer(this._createRecognizer(GestureTypes.touch)); nativeView.addGestureRecognizer(this._createRecognizer(GestureTypes.touch));
break;
} }
} }
} }
private _detach() { private _detach() {
if (this.target && this.target.nativeViewProtected) { for (const type in this._recognizers) {
for (const name in this._recognizers) { const item = this._recognizers[type];
if (this._recognizers.hasOwnProperty(name)) { this.target?.nativeViewProtected?.removeGestureRecognizer(item.recognizer);
const item = <RecognizerCache>this._recognizers[name];
this.target.nativeViewProtected.removeGestureRecognizer(item.recognizer);
item.recognizer = null; item.recognizer = null;
item.target = null; item.target = null;
} delete this._recognizers[type];
}
this._recognizers = {};
} }
} }
@ -271,6 +290,7 @@ export class GesturesObserver extends GesturesObserverBase {
this._onTargetLoaded = null; this._onTargetLoaded = null;
this._onTargetUnloaded = null; this._onTargetUnloaded = null;
} }
// clears target, context and callback references // clears target, context and callback references
super.disconnect(); super.disconnect();
} }
@ -281,9 +301,14 @@ export class GesturesObserver extends GesturesObserverBase {
} }
} }
private _createRecognizer(type: GestureTypes, callback?: (args: GestureEventData) => void, swipeDirection?: UISwipeGestureRecognizerDirection): UIGestureRecognizer { /**
let recognizer: UIGestureRecognizer; * Creates a UIGestureRecognizer (and populates a RecognizerCache entry in
let name = toString(type); * this._recognizers) corresponding to the singular GestureTypes value passed
* in.
*/
private _createRecognizer(type: GestureTypes, callback?: (args: GestureEventData) => void, swipeDirection?: UISwipeGestureRecognizerDirection): UIGestureRecognizer | undefined {
let recognizer: UIGestureRecognizer | undefined;
let typeString = toString(type);
const target = _createUIGestureRecognizerTarget(this, type, callback, this.context); const target = _createUIGestureRecognizerTarget(this, type, callback, this.context);
const recognizerType = _getUIGestureRecognizerType(type); const recognizerType = _getUIGestureRecognizerType(type);
@ -291,7 +316,8 @@ export class GesturesObserver extends GesturesObserverBase {
recognizer = recognizerType.alloc().initWithTargetAction(target, 'recognize'); recognizer = recognizerType.alloc().initWithTargetAction(target, 'recognize');
if (type === GestureTypes.swipe && swipeDirection) { if (type === GestureTypes.swipe && swipeDirection) {
name = name + swipeDirection.toString(); // e.g. "swipe1"
typeString += swipeDirection.toString();
(<UISwipeGestureRecognizer>recognizer).direction = swipeDirection; (<UISwipeGestureRecognizer>recognizer).direction = swipeDirection;
} else if (type === GestureTypes.touch) { } else if (type === GestureTypes.touch) {
(<TouchGestureRecognizer>recognizer).observer = this; (<TouchGestureRecognizer>recognizer).observer = this;
@ -301,16 +327,16 @@ export class GesturesObserver extends GesturesObserverBase {
if (recognizer) { if (recognizer) {
recognizer.delegate = recognizerDelegateInstance; recognizer.delegate = recognizerDelegateInstance;
this._recognizers[name] = <RecognizerCache>{ this._recognizers[typeString] = {
recognizer: recognizer, recognizer,
target: target, target,
}; };
} }
this.target.notify({ this.target.notify({
eventName: GestureEvents.gestureAttached, eventName: GestureEvents.gestureAttached,
object: this.target, object: this.target,
type, type: type,
view: this.target, view: this.target,
ios: recognizer, ios: recognizer,
}); });
@ -329,28 +355,27 @@ interface RecognizerCache {
target: any; target: any;
} }
function _getUIGestureRecognizerType(type: GestureTypes): any { function _getUIGestureRecognizerType(type: GestureTypes): typeof UIGestureRecognizer | null {
let nativeType = null; switch (type) {
case GestureTypes.tap:
if (type === GestureTypes.tap) { return UITapGestureRecognizer;
nativeType = UITapGestureRecognizer; case GestureTypes.doubleTap:
} else if (type === GestureTypes.doubleTap) { return UITapGestureRecognizer;
nativeType = UITapGestureRecognizer; case GestureTypes.pinch:
} else if (type === GestureTypes.pinch) { return UIPinchGestureRecognizer;
nativeType = UIPinchGestureRecognizer; case GestureTypes.pan:
} else if (type === GestureTypes.pan) { return UIPanGestureRecognizer;
nativeType = UIPanGestureRecognizer; case GestureTypes.swipe:
} else if (type === GestureTypes.swipe) { return UISwipeGestureRecognizer;
nativeType = UISwipeGestureRecognizer; case GestureTypes.rotation:
} else if (type === GestureTypes.rotation) { return UIRotationGestureRecognizer;
nativeType = UIRotationGestureRecognizer; case GestureTypes.longPress:
} else if (type === GestureTypes.longPress) { return UILongPressGestureRecognizer;
nativeType = UILongPressGestureRecognizer; case GestureTypes.touch:
} else if (type === GestureTypes.touch) { return TouchGestureRecognizer;
nativeType = TouchGestureRecognizer; default:
return null;
} }
return nativeType;
} }
function getState(recognizer: UIGestureRecognizer) { function getState(recognizer: UIGestureRecognizer) {