Files
openchamber/packages/ui/src/lib/selectionOverlayRects.ts
T

58 lines
2.7 KiB
TypeScript
Raw Normal View History

/**
* 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));
};