| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431 |
- (ns frontend.util
- "Main ns for utility fns. This ns should be split up into more focused namespaces"
- #?(:clj (:refer-clojure :exclude [format]))
- #?(:cljs (:require-macros [frontend.util]))
- #?(:cljs (:require
- ["/frontend/selection" :as selection]
- ["/frontend/utils" :as utils]
- ["@capacitor/status-bar" :refer [^js StatusBar Style]]
- ["grapheme-splitter" :as GraphemeSplitter]
- ["remove-accents" :as removeAccents]
- ["sanitize-filename" :as sanitizeFilename]
- ["check-password-strength" :refer [passwordStrength]]
- [frontend.loader :refer [load]]
- [cljs-bean.core :as bean]
- [cljs-time.coerce :as tc]
- [cljs-time.core :as t]
- [clojure.pprint]
- [dommy.core :as d]
- [frontend.mobile.util :refer [native-platform?]]
- [logseq.graph-parser.util :as gp-util]
- [goog.dom :as gdom]
- [goog.object :as gobj]
- [goog.string :as gstring]
- [goog.userAgent]
- [promesa.core :as p]
- [rum.core :as rum]
- [clojure.core.async :as async]
- [cljs.core.async.impl.channels :refer [ManyToManyChannel]]))
- (:require
- [clojure.pprint]
- [clojure.string :as string]
- [clojure.walk :as walk]))
- #?(:cljs (goog-define NODETEST false)
- :clj (def NODETEST false))
- (defonce node-test? NODETEST)
- #?(:cljs
- (extend-protocol IPrintWithWriter
- js/Symbol
- (-pr-writer [sym writer _]
- (-write writer (str "\"" (.toString sym) "\"")))))
- #?(:cljs (defonce ^js node-path utils/nodePath))
- #?(:cljs (defonce ^js full-path-extname utils/fullPathExtname))
- #?(:cljs (defn app-scroll-container-node
- ([]
- (gdom/getElement "main-content-container"))
- ([el]
- (if (.closest el "#main-content-container")
- (app-scroll-container-node)
- (or
- (gdom/getElementByClass "sidebar-item-list")
- (app-scroll-container-node))))))
- #?(:cljs
- (defn safe-re-find
- [pattern s]
- (when-not (string? s)
- ;; TODO: sentry
- (js/console.trace))
- (when (string? s)
- (re-find pattern s))))
- #?(:cljs
- (do
- (def uuid-pattern "[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}")
- (defonce exactly-uuid-pattern (re-pattern (str "(?i)^" uuid-pattern "$")))
- (defn uuid-string?
- [s]
- (safe-re-find exactly-uuid-pattern s))
- (defn check-password-strength [input]
- (when-let [^js ret (and (string? input)
- (not (string/blank? input))
- (passwordStrength input))]
- (bean/->clj ret)))
- (defn safe-sanitize-file-name [s]
- (sanitizeFilename (str s)))))
- #?(:cljs
- (defn ios?
- []
- (utils/ios)))
- #?(:cljs
- (defn safari?
- []
- (let [ua (string/lower-case js/navigator.userAgent)]
- (and (string/includes? ua "webkit")
- (not (string/includes? ua "chrome"))))))
- #?(:cljs
- (defn mobile?
- "Triggering condition: Mobile phones
- *** Warning!!! ***
- For UX logic only! Don't use for FS logic
- iPad / Android Pad doesn't trigger!"
- []
- (when-not node-test?
- (safe-re-find #"Mobi" js/navigator.userAgent))))
- #?(:cljs
- (defn electron?
- []
- (when (and js/window (gobj/get js/window "navigator"))
- (gstring/caseInsensitiveContains js/navigator.userAgent " electron"))))
- #?(:cljs
- (defn mocked-open-dir-path
- "Mocked open DIR path for by-passing open dir in electron during testing. Nil if not given"
- []
- (when (electron?) (. js/window -__MOCKED_OPEN_DIR_PATH__))))
- #?(:cljs
- (do
- (def nfs? (and (not (electron?))
- (not (native-platform?))))
- (def web-platform? nfs?)))
- #?(:cljs
- (defn file-protocol?
- []
- (string/starts-with? js/window.location.href "file://")))
- (defn format
- [fmt & args]
- #?(:cljs (apply gstring/format fmt args)
- :clj (apply clojure.core/format fmt args)))
- #?(:cljs
- (defn evalue
- [event]
- (gobj/getValueByKeys event "target" "value")))
- #?(:cljs
- (defn ekey [event]
- (gobj/getValueByKeys event "key")))
- #?(:cljs
- (defn echecked? [event]
- (gobj/getValueByKeys event "target" "checked")))
- #?(:cljs
- (defn set-change-value
- "compatible change event for React"
- [node value]
- (utils/triggerInputChange node value)))
- #?(:cljs
- (defn p-handle
- ([p ok-handler]
- (p-handle p ok-handler (fn [error]
- (js/console.error error))))
- ([p ok-handler error-handler]
- (-> p
- (p/then (fn [result]
- (ok-handler result)))
- (p/catch (fn [error]
- (error-handler error)))))))
- #?(:cljs
- (defn get-width
- []
- (gobj/get js/window "innerWidth")))
- #?(:cljs
- (defn set-theme-light
- []
- (p/do!
- (.setStyle StatusBar (clj->js {:style (.-Light Style)})))))
- #?(:cljs
- (defn set-theme-dark
- []
- (p/do!
- (.setStyle StatusBar (clj->js {:style (.-Dark Style)})))))
- (defn find-first
- [pred coll]
- (first (filter pred coll)))
- ;; (defn format
- ;; [fmt & args]
- ;; (apply gstring/format fmt args))
- (defn remove-nils-non-nested
- [nm]
- (into {} (remove (comp nil? second)) nm))
- (defn ext-of-image? [s]
- (some #(-> (string/lower-case s)
- (string/ends-with? %))
- [".png" ".jpg" ".jpeg" ".bmp" ".gif" ".webp" ".svg"]))
- ;; ".lg:absolute.lg:inset-y-0.lg:right-0.lg:w-1/2"
- (defn hiccup->class
- [class]
- (some->> (string/split class #"\.")
- (string/join " ")
- (string/trim)))
- #?(:cljs
- (defn fetch
- ([url on-ok on-failed]
- (fetch url {} on-ok on-failed))
- ([url opts on-ok on-failed]
- (-> (js/fetch url (bean/->js opts))
- (.then (fn [resp]
- (if (>= (.-status resp) 400)
- (on-failed resp)
- (if (.-ok resp)
- (-> (.json resp)
- (.then bean/->clj)
- (.then #(on-ok %)))
- (on-failed resp)))))))))
- #?(:cljs
- (defn upload
- [url file on-ok on-failed on-progress]
- (let [xhr (js/XMLHttpRequest.)]
- (.open xhr "put" url)
- (gobj/set xhr "onload" on-ok)
- (gobj/set xhr "onerror" on-failed)
- (when (and (gobj/get xhr "upload")
- on-progress)
- (gobj/set (gobj/get xhr "upload")
- "onprogress"
- on-progress))
- (.send xhr file))))
- #?(:cljs
- (defn post
- [url body on-ok on-failed]
- (fetch url {:method "post"
- :headers {:Content-Type "application/json"}
- :body (js/JSON.stringify (clj->js body))}
- on-ok
- on-failed)))
- (defn zero-pad
- [n]
- (if (< n 10)
- (str "0" n)
- (str n)))
- #?(:cljs
- (defn safe-parse-int
- "Use if arg could be an int or string. If arg is only a string, use `parse-long`."
- [x]
- (if (string? x)
- (parse-long x)
- x)))
- #?(:cljs
- (defn safe-parse-float
- "Use if arg could be a float or string. If arg is only a string, use `parse-double`"
- [x]
- (if (string? x)
- (parse-double x)
- x)))
- #?(:cljs
- (defn debounce
- "Returns a function that will call f only after threshold has passed without new calls
- to the function. Calls prep-fn on the args in a sync way, which can be used for things like
- calling .persist on the event object to be able to access the event attributes in f"
- ([threshold f] (debounce threshold f (constantly nil)))
- ([threshold f prep-fn]
- (let [t (atom nil)]
- (fn [& args]
- (when @t (js/clearTimeout @t))
- (apply prep-fn args)
- (reset! t (js/setTimeout #(do
- (reset! t nil)
- (apply f args))
- threshold)))))))
- (defn nth-safe [c i]
- (if (or (< i 0) (>= i (count c)))
- nil
- (nth c i)))
- #?(:cljs
- (when-not node-test?
- (extend-type js/NodeList
- ISeqable
- (-seq [array] (array-seq array 0)))))
- ;; Caret
- #?(:cljs
- (defn caret-range [node]
- (when-let [doc (or (gobj/get node "ownerDocument")
- (gobj/get node "document"))]
- (let [win (or (gobj/get doc "defaultView")
- (gobj/get doc "parentWindow"))
- selection (.getSelection win)]
- (if selection
- (let [range-count (gobj/get selection "rangeCount")]
- (when (> range-count 0)
- (let [range (-> (.getSelection win)
- (.getRangeAt 0))
- pre-caret-range (.cloneRange range)]
- (.selectNodeContents pre-caret-range node)
- (.setEnd pre-caret-range
- (gobj/get range "endContainer")
- (gobj/get range "endOffset"))
- (let [contents (.cloneContents pre-caret-range)
- html (some-> (first (.-childNodes contents))
- (gobj/get "innerHTML")
- str)
- ;; FIXME: this depends on the dom structure,
- ;; need a converter from html to text includes newlines
- br-ended? (and html
- (or
- ;; first line with a new line
- (string/ends-with? html "<div class=\"is-paragraph\"></div></div></span></div></div></div>")
- ;; multiple lines with a new line
- (string/ends-with? html "<br></div></div></span></div></div></div>")))
- value (.toString pre-caret-range)]
- (if br-ended?
- (str value "\n")
- value)))))
- (when-let [selection (gobj/get doc "selection")]
- (when (not= "Control" (gobj/get selection "type"))
- (let [text-range (.createRange selection)
- pre-caret-text-range (.createTextRange (gobj/get doc "body"))]
- (.moveToElementText pre-caret-text-range node)
- (.setEndPoint pre-caret-text-range "EndToEnd" text-range)
- (gobj/get pre-caret-text-range "text")))))))))
- (defn get-selection-start
- [input]
- (when input
- (.-selectionStart input)))
- (defn get-selection-end
- [input]
- (when input
- (.-selectionEnd input)))
- (defn input-text-selected?
- [input]
- (not= (get-selection-start input)
- (get-selection-end input)))
- (defn get-selection-direction
- [input]
- (when input
- (.-selectionDirection input)))
- (defn get-first-or-last-line-pos
- [input]
- (let [pos (get-selection-start input)
- value (.-value input)
- last-newline-pos (or (string/last-index-of value \newline (dec pos)) -1)]
- (- pos last-newline-pos 1)))
- #?(:cljs
- (defn stop [e]
- (when e (doto e (.preventDefault) (.stopPropagation)))))
- #?(:cljs
- (defn stop-propagation [e]
- (when e (.stopPropagation e))))
- #?(:cljs
- (defn element-top [elem top]
- (when elem
- (if (.-offsetParent elem)
- (let [client-top (or (.-clientTop elem) 0)
- offset-top (.-offsetTop elem)]
- (+ top client-top offset-top (element-top (.-offsetParent elem) top)))
- top))))
- #?(:cljs
- (defn scroll-to-element
- [elem-id]
- (when-not (safe-re-find #"^/\d+$" elem-id)
- (when elem-id
- (when-let [elem (gdom/getElement elem-id)]
- (.scroll (app-scroll-container-node)
- #js {:top (let [top (element-top elem 0)]
- (if (< top 256)
- 0
- (- top 80)))
- :behavior "smooth"}))))))
- #?(:cljs
- (defn scroll-to
- ([pos]
- (scroll-to (app-scroll-container-node) pos))
- ([node pos]
- (scroll-to node pos true))
- ([node pos animate?]
- (when node
- (.scroll node
- #js {:top pos
- :behavior (if animate? "smooth" "auto")})))))
- #?(:cljs
- (defn scroll-top
- "Returns the scroll top position of the `node`. If `node` is not specified,
- returns the scroll top position of the `app-scroll-container-node`."
- ([]
- (scroll-top (app-scroll-container-node)))
- ([node]
- (when node (.-scrollTop node)))))
- #?(:cljs
- (defn scroll-to-top
- ([]
- (scroll-to (app-scroll-container-node) 0 false))
- ([animate?]
- (scroll-to (app-scroll-container-node) 0 animate?))))
- #?(:cljs
- (defn link?
- [node]
- (contains?
- #{"A" "BUTTON"}
- (gobj/get node "tagName"))))
- #?(:cljs
- (defn time?
- [node]
- (contains?
- #{"TIME"}
- (gobj/get node "tagName"))))
- #?(:cljs
- (defn audio?
- [node]
- (contains?
- #{"AUDIO"}
- (gobj/get node "tagName"))))
- #?(:cljs
- (defn video?
- [node]
- (contains?
- #{"VIDEO"}
- (gobj/get node "tagName"))))
- #?(:cljs
- (defn sup?
- [node]
- (contains?
- #{"SUP"}
- (gobj/get node "tagName"))))
- #?(:cljs
- (defn input?
- [node]
- (when node
- (contains?
- #{"INPUT" "TEXTAREA"}
- (gobj/get node "tagName")))))
- #?(:cljs
- (defn select?
- [node]
- (when node
- (= "SELECT" (gobj/get node "tagName")))))
- #?(:cljs
- (defn details-or-summary?
- [node]
- (when node
- (contains?
- #{"DETAILS" "SUMMARY"}
- (gobj/get node "tagName")))))
- ;; Debug
- (defn starts-with?
- [s substr]
- (string/starts-with? s substr))
- (defn distinct-by
- [f col]
- (reduce
- (fn [acc x]
- (if (some #(= (f x) (f %)) acc)
- acc
- (vec (conj acc x))))
- []
- col))
- (defn distinct-by-last-wins
- [f col]
- (reduce
- (fn [acc x]
- (if (some #(= (f x) (f %)) acc)
- (mapv
- (fn [v]
- (if (= (f x) (f v))
- x
- v))
- acc)
- (vec (conj acc x))))
- []
- col))
- (defn get-git-owner-and-repo
- [repo-url]
- (take-last 2 (string/split repo-url #"/")))
- (defn safe-lower-case
- [s]
- (if (string? s)
- (string/lower-case s) s))
- #?(:cljs
- (defn safe-path-join [prefix & paths]
- (let [path (apply node-path.join (cons prefix paths))]
- (if (and (electron?) (gstring/caseInsensitiveStartsWith path "file://"))
- (gp-util/safe-decode-uri-component (subs path 7))
- path))))
- (defn trim-safe
- [s]
- (when s
- (string/trim s)))
- (defn trimr-without-newlines
- [s]
- (.replace s #"[ \t\r]+$" ""))
- (defn triml-without-newlines
- [s]
- (.replace s #"^[ \t\r]+" ""))
- (defn concat-without-spaces
- [left right]
- (when (and (string? left)
- (string? right))
- (let [left (trimr-without-newlines left)
- not-space? (or
- (string/blank? left)
- (= "\n" (last left)))]
- (str left
- (when-not not-space? " ")
- (triml-without-newlines right)))))
- ;; Add documentation
- (defn replace-first [pattern s new-value]
- (if-let [first-index (string/index-of s pattern)]
- (str new-value (subs s (+ first-index (count pattern))))
- s))
- (defn replace-last
- ([pattern s new-value]
- (replace-last pattern s new-value true))
- ([pattern s new-value space?]
- (if-let [last-index (string/last-index-of s pattern)]
- (let [prefix (subs s 0 last-index)]
- (if space?
- (concat-without-spaces prefix new-value)
- (str prefix new-value)))
- s)))
- (defonce escape-chars "[]{}().+*?|")
- (defn escape-regex-chars
- "Escapes characters in string `old-value"
- [old-value]
- (reduce (fn [acc escape-char]
- (string/replace acc escape-char (str "\\" escape-char)))
- old-value escape-chars))
- (defn replace-ignore-case
- [s old-value new-value]
- (string/replace s (re-pattern (str "(?i)" (escape-regex-chars old-value))) new-value))
- ;; copy from https://stackoverflow.com/questions/18735665/how-can-i-get-the-positions-of-regex-matches-in-clojurescript
- #?(:cljs
- (defn re-pos [re s]
- (let [re (js/RegExp. (.-source re) "g")]
- (loop [res []]
- (if-let [m (.exec re s)]
- (recur (conj res [(.-index m) (first m)]))
- res)))))
- #?(:cljs
- (defn safe-set-range-text!
- ([input text start end]
- (try
- (.setRangeText input text start end)
- (catch :default _e
- nil)))
- ([input text start end select-mode]
- (try
- (.setRangeText input text start end select-mode)
- (catch :default _e
- nil)))))
- #?(:cljs
- ;; for widen char
- (defn safe-dec-current-pos-from-end
- [input current-pos]
- (if-let [len (and (string? input) (.-length input))]
- (when-let [input (and (>= len 2) (<= current-pos len)
- (.substring input (max (- current-pos 20) 0) current-pos))]
- (try
- (let [^js splitter (GraphemeSplitter.)
- ^js input (.splitGraphemes splitter input)]
- (- current-pos (.-length (.pop input))))
- (catch :default e
- (js/console.error e)
- (dec current-pos))))
- (dec current-pos))))
- #?(:cljs
- ;; for widen char
- (defn safe-inc-current-pos-from-start
- [input current-pos]
- (if-let [len (and (string? input) (.-length input))]
- (when-let [input (and (>= len 2) (<= current-pos len)
- (.substr input current-pos 20))]
- (try
- (let [^js splitter (GraphemeSplitter.)
- ^js input (.splitGraphemes splitter input)]
- (+ current-pos (.-length (.shift input))))
- (catch :default e
- (js/console.error e)
- (inc current-pos))))
- (inc current-pos))))
- #?(:cljs
- (defn kill-line-before!
- [input]
- (let [val (.-value input)
- end (get-selection-start input)
- n-pos (string/last-index-of val \newline (dec end))
- start (if n-pos (inc n-pos) 0)]
- (safe-set-range-text! input "" start end))))
- #?(:cljs
- (defn kill-line-after!
- [input]
- (let [val (.-value input)
- start (get-selection-start input)
- end (or (string/index-of val \newline start)
- (count val))]
- (safe-set-range-text! input "" start end))))
- #?(:cljs
- (defn insert-at-current-position!
- [input text]
- (let [start (get-selection-start input)
- end (get-selection-end input)]
- (safe-set-range-text! input text start end "end"))))
- ;; copied from re_com
- #?(:cljs
- (defn deref-or-value
- "Takes a value or an atom
- If it's a value, returns it
- If it's a Reagent object that supports IDeref, returns the value inside it by derefing
- "
- [val-or-atom]
- (if (satisfies? IDeref val-or-atom)
- @val-or-atom
- val-or-atom)))
- ;; copied from re_com
- #?(:cljs
- (defn now->utc
- "Return a goog.date.UtcDateTime based on local date/time."
- []
- (let [local-date-time (js/goog.date.DateTime.)]
- (js/goog.date.UtcDateTime.
- (.getYear local-date-time)
- (.getMonth local-date-time)
- (.getDate local-date-time)
- 0 0 0 0))))
- (defn safe-subvec [xs start end]
- (if (or (neg? start)
- (> end (count xs)))
- []
- (subvec xs start end)))
- #?(:cljs
- (defn get-nodes-between-two-nodes
- [id1 id2 class]
- (when-let [nodes (array-seq (js/document.getElementsByClassName class))]
- (let [node-1 (gdom/getElement id1)
- node-2 (gdom/getElement id2)
- idx-1 (.indexOf nodes node-1)
- idx-2 (.indexOf nodes node-2)
- start (min idx-1 idx-2)
- end (inc (max idx-1 idx-2))]
- (safe-subvec (vec nodes) start end)))))
- #?(:cljs
- (defn get-direction-between-two-nodes
- [id1 id2 class]
- (when-let [nodes (array-seq (js/document.getElementsByClassName class))]
- (let [node-1 (gdom/getElement id1)
- node-2 (gdom/getElement id2)
- idx-1 (.indexOf nodes node-1)
- idx-2 (.indexOf nodes node-2)]
- (if (>= idx-1 idx-2)
- :up
- :down)))))
- #?(:cljs
- (defn rec-get-tippy-container
- [node]
- (if (and node (d/has-class? node "tippy-tooltip-content"))
- node
- (and node
- (rec-get-tippy-container (gobj/get node "parentNode"))))))
- #?(:cljs
- (defn rec-get-blocks-container
- [node]
- (if (and node (d/has-class? node "blocks-container"))
- node
- (and node
- (rec-get-blocks-container (gobj/get node "parentNode"))))))
- #?(:cljs
- (defn rec-get-blocks-content-section
- [node]
- (if (and node (d/has-class? node "content"))
- node
- (and node
- (rec-get-blocks-content-section (gobj/get node "parentNode"))))))
- #?(:cljs
- (defn get-blocks-noncollapse []
- (->> (d/sel "div:not(.reveal) .ls-block")
- (filter (fn [b] (some? (gobj/get b "offsetParent")))))))
- #?(:cljs
- (defn remove-embeded-blocks [blocks]
- (->> blocks
- (remove (fn [b] (= "true" (d/attr b "data-embed")))))))
- #?(:cljs
- (defn get-selected-text
- []
- (utils/getSelectionText)))
- #?(:cljs (def clear-selection! selection/clearSelection))
- #?(:cljs
- (defn copy-to-clipboard!
- ([s]
- (utils/writeClipboard (clj->js {:text s})))
- ([s html]
- (utils/writeClipboard (clj->js {:text s :html html})))))
- (defn drop-nth [n coll]
- (keep-indexed #(when (not= %1 n) %2) coll))
- #?(:cljs
- (defn react
- [ref]
- (if rum/*reactions*
- (rum/react ref)
- @ref)))
- (defn time-ms
- []
- #?(:cljs (tc/to-long (t/now))))
- ;; Returns the milliseconds representation of the provided time, in the local timezone.
- ;; For example, if you run this function at 10pm EDT in the EDT timezone on May 31st,
- ;; it will return 1622433600000, which is equivalent to Mon May 31 2021 00 :00:00.
- #?(:cljs
- (defn today-at-local-ms [hours mins secs millisecs]
- (.setHours (js/Date. (.now js/Date)) hours mins secs millisecs)))
- (defn d
- [k f]
- (let [result (atom nil)]
- (println (str "Debug " k))
- (time (reset! result (doall (f))))
- @result))
- (defn concat-without-nil
- [& cols]
- (->> (apply concat cols)
- (remove nil?)))
- #?(:cljs
- (defn set-title!
- [title]
- (set! (.-title js/document) title)))
- #?(:cljs
- (defn get-block-container
- [block-element]
- (when block-element
- (when-let [section (some-> (rec-get-blocks-content-section block-element)
- (d/parent))]
- (when section
- (gdom/getElement section "id"))))))
- #?(:cljs
- (defn get-prev-block-non-collapsed
- [block]
- (when-let [blocks (get-blocks-noncollapse)]
- (let [block-id (.-id block)
- block-ids (mapv #(.-id %) blocks)]
- (when-let [index (.indexOf block-ids block-id)]
- (let [idx (dec index)]
- (when (>= idx 0)
- (nth-safe blocks idx))))))))
- #?(:cljs
- (defn get-prev-block-non-collapsed-non-embed
- [block]
- (when-let [blocks (->> (get-blocks-noncollapse)
- remove-embeded-blocks)]
- (let [block-id (.-id block)
- block-ids (mapv #(.-id %) blocks)]
- (when-let [index (.indexOf block-ids block-id)]
- (let [idx (dec index)]
- (when (>= idx 0)
- (nth-safe blocks idx))))))))
- #?(:cljs
- (defn get-next-block-non-collapsed
- [block]
- (when-let [blocks (get-blocks-noncollapse)]
- (let [block-id (.-id block)
- block-ids (mapv #(.-id %) blocks)]
- (when-let [index (.indexOf block-ids block-id)]
- (let [idx (inc index)]
- (when (>= (count blocks) idx)
- (nth-safe blocks idx))))))))
- #?(:cljs
- (defn get-next-block-non-collapsed-skip
- [block]
- (when-let [blocks (get-blocks-noncollapse)]
- (let [block-id (.-id block)
- block-ids (mapv #(.-id %) blocks)]
- (when-let [index (.indexOf block-ids block-id)]
- (loop [idx (inc index)]
- (when (>= (count blocks) idx)
- (let [block (nth-safe blocks idx)
- nested? (->> (array-seq (gdom/getElementsByClass "selected"))
- (some (fn [dom] (.contains dom block))))]
- (if nested?
- (recur (inc idx))
- block)))))))))
- (defn rand-str
- [n]
- #?(:cljs (-> (.toString (js/Math.random) 36)
- (.substr 2 n))
- :clj (->> (repeatedly #(Integer/toString (rand 36) 36))
- (take n)
- (apply str))))
- (defn unique-id
- []
- (str (rand-str 6) (rand-str 3)))
- (defn pp-str [x]
- #_:clj-kondo/ignore
- (with-out-str (clojure.pprint/pprint x)))
- (defn hiccup-keywordize
- [hiccup]
- (walk/postwalk
- (fn [f]
- (if (and (vector? f) (string? (first f)))
- (update f 0 keyword)
- f))
- hiccup))
- #?(:cljs
- (defn chrome?
- []
- (let [user-agent js/navigator.userAgent
- vendor js/navigator.vendor]
- (and (safe-re-find #"Chrome" user-agent)
- (safe-re-find #"Google Inc" vendor)))))
- #?(:cljs
- (defn indexeddb-check?
- [error-handler]
- (let [test-db "logseq-test-db-foo-bar-baz"
- db (and js/window.indexedDB
- (js/window.indexedDB.open test-db))]
- (when (and db (not (chrome?)))
- (gobj/set db "onerror" error-handler)
- (gobj/set db "onsuccess"
- (fn []
- (js/window.indexedDB.deleteDatabase test-db)))))))
- (defonce mac? #?(:cljs goog.userAgent/MAC
- :clj nil))
- (defonce win32? #?(:cljs goog.userAgent/WINDOWS
- :clj nil))
- #?(:cljs
- (defn absolute-path?
- [path]
- (try
- (js/window.apis.isAbsolutePath path)
- (catch :default _
- (utils/win32 path)))))
- (defn default-content-with-title
- [text-format]
- (case (name text-format)
- "org"
- "* "
- "- "))
- #?(:cljs
- (defn get-first-block-by-id
- [block-id]
- (when block-id
- (let [block-id (str block-id)]
- (when (uuid-string? block-id)
- (first (array-seq (js/document.getElementsByClassName block-id))))))))
- #?(:cljs
- (defn url-encode
- [string]
- (some-> string str (js/encodeURIComponent) (.replace "+" "%20"))))
- #?(:cljs
- (defn search-normalize
- "Normalize string for searching (loose)"
- [s remove-accents?]
- (let [normalize-str (.normalize (string/lower-case s) "NFKC")]
- (if remove-accents?
- (removeAccents normalize-str)
- normalize-str))))
- #?(:cljs
- (def page-name-sanity-lc
- "Delegate to gp-util to loosely couple app usages to graph-parser"
- gp-util/page-name-sanity-lc))
- #?(:cljs
- (defn safe-page-name-sanity-lc
- [s]
- (if (string? s)
- (page-name-sanity-lc s) s)))
- (defn get-page-original-name
- [page]
- (or (:block/original-name page)
- (:block/name page)))
- #?(:cljs
- (defn add-style!
- [style]
- (when (some? style)
- (let [parent-node (d/sel1 :head)
- id "logseq-custom-theme-id"
- old-link-element (d/sel1 (str "#" id))
- style (if (string/starts-with? style "http")
- style
- (str "data:text/css;charset=utf-8," (js/encodeURIComponent style)))]
- (when old-link-element
- (d/remove! old-link-element))
- (let [link (->
- (d/create-element :link)
- (d/set-attr! :id id)
- (d/set-attr! :rel "stylesheet")
- (d/set-attr! :type "text/css")
- (d/set-attr! :href style)
- (d/set-attr! :media "all"))]
- (d/append! parent-node link))))))
- (defn ->platform-shortcut
- [keyboard-shortcut]
- (let [result (or keyboard-shortcut "")
- result (string/replace result "left" "←")
- result (string/replace result "right" "→")]
- (if mac?
- (-> result
- (string/replace "Ctrl" "Cmd")
- (string/replace "Alt" "Opt"))
- result)))
- (defn remove-common-preceding
- [col1 col2]
- (if (and (= (first col1) (first col2))
- (seq col1))
- (recur (rest col1) (rest col2))
- [col1 col2]))
- ;; fs
- #?(:cljs
- (defn get-file-ext
- [file]
- (and
- (string? file)
- (string/includes? file ".")
- (some-> (gp-util/path->file-ext file) string/lower-case))))
- (defn get-dir-and-basename
- [path]
- (let [parts (string/split path "/")
- basename (last parts)
- dir (->> (butlast parts)
- (string/join "/"))]
- [dir basename]))
- (defn get-relative-path
- [current-file-path another-file-path]
- (let [directories-f #(butlast (string/split % "/"))
- parts-1 (directories-f current-file-path)
- parts-2 (directories-f another-file-path)
- [parts-1 parts-2] (remove-common-preceding parts-1 parts-2)
- another-file-name (last (string/split another-file-path "/"))]
- (->> (concat
- (if (seq parts-1)
- (repeat (count parts-1) "..")
- ["."])
- parts-2
- [another-file-name])
- (string/join "/"))))
- ;; Copied from https://github.com/tonsky/datascript-todo
- #?(:clj
- (defmacro profile
- [k & body]
- `(if goog.DEBUG
- (let [k# ~k]
- (.time js/console k#)
- (let [res# (do ~@body)]
- (.timeEnd js/console k#)
- res#))
- (do ~@body))))
- #?(:clj
- (defmacro with-time
- "Evaluates expr and prints the time it took. Returns the value of expr and the spent time."
- [expr]
- `(let [start# (cljs.core/system-time)
- ret# ~expr]
- {:result ret#
- :time (.toFixed (- (cljs.core/system-time) start#) 6)})))
- ;; TODO: profile and profileEnd
- ;; Copy from hiccup but tweaked for publish usage
- (defn escape-html
- "Change special characters into HTML character entities."
- [text]
- (-> text
- (string/replace "&" "logseq____&")
- (string/replace "<" "logseq____<")
- (string/replace ">" "logseq____>")
- (string/replace "\"" "logseq____"")
- (string/replace "'" "logseq____'")))
- (defn unescape-html
- [text]
- (-> text
- (string/replace "logseq____&" "&")
- (string/replace "logseq____<" "<")
- (string/replace "logseq____>" ">")
- (string/replace "logseq____"" "\"")
- (string/replace "logseq____'" "'")))
- (comment
- (= (get-relative-path "journals/2020_11_18.org" "pages/grant_ideas.org")
- "../pages/grant_ideas.org")
- (= (get-relative-path "journals/2020_11_18.org" "journals/2020_11_19.org")
- "./2020_11_19.org")
- (= (get-relative-path "a/b/c/d/g.org" "a/b/c/e/f.org")
- "../e/f.org"))
- (defn keyname [key] (str (namespace key) "/" (name key)))
- #?(:cljs
- (defn select-highlight!
- [blocks]
- (doseq [block blocks]
- (d/add-class! block "selected noselect"))))
- #?(:cljs
- (defn select-unhighlight!
- [blocks]
- (doseq [block blocks]
- (d/remove-class! block "selected" "noselect"))))
- #?(:cljs
- (defn drain-chan
- "drop all stuffs in CH, and return all of them"
- [ch]
- (->> (repeatedly #(async/poll! ch))
- (take-while identity))))
- #?(:cljs
- (defn <ratelimit
- "return a channel CH,
- ratelimit flush items in in-ch every max-duration(ms),
- opts:
- - :filter-fn filter item before putting items into returned CH, (filter-fn item)
- will poll it when its return value is channel,
- - :flush-fn exec flush-fn when time to flush, (flush-fn item-coll)
- - :stop-ch stop go-loop when stop-ch closed
- - :distinct-coll? distinct coll when put into CH
- - :chan-buffer buffer of return CH, default use (async/chan 1000)
- - :flush-now-ch flush the content in the queue immediately
- - :refresh-timeout-ch refresh (timeout max-duration)"
- [in-ch max-duration & {:keys [filter-fn flush-fn stop-ch distinct-coll? chan-buffer flush-now-ch refresh-timeout-ch]}]
- (let [ch (if chan-buffer (async/chan chan-buffer) (async/chan 1000))
- stop-ch* (or stop-ch (async/chan))
- flush-now-ch* (or flush-now-ch (async/chan))
- refresh-timeout-ch* (or refresh-timeout-ch (async/chan))]
- (async/go-loop [timeout-ch (async/timeout max-duration) coll []]
- (let [{:keys [refresh-timeout timeout e stop flush-now]}
- (async/alt! refresh-timeout-ch* {:refresh-timeout true}
- timeout-ch {:timeout true}
- in-ch ([e] {:e e})
- stop-ch* {:stop true}
- flush-now-ch* {:flush-now true})]
- (cond
- refresh-timeout
- (recur (async/timeout max-duration) coll)
- (or flush-now timeout)
- (do (async/onto-chan! ch coll false)
- (flush-fn coll)
- (drain-chan flush-now-ch*)
- (recur (async/timeout max-duration) []))
- (some? e)
- (let [filter-v (filter-fn e)
- filter-v* (if (instance? ManyToManyChannel filter-v)
- (async/<! filter-v)
- filter-v)]
- (if filter-v*
- (recur timeout-ch (cond-> (conj coll e)
- distinct-coll? distinct
- true vec))
- (recur timeout-ch coll)))
- (or stop
- ;; got nil from in-ch, means in-ch is closed
- ;; so we stop the whole go-loop
- (nil? e))
- (async/close! ch))))
- ch)))
- #?(:cljs
- (defn trace!
- []
- (js/console.trace)))
- (defn remove-first [pred coll]
- ((fn inner [coll]
- (lazy-seq
- (when-let [[x & xs] (seq coll)]
- (if (pred x)
- xs
- (cons x (inner xs))))))
- coll))
- (def pprint clojure.pprint/pprint)
- #?(:cljs
- (defn backward-kill-word
- [input]
- (let [val (.-value input)
- current (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} (nth-safe val idx))
- (recur (dec idx))
- idx))
- inc))]
- (safe-set-range-text! input "" idx current))))
- #?(:cljs
- (defn forward-kill-word
- [input]
- (let [val (.-value input)
- current (get-selection-start input)
- current (loop [idx current]
- (if (#{\space \newline} (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))]
- (safe-set-range-text! input "" current (inc idx)))))
- #?(:cljs
- (defn fix-open-external-with-shift!
- [^js/MouseEvent e]
- (when (and (.-shiftKey e) win32? (electron?)
- (= (string/lower-case (.. e -target -nodeName)) "a")
- (string/starts-with? (.. e -target -href) "file:"))
- (.preventDefault e))))
- (defn classnames
- "Like react classnames utility:
- ```
- [:div {:class (classnames [:a :b {:c true}])}
- ```
- "
- [args]
- (into #{} (mapcat
- #(if (map? %)
- (for [[k v] %]
- (when v (name k)))
- (when-not (nil? %) [(name %)]))
- args)))
- #?(:cljs
- (defn- get-dom-top
- [node]
- (gobj/get (.getBoundingClientRect node) "top")))
- #?(:cljs
- (defn sort-by-height
- [elements]
- (sort (fn [x y]
- (< (get-dom-top x) (get-dom-top y)))
- elements)))
- #?(:cljs
- (defn calc-delta-rect-offset
- [^js/HTMLElement target ^js/HTMLElement container]
- (let [target-rect (bean/->clj (.toJSON (.getBoundingClientRect target)))
- viewport-rect {:width (.-clientWidth container)
- :height (.-clientHeight container)}]
- {:y (- (:height viewport-rect) (:bottom target-rect))
- :x (- (:width viewport-rect) (:right target-rect))})))
- (def regex-char-esc-smap
- (let [esc-chars "{}[]()&^%$#!?*.+|\\"]
- (zipmap esc-chars
- (map #(str "\\" %) esc-chars))))
- (defn regex-escape
- "Escape all regex meta chars in text."
- [text]
- (string/join (replace regex-char-esc-smap text)))
- (comment
- (re-matches (re-pattern (regex-escape "$u^8(d)+w.*[dw]d?")) "$u^8(d)+w.*[dw]d?"))
- #?(:cljs
- (defn meta-key-name []
- (if mac? "Cmd" "Ctrl")))
- #?(:cljs
- (defn meta-key? [e]
- (if mac?
- (gobj/get e "metaKey")
- (gobj/get e "ctrlKey"))))
- #?(:cljs
- (defn right-click?
- [e]
- (let [which (gobj/get e "which")
- button (gobj/get e "button")]
- (or (= which 3)
- (= button 2)))))
- (def keyboard-height (atom nil))
- #?(:cljs
- (defn scroll-editor-cursor
- [^js/HTMLElement el & {:keys [to-vw-one-quarter?]}]
- (when (and el (or (native-platform?) mobile?))
- (let [box-rect (.getBoundingClientRect el)
- box-top (.-top box-rect)
- box-bottom (.-bottom box-rect)
- header-height (-> (gdom/getElementByClass "cp__header")
- .-clientHeight)
- main-node (app-scroll-container-node el)
- scroll-top (.-scrollTop main-node)
- current-pos (get-selection-start el)
- mock-text (some-> (gdom/getElement "mock-text")
- gdom/getChildren
- array-seq
- (nth-safe current-pos))
- offset-top (and mock-text (.-offsetTop mock-text))
- offset-height (and mock-text (.-offsetHeight mock-text))
- cursor-y (if offset-top (+ offset-top box-top offset-height 2) box-bottom)
- vw-height (or (.-height js/window.visualViewport)
- (.-clientHeight js/document.documentElement))
- ;; mobile toolbar height: 40px
- scroll (- cursor-y (- vw-height (+ @keyboard-height 40)))]
- (cond
- (and to-vw-one-quarter? (> cursor-y (* vw-height 0.4)))
- (set! (.-scrollTop main-node) (+ scroll-top (- cursor-y (/ vw-height 4))))
- (and (< cursor-y (+ header-height offset-height 4)) ;; 4 is top+bottom padding for per line
- (>= cursor-y header-height))
- (.scrollBy main-node (bean/->js {:top (- (+ offset-height 4))}))
- (< cursor-y header-height)
- (let [_ (.scrollIntoView el true)
- main-node (app-scroll-container-node el)
- scroll-top (.-scrollTop main-node)]
- (set! (.-scrollTop main-node) (- scroll-top (/ vw-height 4))))
- (> scroll 0)
- (set! (.-scrollTop main-node) (+ scroll-top scroll))
- :else
- nil)))))
- #?(:cljs
- (defn sm-breakpoint?
- []
- (< (.-offsetWidth js/document.documentElement) 640)))
- #?(:cljs
- (defn event-is-composing?
- "Check if keydown event is a composing (IME) event.
- Ignore the IME process by default."
- ([e]
- (event-is-composing? e false))
- ([e include-process?]
- (let [event-composing? (gobj/getValueByKeys e "event_" "isComposing")]
- (if include-process?
- (or event-composing?
- (= (gobj/get e "keyCode") 229)
- (= (gobj/get e "key") "Process"))
- event-composing?)))))
- #?(:cljs
- (defn onchange-event-is-composing?
- "Check if onchange event of Input is a composing (IME) event.
- Always ignore the IME process."
- [e]
- (gobj/getValueByKeys e "nativeEvent" "isComposing"))) ;; No keycode available
- #?(:cljs
- (defn open-url
- [url]
- (let [route? (or (string/starts-with? url
- (string/replace js/location.href js/location.hash ""))
- (string/starts-with? url "#"))]
- (if (and (not route?) (electron?))
- (js/window.apis.openExternal url)
- (set! (.-href js/window.location) url)))))
- (defn collapsed?
- [block]
- (:block/collapsed? block))
- #?(:cljs
- (defn atom? [v]
- (instance? Atom v)))
- ;; https://stackoverflow.com/questions/32511405/how-would-time-ago-function-implementation-look-like-in-clojure
- #?(:cljs
- (defn time-ago
- "time: inst-ms or js/Date"
- [time]
- (let [units [{:name "second" :limit 60 :in-second 1}
- {:name "minute" :limit 3600 :in-second 60}
- {:name "hour" :limit 86400 :in-second 3600}
- {:name "day" :limit 604800 :in-second 86400}
- {:name "week" :limit 2629743 :in-second 604800}
- {:name "month" :limit 31556926 :in-second 2629743}
- {:name "year" :limit js/Number.MAX_SAFE_INTEGER :in-second 31556926}]
- diff (t/in-seconds (t/interval (if (instance? js/Date time) time (js/Date. time)) (t/now)))]
- (if (< diff 5)
- "just now"
- (let [unit (first (drop-while #(or (>= diff (:limit %))
- (not (:limit %)))
- units))]
- (-> (/ diff (:in-second unit))
- Math/floor
- int
- (#(str % " " (:name unit) (when (> % 1) "s") " ago"))))))))
- #?(:cljs
- (def JS_ROOT
- (when-not node-test?
- (if (= js/location.protocol "file:")
- "./js"
- "./static/js"))))
- #?(:cljs
- (defn js-load$
- [url]
- (p/create
- (fn [resolve]
- (load url resolve)))))
- #?(:cljs
- (defn element-visible?
- [element]
- (when element
- (when-let [r (.getBoundingClientRect element)]
- (and (>= (.-top r) 0)
- (<= (+ (.-bottom r) 64)
- (or (.-innerHeight js/window)
- (js/document.documentElement.clientHeight))))))))
- #?(:cljs
- (defn copy-image-to-clipboard
- [src]
- (-> (js/fetch src)
- (.then (fn [data]
- (-> (.blob data)
- (.then (fn [blob]
- (js/navigator.clipboard.write (clj->js [(js/ClipboardItem. (clj->js {(.-type blob) blob}))]))))
- (.catch js/console.error)))))))
|