diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 9ce9e18d78..5525364ee3 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -19,6 +19,7 @@ [app.main.store :as st] [app.main.ui.components.search-bar :refer [search-bar*]] [app.main.ui.components.title-bar :refer [title-bar*]] + [app.main.ui.context :as ctx] [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.controls.input :refer [input*]] @@ -260,18 +261,30 @@ [:span {:class (stl/css :radio-text)} text]]) +(def ^:private default-search-state + {:show-search false + :find-replace-mode? false + :search-scope :layers + :show-menu false + :search-text "" + :replace-text "" + :filters #{} + :num-items 100 + :current-match-idx 0}) + +;; Per-file, session-scoped (in-memory only) so an open search and its +;; filters survive switching between the sidebar tabs, which unmounts the +;; layers panel, without leaking across files or persisting across reloads. +(defonce ^:private session-search* + (atom {})) + (defn use-search [page objects] - (let [state* (mf/use-state - #(do {:show-search false - :find-replace-mode? false - :search-scope :layers - :show-menu false - :search-text "" - :replace-text "" - :filters #{} - :num-items 100 - :current-match-idx 0})) + (let [file-id (mf/use-ctx ctx/current-file-id) + state* (mf/use-state + #(-> default-search-state + (merge (get @session-search* file-id)) + (assoc :show-menu false))) layers-search (mf/deref ref:layers-search) state (deref state*) current-filters (:filters state) @@ -522,6 +535,9 @@ (events/unlistenByKey key1) (events/unlistenByKey key2)))) + (mf/with-effect [file-id state] + (swap! session-search* assoc file-id state)) + (mf/with-effect [layers-search] (if-let [{:keys [open? find-replace-mode? scope]} layers-search] (when open? @@ -821,6 +837,8 @@ [filtered-objects show-more filter-component] (use-search page objects) + searching? (some? filtered-objects) + intersection-callback (fn [entries] (when (and (.-isIntersecting (first entries)) (some? show-more)) @@ -864,8 +882,12 @@ (mf/use-fn #(st/emit! (dw/toggle-focus-mode)))] - (sc/use-restore-scroll scroll-store :layers page-id tree-ref) - (sc/use-restore-scroll scroll-store :layers-search page-id search-ref) + (sc/use-restore-scroll scroll-store :layers page-id tree-ref (not searching?)) + (sc/use-restore-scroll scroll-store :layers-search page-id search-ref searching?) + + (mf/with-effect [page-id searching?] + (when-not ^boolean searching? + (sc/forget-scroll! scroll-store [:layers-search page-id]))) [:div {:id "layers" :class (stl/css :layers) diff --git a/frontend/src/app/main/ui/workspace/sidebar/scroll.cljs b/frontend/src/app/main/ui/workspace/sidebar/scroll.cljs index 235e29dec5..1902c9798f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/scroll.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/scroll.cljs @@ -27,6 +27,12 @@ (when-let [target (dom/get-target event)] (swap! store* assoc k (.-scrollTop target)))) +(defn forget-scroll! + "Drops the position saved under k, so the next restore starts at the + top. Plain swap!, never triggers renders." + [store* k] + (swap! store* dissoc k)) + (defn needs-restore-retry? "Pure decision for the restore loop: keep retrying while the content height is still changing (lazily rendered lists) and attempts remain." @@ -59,12 +65,19 @@ panel or id change. Retries while the content height keeps changing so deep positions in lazily rendered lists are not clamped to the first chunk. With no saved position the node goes to the top. Missing node is - a silent no-op." - [store* panel id node-ref] - (mf/with-effect [panel id] - (let [raf* (volatile! nil)] - (when-let [node (mf/ref-val node-ref)] - (restore-position! node (get @store* [panel id]) raf*)) - (fn [] - (when-some [raf @raf*] - (tm/cancel-af! raf)))))) + a silent no-op. + + `active?` is for panels that swap lists in place: pass whether the list + behind node-ref is shown, so its position is restored again each time + it comes back. Nothing is restored while it is false." + ([store* panel id node-ref] + (use-restore-scroll store* panel id node-ref true)) + ([store* panel id node-ref active?] + (mf/with-effect [panel id active?] + (let [raf* (volatile! nil)] + (when ^boolean active? + (when-let [node (mf/ref-val node-ref)] + (restore-position! node (get @store* [panel id]) raf*))) + (fn [] + (when-some [raf @raf*] + (tm/cancel-af! raf))))))) diff --git a/frontend/test/frontend_tests/ui/sidebar_scroll_test.cljs b/frontend/test/frontend_tests/ui/sidebar_scroll_test.cljs index 609ed6c35b..a1c8febc7b 100644 --- a/frontend/test/frontend_tests/ui/sidebar_scroll_test.cljs +++ b/frontend/test/frontend_tests/ui/sidebar_scroll_test.cljs @@ -31,6 +31,14 @@ (sc/save-scroll! store [:layers :page-1] #js {}) (t/is (= {} @store)))) +(t/deftest forget-scroll-drops-only-that-key + (let [store (atom {[:layers :page-1] 250 + [:layers-search :page-1] 90})] + (sc/forget-scroll! store [:layers-search :page-1]) + (t/is (= {[:layers :page-1] 250} @store)) + (sc/forget-scroll! store [:layers-search :page-1]) + (t/is (= {[:layers :page-1] 250} @store)))) + (t/deftest restore-scroll-writes-saved-position (let [node #js {:scrollTop 0}] (sc/restore-scroll! node 320)