Files
logseq/src/main/frontend/util/cursor.cljs
2023-05-12 13:24:54 -04:00

273 lines
8.5 KiB
Clojure

(ns ^:no-doc frontend.util.cursor
(:require [cljs-bean.core :as bean]
[clojure.string :as string]
[frontend.util :as util]
[goog.dom :as gdom]
[goog.object :as gobj]))
(defn- closer [a b c]
(let [a-left (or (:left a) 0)
b-left (:left b)
c-left (or (:left c) js/Number.MAX_SAFE_INTEGER)]
(if (< (- b-left a-left) (- c-left b-left))
a
c)))
(defn mock-char-pos [e]
{:left (.-offsetLeft e)
:top (.-offsetTop e)
:pos (-> (.-id e)
(string/split "_")
second
int)})
(defn get-caret-pos
"Get caret offset position as well as input element rect.
This function is only used by autocomplete command or up/down command
where offset position is needed.
If you only need character position, use `pos` instead. Do NOT call this."
([input] (get-caret-pos input (util/get-selection-start input)))
([input pos]
(when input
(let [rect (bean/->clj (.. input (getBoundingClientRect) (toJSON)))]
(try
(some-> (gdom/getElement "mock-text")
gdom/getChildren
array-seq
(util/nth-safe pos)
mock-char-pos
(assoc :rect rect))
(catch :default e
(js/console.log "index error" e)
{:pos pos
:rect rect
:left js/Number.MAX_SAFE_INTEGER
:top js/Number.MAX_SAFE_INTEGER}))))))
(defn pos [input]
(when input
(util/get-selection-start input)))
(defn start? [input]
(and input (zero? (util/get-selection-start input))))
(defn end? [input]
(and input
(= (count (.-value input))
(util/get-selection-start input))))
(defn set-selection-to [input n m]
(.setSelectionRange input n m))
(defn move-cursor-to [input n]
(.setSelectionRange input n n))
(defn move-cursor-forward
([input]
(move-cursor-forward input 1))
([input n]
(when input
(let [{:keys [pos]} (get-caret-pos input)
pos (if (and (= n 1) (not (zero? pos)))
(or (util/safe-inc-current-pos-from-start (.-value input) pos)
(inc pos))
(+ pos n))]
(move-cursor-to input pos)))))
(defn move-cursor-backward
([input]
(move-cursor-backward input 1))
([input n]
(when input
(let [{:keys [pos]} (get-caret-pos input)
pos (if (= n 1)
(util/safe-dec-current-pos-from-end (.-value input) pos)
(- pos n))
pos (max 0 (or pos (dec pos)))]
(move-cursor-to input pos)))))
(defn- get-input-content&pos
[input]
[(gobj/get input "value")
(pos input)])
(defn line-beginning-pos
[input]
(let [[content pos] (get-input-content&pos input)]
(if (zero? pos) 0
(let [last-newline-pos (string/last-index-of content \newline (dec pos))]
(if (= nil last-newline-pos) 0 ;; no newline found (first line)
(inc last-newline-pos))))))
(defn line-end-pos
[input]
(let [[content pos] (get-input-content&pos input)]
(or (string/index-of content \newline pos)
(count content))))
(defn beginning-of-line?
[input]
(let [[content pos] (get-input-content&pos input)]
(when content
(or (zero? pos)
(when-let [pre-char (subs content (dec pos) pos)]
(= pre-char \newline))))))
(defn move-cursor-to-line-end
[input]
(move-cursor-to input (line-end-pos input)))
;; (defn move-cursor-to-line-beginning
;; [input]
;; (move-cursor-to input (line-beginning-pos input)))
(defn move-cursor-to-end
[input]
(let [pos (count (gobj/get input "value"))]
(move-cursor-to input pos)))
(defn move-cursor-to-thing
([input thing]
(move-cursor-to-thing input thing (pos input)))
([input thing from]
(let [[content _pos] (get-input-content&pos input)
pos (string/index-of content thing from)]
(move-cursor-to input pos))))
(defn move-cursor-forward-by-word
[input]
(let [val (.-value input)
current (util/get-selection-start input)
current (loop [idx current]
(if (#{\space \newline} (util/nth-safe val idx))
(recur (inc idx))
idx))
idx (or (->> [(string/index-of val \space current)
(string/index-of val \newline current)]
(remove nil?)
(apply min))
(count val))]
(move-cursor-to input idx)))
(defn move-cursor-backward-by-word
[input]
(let [val (.-value input)
current (util/get-selection-start input)
prev (or
(->> [(string/last-index-of val \space (dec current))
(string/last-index-of val \newline (dec current))]
(remove nil?)
(apply max))
0)
idx (if (zero? prev)
0
(->
(loop [idx prev]
(if (#{\space \newline} (util/nth-safe val idx))
(recur (dec idx))
idx))
inc))]
(move-cursor-to input idx)))
(defn textarea-cursor-rect-first-row? [cursor]
(let [elms (-> (gdom/getElement "mock-text")
gdom/getChildren
array-seq)
tops (->> elms
(map mock-char-pos)
(map :top)
(distinct))]
(= (first tops) (:top cursor))))
(defn textarea-cursor-first-row? [input]
(textarea-cursor-rect-first-row? (get-caret-pos input)))
(defn textarea-cursor-rect-last-row? [cursor]
(let [elms (-> (gdom/getElement "mock-text")
gdom/getChildren
array-seq)
tops (->> elms
(map mock-char-pos)
(map :top)
(distinct))]
(= (last tops) (:top cursor))))
(defn textarea-cursor-last-row? [input]
(textarea-cursor-rect-last-row? (get-caret-pos input)))
(defn- next-cursor-pos-up-down [direction cursor]
(let [elms (-> (gdom/getElement "mock-text")
gdom/getChildren
array-seq)
chars (->> elms
(map mock-char-pos)
(group-by :top))
tops (sort (keys chars))
tops-p (partition-by #(== (:top cursor) %) tops)
line-next
(if (= :up direction)
(-> tops-p first last)
(-> tops-p last first))
lefts
(->> (get chars line-next)
(partition-by (fn [char-pos]
(<= (:left char-pos) (:left cursor)))))
left-a (-> lefts first last)
left-c (-> lefts last first)
closer
(if (> 2 (count lefts))
left-a
(closer left-a cursor left-c))]
(:pos closer)))
(defn- move-cursor-up-down
[input direction]
(move-cursor-to input (next-cursor-pos-up-down direction (get-caret-pos input))))
(defn move-cursor-up [input]
(move-cursor-up-down input :up))
(defn move-cursor-down [input]
(move-cursor-up-down input :down))
(defn select-up-down [input direction anchor cursor-rect]
(let [next-cursor (next-cursor-pos-up-down direction cursor-rect)]
(if (<= anchor next-cursor)
(.setSelectionRange input anchor next-cursor "forward")
(.setSelectionRange input next-cursor anchor "backward"))))
(comment
;; previous implementation of up/down
(defn move-cursor-up
"Move cursor up. If EOL, always move cursor to previous EOL."
[input]
(let [val (gobj/get input "value")
pos (util/get-selection-start input)
prev-idx (string/last-index-of val \newline pos)
pprev-idx (or (string/last-index-of val \newline (dec prev-idx)) -1)
cal-idx (+ pprev-idx pos (- prev-idx))]
(if (or (== pos (count val))
(> (- pos prev-idx) (- prev-idx pprev-idx)))
(move-cursor-to input prev-idx)
(move-cursor-to input cal-idx))))
(defn move-cursor-down
"Move cursor down by calculating current cursor line pos.
If EOL, always move cursor to next EOL."
[input]
(let [val (gobj/get input "value")
pos (util/get-selection-start input)
prev-idx (or (string/last-index-of val \newline pos) -1)
next-idx (or (string/index-of val \newline (inc pos))
(count val))
nnext-idx (or (string/index-of val \newline (inc next-idx))
(count val))
cal-idx (+ next-idx pos (- prev-idx))]
(if (> (- pos prev-idx) (- nnext-idx next-idx))
(move-cursor-to input nnext-idx)
(move-cursor-to input cal-idx)))))