(ns outliner.view.focus (:require [outliner.model.state :as state] [outliner.control.dispatch :as dispatch] [outliner.boundaries.dom.caret :as caret] [outliner.boundaries.dom.ui-utils :as ui-utils])) (defn focus-locked? [] (let [until (get @state/sys-state :focus-lock-until 0)] (< (.getTime (js/Date.)) until))) (defn currently-focused-editor [] (js/console.log "currently-focused-editor " (:editor-el (ui-utils/get-element-info js/document.activeElement))) (:editor-el (ui-utils/get-element-info js/document.activeElement))) (defn maybe-clear-focus [id-to-blur] (let [view-state @state/view-state sys-state @state/sys-state] ;; When an editor element is blurred, we check after a short delay if focus ;; has moved to another editor. This is necessary because when a node is ;; re-ordered in the DOM (like during indent/outdent), the browser blurs the ;; element being removed. If we immediately cleared the focus state, the ;; 'manage-focus' logic (which runs on the new element's :ref) would find a ;; nil 'focused-id' and fail to restore focus/caret position. ;; ;; By waiting for the next tick, we can verify if focus has settled on a new ;; editor for the same node. We only clear the global focus if: ;; 1. The application state still considers 'id-to-blur' as the focused node. ;; 2. No other editor element in the DOM currently has focus. (js/console.log "maybe-clear-focus " id-to-blur) (if (and (= id-to-blur (:focused-id view-state)) (not (#{:pending-sync :syncing} (:sync-status sys-state))) (not (:preserved-caret-position view-state)) (not (:preserved-selection view-state)) (not (focus-locked?)) (not (currently-focused-editor)) (js/document.hasFocus)) (do (js/console.log "dispatch! [:outliner.command/focus nil]") (dispatch/dispatch! [:outliner.command/focus nil])) (do (js/console.log "no need to clear focus " (js->clj (.-dataset js/document.activeElement))) (.focus js/document.activeElement))))) (defn manage-focus [el id text] (let [{:keys [focused-id preserved-caret-position preserved-selection]} @state/view-state] (when (and el (.-isConnected el) (= id focused-id)) (js/console.log "manage-focus " el id focused-id) (let [already-focused? (= js/document.activeElement el) sel (js/window.getSelection) has-selection? (and sel (pos? (.-rangeCount sel)) (caret/range-in-element? el (.getRangeAt sel 0)))] (cond preserved-selection (let [current-sel (caret/get-selection-offsets el)] (when (not= current-sel preserved-selection) (when-not already-focused? (.focus el)) (caret/set-selection-range el (:start preserved-selection) (:end preserved-selection))) (state/clear-preserved-selection!)) (some? preserved-caret-position) (let [current-pos (caret/get-caret-pos el)] (when (not= current-pos preserved-caret-position) (js/console.log "focus on preserved-cared-position" id preserved-caret-position) (when-not already-focused? (.focus el)) (js/requestAnimationFrame #(caret/set-caret-pos el preserved-caret-position))) (state/clear-preserved-caret-position!)) (or (not already-focused?) (not has-selection?)) (do (when-not already-focused? (.focus el)) (js/requestAnimationFrame #(caret/set-caret-pos el (count text))))) (caret/scroll-to-selection)))))