util.cljc 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376
  1. (ns frontend.util
  2. #?(:clj (:refer-clojure :exclude [format]))
  3. #?(:cljs (:require-macros [frontend.util]))
  4. #?(:cljs (:require
  5. ["/frontend/selection" :as selection]
  6. ["/frontend/utils" :as utils]
  7. [camel-snake-kebab.core :as csk]
  8. [camel-snake-kebab.extras :as cske]
  9. [cljs-bean.core :as bean]
  10. [cljs-time.coerce :as tc]
  11. [cljs-time.core :as t]
  12. [dommy.core :as d]
  13. [frontend.react-impls :as react-impls]
  14. [goog.dom :as gdom]
  15. [goog.object :as gobj]
  16. [goog.string :as gstring]
  17. [goog.userAgent]
  18. ["path" :as nodePath]
  19. [promesa.core :as p]))
  20. (:require
  21. [clojure.core.async :as async]
  22. [clojure.pprint]
  23. [clojure.string :as string]
  24. [clojure.walk :as walk]))
  25. #?(:cljs (goog-define NODETEST false)
  26. :clj (def NODETEST false))
  27. (defonce node-test? NODETEST)
  28. #?(:cljs
  29. (extend-protocol IPrintWithWriter
  30. js/Symbol
  31. (-pr-writer [sym writer _]
  32. (-write writer (str "\"" (.toString sym) "\"")))))
  33. #?(:cljs (defonce ^js node-path nodePath))
  34. #?(:cljs (defn app-scroll-container-node []
  35. (gdom/getElement "main-container")))
  36. #?(:cljs
  37. (defn ios?
  38. []
  39. (utils/ios)))
  40. #?(:cljs
  41. (defn safari?
  42. []
  43. (let [ua (string/lower-case js/navigator.userAgent)]
  44. (and (string/includes? ua "webkit")
  45. (not (string/includes? ua "chrome"))))))
  46. (defn safe-re-find
  47. [pattern s]
  48. #?(:cljs
  49. (when-not (string? s)
  50. ;; TODO: sentry
  51. (js/console.trace)))
  52. (when (string? s)
  53. (re-find pattern s)))
  54. #?(:cljs
  55. (defn mobile?
  56. []
  57. (when-not node-test?
  58. (safe-re-find #"Mobi" js/navigator.userAgent))))
  59. #?(:cljs
  60. (defn electron?
  61. []
  62. (when (and js/window (gobj/get js/window "navigator"))
  63. (let [ua (string/lower-case js/navigator.userAgent)]
  64. (string/includes? ua " electron")))))
  65. #?(:cljs
  66. (defn file-protocol?
  67. []
  68. (string/starts-with? js/window.location.href "file://")))
  69. (defn format
  70. [fmt & args]
  71. #?(:cljs (apply gstring/format fmt args)
  72. :clj (apply clojure.core/format fmt args)))
  73. #?(:cljs
  74. (defn evalue
  75. [event]
  76. (gobj/getValueByKeys event "target" "value")))
  77. #?(:cljs
  78. (defn ekey [event]
  79. (gobj/getValueByKeys event "key")))
  80. #?(:cljs
  81. (defn echecked? [event]
  82. (gobj/getValueByKeys event "target" "checked")))
  83. #?(:cljs
  84. (defn set-change-value
  85. "compatible change event for React"
  86. [node value]
  87. (utils/triggerInputChange node value)))
  88. #?(:cljs
  89. (defn p-handle
  90. ([p ok-handler]
  91. (p-handle p ok-handler (fn [error]
  92. (js/console.error error))))
  93. ([p ok-handler error-handler]
  94. (-> p
  95. (p/then (fn [result]
  96. (ok-handler result)))
  97. (p/catch (fn [error]
  98. (error-handler error)))))))
  99. #?(:cljs
  100. (defn get-width
  101. []
  102. (gobj/get js/window "innerWidth")))
  103. (defn indexed
  104. [coll]
  105. (map-indexed vector coll))
  106. (defn find-first
  107. [pred coll]
  108. (first (filter pred coll)))
  109. (defn dissoc-in
  110. "Dissociates an entry from a nested associative structure returning a new
  111. nested structure. keys is a sequence of keys. Any empty maps that result
  112. will not be present in the new structure."
  113. [m [k & ks :as keys]]
  114. (if ks
  115. (if-let [nextmap (get m k)]
  116. (let [newmap (dissoc-in nextmap ks)]
  117. (if (seq newmap)
  118. (assoc m k newmap)
  119. (dissoc m k)))
  120. m)
  121. (dissoc m k)))
  122. ;; (defn format
  123. ;; [fmt & args]
  124. ;; (apply gstring/format fmt args))
  125. #?(:cljs
  126. (defn json->clj
  127. ([json-string]
  128. (json->clj json-string false))
  129. ([json-string kebab?]
  130. (let [m (-> json-string
  131. (js/JSON.parse)
  132. (js->clj :keywordize-keys true))]
  133. (if kebab?
  134. (cske/transform-keys csk/->kebab-case-keyword m)
  135. m)))))
  136. (defn remove-nils
  137. "remove pairs of key-value that has nil value from a (possibly nested) map."
  138. [nm]
  139. (walk/postwalk
  140. (fn [el]
  141. (if (map? el)
  142. (into {} (remove (comp nil? second)) el)
  143. el))
  144. nm))
  145. (defn remove-nils-non-nested
  146. [nm]
  147. (into {} (remove (comp nil? second)) nm))
  148. (defn remove-nils-or-empty
  149. [nm]
  150. (walk/postwalk
  151. (fn [el]
  152. (if (map? el)
  153. (not-empty (into {} (remove (comp #(or
  154. (nil? %)
  155. (and (coll? %)
  156. (empty? %))) second)) el))
  157. el))
  158. nm))
  159. (defn index-by
  160. [col k]
  161. (->> (map (fn [entry] [(get entry k) entry])
  162. col)
  163. (into {})))
  164. (defn ext-of-image? [s]
  165. (some #(string/ends-with? s %)
  166. [".png" ".jpg" ".jpeg" ".bmp" ".gif" ".webp"]))
  167. ;; ".lg:absolute.lg:inset-y-0.lg:right-0.lg:w-1/2"
  168. (defn hiccup->class
  169. [class]
  170. (some->> (string/split class #"\.")
  171. (string/join " ")
  172. (string/trim)))
  173. #?(:cljs
  174. (defn fetch-raw
  175. ([url on-ok on-failed]
  176. (fetch-raw url {} on-ok on-failed))
  177. ([url opts on-ok on-failed]
  178. (-> (js/fetch url (bean/->js opts))
  179. (.then (fn [resp]
  180. (if (>= (.-status resp) 400)
  181. (on-failed resp)
  182. (if (.-ok resp)
  183. (-> (.text resp)
  184. (.then bean/->clj)
  185. (.then #(on-ok %)))
  186. (on-failed resp)))))))))
  187. #?(:cljs
  188. (defn fetch
  189. ([url on-ok on-failed]
  190. (fetch url {} on-ok on-failed))
  191. ([url opts on-ok on-failed]
  192. (-> (js/fetch url (bean/->js opts))
  193. (.then (fn [resp]
  194. (if (>= (.-status resp) 400)
  195. (on-failed resp)
  196. (if (.-ok resp)
  197. (-> (.json resp)
  198. (.then bean/->clj)
  199. (.then #(on-ok %)))
  200. (on-failed resp)))))))))
  201. #?(:cljs
  202. (defn upload
  203. [url file on-ok on-failed on-progress]
  204. (let [xhr (js/XMLHttpRequest.)]
  205. (.open xhr "put" url)
  206. (gobj/set xhr "onload" on-ok)
  207. (gobj/set xhr "onerror" on-failed)
  208. (when (and (gobj/get xhr "upload")
  209. on-progress)
  210. (gobj/set (gobj/get xhr "upload")
  211. "onprogress"
  212. on-progress))
  213. (.send xhr file))))
  214. (defn post
  215. [url body on-ok on-failed]
  216. #?(:cljs
  217. (fetch url {:method "post"
  218. :headers {:Content-Type "application/json"}
  219. :body (js/JSON.stringify (clj->js body))}
  220. on-ok
  221. on-failed)))
  222. (defn patch
  223. [url body on-ok on-failed]
  224. #?(:cljs
  225. (fetch url {:method "patch"
  226. :headers {:Content-Type "application/json"}
  227. :body (js/JSON.stringify (clj->js body))}
  228. on-ok
  229. on-failed)))
  230. (defn delete
  231. [url on-ok on-failed]
  232. #?(:cljs
  233. (fetch url {:method "delete"
  234. :headers {:Content-Type "application/json"}}
  235. on-ok
  236. on-failed)))
  237. (defn zero-pad
  238. [n]
  239. (if (< n 10)
  240. (str "0" n)
  241. (str n)))
  242. (defn parse-int
  243. [x]
  244. #?(:cljs (if (string? x)
  245. (js/parseInt x)
  246. x)
  247. :clj (if (string? x)
  248. (Integer/parseInt x)
  249. x)))
  250. (defn safe-parse-int
  251. [x]
  252. #?(:cljs (let [result (parse-int x)]
  253. (if (js/isNaN result)
  254. nil
  255. result))
  256. :clj ((try
  257. (parse-int x)
  258. (catch Exception _
  259. nil)))))
  260. #?(:cljs
  261. (defn parse-float
  262. [x]
  263. (if (string? x)
  264. (js/parseFloat x)
  265. x)))
  266. #?(:cljs
  267. (defn safe-parse-float
  268. [x]
  269. (let [result (parse-float x)]
  270. (if (js/isNaN result)
  271. nil
  272. result))))
  273. #?(:cljs
  274. (defn debounce
  275. "Returns a function that will call f only after threshold has passed without new calls
  276. to the function. Calls prep-fn on the args in a sync way, which can be used for things like
  277. calling .persist on the event object to be able to access the event attributes in f"
  278. ([threshold f] (debounce threshold f (constantly nil)))
  279. ([threshold f prep-fn]
  280. (let [t (atom nil)]
  281. (fn [& args]
  282. (when @t (js/clearTimeout @t))
  283. (apply prep-fn args)
  284. (reset! t (js/setTimeout #(do
  285. (reset! t nil)
  286. (apply f args))
  287. threshold)))))))
  288. ;; Caret
  289. #?(:cljs
  290. (defn caret-range [node]
  291. (when-let [doc (or (gobj/get node "ownerDocument")
  292. (gobj/get node "document"))]
  293. (let [win (or (gobj/get doc "defaultView")
  294. (gobj/get doc "parentWindow"))
  295. selection (.getSelection win)]
  296. (if selection
  297. (let [range-count (gobj/get selection "rangeCount")]
  298. (when (> range-count 0)
  299. (let [range (-> (.getSelection win)
  300. (.getRangeAt 0))
  301. pre-caret-range (.cloneRange range)]
  302. (.selectNodeContents pre-caret-range node)
  303. (.setEnd pre-caret-range
  304. (gobj/get range "endContainer")
  305. (gobj/get range "endOffset"))
  306. (.toString pre-caret-range))))
  307. (when-let [selection (gobj/get doc "selection")]
  308. (when (not= "Control" (gobj/get selection "type"))
  309. (let [text-range (.createRange selection)
  310. pre-caret-text-range (.createTextRange (gobj/get doc "body"))]
  311. (.moveToElementText pre-caret-text-range node)
  312. (.setEndPoint pre-caret-text-range "EndToEnd" text-range)
  313. (gobj/get pre-caret-text-range "text")))))))))
  314. (defn get-first-or-last-line-pos
  315. [input]
  316. (let [pos (.-selectionStart input)
  317. value (.-value input)
  318. last-newline-pos (or (string/last-index-of value \newline (dec pos)) -1)]
  319. (- pos last-newline-pos 1)))
  320. (defn minimize-html
  321. [s]
  322. (->> s
  323. (string/split-lines)
  324. (map string/trim)
  325. (string/join "")))
  326. #?(:cljs
  327. (defn stop [e]
  328. (when e (doto e (.preventDefault) (.stopPropagation)))))
  329. (def speed 500)
  330. (def moving-frequency 15)
  331. #?(:cljs
  332. (defn cur-doc-top []
  333. (.. js/document -documentElement -scrollTop)))
  334. #?(:cljs
  335. (defn lock-global-scroll
  336. ([] (lock-global-scroll true))
  337. ([v] (js-invoke (.-classList (app-scroll-container-node))
  338. (if v "add" "remove")
  339. "locked-scroll"))))
  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-element-v2
  362. [elem-id]
  363. (when elem-id
  364. (when-let [elem (gdom/getElement elem-id)]
  365. (.scroll (app-scroll-container-node)
  366. #js {:top (element-top elem 0)
  367. :behavior "auto"})))))
  368. #?(:cljs
  369. (defn scroll-to
  370. ([pos]
  371. (scroll-to (app-scroll-container-node) pos))
  372. ([node pos]
  373. (scroll-to node pos true))
  374. ([node pos animate?]
  375. (when node
  376. (.scroll node
  377. #js {:top pos
  378. :behavior (if animate? "smooth" "auto")})))))
  379. #?(:cljs
  380. (defn scroll-top
  381. "Returns the scroll top position of the `node`. If `node` is not specified,
  382. returns the scroll top position of the `app-scroll-container-node`."
  383. ([]
  384. (scroll-top (app-scroll-container-node)))
  385. ([node]
  386. (when node (.-scrollTop node)))))
  387. #?(:cljs
  388. (defn scroll-to-top
  389. []
  390. (scroll-to (app-scroll-container-node) 0 false)))
  391. #?(:cljs
  392. (defn scroll-to-bottom
  393. [node]
  394. (when-let [node ^js (or node (app-scroll-container-node))]
  395. (let [bottom (.-scrollHeight node)]
  396. (scroll-to node bottom false)))))
  397. (defn url-encode
  398. [string]
  399. #?(:cljs (some-> string str (js/encodeURIComponent) (.replace "+" "%20"))))
  400. (defn url-decode
  401. [string]
  402. #?(:cljs (some-> string str (js/decodeURIComponent))))
  403. #?(:cljs
  404. (defn link?
  405. [node]
  406. (contains?
  407. #{"A" "BUTTON"}
  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. (defn drop-first-line
  439. [s]
  440. (let [lines (string/split-lines s)
  441. others (some->> (next lines)
  442. (string/join "\n"))]
  443. [(first lines)]))
  444. (defn distinct-by
  445. [f col]
  446. (reduce
  447. (fn [acc x]
  448. (if (some #(= (f x) (f %)) acc)
  449. acc
  450. (vec (conj acc x))))
  451. []
  452. col))
  453. (defn distinct-by-last-wins
  454. [f col]
  455. (reduce
  456. (fn [acc x]
  457. (if (some #(= (f x) (f %)) acc)
  458. (mapv
  459. (fn [v]
  460. (if (= (f x) (f v))
  461. x
  462. v))
  463. acc)
  464. (vec (conj acc x))))
  465. []
  466. col))
  467. (defn get-git-owner-and-repo
  468. [repo-url]
  469. (take-last 2 (string/split repo-url #"/")))
  470. (defn safe-split-first [pattern s]
  471. (if-let [first-index (string/index-of s pattern)]
  472. [(subs s 0 first-index)
  473. (subs s (+ first-index (count pattern)) (count s))]
  474. [s ""]))
  475. (defn split-first [pattern s]
  476. (when-let [first-index (string/index-of s pattern)]
  477. [(subs s 0 first-index)
  478. (subs s (+ first-index (count pattern)) (count s))]))
  479. (defn split-last [pattern s]
  480. (when-let [last-index (string/last-index-of s pattern)]
  481. [(subs s 0 last-index)
  482. (subs s (+ last-index (count pattern)) (count s))]))
  483. (defn trim-safe
  484. [s]
  485. (when s
  486. (string/trim s)))
  487. (defn trimr-without-newlines
  488. [s]
  489. (.replace s #"[ \t\r]+$" ""))
  490. (defn trim-only-newlines
  491. [s]
  492. (-> s
  493. (.replace #"[\n]+$" "")
  494. (.replace #"^[\n]+" "")))
  495. (defn triml-without-newlines
  496. [s]
  497. (.replace s #"^[ \t\r]+" ""))
  498. (defn concat-without-spaces
  499. [left right]
  500. (when (and (string? left)
  501. (string? right))
  502. (let [left (trimr-without-newlines left)
  503. not-space? (or
  504. (string/blank? left)
  505. (= "\n" (last left)))]
  506. (str left
  507. (when-not not-space? " ")
  508. (triml-without-newlines right)))))
  509. (defn join-newline
  510. [& col]
  511. #?(:cljs
  512. (let [col (remove nil? col)]
  513. (reduce (fn [acc s]
  514. (if (or (= acc "") (= "\n" (last acc)))
  515. (str acc s)
  516. (str acc "\n"
  517. (.replace s #"^[\n]+" "")))) "" col))))
  518. ;; Add documentation
  519. (defn replace-first [pattern s new-value]
  520. (when-let [first-index (string/index-of s pattern)]
  521. (str new-value (subs s (+ first-index (count pattern))))))
  522. (defn replace-last
  523. ([pattern s new-value]
  524. (replace-last pattern s new-value true))
  525. ([pattern s new-value space?]
  526. (when-let [last-index (string/last-index-of s pattern)]
  527. (let [prefix (subs s 0 last-index)]
  528. (if space?
  529. (concat-without-spaces prefix new-value)
  530. (str prefix new-value))))))
  531. ;; copy from https://stackoverflow.com/questions/18735665/how-can-i-get-the-positions-of-regex-matches-in-clojurescript
  532. #?(:cljs
  533. (defn re-pos [re s]
  534. (let [re (js/RegExp. (.-source re) "g")]
  535. (loop [res []]
  536. (if-let [m (.exec re s)]
  537. (recur (conj res [(.-index m) (first m)]))
  538. res)))))
  539. #?(:cljs
  540. (defn kill-line-before!
  541. [input]
  542. (let [val (.-value input)
  543. end (.-selectionStart input)
  544. n-pos (string/last-index-of val \newline (dec end))
  545. start (if n-pos (inc n-pos) 0)]
  546. (.setRangeText input "" start end))))
  547. #?(:cljs
  548. (defn kill-line-after!
  549. [input]
  550. (let [val (.-value input)
  551. start (.-selectionStart input)
  552. end (or (string/index-of val \newline start)
  553. (count val))]
  554. (.setRangeText input "" start end))))
  555. ;; copied from re_com
  556. #?(:cljs
  557. (defn deref-or-value
  558. "Takes a value or an atom
  559. If it's a value, returns it
  560. If it's a Reagent object that supports IDeref, returns the value inside it by derefing
  561. "
  562. [val-or-atom]
  563. (if (satisfies? IDeref val-or-atom)
  564. @val-or-atom
  565. val-or-atom)))
  566. ;; copied from re_com
  567. #?(:cljs
  568. (defn now->utc
  569. "Return a goog.date.UtcDateTime based on local date/time."
  570. []
  571. (let [local-date-time (js/goog.date.DateTime.)]
  572. (js/goog.date.UtcDateTime.
  573. (.getYear local-date-time)
  574. (.getMonth local-date-time)
  575. (.getDate local-date-time)
  576. 0 0 0 0))))
  577. (defn safe-subvec [xs start end]
  578. (if (or (neg? start)
  579. (> end (count xs)))
  580. []
  581. (subvec xs start end)))
  582. (defn safe-subs
  583. ([s start]
  584. (let [c (count s)]
  585. (safe-subs s start c)))
  586. ([s start end]
  587. (let [c (count s)]
  588. (subs s (min c start) (min c end)))))
  589. #?(:cljs
  590. (defn get-nodes-between-two-nodes
  591. [id1 id2 class]
  592. (when-let [nodes (array-seq (js/document.getElementsByClassName class))]
  593. (let [id #(gobj/get % "id")
  594. node-1 (gdom/getElement id1)
  595. node-2 (gdom/getElement id2)
  596. idx-1 (.indexOf nodes node-1)
  597. idx-2 (.indexOf nodes node-2)
  598. start (min idx-1 idx-2)
  599. end (inc (max idx-1 idx-2))]
  600. (safe-subvec (vec nodes) start end)))))
  601. #?(:cljs
  602. (defn get-direction-between-two-nodes
  603. [id1 id2 class]
  604. (when-let [nodes (array-seq (js/document.getElementsByClassName class))]
  605. (let [node-1 (gdom/getElement id1)
  606. node-2 (gdom/getElement id2)
  607. idx-1 (.indexOf nodes node-1)
  608. idx-2 (.indexOf nodes node-2)]
  609. (if (>= idx-1 idx-2)
  610. :up
  611. :down)))))
  612. #?(:cljs
  613. (defn rec-get-block-node
  614. [node]
  615. (if (and node (d/has-class? node "ls-block"))
  616. node
  617. (and node
  618. (rec-get-block-node (gobj/get node "parentNode"))))))
  619. #?(:cljs
  620. (defn rec-get-blocks-container
  621. [node]
  622. (if (and node (d/has-class? node "blocks-container"))
  623. node
  624. (and node
  625. (rec-get-blocks-container (gobj/get node "parentNode"))))))
  626. #?(:cljs
  627. (defn rec-get-blocks-content-section
  628. [node]
  629. (if (and node (d/has-class? node "content"))
  630. node
  631. (and node
  632. (rec-get-blocks-content-section (gobj/get node "parentNode"))))))
  633. #?(:cljs
  634. (defn node-in-viewpoint?
  635. [node]
  636. (let [rect (.getBoundingClientRect node)
  637. height (or (.-innerHeight js/window)
  638. (.. js/document -documentElement -clientHeight))]
  639. (and
  640. (> (.-top rect) (.-clientHeight (d/by-id "head")))
  641. (<= (.-bottom rect) height)))))
  642. #?(:cljs
  643. (defn get-blocks-noncollapse []
  644. (->> (d/by-class "ls-block")
  645. (filter (fn [b] (some? (gobj/get b "offsetParent")))))))
  646. ;; Take the idea from https://stackoverflow.com/questions/4220478/get-all-dom-block-elements-for-selected-texts.
  647. ;; FIXME: Note that it might not works for IE.
  648. #?(:cljs
  649. (defn get-selected-nodes
  650. [class-name]
  651. (try
  652. (when (gobj/get js/window "getSelection")
  653. (let [selection (js/window.getSelection)
  654. range (.getRangeAt selection 0)
  655. container (-> (gobj/get range "commonAncestorContainer")
  656. (rec-get-blocks-container))
  657. start-node (gobj/get range "startContainer")
  658. container-nodes (array-seq (selection/getSelectedNodes container start-node))]
  659. (map
  660. (fn [node]
  661. (if (or (= 3 (gobj/get node "nodeType"))
  662. (not (d/has-class? node class-name))) ;textnode
  663. (rec-get-block-node node)
  664. node))
  665. container-nodes)))
  666. (catch js/Error _e
  667. nil))))
  668. #?(:cljs
  669. (defn input-selected?
  670. [input]
  671. (not= (.-selectionStart input)
  672. (.-selectionEnd input))))
  673. #?(:cljs
  674. (defn get-selected-text
  675. []
  676. (utils/getSelectionText)))
  677. #?(:cljs (def clear-selection! selection/clearSelection))
  678. #?(:cljs
  679. (defn copy-to-clipboard! [s]
  680. (let [el (js/document.createElement "textarea")]
  681. (set! (.-value el) s)
  682. (.setAttribute el "readonly" "")
  683. (set! (-> el .-style .-position) "absolute")
  684. (set! (-> el .-style .-left) "-9999px")
  685. (js/document.body.appendChild el)
  686. (.select el)
  687. (js/document.execCommand "copy")
  688. (js/document.body.removeChild el))))
  689. (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}")
  690. (defonce exactly-uuid-pattern (re-pattern (str "(?i)^" uuid-pattern "$")))
  691. (defn uuid-string?
  692. [s]
  693. (safe-re-find exactly-uuid-pattern s))
  694. (defn extract-uuid
  695. [s]
  696. (safe-re-find (re-pattern uuid-pattern) s))
  697. (defn drop-nth [n coll]
  698. (keep-indexed #(if (not= %1 n) %2) coll))
  699. (defn capitalize-all [s]
  700. (some->> (string/split s #" ")
  701. (map string/capitalize)
  702. (string/join " ")))
  703. (defn file-page?
  704. [page-name]
  705. (when page-name (safe-re-find #"\." page-name)))
  706. #?(:cljs
  707. (defn react
  708. [ref]
  709. (let [r @react-impls/react]
  710. (r ref))))
  711. (defn time-ms
  712. []
  713. #?(:cljs (tc/to-long (cljs-time.core/now))))
  714. ;; Returns the milliseconds representation of the provided time, in the local timezone.
  715. ;; For example, if you run this function at 10pm EDT in the EDT timezone on May 31st,
  716. ;; it will return 1622433600000, which is equivalent to Mon May 31 2021 00 :00:00.
  717. #?(:cljs
  718. (defn today-at-local-ms [hours mins secs millisecs]
  719. (.setHours (js/Date. (.now js/Date)) hours mins secs millisecs)))
  720. (defn d
  721. [k f]
  722. (let [result (atom nil)]
  723. (println (str "Debug " k))
  724. (time (reset! result (doall (f))))
  725. @result))
  726. (defn concat-without-nil
  727. [& cols]
  728. (->> (apply concat cols)
  729. (remove nil?)))
  730. #?(:cljs
  731. (defn set-title!
  732. [title]
  733. (set! (.-title js/document) title)))
  734. #?(:cljs
  735. (defn get-prev-block-with-same-level
  736. [block]
  737. (let [id (gobj/get block "id")
  738. prefix (safe-re-find #"ls-block-[\d]+" id)]
  739. (when-let [blocks (d/by-class "ls-block")]
  740. (when-let [index (.indexOf blocks block)]
  741. (let [level (d/attr block "level")]
  742. (when (> index 0)
  743. (loop [idx (dec index)]
  744. (if (>= idx 0)
  745. (let [block (nth blocks idx)
  746. prefix-match? (starts-with? (gobj/get block "id") prefix)]
  747. (if (and prefix-match?
  748. (= level (d/attr block "level")))
  749. block
  750. (recur (dec idx))))
  751. nil)))))))))
  752. #?(:cljs
  753. (defn get-next-block-with-same-level
  754. [block]
  755. (when-let [blocks (d/by-class "ls-block")]
  756. (when-let [index (.indexOf blocks block)]
  757. (let [level (d/attr block "level")]
  758. (when (> (count blocks) (inc index))
  759. (loop [idx (inc index)]
  760. (if (< idx (count blocks))
  761. (let [block (nth blocks idx)]
  762. (if (= level (d/attr block "level"))
  763. block
  764. (recur (inc idx))))
  765. nil))))))))
  766. #?(:cljs
  767. (defn get-block-container
  768. [block-element]
  769. (when block-element
  770. (when-let [section (some-> (rec-get-blocks-content-section block-element)
  771. (d/parent))]
  772. (when section
  773. (gdom/getElement section "id"))))))
  774. (defn nth-safe [c i]
  775. (if (or (< i 0) (>= i (count c)))
  776. nil
  777. (nth c i)))
  778. #?(:cljs
  779. (defn get-prev-block-non-collapsed
  780. [block]
  781. (when-let [blocks (get-blocks-noncollapse)]
  782. (when-let [index (.indexOf blocks block)]
  783. (let [idx (dec index)]
  784. (when (>= idx 0)
  785. (nth blocks idx)))))))
  786. #?(:cljs
  787. (defn get-next-block-non-collapsed
  788. [block]
  789. (when-let [blocks (get-blocks-noncollapse)]
  790. (when-let [index (.indexOf blocks block)]
  791. (let [idx (inc index)]
  792. (when (>= (count blocks) idx)
  793. (nth-safe blocks idx)))))))
  794. #?(:cljs
  795. (defn get-next-block-non-collapsed-skip
  796. [block]
  797. (when-let [blocks (get-blocks-noncollapse)]
  798. (when-let [index (.indexOf blocks block)]
  799. (loop [idx (inc index)]
  800. (when (>= (count blocks) idx)
  801. (let [block (nth-safe blocks idx)
  802. nested? (->> (array-seq (gdom/getElementsByClass "selected"))
  803. (some (fn [dom] (.contains dom block))))]
  804. (if nested?
  805. (recur (inc idx))
  806. block))))))))
  807. (defn sort-by-value
  808. [order m]
  809. (into (sorted-map-by
  810. (fn [k1 k2]
  811. (let [v1 (get m k1)
  812. v2 (get m k2)]
  813. (if (= order :desc)
  814. (compare [v2 k2] [v1 k1])
  815. (compare [v1 k1] [v2 k2])))))
  816. m))
  817. (defn rand-str
  818. [n]
  819. #?(:cljs (-> (.toString (js/Math.random) 36)
  820. (.substr 2 n))
  821. :clj (->> (repeatedly #(Integer/toString (rand 36) 36))
  822. (take n)
  823. (apply str))))
  824. (defn unique-id
  825. []
  826. (str (rand-str 6) (rand-str 3)))
  827. (defn tag-valid?
  828. [tag-name]
  829. (when (string? tag-name)
  830. (not (safe-re-find #"[# \t\r\n]+" tag-name))))
  831. (defn encode-str
  832. [s]
  833. (if (tag-valid? s)
  834. s
  835. (url-encode s)))
  836. #?(:cljs
  837. (defn- get-clipboard-as-html
  838. [event]
  839. (if-let [c (gobj/get event "clipboardData")]
  840. [(.getData c "text/html") (.getData c "text")]
  841. (if-let [c (gobj/getValueByKeys event "originalEvent" "clipboardData")]
  842. [(.getData c "text/html") (.getData c "text")]
  843. (if-let [c (gobj/get js/window "clipboardData")]
  844. [(.getData c "Text") (.getData c "Text")])))))
  845. (defn pp-str [x]
  846. (with-out-str (clojure.pprint/pprint x)))
  847. (defn hiccup-keywordize
  848. [hiccup]
  849. (walk/postwalk
  850. (fn [f]
  851. (if (and (vector? f) (string? (first f)))
  852. (update f 0 keyword)
  853. f))
  854. hiccup))
  855. #?(:cljs
  856. (defn chrome?
  857. []
  858. (let [user-agent js/navigator.userAgent
  859. vendor js/navigator.vendor]
  860. (and (safe-re-find #"Chrome" user-agent)
  861. (safe-re-find #"Google Inc" vendor)))))
  862. #?(:cljs
  863. (defn indexeddb-check?
  864. [error-handler]
  865. (let [test-db "logseq-test-db-foo-bar-baz"
  866. db (and js/window.indexedDB
  867. (js/window.indexedDB.open test-db))]
  868. (when (and db (not (chrome?)))
  869. (gobj/set db "onerror" error-handler)
  870. (gobj/set db "onsuccess"
  871. (fn []
  872. (js/window.indexedDB.deleteDatabase test-db)))))))
  873. (defonce mac? #?(:cljs goog.userAgent/MAC
  874. :clj nil))
  875. (defonce win32? #?(:cljs goog.userAgent/WINDOWS
  876. :clj nil))
  877. #?(:cljs
  878. (defn absolute-path?
  879. [path]
  880. (try
  881. (js/window.apis.isAbsolutePath path)
  882. (catch js/Error _
  883. (node-path.isAbsolute path)))))
  884. (defn ->system-modifier
  885. [keyboard-shortcut]
  886. (if mac?
  887. (-> keyboard-shortcut
  888. (string/replace "ctrl" "meta")
  889. (string/replace "alt" "meta"))
  890. keyboard-shortcut))
  891. (defn default-content-with-title
  892. [text-format]
  893. (case (name text-format)
  894. "org"
  895. "* "
  896. "- "))
  897. #?(:cljs
  898. (defn get-first-block-by-id
  899. [block-id]
  900. (when block-id
  901. (let [block-id (str block-id)]
  902. (when (uuid-string? block-id)
  903. (first (array-seq (js/document.getElementsByClassName block-id))))))))
  904. (def windows-reserved-chars #"[\\/:\\*\\?\"<>|]+")
  905. (defn include-windows-reserved-chars?
  906. [s]
  907. (safe-re-find windows-reserved-chars s))
  908. (defn create-title-property?
  909. [s]
  910. (and (string? s)
  911. (or (include-windows-reserved-chars? s)
  912. (string/includes? s "."))))
  913. (defn page-name-sanity
  914. [page-name]
  915. (-> page-name
  916. (string/replace #"/" ".")
  917. ;; Windows reserved path characters
  918. (string/replace windows-reserved-chars "_")))
  919. (defn lowercase-first
  920. [s]
  921. (when s
  922. (str (string/lower-case (.charAt s 0))
  923. (subs s 1))))
  924. #?(:cljs
  925. (defn add-style!
  926. [style]
  927. (when (some? style)
  928. (let [parent-node (d/sel1 :head)
  929. id "logseq-custom-theme-id"
  930. old-link-element (d/sel1 (str "#" id))
  931. style (if (string/starts-with? style "http")
  932. style
  933. (str "data:text/css;charset=utf-8," (js/encodeURIComponent style)))]
  934. (when old-link-element
  935. (d/remove! old-link-element))
  936. (let [link (->
  937. (d/create-element :link)
  938. (d/set-attr! :id id)
  939. (d/set-attr! :rel "stylesheet")
  940. (d/set-attr! :type "text/css")
  941. (d/set-attr! :href style)
  942. (d/set-attr! :media "all"))]
  943. (d/append! parent-node link))))))
  944. (defn ->platform-shortcut
  945. [keyboard-shortcut]
  946. (if mac?
  947. (-> keyboard-shortcut
  948. (string/replace "Ctrl" "Cmd")
  949. (string/replace "Alt" "Opt"))
  950. keyboard-shortcut))
  951. (defn remove-common-preceding
  952. [col1 col2]
  953. (if (and (= (first col1) (first col2))
  954. (seq col1))
  955. (recur (rest col1) (rest col2))
  956. [col1 col2]))
  957. ;; fs
  958. (defn get-file-ext
  959. [file]
  960. (and
  961. (string? file)
  962. (string/includes? file ".")
  963. (last (string/split file #"\."))))
  964. (defn get-dir-and-basename
  965. [path]
  966. (let [parts (string/split path "/")
  967. basename (last parts)
  968. dir (->> (butlast parts)
  969. (string/join "/"))]
  970. [dir basename]))
  971. (defn get-relative-path
  972. [current-file-path another-file-path]
  973. (let [directories-f #(butlast (string/split % "/"))
  974. parts-1 (directories-f current-file-path)
  975. parts-2 (directories-f another-file-path)
  976. [parts-1 parts-2] (remove-common-preceding parts-1 parts-2)
  977. another-file-name (last (string/split another-file-path "/"))]
  978. (->> (concat
  979. (if (seq parts-1)
  980. (repeat (count parts-1) "..")
  981. ["."])
  982. parts-2
  983. [another-file-name])
  984. (string/join "/"))))
  985. ;; Copied from https://github.com/tonsky/datascript-todo
  986. (defmacro profile [k & body]
  987. #?(:clj
  988. `(if goog.DEBUG
  989. (let [k# ~k]
  990. (.time js/console k#)
  991. (let [res# (do ~@body)]
  992. (.timeEnd js/console k#)
  993. res#))
  994. (do ~@body))))
  995. ;; TODO: profile and profileEnd
  996. ;; Copy from hiccup
  997. (defn escape-html
  998. "Change special characters into HTML character entities."
  999. [text]
  1000. (-> text
  1001. (string/replace "&" "&amp;")
  1002. (string/replace "<" "&lt;")
  1003. (string/replace ">" "&gt;")
  1004. (string/replace "\"" "&quot;")
  1005. (string/replace "'" "&apos;")))
  1006. (defn unescape-html
  1007. [text]
  1008. (-> text
  1009. (string/replace "&amp;" "&")
  1010. (string/replace "&lt;" "<")
  1011. (string/replace "&gt;" ">")
  1012. (string/replace "&quot;" "\"")
  1013. (string/replace "&apos;" "'")))
  1014. #?(:cljs
  1015. (defn system-locales
  1016. []
  1017. (when-not node-test?
  1018. (when-let [navigator (and js/window (.-navigator js/window))]
  1019. ;; https://zzz.buzz/2016/01/13/detect-browser-language-in-javascript/
  1020. (when navigator
  1021. (let [v (js->clj
  1022. (or
  1023. (.-languages navigator)
  1024. (.-language navigator)
  1025. (.-userLanguage navigator)
  1026. (.-browserLanguage navigator)
  1027. (.-systemLanguage navigator)))]
  1028. (if (string? v) [v] v)))))))
  1029. #?(:cljs
  1030. (defn zh-CN-supported?
  1031. []
  1032. (contains? (set (system-locales)) "zh-CN")))
  1033. #?(:cljs
  1034. (defn get-element-width
  1035. [id]
  1036. (when-let [element (gdom/getElement id)]
  1037. (gobj/get element "offsetWidth"))))
  1038. (comment
  1039. (= (get-relative-path "journals/2020_11_18.org" "pages/grant_ideas.org")
  1040. "../pages/grant_ideas.org")
  1041. (= (get-relative-path "journals/2020_11_18.org" "journals/2020_11_19.org")
  1042. "./2020_11_19.org")
  1043. (= (get-relative-path "a/b/c/d/g.org" "a/b/c/e/f.org")
  1044. "../e/f.org"))
  1045. #?(:cljs
  1046. (defn select-highlight!
  1047. [blocks]
  1048. (doseq [block blocks]
  1049. (d/add-class! block "selected noselect"))))
  1050. #?(:cljs
  1051. (defn select-unhighlight!
  1052. [blocks]
  1053. (doseq [block blocks]
  1054. (d/remove-class! block "selected" "noselect"))))
  1055. (defn keyname [key] (str (namespace key) "/" (name key)))
  1056. (defn batch [in max-time idle? handler]
  1057. (async/go-loop [buf [] t (async/timeout max-time)]
  1058. (let [[v p] (async/alts! [in t])]
  1059. (cond
  1060. (= p t)
  1061. (let [timeout (async/timeout max-time)]
  1062. (if (idle?)
  1063. (do
  1064. (handler buf)
  1065. (recur [] timeout))
  1066. (recur buf timeout)))
  1067. (nil? v) ; stop
  1068. (when (seq buf)
  1069. (handler buf))
  1070. :else
  1071. (recur (conj buf v) t)))))
  1072. #?(:cljs
  1073. (defn trace!
  1074. []
  1075. (js/console.trace)))
  1076. (defn remove-first [pred coll]
  1077. ((fn inner [coll]
  1078. (lazy-seq
  1079. (when-let [[x & xs] (seq coll)]
  1080. (if (pred x)
  1081. xs
  1082. (cons x (inner xs))))))
  1083. coll))
  1084. (def pprint clojure.pprint/pprint)
  1085. #?(:cljs
  1086. (defn backward-kill-word
  1087. [input]
  1088. (let [val (.-value input)
  1089. current (.-selectionStart input)
  1090. prev (or
  1091. (->> [(string/last-index-of val \space (dec current))
  1092. (string/last-index-of val \newline (dec current))]
  1093. (remove nil?)
  1094. (apply max))
  1095. 0)
  1096. idx (if (zero? prev)
  1097. 0
  1098. (->
  1099. (loop [idx prev]
  1100. (if (#{\space \newline} (nth-safe val idx))
  1101. (recur (dec idx))
  1102. idx))
  1103. inc))]
  1104. (.setRangeText input "" idx current))))
  1105. #?(:cljs
  1106. (defn forward-kill-word
  1107. [input]
  1108. (let [val (.-value input)
  1109. current (.-selectionStart input)
  1110. current (loop [idx current]
  1111. (if (#{\space \newline} (nth-safe val idx))
  1112. (recur (inc idx))
  1113. idx))
  1114. idx (or (->> [(string/index-of val \space current)
  1115. (string/index-of val \newline current)]
  1116. (remove nil?)
  1117. (apply min))
  1118. (count val))]
  1119. (.setRangeText input "" current (inc idx)))))
  1120. #?(:cljs
  1121. (defn fix-open-external-with-shift!
  1122. [^js/MouseEvent e]
  1123. (when (and (.-shiftKey e) win32? (electron?)
  1124. (= (string/lower-case (.. e -target -nodeName)) "a")
  1125. (string/starts-with? (.. e -target -href) "file:"))
  1126. (.preventDefault e))))
  1127. (defn classnames
  1128. "Like react classnames utility:
  1129. ```
  1130. [:div {:class (classnames [:a :b {:c true}])}
  1131. ```
  1132. "
  1133. [args]
  1134. (into #{} (mapcat
  1135. #(if (map? %)
  1136. (for [[k v] %]
  1137. (when v (name k)))
  1138. (name %))
  1139. args)))
  1140. #?(:cljs
  1141. (defn- get-dom-top
  1142. [node]
  1143. (gobj/get (.getBoundingClientRect node) "top")))
  1144. #?(:cljs
  1145. (defn sort-by-height
  1146. [elements]
  1147. (sort (fn [x y]
  1148. (< (get-dom-top x) (get-dom-top y)))
  1149. elements)))
  1150. (def regex-char-esc-smap
  1151. (let [esc-chars "{}[]()&^%$#!?*.+|\\"]
  1152. (zipmap esc-chars
  1153. (map #(str "\\" %) esc-chars))))
  1154. (defn regex-escape
  1155. "Escape all regex meta chars in text."
  1156. [text]
  1157. (string/join (replace regex-char-esc-smap text)))
  1158. (defn split-namespace-pages
  1159. [title]
  1160. (let [parts (string/split title "/")]
  1161. (loop [others (rest parts)
  1162. result [(first parts)]]
  1163. (if (seq others)
  1164. (let [prev (last result)]
  1165. (recur (rest others)
  1166. (conj result (str prev "/" (first others)))))
  1167. result))))
  1168. (comment
  1169. (re-matches (re-pattern (regex-escape "$u^8(d)+w.*[dw]d?")) "$u^8(d)+w.*[dw]d?"))
  1170. #?(:cljs
  1171. (defn meta-key-name []
  1172. (let [user-agent (.. js/navigator -userAgent)]
  1173. (if mac? "Cmd" "Ctrl"))))
  1174. ;; TODO: share with electron
  1175. (defn ignored-path?
  1176. [dir path]
  1177. (when (string? path)
  1178. (or
  1179. (some #(string/starts-with? path (str dir "/" %))
  1180. ["." ".recycle" "assets" "node_modules"])
  1181. (some #(string/includes? path (str "/" % "/"))
  1182. ["." ".recycle" "assets" "node_modules"])
  1183. ;; hidden directory or file
  1184. (re-find #"/\.[^.]+" path)
  1185. (re-find #"^\.[^.]+" path)
  1186. (let [path (string/lower-case path)]
  1187. (not
  1188. (some #(string/ends-with? path %)
  1189. [".md" ".markdown" ".org" ".edn" ".css"]))))))
  1190. (defn wrapped-by-quotes?
  1191. [v]
  1192. (and (string? v) (>= (count v) 2) (= "\"" (first v) (last v))))
  1193. (defn unquote-string
  1194. [v]
  1195. (string/trim (subs v 1 (dec (count v)))))
  1196. (defn unquote-string-if-wrapped
  1197. [v]
  1198. (if (wrapped-by-quotes? v)
  1199. (unquote-string v)
  1200. v))