Telegram Web K with changes to work inside I2P
https://web.telegram.i2p/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
502 lines
14 KiB
502 lines
14 KiB
/* |
|
* https://github.com/morethanwords/tweb |
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
|
*/ |
|
|
|
// * zoom part from WebZ |
|
// * https://github.com/Ajaxy/telegram-tt/blob/069f4f5b2f2c7c22529ccced876842e7f9cb81f4/src/util/captureEvents.ts |
|
|
|
import cancelEvent from '../helpers/dom/cancelEvent'; |
|
import IS_TOUCH_SUPPORTED from '../environment/touchSupport'; |
|
import safeAssign from '../helpers/object/safeAssign'; |
|
import contextMenuController from '../helpers/contextMenuController'; |
|
import {Middleware} from '../helpers/middleware'; |
|
import ListenerSetter, {Listener, ListenerOptions} from '../helpers/listenerSetter'; |
|
import {attachContextMenuListener} from '../helpers/dom/attachContextMenuListener'; |
|
import pause from '../helpers/schedulers/pause'; |
|
import deferredPromise from '../helpers/cancellablePromise'; |
|
import clamp from '../helpers/number/clamp'; |
|
import debounce from '../helpers/schedulers/debounce'; |
|
import {logger} from '../lib/logger'; |
|
import isSwipingBackSafari from '../helpers/dom/isSwipingBackSafari'; |
|
import windowSize from '../helpers/windowSize'; |
|
|
|
type E = { |
|
clientX: number, |
|
clientY: number, |
|
target: EventTarget, |
|
button?: number, |
|
type?: string |
|
}; |
|
|
|
type EE = E | (Exclude<E, 'clientX' | 'clientY'> & { |
|
touches: E[] |
|
}); |
|
|
|
const getEvent = (e: EE) => { |
|
return 'touches' in e ? e.touches[0] : e; |
|
}; |
|
|
|
function getDistance(a: Touch, b?: Touch) { |
|
if(!b) return 0; |
|
return Math.hypot((b.pageX - a.pageX), (b.pageY - a.pageY)); |
|
} |
|
|
|
function getTouchCenter(a: Touch, b: Touch) { |
|
return { |
|
x: (a.pageX + b.pageX) / 2, |
|
y: (a.pageY + b.pageY) / 2 |
|
}; |
|
} |
|
|
|
const attachGlobalListenerTo = document; |
|
|
|
let RESET_GLOBAL = false; |
|
contextMenuController.addEventListener('toggle', (visible) => { |
|
RESET_GLOBAL = visible; |
|
}); |
|
|
|
export type SwipeHandlerOptions = { |
|
element: SwipeHandler['element'], |
|
onSwipe: SwipeHandler['onSwipe'], |
|
verifyTouchTarget?: SwipeHandler['verifyTouchTarget'], |
|
onFirstSwipe?: SwipeHandler['onFirstSwipe'], |
|
onReset?: SwipeHandler['onReset'], |
|
onStart?: SwipeHandler['onStart'], |
|
onZoom?: SwipeHandler['onZoom'], |
|
onDrag?: SwipeHandler['onDrag'], |
|
onDoubleClick?: SwipeHandler['onDoubleClick'], |
|
cursor?: SwipeHandler['cursor'], |
|
cancelEvent?: SwipeHandler['cancelEvent'], |
|
listenerOptions?: SwipeHandler['listenerOptions'], |
|
setCursorTo?: HTMLElement, |
|
middleware?: Middleware, |
|
withDelay?: boolean, |
|
minZoom?: number, |
|
maxZoom?: number |
|
}; |
|
|
|
const TOUCH_MOVE_OPTIONS: ListenerOptions = {passive: false}; |
|
const MOUSE_MOVE_OPTIONS: ListenerOptions = false as any; |
|
const WHEEL_OPTIONS: ListenerOptions = {capture: true, passive: false}; |
|
|
|
export type ZoomDetails = { |
|
zoom?: number; |
|
zoomFactor?: number; |
|
zoomAdd?: number; |
|
initialCenterX: number; |
|
initialCenterY: number; |
|
dragOffsetX: number; |
|
dragOffsetY: number; |
|
currentCenterX: number; |
|
currentCenterY: number; |
|
}; |
|
|
|
export default class SwipeHandler { |
|
private element: HTMLElement; |
|
private onSwipe: (xDiff: number, yDiff: number, e: EE, cancelDrag?: (x: boolean, y: boolean) => void) => boolean | void; |
|
private verifyTouchTarget: (evt: EE) => boolean | Promise<boolean>; |
|
private onFirstSwipe: (e: EE) => void; |
|
private onReset: (e?: Event) => void; |
|
private onStart: () => void; |
|
private onZoom: (details: ZoomDetails) => void; |
|
private onDrag: (e: EE, captureEvent: E, details: {dragOffsetX: number, dragOffsetY: number}, cancelDrag: (x: boolean, y: boolean) => void) => void; |
|
private onDoubleClick: (details: {centerX: number, centerY: number}) => void; |
|
private cursor: 'grabbing' | 'move' | 'row-resize' | 'col-resize' | 'nesw-resize' | 'nwse-resize' | 'ne-resize' | 'se-resize' | 'sw-resize' | 'nw-resize' | 'n-resize' | 'e-resize' | 's-resize' | 'w-resize' | ''; |
|
private cancelEvent: boolean; |
|
private listenerOptions: ListenerOptions; |
|
private setCursorTo: HTMLElement; |
|
|
|
private isMouseDown: boolean; |
|
private tempId: number; |
|
|
|
private hadMove: boolean; |
|
private eventUp: E; |
|
private xDown: number; |
|
private yDown: number; |
|
private xAdded: number; |
|
private yAdded: number; |
|
|
|
private withDelay: boolean; |
|
private listenerSetter: ListenerSetter; |
|
|
|
private initialDistance: number; |
|
private initialTouchCenter: ReturnType<typeof getTouchCenter>; |
|
private initialDragOffset: {x: number, y: number}; |
|
private isDragCanceled: {x: boolean, y: boolean}; |
|
private wheelZoom: number; |
|
private releaseWheelDrag: ReturnType<typeof debounce<(e: Event) => void>>; |
|
private releaseWheelZoom: ReturnType<typeof debounce<(e: Event) => void>>; |
|
|
|
private log: ReturnType<typeof logger>; |
|
|
|
constructor(options: SwipeHandlerOptions) { |
|
safeAssign(this, options); |
|
|
|
this.log = logger('SWIPE-HANDLER'); |
|
this.cursor ??= 'grabbing'; |
|
this.cancelEvent ??= true; |
|
// this.listenerOptions ??= false as any; |
|
this.listenerOptions ??= TOUCH_MOVE_OPTIONS; |
|
|
|
this.setCursorTo ??= this.element; |
|
this.listenerSetter = new ListenerSetter(); |
|
this.setListeners(); |
|
|
|
this.resetValues(); |
|
this.tempId = 0; |
|
|
|
options.middleware?.onDestroy(() => { |
|
this.reset(); |
|
this.removeListeners(); |
|
}); |
|
|
|
this.releaseWheelDrag = debounce(this.reset, 150, false); |
|
this.releaseWheelZoom = debounce(this.reset, 150, false); |
|
} |
|
|
|
public setListeners() { |
|
if(!IS_TOUCH_SUPPORTED) { |
|
// @ts-ignore |
|
this.listenerSetter.add(this.element)('mousedown', this.handleStart, this.listenerOptions); |
|
this.listenerSetter.add(attachGlobalListenerTo)('mouseup', this.reset); |
|
|
|
if(this.onZoom || this.onDoubleClick) { |
|
this.listenerSetter.add(this.element)('wheel', this.handleWheel, WHEEL_OPTIONS); |
|
} |
|
} else { |
|
if(this.withDelay) { |
|
attachContextMenuListener({ |
|
element: this.element, |
|
callback: (e) => { |
|
cancelEvent(e); |
|
// @ts-ignore |
|
this.handleStart(e); |
|
}, |
|
listenerSetter: this.listenerSetter, |
|
listenerOptions: this.listenerOptions |
|
}); |
|
} else { |
|
// @ts-ignore |
|
this.listenerSetter.add(this.element)('touchstart', this.handleStart, this.listenerOptions); |
|
} |
|
|
|
if(this.onDoubleClick) { |
|
this.listenerSetter.add(this.element)('dblclick', (e) => { |
|
this.onDoubleClick({centerX: e.pageX, centerY: e.pageY}); |
|
}); |
|
} |
|
|
|
this.listenerSetter.add(attachGlobalListenerTo)('touchend', this.reset); |
|
} |
|
} |
|
|
|
public removeListeners() { |
|
this.log('remove listeners'); |
|
this.reset(); |
|
this.listenerSetter.removeAll(); |
|
} |
|
|
|
public setCursor(cursor: SwipeHandler['cursor'] = '') { |
|
this.cursor = cursor; |
|
|
|
if(!IS_TOUCH_SUPPORTED && this.hadMove) { |
|
this.setCursorTo.style.setProperty('cursor', this.cursor, 'important'); |
|
} |
|
} |
|
|
|
public add(x: number, y: number) { |
|
this.xAdded = x; |
|
this.yAdded = y; |
|
this.handleMove({ |
|
clientX: this.eventUp.clientX, |
|
clientY: this.eventUp.clientY, |
|
target: this.eventUp.target |
|
}); |
|
} |
|
|
|
protected resetValues() { |
|
++this.tempId; |
|
this.hadMove = false; |
|
this.xAdded = this.yAdded = 0; |
|
this.xDown = |
|
this.yDown = |
|
this.eventUp = |
|
this.isMouseDown = |
|
undefined; |
|
|
|
if(this.onZoom) { |
|
this.initialDistance = 0; |
|
this.initialTouchCenter = { |
|
x: windowSize.width / 2, |
|
y: windowSize.height / 2 |
|
}; |
|
this.initialDragOffset = {x: 0, y: 0}; |
|
this.isDragCanceled = {x: false, y: false}; |
|
this.wheelZoom = 1; |
|
} |
|
} |
|
|
|
public reset = (e?: Event) => { |
|
this.log('reset'); |
|
/* if(e) { |
|
cancelEvent(e); |
|
} */ |
|
|
|
if(IS_TOUCH_SUPPORTED) { |
|
this.listenerSetter.removeManual(attachGlobalListenerTo, 'touchmove', this.handleMove, TOUCH_MOVE_OPTIONS); |
|
} else { |
|
this.listenerSetter.removeManual(attachGlobalListenerTo, 'mousemove', this.handleMove, MOUSE_MOVE_OPTIONS); |
|
this.setCursorTo.style.cursor = ''; |
|
} |
|
|
|
if(this.hadMove) { |
|
this.onReset?.(e); |
|
} |
|
|
|
this.releaseWheelDrag?.clearTimeout(); |
|
this.releaseWheelZoom?.clearTimeout(); |
|
|
|
this.resetValues(); |
|
}; |
|
|
|
protected setHadMove(_e: EE) { |
|
if(!this.hadMove) { |
|
this.log('had move'); |
|
this.hadMove = true; |
|
this.setCursorTo.style.setProperty('cursor', this.cursor, 'important'); |
|
this.onFirstSwipe?.(_e); |
|
} |
|
} |
|
|
|
protected dispatchOnSwipe(...args: Parameters<SwipeHandlerOptions['onSwipe']>) { |
|
const onSwipeResult = this.onSwipe(...args); |
|
if(onSwipeResult !== undefined && onSwipeResult) { |
|
this.reset(); |
|
} |
|
} |
|
|
|
protected handleStart = async(_e: EE) => { |
|
this.log('start'); |
|
|
|
if(this.isMouseDown) { |
|
const touches = (_e as any as TouchEvent).touches; |
|
if(touches?.length === 2) { |
|
this.initialDistance = getDistance(touches[0], touches[1]); |
|
this.initialTouchCenter = getTouchCenter(touches[0], touches[1]); |
|
} |
|
|
|
return; |
|
} |
|
|
|
const e = getEvent(_e); |
|
if(![0, 1].includes(Math.max(0, e.button ?? 0))) { |
|
return; |
|
} |
|
|
|
if(e.button === 1) { |
|
cancelEvent(_e as any); |
|
} |
|
|
|
if(isSwipingBackSafari(_e as any)) { |
|
return; |
|
} |
|
|
|
const tempId = ++this.tempId; |
|
|
|
const verifyResult = this.verifyTouchTarget?.(_e); |
|
if(verifyResult !== undefined) { |
|
let result: any; |
|
if(verifyResult instanceof Promise) { |
|
// const tempId = this.tempId; |
|
result = await verifyResult; |
|
|
|
if(this.tempId !== tempId) { |
|
return; |
|
} |
|
} else { |
|
result = verifyResult; |
|
} |
|
|
|
if(!result) { |
|
return this.reset(); |
|
} |
|
} |
|
|
|
this.isMouseDown = true; |
|
|
|
if(this.withDelay && !IS_TOUCH_SUPPORTED) { |
|
const options = {...MOUSE_MOVE_OPTIONS, once: true}; |
|
const deferred = deferredPromise<void>(); |
|
const cb = () => deferred.resolve(); |
|
const listener = this.listenerSetter.add(attachGlobalListenerTo)('mousemove', cb, options) as any as Listener; |
|
|
|
await Promise.race([ |
|
pause(300), |
|
deferred |
|
]); |
|
|
|
deferred.resolve(); |
|
this.listenerSetter.remove(listener); |
|
|
|
if(this.tempId !== tempId) { |
|
return; |
|
} |
|
} |
|
|
|
this.xDown = e.clientX; |
|
this.yDown = e.clientY; |
|
this.eventUp = e; |
|
|
|
if(IS_TOUCH_SUPPORTED) { |
|
// @ts-ignore |
|
this.listenerSetter.add(attachGlobalListenerTo)('touchmove', this.handleMove, TOUCH_MOVE_OPTIONS); |
|
} else { |
|
// @ts-ignore |
|
this.listenerSetter.add(attachGlobalListenerTo)('mousemove', this.handleMove, MOUSE_MOVE_OPTIONS); |
|
} |
|
|
|
if(this.onStart) { |
|
this.onStart(); |
|
|
|
// have to initiate move instantly |
|
this.hadMove = true; |
|
this.handleMove(e); |
|
} |
|
}; |
|
|
|
protected handleMove = (_e: EE) => { |
|
if(this.xDown === undefined || this.yDown === undefined || RESET_GLOBAL) { |
|
this.reset(); |
|
return; |
|
} |
|
|
|
if(this.cancelEvent) { |
|
cancelEvent(_e as any); |
|
} |
|
|
|
if(this.releaseWheelDrag?.isDebounced() || this.releaseWheelZoom?.isDebounced()) { |
|
return; |
|
} |
|
|
|
this.log('move'); |
|
|
|
const e = this.eventUp = getEvent(_e); |
|
const xUp = e.clientX; |
|
const yUp = e.clientY; |
|
|
|
const xDiff = xUp - this.xDown + this.xAdded; |
|
const yDiff = yUp - this.yDown + this.yAdded; |
|
|
|
if(!this.hadMove) { |
|
if(!xDiff && !yDiff) { |
|
return; |
|
} |
|
|
|
this.setHadMove(_e); |
|
} |
|
|
|
const touches = (_e as any as TouchEvent).touches; |
|
if(this.onZoom && this.initialDistance > 0 && touches.length === 2) { |
|
const endDistance = getDistance(touches[0], touches[1]); |
|
const touchCenter = getTouchCenter(touches[0], touches[1]); |
|
const dragOffsetX = touchCenter.x - this.initialTouchCenter.x; |
|
const dragOffsetY = touchCenter.y - this.initialTouchCenter.y; |
|
const zoomFactor = endDistance / this.initialDistance; |
|
const details: ZoomDetails = { |
|
zoomFactor, |
|
initialCenterX: this.initialTouchCenter.x, |
|
initialCenterY: this.initialTouchCenter.y, |
|
dragOffsetX, |
|
dragOffsetY, |
|
currentCenterX: touchCenter.x, |
|
currentCenterY: touchCenter.y |
|
}; |
|
|
|
this.onZoom(details); |
|
} |
|
|
|
this.dispatchOnSwipe(xDiff, yDiff, _e); |
|
}; |
|
|
|
protected handleWheel = (e: WheelEvent) => { |
|
if(!this.hadMove && this.verifyTouchTarget) { |
|
const result = this.verifyTouchTarget(e); |
|
if(result !== undefined && !result) { |
|
this.reset(e); |
|
return; |
|
} |
|
} |
|
|
|
cancelEvent(e); |
|
|
|
this.log('wheel'); |
|
|
|
if(this.onDoubleClick && Object.is(e.deltaX, -0) && Object.is(e.deltaY, -0) && e.ctrlKey) { |
|
this.onWheelCapture(e); |
|
this.onDoubleClick({centerX: e.pageX, centerY: e.pageY}); |
|
this.reset(); |
|
return; |
|
} |
|
|
|
const metaKeyPressed = e.metaKey || e.ctrlKey || e.shiftKey; |
|
if(metaKeyPressed) { |
|
// * fix zooming while dragging is in inertia |
|
if(this.releaseWheelDrag?.isDebounced()) { |
|
this.reset(); |
|
} |
|
|
|
this.onWheelZoom(e); |
|
} else { |
|
this.handleWheelDrag(e); |
|
} |
|
}; |
|
|
|
protected handleWheelDrag = (e: WheelEvent) => { |
|
this.log('wheel drag'); |
|
|
|
this.onWheelCapture(e); |
|
// Ignore wheel inertia if drag is canceled in this direction |
|
if(!this.isDragCanceled.x || Math.sign(this.initialDragOffset.x) === Math.sign(e.deltaX)) { |
|
this.initialDragOffset.x -= e.deltaX; |
|
} |
|
if(!this.isDragCanceled.y || Math.sign(this.initialDragOffset.y) === Math.sign(e.deltaY)) { |
|
this.initialDragOffset.y -= e.deltaY; |
|
} |
|
const {x, y} = this.initialDragOffset; |
|
this.releaseWheelDrag(e); |
|
this.dispatchOnSwipe(x, y, e, (dx, dy) => { |
|
this.isDragCanceled = {x: dx, y: dy}; |
|
}); |
|
}; |
|
|
|
protected onWheelCapture = (e: WheelEvent) => { |
|
if(this.hadMove) return; |
|
this.log('wheel capture'); |
|
this.handleStart(e); |
|
this.setHadMove(e); |
|
this.initialTouchCenter = {x: e.x, y: e.y}; |
|
}; |
|
|
|
protected onWheelZoom = (e: WheelEvent) => { |
|
if(!this.onZoom) return; |
|
this.log('wheel zoom'); |
|
this.onWheelCapture(e); |
|
const dragOffsetX = e.x - this.initialTouchCenter.x; |
|
const dragOffsetY = e.y - this.initialTouchCenter.y; |
|
const delta = clamp(e.deltaY, -25, 25); |
|
this.wheelZoom -= delta * 0.01; |
|
const details: ZoomDetails = { |
|
zoomAdd: this.wheelZoom - 1, |
|
initialCenterX: this.initialTouchCenter.x, |
|
initialCenterY: this.initialTouchCenter.y, |
|
dragOffsetX, |
|
dragOffsetY, |
|
currentCenterX: e.x, |
|
currentCenterY: e.y |
|
}; |
|
this.onZoom(details); |
|
this.releaseWheelZoom(e); |
|
} |
|
}
|
|
|