From a28d47f437824b4b62624de6192970f7d678198f Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 19 Mar 2026 11:05:29 +0100 Subject: [PATCH] :bug: Fix embedded editor pasting text --- .../app/main/data/workspace/clipboard.cljs | 37 ++-- .../src/app/main/data/workspace/shapes.cljs | 70 ++++---- .../app/main/data/workspace/wasm_text.cljs | 162 +++++++++++------- 3 files changed, 161 insertions(+), 108 deletions(-) diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index 279666db33..8c1b8021a0 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -41,6 +41,7 @@ [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.texts :as dwtxt] [app.main.data.workspace.undo :as dwu] + [app.main.data.workspace.wasm-text :as dwwt] [app.main.errors] [app.main.features :as features] [app.main.refs :as refs] @@ -970,10 +971,11 @@ text (.-textContent root) content (tc/dom->cljs root)] (when (types.text/valid-content? content) - (let [id (uuid/next) - width (max 8 (min (* 7 (count text)) 700)) - height 16 + (let [id (uuid/next) + width (max 8 (min (* 7 (count text)) 700)) + height 16 {:keys [x y]} (calculate-paste-position state) + skip-edition? (features/active-feature? state "text-editor-wasm/v1") shape {:id id :type :text @@ -985,9 +987,14 @@ :grow-type (if (> (count text) 100) :auto-height :auto-width) :content content} undo-id (js/Symbol)] - (rx/of (dwu/start-undo-transaction undo-id) - (dwsh/create-and-add-shape :text x y shape) - (dwu/commit-undo-transaction undo-id)))))))) + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id) + (dwsh/create-and-add-shape :text x y shape + (when skip-edition? {:skip-edition? true}))) + (if skip-edition? + (rx/of (dwwt/resize-wasm-text-debounce id {:undo-group id + :undo-id undo-id})) + (rx/of (dwu/commit-undo-transaction undo-id)))))))))) (defn- paste-text [text] @@ -995,10 +1002,11 @@ (ptk/reify ::paste-text ptk/WatchEvent (watch [_ state _] - (let [id (uuid/next) - width (max 8 (min (* 7 (count text)) 700)) - height 16 + (let [id (uuid/next) + width (max 8 (min (* 7 (count text)) 700)) + height 16 {:keys [x y]} (calculate-paste-position state) + skip-edition? (features/active-feature? state "text-editor-wasm/v1") shape {:id id :type :text @@ -1011,9 +1019,14 @@ :content (as-content text)} undo-id (js/Symbol)] - (rx/of (dwu/start-undo-transaction undo-id) - (dwsh/create-and-add-shape :text x y shape) - (dwu/commit-undo-transaction undo-id)))))) + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id) + (dwsh/create-and-add-shape :text x y shape + (when skip-edition? {:skip-edition? true}))) + (if skip-edition? + (rx/of (dwwt/resize-wasm-text-debounce id {:undo-group id + :undo-id undo-id})) + (rx/of (dwu/commit-undo-transaction undo-id)))))))) ;; TODO: why not implement it in terms of upload-media-workspace? (defn- paste-svg-text diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 989e85eb38..022dd11d21 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -109,7 +109,7 @@ (defn add-shape ([shape] (add-shape shape {})) - ([shape {:keys [no-select? no-update-layout?]}] + ([shape {:keys [no-select? no-update-layout? skip-edition?]}] (cts/check-shape shape) @@ -139,7 +139,11 @@ (js/Symbol) parent-type - (cfh/get-shape-type objects (:parent-id shape))] + (cfh/get-shape-type objects (:parent-id shape)) + + ;; Skip edition when using embedded editor (v3) and shape already has content (e.g. paste) + start-edition? (and (cfh/text-shape? shape) + (not (and skip-edition? (some? (:content shape)))))] (rx/concat (rx/of (dwu/start-undo-transaction undo-id) @@ -149,7 +153,7 @@ (when-not no-select? (dws/select-shapes (d/ordered-set (:id shape)))) (dwu/commit-undo-transaction undo-id)) - (when (cfh/text-shape? shape) + (when start-edition? (->> (rx/of (dwe/start-edition-mode (:id shape))) (rx/observe-on :async))) @@ -217,40 +221,42 @@ (dwu/commit-undo-transaction undo-id))))))) (defn create-and-add-shape - [type frame-x frame-y {:keys [width height] :as attrs}] - (ptk/reify ::create-and-add-shape - ptk/WatchEvent - (watch [_ state _] - (let [vbc (dsh/get-viewport-center state) - x (:x attrs (- (:x vbc) (/ width 2))) - y (:y attrs (- (:y vbc) (/ height 2))) - page-id (:current-page-id state) - objects (dsh/lookup-page-objects state page-id) - frame-id (-> (dsh/lookup-page-objects state page-id) - (ctst/top-nested-frame {:x frame-x :y frame-y})) + ([type frame-x frame-y attrs] + (create-and-add-shape type frame-x frame-y attrs nil)) + ([type frame-x frame-y {:keys [width height] :as attrs} {:keys [skip-edition?]}] + (ptk/reify ::create-and-add-shape + ptk/WatchEvent + (watch [_ state _] + (let [vbc (dsh/get-viewport-center state) + x (:x attrs (- (:x vbc) (/ width 2))) + y (:y attrs (- (:y vbc) (/ height 2))) + page-id (:current-page-id state) + objects (dsh/lookup-page-objects state page-id) + frame-id (-> (dsh/lookup-page-objects state page-id) + (ctst/top-nested-frame {:x frame-x :y frame-y})) - selected (dsh/lookup-selected state) - base (cfh/get-base-shape objects selected) + selected (dsh/lookup-selected state) + base (cfh/get-base-shape objects selected) - parent-id (if (or (and (= 1 (count selected)) - (cfh/frame-shape? (get objects (first selected)))) - (empty? selected)) - frame-id - (:parent-id base)) + parent-id (if (or (and (= 1 (count selected)) + (cfh/frame-shape? (get objects (first selected)))) + (empty? selected)) + frame-id + (:parent-id base)) - ;; If the parent-id or the frame-id are component-copies, we need to get the first not copy parent - parent-id (:id (ctn/get-first-valid-parent objects parent-id)) ;; We don't want to change the structure of component copies - frame-id (:id (ctn/get-first-valid-parent objects frame-id)) + ;; If the parent-id or the frame-id are component-copies, we need to get the first not copy parent + parent-id (:id (ctn/get-first-valid-parent objects parent-id)) ;; We don't want to change the structure of component copies + frame-id (:id (ctn/get-first-valid-parent objects frame-id)) - shape (cts/setup-shape - (-> attrs - (assoc :type type) - (assoc :x x) - (assoc :y y) - (assoc :frame-id frame-id) - (assoc :parent-id parent-id)))] + shape (cts/setup-shape + (-> attrs + (assoc :type type) + (assoc :x x) + (assoc :y y) + (assoc :frame-id frame-id) + (assoc :parent-id parent-id)))] - (rx/of (add-shape shape)))))) + (rx/of (add-shape shape {:skip-edition? skip-edition?}))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Artboard diff --git a/frontend/src/app/main/data/workspace/wasm_text.cljs b/frontend/src/app/main/data/workspace/wasm_text.cljs index c43cee567f..c175b46bdf 100644 --- a/frontend/src/app/main/data/workspace/wasm_text.cljs +++ b/frontend/src/app/main/data/workspace/wasm_text.cljs @@ -17,6 +17,7 @@ [app.common.types.modifiers :as ctm] [app.main.data.helpers :as dsh] [app.main.data.workspace.modifiers :as dwm] + [app.main.data.workspace.undo :as dwu] [app.render-wasm.api :as wasm.api] [app.render-wasm.api.fonts :as wasm.fonts] [beicon.v2.core :as rx] @@ -76,82 +77,115 @@ (rx/empty)))))) (defn resize-wasm-text-debounce-commit - [] - (ptk/reify ::resize-wasm-text-debounce-commit - ptk/WatchEvent - (watch [_ state _] - (let [ids (get state ::resize-wasm-text-debounce-ids) - objects (dsh/lookup-page-objects state) + ([] + (resize-wasm-text-debounce-commit nil nil)) + ([undo-group undo-id] + (ptk/reify ::resize-wasm-text-debounce-commit + ptk/WatchEvent + (watch [_ state _] + (let [ids (get state ::resize-wasm-text-debounce-ids) + objects (dsh/lookup-page-objects state) - modifiers - (reduce - (fn [modifiers id] - (let [shape (get objects id)] - (cond-> modifiers - (and (some? shape) - (cfh/text-shape? shape) - (not= :fixed (:grow-type shape))) - (merge (resize-wasm-text-modifiers shape))))) - {} - ids)] - (if (not (empty? modifiers)) - (rx/of (dwm/apply-wasm-modifiers modifiers)) - (rx/empty)))))) + modifiers + (reduce + (fn [modifiers id] + (let [shape (get objects id)] + (cond-> modifiers + (and (some? shape) + (cfh/text-shape? shape) + (not= :fixed (:grow-type shape))) + (merge (resize-wasm-text-modifiers shape))))) + {} + ids) + + ;; When undo-id is present, extend the current undo transaction instead of + ;; creating a new one, and commit it after the resize (single undo action). + extend-tx? (some? undo-id) + apply-opts (cond-> {} + (some? undo-group) (assoc :undo-group undo-group) + extend-tx? (assoc :undo-transation? false))] + (cond + (not (empty? modifiers)) + (if extend-tx? + (rx/concat + (rx/of (dwm/apply-wasm-modifiers modifiers apply-opts)) + (rx/of (dwu/commit-undo-transaction undo-id))) + (rx/of (dwm/apply-wasm-modifiers modifiers apply-opts))) + + extend-tx? + ;; No resize needed (e.g. :fixed grow-type) but we must commit the add + (rx/of (dwu/commit-undo-transaction undo-id)) + + :else + (rx/empty))))))) ;; This event will debounce the resize events so, if there are many, they ;; are processed at the same time and not one-by-one. This will improve ;; performance because it's better to make only one layout calculation instead ;; of (potentialy) hundreds. (defn resize-wasm-text-debounce-inner - [id] - (let [cur-event (js/Symbol)] - (ptk/reify ::resize-wasm-text-debounce-inner - ptk/UpdateEvent - (update [_ state] - (-> state - (update ::resize-wasm-text-debounce-ids (fnil conj []) id) - (cond-> (nil? (::resize-wasm-text-debounce-event state)) - (assoc ::resize-wasm-text-debounce-event cur-event)))) + ([id] + (resize-wasm-text-debounce-inner id nil)) + ([id {:keys [undo-group undo-id]}] + (let [cur-event (js/Symbol)] + (ptk/reify ::resize-wasm-text-debounce-inner + ptk/UpdateEvent + (update [_ state] + (-> state + (update ::resize-wasm-text-debounce-ids (fnil conj []) id) + (cond-> (nil? (::resize-wasm-text-debounce-event state)) + (assoc ::resize-wasm-text-debounce-event cur-event)))) - ptk/WatchEvent - (watch [_ state stream] - (if (= (::resize-wasm-text-debounce-event state) cur-event) - (let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))] - (rx/concat - (rx/merge - (->> stream - (rx/filter (ptk/type? ::resize-wasm-text-debounce-inner)) - (rx/debounce 40) - (rx/take 1) - (rx/map #(resize-wasm-text-debounce-commit)) - (rx/take-until stopper)) - (rx/of (resize-wasm-text-debounce-inner id))) - (rx/of #(dissoc % - ::resize-wasm-text-debounce-ids - ::resize-wasm-text-debounce-event)))) - (rx/empty)))))) + ptk/WatchEvent + (watch [_ state stream] + (if (= (::resize-wasm-text-debounce-event state) cur-event) + (let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))] + (rx/concat + (rx/merge + (->> stream + (rx/filter (ptk/type? ::resize-wasm-text-debounce-inner)) + (rx/debounce 40) + (rx/take 1) + (rx/map (fn [evt] + (resize-wasm-text-debounce-commit + (some-> evt meta :undo-group) + (some-> evt meta :undo-id)))) + (rx/take-until stopper)) + (rx/of (with-meta + (resize-wasm-text-debounce-inner id) + {:undo-group undo-group :undo-id undo-id}))) + (rx/of #(dissoc % + ::resize-wasm-text-debounce-ids + ::resize-wasm-text-debounce-event)))) + (rx/empty))))))) (defn resize-wasm-text-debounce - [id] - (ptk/reify ::resize-wasm-text-debounce - ptk/WatchEvent - (watch [_ state _] - (let [page-id (:current-page-id state) - objects (dsh/lookup-page-objects state page-id) - content (dm/get-in objects [id :content]) - fonts (wasm.fonts/get-content-fonts content) + ([id] + (resize-wasm-text-debounce id nil)) + ([id {:keys [undo-group undo-id] :as opts}] + (ptk/reify ::resize-wasm-text-debounce + ptk/WatchEvent + (watch [_ state _] + (let [page-id (:current-page-id state) + objects (dsh/lookup-page-objects state page-id) + content (dm/get-in objects [id :content]) + fonts (wasm.fonts/get-content-fonts content) - fonts-loaded? - (->> fonts - (every? - (fn [font] - (let [font-data (wasm.fonts/make-font-data font)] - (wasm.fonts/font-stored? font-data (:emoji? font-data))))))] + fonts-loaded? + (->> fonts + (every? + (fn [font] + (let [font-data (wasm.fonts/make-font-data font)] + (wasm.fonts/font-stored? font-data (:emoji? font-data))))))] - (if (not fonts-loaded?) - (->> (rx/of (resize-wasm-text-debounce id)) - (rx/delay 20)) - (rx/of (resize-wasm-text-debounce-inner id))))))) + (if (not fonts-loaded?) + (->> (rx/of (resize-wasm-text-debounce id opts)) + (rx/delay 20)) + (let [pass-opts (when (or (some? undo-group) (some? undo-id)) + (cond-> {} + (some? undo-group) (assoc :undo-group undo-group) + (some? undo-id) (assoc :undo-id undo-id)))] + (rx/of (resize-wasm-text-debounce-inner id pass-opts))))))))) (defn resize-wasm-text-all "Resize all text shapes (auto-width/auto-height) from a collection of ids."