util.cljc 43 KB

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