fix: unify chat process and reply presentation
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import type { ClipboardEvent } from "react";
|
||||
|
||||
export function copySelectedText(event: ClipboardEvent<HTMLElement>) {
|
||||
if (event.defaultPrevented) return;
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
if (target?.closest("input, textarea, [contenteditable]:not([contenteditable=false])")) return;
|
||||
const selection = event.currentTarget.ownerDocument.defaultView?.getSelection();
|
||||
if (!selection || selection.isCollapsed || selection.rangeCount !== 1) return;
|
||||
const range = selection.getRangeAt(0);
|
||||
const text = selection.toString();
|
||||
const suffix = text.match(/\n+$/)?.[0].length ?? 0;
|
||||
const sourceSuffix = range.toString().match(/\n+$/)?.[0].length ?? 0;
|
||||
// Chromium includes the block boundary when selecting an entire paragraph.
|
||||
// Keep native internal paragraphs, table tabs and code indentation, removing
|
||||
// only the trailing boundary that is absent from the selected source.
|
||||
if (suffix < 2 || suffix <= sourceSuffix) return;
|
||||
event.clipboardData.setData("text/plain", text.slice(0, text.length - suffix + sourceSuffix));
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
export function copySelectedParagraph(event: ClipboardEvent<HTMLElement>, paragraph: HTMLParagraphElement, hasNonSelectableControls = false) {
|
||||
const selection = paragraph.ownerDocument.defaultView?.getSelection();
|
||||
if (!selection || selection.isCollapsed || selection.rangeCount !== 1) return;
|
||||
|
||||
const selected = selection.getRangeAt(0);
|
||||
if (!selected.intersectsNode(paragraph)) return;
|
||||
|
||||
const contents = paragraph.ownerDocument.createRange();
|
||||
contents.selectNodeContents(paragraph);
|
||||
const bounded = selected.cloneRange();
|
||||
if (bounded.compareBoundaryPoints(Range.START_TO_START, contents) < 0) {
|
||||
bounded.setStart(contents.startContainer, contents.startOffset);
|
||||
}
|
||||
if (bounded.compareBoundaryPoints(Range.END_TO_END, contents) > 0) {
|
||||
bounded.setEnd(contents.endContainer, contents.endOffset);
|
||||
}
|
||||
|
||||
// Native paragraph selection can extend to the next message and serialize
|
||||
// the paragraph boundary as extra newlines. Range text preserves the actual
|
||||
// selected source, including intentional whitespace inside the message.
|
||||
const text = bounded.toString();
|
||||
if (!text) return;
|
||||
if (hasNonSelectableControls) {
|
||||
// DOM ranges can include adjacent user-select:none toolbar labels, while
|
||||
// the rendered selection contains only this paragraph and its boundary.
|
||||
if (selection.toString() !== `${text}\n\n`) return;
|
||||
} else if (selected.toString() !== text || selection.toString() === text) return;
|
||||
|
||||
event.clipboardData.setData("text/plain", text);
|
||||
event.preventDefault();
|
||||
}
|
||||
Reference in New Issue
Block a user