mirror of
https://github.com/logseq/logseq.git
synced 2026-05-05 03:16:37 +00:00
- file sync for electron/ios/android - age encryption of both file content and file path - massive UI enhancement - corresponding CI tasks Co-authored-by: llcc <lzhes43@gmail.com> Co-authored-by: rcmerci <rcmerci@gmail.com> Co-authored-by: Tienson Qin <tiensonqin@gmail.com> Co-authored-by: Andelf <andelf@gmail.com> Co-authored-by: Gabriel Horner <gabriel@logseq.com>
1404 lines
41 KiB
Clojure
1404 lines
41 KiB
Clojure
(ns frontend.util
|
|
#?(: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]
|
|
["check-password-strength" :refer [passwordStrength]]
|
|
[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 (defn app-scroll-container-node []
|
|
(gdom/getElement "main-content-container")))
|
|
|
|
#?(: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)))))
|
|
|
|
#?(: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?
|
|
[]
|
|
(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://"))
|
|
(js/decodeURIComponent (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 js/Error _e
|
|
nil)))
|
|
([input text start end select-mode]
|
|
(try
|
|
(.setRangeText input text start end select-mode)
|
|
(catch js/Error _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 js/Error 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 js/Error 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/by-class "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.core/*reactions*
|
|
(rum/react ref)
|
|
@ref)))
|
|
|
|
(defn time-ms
|
|
[]
|
|
#?(:cljs (tc/to-long (cljs-time.core/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)]
|
|
(when-let [index (.indexOf blocks block)]
|
|
(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)]
|
|
(when-let [index (.indexOf blocks block)]
|
|
(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)]
|
|
(when-let [index (.indexOf blocks block)]
|
|
(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)]
|
|
(when-let [index (.indexOf blocks block)]
|
|
(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 js/Error _
|
|
(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"))))
|
|
|
|
(def windows-reserved-chars #"[:\\*\\?\"<>|]+")
|
|
|
|
#?(:cljs
|
|
(do
|
|
(defn include-windows-reserved-chars?
|
|
[s]
|
|
(safe-re-find windows-reserved-chars s))
|
|
|
|
(defn create-title-property?
|
|
[s]
|
|
(and (string? s)
|
|
(or (include-windows-reserved-chars? s)
|
|
(string/includes? s "_")
|
|
(string/includes? s "/")
|
|
(string/includes? s ".")
|
|
(string/includes? s "%")
|
|
(string/includes? s "#"))))))
|
|
|
|
#?(: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
|
|
(defn file-name-sanity
|
|
"Sanitize page-name for file name (strict), for file writing."
|
|
[page-name]
|
|
(some-> page-name
|
|
gp-util/page-name-sanity
|
|
;; for android filesystem compatiblity
|
|
(string/replace #"[\\#|%]+" url-encode)
|
|
;; Windows reserved path characters
|
|
(string/replace windows-reserved-chars url-encode)
|
|
(string/replace #"/" url-encode)
|
|
(string/replace "*" "%2A"))))
|
|
|
|
#?(: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
|
|
(defn get-file-ext
|
|
[file]
|
|
(and
|
|
(string? file)
|
|
(string/includes? file ".")
|
|
(some-> (last (string/split 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
|
|
(defn escape-html
|
|
"Change special characters into HTML character entities."
|
|
[text]
|
|
(-> text
|
|
(string/replace "&" "&")
|
|
(string/replace "<" "<")
|
|
(string/replace ">" ">")
|
|
(string/replace "\"" """)
|
|
(string/replace "'" "'")))
|
|
|
|
(defn unescape-html
|
|
[text]
|
|
(-> text
|
|
(string/replace "&" "&")
|
|
(string/replace "<" "<")
|
|
(string/replace ">" ">")
|
|
(string/replace """ "\"")
|
|
(string/replace "'" "'")))
|
|
|
|
(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)))
|
|
[(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)
|
|
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)
|
|
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"))))))))
|