劃詞高亮實現 (更新中)
阿新 • • 發佈:2020-12-08
技術標籤:jsdomjavascript
劃詞高亮
資料結構
HighlightRange
class HighlightRange {
start: DomNode; // Range 開始的資訊
end: DomNode; // Range 結束的資訊
text: string; // 文字內容
id: string; // 唯一標識
}
DomNode
export interface DomNode {
$node: Node; // 節點
offset: number; // 節點中的偏移量
}
選區高亮
獲取Range物件
- 獲取當前選取的DOM Range物件
export const getDomRange = (): Range => {
const selection = window.getSelection();
// 選區的起始點和終點是否在同一個位置
if (selection.isCollapsed) {
console.debug('no text selected');
return null;
}
// 選取的range集合。但是規範要求選擇的內容始終(僅)具有一個範圍。
return selection.getRangeAt(0);
};
生成HighlightRange 物件
- Range物件 + id -> HighlightRange 物件
static fromSelection(idHook: Hook) {
const range = getDomRange(); //當前選取的DOM Range物件
if (!range) {
return null;
}
const start: DomNode = {
$node: range.startContainer,
offset: range.startOffset
} ;
const end: DomNode = {
$node: range.endContainer,
offset: range.endOffset
}
const text = range.toString();
const id = uuid(); //當前的時間戳
return new HighlightRange(start, end, text, id);
}
獲取區域內所有節點
- 獲取首尾區域包含的所有節點
// $root:Document | HTMLElement 可劃詞高亮範圍的節點
let $selectedNodes = getSelectedNodes($root, range.start, range.end);
const getSelectedNodes = (
$root: HTMLElement | Document,
start: DomNode,
end: DomNode,
): SelectedNode[] => {
const $startNode = start.$node;
const $endNode = end.$node;
const startOffset = start.offset;
const endOffset = end.offset;
// split current node when the start-node and end-node is the same
if ($startNode === $endNode && $startNode instanceof Text) {
return getNodesIfSameStartEnd($startNode, startOffset, endOffset);
}
const nodeStack: Array<HTMLElement | Document | ChildNode | Text> = [$root];
const selectedNodes: SelectedNode[] = [];
let withinSelectedRange = false;
let curNode: Node = null;
while (curNode = nodeStack.pop()) {
const children = curNode.childNodes;
for (let i = children.length - 1; i >= 0; i--) {
nodeStack.push(children[i]);
}
// only collect text nodes
if (curNode === $startNode) {
if (curNode.nodeType === 3) {
(curNode as Text).splitText(startOffset);
const node = curNode.nextSibling as Text;
selectedNodes.push({
$node: node,
type: SelectedNodeType.text,
splitType: SplitType.head
});
}
// meet the start-node (begin to traverse)
withinSelectedRange = true;
}
else if (curNode === $endNode) {
if (curNode.nodeType === 3) {
const node = (curNode as Text);
node.splitText(endOffset);
selectedNodes.push({
$node: node,
type: SelectedNodeType.text,
splitType: SplitType.tail
});
}
// meet the end-node
break;
}
// handle text nodes between the range
else if (withinSelectedRange && curNode.nodeType === 3) {
selectedNodes.push({
$node: curNode as Text,
type: SelectedNodeType.text,
splitType: SplitType.none
});
}
}
return selectedNodes;
};
節點包裝高亮處理
其原理就是在選區外包裹上定義的標籤,利用className,定義高亮樣式
- 對所有的節點進行包裝處理
$selectedNodes.map(n => {
let $node = wrapHighlight(n, range, className, this.options.wrapTag);
return $node;
});
/**
* wrap a dom node with highlight wrapper
*
* Because of supporting the highlight-overlapping,
* Highlighter can't just wrap all nodes in a simple way.
* There are three types:
* - wrapping a whole new node (without any wrapper)
* - wrapping part of the node
* - wrapping the whole wrapped node
*/
export const wrapHighlight = (
selected: SelectedNode,
range: HighlightRange,
className: string | Array<string>,
wrapTag: string
): HTMLElement => {
const $parent = selected.$node.parentNode as HTMLElement;
const $prev = selected.$node.previousSibling;
const $next = selected.$node.nextSibling;
let $wrap: HTMLElement;
// text node, not in a highlight wrapper -> should be wrapped in a highlight wrapper
if (!isHighlightWrapNode($parent)) {
$wrap = wrapNewNode(selected, range, className, wrapTag);
}
// text node, in a highlight wrap -> should split the existing highlight wrapper
else if (isHighlightWrapNode($parent) && (!isNodeEmpty($prev) || !isNodeEmpty($next))) {
$wrap = wrapPartialNode(selected, range, className, wrapTag);
}
// completely overlap (with a highlight wrap) -> only add extra id info
else {
$wrap = wrapOverlapNode(selected, range, className);
}
return $wrap;
};
含有3種情況,這裡展示一種簡單情況的包裝處理
- 外層沒有包裹highlight wrapper,則直接在外層包裹highlight wrapper
function wrapNewNode(
selected: SelectedNode,
className: string | Array<string>,
wrapTag: string
): HTMLElement {
let $wrap: HTMLElement;
$wrap = document.createElement(wrapTag);
addClass($wrap, className);
$wrap.appendChild(selected.$node.cloneNode(false));
selected.$node.parentNode.replaceChild($wrap, selected.$node);
return $wrap;
}
事件觸發
mouseUp時選區高亮
// $root:Document | HTMLElement 可劃詞高亮範圍的節點
addEventListener($root, 'mouseup', ()=>{
//...高亮處理
});