type TapCallback = (event: { clientX: number, clientY: number }) => void type CallbackType = 'single' | 'double' export default class TapDetector { private singleTapCallbacks: TapCallback[] = [] private doubleTapCallbacks: TapCallback[] = [] private isTouchMode = false private lastTapTimestamp = 0 private tappedCount = 0 private touchMovedLength = 0 private lastPointerX = 0 private lastPointerY = 0 attach(dom: HTMLElement): void { if (!(dom instanceof Element)) { console.error('TapDetector.attach: arg must be an Element') return } dom.addEventListener('touchstart', this.onTouchStart) dom.addEventListener('touchmove', this.onTouchMove) dom.addEventListener('touchend', this.onTouchEnd) dom.addEventListener('mousedown', this.onMouseDown) dom.addEventListener('mouseup', this.onMouseUp) dom.addEventListener('mousemove', this.onMouseMove) } detach(dom: HTMLElement): void { dom.removeEventListener('touchstart', this.onTouchStart) dom.removeEventListener('touchmove', this.onTouchMove) dom.removeEventListener('touchend', this.onTouchEnd) dom.removeEventListener('mousedown', this.onMouseDown) dom.removeEventListener('mouseup', this.onMouseUp) dom.removeEventListener('mousemove', this.onMouseMove) } onSingleTap(callback: TapCallback): void { if (typeof callback === 'function' && !this.singleTapCallbacks.includes(callback)) { this.singleTapCallbacks.push(callback) } } onDoubleTap(callback: TapCallback): void { if (typeof callback === 'function' && !this.doubleTapCallbacks.includes(callback)) { this.doubleTapCallbacks.push(callback) } } private triggerCallbacks(callbackType: CallbackType, event: { clientX: number, clientY: number }): void { const callbacks = callbackType === 'single' ? this.singleTapCallbacks : this.doubleTapCallbacks callbacks.forEach((callback) => { callback(event) }) } private onTouchStart = (event: TouchEvent): void => { this.isTouchMode = true if (event.touches.length === 1) { this.onPointerDown(event.touches[0].clientX, event.touches[0].clientY) } } private onTouchMove = (event: TouchEvent): void => { if (event.touches.length === 1) { this.onPointerMove(event.touches[0].clientX, event.touches[0].clientY) } } private onTouchEnd = (): void => { if (this.isTouchMode) { this.onPointerUp() } } private onMouseDown = (event: MouseEvent): void => { if (!this.isTouchMode) { this.onPointerDown(event.clientX, event.clientY) } } private onMouseMove = (event: MouseEvent): void => { if (!this.isTouchMode && event.button === 0) { this.onPointerMove(event.clientX, event.clientY) } } private onMouseUp = (): void => { if (!this.isTouchMode) { this.onPointerUp() } } private onPointerDown(x: number, y: number): void { this.lastPointerX = x this.lastPointerY = y this.touchMovedLength = 0 } private onPointerUp(): void { const currTimeStamp = Date.now() if (this.touchMovedLength < 10) { if (currTimeStamp - this.lastTapTimestamp < 300) { this.tappedCount++ } else { this.tappedCount = 1 } this.lastTapTimestamp = currTimeStamp this.triggerCallbacks('single', { clientX: this.lastPointerX, clientY: this.lastPointerY }) if (this.tappedCount === 2) { this.triggerCallbacks('double', { clientX: this.lastPointerX, clientY: this.lastPointerY }) this.tappedCount = 0 } } this.touchMovedLength = 0 } private onPointerMove(x: number, y: number): void { const deltaX = this.lastPointerX - x const deltaY = this.lastPointerY - y this.touchMovedLength += Math.sqrt(deltaX * deltaX + deltaY * deltaY) this.lastPointerX = x this.lastPointerY = y } }