UNPKG

clarity-js

Version:

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

105 lines (96 loc) 4.55 kB
import { Character } from "../../types/data"; import { Constant, Selector, type SelectorInput } from "../../types/layout"; const excludeClassNames = Constant.ExcludeClassNames.split(Constant.Comma); let selectorMap: { [selector: string]: number[] } = {}; export function reset(): void { selectorMap = {}; } export function get(input: SelectorInput, type: Selector): string { const a = input.attributes; let prefix = input.prefix ? input.prefix[type] : null; const suffix = type === Selector.Alpha ? `${Constant.Tilde}${input.position - 1}` : `:nth-of-type(${input.position})`; switch (input.tag) { case "STYLE": case "TITLE": case "LINK": case "META": case Constant.TextTag: case Constant.DocumentTag: return Constant.Empty; case "HTML": return Constant.HTML; default: { if (prefix === null) { return Constant.Empty; } prefix = `${prefix}${Constant.Separator}`; input.tag = input.tag.indexOf(Constant.SvgPrefix) === 0 ? input.tag.substr(Constant.SvgPrefix.length) : input.tag; let selector = `${prefix}${input.tag}${suffix}`; const id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null; const classes = input.tag !== Constant.BodyTag && Constant.Class in a && a[Constant.Class].length > 0 ? a[Constant.Class] .trim() .split(/\s+/) .filter((c) => filter(c)) .join(Constant.Period) : null; if (classes && classes.length > 0) { if (type === Selector.Alpha) { // In Alpha mode, update selector to use class names, with relative positioning within the parent id container. // If the node has valid class name(s) then drop relative positioning within the parent path to keep things simple. const key = `${getDomPath(prefix)}${input.tag}${Constant.Dot}${classes}`; if (!(key in selectorMap)) { selectorMap[key] = []; } if (selectorMap[key].indexOf(input.id) < 0) { selectorMap[key].push(input.id); } selector = `${key}${Constant.Tilde}${selectorMap[key].indexOf(input.id)}`; } else { // In Beta mode, we continue to look at query selectors in context of the full page selector = `${prefix}${input.tag}.${classes}${suffix}`; } } // Update selector to use "id" field when available. There are two exceptions: // (1) if "id" appears to be an auto generated string token, e.g. guid or a random id containing digits // (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts selector = id && filter(id) ? `${getDomPrefix(prefix)}${Constant.Hash}${id}` : selector; return selector; } } } function getDomPrefix(prefix: string): string { const shadowDomStart = prefix.lastIndexOf(Constant.ShadowDomTag); const iframeDomStart = prefix.lastIndexOf(`${Constant.IFramePrefix}${Constant.HTML}`); const domStart = Math.max(shadowDomStart, iframeDomStart); if (domStart < 0) { return Constant.Empty; } return prefix.substring(0, prefix.indexOf(Constant.Separator, domStart) + 1); } function getDomPath(input: string): string { const parts = input.split(Constant.Separator); for (let i = 0; i < parts.length; i++) { const tIndex = parts[i].indexOf(Constant.Tilde); const dIndex = parts[i].indexOf(Constant.Dot); parts[i] = parts[i].substring(0, dIndex > 0 ? dIndex : tIndex > 0 ? tIndex : parts[i].length); } return parts.join(Constant.Separator); } // Check if the given input string has digits or excluded class names function filter(value: string): boolean { if (!value) { return false; } // Do not process empty strings if (excludeClassNames.some((x) => value.toLowerCase().indexOf(x) >= 0)) { return false; } for (let i = 0; i < value.length; i++) { const c = value.charCodeAt(i); if (c >= Character.Zero && c <= Character.Nine) { return false; } } return true; }