UNPKG

clarity-js

Version:

An analytics library that uses web page interactions to generate aggregated insights

131 lines (112 loc) 4.89 kB
import { Constant, Dimension, Event } from "@clarity-types/data"; import { type ScrollState, Setting } from "@clarity-types/interaction"; import { FunctionNames } from "@clarity-types/performance"; 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 throttle from "@src/core/throttle"; import * as dimension from "@src/data/dimension"; import { iframe } from "@src/layout/dom"; import { metadata, target } from "@src/layout/target"; import encode from "./encode"; export let state: ScrollState[] = []; let initialTop: Node = null; let initialBottom: Node = null; let timeout: number = null; export function start(): void { state = []; recompute(); } export function observe(root: Node): void { const frame = iframe(root); const node = frame ? frame.contentWindow : root === document ? window : root; bind(node, "scroll", throttledRecompute, true); } function recompute(event: UIEvent = null): void { recompute.dn = FunctionNames.ScrollRecompute; let w = window as Window; let de = document.documentElement; let element = event ? target(event) : de; // If the target is a Document node, then identify corresponding documentElement and window for this document if (element && element.nodeType === Node.DOCUMENT_NODE) { const frame = iframe(element); w = frame ? frame.contentWindow : w; element = de = (element as Document).documentElement; } // Edge doesn't support scrollTop position on document.documentElement. // For cross browser compatibility, looking up pageYOffset on window if the scroll is on document. // And, if for some reason that is not available, fall back to looking up scrollTop on document.documentElement. const x = element === de && "pageXOffset" in w ? Math.round(w.pageXOffset) : Math.round((element as HTMLElement).scrollLeft); const y = element === de && "pageYOffset" in w ? Math.round(w.pageYOffset) : Math.round((element as HTMLElement).scrollTop); const width = window.innerWidth; const height = window.innerHeight; const xPosition = width / 3; const yOffset = width > height ? height * 0.15 : height * 0.2; const startYPosition = yOffset; const endYPosition = height - yOffset; const top = getPositionNode(xPosition, startYPosition); const bottom = getPositionNode(xPosition, endYPosition); const current: ScrollState = { time: time(event), event: Event.Scroll, data: { target: element, x, y, top, bottom } }; // We don't send any scroll events if this is the first event and the current position is top (0,0) if ((event === null && x === 0 && y === 0) || x === null || y === null) { initialTop = top; initialBottom = bottom; return; } const length = state.length; const last = length > 1 ? state[length - 2] : null; if (last && similar(last, current)) { state.pop(); } state.push(current); clearTimeout(timeout); timeout = setTimeout(process, Setting.LookAhead, Event.Scroll); } const throttledRecompute = throttle(recompute, Setting.Throttle); function getPositionNode(x: number, y: number): Node { let node: Node; if ("caretPositionFromPoint" in document) { // biome-ignore lint/suspicious/noExplicitAny: caretPositionFromPoint is not defined on all browsers, makes typescript unhappy node = (document as any).caretPositionFromPoint(x, y)?.offsetNode; } else if ("caretRangeFromPoint" in document) { node = (document as Document).caretRangeFromPoint(x, y)?.startContainer; } if (!node) { node = document.elementFromPoint(x, y) as Node; } if (node && node.nodeType === Node.TEXT_NODE) { node = node.parentNode; } return node; } export function reset(): void { state = []; initialTop = null; initialBottom = null; } function process(event: Event): void { schedule(encode.bind(this, event)); } function similar(last: ScrollState, current: ScrollState): boolean { const dx = last.data.x - current.data.x; const dy = last.data.y - current.data.y; return dx * dx + dy * dy < Setting.Distance * Setting.Distance && current.time - last.time < Setting.ScrollInterval; } export function compute(): void { compute.dn = FunctionNames.ScrollCompute; if (initialTop) { const top = metadata(initialTop, null); dimension.log(Dimension.InitialScrollTop, top?.hash?.join(Constant.Dot)); } if (initialBottom) { const bottom = metadata(initialBottom, null); dimension.log(Dimension.InitialScrollBottom, bottom?.hash?.join(Constant.Dot)); } } export function stop(): void { clearTimeout(timeout); state = []; initialTop = null; initialBottom = null; }