mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-05 15:58:12 +08:00
Fix frontend HMR so the page doesn't break upon saving TS files and restructure frontend architecture (#3871)
* Clean up component setup/tear-down side effects * Clean up more component setup/tear-down side effects * Remove nonfunctional debouncer * Clean up even more component setup/tear-down side effects * Reuse backend state * Fix HMR for IO Managers and for State Providers * Rename IO Managers -> Managers and State Providers -> Stores * Restructure and partially flatten managers/stores * Code review fixes * Review fixes
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import type { Editor } from "@graphite/editor";
|
||||
|
||||
let currentCleanup: (() => void) | undefined;
|
||||
let currentArgs: [Editor] | undefined;
|
||||
|
||||
export function createClipboardManager(editor: Editor) {
|
||||
currentArgs = [editor];
|
||||
|
||||
// Subscribe to process backend event
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerClipboardWrite", (data) => {
|
||||
// If the Clipboard API is supported in the browser, copy text to the clipboard
|
||||
navigator.clipboard?.writeText?.(data.content);
|
||||
});
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerSelectionRead", async (data) => {
|
||||
editor.handle.readSelection(readAtCaret(data.cut), data.cut);
|
||||
});
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerSelectionWrite", async (data) => {
|
||||
insertAtCaret(data.content);
|
||||
});
|
||||
|
||||
function destroy() {
|
||||
editor.subscriptions.unsubscribeFrontendMessage("TriggerClipboardWrite");
|
||||
editor.subscriptions.unsubscribeFrontendMessage("TriggerSelectionRead");
|
||||
editor.subscriptions.unsubscribeFrontendMessage("TriggerSelectionWrite");
|
||||
}
|
||||
|
||||
currentCleanup = destroy;
|
||||
return { destroy };
|
||||
}
|
||||
export type ClipboardManager = ReturnType<typeof createClipboardManager>;
|
||||
|
||||
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 = String(selection);
|
||||
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 }));
|
||||
}
|
||||
|
||||
// Self-accepting HMR: tear down the old instance and re-create with the new module's code
|
||||
import.meta.hot?.accept((newModule) => {
|
||||
currentCleanup?.();
|
||||
if (currentArgs) newModule?.createClipboardManager(...currentArgs);
|
||||
});
|
||||
Reference in New Issue
Block a user