import type { IMirror, SerializedNodeWithId, SnapshotNodeId } from './types'; import {InteractionId, XPath} from './types'; export type IdNodeMap = Map; export type NodeMetaMap = WeakMap; export function isValidCssSelector(selector: string) { try { return document.createDocumentFragment().querySelector(selector) || true; } catch (ex) { return false; } } export class Mirror implements IMirror { private idNodeMap: IdNodeMap = new Map(); private nodeIdMap: Map = new Map(); private nodeMetaMap: NodeMetaMap = new WeakMap(); public static UNSERIALIZED_NODE_ID: SnapshotNodeId = '-1'; getId(n: Node | undefined | null): SnapshotNodeId { if (!n) return Mirror.UNSERIALIZED_NODE_ID; const id = this.getMeta(n)?.id; // if n is not a serialized Node, use -1 as its id. return id ?? Mirror.UNSERIALIZED_NODE_ID; } getNode(id: SnapshotNodeId): Node | null { return this.idNodeMap.get(id) || null; } getIds(): SnapshotNodeId[] { return Array.from(this.idNodeMap.keys()); } getMeta(n: Node): SerializedNodeWithId | null { return this.nodeMetaMap.get(n) || null; } // removes the node from idNodeMap // doesn't remove the node from nodeMetaMap removeNodeFromMap(n: Node): void { const id = this.getId(n); this.idNodeMap.delete(id); if (n.childNodes) { n.childNodes.forEach((childNode) => this.removeNodeFromMap((childNode as unknown) as Node), ); } } has(id: SnapshotNodeId): boolean { return this.idNodeMap.has(id); } hasNode(node: Node): boolean { return this.nodeMetaMap.has(node); } add(n: Node, meta: SerializedNodeWithId): void { const id = meta.id; this.idNodeMap.set(id, n); this.nodeMetaMap.set(n, meta); } replace(id: SnapshotNodeId, n: Node): void { this.idNodeMap.set(id, n); } reset(): void { this.idNodeMap = new Map(); this.nodeMetaMap = new WeakMap(); } static create(): Mirror { return new Mirror(); } } function getXPath(node: Node | null): XPath { let xpath = ''; // Starting from the given node, traverse up the DOM tree for (; node && node.nodeType === Node.ELEMENT_NODE; node = node.parentNode) { let idx = 0; // Count the number of preceding siblings with the same tag name for (let sibling = node.previousSibling; sibling; sibling = sibling.previousSibling) { if (sibling.nodeType === Node.ELEMENT_NODE && (sibling as Element).tagName === (node as Element).tagName) { idx++; } } // Construct the XPath expression segment const tagName = (node as Element).tagName.toLowerCase(); const position = (idx === 0 ? '' : '[' + (idx + 1) + ']'); xpath = '/' + tagName + position + xpath; } return xpath as XPath; } export function getInteractionId(node: Node | null): InteractionId { return getXPath(node); } function getByXPath(doc: Document, xpath: XPath) { const result = document.evaluate(xpath, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); if (result.singleNodeValue) { return result.singleNodeValue; } else { console.log('Element not found', xpath); return null; } } export function getElementByInteractionId(interactionId: InteractionId, doc: Document): Node | null { return getByXPath(doc, interactionId); }