diff --git a/ts/editor/rich-text-input/RichTextInput.svelte b/ts/editor/rich-text-input/RichTextInput.svelte index 88549aefc..2409a169b 100644 --- a/ts/editor/rich-text-input/RichTextInput.svelte +++ b/ts/editor/rich-text-input/RichTextInput.svelte @@ -40,7 +40,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html const key = Symbol("richText"); const [context, setContextProperty] = contextProperty(key); - const [globalInputHandler, setupGlobalInputHandler] = useInputHandler(); + const [globalInputHandler, setupGlobalInputHandler] = useInputHandler( { handleUndo: false }); const [lifecycle, instances, setupLifecycleHooks] = lifecycleHooks(); const apiStore = writable(null); @@ -97,7 +97,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html const nodes = getNormalizingNodeStore(); const [richTextPromise, resolve] = useRichTextResolve(); const { mirror, preventResubscription } = useDOMMirror(); - const [inputHandler, setupInputHandler] = useInputHandler(); + const [inputHandler, setupInputHandler] = useInputHandler({ handleUndo: true }); const [customStyles, stylesResolve] = promiseWithResolver>(); export function attachShadow(element: Element): void { diff --git a/ts/lib/sveltelib/input-handler.ts b/ts/lib/sveltelib/input-handler.ts index c6ea73e9f..810481561 100644 --- a/ts/lib/sveltelib/input-handler.ts +++ b/ts/lib/sveltelib/input-handler.ts @@ -96,23 +96,37 @@ function getCaretPosition(element: Element) { * Prevents that too many event listeners are attached and allows for some * coordination between them. */ -function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] { +interface InputHandlerOptions { + handleUndo: boolean; +} +function useInputHandler( + options: InputHandlerOptions, +): [InputHandlerAPI, SetupInputHandlerAction] { const beforeInput = new HandlerList(); const insertText = new HandlerList(); const afterInput = new HandlerList(); - const undoManager = new UndoManager(); - let hasSetupObserver = false; - const config = { + let undoManager: UndoManager | null = null; + let isUndoing = false; + + let observer: MutationObserver | null = null; + const observerConfig = { attributes: true, childList: true, subtree: true, }; - const observer = new MutationObserver(onMutation); + let hasSetupObserver = false; + + if (options.handleUndo){ + undoManager = new UndoManager(); + observer = new MutationObserver(onMutation); + } function onMutation(mutationsList: MutationRecord[]) { + if(isUndoing) return; + const element = mutationsList[0].target; - undoManager.register(element.innerHTML, getMaxOffset(element)); + undoManager!.register(element.innerHTML, getMaxOffset(element)); } async function onBeforeInput(this: Element, event: InputEvent): Promise { @@ -149,14 +163,17 @@ function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] { } async function onInput(this: Element, event: Event): Promise { + await afterInput.dispatch({ event }); + + if (!undoManager) { return; } if (!hasSetupObserver) { - observer.observe(this, config); + observer!.observe(this, observerConfig); hasSetupObserver = true; } + const position = getCaretPosition(this); undoManager.register(this.innerHTML, position - 1); undoManager.clearRedoStack(); - await afterInput.dispatch({ event }); } const pointerDown = new HandlerList<{ event: PointerEvent }>(); @@ -186,11 +203,19 @@ function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] { } else if (event.code === "Tab") { specialKey.dispatch({ event, action: "tab" }); } else if ((event.ctrlKey || event.metaKey) && event.key == "z") { + if (!undoManager) { return; } + event.preventDefault(); + isUndoing = true; undoManager.undo(this); + setTimeout(() => { isUndoing = false }); //reset the flag when the current execution stack is cleared } else if ((event.ctrlKey || event.metaKey) && event.key == "y") { + if (!undoManager) { return; } + event.preventDefault(); + isUndoing = true; undoManager.redo(this); + setTimeout(() => { isUndoing = false }); //reset the flag when the current execution stack is cleared } }