Skip to content

Instantly share code, notes, and snippets.

@azzgo
Last active July 20, 2024 14:33
Show Gist options
  • Save azzgo/e06d18e6a9c6ac71794dfac15d1518e5 to your computer and use it in GitHub Desktop.
Save azzgo/e06d18e6a9c6ac71794dfac15d1518e5 to your computer and use it in GitHub Desktop.
一个单文件实现标记和重定位,核心逻辑和思路,完全 copy from alienzhou/web-highlighter
/**
* @description minimal mark and relocation utils files
* all ideas extract from alienzhou/web-highlighter
**/
const ROOT_IDX = -2;
const UNKNOWN_IDX = -1;
function genDomMeta($node, offset, $root) {
const $originParent = getOriginParent($node);
const index =
$originParent === $root
? ROOT_IDX
: countGlobalNodeIndex($originParent, $root);
const preNodeOffset = getTextPreOffset($originParent, $node);
const tagName = $originParent.tagName;
return {
parentTagName: tagName,
parentIndex: index,
textOffset: preNodeOffset + offset,
};
}
const formatDomNode = (n) => {
if (
// Text
n.$node.nodeType === 3 ||
// CDATASection
n.$node.nodeType === 4 ||
// Comment
n.$node.nodeType === 8
) {
return n;
}
return {
$node: n.$node.childNodes[n.offset],
offset: 0,
};
};
const getOriginParent = ($node) => {
if ($node instanceof HTMLElement) {
return $node;
}
let $originParent = $node.parentNode;
return $originParent;
};
const countGlobalNodeIndex = ($node, $root) => {
const tagName = $node.tagName;
const $list = $root.getElementsByTagName(tagName);
for (let i = 0; i < $list.length; i++) {
if ($node === $list[i]) {
return i;
}
}
return UNKNOWN_IDX;
};
const getTextPreOffset = ($root, $text) => {
const nodeStack = [$root];
let $curNode = null;
let offset = 0;
while (($curNode = nodeStack.pop())) {
const children = $curNode.childNodes;
for (let i = children.length - 1; i >= 0; i--) {
nodeStack.push(children[i]);
}
if ($curNode.nodeType === 3 && $curNode !== $text) {
offset += $curNode.textContent.length;
} else if ($curNode.nodeType === 3) {
break;
}
}
return offset;
};
// from source
const queryElementNode = (startMeta, endMeta, $root) => {
const start =
startMeta.parentIndex === ROOT_IDX
? $root
: $root.getElementsByTagName(startMeta.parentTagName)[
startMeta.parentIndex
];
const end =
endMeta.parentIndex === ROOT_IDX
? $root
: $root.getElementsByTagName(endMeta.parentTagName)[endMeta.parentIndex];
return { start, end };
};
const getTextChildByOffset = ($parent, offset) => {
const nodeStack = [$parent];
let $curNode = null;
let curOffset = 0;
let startOffset = 0;
while (($curNode = nodeStack.pop())) {
const children = $curNode.childNodes;
for (let i = children.length - 1; i >= 0; i--) {
nodeStack.push(children[i]);
}
if ($curNode.nodeType === 3) {
startOffset = offset - curOffset;
curOffset += $curNode.textContent.length;
if (curOffset >= offset) {
break;
}
}
}
if (!$curNode) {
$curNode = $parent;
}
return {
$node: $curNode,
offset: startOffset,
};
};
// export functions
export const getDomMetaFromRange = (range, $root) => {
const start = {
$node: range.startContainer,
offset: range.startOffset,
};
const end = {
$node: range.endContainer,
offset: range.endOffset,
};
const startDomNode = formatDomNode(start);
const endDomNode = formatDomNode(end);
return {
startMeta: genDomMeta(startDomNode.$node, startDomNode.offset, $root),
endMeta: genDomMeta(endDomNode.$node, endDomNode.offset, $root),
};
};
export const getDomNodeFromMeta = (startMeta, endMeta, $root) => {
const { start, end } = queryElementNode(startMeta, endMeta, $root);
let startInfo = getTextChildByOffset(start, startMeta.textOffset);
let endInfo = getTextChildByOffset(end, endMeta.textOffset);
return {
start: startInfo,
end: endInfo,
};
};
export const selectAndScrollToView = (start, end) => {
const range = new Range();
range.setStart(start.$node, start.offset);
range.setEnd(end.$node, end.offset);
const span = document.createElement("span");
range.insertNode(span);
span.scrollIntoView();
span.remove();
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment