From e46f03813214da79c1bdb62244d19cb41cb71090 Mon Sep 17 00:00:00 2001 From: Marcel Mraz Date: Thu, 17 Jul 2025 15:22:32 +0200 Subject: [PATCH] feat: expose `applyTo` options, don't commit empty text element (#9744) * Expose applyTo options, skip re-draw for empty text * Don't commit empty text elements --- packages/element/src/store.ts | 12 ++++++++++-- packages/excalidraw/components/App.tsx | 19 +++++++++++++++---- .../excalidraw/wysiwyg/textWysiwyg.test.tsx | 4 ++-- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/element/src/store.ts b/packages/element/src/store.ts index 0f5933422c..ae0c969e5a 100644 --- a/packages/element/src/store.ts +++ b/packages/element/src/store.ts @@ -27,6 +27,8 @@ import { isImageElement, } from "./index"; +import type { ApplyToOptions } from "./delta"; + import type { ExcalidrawElement, OrderedExcalidrawElement, @@ -570,9 +572,15 @@ export class StoreDelta { delta: StoreDelta, elements: SceneElementsMap, appState: AppState, + options: ApplyToOptions = { + excludedProperties: new Set(), + }, ): [SceneElementsMap, AppState, boolean] { - const [nextElements, elementsContainVisibleChange] = - delta.elements.applyTo(elements); + const [nextElements, elementsContainVisibleChange] = delta.elements.applyTo( + elements, + StoreSnapshot.empty().elements, + options, + ); const [nextAppState, appStateContainsVisibleChange] = delta.appState.applyTo(appState, nextElements); diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 87d1be2779..1397a8b7c2 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -4925,7 +4925,17 @@ class App extends React.Component { }), onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => { const isDeleted = !nextOriginalText.trim(); - updateElement(nextOriginalText, isDeleted); + + if (isDeleted && !isExistingElement) { + // let's just remove the element from the scene, as it's an empty just created text element + this.scene.replaceAllElements( + this.scene + .getElementsIncludingDeleted() + .filter((x) => x.id !== element.id), + ); + } else { + updateElement(nextOriginalText, isDeleted); + } // select the created text element only if submitting via keyboard // (when submitting via click it should act as signal to deselect) if (!isDeleted && viaKeyboard) { @@ -4954,9 +4964,10 @@ class App extends React.Component { element, ]); } - if (!isDeleted || isExistingElement) { - this.store.scheduleCapture(); - } + + // we need to record either way, whether the text element was added or removed + // since we need to sync this delta to other clients, otherwise it would end up with inconsistencies + this.store.scheduleCapture(); flushSync(() => { this.setState({ diff --git a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx index c1a2f33094..08301a3042 100644 --- a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx +++ b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx @@ -704,7 +704,7 @@ describe("textWysiwyg", () => { rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 2, ); - expect(h.elements.length).toBe(3); + expect(h.elements.length).toBe(2); text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.type).toBe("text"); @@ -1198,7 +1198,7 @@ describe("textWysiwyg", () => { updateTextEditor(editor, " "); Keyboard.exitTextEditor(editor); expect(rectangle.boundElements).toStrictEqual([]); - expect(h.elements[1].isDeleted).toBe(true); + expect(h.elements[1]).toBeUndefined(); }); it("should restore original container height and clear cache once text is unbind", async () => {