From 95a8b45415ef22b7216798f11c7331312724ebe6 Mon Sep 17 00:00:00 2001 From: vanelsas <58037137+avanelsas@users.noreply.github.com> Date: Tue, 12 May 2026 11:34:56 +0200 Subject: [PATCH 1/3] inline-edit: replace mutable JS cell with Clojure atom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The edit-state was a defonce #js {} accessed via unchecked-get/set — a second state cell outside app-state, invisible to devtools and indistinguishable from PLOP. Promotes it to a defonce atom holding a Clojure map; live DOM element refs ride in the map values. The serializable slice (:text-editing-id) is still mirrored to state/app-state under :ui, unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/bareforge/ui/inline_edit.cljs | 59 +++++++++++++++---------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/src/bareforge/ui/inline_edit.cljs b/src/bareforge/ui/inline_edit.cljs index f6d8182..ba8501a 100644 --- a/src/bareforge/ui/inline_edit.cljs +++ b/src/bareforge/ui/inline_edit.cljs @@ -43,11 +43,16 @@ ;; --- effectful ----------------------------------------------------------- +;; Live DOM element refs and the in-flight edit's mode/node-id. Kept in a +;; Clojure atom (not a JS object) so the cell is observable and the values +;; behave like Clojure data. The serializable slice (`:text-editing-id`) +;; is mirrored to `state/app-state` under `:ui` so the rest of the app can +;; react to it via the single-atom convention. (defonce ^:private edit-state - #js {:el nil :host nil :node-id nil :mode nil :target nil :saved-color nil}) + (atom {:el nil :host nil :node-id nil :mode nil :target nil :saved-color nil})) (defn- active? [] - (some? (unchecked-get edit-state "el"))) + (some? (:el @edit-state))) (defn- bcr->map [^js r] {:left (.-left r) :top (.-top r) :width (.-width r) :height (.-height r)}) @@ -62,9 +67,8 @@ (declare teardown!) (defn- on-input! [^js e] - (let [value (.. e -target -value) - node-id (unchecked-get edit-state "node-id") - mode (unchecked-get edit-state "mode")] + (let [value (.. e -target -value) + {:keys [node-id mode]} @edit-state] (commit-text-edit! node-id mode value))) (defn- on-keydown! [^js e] @@ -79,23 +83,16 @@ "Exit inline edit mode. Safe to call when not editing (no-op)." [] (when (active?) - (let [^js el (unchecked-get edit-state "el") - ^js host (unchecked-get edit-state "host") - ^js target (unchecked-get edit-state "target") - saved (unchecked-get edit-state "saved-color")] + (let [{:keys [^js el ^js host ^js target saved-color]} @edit-state] (.removeEventListener el "input" on-input!) (.removeEventListener el "keydown" on-keydown!) (.removeEventListener el "blur" on-blur!) (when (and host (.-parentNode el)) (.removeChild host el)) (when target - (set! (.. target -style -visibility) (or saved "")))) - (unchecked-set edit-state "el" nil) - (unchecked-set edit-state "host" nil) - (unchecked-set edit-state "node-id" nil) - (unchecked-set edit-state "mode" nil) - (unchecked-set edit-state "target" nil) - (unchecked-set edit-state "saved-color" nil) + (set! (.. target -style -visibility) (or saved-color "")))) + (reset! edit-state + {:el nil :host nil :node-id nil :mode nil :target nil :saved-color nil}) (state/assoc-ui! :text-editing-id nil))) (defn- apply-font-style! [^js textarea ^js target-el] @@ -140,21 +137,21 @@ (set! (.-value ta) (or (read-text mode node) "")) ;; Hide the element's rendered text so it doesn't show ;; through behind the textarea overlay. - (unchecked-set edit-state "saved-color" - (.. target-el -style -visibility)) - (set! (.. target-el -style -visibility) "hidden") - (.addEventListener ta "input" on-input!) - (.addEventListener ta "keydown" on-keydown!) - (.addEventListener ta "blur" on-blur!) - (.appendChild canvas-host ta) - (unchecked-set edit-state "el" ta) - (unchecked-set edit-state "host" canvas-host) - ;; Store the canonical doc id — on-input! commits a - ;; doc mutation (ops/set-text) and must address the - ;; template, not the clicked clone. - (unchecked-set edit-state "node-id" canonical) - (unchecked-set edit-state "mode" mode) - (unchecked-set edit-state "target" target-el) + (let [saved (.. target-el -style -visibility)] + (set! (.. target-el -style -visibility) "hidden") + (.addEventListener ta "input" on-input!) + (.addEventListener ta "keydown" on-keydown!) + (.addEventListener ta "blur" on-blur!) + (.appendChild canvas-host ta) + ;; Store the canonical doc id — on-input! commits a + ;; doc mutation (ops/set-text) and must address the + ;; template, not the clicked clone. + (reset! edit-state {:el ta + :host canvas-host + :node-id canonical + :mode mode + :target target-el + :saved-color saved})) (state/assoc-ui! :text-editing-id canonical) (.focus ta) (.select ta))))))) From 901d346640fd35877ac8698275e71d8bbbb8de7d Mon Sep 17 00:00:00 2001 From: vanelsas <58037137+avanelsas@users.noreply.github.com> Date: Tue, 12 May 2026 11:36:02 +0200 Subject: [PATCH 2/3] selection: install resize pointer listeners once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit start-resize! attached pointermove / pointerup on every drag and finish-resize! removed them — that worked but meant the listeners were churned per gesture, and a missed cleanup (hot reload, exception) would silently leak handlers. The pattern mirrors dnd/drag.cljs already: install once at mount, the handlers themselves early-exit on resize-active? until a drag begins. No behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/bareforge/render/selection.cljs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/bareforge/render/selection.cljs b/src/bareforge/render/selection.cljs index f573268..1e5393a 100644 --- a/src/bareforge/render/selection.cljs +++ b/src/bareforge/render/selection.cljs @@ -288,9 +288,7 @@ (unchecked-set resize-state "element" nil) (unchecked-set resize-state "node-id" nil) (when-let [overlay (overlay-el)] - (.removeAttribute overlay "data-resizing")) - (.removeEventListener js/window "pointermove" on-resize-move!) - (.removeEventListener js/window "pointerup" on-resize-up!)) + (.removeAttribute overlay "data-resizing"))) (defn- commit-resize! "Build the updated document for a resize commit. Free mode writes @@ -358,9 +356,7 @@ (unchecked-set resize-state "start-w" cur-w) (unchecked-set resize-state "start-h" cur-h) (when-let [overlay (overlay-el)] - (.setAttribute overlay "data-resizing" "")) - (.addEventListener js/window "pointermove" on-resize-move!) - (.addEventListener js/window "pointerup" on-resize-up!))))) + (.setAttribute overlay "data-resizing" "")))))) (defn- on-handle-pointerdown! [^js e] (let [^js target (.-target e) @@ -395,18 +391,21 @@ "Mount the selection overlay pool inside `canvas-host-el`. Seeds the pool with one primary overlay (handles attached), installs a watcher on `state/app-state` that fires on selection / document - changes, and wires a window resize listener for layout-shift - cases. Safe to call once at app startup." + changes, wires a window resize listener for layout-shift cases, and + attaches the resize-drag pointermove / pointerup listeners *once* + here — both handlers are inert (early-exit on `resize-active?`) + until a handle pointerdown starts a drag. Safe to call once at app + startup." [^js canvas-host-el] (unchecked-set overlay-state "host" canvas-host-el) (unchecked-set overlay-state "pool" #js []) (create-overlay! true) (schedule-refresh!) ;; Refresh on selection or document changes (an edit or a click moves - ;; the rect under us) AND on canvas-view changes (zoom + pan move - ;; the rendered nodes' visual rects, and overlays are read off - ;; getBoundingClientRect — without this the blue border stays put - ;; while the canvas slides under it). + ;; the rect under us) AND on canvas-view changes (zoom + pan move + ;; the rendered nodes' visual rects, and overlays are read off + ;; getBoundingClientRect — without this the blue border stays put + ;; while the canvas slides under it). (add-watch state/app-state ::selection-overlay (fn [_ _ old-state new-state] (when (or (not= (:selection old-state) (:selection new-state)) @@ -415,4 +414,6 @@ (state/canvas-view new-state))) (schedule-refresh!)))) (.addEventListener js/window "resize" - (fn [_] (schedule-refresh!)))) + (fn [_] (schedule-refresh!))) + (.addEventListener js/window "pointermove" on-resize-move!) + (.addEventListener js/window "pointerup" on-resize-up!)) From c3f27507c87b288bfa57ffd7f970b4d4e4dfb264 Mon Sep 17 00:00:00 2001 From: vanelsas <58037137+avanelsas@users.noreply.github.com> Date: Tue, 12 May 2026 11:59:48 +0200 Subject: [PATCH 3/3] Revert "inline-edit: replace mutable JS cell with Clojure atom" This reverts commit 95a8b45415ef22b7216798f11c7331312724ebe6. --- src/bareforge/ui/inline_edit.cljs | 59 ++++++++++++++++--------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/src/bareforge/ui/inline_edit.cljs b/src/bareforge/ui/inline_edit.cljs index ba8501a..f6d8182 100644 --- a/src/bareforge/ui/inline_edit.cljs +++ b/src/bareforge/ui/inline_edit.cljs @@ -43,16 +43,11 @@ ;; --- effectful ----------------------------------------------------------- -;; Live DOM element refs and the in-flight edit's mode/node-id. Kept in a -;; Clojure atom (not a JS object) so the cell is observable and the values -;; behave like Clojure data. The serializable slice (`:text-editing-id`) -;; is mirrored to `state/app-state` under `:ui` so the rest of the app can -;; react to it via the single-atom convention. (defonce ^:private edit-state - (atom {:el nil :host nil :node-id nil :mode nil :target nil :saved-color nil})) + #js {:el nil :host nil :node-id nil :mode nil :target nil :saved-color nil}) (defn- active? [] - (some? (:el @edit-state))) + (some? (unchecked-get edit-state "el"))) (defn- bcr->map [^js r] {:left (.-left r) :top (.-top r) :width (.-width r) :height (.-height r)}) @@ -67,8 +62,9 @@ (declare teardown!) (defn- on-input! [^js e] - (let [value (.. e -target -value) - {:keys [node-id mode]} @edit-state] + (let [value (.. e -target -value) + node-id (unchecked-get edit-state "node-id") + mode (unchecked-get edit-state "mode")] (commit-text-edit! node-id mode value))) (defn- on-keydown! [^js e] @@ -83,16 +79,23 @@ "Exit inline edit mode. Safe to call when not editing (no-op)." [] (when (active?) - (let [{:keys [^js el ^js host ^js target saved-color]} @edit-state] + (let [^js el (unchecked-get edit-state "el") + ^js host (unchecked-get edit-state "host") + ^js target (unchecked-get edit-state "target") + saved (unchecked-get edit-state "saved-color")] (.removeEventListener el "input" on-input!) (.removeEventListener el "keydown" on-keydown!) (.removeEventListener el "blur" on-blur!) (when (and host (.-parentNode el)) (.removeChild host el)) (when target - (set! (.. target -style -visibility) (or saved-color "")))) - (reset! edit-state - {:el nil :host nil :node-id nil :mode nil :target nil :saved-color nil}) + (set! (.. target -style -visibility) (or saved "")))) + (unchecked-set edit-state "el" nil) + (unchecked-set edit-state "host" nil) + (unchecked-set edit-state "node-id" nil) + (unchecked-set edit-state "mode" nil) + (unchecked-set edit-state "target" nil) + (unchecked-set edit-state "saved-color" nil) (state/assoc-ui! :text-editing-id nil))) (defn- apply-font-style! [^js textarea ^js target-el] @@ -137,21 +140,21 @@ (set! (.-value ta) (or (read-text mode node) "")) ;; Hide the element's rendered text so it doesn't show ;; through behind the textarea overlay. - (let [saved (.. target-el -style -visibility)] - (set! (.. target-el -style -visibility) "hidden") - (.addEventListener ta "input" on-input!) - (.addEventListener ta "keydown" on-keydown!) - (.addEventListener ta "blur" on-blur!) - (.appendChild canvas-host ta) - ;; Store the canonical doc id — on-input! commits a - ;; doc mutation (ops/set-text) and must address the - ;; template, not the clicked clone. - (reset! edit-state {:el ta - :host canvas-host - :node-id canonical - :mode mode - :target target-el - :saved-color saved})) + (unchecked-set edit-state "saved-color" + (.. target-el -style -visibility)) + (set! (.. target-el -style -visibility) "hidden") + (.addEventListener ta "input" on-input!) + (.addEventListener ta "keydown" on-keydown!) + (.addEventListener ta "blur" on-blur!) + (.appendChild canvas-host ta) + (unchecked-set edit-state "el" ta) + (unchecked-set edit-state "host" canvas-host) + ;; Store the canonical doc id — on-input! commits a + ;; doc mutation (ops/set-text) and must address the + ;; template, not the clicked clone. + (unchecked-set edit-state "node-id" canonical) + (unchecked-set edit-state "mode" mode) + (unchecked-set edit-state "target" target-el) (state/assoc-ui! :text-editing-id canonical) (.focus ta) (.select ta)))))))