util.cljc 48 KB

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