🐛 Fix workspace crash and cleanup viewport_ref event/resize handling (#10721)

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
This commit is contained in:
Andrey Antukh 2026-07-27 12:11:12 +02:00 committed by GitHub
parent 5a0cee44b1
commit af120feb1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 56 additions and 44 deletions

View File

@ -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))))

View File

@ -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]