util.cljc 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431
  1. (ns frontend.util
  2. "Main ns for utility fns. This ns should be split up into more focused namespaces"
  3. #?(:clj (:refer-clojure :exclude [format]))
  4. #?(:cljs (:require-macros [frontend.util]))
  5. #?(:cljs (:require
  6. ["/frontend/selection" :as selection]
  7. ["/frontend/utils" :as utils]
  8. ["@capacitor/status-bar" :refer [^js StatusBar Style]]
  9. ["grapheme-splitter" :as GraphemeSplitter]
  10. ["remove-accents" :as removeAccents]
  11. ["sanitize-filename" :as sanitizeFilename]
  12. ["check-password-strength" :refer [passwordStrength]]
  13. [frontend.loader :refer [load]]
  14. [cljs-bean.core :as bean]
  15. [cljs-time.coerce :as tc]
  16. [cljs-time.core :as t]
  17. [clojure.pprint]
  18. [dommy.core :as d]
  19. [frontend.mobile.util :refer [native-platform?]]
  20. [logseq.graph-parser.util :as gp-util]
  21. [goog.dom :as gdom]
  22. [goog.object :as gobj]
  23. [goog.string :as gstring]
  24. [goog.userAgent]
  25. [promesa.core :as p]
  26. [rum.core :as rum]
  27. [clojure.core.async :as async]
  28. [cljs.core.async.impl.channels :refer [ManyToManyChannel]]))
  29. (:require
  30. [clojure.pprint]
  31. [clojure.string :as string]
  32. [clojure.walk :as walk]))
  33. #?(:cljs (goog-define NODETEST false)
  34. :clj (def NODETEST false))
  35. (defonce node-test? NODETEST)
  36. #?(:cljs
  37. (extend-protocol IPrintWithWriter
  38. js/Symbol
  39. (-pr-writer [sym writer _]
  40. (-write writer (str "\"" (.toString sym) "\"")))))
  41. #?(:cljs (defonce ^js node-path utils/nodePath))
  42. #?(:cljs (defonce ^js full-path-extname utils/fullPathExtname))
  43. #?(:cljs (defn app-scroll-container-node
  44. ([]
  45. (gdom/getElement "main-content-container"))
  46. ([el]
  47. (if (.closest el "#main-content-container")
  48. (app-scroll-container-node)
  49. (or
  50. (gdom/getElementByClass "sidebar-item-list")
  51. (app-scroll-container-node))))))
  52. #?(:cljs
  53. (defn safe-re-find
  54. [pattern s]
  55. (when-not (string? s)
  56. ;; TODO: sentry
  57. (js/console.trace))
  58. (when (string? s)
  59. (re-find pattern s))))
  60. #?(:cljs
  61. (do
  62. (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}")
  63. (defonce exactly-uuid-pattern (re-pattern (str "(?i)^" uuid-pattern "$")))
  64. (defn uuid-string?
  65. [s]
  66. (safe-re-find exactly-uuid-pattern s))
  67. (defn check-password-strength [input]
  68. (when-let [^js ret (and (string? input)
  69. (not (string/blank? input))
  70. (passwordStrength input))]
  71. (bean/->clj ret)))
  72. (defn safe-sanitize-file-name [s]
  73. (sanitizeFilename (str s)))))
  74. #?(:cljs
  75. (defn ios?
  76. []
  77. (utils/ios)))
  78. #?(:cljs
  79. (defn safari?
  80. []
  81. (let [ua (string/lower-case js/navigator.userAgent)]
  82. (and (string/includes? ua "webkit")
  83. (not (string/includes? ua "chrome"))))))
  84. #?(:cljs
  85. (defn mobile?
  86. "Triggering condition: Mobile phones
  87. *** Warning!!! ***
  88. For UX logic only! Don't use for FS logic
  89. iPad / Android Pad doesn't trigger!"
  90. []
  91. (when-not node-test?
  92. (safe-re-find #"Mobi" js/navigator.userAgent))))
  93. #?(:cljs
  94. (defn electron?
  95. []
  96. (when (and js/window (gobj/get js/window "navigator"))
  97. (gstring/caseInsensitiveContains js/navigator.userAgent " electron"))))
  98. #?(:cljs
  99. (defn mocked-open-dir-path
  100. "Mocked open DIR path for by-passing open dir in electron during testing. Nil if not given"
  101. []
  102. (when (electron?) (. js/window -__MOCKED_OPEN_DIR_PATH__))))
  103. #?(:cljs
  104. (do
  105. (def nfs? (and (not (electron?))
  106. (not (native-platform?))))
  107. (def web-platform? nfs?)))
  108. #?(:cljs
  109. (defn file-protocol?
  110. []
  111. (string/starts-with? js/window.location.href "file://")))
  112. (defn format
  113. [fmt & args]
  114. #?(:cljs (apply gstring/format fmt args)
  115. :clj (apply clojure.core/format fmt args)))
  116. #?(:cljs
  117. (defn evalue
  118. [event]
  119. (gobj/getValueByKeys event "target" "value")))
  120. #?(:cljs
  121. (defn ekey [event]
  122. (gobj/getValueByKeys event "key")))
  123. #?(:cljs
  124. (defn echecked? [event]
  125. (gobj/getValueByKeys event "target" "checked")))
  126. #?(:cljs
  127. (defn set-change-value
  128. "compatible change event for React"
  129. [node value]
  130. (utils/triggerInputChange node value)))
  131. #?(:cljs
  132. (defn p-handle
  133. ([p ok-handler]
  134. (p-handle p ok-handler (fn [error]
  135. (js/console.error error))))
  136. ([p ok-handler error-handler]
  137. (-> p
  138. (p/then (fn [result]
  139. (ok-handler result)))
  140. (p/catch (fn [error]
  141. (error-handler error)))))))
  142. #?(:cljs
  143. (defn get-width
  144. []
  145. (gobj/get js/window "innerWidth")))
  146. #?(:cljs
  147. (defn set-theme-light
  148. []
  149. (p/do!
  150. (.setStyle StatusBar (clj->js {:style (.-Light Style)})))))
  151. #?(:cljs
  152. (defn set-theme-dark
  153. []
  154. (p/do!
  155. (.setStyle StatusBar (clj->js {:style (.-Dark Style)})))))
  156. (defn find-first
  157. [pred coll]
  158. (first (filter pred coll)))
  159. ;; (defn format
  160. ;; [fmt & args]
  161. ;; (apply gstring/format fmt args))
  162. (defn remove-nils-non-nested
  163. [nm]
  164. (into {} (remove (comp nil? second)) nm))
  165. (defn ext-of-image? [s]
  166. (some #(-> (string/lower-case s)
  167. (string/ends-with? %))
  168. [".png" ".jpg" ".jpeg" ".bmp" ".gif" ".webp" ".svg"]))
  169. ;; ".lg:absolute.lg:inset-y-0.lg:right-0.lg:w-1/2"
  170. (defn hiccup->class
  171. [class]
  172. (some->> (string/split class #"\.")
  173. (string/join " ")
  174. (string/trim)))
  175. #?(:cljs
  176. (defn fetch
  177. ([url on-ok on-failed]
  178. (fetch url {} on-ok on-failed))
  179. ([url opts on-ok on-failed]
  180. (-> (js/fetch url (bean/->js opts))
  181. (.then (fn [resp]
  182. (if (>= (.-status resp) 400)
  183. (on-failed resp)
  184. (if (.-ok resp)
  185. (-> (.json resp)
  186. (.then bean/->clj)
  187. (.then #(on-ok %)))
  188. (on-failed resp)))))))))
  189. #?(:cljs
  190. (defn upload
  191. [url file on-ok on-failed on-progress]
  192. (let [xhr (js/XMLHttpRequest.)]
  193. (.open xhr "put" url)
  194. (gobj/set xhr "onload" on-ok)
  195. (gobj/set xhr "onerror" on-failed)
  196. (when (and (gobj/get xhr "upload")
  197. on-progress)
  198. (gobj/set (gobj/get xhr "upload")
  199. "onprogress"
  200. on-progress))
  201. (.send xhr file))))
  202. #?(:cljs
  203. (defn post
  204. [url body on-ok on-failed]
  205. (fetch url {:method "post"
  206. :headers {:Content-Type "application/json"}
  207. :body (js/JSON.stringify (clj->js body))}
  208. on-ok
  209. on-failed)))
  210. (defn zero-pad
  211. [n]
  212. (if (< n 10)
  213. (str "0" n)
  214. (str n)))
  215. #?(:cljs
  216. (defn safe-parse-int
  217. "Use if arg could be an int or string. If arg is only a string, use `parse-long`."
  218. [x]
  219. (if (string? x)
  220. (parse-long x)
  221. x)))
  222. #?(:cljs
  223. (defn safe-parse-float
  224. "Use if arg could be a float or string. If arg is only a string, use `parse-double`"
  225. [x]
  226. (if (string? x)
  227. (parse-double x)
  228. x)))
  229. #?(:cljs
  230. (defn debounce
  231. "Returns a function that will call f only after threshold has passed without new calls
  232. to the function. Calls prep-fn on the args in a sync way, which can be used for things like
  233. calling .persist on the event object to be able to access the event attributes in f"
  234. ([threshold f] (debounce threshold f (constantly nil)))
  235. ([threshold f prep-fn]
  236. (let [t (atom nil)]
  237. (fn [& args]
  238. (when @t (js/clearTimeout @t))
  239. (apply prep-fn args)
  240. (reset! t (js/setTimeout #(do
  241. (reset! t nil)
  242. (apply f args))
  243. threshold)))))))
  244. (defn nth-safe [c i]
  245. (if (or (< i 0) (>= i (count c)))
  246. nil
  247. (nth c i)))
  248. #?(:cljs
  249. (when-not node-test?
  250. (extend-type js/NodeList
  251. ISeqable
  252. (-seq [array] (array-seq array 0)))))
  253. ;; Caret
  254. #?(:cljs
  255. (defn caret-range [node]
  256. (when-let [doc (or (gobj/get node "ownerDocument")
  257. (gobj/get node "document"))]
  258. (let [win (or (gobj/get doc "defaultView")
  259. (gobj/get doc "parentWindow"))
  260. selection (.getSelection win)]
  261. (if selection
  262. (let [range-count (gobj/get selection "rangeCount")]
  263. (when (> range-count 0)
  264. (let [range (-> (.getSelection win)
  265. (.getRangeAt 0))
  266. pre-caret-range (.cloneRange range)]
  267. (.selectNodeContents pre-caret-range node)
  268. (.setEnd pre-caret-range
  269. (gobj/get range "endContainer")
  270. (gobj/get range "endOffset"))
  271. (let [contents (.cloneContents pre-caret-range)
  272. html (some-> (first (.-childNodes contents))
  273. (gobj/get "innerHTML")
  274. str)
  275. ;; FIXME: this depends on the dom structure,
  276. ;; need a converter from html to text includes newlines
  277. br-ended? (and html
  278. (or
  279. ;; first line with a new line
  280. (string/ends-with? html "<div class=\"is-paragraph\"></div></div></span></div></div></div>")
  281. ;; multiple lines with a new line
  282. (string/ends-with? html "<br></div></div></span></div></div></div>")))
  283. value (.toString pre-caret-range)]
  284. (if br-ended?
  285. (str value "\n")
  286. value)))))
  287. (when-let [selection (gobj/get doc "selection")]
  288. (when (not= "Control" (gobj/get selection "type"))
  289. (let [text-range (.createRange selection)
  290. pre-caret-text-range (.createTextRange (gobj/get doc "body"))]
  291. (.moveToElementText pre-caret-text-range node)
  292. (.setEndPoint pre-caret-text-range "EndToEnd" text-range)
  293. (gobj/get pre-caret-text-range "text")))))))))
  294. (defn get-selection-start
  295. [input]
  296. (when input
  297. (.-selectionStart input)))
  298. (defn get-selection-end
  299. [input]
  300. (when input
  301. (.-selectionEnd input)))
  302. (defn input-text-selected?
  303. [input]
  304. (not= (get-selection-start input)
  305. (get-selection-end input)))
  306. (defn get-selection-direction
  307. [input]
  308. (when input
  309. (.-selectionDirection input)))
  310. (defn get-first-or-last-line-pos
  311. [input]
  312. (let [pos (get-selection-start input)
  313. value (.-value input)
  314. last-newline-pos (or (string/last-index-of value \newline (dec pos)) -1)]
  315. (- pos last-newline-pos 1)))
  316. #?(:cljs
  317. (defn stop [e]
  318. (when e (doto e (.preventDefault) (.stopPropagation)))))
  319. #?(:cljs
  320. (defn stop-propagation [e]
  321. (when e (.stopPropagation e))))
  322. #?(:cljs
  323. (defn element-top [elem top]
  324. (when elem
  325. (if (.-offsetParent elem)
  326. (let [client-top (or (.-clientTop elem) 0)
  327. offset-top (.-offsetTop elem)]
  328. (+ top client-top offset-top (element-top (.-offsetParent elem) top)))
  329. top))))
  330. #?(:cljs
  331. (defn scroll-to-element
  332. [elem-id]
  333. (when-not (safe-re-find #"^/\d+$" elem-id)
  334. (when elem-id
  335. (when-let [elem (gdom/getElement elem-id)]
  336. (.scroll (app-scroll-container-node)
  337. #js {:top (let [top (element-top elem 0)]
  338. (if (< top 256)
  339. 0
  340. (- top 80)))
  341. :behavior "smooth"}))))))
  342. #?(:cljs
  343. (defn scroll-to
  344. ([pos]
  345. (scroll-to (app-scroll-container-node) pos))
  346. ([node pos]
  347. (scroll-to node pos true))
  348. ([node pos animate?]
  349. (when node
  350. (.scroll node
  351. #js {:top pos
  352. :behavior (if animate? "smooth" "auto")})))))
  353. #?(:cljs
  354. (defn scroll-top
  355. "Returns the scroll top position of the `node`. If `node` is not specified,
  356. returns the scroll top position of the `app-scroll-container-node`."
  357. ([]
  358. (scroll-top (app-scroll-container-node)))
  359. ([node]
  360. (when node (.-scrollTop node)))))
  361. #?(:cljs
  362. (defn scroll-to-top
  363. ([]
  364. (scroll-to (app-scroll-container-node) 0 false))
  365. ([animate?]
  366. (scroll-to (app-scroll-container-node) 0 animate?))))
  367. #?(:cljs
  368. (defn link?
  369. [node]
  370. (contains?
  371. #{"A" "BUTTON"}
  372. (gobj/get node "tagName"))))
  373. #?(:cljs
  374. (defn time?
  375. [node]
  376. (contains?
  377. #{"TIME"}
  378. (gobj/get node "tagName"))))
  379. #?(:cljs
  380. (defn audio?
  381. [node]
  382. (contains?
  383. #{"AUDIO"}
  384. (gobj/get node "tagName"))))
  385. #?(:cljs
  386. (defn video?
  387. [node]
  388. (contains?
  389. #{"VIDEO"}
  390. (gobj/get node "tagName"))))
  391. #?(:cljs
  392. (defn sup?
  393. [node]
  394. (contains?
  395. #{"SUP"}
  396. (gobj/get node "tagName"))))
  397. #?(:cljs
  398. (defn input?
  399. [node]
  400. (when node
  401. (contains?
  402. #{"INPUT" "TEXTAREA"}
  403. (gobj/get node "tagName")))))
  404. #?(:cljs
  405. (defn select?
  406. [node]
  407. (when node
  408. (= "SELECT" (gobj/get node "tagName")))))
  409. #?(:cljs
  410. (defn details-or-summary?
  411. [node]
  412. (when node
  413. (contains?
  414. #{"DETAILS" "SUMMARY"}
  415. (gobj/get node "tagName")))))
  416. ;; Debug
  417. (defn starts-with?
  418. [s substr]
  419. (string/starts-with? s substr))
  420. (defn distinct-by
  421. [f col]
  422. (reduce
  423. (fn [acc x]
  424. (if (some #(= (f x) (f %)) acc)
  425. acc
  426. (vec (conj acc x))))
  427. []
  428. col))
  429. (defn distinct-by-last-wins
  430. [f col]
  431. (reduce
  432. (fn [acc x]
  433. (if (some #(= (f x) (f %)) acc)
  434. (mapv
  435. (fn [v]
  436. (if (= (f x) (f v))
  437. x
  438. v))
  439. acc)
  440. (vec (conj acc x))))
  441. []
  442. col))
  443. (defn get-git-owner-and-repo
  444. [repo-url]
  445. (take-last 2 (string/split repo-url #"/")))
  446. (defn safe-lower-case
  447. [s]
  448. (if (string? s)
  449. (string/lower-case s) s))
  450. #?(:cljs
  451. (defn safe-path-join [prefix & paths]
  452. (let [path (apply node-path.join (cons prefix paths))]
  453. (if (and (electron?) (gstring/caseInsensitiveStartsWith path "file://"))
  454. (gp-util/safe-decode-uri-component (subs path 7))
  455. path))))
  456. (defn trim-safe
  457. [s]
  458. (when s
  459. (string/trim s)))
  460. (defn trimr-without-newlines
  461. [s]
  462. (.replace s #"[ \t\r]+$" ""))
  463. (defn triml-without-newlines
  464. [s]
  465. (.replace s #"^[ \t\r]+" ""))
  466. (defn concat-without-spaces
  467. [left right]
  468. (when (and (string? left)
  469. (string? right))
  470. (let [left (trimr-without-newlines left)
  471. not-space? (or
  472. (string/blank? left)
  473. (= "\n" (last left)))]
  474. (str left
  475. (when-not not-space? " ")
  476. (triml-without-newlines right)))))
  477. ;; Add documentation
  478. (defn replace-first [pattern s new-value]
  479. (if-let [first-index (string/index-of s pattern)]
  480. (str new-value (subs s (+ first-index (count pattern))))
  481. s))
  482. (defn replace-last
  483. ([pattern s new-value]
  484. (replace-last pattern s new-value true))
  485. ([pattern s new-value space?]
  486. (if-let [last-index (string/last-index-of s pattern)]
  487. (let [prefix (subs s 0 last-index)]
  488. (if space?
  489. (concat-without-spaces prefix new-value)
  490. (str prefix new-value)))
  491. s)))
  492. (defonce escape-chars "[]{}().+*?|")
  493. (defn escape-regex-chars
  494. "Escapes characters in string `old-value"
  495. [old-value]
  496. (reduce (fn [acc escape-char]
  497. (string/replace acc escape-char (str "\\" escape-char)))
  498. old-value escape-chars))
  499. (defn replace-ignore-case
  500. [s old-value new-value]
  501. (string/replace s (re-pattern (str "(?i)" (escape-regex-chars old-value))) new-value))
  502. ;; copy from https://stackoverflow.com/questions/18735665/how-can-i-get-the-positions-of-regex-matches-in-clojurescript
  503. #?(:cljs
  504. (defn re-pos [re s]
  505. (let [re (js/RegExp. (.-source re) "g")]
  506. (loop [res []]
  507. (if-let [m (.exec re s)]
  508. (recur (conj res [(.-index m) (first m)]))
  509. res)))))
  510. #?(:cljs
  511. (defn safe-set-range-text!
  512. ([input text start end]
  513. (try
  514. (.setRangeText input text start end)
  515. (catch :default _e
  516. nil)))
  517. ([input text start end select-mode]
  518. (try
  519. (.setRangeText input text start end select-mode)
  520. (catch :default _e
  521. nil)))))
  522. #?(:cljs
  523. ;; for widen char
  524. (defn safe-dec-current-pos-from-end
  525. [input current-pos]
  526. (if-let [len (and (string? input) (.-length input))]
  527. (when-let [input (and (>= len 2) (<= current-pos len)
  528. (.substring input (max (- current-pos 20) 0) current-pos))]
  529. (try
  530. (let [^js splitter (GraphemeSplitter.)
  531. ^js input (.splitGraphemes splitter input)]
  532. (- current-pos (.-length (.pop input))))
  533. (catch :default e
  534. (js/console.error e)
  535. (dec current-pos))))
  536. (dec current-pos))))
  537. #?(:cljs
  538. ;; for widen char
  539. (defn safe-inc-current-pos-from-start
  540. [input current-pos]
  541. (if-let [len (and (string? input) (.-length input))]
  542. (when-let [input (and (>= len 2) (<= current-pos len)
  543. (.substr input current-pos 20))]
  544. (try
  545. (let [^js splitter (GraphemeSplitter.)
  546. ^js input (.splitGraphemes splitter input)]
  547. (+ current-pos (.-length (.shift input))))
  548. (catch :default e
  549. (js/console.error e)
  550. (inc current-pos))))
  551. (inc current-pos))))
  552. #?(:cljs
  553. (defn kill-line-before!
  554. [input]
  555. (let [val (.-value input)
  556. end (get-selection-start input)
  557. n-pos (string/last-index-of val \newline (dec end))
  558. start (if n-pos (inc n-pos) 0)]
  559. (safe-set-range-text! input "" start end))))
  560. #?(:cljs
  561. (defn kill-line-after!
  562. [input]
  563. (let [val (.-value input)
  564. start (get-selection-start input)
  565. end (or (string/index-of val \newline start)
  566. (count val))]
  567. (safe-set-range-text! input "" start end))))
  568. #?(:cljs
  569. (defn insert-at-current-position!
  570. [input text]
  571. (let [start (get-selection-start input)
  572. end (get-selection-end input)]
  573. (safe-set-range-text! input text start end "end"))))
  574. ;; copied from re_com
  575. #?(:cljs
  576. (defn deref-or-value
  577. "Takes a value or an atom
  578. If it's a value, returns it
  579. If it's a Reagent object that supports IDeref, returns the value inside it by derefing
  580. "
  581. [val-or-atom]
  582. (if (satisfies? IDeref val-or-atom)
  583. @val-or-atom
  584. val-or-atom)))
  585. ;; copied from re_com
  586. #?(:cljs
  587. (defn now->utc
  588. "Return a goog.date.UtcDateTime based on local date/time."
  589. []
  590. (let [local-date-time (js/goog.date.DateTime.)]
  591. (js/goog.date.UtcDateTime.
  592. (.getYear local-date-time)
  593. (.getMonth local-date-time)
  594. (.getDate local-date-time)
  595. 0 0 0 0))))
  596. (defn safe-subvec [xs start end]
  597. (if (or (neg? start)
  598. (> end (count xs)))
  599. []
  600. (subvec xs start end)))
  601. #?(:cljs
  602. (defn get-nodes-between-two-nodes
  603. [id1 id2 class]
  604. (when-let [nodes (array-seq (js/document.getElementsByClassName class))]
  605. (let [node-1 (gdom/getElement id1)
  606. node-2 (gdom/getElement id2)
  607. idx-1 (.indexOf nodes node-1)
  608. idx-2 (.indexOf nodes node-2)
  609. start (min idx-1 idx-2)
  610. end (inc (max idx-1 idx-2))]
  611. (safe-subvec (vec nodes) start end)))))
  612. #?(:cljs
  613. (defn get-direction-between-two-nodes
  614. [id1 id2 class]
  615. (when-let [nodes (array-seq (js/document.getElementsByClassName class))]
  616. (let [node-1 (gdom/getElement id1)
  617. node-2 (gdom/getElement id2)
  618. idx-1 (.indexOf nodes node-1)
  619. idx-2 (.indexOf nodes node-2)]
  620. (if (>= idx-1 idx-2)
  621. :up
  622. :down)))))
  623. #?(:cljs
  624. (defn rec-get-tippy-container
  625. [node]
  626. (if (and node (d/has-class? node "tippy-tooltip-content"))
  627. node
  628. (and node
  629. (rec-get-tippy-container (gobj/get node "parentNode"))))))
  630. #?(:cljs
  631. (defn rec-get-blocks-container
  632. [node]
  633. (if (and node (d/has-class? node "blocks-container"))
  634. node
  635. (and node
  636. (rec-get-blocks-container (gobj/get node "parentNode"))))))
  637. #?(:cljs
  638. (defn rec-get-blocks-content-section
  639. [node]
  640. (if (and node (d/has-class? node "content"))
  641. node
  642. (and node
  643. (rec-get-blocks-content-section (gobj/get node "parentNode"))))))
  644. #?(:cljs
  645. (defn get-blocks-noncollapse []
  646. (->> (d/sel "div:not(.reveal) .ls-block")
  647. (filter (fn [b] (some? (gobj/get b "offsetParent")))))))
  648. #?(:cljs
  649. (defn remove-embeded-blocks [blocks]
  650. (->> blocks
  651. (remove (fn [b] (= "true" (d/attr b "data-embed")))))))
  652. #?(:cljs
  653. (defn get-selected-text
  654. []
  655. (utils/getSelectionText)))
  656. #?(:cljs (def clear-selection! selection/clearSelection))
  657. #?(:cljs
  658. (defn copy-to-clipboard!
  659. ([s]
  660. (utils/writeClipboard (clj->js {:text s})))
  661. ([s html]
  662. (utils/writeClipboard (clj->js {:text s :html html})))))
  663. (defn drop-nth [n coll]
  664. (keep-indexed #(when (not= %1 n) %2) coll))
  665. #?(:cljs
  666. (defn react
  667. [ref]
  668. (if rum/*reactions*
  669. (rum/react ref)
  670. @ref)))
  671. (defn time-ms
  672. []
  673. #?(:cljs (tc/to-long (t/now))))
  674. ;; Returns the milliseconds representation of the provided time, in the local timezone.
  675. ;; For example, if you run this function at 10pm EDT in the EDT timezone on May 31st,
  676. ;; it will return 1622433600000, which is equivalent to Mon May 31 2021 00 :00:00.
  677. #?(:cljs
  678. (defn today-at-local-ms [hours mins secs millisecs]
  679. (.setHours (js/Date. (.now js/Date)) hours mins secs millisecs)))
  680. (defn d
  681. [k f]
  682. (let [result (atom nil)]
  683. (println (str "Debug " k))
  684. (time (reset! result (doall (f))))
  685. @result))
  686. (defn concat-without-nil
  687. [& cols]
  688. (->> (apply concat cols)
  689. (remove nil?)))
  690. #?(:cljs
  691. (defn set-title!
  692. [title]
  693. (set! (.-title js/document) title)))
  694. #?(:cljs
  695. (defn get-block-container
  696. [block-element]
  697. (when block-element
  698. (when-let [section (some-> (rec-get-blocks-content-section block-element)
  699. (d/parent))]
  700. (when section
  701. (gdom/getElement section "id"))))))
  702. #?(:cljs
  703. (defn get-prev-block-non-collapsed
  704. [block]
  705. (when-let [blocks (get-blocks-noncollapse)]
  706. (let [block-id (.-id block)
  707. block-ids (mapv #(.-id %) blocks)]
  708. (when-let [index (.indexOf block-ids block-id)]
  709. (let [idx (dec index)]
  710. (when (>= idx 0)
  711. (nth-safe blocks idx))))))))
  712. #?(:cljs
  713. (defn get-prev-block-non-collapsed-non-embed
  714. [block]
  715. (when-let [blocks (->> (get-blocks-noncollapse)
  716. remove-embeded-blocks)]
  717. (let [block-id (.-id block)
  718. block-ids (mapv #(.-id %) blocks)]
  719. (when-let [index (.indexOf block-ids block-id)]
  720. (let [idx (dec index)]
  721. (when (>= idx 0)
  722. (nth-safe blocks idx))))))))
  723. #?(:cljs
  724. (defn get-next-block-non-collapsed
  725. [block]
  726. (when-let [blocks (get-blocks-noncollapse)]
  727. (let [block-id (.-id block)
  728. block-ids (mapv #(.-id %) blocks)]
  729. (when-let [index (.indexOf block-ids block-id)]
  730. (let [idx (inc index)]
  731. (when (>= (count blocks) idx)
  732. (nth-safe blocks idx))))))))
  733. #?(:cljs
  734. (defn get-next-block-non-collapsed-skip
  735. [block]
  736. (when-let [blocks (get-blocks-noncollapse)]
  737. (let [block-id (.-id block)
  738. block-ids (mapv #(.-id %) blocks)]
  739. (when-let [index (.indexOf block-ids block-id)]
  740. (loop [idx (inc index)]
  741. (when (>= (count blocks) idx)
  742. (let [block (nth-safe blocks idx)
  743. nested? (->> (array-seq (gdom/getElementsByClass "selected"))
  744. (some (fn [dom] (.contains dom block))))]
  745. (if nested?
  746. (recur (inc idx))
  747. block)))))))))
  748. (defn rand-str
  749. [n]
  750. #?(:cljs (-> (.toString (js/Math.random) 36)
  751. (.substr 2 n))
  752. :clj (->> (repeatedly #(Integer/toString (rand 36) 36))
  753. (take n)
  754. (apply str))))
  755. (defn unique-id
  756. []
  757. (str (rand-str 6) (rand-str 3)))
  758. (defn pp-str [x]
  759. #_:clj-kondo/ignore
  760. (with-out-str (clojure.pprint/pprint x)))
  761. (defn hiccup-keywordize
  762. [hiccup]
  763. (walk/postwalk
  764. (fn [f]
  765. (if (and (vector? f) (string? (first f)))
  766. (update f 0 keyword)
  767. f))
  768. hiccup))
  769. #?(:cljs
  770. (defn chrome?
  771. []
  772. (let [user-agent js/navigator.userAgent
  773. vendor js/navigator.vendor]
  774. (and (safe-re-find #"Chrome" user-agent)
  775. (safe-re-find #"Google Inc" vendor)))))
  776. #?(:cljs
  777. (defn indexeddb-check?
  778. [error-handler]
  779. (let [test-db "logseq-test-db-foo-bar-baz"
  780. db (and js/window.indexedDB
  781. (js/window.indexedDB.open test-db))]
  782. (when (and db (not (chrome?)))
  783. (gobj/set db "onerror" error-handler)
  784. (gobj/set db "onsuccess"
  785. (fn []
  786. (js/window.indexedDB.deleteDatabase test-db)))))))
  787. (defonce mac? #?(:cljs goog.userAgent/MAC
  788. :clj nil))
  789. (defonce win32? #?(:cljs goog.userAgent/WINDOWS
  790. :clj nil))
  791. #?(:cljs
  792. (defn absolute-path?
  793. [path]
  794. (try
  795. (js/window.apis.isAbsolutePath path)
  796. (catch :default _
  797. (utils/win32 path)))))
  798. (defn default-content-with-title
  799. [text-format]
  800. (case (name text-format)
  801. "org"
  802. "* "
  803. "- "))
  804. #?(:cljs
  805. (defn get-first-block-by-id
  806. [block-id]
  807. (when block-id
  808. (let [block-id (str block-id)]
  809. (when (uuid-string? block-id)
  810. (first (array-seq (js/document.getElementsByClassName block-id))))))))
  811. #?(:cljs
  812. (defn url-encode
  813. [string]
  814. (some-> string str (js/encodeURIComponent) (.replace "+" "%20"))))
  815. #?(:cljs
  816. (defn search-normalize
  817. "Normalize string for searching (loose)"
  818. [s remove-accents?]
  819. (let [normalize-str (.normalize (string/lower-case s) "NFKC")]
  820. (if remove-accents?
  821. (removeAccents normalize-str)
  822. normalize-str))))
  823. #?(:cljs
  824. (def page-name-sanity-lc
  825. "Delegate to gp-util to loosely couple app usages to graph-parser"
  826. gp-util/page-name-sanity-lc))
  827. #?(:cljs
  828. (defn safe-page-name-sanity-lc
  829. [s]
  830. (if (string? s)
  831. (page-name-sanity-lc s) s)))
  832. (defn get-page-original-name
  833. [page]
  834. (or (:block/original-name page)
  835. (:block/name page)))
  836. #?(:cljs
  837. (defn add-style!
  838. [style]
  839. (when (some? style)
  840. (let [parent-node (d/sel1 :head)
  841. id "logseq-custom-theme-id"
  842. old-link-element (d/sel1 (str "#" id))
  843. style (if (string/starts-with? style "http")
  844. style
  845. (str "data:text/css;charset=utf-8," (js/encodeURIComponent style)))]
  846. (when old-link-element
  847. (d/remove! old-link-element))
  848. (let [link (->
  849. (d/create-element :link)
  850. (d/set-attr! :id id)
  851. (d/set-attr! :rel "stylesheet")
  852. (d/set-attr! :type "text/css")
  853. (d/set-attr! :href style)
  854. (d/set-attr! :media "all"))]
  855. (d/append! parent-node link))))))
  856. (defn ->platform-shortcut
  857. [keyboard-shortcut]
  858. (let [result (or keyboard-shortcut "")
  859. result (string/replace result "left" "←")
  860. result (string/replace result "right" "→")]
  861. (if mac?
  862. (-> result
  863. (string/replace "Ctrl" "Cmd")
  864. (string/replace "Alt" "Opt"))
  865. result)))
  866. (defn remove-common-preceding
  867. [col1 col2]
  868. (if (and (= (first col1) (first col2))
  869. (seq col1))
  870. (recur (rest col1) (rest col2))
  871. [col1 col2]))
  872. ;; fs
  873. #?(:cljs
  874. (defn get-file-ext
  875. [file]
  876. (and
  877. (string? file)
  878. (string/includes? file ".")
  879. (some-> (gp-util/path->file-ext file) string/lower-case))))
  880. (defn get-dir-and-basename
  881. [path]
  882. (let [parts (string/split path "/")
  883. basename (last parts)
  884. dir (->> (butlast parts)
  885. (string/join "/"))]
  886. [dir basename]))
  887. (defn get-relative-path
  888. [current-file-path another-file-path]
  889. (let [directories-f #(butlast (string/split % "/"))
  890. parts-1 (directories-f current-file-path)
  891. parts-2 (directories-f another-file-path)
  892. [parts-1 parts-2] (remove-common-preceding parts-1 parts-2)
  893. another-file-name (last (string/split another-file-path "/"))]
  894. (->> (concat
  895. (if (seq parts-1)
  896. (repeat (count parts-1) "..")
  897. ["."])
  898. parts-2
  899. [another-file-name])
  900. (string/join "/"))))
  901. ;; Copied from https://github.com/tonsky/datascript-todo
  902. #?(:clj
  903. (defmacro profile
  904. [k & body]
  905. `(if goog.DEBUG
  906. (let [k# ~k]
  907. (.time js/console k#)
  908. (let [res# (do ~@body)]
  909. (.timeEnd js/console k#)
  910. res#))
  911. (do ~@body))))
  912. #?(:clj
  913. (defmacro with-time
  914. "Evaluates expr and prints the time it took. Returns the value of expr and the spent time."
  915. [expr]
  916. `(let [start# (cljs.core/system-time)
  917. ret# ~expr]
  918. {:result ret#
  919. :time (.toFixed (- (cljs.core/system-time) start#) 6)})))
  920. ;; TODO: profile and profileEnd
  921. ;; Copy from hiccup but tweaked for publish usage
  922. (defn escape-html
  923. "Change special characters into HTML character entities."
  924. [text]
  925. (-> text
  926. (string/replace "&" "logseq____&amp;")
  927. (string/replace "<" "logseq____&lt;")
  928. (string/replace ">" "logseq____&gt;")
  929. (string/replace "\"" "logseq____&quot;")
  930. (string/replace "'" "logseq____&apos;")))
  931. (defn unescape-html
  932. [text]
  933. (-> text
  934. (string/replace "logseq____&amp;" "&")
  935. (string/replace "logseq____&lt;" "<")
  936. (string/replace "logseq____&gt;" ">")
  937. (string/replace "logseq____&quot;" "\"")
  938. (string/replace "logseq____&apos;" "'")))
  939. (comment
  940. (= (get-relative-path "journals/2020_11_18.org" "pages/grant_ideas.org")
  941. "../pages/grant_ideas.org")
  942. (= (get-relative-path "journals/2020_11_18.org" "journals/2020_11_19.org")
  943. "./2020_11_19.org")
  944. (= (get-relative-path "a/b/c/d/g.org" "a/b/c/e/f.org")
  945. "../e/f.org"))
  946. (defn keyname [key] (str (namespace key) "/" (name key)))
  947. #?(:cljs
  948. (defn select-highlight!
  949. [blocks]
  950. (doseq [block blocks]
  951. (d/add-class! block "selected noselect"))))
  952. #?(:cljs
  953. (defn select-unhighlight!
  954. [blocks]
  955. (doseq [block blocks]
  956. (d/remove-class! block "selected" "noselect"))))
  957. #?(:cljs
  958. (defn drain-chan
  959. "drop all stuffs in CH, and return all of them"
  960. [ch]
  961. (->> (repeatedly #(async/poll! ch))
  962. (take-while identity))))
  963. #?(:cljs
  964. (defn <ratelimit
  965. "return a channel CH,
  966. ratelimit flush items in in-ch every max-duration(ms),
  967. opts:
  968. - :filter-fn filter item before putting items into returned CH, (filter-fn item)
  969. will poll it when its return value is channel,
  970. - :flush-fn exec flush-fn when time to flush, (flush-fn item-coll)
  971. - :stop-ch stop go-loop when stop-ch closed
  972. - :distinct-coll? distinct coll when put into CH
  973. - :chan-buffer buffer of return CH, default use (async/chan 1000)
  974. - :flush-now-ch flush the content in the queue immediately
  975. - :refresh-timeout-ch refresh (timeout max-duration)"
  976. [in-ch max-duration & {:keys [filter-fn flush-fn stop-ch distinct-coll? chan-buffer flush-now-ch refresh-timeout-ch]}]
  977. (let [ch (if chan-buffer (async/chan chan-buffer) (async/chan 1000))
  978. stop-ch* (or stop-ch (async/chan))
  979. flush-now-ch* (or flush-now-ch (async/chan))
  980. refresh-timeout-ch* (or refresh-timeout-ch (async/chan))]
  981. (async/go-loop [timeout-ch (async/timeout max-duration) coll []]
  982. (let [{:keys [refresh-timeout timeout e stop flush-now]}
  983. (async/alt! refresh-timeout-ch* {:refresh-timeout true}
  984. timeout-ch {:timeout true}
  985. in-ch ([e] {:e e})
  986. stop-ch* {:stop true}
  987. flush-now-ch* {:flush-now true})]
  988. (cond
  989. refresh-timeout
  990. (recur (async/timeout max-duration) coll)
  991. (or flush-now timeout)
  992. (do (async/onto-chan! ch coll false)
  993. (flush-fn coll)
  994. (drain-chan flush-now-ch*)
  995. (recur (async/timeout max-duration) []))
  996. (some? e)
  997. (let [filter-v (filter-fn e)
  998. filter-v* (if (instance? ManyToManyChannel filter-v)
  999. (async/<! filter-v)
  1000. filter-v)]
  1001. (if filter-v*
  1002. (recur timeout-ch (cond-> (conj coll e)
  1003. distinct-coll? distinct
  1004. true vec))
  1005. (recur timeout-ch coll)))
  1006. (or stop
  1007. ;; got nil from in-ch, means in-ch is closed
  1008. ;; so we stop the whole go-loop
  1009. (nil? e))
  1010. (async/close! ch))))
  1011. ch)))
  1012. #?(:cljs
  1013. (defn trace!
  1014. []
  1015. (js/console.trace)))
  1016. (defn remove-first [pred coll]
  1017. ((fn inner [coll]
  1018. (lazy-seq
  1019. (when-let [[x & xs] (seq coll)]
  1020. (if (pred x)
  1021. xs
  1022. (cons x (inner xs))))))
  1023. coll))
  1024. (def pprint clojure.pprint/pprint)
  1025. #?(:cljs
  1026. (defn backward-kill-word
  1027. [input]
  1028. (let [val (.-value input)
  1029. current (get-selection-start input)
  1030. prev (or
  1031. (->> [(string/last-index-of val \space (dec current))
  1032. (string/last-index-of val \newline (dec current))]
  1033. (remove nil?)
  1034. (apply max))
  1035. 0)
  1036. idx (if (zero? prev)
  1037. 0
  1038. (->
  1039. (loop [idx prev]
  1040. (if (#{\space \newline} (nth-safe val idx))
  1041. (recur (dec idx))
  1042. idx))
  1043. inc))]
  1044. (safe-set-range-text! input "" idx current))))
  1045. #?(:cljs
  1046. (defn forward-kill-word
  1047. [input]
  1048. (let [val (.-value input)
  1049. current (get-selection-start input)
  1050. current (loop [idx current]
  1051. (if (#{\space \newline} (nth-safe val idx))
  1052. (recur (inc idx))
  1053. idx))
  1054. idx (or (->> [(string/index-of val \space current)
  1055. (string/index-of val \newline current)]
  1056. (remove nil?)
  1057. (apply min))
  1058. (count val))]
  1059. (safe-set-range-text! input "" current (inc idx)))))
  1060. #?(:cljs
  1061. (defn fix-open-external-with-shift!
  1062. [^js/MouseEvent e]
  1063. (when (and (.-shiftKey e) win32? (electron?)
  1064. (= (string/lower-case (.. e -target -nodeName)) "a")
  1065. (string/starts-with? (.. e -target -href) "file:"))
  1066. (.preventDefault e))))
  1067. (defn classnames
  1068. "Like react classnames utility:
  1069. ```
  1070. [:div {:class (classnames [:a :b {:c true}])}
  1071. ```
  1072. "
  1073. [args]
  1074. (into #{} (mapcat
  1075. #(if (map? %)
  1076. (for [[k v] %]
  1077. (when v (name k)))
  1078. (when-not (nil? %) [(name %)]))
  1079. args)))
  1080. #?(:cljs
  1081. (defn- get-dom-top
  1082. [node]
  1083. (gobj/get (.getBoundingClientRect node) "top")))
  1084. #?(:cljs
  1085. (defn sort-by-height
  1086. [elements]
  1087. (sort (fn [x y]
  1088. (< (get-dom-top x) (get-dom-top y)))
  1089. elements)))
  1090. #?(:cljs
  1091. (defn calc-delta-rect-offset
  1092. [^js/HTMLElement target ^js/HTMLElement container]
  1093. (let [target-rect (bean/->clj (.toJSON (.getBoundingClientRect target)))
  1094. viewport-rect {:width (.-clientWidth container)
  1095. :height (.-clientHeight container)}]
  1096. {:y (- (:height viewport-rect) (:bottom target-rect))
  1097. :x (- (:width viewport-rect) (:right target-rect))})))
  1098. (def regex-char-esc-smap
  1099. (let [esc-chars "{}[]()&^%$#!?*.+|\\"]
  1100. (zipmap esc-chars
  1101. (map #(str "\\" %) esc-chars))))
  1102. (defn regex-escape
  1103. "Escape all regex meta chars in text."
  1104. [text]
  1105. (string/join (replace regex-char-esc-smap text)))
  1106. (comment
  1107. (re-matches (re-pattern (regex-escape "$u^8(d)+w.*[dw]d?")) "$u^8(d)+w.*[dw]d?"))
  1108. #?(:cljs
  1109. (defn meta-key-name []
  1110. (if mac? "Cmd" "Ctrl")))
  1111. #?(:cljs
  1112. (defn meta-key? [e]
  1113. (if mac?
  1114. (gobj/get e "metaKey")
  1115. (gobj/get e "ctrlKey"))))
  1116. #?(:cljs
  1117. (defn right-click?
  1118. [e]
  1119. (let [which (gobj/get e "which")
  1120. button (gobj/get e "button")]
  1121. (or (= which 3)
  1122. (= button 2)))))
  1123. (def keyboard-height (atom nil))
  1124. #?(:cljs
  1125. (defn scroll-editor-cursor
  1126. [^js/HTMLElement el & {:keys [to-vw-one-quarter?]}]
  1127. (when (and el (or (native-platform?) mobile?))
  1128. (let [box-rect (.getBoundingClientRect el)
  1129. box-top (.-top box-rect)
  1130. box-bottom (.-bottom box-rect)
  1131. header-height (-> (gdom/getElementByClass "cp__header")
  1132. .-clientHeight)
  1133. main-node (app-scroll-container-node el)
  1134. scroll-top (.-scrollTop main-node)
  1135. current-pos (get-selection-start el)
  1136. mock-text (some-> (gdom/getElement "mock-text")
  1137. gdom/getChildren
  1138. array-seq
  1139. (nth-safe current-pos))
  1140. offset-top (and mock-text (.-offsetTop mock-text))
  1141. offset-height (and mock-text (.-offsetHeight mock-text))
  1142. cursor-y (if offset-top (+ offset-top box-top offset-height 2) box-bottom)
  1143. vw-height (or (.-height js/window.visualViewport)
  1144. (.-clientHeight js/document.documentElement))
  1145. ;; mobile toolbar height: 40px
  1146. scroll (- cursor-y (- vw-height (+ @keyboard-height 40)))]
  1147. (cond
  1148. (and to-vw-one-quarter? (> cursor-y (* vw-height 0.4)))
  1149. (set! (.-scrollTop main-node) (+ scroll-top (- cursor-y (/ vw-height 4))))
  1150. (and (< cursor-y (+ header-height offset-height 4)) ;; 4 is top+bottom padding for per line
  1151. (>= cursor-y header-height))
  1152. (.scrollBy main-node (bean/->js {:top (- (+ offset-height 4))}))
  1153. (< cursor-y header-height)
  1154. (let [_ (.scrollIntoView el true)
  1155. main-node (app-scroll-container-node el)
  1156. scroll-top (.-scrollTop main-node)]
  1157. (set! (.-scrollTop main-node) (- scroll-top (/ vw-height 4))))
  1158. (> scroll 0)
  1159. (set! (.-scrollTop main-node) (+ scroll-top scroll))
  1160. :else
  1161. nil)))))
  1162. #?(:cljs
  1163. (defn sm-breakpoint?
  1164. []
  1165. (< (.-offsetWidth js/document.documentElement) 640)))
  1166. #?(:cljs
  1167. (defn event-is-composing?
  1168. "Check if keydown event is a composing (IME) event.
  1169. Ignore the IME process by default."
  1170. ([e]
  1171. (event-is-composing? e false))
  1172. ([e include-process?]
  1173. (let [event-composing? (gobj/getValueByKeys e "event_" "isComposing")]
  1174. (if include-process?
  1175. (or event-composing?
  1176. (= (gobj/get e "keyCode") 229)
  1177. (= (gobj/get e "key") "Process"))
  1178. event-composing?)))))
  1179. #?(:cljs
  1180. (defn onchange-event-is-composing?
  1181. "Check if onchange event of Input is a composing (IME) event.
  1182. Always ignore the IME process."
  1183. [e]
  1184. (gobj/getValueByKeys e "nativeEvent" "isComposing"))) ;; No keycode available
  1185. #?(:cljs
  1186. (defn open-url
  1187. [url]
  1188. (let [route? (or (string/starts-with? url
  1189. (string/replace js/location.href js/location.hash ""))
  1190. (string/starts-with? url "#"))]
  1191. (if (and (not route?) (electron?))
  1192. (js/window.apis.openExternal url)
  1193. (set! (.-href js/window.location) url)))))
  1194. (defn collapsed?
  1195. [block]
  1196. (:block/collapsed? block))
  1197. #?(:cljs
  1198. (defn atom? [v]
  1199. (instance? Atom v)))
  1200. ;; https://stackoverflow.com/questions/32511405/how-would-time-ago-function-implementation-look-like-in-clojure
  1201. #?(:cljs
  1202. (defn time-ago
  1203. "time: inst-ms or js/Date"
  1204. [time]
  1205. (let [units [{:name "second" :limit 60 :in-second 1}
  1206. {:name "minute" :limit 3600 :in-second 60}
  1207. {:name "hour" :limit 86400 :in-second 3600}
  1208. {:name "day" :limit 604800 :in-second 86400}
  1209. {:name "week" :limit 2629743 :in-second 604800}
  1210. {:name "month" :limit 31556926 :in-second 2629743}
  1211. {:name "year" :limit js/Number.MAX_SAFE_INTEGER :in-second 31556926}]
  1212. diff (t/in-seconds (t/interval (if (instance? js/Date time) time (js/Date. time)) (t/now)))]
  1213. (if (< diff 5)
  1214. "just now"
  1215. (let [unit (first (drop-while #(or (>= diff (:limit %))
  1216. (not (:limit %)))
  1217. units))]
  1218. (-> (/ diff (:in-second unit))
  1219. Math/floor
  1220. int
  1221. (#(str % " " (:name unit) (when (> % 1) "s") " ago"))))))))
  1222. #?(:cljs
  1223. (def JS_ROOT
  1224. (when-not node-test?
  1225. (if (= js/location.protocol "file:")
  1226. "./js"
  1227. "./static/js"))))
  1228. #?(:cljs
  1229. (defn js-load$
  1230. [url]
  1231. (p/create
  1232. (fn [resolve]
  1233. (load url resolve)))))
  1234. #?(:cljs
  1235. (defn element-visible?
  1236. [element]
  1237. (when element
  1238. (when-let [r (.getBoundingClientRect element)]
  1239. (and (>= (.-top r) 0)
  1240. (<= (+ (.-bottom r) 64)
  1241. (or (.-innerHeight js/window)
  1242. (js/document.documentElement.clientHeight))))))))
  1243. #?(:cljs
  1244. (defn copy-image-to-clipboard
  1245. [src]
  1246. (-> (js/fetch src)
  1247. (.then (fn [data]
  1248. (-> (.blob data)
  1249. (.then (fn [blob]
  1250. (js/navigator.clipboard.write (clj->js [(js/ClipboardItem. (clj->js {(.-type blob) blob}))]))))
  1251. (.catch js/console.error)))))))