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.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 () {

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.
* 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).
* @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;
return;
}
list.push({
@ -209,18 +206,17 @@ export class Observable {
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,10 +224,9 @@ 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;
return;
}
Observable.innerRemoveEventListener(entries, callback, thisArg);
@ -241,7 +236,6 @@ export class Observable {
delete this._observers[eventName];
}
}
}
/**
* Please avoid using the static event-handling APIs as they will be removed
@ -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,10 +304,9 @@ 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;
return;
}
Observable.innerRemoveEventListener(entries, callback, thisArg);
@ -329,19 +322,18 @@ export class Observable {
delete _globalEventHandlers[eventClass];
}
}
}
/**
* Please avoid using the static event-handling APIs as they will be removed
* 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,7 +345,6 @@ export class Observable {
_globalEventHandlers[eventClass] = {};
}
for (const eventName of eventNames.trim().split(eventNamesRegex)) {
if (!_globalEventHandlers[eventClass][eventName]) {
_globalEventHandlers[eventClass][eventName] = [];
}
@ -364,7 +355,6 @@ export class Observable {
_globalEventHandlers[eventClass][eventName].push({ callback, thisArg, once });
}
}
private _globalNotify<T extends EventData>(eventClass: string, eventType: string, data: T): void {
// Check for the Global handlers for JUST this class
@ -464,11 +454,9 @@ export class Observable {
};
}
public _emit(eventNames: string): void {
for (const eventName of eventNames.trim().split(eventNamesRegex)) {
public _emit(eventName: string): void {
this.notify({ eventName, object: this });
}
}
private _getEventList(eventName: string, createIfNeeded?: boolean): Array<ListenerEntry> | undefined {
if (!eventName) {

View File

@ -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;

View File

@ -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;
}

View File

@ -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]>();
export function toString(type: GestureTypes): (typeof GestureTypes)[GestureTypes] | undefined {
switch (type) {
case GestureTypes.tap:
return GestureTypes[GestureTypes.tap];
if (type & GestureTypes.tap) {
types.push(GestureTypes[GestureTypes.tap]);
case GestureTypes.doubleTap:
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).
* @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;

View File

@ -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,12 +252,16 @@ export class GesturesObserver extends GesturesObserverBase {
private _onTargetUnloaded: (data: EventData) => void;
public observe(type: GestureTypes) {
if (this.target) {
this.type = type;
this._onTargetLoaded = (args) => {
if (!this.target) {
return;
}
this._onTargetLoaded = () => {
this._attach(this.target, type);
};
this._onTargetUnloaded = (args) => {
this._onTargetUnloaded = () => {
this._detach();
};
@ -268,7 +272,6 @@ export class GesturesObserver extends GesturesObserverBase {
this._attach(this.target, type);
}
}
}
public disconnect() {
this._detach();
@ -280,6 +283,7 @@ export class GesturesObserver extends GesturesObserverBase {
this._onTargetLoaded = null;
this._onTargetUnloaded = null;
}
// clears target, context and callback references
super.disconnect();
}
@ -297,43 +301,57 @@ export class GesturesObserver extends GesturesObserverBase {
private _attach(target: View, type: GestureTypes) {
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();
recognizer = this._simpleGestureDetector = <any>new androidx.core.view.GestureDetectorCompat(target._context, new TapAndDoubleTapGestureListener(this, this.target, type));
break;
}
if (type & GestureTypes.pinch) {
case GestureTypes.pinch: {
initializePinchGestureListener();
recognizer = this._scaleGestureDetector = new android.view.ScaleGestureDetector(target._context, new PinchGestureListener(this, this.target));
break;
}
if (type & GestureTypes.swipe) {
case GestureTypes.swipe: {
initializeSwipeGestureListener();
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);
break;
}
if (type & GestureTypes.rotation) {
case GestureTypes.rotation: {
recognizer = this._rotateGestureDetector = new CustomRotateGestureDetector(this, this.target);
break;
}
if (type & GestureTypes.touch) {
case GestureTypes.touch: {
this._notifyTouch = true;
} else {
// For touch events, return early rather than breaking from the switch
// statement.
return;
}
}
this.target.notify({
eventName: GestureEvents.gestureAttached,
object: this.target,
type,
type: type,
view: this.target,
android: recognizer,
});
}
}
public androidOnTouchEvent(motionEvent: android.view.MotionEvent) {
if (this._notifyTouch) {
@ -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;
}

View File

@ -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;

View File

@ -91,27 +91,32 @@ 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) => {
if (!this.target) {
return;
}
this._onTargetLoaded = () => {
this._attach(this.target, type);
};
this._onTargetUnloaded = (args) => {
this._onTargetUnloaded = () => {
this._detach();
};
@ -122,55 +127,69 @@ export class GesturesObserver extends GesturesObserverBase {
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 = <UIView>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 = <RecognizerCache>this._recognizers[name];
this.target.nativeViewProtected.removeGestureRecognizer(item.recognizer);
for (const type in this._recognizers) {
const item = this._recognizers[type];
this.target?.nativeViewProtected?.removeGestureRecognizer(item.recognizer);
item.recognizer = null;
item.target = null;
}
}
this._recognizers = {};
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();
(<UISwipeGestureRecognizer>recognizer).direction = swipeDirection;
} else if (type === GestureTypes.touch) {
(<TouchGestureRecognizer>recognizer).observer = this;
@ -301,16 +327,16 @@ export class GesturesObserver extends GesturesObserverBase {
if (recognizer) {
recognizer.delegate = recognizerDelegateInstance;
this._recognizers[name] = <RecognizerCache>{
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) {