From af120feb1f86bffeb1abcea2f9b0717e3c338e82 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 27 Jul 2026 12:11:12 +0200 Subject: [PATCH] :bug: Fix workspace crash and cleanup viewport_ref event/resize handling (#10721) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace `globals/document` and `globals/window` with `js/document` and `js/window` in workspace.cljs, removing the unused `app.util.globals` import. This avoids "can't access dead object" errors in Firefox when navigating between pages/files, matching the existing pattern used in viewport/hooks.cljs. Fix a leaked MOUSELEAVE listener in viewport_ref.cljs — the ref callback added a new listener on every mount but never unregistered the previous one. Now uses standard .addEventListener/.removeEventListener with a React ref to track the handler for proper cleanup. Fix ResizeObserver cleanup in viewport_ref.cljs — `init-observer` is now a private function that only creates an observer when a node is provided, and cleanup is handled via the ref callback on unmount. AI-assisted-by: mimo-v2.5-pro --- frontend/src/app/main/ui/workspace.cljs | 5 +- .../ui/workspace/viewport/viewport_ref.cljs | 95 +++++++++++-------- 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index e7a3e7b119..124eee9b1b 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -44,7 +44,6 @@ [app.main.ui.workspace.webgl-unavailable-modal] [app.util.debug :as dbg] [app.util.dom :as dom] - [app.util.globals :as globals] [app.util.i18n :as i18n :refer [tr]] [goog.events :as events] [okulary.core :as l] @@ -177,7 +176,7 @@ (mf/with-effect [] (let [focus-out #(st/emit! (dw/workspace-focus-lost)) - key (events/listen globals/window "blur" focus-out)] + key (events/listen js/window "blur" focus-out)] (partial events/unlistenByKey key))) (mf/with-effect [file-id page-id] @@ -252,7 +251,7 @@ (let [handle-wasm-render (fn [_] (reset! first-frame-rendered? true)) - listener-key (events/listen globals/document "penpot:wasm:render" handle-wasm-render)] + listener-key (events/listen js/document "penpot:wasm:render" handle-wasm-render)] (fn [] (events/unlistenByKey listener-key)))) diff --git a/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs b/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs index 46e1356596..38349f8f24 100644 --- a/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs @@ -7,62 +7,75 @@ (ns app.main.ui.workspace.viewport.viewport-ref (:require [app.common.data :as d] - [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.main.refs :as refs] [app.main.store :as st] [app.util.dom :as dom] [app.util.mouse :as mse] - [goog.events :as events] - [rumext.v2 :as mf]) - (:import goog.events.EventType)) + [rumext.v2 :as mf])) (defonce viewport-ref (atom nil)) -(defonce current-observer (atom nil)) (defonce viewport-brect (atom nil)) -(defn init-observer - [node on-change-bounds] +(defn- init-observer + [node] + (let [on-change-bounds + (fn [_] + (let [brect (dom/get-bounding-rect node) + brect (gpt/point (d/parse-integer (:left brect)) + (d/parse-integer (:top brect)))] + (reset! viewport-brect brect))) - (let [observer (js/ResizeObserver. on-change-bounds)] - (when (some? @current-observer) - (.disconnect @current-observer)) + observer + (js/ResizeObserver. on-change-bounds)] - (reset! current-observer observer) - - (when (some? node) - (.observe observer node)))) - -(defn on-change-bounds - [_] - (when @viewport-ref - (let [brect (dom/get-bounding-rect @viewport-ref) - brect (gpt/point (d/parse-integer (:left brect)) - (d/parse-integer (:top brect)))] - (reset! viewport-brect brect)))) + (.observe observer node) + observer)) (defn create-viewport-ref [] - (let [ref (mf/use-ref nil)] - [ref - (mf/use-memo - #(fn [node] - (mf/set-ref-val! ref node) - (reset! viewport-ref node) - (when (some? node) - (events/listen node EventType.MOUSELEAVE (fn [] (st/emit! (mse/->BlurEvent))))) - (init-observer node on-change-bounds)))])) + (let [node-ref (mf/use-ref nil) + handler-ref (mf/use-ref nil) + observer-ref (mf/use-ref nil) + callback (mf/use-fn + (fn [node] + ;; Dispose all previous resources + (when-let [observer (mf/ref-val observer-ref)] + (.disconnect ^js observer) + (mf/set-ref-val! observer-ref nil)) + + + (when-let [handler (mf/ref-val handler-ref)] + (when-let [node (mf/ref-val node-ref)] + (.removeEventListener ^js node "mouseleave" handler) + (mf/set-ref-val! handler-ref nil))) + + ;; Reset the ref values to the current node (can be nil) + (mf/set-ref-val! node-ref node) + (reset! viewport-ref node) + + (when (some? node) + (let [handler (fn [] (st/emit! (mse/->BlurEvent))) + observer (init-observer node)] + (.addEventListener ^js node "mouseleave" handler) + + (mf/set-ref-val! handler-ref handler) + (mf/set-ref-val! observer-ref observer)))))] + [node-ref callback])) (defn point->viewport [pt] - (let [zoom (dm/get-in @st/state [:workspace-local :zoom] 1)] - (when (and (some? @viewport-ref) - (some? @viewport-brect)) - (let [vbox (.. ^js @viewport-ref -viewBox -baseVal) - brect @viewport-brect - box (gpt/point (.-x vbox) (.-y vbox)) - zoom (gpt/point zoom)] + (let [zoom (d/nilv @refs/selected-zoom 1) + viewport-node @viewport-ref + viewport-brect @viewport-brect] - (-> (gpt/subtract pt brect) + (when (and (some? viewport-brect) + (some? viewport-node)) + (let [vbox (.. ^js viewport-node -viewBox -baseVal) + box (gpt/point (.-x vbox) (.-y vbox)) + zoom (gpt/point zoom)] + + (-> (gpt/subtract pt viewport-brect) (gpt/divide zoom) (gpt/add box)))))) @@ -71,8 +84,8 @@ Unlike point->viewport, this does NOT convert to canvas coordinates - it just subtracts the viewport's bounding rect offset." [pt] - (when (some? @viewport-brect) - (gpt/subtract pt @viewport-brect))) + (when-let [brect @viewport-brect] + (gpt/subtract pt brect))) (defn inside-viewport? [target]