util.cljc 34 KB

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