Selecting text in a rendered markdown preview shows a Comment pill; attaching stores a file-quote context draft carrying the file path, the selected fragment (not whole lines), the user's comment, and a best-effort source line range resolved by anchoring the fragment's first and last lines in the raw content — a partially located fragment gets no range rather than a misleading one. The fragment stays highlighted while the comment input is open, using the selection overlay rects shared with chat quote comments, and the preview's native selection color now matches chat messages. file-quote flows through the same context contract: composer chip previews, the message context card, and the metadata round-trip.
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));
|
|
};
|