Desktop: Text clipboard support (#3461)

* gray background for viewport texture

* cust copy paste support

* connect clipboard read on web

* fix eyedropper bounds

* cleanup

* add missing char events for some named keys like enter
This commit is contained in:
Timon
2025-12-12 00:16:35 +00:00
committed by GitHub
parent d6c06da878
commit 6d852f11af
30 changed files with 602 additions and 115 deletions
+90 -3
View File
@@ -1,10 +1,97 @@
import { type Editor } from "@graphite/editor";
import { TriggerTextCopy } from "@graphite/messages";
import { TriggerClipboardWrite, TriggerSelectionRead, TriggerSelectionWrite } from "@graphite/messages";
export function createClipboardManager(editor: Editor) {
// Subscribe to process backend event
editor.subscriptions.subscribeJsMessage(TriggerTextCopy, (triggerTextCopy) => {
editor.subscriptions.subscribeJsMessage(TriggerClipboardWrite, (triggerTextCopy) => {
// If the Clipboard API is supported in the browser, copy text to the clipboard
navigator.clipboard?.writeText?.(triggerTextCopy.copyText);
navigator.clipboard?.writeText?.(triggerTextCopy.content);
});
editor.subscriptions.subscribeJsMessage(TriggerSelectionRead, async (data) => {
editor.handle.readSelection(readAtCaret(data.cut), data.cut);
});
editor.subscriptions.subscribeJsMessage(TriggerSelectionWrite, async (data) => {
insertAtCaret(data.content);
});
}
function readAtCaret(cut: boolean): string | undefined {
const element = window.document.activeElement;
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
const start = element.selectionStart;
const end = element.selectionEnd;
if ((!start && start !== 0) || (!end && end !== 0) || start === end) {
return undefined;
}
const value = element.value;
const selectedText = value.slice(start, end);
if (cut) {
element.value = value.slice(0, start) + value.slice(end);
element.selectionStart = element.selectionEnd = start;
element.dispatchEvent(new Event("input", { bubbles: true }));
}
return selectedText;
}
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
return undefined;
}
const selectedText = selection.toString();
if (!selectedText) return undefined;
if (cut) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
return selectedText;
}
function insertAtCaret(text: string) {
const element = window.document.activeElement;
if (!element) return;
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
const start = element.selectionStart;
const end = element.selectionEnd;
if ((!start && start !== 0) || (!end && end !== 0)) return;
const value = element.value;
element.value = value.slice(0, start) + text + value.slice(end);
const newPos = start + text.length;
element.selectionStart = element.selectionEnd = newPos;
} else if (element instanceof HTMLElement && element.isContentEditable) {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
range.deleteContents();
const textNode = window.document.createTextNode(text);
range.insertNode(textNode);
range.setStartAfter(textNode);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
element.dispatchEvent(new Event("input", { bubbles: true }));
}