import { Event } from "@clarity-types/data"; import { PointerState, Setting } from "@clarity-types/interaction"; import { bind } from "@src/core/event"; import { schedule } from "@src/core/task"; import { time } from "@src/core/time"; import { clearTimeout, setTimeout } from "@src/core/timeout"; import { iframe } from "@src/layout/dom"; import { offset } from "@src/layout/offset"; import { target } from "@src/layout/target"; import encode from "./encode"; export let state: PointerState[] = []; let timeout: number = null; let hasPrimaryTouch = false; let primaryTouchId = 0; const activeTouchPointIds = new Set(); export function start(): void { reset(); } export function observe(root: Node): void { bind(root, "mousedown", mouse.bind(this, Event.MouseDown, root), true); bind(root, "mouseup", mouse.bind(this, Event.MouseUp, root), true); bind(root, "mousemove", mouse.bind(this, Event.MouseMove, root), true); bind(root, "wheel", mouse.bind(this, Event.MouseWheel, root), true); bind(root, "dblclick", mouse.bind(this, Event.DoubleClick, root), true); bind(root, "touchstart", touch.bind(this, Event.TouchStart, root), true); bind(root, "touchend", touch.bind(this, Event.TouchEnd, root), true); bind(root, "touchmove", touch.bind(this, Event.TouchMove, root), true); bind(root, "touchcancel", touch.bind(this, Event.TouchCancel, root), true); } function mouse(event: Event, root: Node, evt: MouseEvent): void { let frame = iframe(root); let d = frame && frame.contentDocument ? frame.contentDocument.documentElement : document.documentElement; let x = "pageX" in evt ? Math.round(evt.pageX) : ("clientX" in evt ? Math.round(evt["clientX"] + d.scrollLeft) : null); let y = "pageY" in evt ? Math.round(evt.pageY) : ("clientY" in evt ? Math.round(evt["clientY"] + d.scrollTop) : null); // In case of iframe, we adjust (x,y) to be relative to top parent's origin if (frame) { let distance = offset(frame); x = x ? x + Math.round(distance.x) : x; y = y ? y + Math.round(distance.y) : y; } // Check for null values before processing this event if (x !== null && y !== null) { handler({ time: time(evt), event, data: { target: target(evt), x, y } }); } } function touch(event: Event, root: Node, evt: TouchEvent): void { let frame = iframe(root); let d = frame && frame.contentDocument ? frame.contentDocument.documentElement : document.documentElement; let touches = evt.changedTouches; let t = time(evt); if (touches) { for (let i = 0; i < touches.length; i++) { let entry = touches[i]; let x = "clientX" in entry ? Math.round(entry["clientX"] + d.scrollLeft) : null; let y = "clientY" in entry ? Math.round(entry["clientY"] + d.scrollTop) : null; x = x && frame ? x + Math.round(frame.offsetLeft) : x; y = y && frame ? y + Math.round(frame.offsetTop) : y; // We cannot rely on identifier to determine primary touch as its value doesn't always start with 0. // Safari/Webkit uses the address of the UITouch object as the identifier value for each touch point. const id = "identifier" in entry ? entry["identifier"] : undefined; switch(event) { case Event.TouchStart: if (activeTouchPointIds.size === 0) { // Track presence of primary touch separately to handle scenarios when same id is repeated hasPrimaryTouch = true; primaryTouchId = id; } activeTouchPointIds.add(id); break; case Event.TouchEnd: case Event.TouchCancel: activeTouchPointIds.delete(id); break; } const isPrimary = hasPrimaryTouch && primaryTouchId === id; // Check for null values before processing this event if (x !== null && y !== null) { handler({ time: t, event, data: { target: target(evt), x, y, id, isPrimary } }); } // Reset primary touch point id once touch event ends if (event === Event.TouchCancel || event === Event.TouchEnd) { if (primaryTouchId === id) { hasPrimaryTouch = false; } } } } } function handler(current: PointerState): void { switch (current.event) { case Event.MouseMove: case Event.MouseWheel: case Event.TouchMove: let length = state.length; let last = length > 1 ? state[length - 2] : null; if (last && similar(last, current)) { state.pop(); } state.push(current); clearTimeout(timeout); timeout = setTimeout(process, Setting.LookAhead, current.event); break; default: state.push(current); process(current.event); break; } } function process(event: Event): void { schedule(encode.bind(this, event)); } export function reset(): void { state = []; } function similar(last: PointerState, current: PointerState): boolean { let dx = last.data.x - current.data.x; let dy = last.data.y - current.data.y; let distance = Math.sqrt(dx * dx + dy * dy); let gap = current.time - last.time; let match = current.data.target === last.data.target; let sameId = current.data.id !== undefined ? current.data.id === last.data.id : true; return current.event === last.event && match && distance < Setting.Distance && gap < Setting.PointerInterval && sameId; } export function stop(): void { clearTimeout(timeout); // Send out any pending pointer events in the pipeline if (state.length > 0) { process(state[state.length - 1].event); } }