/**
* DOM Path - DOM 路径计算和定位
*
* DOM 路径是元素在 DOM 树中的索引路径,用于:
* - 元素位置追踪
* - 选择器失效后的快速恢复
* - 元素比较和验证
*/
// =============================================================================
// Types
// =============================================================================
/**
* DOM 路径:从根到目标元素的子元素索引数组
*
* @example
* ```
* [0, 2, 1] 表示:
* root
* └─ children[0]
* └─ children[2]
* └─ children[1] <- 目标元素
* ```
*/
export type DomPath = number[];
// =============================================================================
// Core Functions
// =============================================================================
/**
* 计算元素在 DOM 树中的路径
*
* 从目标元素向上遍历到根节点(Document 或 ShadowRoot),
* 记录每一层在父元素 children 中的索引。
*
* @example
* ```ts
* const path = computeDomPath(button);
* // => [0, 2, 1] - 从 body/shadowRoot 开始的路径
* ```
*/
export function computeDomPath(element: Element): DomPath {
const path: DomPath = [];
let current: Element | null = element;
while (current) {
const parent: Element | null = current.parentElement;
if (parent) {
// 正常父元素
const siblings = Array.from(parent.children);
const index = siblings.indexOf(current);
if (index >= 0) {
path.unshift(index);
}
current = parent;
continue;
}
// 检查是否是 ShadowRoot 或 Document 的直接子元素
const parentNode = current.parentNode;
if (parentNode instanceof ShadowRoot || parentNode instanceof Document) {
const children = Array.from(parentNode.children);
const index = children.indexOf(current);
if (index >= 0) {
path.unshift(index);
}
}
// 到达根节点,停止遍历
break;
}
return path;
}
/**
* 根据 DOM 路径定位元素
*
* @param root - 查询根节点(Document 或 ShadowRoot)
* @param path - DOM 路径
* @returns 找到的元素,如果路径无效则返回 null
*
* @example
* ```ts
* const element = locateByDomPath(document, [0, 2, 1]);
* // => 返回 body > children[0] > children[2] > children[1]
* ```
*/
export function locateByDomPath(root: Document | ShadowRoot, path: DomPath): Element | null {
if (path.length === 0) {
return null;
}
let current: Element | null = root.children[path[0]] ?? null;
for (let i = 1; i < path.length && current; i++) {
const index = path[i];
current = current.children[index] ?? null;
}
return current;
}
/**
* 比较两个 DOM 路径
*
* @returns 包含是否相同和公共前缀长度的结果
*
* @example
* ```ts
* const result = compareDomPaths([0, 2, 1], [0, 2, 3]);
* // => { same: false, commonPrefixLength: 2 }
* ```
*/
export function compareDomPaths(
a: DomPath,
b: DomPath,
): { same: boolean; commonPrefixLength: number } {
const minLen = Math.min(a.length, b.length);
let commonPrefixLength = 0;
for (let i = 0; i < minLen; i++) {
if (a[i] === b[i]) {
commonPrefixLength++;
} else {
break;
}
}
const same = a.length === b.length && commonPrefixLength === a.length;
return { same, commonPrefixLength };
}
/**
* 检查路径 A 是否是路径 B 的祖先
*
* @example
* ```ts
* isAncestorPath([0, 2], [0, 2, 1]); // true
* isAncestorPath([0, 2, 1], [0, 2]); // false
* ```
*/
export function isAncestorPath(ancestor: DomPath, descendant: DomPath): boolean {
if (ancestor.length >= descendant.length) {
return false;
}
for (let i = 0; i < ancestor.length; i++) {
if (ancestor[i] !== descendant[i]) {
return false;
}
}
return true;
}
/**
* 获取从祖先路径到后代路径的相对路径
*
* @example
* ```ts
* getRelativePath([0, 2], [0, 2, 1, 3]); // [1, 3]
* ```
*/
export function getRelativePath(ancestor: DomPath, descendant: DomPath): DomPath | null {
if (!isAncestorPath(ancestor, descendant)) {
return null;
}
return descendant.slice(ancestor.length);
}