(ns outliner.control.dispatch (:require [outliner.model.state :as state] [outliner.control.commands :as commands] [outliner.control.system :as system] [outliner.boundaries.dom.caret :as caret])) (defn- prepare-view "Prepares the view state for commands that need caret preservation. If the command is marked with ':preserve-caret-position?', captures the current selection/caret from the DOM." [cmd config curr-view] (if (:preserve-caret-position? config) (let [id (second cmd) el (when id (js/document.querySelector (str "[data-node-id='" id "'].node-text"))) sel (when el (caret/get-selection-offsets el))] (cond-> curr-view id (assoc :focused-id id) (and sel (not= (:start sel) (:end sel))) (assoc :preserved-selection sel) (and sel (= (:start sel) (:end sel))) (assoc :preserved-caret-position (:start sel)))) curr-view)) (defn- update-location-history "Records location history if focus/zoom changed, and not going back. Implements an 'auto-pop' behavior to skip transient states." [op curr-view view sys] (let [old-loc (select-keys curr-view [:focused-id :zoom-id :preserved-caret-position]) new-loc (select-keys view [:focused-id :zoom-id :preserved-caret-position]) old-loc-base (select-keys old-loc [:focused-id :zoom-id]) new-loc-base (select-keys new-loc [:focused-id :zoom-id])] (cond (= op :outliner.command/go-back) sys (= new-loc-base (select-keys (peek (:location-history sys)) [:focused-id :zoom-id])) (update sys :location-history pop) (not= old-loc-base new-loc-base) (update sys :location-history (fn [h] (conj (vec (take-last 50 h)) old-loc))) :else sys))) (defn- apply-focus-lock "Applies a focus lock to the system state for structural or history commands." [config sys] (let [is-structural? (or (:preserve-caret-position? config) (:history? config) (:structural? config))] (if is-structural? (assoc sys :focus-lock-until (+ (.getTime (js/Date.)) 500)) sys))) (defn- apply-state-updates! "Applies the new document, view, and system states to atoms and handles side effects." [doc view sys results] (let [curr-doc @state/doc-state curr-view @state/view-state curr-sys @state/sys-state] (when (not= curr-doc doc) (reset! state/doc-state doc)) (when (not= curr-view view) (reset! state/view-state view)) (when (not= curr-sys sys) (reset! state/sys-state sys)) ;; Handle side effects returned by commands (when-let [expand-id (:on-expand results)] (system/handle-node-expanded doc sys expand-id)))) (defn dispatch! [cmd] (let [op (first cmd) config (get commands/command-registry op) curr-doc @state/doc-state curr-view @state/view-state curr-sys @state/sys-state prepared-view (prepare-view cmd config curr-view) results (commands/process-command curr-doc prepared-view curr-sys cmd) {:keys [doc view sys]} results new-sys-final (->> sys (apply-focus-lock config) (update-location-history op curr-view view))] (apply-state-updates! doc view new-sys-final results)))