From d04a6f41b30aa4eaf5251ba6d1350297a42f3065 Mon Sep 17 00:00:00 2001 From: kjimlau Date: Mon, 20 Sep 2021 15:53:32 +0800 Subject: [PATCH] feat: parse dom queryPath --- src/content-scripts/parser/selection-meta.ts | 41 ++++++++--- src/utils/dom.ts | 72 ++++++++++++++++---- 2 files changed, 90 insertions(+), 23 deletions(-) diff --git a/src/content-scripts/parser/selection-meta.ts b/src/content-scripts/parser/selection-meta.ts index 09cd75c..cab346d 100644 --- a/src/content-scripts/parser/selection-meta.ts +++ b/src/content-scripts/parser/selection-meta.ts @@ -1,5 +1,5 @@ import { isObject } from "@/utils/utils"; -import { getNodeText } from "@/utils/dom"; +import { filterAncestorNodes, getDomQueryPath, getNodeText } from "@/utils/dom"; import { Rect } from "@/types/common"; /** @@ -98,14 +98,37 @@ function filterInvalidCoorRects(rects: Rect[]) { return rects.filter((ele) => !excludeRects.includes(ele)); } +function parseSelectionRects(range: Range) { + let rects = Array.from(range.getClientRects()).map((r) => ({ + x: r.x, + y: r.y + window.scrollY, + width: r.width, + height: r.height, + })); + rects = filterDuplicateRects(rects); + rects = filterInvalidCoorRects(rects); + return rects; +} + +function parseSelectedQueryPaths(selection: Selection, range: Range) { + const allWithinRangeParentNodes = (range.commonAncestorContainer as HTMLElement)?.getElementsByTagName( + "*" + ); + const allSelectedNodes = Array.from(allWithinRangeParentNodes).filter(n => selection.containsNode(n, true)); + const textNodes = filterAncestorNodes(allSelectedNodes); + return textNodes.map(n => getDomQueryPath(n as HTMLElement)); +} + /** * Get the `SelectionMeta` from current mouse selection object. */ export interface SelectionMeta { + queryPaths: string[]; rects: Rect[]; texts: string[]; } export function parseRectsAndTextFromSelection(): SelectionMeta { + let queryPaths: string[] = []; let rects: Rect[] = []; let texts: string[] = []; try { @@ -114,22 +137,22 @@ export function parseRectsAndTextFromSelection(): SelectionMeta { if (selection) { const range = selection.getRangeAt(0); if (range) { + // 1. texts const cloneFragment = range.cloneContents(); texts = getNodeTextList(cloneFragment); - rects = Array.from(range.getClientRects()).map((r) => ({ - x: r.x, - y: r.y + window.scrollY, - width: r.width, - height: r.height, - })); - rects = filterDuplicateRects(rects); - rects = filterInvalidCoorRects(rects); + + // 2. rects + rects = parseSelectionRects(range); + + // 3. queryPaths + queryPaths = parseSelectedQueryPaths(selection, range); } } } catch (err) { console.log(err); } return { + queryPaths, rects, texts, }; diff --git a/src/utils/dom.ts b/src/utils/dom.ts index 2e9a86e..7e244fa 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -1,25 +1,69 @@ -import { getObjectType } from '../utils/utils' +import { getObjectType } from "../utils/utils"; export const getNodeText = (node: any): string => { - let text = '' - const objType = getObjectType(node) + let text = ""; + const objType = getObjectType(node); switch (objType) { - case '[object HTMLTimeElement]': { - text = node.dateTime || '' - break + case "[object HTMLTimeElement]": { + text = node.dateTime || ""; + break; } - case '[object Text]': { - text = node.textContent || '' - break + case "[object Text]": { + text = node.textContent || ""; + break; } - case '[object HTMLSpanElement]': { - text = node.innerText || '' - break + case "[object HTMLSpanElement]": { + text = node.innerText || ""; + break; } // TODO: more options default: { - break + break; } } - return text + return text; +}; + +export function getDomQueryPath(el: HTMLElement) { + if (!el) return ''; + + const stack = []; + while (el.parentNode != null) { + let sibCount = 0; + let sibIndex = 0; + for (let i = 0; i < el.parentNode.childNodes.length; i++) { + const sib = el.parentNode.childNodes[i]; + if (sib.nodeName == el.nodeName) { + if (sib === el) { + sibIndex = sibCount; + } + sibCount++; + } + } + const name = el.nodeName.toLowerCase(); + if (el.hasAttribute("id") && el.id != "") { + stack.unshift(`${name}#${el.id}`); + } else if (sibCount > 1) { + stack.unshift(`${name}:n-th-of-type(${sibIndex + 1})`); + } else { + stack.unshift(name); + } + el = el.parentNode as HTMLElement; + } + + return stack.slice(1).join(' '); // removes the html element +} + +export function filterAncestorNodes(nodes: Node[]) { + const ancestors = new Set(); + for (const node of nodes) { + let parent = node.parentNode; + while(parent) { + if (nodes.includes(parent) && node.textContent) { + ancestors.add(parent); + } + parent = parent.parentNode; + } + } + return nodes.filter(n => !ancestors.has(n)); } \ No newline at end of file