import { now, pointerCoord } from './helpers'; export function startTapClick(doc: Document) { let lastTouch = -MOUSE_WAIT * 10; let lastActivated = 0; let cancelled = false; let activatableEle: HTMLElement | undefined; let activeDefer: any; const clearDefers = new WeakMap(); function onBodyClick(ev: Event) { if (cancelled) { ev.preventDefault(); ev.stopPropagation(); } } // Touch Events function onTouchStart(ev: TouchEvent) { lastTouch = now(ev); pointerDown(ev); } function onTouchEnd(ev: TouchEvent) { lastTouch = now(ev); pointerUp(ev); } function onMouseDown(ev: MouseEvent) { const t = now(ev) - MOUSE_WAIT; if (lastTouch < t) { pointerDown(ev); } } function onMouseUp(ev: MouseEvent) { const t = now(ev) - MOUSE_WAIT; if (lastTouch < t) { pointerUp(ev); } } function cancelActive() { clearTimeout(activeDefer); if (activatableEle) { removeActivated(false); activatableEle = undefined; } cancelled = true; } function pointerDown(ev: any) { if (activatableEle) { return; } cancelled = false; setActivatedElement(getActivatableTarget(ev), ev); } function pointerUp(ev: UIEvent) { setActivatedElement(undefined, ev); if (cancelled && ev.cancelable) { ev.preventDefault(); } } function setActivatedElement(el: HTMLElement | undefined, ev: UIEvent) { // do nothing if (el && el === activatableEle) { return; } clearTimeout(activeDefer); activeDefer = undefined; const { x, y } = pointerCoord(ev); // unactivate selected if (activatableEle) { if (clearDefers.has(activatableEle)) { throw new Error('internal error'); } if (!activatableEle.classList.contains(ACTIVATED)) { addActivated(activatableEle, x, y); } removeActivated(true); } // activate if (el) { const deferId = clearDefers.get(el); if (deferId) { clearTimeout(deferId); clearDefers.delete(el); } el.classList.remove(ACTIVATED); activeDefer = setTimeout(() => { addActivated(el, x, y); activeDefer = undefined; }, ADD_ACTIVATED_DEFERS); } activatableEle = el; } function addActivated(el: HTMLElement, x: number, y: number) { lastActivated = Date.now(); el.classList.add(ACTIVATED); const rippleEffect = getRippleEffect(el); if (rippleEffect && rippleEffect.addRipple) { rippleEffect.addRipple(x, y); } } function removeActivated(smooth: boolean) { const active = activatableEle; if (!active) { return; } const time = CLEAR_STATE_DEFERS - Date.now() + lastActivated; if (smooth && time > 0) { const deferId = setTimeout(() => { active.classList.remove(ACTIVATED); clearDefers.delete(active); }, CLEAR_STATE_DEFERS); clearDefers.set(active, deferId); } else { active.classList.remove(ACTIVATED); } } doc.body.addEventListener('click', onBodyClick, true); doc.body.addEventListener('ionScrollStart', cancelActive); doc.body.addEventListener('ionGestureCaptured', cancelActive); doc.addEventListener('touchstart', onTouchStart, true); doc.addEventListener('touchcancel', onTouchEnd, true); doc.addEventListener('touchend', onTouchEnd, true); doc.addEventListener('mousedown', onMouseDown, true); doc.addEventListener('mouseup', onMouseUp, true); } function getActivatableTarget(ev: any): any { if (ev.composedPath) { const path = ev.composedPath() as HTMLElement[]; for (let i = 0; i < path.length - 2; i++) { const el = path[i]; if (el.hasAttribute && el.hasAttribute('ion-activable')) { return el; } } } else { return ev.target.closest('[ion-activable]'); } } function getRippleEffect(el: HTMLElement) { if (el.shadowRoot) { const ripple = el.shadowRoot.querySelector('ion-ripple-effect'); if (ripple) { return ripple; } } return el.querySelector('ion-ripple-effect'); } const ACTIVATED = 'activated'; const ADD_ACTIVATED_DEFERS = 200; const CLEAR_STATE_DEFERS = 200; const MOUSE_WAIT = 2500;