util.cljc 43 KB

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