import Hammer from '@egjs/hammerjs' ; import { findNodeHandle } from 'react-native' ; import { State } from '../State' ; import { EventMap } from './constants' ; import * as NodeManager from './NodeManager' ; export type HammerInputExt = Omit < HammerInput , 'destroy' | 'handler' | 'init' > ; export type Config = Partial < { enabled : boolean ; minPointers : number ; maxPointers : number ; minDist : number ; minDistSq : number ; minVelocity : number ; minVelocitySq : number ; maxDist : number ; maxDistSq : number ; failOffsetXStart : number ; failOffsetYStart : number ; failOffsetXEnd : number ; failOffsetYEnd : number ; activeOffsetXStart : number ; activeOffsetXEnd : number ; activeOffsetYStart : number ; activeOffsetYEnd : number ; waitFor : any [ ] | null ; } > ; type NativeEvent = ReturnType < GestureHandler [ 'transformEventData' ] > ; let gestureInstances = 0 ; abstract class GestureHandler { public handlerTag : any ; public isGestureRunning = false ; public view : number | null = null ; protected hasCustomActivationCriteria : boolean ; protected hasGestureFailed = false ; protected hammer : HammerManager | null = null ; protected initialRotation : number | null = null ; protected __initialX : any ; protected __initialY : any ; protected config : Config = { } ; protected previousState : State = State . UNDETERMINED ; private pendingGestures : Record < string , this > = { } ; private oldState : State = State . UNDETERMINED ; private lastSentState : State | null = null ; private gestureInstance : number ; private _stillWaiting : any ; private propsRef : any ; private ref : any ; abstract get name ( ) : string ; get id ( ) { return `${ this . name }${ this . gestureInstance }` ; } get isDiscrete ( ) { return false ; } get shouldEnableGestureOnSetup ( ) : boolean { throw new Error ( 'Must override GestureHandler.shouldEnableGestureOnSetup' ) ; } constructor ( ) { this . gestureInstance = gestureInstances ++ ; this . hasCustomActivationCriteria = false ; } getConfig ( ) { return this . config ; } onWaitingEnded ( _gesture ) { } removePendingGesture ( id ) { delete this . pendingGestures [ id ] ; } addPendingGesture ( gesture ) { this . pendingGestures [ gesture . id ] = gesture ; } isGestureEnabledForEvent ( _config , _recognizer , _event ) { return { success : true } ; } get NativeGestureClass ( ) : RecognizerStatic { throw new Error ( 'Must override GestureHandler.NativeGestureClass' ) ; } updateHasCustomActivationCriteria ( _config ) { return true ; } clearSelfAsPending = ( ) => { if ( Array . isArray ( this . config . waitFor ) ) { for ( const gesture of this . config . waitFor ) { gesture . removePendingGesture ( this . id ) ; } } } ; updateGestureConfig ( { enabled = true , ... props } ) { this . clearSelfAsPending ( ) ; this . config = ensureConfig ( { enabled , ... props } ) ; this . hasCustomActivationCriteria = this . updateHasCustomActivationCriteria ( this . config ) ; if ( Array . isArray ( this . config . waitFor ) ) { for ( const gesture of this . config . waitFor ) { gesture . addPendingGesture ( this ) ; } } if ( this . hammer ) { this . sync ( ) ; } return this . config ; } destroy = ( ) => { this . clearSelfAsPending ( ) ; if ( this . hammer ) { this . hammer . stop ( false ) ; this . hammer . destroy ( ) ; } this . hammer = null ; } ; isPointInView = ( { x , y } ) => { const rect = this . view ! . getBoundingClientRect ( ) ; const pointerInside = x >= rect . left && x <= rect . right && y >= rect . top && y <= rect . bottom ; return pointerInside ; } ; getState ( type ) { if ( type == 0 ) { return 0 ; } return EventMap [ type ] ; } transformEventData ( event ) { const { eventType , maxPointers : numberOfPointers } = event ; const changedTouch = event . changedPointers [ 0 ] ; const pointerInside = this . isPointInView ( { x : changedTouch . clientX , y : changedTouch . clientY , } ) ; const state = this . getState ( eventType as 1 | 2 | 4 | 8 ) ; if ( state !== this . previousState ) { this . oldState = this . previousState ; this . previousState = state ; } return { nativeEvent : { numberOfPointers , state , pointerInside , ... this . transformNativeEvent ( event ) , handlerTag : this . handlerTag , target : this . ref , oldState : this . oldState , } , timeStamp : Date . now ( ) , } ; } transformNativeEvent ( _event ) { return { } ; } sendEvent = ( nativeEvent ) => { const { onGestureHandlerEvent , onGestureHandlerStateChange , } = this . propsRef . current ; const event = this . transformEventData ( nativeEvent ) ; invokeNullableMethod ( onGestureHandlerEvent , event ) ; if ( this . lastSentState !== event . nativeEvent . state ) { this . lastSentState = event . nativeEvent . state as State ; invokeNullableMethod ( onGestureHandlerStateChange , event ) ; } } ; cancelPendingGestures ( event ) { for ( const gesture of Object . values ( this . pendingGestures ) ) { if ( gesture && gesture . isGestureRunning ) { gesture . hasGestureFailed = true ; gesture . cancelEvent ( event ) ; } } } notifyPendingGestures ( ) { for ( const gesture of Object . values ( this . pendingGestures ) ) { if ( gesture ) { gesture . onWaitingEnded ( this ) ; } } } onGestureEnded ( event ) { this . isGestureRunning = false ; this . cancelPendingGestures ( event ) ; } forceInvalidate ( event ) { if ( this . isGestureRunning ) { this . hasGestureFailed = true ; this . cancelEvent ( event ) ; } } cancelEvent ( event ) { this . notifyPendingGestures ( ) ; this . sendEvent ( { ... event , eventType : Hammer . INPUT_CANCEL , isFinal : true , } ) ; this . onGestureEnded ( event ) ; } onRawEvent ( { isFirst } ) { if ( isFirst ) { this . hasGestureFailed = false ; } } setView ( ref , propsRef ) { if ( ref == null ) { this . destroy ( ) ; this . view = null ; return ; } this . propsRef = propsRef ; this . ref = ref ; this . view = findNodeHandle ( ref ) ; this . hammer = new Hammer . Manager ( this . view as any ) ; this . oldState = State . UNDETERMINED ; this . previousState = State . UNDETERMINED ; this . lastSentState = null ; const { NativeGestureClass } = this ; const gesture = new NativeGestureClass ( this . getHammerConfig ( ) ) ; this . hammer . add ( gesture ) ; this . hammer . on ( 'hammer.input' , ( ev ) => { if ( ! this . config . enabled ) { this . hasGestureFailed = false ; this . isGestureRunning = false ; return ; } this . onRawEvent ( ( ev as unknown ) as HammerInputExt ) ; if ( this . initialRotation === null && ev . rotation !== 0 ) { this . initialRotation = ev . rotation ; } if ( ev . isFinal ) { setTimeout ( ( ) => { this . initialRotation = null ; this . hasGestureFailed = false ; } ) ; } } ) ; this . setupEvents ( ) ; this . sync ( ) ; } setupEvents ( ) { if ( ! this . isDiscrete ) { this . hammer ! . on ( `${ this . name }start` , ( event ) => this . onStart ( ( event as unknown ) as HammerInputExt ) ) ; this . hammer ! . on ( `${ this . name }end ${ this . name }cancel` , ( event ) => { this . onGestureEnded ( ( event as unknown ) as HammerInputExt ) ; } ) ; } this . hammer ! . on ( this . name , ( ev ) => this . onGestureActivated ( ( ev as unknown ) as HammerInputExt ) ) ; } onStart ( { deltaX , deltaY , rotation } ) { this . oldState = State . UNDETERMINED ; this . previousState = State . UNDETERMINED ; this . lastSentState = null ; this . isGestureRunning = true ; this . __initialX = deltaX ; this . __initialY = deltaY ; this . initialRotation = rotation ; } onGestureActivated ( ev ) { this . sendEvent ( ev ) ; } onSuccess ( ) { } _getPendingGestures ( ) { if ( Array . isArray ( this . config . waitFor ) && this . config . waitFor . length ) { const stillWaiting = this . config . waitFor . filter ( ( { hasGestureFailed } ) => hasGestureFailed === false ) ; return stillWaiting ; } return [ ] ; } getHammerConfig ( ) { const pointers = this . config . minPointers === this . config . maxPointers ? this . config . minPointers : 0 ; return { pointers , } ; } sync = ( ) => { const gesture = this . hammer ! . get ( this . name ) ; if ( ! gesture ) return ; const enable = ( recognizer , inputData ) => { if ( ! this . config . enabled ) { this . isGestureRunning = false ; this . hasGestureFailed = false ; return false ; } if ( ! inputData || ! recognizer . options || typeof inputData . maxPointers === 'undefined' ) { return this . shouldEnableGestureOnSetup ; } if ( this . hasGestureFailed ) { return false ; } if ( ! this . isDiscrete ) { if ( this . isGestureRunning ) { return true ; } this . _stillWaiting = this . _getPendingGestures ( ) ; if ( this . _stillWaiting . length ) { for ( const gesture of this . _stillWaiting ) { if ( ! gesture . isDiscrete && gesture . isGestureRunning ) { this . hasGestureFailed = true ; this . isGestureRunning = false ; return false ; } } return false ; } } if ( ! this . hasCustomActivationCriteria ) { return true ; } const deltaRotation = this . initialRotation == null ? 0 : inputData . rotation - this . initialRotation ; const { success , failed } = this . isGestureEnabledForEvent ( this . getConfig ( ) , recognizer , { ... inputData , deltaRotation , } ) ; if ( failed ) { this . simulateCancelEvent ( inputData ) ; this . hasGestureFailed = true ; } return success ; } ; const params = this . getHammerConfig ( ) ; gesture . set ( { ... params , enable } ) ; } ; simulateCancelEvent ( _inputData ) { } } function invokeNullableMethod ( method , event ) { if ( method ) { if ( typeof method === 'function' ) { method ( event ) ; } else { if ( '__getHandler' in method && typeof method . __getHandler === 'function' ) { const handler = method . __getHandler ( ) ; invokeNullableMethod ( handler , event ) ; } else { if ( '__nodeConfig' in method ) { const { argMapping } = method . __nodeConfig ; if ( Array . isArray ( argMapping ) ) { for ( const [ index , [ key , value ] ] of argMapping . entries ( ) ) { if ( key in event . nativeEvent ) { const nativeValue = event . nativeEvent [ key ] ; if ( value && value . setValue ) { value . setValue ( nativeValue ) ; } else { method . __nodeConfig . argMapping [ index ] = [ key , nativeValue ] ; } } } } } } } } } function ensureConfig ( config ) { const props = { ... config } ; if ( 'minDist' in config ) { props . minDist = config . minDist ; props . minDistSq = props . minDist ! * props . minDist ! ; } if ( 'minVelocity' in config ) { props . minVelocity = config . minVelocity ; props . minVelocitySq = props . minVelocity ! * props . minVelocity ! ; } if ( 'maxDist' in config ) { props . maxDist = config . maxDist ; props . maxDistSq = config . maxDist ! * config . maxDist ! ; } if ( 'waitFor' in config ) { props . waitFor = asArray ( config . waitFor ) . map ( ( { handlerTag } ) => NodeManager . getHandler ( handlerTag ) ) . filter ( ( v ) => v ) ; } else { props . waitFor = null ; } const configProps = [ 'minPointers' , 'maxPointers' , 'minDist' , 'maxDist' , 'maxDistSq' , 'minVelocitySq' , 'minDistSq' , 'minVelocity' , 'failOffsetXStart' , 'failOffsetYStart' , 'failOffsetXEnd' , 'failOffsetYEnd' , 'activeOffsetXStart' , 'activeOffsetXEnd' , 'activeOffsetYStart' , 'activeOffsetYEnd' , ] as const ; configProps . forEach ( ( prop ) => { if ( typeof props [ prop ] === 'undefined' ) { props [ prop ] = Number . NaN ; } } ) ; return props as Required < Config > ; } function asArray < T > ( value ) { return value == null ? [ ] : Array . isArray ( value ) ? value : [ value ] ; } export default GestureHandler ;