Merge pull request #8302 from penpot/azazeln28-issue-13124-text-not-restored-undoing

🐛 Fix text not restored on ctrl+z
This commit is contained in:
Elena Torró 2026-02-09 13:41:43 +01:00 committed by GitHub
commit cf2b40a097
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 69 additions and 20 deletions

View File

@ -46,6 +46,7 @@
(def ^function create-editor editor.v2/create) (def ^function create-editor editor.v2/create)
(def ^function set-editor-root! editor.v2/setRoot) (def ^function set-editor-root! editor.v2/setRoot)
(def ^function get-editor-root editor.v2/getRoot) (def ^function get-editor-root editor.v2/getRoot)
(def ^function is-empty? editor.v2/isEmpty)
(def ^function dispose! editor.v2/dispose) (def ^function dispose! editor.v2/dispose)
(declare v2-update-text-shape-content) (declare v2-update-text-shape-content)
@ -901,15 +902,22 @@
(update-in state [:workspace-text-modifier shape-id] {:position-data position-data})))) (update-in state [:workspace-text-modifier shape-id] {:position-data position-data}))))
(defn v2-update-text-shape-content (defn v2-update-text-shape-content
[id content & {:keys [update-name? name finalize?] [id content & {:keys [update-name? name finalize? save-undo?]
:or {update-name? false name nil finalize? false}}] :or {update-name? false name nil finalize? false save-undo? true}}]
(ptk/reify ::v2-update-text-shape-content (ptk/reify ::v2-update-text-shape-content
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(if (features/active-feature? state "render-wasm/v1") (if (features/active-feature? state "render-wasm/v1")
(let [objects (dsh/lookup-page-objects state) (let [objects (dsh/lookup-page-objects state)
shape (get objects id) shape (get objects id)
new-shape? (nil? (:content shape))] new-shape? (nil? (:content shape))
prev-content (:content shape)
has-prev-content? (not (nil? (:prev-content shape)))
has-content? (when-not new-shape?
(v2-content-has-text? content))
did-has-content? (when-not new-shape?
(v2-content-has-text? prev-content))]
(rx/concat (rx/concat
(rx/of (rx/of
(dwsh/update-shapes (dwsh/update-shapes
@ -917,10 +925,16 @@
(fn [shape] (fn [shape]
(let [new-shape (-> shape (let [new-shape (-> shape
(assoc :content content) (assoc :content content)
(cond-> (and has-content?
has-prev-content?)
(dissoc :prev-content))
(cond-> (and did-has-content?
(not has-content?))
(assoc :prev-content prev-content))
(cond-> (and update-name? (some? name)) (cond-> (and update-name? (some? name))
(assoc :name name)))] (assoc :name name)))]
new-shape)) new-shape))
{:undo-group (when new-shape? id)}) {:save-undo? save-undo? :undo-group (when new-shape? id)})
(if (and (not= :fixed (:grow-type shape)) finalize?) (if (and (not= :fixed (:grow-type shape)) finalize?)
(dwm/apply-wasm-modifiers (dwm/apply-wasm-modifiers
@ -933,8 +947,16 @@
(when finalize? (when finalize?
(rx/concat (rx/concat
(when (and (not (v2-content-has-text? content)) (some? id)) (when (and (not has-content?) (some? id))
(rx/of (rx/of
(when has-prev-content?
(dwsh/update-shapes
[id]
(fn [shape]
(let [new-shape (-> shape
(assoc :content (:prev-content shape)))]
new-shape))
{:save-undo? false}))
(dws/deselect-shape id) (dws/deselect-shape id)
(dwsh/delete-shapes #{id}))) (dwsh/delete-shapes #{id})))
(rx/of (dwt/finish-transform)))))) (rx/of (dwt/finish-transform))))))

View File

@ -117,7 +117,8 @@
(st/emit! (dwt/v2-update-text-shape-content shape-id content (st/emit! (dwt/v2-update-text-shape-content shape-id content
:update-name? update-name? :update-name? update-name?
:name generated-name :name generated-name
:finalize? true)))) :finalize? true
:save-undo? false))))
(let [container-node (mf/ref-val container-ref)] (let [container-node (mf/ref-val container-ref)]
(dom/set-style! container-node "opacity" 0))) (dom/set-style! container-node "opacity" 0)))
@ -135,15 +136,21 @@
on-needs-layout on-needs-layout
(fn [] (fn []
(when-let [content (content/dom->cljs (dwt/get-editor-root instance))] (when-let [content (content/dom->cljs (dwt/get-editor-root instance))]
(st/emit! (dwt/v2-update-text-shape-content shape-id content :update-name? true))) (st/emit! (dwt/v2-update-text-shape-content shape-id content
:update-name? true
:save-undo? false)))
;; FIXME: We need to find a better way to trigger layout changes. ;; FIXME: We need to find a better way to trigger layout changes.
#_(st/emit! #_(st/emit!
(dwt/v2-update-text-shape-position-data shape-id []))) (dwt/v2-update-text-shape-position-data shape-id [])))
on-change on-change
(fn [] (fn []
(when-let [content (content/dom->cljs (dwt/get-editor-root instance))] (let [is-empty? (dwt/is-empty? instance)
(st/emit! (dwt/v2-update-text-shape-content shape-id content :update-name? true)))) save-undo? (not is-empty?)]
(when-let [content (content/dom->cljs (dwt/get-editor-root instance))]
(st/emit! (dwt/v2-update-text-shape-content shape-id content
:update-name? true
:save-undo? save-undo?)))))
on-clipboard-change on-clipboard-change
(fn [event] (fn [event]
@ -247,7 +254,7 @@
:ref container-ref :ref container-ref
:data-testid "text-editor-container" :data-testid "text-editor-container"
:style {:width "var(--editor-container-width)" :style {:width "var(--editor-container-width)"
:height "var(--editor-container-height)"} :height "var(--editor-container-height)"}}
;; We hide the editor when is blurred because otherwise the ;; We hide the editor when is blurred because otherwise the
;; selection won't let us see the underlying text. Use opacity ;; selection won't let us see the underlying text. Use opacity
;; because display or visibility won't allow to recover focus ;; because display or visibility won't allow to recover focus
@ -256,7 +263,7 @@
;; IMPORTANT! This is now done through DOM mutations (see ;; IMPORTANT! This is now done through DOM mutations (see
;; on-blur and on-focus) but I keep this for future references. ;; on-blur and on-focus) but I keep this for future references.
;; :opacity (when @blurred 0)}} ;; :opacity (when @blurred 0)}}
}
[:div [:div
{:class (dm/str {:class (dm/str
"mousetrap " "mousetrap "

View File

@ -32,7 +32,8 @@
"This function adds units to style values" "This function adds units to style values"
[k v] [k v]
(cond (cond
(and (or (= k :font-size) (and (keyword? k)
(or (= k :font-size)
(= k :letter-spacing)) (= k :letter-spacing))
(not= (str/slice v -2) "px")) (not= (str/slice v -2) "px"))
(str v "px") (str v "px")

View File

@ -326,7 +326,9 @@ export class TextEditor extends EventTarget {
* @param {FocusEvent} e * @param {FocusEvent} e
*/ */
#onBlur = (e) => { #onBlur = (e) => {
this.#changeController.notifyImmediately(); if (!this.isEmpty) {
this.#changeController.notifyImmediately();
}
this.#selectionController.saveSelection(); this.#selectionController.saveSelection();
this.dispatchEvent(new FocusEvent(e.type, e)); this.dispatchEvent(new FocusEvent(e.type, e));
}; };
@ -683,13 +685,26 @@ export function createRootFromString(string) {
* Returns true if the passed object is a TextEditor * Returns true if the passed object is a TextEditor
* instance. * instance.
* *
* @param {*} instance * @param {TextEditor} instance
* @returns {boolean} * @returns {boolean}
*/ */
export function isTextEditor(instance) { export function isTextEditor(instance) {
return instance instanceof TextEditor; return instance instanceof TextEditor;
} }
/**
* Returns true if the TextEditor is empty.
*
* @param {TextEditor} instance
* @returns {boolean}
*/
export function isEmpty(instance) {
if (isTextEditor(instance)) {
return instance.isEmpty;
}
throw new TypeError('Instance is not a TextEditor');
}
/** /**
* Returns the root element of a TextEditor * Returns the root element of a TextEditor
* instance. * instance.
@ -701,7 +716,7 @@ export function getRoot(instance) {
if (isTextEditor(instance)) { if (isTextEditor(instance)) {
return instance.root; return instance.root;
} }
return null; throw new TypeError("Instance is not a TextEditor");
} }
/** /**
@ -714,9 +729,9 @@ export function getRoot(instance) {
export function setRoot(instance, root) { export function setRoot(instance, root) {
if (isTextEditor(instance)) { if (isTextEditor(instance)) {
instance.root = root; instance.root = root;
return instance;
} }
throw new TypeError("Instance is not a TextEditor");
return instance;
} }
/** /**
@ -741,7 +756,7 @@ export function getCurrentStyle(instance) {
if (isTextEditor(instance)) { if (isTextEditor(instance)) {
return instance.currentStyle; return instance.currentStyle;
} }
return null; throw new TypeError("Instance is not a TextEditor");
} }
/** /**
@ -756,7 +771,7 @@ export function applyStylesToSelection(instance, styles) {
if (isTextEditor(instance)) { if (isTextEditor(instance)) {
return instance.applyStylesToSelection(styles); return instance.applyStylesToSelection(styles);
} }
return null; throw new TypeError("Instance is not a TextEditor");
} }
/** /**
@ -770,7 +785,7 @@ export function dispose(instance) {
if (isTextEditor(instance)) { if (isTextEditor(instance)) {
return instance.dispose(); return instance.dispose();
} }
return null; throw new TypeError("Instance is not a TextEditor");
} }
export default TextEditor; export default TextEditor;

View File

@ -54,8 +54,12 @@ export class ChangeController extends EventTarget {
return this.#hasPendingChanges; return this.#hasPendingChanges;
} }
/**
* Handles timeout.
*/
#onTimeout = () => { #onTimeout = () => {
this.dispatchEvent(new Event("change")); this.dispatchEvent(new Event("change"));
this.#hasPendingChanges = false;
}; };
/** /**