clarity-js
Version:
An analytics library that uses web page interactions to generate aggregated insights
131 lines (111 loc) • 4.87 kB
text/typescript
import { BooleanFlag, Constant, Dimension, Event } from "@clarity-types/data";
import { ScrollState, 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 throttle from "@src/core/throttle";
import { iframe } from "@src/layout/dom";
import { target, metadata } from "@src/layout/target";
import encode from "./encode";
import * as dimension from "@src/data/dimension";
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 {
let frame = iframe(root);
let node = frame ? frame.contentWindow : (root === document ? window : root);
bind(node, "scroll", throttledRecompute, true);
}
function recompute(event: UIEvent = null): void {
let w = window as Window;
let de = document.documentElement;
let element = event ? target(event) : de;
// In some edge cases, it's possible for target to be null.
// In those cases, we cannot proceed with scroll event instrumentation.
if (!element) { return; }
// If the target is a Document node, then identify corresponding documentElement and window for this document
if (element && element.nodeType === Node.DOCUMENT_NODE) {
let 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.
let x = element === de && "pageXOffset" in w ? Math.round(w.pageXOffset) : Math.round((element as HTMLElement).scrollLeft);
let 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 trust = event && event.isTrusted ? BooleanFlag.True : BooleanFlag.False;
let current: ScrollState = { time: time(event), event: Event.Scroll, data: {target: element, x, y, top, bottom, trust} };
// 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;
}
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, Event.Scroll);
}
const throttledRecompute = throttle(recompute, Setting.Throttle);
function getPositionNode(x: number, y: number): Node {
let node: Node;
if ("caretPositionFromPoint" in document) {
node = (document as any).caretPositionFromPoint(x, y)?.offsetNode;
} else if ("caretRangeFromPoint" in document) {
node = (document as any).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 {
let dx = last.data.x - current.data.x;
let 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 {
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);
throttledRecompute.cleanup();
state = [];
initialTop = null;
initialBottom = null;
}