diff --git a/apps/automated/src/data/observable-tests.ts b/apps/automated/src/data/observable-tests.ts index 9bc36cb9e2..32efd1f956 100644 --- a/apps/automated/src/data/observable-tests.ts +++ b/apps/automated/src/data/observable-tests.ts @@ -163,7 +163,7 @@ export var test_Observable_addEventListener_MultipleEvents = function () { obj.addEventListener(events, callback); obj.set('testName', 1); 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 () { @@ -176,13 +176,14 @@ export var test_Observable_addEventListener_MultipleEvents_ShouldTrim = function var events = Observable.propertyChangeEvent + ' , ' + TESTED_NAME; obj.addEventListener(events, callback); - TKUnit.assert(obj.hasListeners(Observable.propertyChangeEvent), 'Observable.addEventListener for multiple events should trim each event name.'); - TKUnit.assert(obj.hasListeners(TESTED_NAME), '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(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.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 () { @@ -223,7 +224,7 @@ export var test_Observable_addEventListener_MultipleCallbacks_MultipleEvents = f obj.set('testName', 1); 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 () { @@ -341,19 +342,22 @@ export var test_Observable_removeEventListener_MultipleEvents_SingleCallback = f var events = Observable.propertyChangeEvent + ' , ' + TESTED_NAME; 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.test(); obj.removeEventListener(events, callback); - TKUnit.assert(!obj.hasListeners(Observable.propertyChangeEvent), 'Expected result for hasObservers is false'); - TKUnit.assert(!obj.hasListeners(TESTED_NAME), '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."); obj.set('testName', 2); 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 () { diff --git a/packages/core/data/observable/index.ts b/packages/core/data/observable/index.ts index 53d801e172..f1d2d4a605 100644 --- a/packages/core/data/observable/index.ts +++ b/packages/core/data/observable/index.ts @@ -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. * Please note that should you be using the `new Observable({})` constructor, it is **obsolete** since v3.0, @@ -153,74 +151,72 @@ export class Observable { /** * 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 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 { - this.addEventListener(eventNames, callback, thisArg); + public on(eventName: string, callback: (data: EventData) => void, thisArg?: any): void { + this.addEventListener(eventName, callback, thisArg); } /** * 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 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 { - this.addEventListener(event, callback, thisArg, true); + public once(eventName: string, callback: (data: EventData) => void, thisArg?: any): void { + this.addEventListener(eventName, callback, thisArg, true); } /** * Shortcut alias to the removeEventListener method. */ - public off(eventNames: string, callback?: (data: EventData) => void, thisArg?: any): void { - this.removeEventListener(eventNames, callback, thisArg); + public off(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void { + this.removeEventListener(eventName, callback, thisArg); } /** * 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 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; thisArg = thisArg || undefined; - if (typeof eventNames !== 'string') { - throw new TypeError('Event name(s) must be a string.'); + if (typeof eventName !== 'string') { + throw new TypeError('Event name must be a string.'); } if (typeof callback !== '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); - if (Observable._indexOfListener(list, callback, thisArg) !== -1) { - // Already added. - continue; - } - - list.push({ - callback, - thisArg, - once, - }); + const list = this._getEventList(eventName, true); + if (Observable._indexOfListener(list, callback, thisArg) !== -1) { + // Already added. + return; } + + list.push({ + callback, + thisArg, + once, + }); } /** * 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 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; - if (typeof eventNames !== 'string') { + if (typeof eventName !== 'string') { throw new TypeError('Events name(s) must be string.'); } @@ -228,18 +224,16 @@ export class Observable { throw new TypeError('callback must be function.'); } - for (const eventName of eventNames.trim().split(eventNamesRegex)) { - const entries = this._observers[eventName]; - if (!entries) { - continue; - } + const entries = this._observers[eventName]; + if (!entries) { + return; + } - Observable.innerRemoveEventListener(entries, callback, thisArg); + Observable.innerRemoveEventListener(entries, callback, thisArg); - if (!entries.length) { - // Clear all entries of this type - delete this._observers[eventName]; - } + if (!entries.length) { + // Clear all entries of this type + delete this._observers[eventName]; } } @@ -297,11 +291,11 @@ export class Observable { * in future. * @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; - if (typeof eventNames !== 'string') { - throw new TypeError('Event name(s) must be a string.'); + if (typeof eventName !== 'string') { + throw new TypeError('Event name must be a string.'); } if (callback && typeof callback !== 'function') { @@ -310,24 +304,22 @@ export class Observable { const eventClass = this.name === 'Observable' ? '*' : this.name; - for (const eventName of eventNames.trim().split(eventNamesRegex)) { - const entries = _globalEventHandlers?.[eventClass]?.[eventName]; - if (!entries) { - continue; - } + const entries = _globalEventHandlers?.[eventClass]?.[eventName]; + if (!entries) { + return; + } - Observable.innerRemoveEventListener(entries, callback, thisArg); + Observable.innerRemoveEventListener(entries, callback, thisArg); - if (!entries.length) { - // Clear all entries of this type - delete _globalEventHandlers[eventClass][eventName]; - } + if (!entries.length) { + // Clear all entries of this type + delete _globalEventHandlers[eventClass][eventName]; + } - // Clear the primary class grouping if no list are left - const keys = Object.keys(_globalEventHandlers[eventClass]); - if (keys.length === 0) { - delete _globalEventHandlers[eventClass]; - } + // Clear the primary class grouping if no list are left + const keys = Object.keys(_globalEventHandlers[eventClass]); + if (keys.length === 0) { + delete _globalEventHandlers[eventClass]; } } @@ -336,12 +328,12 @@ export class Observable { * in future. * @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; thisArg = thisArg || undefined; - if (typeof eventNames !== 'string') { - throw new TypeError('Event name(s) must be a string.'); + if (typeof eventName !== 'string') { + throw new TypeError('Event name must be a string.'); } if (typeof callback !== 'function') { @@ -353,17 +345,15 @@ export class Observable { _globalEventHandlers[eventClass] = {}; } - for (const eventName of eventNames.trim().split(eventNamesRegex)) { - if (!_globalEventHandlers[eventClass][eventName]) { - _globalEventHandlers[eventClass][eventName] = []; - } - if (Observable._indexOfListener(_globalEventHandlers[eventClass][eventName], callback, thisArg) !== -1) { - // Already added. - return; - } - - _globalEventHandlers[eventClass][eventName].push({ callback, thisArg, once }); + if (!_globalEventHandlers[eventClass][eventName]) { + _globalEventHandlers[eventClass][eventName] = []; + } + if (Observable._indexOfListener(_globalEventHandlers[eventClass][eventName], callback, thisArg) !== -1) { + // Already added. + return; } + + _globalEventHandlers[eventClass][eventName].push({ callback, thisArg, once }); } private _globalNotify(eventClass: string, eventType: string, data: T): void { @@ -464,10 +454,8 @@ export class Observable { }; } - public _emit(eventNames: string): void { - for (const eventName of eventNames.trim().split(eventNamesRegex)) { - this.notify({ eventName, object: this }); - } + public _emit(eventName: string): void { + this.notify({ eventName, object: this }); } private _getEventList(eventName: string, createIfNeeded?: boolean): Array | undefined { diff --git a/packages/core/ui/core/bindable/index.ts b/packages/core/ui/core/bindable/index.ts index 5e13cb6112..d893e51a7a 100644 --- a/packages/core/ui/core/bindable/index.ts +++ b/packages/core/ui/core/bindable/index.ts @@ -4,6 +4,7 @@ import { ViewBase } from '../view-base'; // Requires import { unsetValue } from '../properties'; import { Observable, PropertyChangeData } from '../../../data/observable'; +import { fromString as gestureFromString } from '../../../ui/gestures/gestures-common'; import { addWeakEventListener, removeWeakEventListener } from '../weak-event-listener'; import { bindingConstants, parentsRegex } from '../../builder/binding-builder'; import { escapeRegexSymbols } from '../../../utils'; @@ -87,25 +88,45 @@ export interface ValueConverter { 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 { - 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 { + // 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(); + // 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'; } -// 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. export function isEventOrGesture(name: string, view: ViewBase): boolean { if (typeof name === 'string') { const eventOrGestureName = getEventOrGestureName(name); 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; diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts index 77b6079902..6b68d42cc9 100644 --- a/packages/core/ui/core/view/view-common.ts +++ b/packages/core/ui/core/view/view-common.ts @@ -305,11 +305,11 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { // Coerce "tap" -> GestureTypes.tap // 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 (gesture && !this._isEvent(normalizedName)) { - this._observe(gesture, callback, thisArg); + if (gestureType && !this._isEvent(normalizedName)) { + this._observe(gestureType, callback, thisArg); return; } @@ -324,11 +324,11 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { // Coerce "tap" -> GestureTypes.tap // 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 (gesture && !this._isEvent(normalizedName)) { - this._disconnectGestureObservers(gesture, callback, thisArg); + if (gestureType && !this._isEvent(normalizedName)) { + this._disconnectGestureObservers(gestureType, callback, thisArg); return; } diff --git a/packages/core/ui/gestures/gestures-common.ts b/packages/core/ui/gestures/gestures-common.ts index 2a42e782c1..177b0bc4be 100644 --- a/packages/core/ui/gestures/gestures-common.ts +++ b/packages/core/ui/gestures/gestures-common.ts @@ -280,59 +280,45 @@ export interface RotationGestureEventData extends GestureEventDataWithState { /** * Returns a string representation of a gesture type. - * @param type - Type of the gesture. - * @param separator(optional) - Text separator between gesture type strings. + * @param type - The singular type of the gesture. Looks for an exact match, so + * passing plural types like `GestureTypes.tap & GestureTypes.doubleTap` will + * simply return undefined. */ -export function toString(type: GestureTypes, separator?: string): string { - // We can get stronger typings with `keyof typeof GestureTypes`, but sadly - // indexing into an enum simply returns `string`, so we'd have to type-assert - // all of the below anyway. Even this `(typeof GestureTypes)[GestureTypes]` is - // more for documentation than for type-safety (it resolves to `string`, too). - const types = new Array<(typeof GestureTypes)[GestureTypes]>(); - - if (type & GestureTypes.tap) { - types.push(GestureTypes[GestureTypes.tap]); - } +export function toString(type: GestureTypes): (typeof GestureTypes)[GestureTypes] | undefined { + switch (type) { + case GestureTypes.tap: + return GestureTypes[GestureTypes.tap]; - if (type & GestureTypes.doubleTap) { - types.push(GestureTypes[GestureTypes.doubleTap]); - } + case GestureTypes.doubleTap: + return GestureTypes[GestureTypes.doubleTap]; - if (type & GestureTypes.pinch) { - types.push(GestureTypes[GestureTypes.pinch]); - } + case GestureTypes.pinch: + return GestureTypes[GestureTypes.pinch]; - if (type & GestureTypes.pan) { - types.push(GestureTypes[GestureTypes.pan]); - } + case GestureTypes.pan: + return GestureTypes[GestureTypes.pan]; - if (type & GestureTypes.swipe) { - types.push(GestureTypes[GestureTypes.swipe]); - } + case GestureTypes.swipe: + return GestureTypes[GestureTypes.swipe]; - if (type & GestureTypes.rotation) { - types.push(GestureTypes[GestureTypes.rotation]); - } + case GestureTypes.rotation: + return GestureTypes[GestureTypes.rotation]; - if (type & GestureTypes.longPress) { - types.push(GestureTypes[GestureTypes.longPress]); - } + case GestureTypes.longPress: + return GestureTypes[GestureTypes.longPress]; - if (type & GestureTypes.touch) { - types.push(GestureTypes[GestureTypes.touch]); + case GestureTypes.touch: + return 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). - * @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 { - return GestureTypes[type.trim()]; +export function fromString(type: (typeof GestureTypes)[GestureTypes]): GestureTypes | undefined { + return GestureTypes[type]; } export abstract class GesturesObserverBase implements GesturesObserverDefinition { @@ -340,7 +326,8 @@ export abstract class GesturesObserverBase implements GesturesObserverDefinition private _target: View; private _context?: any; - public type: GestureTypes; + /** This is populated on the first call to observe(). */ + type: GestureTypes; public get callback(): (args: GestureEventData) => void { return this._callback; diff --git a/packages/core/ui/gestures/index.android.ts b/packages/core/ui/gestures/index.android.ts index 3d3e7e3e29..aef28d4218 100644 --- a/packages/core/ui/gestures/index.android.ts +++ b/packages/core/ui/gestures/index.android.ts @@ -27,14 +27,14 @@ function initializeTapAndDoubleTapGestureListener() { class TapAndDoubleTapGestureListenerImpl extends android.view.GestureDetector.SimpleOnGestureListener { private _observer: GesturesObserver; private _target: View; - private _type: number; + private _type: GestureTypes; private _lastUpTime = 0; private _tapTimeoutId: number; private static DoubleTapTimeout = android.view.ViewConfiguration.getDoubleTapTimeout(); - constructor(observer: GesturesObserver, target: View, type: number) { + constructor(observer: GesturesObserver, target: View, type: GestureTypes) { super(); this._observer = observer; @@ -61,7 +61,7 @@ function initializeTapAndDoubleTapGestureListener() { } 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); _executeCallback(this._observer, args); } @@ -70,14 +70,14 @@ function initializeTapAndDoubleTapGestureListener() { private _handleSingleTap(motionEvent: android.view.MotionEvent): void { if (this._target.getGestureObservers(GestureTypes.doubleTap)) { this._tapTimeoutId = timer.setTimeout(() => { - if (this._type & GestureTypes.tap) { + if (this._type === GestureTypes.tap) { const args = _getTapArgs(GestureTypes.tap, this._target, motionEvent); _executeCallback(this._observer, args); } timer.clearTimeout(this._tapTimeoutId); }, TapAndDoubleTapGestureListenerImpl.DoubleTapTimeout); } else { - if (this._type & GestureTypes.tap) { + if (this._type === GestureTypes.tap) { const args = _getTapArgs(GestureTypes.tap, this._target, motionEvent); _executeCallback(this._observer, args); } @@ -88,7 +88,7 @@ function initializeTapAndDoubleTapGestureListener() { if (this._tapTimeoutId) { timer.clearTimeout(this._tapTimeoutId); } - if (this._type & GestureTypes.doubleTap) { + if (this._type === GestureTypes.doubleTap) { const args = _getTapArgs(GestureTypes.doubleTap, this._target, motionEvent); _executeCallback(this._observer, args); } @@ -252,21 +252,24 @@ export class GesturesObserver extends GesturesObserverBase { private _onTargetUnloaded: (data: EventData) => void; public observe(type: GestureTypes) { - if (this.target) { - this.type = type; - this._onTargetLoaded = (args) => { - this._attach(this.target, type); - }; - this._onTargetUnloaded = (args) => { - this._detach(); - }; + this.type = type; + + if (!this.target) { + return; + } - this.target.on('loaded', this._onTargetLoaded); - this.target.on('unloaded', this._onTargetUnloaded); + this._onTargetLoaded = () => { + this._attach(this.target, type); + }; + this._onTargetUnloaded = () => { + this._detach(); + }; - if (this.target.isLoaded) { - this._attach(this.target, type); - } + this.target.on('loaded', this._onTargetLoaded); + this.target.on('unloaded', this._onTargetUnloaded); + + if (this.target.isLoaded) { + this._attach(this.target, type); } } @@ -280,6 +283,7 @@ export class GesturesObserver extends GesturesObserverBase { this._onTargetLoaded = null; this._onTargetUnloaded = null; } + // clears target, context and callback references super.disconnect(); } @@ -297,42 +301,56 @@ export class GesturesObserver extends GesturesObserverBase { private _attach(target: View, type: GestureTypes) { this._detach(); - let recognizer; - - if (type & GestureTypes.tap || type & GestureTypes.doubleTap || type & GestureTypes.longPress) { - initializeTapAndDoubleTapGestureListener(); - recognizer = this._simpleGestureDetector = new androidx.core.view.GestureDetectorCompat(target._context, new TapAndDoubleTapGestureListener(this, this.target, type)); - } + let recognizer: unknown; + + 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(); + recognizer = this._simpleGestureDetector = new androidx.core.view.GestureDetectorCompat(target._context, new TapAndDoubleTapGestureListener(this, this.target, type)); + break; + } + case GestureTypes.pinch: { + initializePinchGestureListener(); + recognizer = this._scaleGestureDetector = new android.view.ScaleGestureDetector(target._context, new PinchGestureListener(this, this.target)); + break; + } - if (type & GestureTypes.pinch) { - initializePinchGestureListener(); - recognizer = this._scaleGestureDetector = new android.view.ScaleGestureDetector(target._context, new PinchGestureListener(this, this.target)); - } + case GestureTypes.swipe: { + initializeSwipeGestureListener(); + recognizer = this._swipeGestureDetector = new androidx.core.view.GestureDetectorCompat(target._context, new SwipeGestureListener(this, this.target)); + break; + } - if (type & GestureTypes.swipe) { - initializeSwipeGestureListener(); - recognizer = this._swipeGestureDetector = new androidx.core.view.GestureDetectorCompat(target._context, new SwipeGestureListener(this, this.target)); - } + case GestureTypes.pan: { + recognizer = this._panGestureDetector = new CustomPanGestureDetector(this, this.target); + break; + } - if (type & GestureTypes.pan) { - recognizer = this._panGestureDetector = new CustomPanGestureDetector(this, this.target); - } + case GestureTypes.rotation: { + recognizer = this._rotateGestureDetector = new CustomRotateGestureDetector(this, this.target); + break; + } - if (type & GestureTypes.rotation) { - recognizer = this._rotateGestureDetector = new CustomRotateGestureDetector(this, this.target); + case GestureTypes.touch: { + this._notifyTouch = true; + // For touch events, return early rather than breaking from the switch + // statement. + return; + } } - if (type & GestureTypes.touch) { - this._notifyTouch = true; - } else { - this.target.notify({ - eventName: GestureEvents.gestureAttached, - object: this.target, - type, - view: this.target, - android: recognizer, - }); - } + this.target.notify({ + eventName: GestureEvents.gestureAttached, + object: this.target, + type: type, + view: this.target, + android: recognizer, + }); } public androidOnTouchEvent(motionEvent: android.view.MotionEvent) { @@ -430,7 +448,13 @@ class PinchGestureEventData implements PinchGestureEventData { public eventName = toString(GestureTypes.pinch); 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 { return this.android.getFocusX() / layout.getDisplayDensity(); @@ -669,7 +693,10 @@ class Pointer implements Pointer { public android: number; public ios: any = undefined; - constructor(id: number, private event: android.view.MotionEvent) { + constructor( + id: number, + private event: android.view.MotionEvent, + ) { this.android = id; } diff --git a/packages/core/ui/gestures/index.d.ts b/packages/core/ui/gestures/index.d.ts index 077473139f..81e13e7ae4 100644 --- a/packages/core/ui/gestures/index.d.ts +++ b/packages/core/ui/gestures/index.d.ts @@ -27,7 +27,9 @@ export class GesturesObserver { 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; diff --git a/packages/core/ui/gestures/index.ios.ts b/packages/core/ui/gestures/index.ios.ts index 82b2aa804e..5a401cd0b6 100644 --- a/packages/core/ui/gestures/index.ios.ts +++ b/packages/core/ui/gestures/index.ios.ts @@ -91,86 +91,105 @@ class UIGestureRecognizerImpl extends NSObject { } export class GesturesObserver extends GesturesObserverBase { - private _recognizers: {}; + private readonly _recognizers: { [type: string]: RecognizerCache } = {}; private _onTargetLoaded: (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 { // } + /** + * 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) { - if (this.target) { - this.type = type; - this._onTargetLoaded = (args) => { - this._attach(this.target, type); - }; - this._onTargetUnloaded = (args) => { - this._detach(); - }; - - this.target.on('loaded', this._onTargetLoaded); - this.target.on('unloaded', this._onTargetUnloaded); - - if (this.target.isLoaded) { - this._attach(this.target, type); - } + this.type = type; + + if (!this.target) { + return; + } + + this._onTargetLoaded = () => { + this._attach(this.target, type); + }; + this._onTargetUnloaded = () => { + this._detach(); + }; + + this.target.on('loaded', this._onTargetLoaded); + this.target.on('unloaded', this._onTargetUnloaded); + + if (this.target.isLoaded) { + 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) { this._detach(); - if (target && target.nativeViewProtected && target.nativeViewProtected.addGestureRecognizer) { - const nativeView = target.nativeViewProtected; + const nativeView = target?.nativeViewProtected as UIView | undefined; + if (!nativeView?.addGestureRecognizer) { + return; + } - if (type & GestureTypes.tap) { + switch (type) { + case GestureTypes.tap: { nativeView.addGestureRecognizer( this._createRecognizer(GestureTypes.tap, (args) => { if (args.view) { this._executeCallback(_getTapData(args)); } - }) + }), ); + break; } - if (type & GestureTypes.doubleTap) { + case GestureTypes.doubleTap: { nativeView.addGestureRecognizer( this._createRecognizer(GestureTypes.doubleTap, (args) => { if (args.view) { this._executeCallback(_getTapData(args)); } - }) + }), ); + break; } - if (type & GestureTypes.pinch) { + case GestureTypes.pinch: { nativeView.addGestureRecognizer( this._createRecognizer(GestureTypes.pinch, (args) => { if (args.view) { this._executeCallback(_getPinchData(args)); } - }) + }), ); + break; } - if (type & GestureTypes.pan) { + case GestureTypes.pan: { nativeView.addGestureRecognizer( this._createRecognizer(GestureTypes.pan, (args) => { if (args.view) { this._executeCallback(_getPanData(args, target.nativeViewProtected)); } - }) + }), ); + break; } - if (type & GestureTypes.swipe) { + case GestureTypes.swipe: { nativeView.addGestureRecognizer( this._createRecognizer( GestureTypes.swipe, @@ -179,8 +198,8 @@ export class GesturesObserver extends GesturesObserverBase { this._executeCallback(_getSwipeData(args)); } }, - UISwipeGestureRecognizerDirection.Down - ) + UISwipeGestureRecognizerDirection.Down, + ), ); nativeView.addGestureRecognizer( @@ -191,8 +210,8 @@ export class GesturesObserver extends GesturesObserverBase { this._executeCallback(_getSwipeData(args)); } }, - UISwipeGestureRecognizerDirection.Left - ) + UISwipeGestureRecognizerDirection.Left, + ), ); nativeView.addGestureRecognizer( @@ -203,8 +222,8 @@ export class GesturesObserver extends GesturesObserverBase { this._executeCallback(_getSwipeData(args)); } }, - UISwipeGestureRecognizerDirection.Right - ) + UISwipeGestureRecognizerDirection.Right, + ), ); nativeView.addGestureRecognizer( @@ -215,49 +234,49 @@ export class GesturesObserver extends GesturesObserverBase { this._executeCallback(_getSwipeData(args)); } }, - UISwipeGestureRecognizerDirection.Up - ) + UISwipeGestureRecognizerDirection.Up, + ), ); + break; } - if (type & GestureTypes.rotation) { + case GestureTypes.rotation: { nativeView.addGestureRecognizer( this._createRecognizer(GestureTypes.rotation, (args) => { if (args.view) { this._executeCallback(_getRotationData(args)); } - }) + }), ); + break; } - if (type & GestureTypes.longPress) { + case GestureTypes.longPress: { nativeView.addGestureRecognizer( this._createRecognizer(GestureTypes.longPress, (args) => { if (args.view) { this._executeCallback(_getLongPressData(args)); } - }) + }), ); + break; } - if (type & GestureTypes.touch) { + case GestureTypes.touch: { nativeView.addGestureRecognizer(this._createRecognizer(GestureTypes.touch)); + break; } } } private _detach() { - if (this.target && this.target.nativeViewProtected) { - for (const name in this._recognizers) { - if (this._recognizers.hasOwnProperty(name)) { - const item = this._recognizers[name]; - this.target.nativeViewProtected.removeGestureRecognizer(item.recognizer); - - item.recognizer = null; - item.target = null; - } - } - this._recognizers = {}; + for (const type in this._recognizers) { + const item = this._recognizers[type]; + this.target?.nativeViewProtected?.removeGestureRecognizer(item.recognizer); + + item.recognizer = null; + item.target = null; + delete this._recognizers[type]; } } @@ -271,6 +290,7 @@ export class GesturesObserver extends GesturesObserverBase { this._onTargetLoaded = null; this._onTargetUnloaded = null; } + // clears target, context and callback references 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; - let name = toString(type); + /** + * Creates a UIGestureRecognizer (and populates a RecognizerCache entry in + * 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 recognizerType = _getUIGestureRecognizerType(type); @@ -291,7 +316,8 @@ export class GesturesObserver extends GesturesObserverBase { recognizer = recognizerType.alloc().initWithTargetAction(target, 'recognize'); if (type === GestureTypes.swipe && swipeDirection) { - name = name + swipeDirection.toString(); + // e.g. "swipe1" + typeString += swipeDirection.toString(); (recognizer).direction = swipeDirection; } else if (type === GestureTypes.touch) { (recognizer).observer = this; @@ -301,16 +327,16 @@ export class GesturesObserver extends GesturesObserverBase { if (recognizer) { recognizer.delegate = recognizerDelegateInstance; - this._recognizers[name] = { - recognizer: recognizer, - target: target, + this._recognizers[typeString] = { + recognizer, + target, }; } this.target.notify({ eventName: GestureEvents.gestureAttached, object: this.target, - type, + type: type, view: this.target, ios: recognizer, }); @@ -329,28 +355,27 @@ interface RecognizerCache { target: any; } -function _getUIGestureRecognizerType(type: GestureTypes): any { - let nativeType = null; - - if (type === GestureTypes.tap) { - nativeType = UITapGestureRecognizer; - } else if (type === GestureTypes.doubleTap) { - nativeType = UITapGestureRecognizer; - } else if (type === GestureTypes.pinch) { - nativeType = UIPinchGestureRecognizer; - } else if (type === GestureTypes.pan) { - nativeType = UIPanGestureRecognizer; - } else if (type === GestureTypes.swipe) { - nativeType = UISwipeGestureRecognizer; - } else if (type === GestureTypes.rotation) { - nativeType = UIRotationGestureRecognizer; - } else if (type === GestureTypes.longPress) { - nativeType = UILongPressGestureRecognizer; - } else if (type === GestureTypes.touch) { - nativeType = TouchGestureRecognizer; +function _getUIGestureRecognizerType(type: GestureTypes): typeof UIGestureRecognizer | null { + switch (type) { + case GestureTypes.tap: + return UITapGestureRecognizer; + case GestureTypes.doubleTap: + return UITapGestureRecognizer; + case GestureTypes.pinch: + return UIPinchGestureRecognizer; + case GestureTypes.pan: + return UIPanGestureRecognizer; + case GestureTypes.swipe: + return UISwipeGestureRecognizer; + case GestureTypes.rotation: + return UIRotationGestureRecognizer; + case GestureTypes.longPress: + return UILongPressGestureRecognizer; + case GestureTypes.touch: + return TouchGestureRecognizer; + default: + return null; } - - return nativeType; } function getState(recognizer: UIGestureRecognizer) {