58 lines
2.7 KiB
TypeScript
58 lines
2.7 KiB
TypeScript
/**
|
|||
|
|
* Overlay rectangles for a captured text selection.
|
||
|
|
*
|
||
|
|
* While a comment input owns focus the native selection is gone, so the
|
||
|
|
* quoted fragment is repainted with these rects (styled by
|
||
|
|
* `.oc-chat-comment-rect`). Raw Range.getClientRects() mixes block-container
|
||
|
|
* boxes with text boxes and the translucent overlaps paint double-dark bands,
|
||
|
|
* so rects are taken from the text nodes only and merged into one strip per
|
||
|
|
* visual line, each stretched to its element's line-height the way the native
|
||
|
|
* selection paints a line box.
|
||
|
|
*/
|
||
|
|
export const collectSelectionOverlayRects = (range: Range): DOMRect[] => {
|
||
|
|
const textRects: DOMRect[] = [];
|
||
|
|
const pushNodeRects = (node: Text) => {
|
||
|
|
const nodeRange = document.createRange();
|
||
|
|
nodeRange.selectNodeContents(node);
|
||
|
|
if (node === range.startContainer) nodeRange.setStart(node, range.startOffset);
|
||
|
|
if (node === range.endContainer) nodeRange.setEnd(node, range.endOffset);
|
||
|
|
const lineHeight = node.parentElement
|
||
|
|
? Number.parseFloat(window.getComputedStyle(node.parentElement).lineHeight)
|
||
|
|
: Number.NaN;
|
||
|
|
for (const rect of nodeRange.getClientRects()) {
|
||
|
|
if (rect.width <= 0 || rect.height <= 0) continue;
|
||
|
|
if (Number.isFinite(lineHeight) && lineHeight > rect.height) {
|
||
|
|
const expand = (lineHeight - rect.height) / 2;
|
||
|
|
textRects.push(new DOMRect(rect.left, rect.top - expand, rect.width, lineHeight));
|
||
|
|
} else {
|
||
|
|
textRects.push(rect);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
const root = range.commonAncestorContainer;
|
||
|
|
if (root instanceof Text) {
|
||
|
|
pushNodeRects(root);
|
||
|
|
} else {
|
||
|
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||
|
|
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
||
|
|
if (node instanceof Text && range.intersectsNode(node)) pushNodeRects(node);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const lines: Array<{ left: number; right: number; top: number; bottom: number }> = [];
|
||
|
|
for (const rect of textRects) {
|
||
|
|
const line = lines.find((candidate) => (
|
||
|
|
Math.abs(candidate.top - rect.top) < 6 && Math.abs(candidate.bottom - rect.bottom) < 6
|
||
|
|
));
|
||
|
|
if (line) {
|
||
|
|
line.left = Math.min(line.left, rect.left);
|
||
|
|
line.right = Math.max(line.right, rect.right);
|
||
|
|
line.top = Math.min(line.top, rect.top);
|
||
|
|
line.bottom = Math.max(line.bottom, rect.bottom);
|
||
|
|
} else {
|
||
|
|
lines.push({ left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return lines.map((line) => new DOMRect(line.left, line.top, line.right - line.left, line.bottom - line.top));
|
||
|
|
};
|