划词高亮
数据结构
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', ()=>{
//...高亮处理
});