UNPKG

clarity-js

Version:

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

84 lines (75 loc) 4.15 kB
import { Character } from "../../types/data"; import { Constant, Selector, SelectorInput } from "../../types/layout"; import { ExcludeClassNamesList } from "./constants"; const excludeClassNames = ExcludeClassNamesList; let selectorMap: { [selector: string]: number[] } = {}; export function reset(): void { selectorMap = {}; } export function get(input: SelectorInput, type: Selector): string { let a = input.attributes; let prefix = input.prefix ? input.prefix[type] : null; let 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; let id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null; let 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. let key = getDomPath(prefix) + input.tag + Constant.Dot + classes; if (!(key in selectorMap)) { selectorMap[key] = []; } if (!selectorMap[key].includes(input.id)) { 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 { let parts = input.split(Constant.Separator); for (let i = 0; i < parts.length; i++) { let tIndex = parts[i].indexOf(Constant.Tilde); let 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().includes(x))) { return false; } for (let i = 0; i < value.length; i++) { let c = value.charCodeAt(i); if (c >= Character.Zero && c <= Character.Nine) { return false }; } return true; }