ui.cljs 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219
  1. (ns frontend.ui
  2. "Main ns for reusable components"
  3. (:require ["@logseq/react-tweet-embed" :as react-tweet-embed]
  4. ["react-intersection-observer" :as react-intersection-observer]
  5. ["react-resize-context" :as Resize]
  6. ["react-textarea-autosize" :as TextareaAutosize]
  7. ["react-tippy" :as react-tippy]
  8. ["react-transition-group" :refer [CSSTransition TransitionGroup]]
  9. ["@emoji-mart/data" :as emoji-data]
  10. ;; ["@emoji-mart/react" :as Picker]
  11. ["emoji-mart" :as emoji-mart]
  12. [cljs-bean.core :as bean]
  13. [clojure.string :as string]
  14. [datascript.core :as d]
  15. [electron.ipc :as ipc]
  16. [frontend.components.svg :as svg]
  17. [frontend.config :as config]
  18. [frontend.context.i18n :refer [t]]
  19. [frontend.db-mixins :as db-mixins]
  20. [frontend.handler.notification :as notification]
  21. [frontend.handler.plugin :as plugin-handler]
  22. [frontend.mixins :as mixins]
  23. [frontend.mobile.util :as mobile-util]
  24. [frontend.modules.shortcut.config :as shortcut-config]
  25. [frontend.modules.shortcut.core :as shortcut]
  26. [frontend.modules.shortcut.utils :as shortcut-utils]
  27. [frontend.rum :as r]
  28. [frontend.state :as state]
  29. [frontend.storage :as storage]
  30. [frontend.ui.date-picker]
  31. [frontend.util :as util]
  32. [frontend.util.cursor :as cursor]
  33. [goog.dom :as gdom]
  34. [goog.functions :refer [debounce]]
  35. [goog.object :as gobj]
  36. [lambdaisland.glogi :as log]
  37. [medley.core :as medley]
  38. [promesa.core :as p]
  39. [rum.core :as rum]
  40. [logseq.shui.core :as shui-core]
  41. [logseq.shui.ui :as shui]))
  42. (declare icon)
  43. (defonce transition-group (r/adapt-class TransitionGroup))
  44. (defonce css-transition (r/adapt-class CSSTransition))
  45. (defonce textarea (r/adapt-class (gobj/get TextareaAutosize "default")))
  46. (def resize-provider (r/adapt-class (gobj/get Resize "ResizeProvider")))
  47. (def resize-consumer (r/adapt-class (gobj/get Resize "ResizeConsumer")))
  48. (def Tippy (r/adapt-class (gobj/get react-tippy "Tooltip")))
  49. (def ReactTweetEmbed (r/adapt-class react-tweet-embed))
  50. (def useInView (gobj/get react-intersection-observer "useInView"))
  51. (defonce _emoji-init-data ((gobj/get emoji-mart "init") #js {:data emoji-data}))
  52. ;; (def EmojiPicker (r/adapt-class (gobj/get Picker "default")))
  53. (defonce icon-size (if (mobile-util/native-platform?) 26 20))
  54. (def built-in-colors
  55. ["yellow"
  56. "red"
  57. "pink"
  58. "green"
  59. "blue"
  60. "purple"
  61. "gray"])
  62. (defn ->block-background-color
  63. [color]
  64. (if (some #{color} built-in-colors)
  65. (str "var(--ls-highlight-color-" color ")")
  66. color))
  67. (defn built-in-color?
  68. [color]
  69. (some #{color} built-in-colors))
  70. (rum/defc menu-background-color
  71. [add-bgcolor-fn rm-bgcolor-fn]
  72. [:div.flex.flex-row.justify-between.py-1.px-2.items-center
  73. [:div.flex.flex-row.justify-between.flex-1.mx-2.mt-2
  74. (for [color built-in-colors]
  75. [:a
  76. {:key (str "key-" color)
  77. :title (t (keyword "color" color))
  78. :on-click #(add-bgcolor-fn color)}
  79. [:div.heading-bg {:style {:background-color (str "var(--color-" color "-500)")}}]])
  80. [:a
  81. {:title (t :remove-background)
  82. :on-click rm-bgcolor-fn}
  83. [:div.heading-bg.remove "-"]]]])
  84. (rum/defc ls-textarea
  85. < rum/reactive
  86. {:did-mount (fn [state]
  87. (let [^js el (rum/dom-node state)
  88. *mouse-point (volatile! nil)]
  89. ;; Passing aria-label as a prop to TextareaAutosize removes the dash
  90. (.setAttribute el "aria-label" "editing block")
  91. (doto el
  92. (.addEventListener "select"
  93. #(let [start (util/get-selection-start el)
  94. end (util/get-selection-end el)]
  95. (when (and start end)
  96. (when-let [e (and (not= start end)
  97. (let [caret-pos (cursor/get-caret-pos el)]
  98. {:caret caret-pos
  99. :start start :end end
  100. :text (. (.-value el) substring start end)
  101. :point (select-keys (or @*mouse-point caret-pos) [:x :y])}))]
  102. (plugin-handler/hook-plugin-editor :input-selection-end (bean/->js e))
  103. (vreset! *mouse-point nil)))))
  104. (.addEventListener "mouseup" #(vreset! *mouse-point {:x (.-x %) :y (.-y %)}))))
  105. state)}
  106. [{:keys [on-change] :as props}]
  107. (let [skip-composition? (state/sub :editor/action)
  108. on-composition (fn [e]
  109. (if skip-composition?
  110. (on-change e)
  111. (case e.type
  112. "compositionend" (do
  113. (state/set-editor-in-composition! false)
  114. (on-change e))
  115. (state/set-editor-in-composition! true))))
  116. props (assoc props
  117. :on-change (fn [e] (when-not (state/editor-in-composition?)
  118. (on-change e)))
  119. :on-composition-start on-composition
  120. :on-composition-update on-composition
  121. :on-composition-end on-composition)]
  122. (textarea props)))
  123. (rum/defc dropdown-content-wrapper
  124. < {:did-mount (fn [state]
  125. (let [k (inc (count (state/sub :modal/dropdowns)))
  126. args (:rum/args state)]
  127. (state/set-state! [:modal/dropdowns k] (second args))
  128. (assoc state ::k k)))
  129. :will-unmount (fn [state]
  130. (state/update-state! :modal/dropdowns #(dissoc % (::k state)))
  131. state)}
  132. [dropdown-state _close-fn content class style-opts]
  133. (let [class (or class
  134. (util/hiccup->class "origin-top-right.absolute.right-0.mt-2"))]
  135. [:div.dropdown-wrapper.max-h-screen.overflow-y-auto
  136. {:style style-opts
  137. :class (str class " "
  138. (case dropdown-state
  139. "entering" "transition ease-out duration-100 transform opacity-0 scale-95"
  140. "entered" "transition ease-out duration-100 transform opacity-100 scale-100"
  141. "exiting" "transition ease-in duration-75 transform opacity-100 scale-100"
  142. "exited" "transition ease-in duration-75 transform opacity-0 scale-95"))}
  143. content]))
  144. ;; public exports
  145. (rum/defcs dropdown < (mixins/modal :open?)
  146. {:init (fn [state]
  147. (let [opts (if (map? (last (:rum/args state)))
  148. (last (:rum/args state))
  149. (->> (drop 2 (:rum/args state))
  150. (partition 2)
  151. (map vec)
  152. (into {})))]
  153. (when (:initial-open? opts)
  154. (reset! (:open? state) true))
  155. (let [on-toggle (:on-toggle opts)]
  156. (when (fn? on-toggle)
  157. (add-watch (:open? state) ::listen-open-value
  158. (fn [_ _ _ _]
  159. (on-toggle @(:open? state)))))))
  160. state)}
  161. [state content-fn modal-content-fn
  162. & [{:keys [modal-class z-index trigger-class _initial-open? *toggle-fn
  163. _on-toggle]
  164. :or {z-index 999}}]]
  165. (let [{:keys [open?]} state
  166. _ (when (and (util/atom? *toggle-fn)
  167. (nil? @*toggle-fn)
  168. (:toggle-fn state))
  169. (reset! *toggle-fn (:toggle-fn state)))
  170. modal-content (modal-content-fn state)
  171. close-fn (:close-fn state)]
  172. [:div.relative.ui__dropdown-trigger {:class trigger-class}
  173. (content-fn state)
  174. (css-transition
  175. {:in @open? :timeout 0}
  176. (fn [dropdown-state]
  177. (when @open?
  178. (dropdown-content-wrapper dropdown-state close-fn modal-content modal-class {:z-index z-index}))))]))
  179. ;; `sequence` can be a list of symbols, a list of strings, or a string
  180. (defn render-keyboard-shortcut [sequence & {:as opts}]
  181. (let [sequence (if (string? sequence)
  182. (-> sequence ;; turn string into sequence
  183. (string/trim)
  184. (string/lower-case)
  185. (string/split #" "))
  186. sequence)]
  187. [:span.keyboard-shortcut
  188. (shui-core/shortcut sequence opts)]))
  189. (rum/defc menu-link
  190. [{:keys [only-child? no-padding? class shortcut] :as options} child]
  191. (if only-child?
  192. [:div.menu-link
  193. (dissoc options :only-child?) child]
  194. [:a.flex.justify-between.menu-link
  195. (cond-> options
  196. (true? no-padding?)
  197. (assoc :class (str class " no-padding"))
  198. true
  199. (dissoc :no-padding?))
  200. [:span.flex-1 child]
  201. (when shortcut
  202. [:span.ml-1 (render-keyboard-shortcut shortcut {:interactive? false})])]))
  203. (rum/defc dropdown-with-links
  204. [content-fn links
  205. {:keys [outer-header outer-footer links-header links-footer] :as opts}]
  206. (dropdown
  207. content-fn
  208. (fn [{:keys [close-fn]}]
  209. (let [links-children
  210. (let [links (if (fn? links) (links) links)
  211. links (remove nil? links)]
  212. (for [{:keys [options title icon key hr hover-detail item _as-link?]} links]
  213. (let [new-options
  214. (merge options
  215. (cond->
  216. {:title hover-detail
  217. :on-click (fn [e]
  218. (when-not (false? (when-let [on-click-fn (:on-click options)]
  219. (on-click-fn e)))
  220. (close-fn)))}
  221. key
  222. (assoc :key key)))
  223. child (if hr
  224. nil
  225. (or item
  226. [:div.flex.items-center
  227. (when icon icon)
  228. [:div.title-wrap {:style {:margin-right "8px"
  229. :margin-left "4px"}} title]]))]
  230. (if hr
  231. [:hr.menu-separator {:key (or key "dropdown-hr")}]
  232. (rum/with-key
  233. (menu-link new-options child)
  234. title)))))
  235. wrapper-children
  236. [:.menu-links-wrapper
  237. (when links-header links-header)
  238. links-children
  239. (when links-footer links-footer)]]
  240. (if (or outer-header outer-footer)
  241. [:.menu-links-outer
  242. outer-header wrapper-children outer-footer]
  243. wrapper-children)))
  244. opts))
  245. (declare button)
  246. (rum/defc notification-content
  247. [state content status uid]
  248. (when (and content status)
  249. (let [svg
  250. (if (keyword? status)
  251. (case status
  252. :success
  253. (icon "circle-check" {:class "text-success" :size "20"})
  254. :warning
  255. (icon "alert-circle" {:class "text-warning" :size "20"})
  256. :error
  257. (icon "circle-x" {:class "text-error" :size "20"})
  258. (icon "info-circle" {:class "text-indigo-500" :size "20"}))
  259. status)]
  260. [:div.ui__notifications-content
  261. {:style
  262. (when (or (= state "exiting")
  263. (= state "exited"))
  264. {:z-index -1})}
  265. [:div.max-w-sm.w-full.shadow-lg.rounded-lg.pointer-events-auto.notification-area
  266. {:class (case state
  267. "entering" "transition ease-out duration-300 transform opacity-0 translate-y-2 sm:translate-x-0"
  268. "entered" "transition ease-out duration-300 transform translate-y-0 opacity-100 sm:translate-x-0"
  269. "exiting" "transition ease-in duration-100 opacity-100"
  270. "exited" "transition ease-in duration-100 opacity-0")}
  271. [:div.rounded-lg.shadow-xs {:style {:max-height "calc(100vh - 200px)"
  272. :overflow-y "auto"
  273. :overflow-x "hidden"}}
  274. [:div.p-4
  275. [:div.flex.items-start
  276. [:div.flex-shrink-0.pt-2
  277. svg]
  278. [:div.ml-3.w-0.flex-1.pt-2
  279. [:div.text-sm.leading-5.font-medium.whitespace-pre-line {:style {:margin 0}}
  280. content]]
  281. [:div.flex-shrink-0.flex {:style {:margin-top -9
  282. :margin-right -18}}
  283. (button
  284. {:button-props {:aria-label "Close"}
  285. :variant :ghost
  286. :class "hover:bg-transparent hover:text-foreground"
  287. :on-click (fn []
  288. (notification/clear! uid))
  289. :icon "x"})]]]]]])))
  290. (declare button)
  291. (rum/defc notification-clear-all
  292. []
  293. [:div.ui__notifications-content
  294. [:div.pointer-events-auto.notification-clear
  295. (button (t :notification/clear-all)
  296. :intent "logseq"
  297. :on-click (fn []
  298. (notification/clear-all!)))]])
  299. (rum/defc notification < rum/reactive
  300. []
  301. (let [contents (state/sub :notification/contents)]
  302. (transition-group
  303. {:class-name "notifications ui__notifications"}
  304. (let [notifications (map (fn [el]
  305. (let [k (first el)
  306. v (second el)]
  307. (css-transition
  308. {:timeout 100
  309. :key (name k)}
  310. (fn [state]
  311. (notification-content state (:content v) (:status v) k)))))
  312. contents)
  313. clear-all (when (> (count contents) 1)
  314. (css-transition
  315. {:timeout 100
  316. :k "clear-all"}
  317. (fn [_state]
  318. (notification-clear-all))))
  319. items (if clear-all (cons clear-all notifications) notifications)]
  320. (doall items)))))
  321. (rum/defc humanity-time-ago
  322. [input opts]
  323. (let [time-fn (fn []
  324. (try
  325. (util/time-ago input)
  326. (catch :default e
  327. (js/console.error e)
  328. input)))
  329. [time set-time] (rum/use-state (time-fn))]
  330. (rum/use-effect!
  331. (fn []
  332. (let [timer (js/setInterval
  333. #(set-time (time-fn)) (* 1000 30))]
  334. #(js/clearInterval timer)))
  335. [])
  336. [:span.ui__humanity-time (merge {} opts) time]))
  337. (defn checkbox
  338. [option]
  339. (let [on-change (:on-change option)
  340. option (cond-> (dissoc option :on-change)
  341. on-change
  342. (assoc :on-checked-change on-change))]
  343. (shui/checkbox
  344. (merge option
  345. {:disabled (or (:disabled option) config/publishing?)}))))
  346. (defn main-node
  347. []
  348. (gdom/getElement "main-content-container"))
  349. (defn focus-element
  350. [element]
  351. (when-let [element ^js (gdom/getElement element)]
  352. (.focus element)))
  353. (defn get-scroll-top []
  354. (.-scrollTop (main-node)))
  355. (defn get-dynamic-style-node
  356. []
  357. (js/document.getElementById "dynamic-style-scope"))
  358. (defn inject-document-devices-envs!
  359. []
  360. (let [^js cl (.-classList js/document.documentElement)]
  361. (when config/publishing? (.add cl "is-publish-mode"))
  362. (when util/mac? (.add cl "is-mac"))
  363. (when util/win32? (.add cl "is-win32"))
  364. (when util/linux? (.add cl "is-linux"))
  365. (when (util/electron?) (.add cl "is-electron"))
  366. (when (util/ios?) (.add cl "is-ios"))
  367. (when (util/mobile?) (.add cl "is-mobile"))
  368. (when (util/safari?) (.add cl "is-safari"))
  369. (when (mobile-util/native-ios?) (.add cl "is-native-ios"))
  370. (when (mobile-util/native-android?) (.add cl "is-native-android"))
  371. (when (mobile-util/native-iphone?) (.add cl "is-native-iphone"))
  372. (when (mobile-util/native-iphone-without-notch?) (.add cl "is-native-iphone-without-notch"))
  373. (when (mobile-util/native-ipad?) (.add cl "is-native-ipad"))
  374. (when (util/electron?)
  375. (doseq [[event function]
  376. [["persist-zoom-level" #(storage/set :zoom-level %)]
  377. ["restore-zoom-level" #(when-let [zoom-level (storage/get :zoom-level)] (js/window.apis.setZoomLevel zoom-level))]
  378. ["full-screen" #(do (js-invoke cl (if (= % "enter") "add" "remove") "is-fullscreen")
  379. (state/set-state! :electron/window-fullscreen? (= % "enter")))]
  380. ["maximize" #(state/set-state! :electron/window-maximized? %)]]]
  381. (.on js/window.apis event function))
  382. (p/then (ipc/ipc :getAppBaseInfo) #(let [{:keys [isFullScreen isMaximized]} (js->clj % :keywordize-keys true)]
  383. (when isFullScreen
  384. (.add cl "is-fullscreen")
  385. (state/set-state! :electron/window-fullscreen? true))
  386. (when isMaximized (state/set-state! :electron/window-maximized? true)))))))
  387. (defn inject-dynamic-style-node!
  388. []
  389. (let [style (get-dynamic-style-node)]
  390. (if (nil? style)
  391. (let [node (js/document.createElement "style")]
  392. (set! (.-id node) "dynamic-style-scope")
  393. (.appendChild js/document.head node))
  394. style)))
  395. (defn apply-custom-theme-effect! [theme]
  396. (when config/lsp-enabled?
  397. (when-let [custom-theme (state/sub [:ui/custom-theme (keyword theme)])]
  398. ;; If the name is nil, the user has not set a custom theme (initially {:mode light/dark}).
  399. ;; The url is not used because the default theme does not have an url.
  400. (if (some? (:name custom-theme))
  401. (js/LSPluginCore.selectTheme (bean/->js custom-theme)
  402. (bean/->js {:emit false}))
  403. (state/set-state! :plugin/selected-theme (:url custom-theme))))))
  404. (defn setup-system-theme-effect!
  405. []
  406. (let [^js schemaMedia (js/window.matchMedia "(prefers-color-scheme: dark)")]
  407. (try (.addEventListener schemaMedia "change" state/sync-system-theme!)
  408. (catch :default _error
  409. (.addListener schemaMedia state/sync-system-theme!)))
  410. (state/sync-system-theme!)
  411. #(try (.removeEventListener schemaMedia "change" state/sync-system-theme!)
  412. (catch :default _error
  413. (.removeListener schemaMedia state/sync-system-theme!)))))
  414. (defn set-global-active-keystroke [val]
  415. (.setAttribute js/document.body "data-active-keystroke" val))
  416. (defn setup-active-keystroke! []
  417. (let [active-keystroke (atom #{})
  418. heads #{:shift :alt :meta :control}
  419. handle-global-keystroke (fn [down? e]
  420. (let [handler (if down? conj disj)
  421. keystroke e.key]
  422. (swap! active-keystroke handler keystroke))
  423. (when (contains? heads (keyword (util/safe-lower-case e.key)))
  424. (set-global-active-keystroke (string/join "+" @active-keystroke))))
  425. keydown-handler (partial handle-global-keystroke true)
  426. keyup-handler (partial handle-global-keystroke false)
  427. clear-all #(do (set-global-active-keystroke "")
  428. (reset! active-keystroke #{}))]
  429. (.addEventListener js/window "keydown" keydown-handler)
  430. (.addEventListener js/window "keyup" keyup-handler)
  431. (.addEventListener js/window "blur" clear-all)
  432. (.addEventListener js/window "visibilitychange" clear-all)
  433. (fn []
  434. (.removeEventListener js/window "keydown" keydown-handler)
  435. (.removeEventListener js/window "keyup" keyup-handler)
  436. (.removeEventListener js/window "blur" clear-all)
  437. (.removeEventListener js/window "visibilitychange" clear-all))))
  438. (defn setup-viewport-listeners! []
  439. (when-let [^js vw (gobj/get js/window "visualViewport")]
  440. (let [handler #(state/set-state! :ui/viewport {:width (.-width vw) :height (.-height vw) :scale (.-scale vw)})]
  441. (.addEventListener js/window.visualViewport "resize" handler)
  442. (handler)
  443. #(.removeEventListener js/window.visualViewport "resize" handler))))
  444. (defonce last-scroll-top (atom 0))
  445. (defn scroll-down?
  446. []
  447. (let [scroll-top (get-scroll-top)
  448. down? (> scroll-top @last-scroll-top)]
  449. (reset! last-scroll-top scroll-top)
  450. down?))
  451. (defn on-scroll
  452. [node {:keys [on-load on-top-reached threshold bottom-reached]
  453. :or {threshold 500}}]
  454. (let [scroll-top (gobj/get node "scrollTop")
  455. bottom-reached? (if (fn? bottom-reached)
  456. (bottom-reached)
  457. (util/bottom-reached? node threshold))
  458. top-reached? (= scroll-top 0)
  459. down? (scroll-down?)]
  460. (when (and bottom-reached? down? on-load)
  461. (on-load))
  462. (when (and (not down?) top-reached? on-top-reached)
  463. (on-top-reached))))
  464. (defn attach-listeners
  465. "Attach scroll and resize listeners."
  466. [state]
  467. (let [list-element-id (first (:rum/args state))
  468. opts (-> state :rum/args (nth 2))
  469. node (js/document.getElementById list-element-id)
  470. debounced-on-scroll (debounce #(on-scroll node opts) 100)]
  471. (mixins/listen state node :scroll debounced-on-scroll)))
  472. (rum/defcs infinite-list <
  473. (mixins/event-mixin attach-listeners)
  474. "Render an infinite list."
  475. [state _list-element-id body {:keys [on-load has-more more more-class]
  476. :or {more-class "text-sm"}}]
  477. [:div
  478. body
  479. (when has-more
  480. [:div.w-full.p-4
  481. [:a.fade-link.text-link.font-bold
  482. {:on-click on-load
  483. :class more-class}
  484. (or more (t :page/earlier))]])])
  485. (rum/defcs auto-complete <
  486. (rum/local 0 ::current-idx)
  487. (shortcut/mixin* :shortcut.handler/auto-complete)
  488. [state
  489. matched
  490. {:keys [on-chosen
  491. on-shift-chosen
  492. get-group-name
  493. empty-placeholder
  494. item-render
  495. class
  496. header]}]
  497. (let [*current-idx (get state ::current-idx)]
  498. [:div#ui__ac {:class class}
  499. (if (seq matched)
  500. [:div#ui__ac-inner.hide-scrollbar
  501. (when header header)
  502. (for [[idx item] (medley/indexed matched)]
  503. [:<>
  504. {:key idx}
  505. (let [item-cp
  506. [:div.menu-link-wrap
  507. {:key idx
  508. ;; mouse-move event to indicate that cursor moved by user
  509. :on-mouse-move #(reset! *current-idx idx)}
  510. (let [chosen? (= @*current-idx idx)]
  511. (menu-link
  512. {:id (str "ac-" idx)
  513. :tab-index "0"
  514. :class (when chosen? "chosen")
  515. :on-pointer-down (fn [e]
  516. (util/stop e)
  517. (if (and (gobj/get e "shiftKey") on-shift-chosen)
  518. (on-shift-chosen item)
  519. (on-chosen item e)))}
  520. (if item-render (item-render item chosen?) item)))]]
  521. (if get-group-name
  522. (if-let [group-name (get-group-name item)]
  523. [:div
  524. [:div.ui__ac-group-name group-name]
  525. item-cp]
  526. item-cp)
  527. item-cp))])]
  528. (when empty-placeholder
  529. empty-placeholder))]))
  530. (def datepicker frontend.ui.date-picker/date-picker)
  531. (defn toggle
  532. ([on? on-click] (toggle on? on-click false))
  533. ([on? on-click small?]
  534. [:a.ui__toggle {:on-click on-click
  535. :class (if small? "is-small" "")
  536. :tab-index "0"
  537. :on-key-down (fn [e] (when (and e (= (.-key e) "Enter"))
  538. (util/stop e)
  539. (on-click e)))}
  540. [:span.wrapper.transition-colors.ease-in-out.duration-200
  541. {:aria-checked (if on? "true" "false"), :tab-index "0", :role "checkbox"
  542. :class (if on? "ui__toggle-background-on" "ui__toggle-background-off")}
  543. [:span.switcher.transform.transition.ease-in-out.duration-200
  544. {:class (if on? (if small? "translate-x-4" "translate-x-5") "translate-x-0")
  545. :aria-hidden "true"}]]]))
  546. (defn keyboard-shortcut-from-config [shortcut-name & {:keys [pick-first?]}]
  547. (let [built-in-binding (:binding (get shortcut-config/all-built-in-keyboard-shortcuts shortcut-name))
  548. custom-binding (when (state/shortcuts) (get (state/shortcuts) shortcut-name))
  549. binding (or custom-binding built-in-binding)]
  550. (if (and pick-first? (coll? binding))
  551. (first binding)
  552. (shortcut-utils/decorate-binding binding))))
  553. (rum/defc modal-overlay
  554. [state close-fn close-backdrop?]
  555. [:div.ui__modal-overlay
  556. {:class (case state
  557. "entering" "ease-out duration-300 opacity-0"
  558. "entered" "ease-out duration-300 opacity-100"
  559. "exiting" "ease-in duration-200 opacity-100"
  560. "exited" "ease-in duration-200 opacity-0")
  561. :on-click #(when close-backdrop? (close-fn))}
  562. [:div.absolute.inset-0.opacity-75]])
  563. (rum/defc modal-panel-content <
  564. mixins/component-editing-mode
  565. [panel-content close-fn]
  566. (panel-content close-fn))
  567. (rum/defc modal-panel
  568. [show? panel-content transition-state close-fn fullscreen? close-btn? style]
  569. [:div.ui__modal-panel.transform.transition-all.sm:min-w-lg.sm
  570. (cond->
  571. {:class (case transition-state
  572. "entering" "ease-out duration-300 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
  573. "entered" "ease-out duration-300 opacity-100 translate-y-0 sm:scale-100"
  574. "exiting" "ease-in duration-200 opacity-100 translate-y-0 sm:scale-100"
  575. "exited" "ease-in duration-200 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95")}
  576. (seq style)
  577. (assoc :style style))
  578. [:div.ui__modal-close-wrap
  579. (when-not (false? close-btn?)
  580. [:a.ui__modal-close
  581. {:aria-label "Close"
  582. :type "button"
  583. :on-click close-fn}
  584. [:svg.h-6.w-6
  585. {:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
  586. [:path
  587. {:d "M6 18L18 6M6 6l12 12"
  588. :stroke-width "2"
  589. :stroke-linejoin "round"
  590. :stroke-linecap "round"}]]])]
  591. (when show?
  592. [:div (cond-> {:class (if fullscreen? "" "panel-content")}
  593. (seq style)
  594. (assoc :style style))
  595. (modal-panel-content panel-content close-fn)])])
  596. (rum/defc modal < rum/reactive
  597. (mixins/event-mixin
  598. (fn [state]
  599. (mixins/hide-when-esc-or-outside
  600. state
  601. :on-hide (fn []
  602. (some->
  603. (.querySelector (rum/dom-node state) "button.ui__modal-close")
  604. (.click)))
  605. :outside? false)
  606. (mixins/on-key-down
  607. state
  608. {;; enter
  609. 13 (fn [state e]
  610. (.preventDefault e)
  611. (some->
  612. (.querySelector (rum/dom-node state) "button.ui__modal-enter")
  613. (.click)))})))
  614. []
  615. (let [modal-panel-content (state/sub :modal/panel-content)
  616. fullscreen? (state/sub :modal/fullscreen?)
  617. close-btn? (state/sub :modal/close-btn?)
  618. close-backdrop? (state/sub :modal/close-backdrop?)
  619. show? (state/sub :modal/show?)
  620. label (state/sub :modal/label)
  621. style (state/sub :modal/style)
  622. class (state/sub :modal/class)
  623. close-fn (fn []
  624. (state/close-modal!)
  625. (state/close-settings!))
  626. modal-panel-content (or modal-panel-content (fn [_close] [:div]))]
  627. [:div.ui__modal
  628. {:style {:z-index (if show? 999 -1)
  629. :display (if show? "flex" "none")}
  630. :label label
  631. :class class}
  632. (css-transition
  633. {:in show? :timeout 0}
  634. (fn [state]
  635. (modal-overlay state close-fn close-backdrop?)))
  636. (css-transition
  637. {:in show? :timeout 0}
  638. (fn [state]
  639. (modal-panel show? modal-panel-content state close-fn fullscreen? close-btn? style)))]))
  640. (defn make-confirm-modal
  641. [{:keys [tag title sub-title sub-checkbox? on-cancel on-confirm]
  642. :or {on-cancel #()}}]
  643. (fn [close-fn]
  644. (let [*sub-checkbox-selected (and sub-checkbox? (atom []))]
  645. [:div.ui__confirm-modal
  646. {:class (str "is-" tag)}
  647. [:div.sm:flex.sm:items-start
  648. [:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-error.sm:mx-0.sm:h-10.sm:w-10
  649. [:svg.h-6.w-6.text-error
  650. {:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
  651. [:path
  652. {:d
  653. "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
  654. :stroke-width "2"
  655. :stroke-linejoin "round"
  656. :stroke-linecap "round"}]]]
  657. [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
  658. [:h2.headline.text-lg.leading-6.font-medium
  659. (if (keyword? title) (t title) title)]
  660. [:label.sublabel
  661. (when sub-checkbox?
  662. (checkbox
  663. {:checked false
  664. :on-change (fn [e]
  665. (let [checked (.. e -target -checked)]
  666. (reset! *sub-checkbox-selected [checked])))}))
  667. [:h3.subline.text-gray-400
  668. (if (keyword? sub-title)
  669. (t sub-title)
  670. sub-title)]]]]
  671. [:div.mt-5.sm:mt-4.flex.gap-4
  672. (button
  673. (t :cancel)
  674. {:theme :gray
  675. :on-click (comp on-cancel close-fn)})
  676. (button
  677. (t :yes)
  678. {:class "ui__modal-enter"
  679. :on-click #(and (fn? on-confirm)
  680. (on-confirm % {:close-fn close-fn
  681. :sub-selected (and *sub-checkbox-selected @*sub-checkbox-selected)}))
  682. :button-props {:autoFocus "on"}})]])))
  683. (rum/defc sub-modal < rum/reactive
  684. []
  685. (when-let [modals (seq (state/sub :modal/subsets))]
  686. (for [[idx modal] (medley/indexed modals)]
  687. (let [id (:modal/id modal)
  688. modal-panel-content (:modal/panel-content modal)
  689. close-btn? (:modal/close-btn? modal)
  690. close-backdrop? (:modal/close-backdrop? modal)
  691. show? (:modal/show? modal)
  692. label (:modal/label modal)
  693. style (:modal/style modal)
  694. class (:modal/class modal)
  695. close-fn (fn []
  696. (state/close-sub-modal! id))
  697. modal-panel-content (or modal-panel-content (fn [_close] [:div]))]
  698. [:div.ui__modal.is-sub-modal
  699. {:style {:z-index (if show? (+ 999 idx) -1)}
  700. :label label
  701. :class class}
  702. (css-transition
  703. {:in show? :timeout 0}
  704. (fn [state]
  705. (modal-overlay state close-fn close-backdrop?)))
  706. (css-transition
  707. {:in show? :timeout 0}
  708. (fn [state]
  709. (modal-panel show? modal-panel-content state close-fn false close-btn? style)))]))))
  710. (defn loading
  711. ([] (loading (t :loading)))
  712. ([content] (loading content nil))
  713. ([content opts]
  714. [:div.flex.flex-row.items-center.inline.icon-loading
  715. [:span.icon.flex.items-center (svg/loader-fn opts)
  716. (when-not (string/blank? content)
  717. [:span.text.pl-2 content])]]))
  718. (rum/defc rotating-arrow
  719. [collapsed?]
  720. [:span
  721. {:class (if collapsed? "rotating-arrow collapsed" "rotating-arrow not-collapsed")}
  722. (svg/caret-right)])
  723. (rum/defcs foldable-title <
  724. (rum/local false ::control?)
  725. [state {:keys [on-pointer-down header title-trigger? collapsed?]}]
  726. (let [control? (get state ::control?)]
  727. [:div.content
  728. [:div.flex-1.flex-row.foldable-title (cond->
  729. {:on-mouse-over #(reset! control? true)
  730. :on-mouse-out #(reset! control? false)}
  731. title-trigger?
  732. (assoc :on-pointer-down on-pointer-down
  733. :class "cursor"))
  734. [:div.flex.flex-row.items-center
  735. (when-not (mobile-util/native-platform?)
  736. [:a.block-control.opacity-50.hover:opacity-100.mr-2
  737. (cond->
  738. {:style {:width 14
  739. :height 16
  740. :margin-left -30}}
  741. (not title-trigger?)
  742. (assoc :on-pointer-down on-pointer-down))
  743. [:span {:class (if (or @control? @collapsed?) "control-show cursor-pointer" "control-hide")}
  744. (rotating-arrow @collapsed?)]])
  745. (if (fn? header)
  746. (header @collapsed?)
  747. header)]]]))
  748. (rum/defcs foldable < db-mixins/query rum/reactive
  749. (rum/local false ::collapsed?)
  750. {:will-mount (fn [state]
  751. (let [args (:rum/args state)]
  752. (when (true? (:default-collapsed? (last args)))
  753. (reset! (get state ::collapsed?) true)))
  754. state)
  755. :did-mount (fn [state]
  756. (when-let [f (:init-collapsed (last (:rum/args state)))]
  757. (f (::collapsed? state)))
  758. state)}
  759. [state header content {:keys [title-trigger? on-pointer-down class
  760. _default-collapsed? _init-collapsed]}]
  761. (let [collapsed? (get state ::collapsed?)
  762. on-pointer-down (fn [e]
  763. (util/stop e)
  764. (swap! collapsed? not)
  765. (when on-pointer-down
  766. (on-pointer-down @collapsed?)))]
  767. [:div.flex.flex-col
  768. {:class class}
  769. (foldable-title {:on-pointer-down on-pointer-down
  770. :header header
  771. :title-trigger? title-trigger?
  772. :collapsed? collapsed?})
  773. [:div {:class (if @collapsed? "hidden" "initial")
  774. :on-pointer-down (fn [e] (.stopPropagation e))}
  775. (if (fn? content)
  776. (if (not @collapsed?) (content) nil)
  777. content)]]))
  778. (rum/defc admonition
  779. [type content]
  780. (let [type (name type)]
  781. (when-let [icon (case (string/lower-case type)
  782. "note" svg/note
  783. "tip" svg/tip
  784. "important" svg/important
  785. "caution" svg/caution
  786. "warning" svg/warning
  787. "pinned" svg/pinned
  788. nil)]
  789. [:div.flex.flex-row.admonitionblock.align-items {:class type}
  790. [:div.pr-4.admonition-icon.flex.flex-col.justify-center
  791. {:title (string/capitalize type)} (icon)]
  792. [:div.ml-4.text-lg
  793. content]])))
  794. (rum/defcs catch-error
  795. < {:did-catch
  796. (fn [state error _info]
  797. (log/error :exception error)
  798. (assoc state ::error error))}
  799. [{error ::error, c :rum/react-component} error-view view]
  800. (if (some? error)
  801. error-view
  802. view))
  803. (rum/defcs catch-error-and-notify
  804. < {:did-catch
  805. (fn [state error _info]
  806. (log/error :exception error)
  807. (notification/show!
  808. (str "Error caught by UI!\n " error)
  809. :error)
  810. (assoc state ::error error))}
  811. [{error ::error, c :rum/react-component} error-view view]
  812. (if (some? error)
  813. error-view
  814. view))
  815. (rum/defc block-error
  816. "Well styled error message for blocks"
  817. [title {:keys [content section-attrs]}]
  818. [:section.border.mt-1.p-1.cursor-pointer.block-content-fallback-ui
  819. section-attrs
  820. [:div.flex.justify-between.items-center.px-1
  821. [:h5.text-error.pb-1 title]
  822. [:a.text-xs.opacity-50.hover:opacity-80
  823. {:href "https://github.com/logseq/logseq/issues/new?labels=from:in-app&template=bug_report.yaml"
  824. :target "_blank"} "report issue"]]
  825. (when content [:pre.m-0.text-sm content])])
  826. (def component-error
  827. "Well styled error message for higher level components. Currently same as
  828. block-error but this could change"
  829. block-error)
  830. (rum/defc select
  831. ([options on-change]
  832. (select options on-change {}))
  833. ([options on-change select-options]
  834. [:select.pl-6.block.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5
  835. (merge
  836. {:class "form-select"
  837. :on-change (fn [e]
  838. (let [value (util/evalue e)]
  839. (on-change e value)))}
  840. select-options)
  841. (for [{:keys [label value selected disabled]
  842. :or {selected false disabled false}} options]
  843. [:option (cond->
  844. {:key label
  845. :value (or value label)} ;; NOTE: value might be an empty string, `or` is safe here
  846. disabled
  847. (assoc :disabled disabled)
  848. selected
  849. (assoc :selected selected))
  850. label])]))
  851. (rum/defc radio-list
  852. [options on-change class]
  853. [:div.ui__radio-list
  854. {:class class}
  855. (for [{:keys [label value selected]} options]
  856. [:label
  857. {:key (str "radio-list-" label)}
  858. [:input.form-radio
  859. {:value value
  860. :type "radio"
  861. :on-change #(on-change (util/evalue %))
  862. :checked selected}]
  863. label])])
  864. (rum/defc checkbox-list
  865. [options on-change class]
  866. (let [checked-vals
  867. (->> options (filter :selected) (map :value) (into #{}))
  868. on-item-change
  869. (fn [^js e]
  870. (let [^js target (.-target e)
  871. checked? (.-checked target)
  872. value (.-value target)]
  873. (on-change
  874. (into []
  875. (if checked?
  876. (conj checked-vals value)
  877. (disj checked-vals value))))))]
  878. [:div.ui__checkbox-list
  879. {:class class}
  880. (for [{:keys [label value selected]} options]
  881. [:label
  882. {:key (str "check-list-" label)}
  883. [:input.form-checkbox
  884. {:value value
  885. :type "checkbox"
  886. :on-change on-item-change
  887. :checked selected}]
  888. label])]))
  889. (rum/defcs tippy < rum/static
  890. (rum/local false ::mounted?)
  891. [state {:keys [fixed-position? open? html] :as opts} child]
  892. (let [*mounted? (::mounted? state)
  893. manual (not= open? nil)
  894. open? (if manual open? @*mounted?)
  895. disabled? (not (state/enable-tooltip?))]
  896. (Tippy (->
  897. (merge {:arrow true
  898. :sticky true
  899. :delay 600
  900. :theme "customized"
  901. :disabled disabled?
  902. :unmountHTMLWhenHide true
  903. :open (if disabled? false open?)
  904. :trigger (if manual "manual" "mouseenter focus")
  905. ;; See https://github.com/tvkhoa/react-tippy/issues/13
  906. :popperOptions {:modifiers {:flip {:enabled (not fixed-position?)}
  907. :hide {:enabled false}
  908. :preventOverflow {:enabled false}}}
  909. :onShow #(when-not (or (some? @state/*editor-editing-ref)
  910. @(:ui/scrolling? @state/state))
  911. (reset! *mounted? true))
  912. :onHide #(reset! *mounted? false)}
  913. opts)
  914. (assoc :html (or
  915. (when open?
  916. (try
  917. (when html
  918. (if (fn? html)
  919. (html)
  920. [:div.px-2.py-1
  921. html]))
  922. (catch :default e
  923. (log/error :exception e)
  924. [:div])))
  925. [:div {:key "tippy"} ""])))
  926. (rum/fragment {:key "tippy-children"} child))))
  927. (rum/defcs slider < rum/reactive
  928. {:init (fn [state]
  929. (assoc state ::value (atom (first (:rum/args state)))))}
  930. [state _default-value {:keys [min max on-change]}]
  931. (let [*value (::value state)
  932. value (rum/react *value)
  933. value' (int value)]
  934. (assert (int? value'))
  935. [:input.cursor-pointer
  936. {:type "range"
  937. :value value'
  938. :min min
  939. :max max
  940. :style {:width "100%"}
  941. :on-change #(let [value (util/evalue %)]
  942. (reset! *value value))
  943. :on-pointer-up #(let [value (util/evalue %)]
  944. (on-change value))}]))
  945. (rum/defcs tweet-embed < (rum/local true :loading?)
  946. [state id]
  947. (let [*loading? (:loading? state)]
  948. [:div [(when @*loading? [:span.flex.items-center [svg/loading " ... loading"]])
  949. (ReactTweetEmbed
  950. {:id id
  951. :class "contents"
  952. :options {:theme (when (= (state/sub :ui/theme) "dark") "dark")}
  953. :on-tweet-load-success #(reset! *loading? false)})]]))
  954. (def icon shui-core/icon)
  955. (rum/defc button-inner
  956. [text & {:keys [theme background variant href size class intent small? icon icon-props disabled? button-props]
  957. :or {small? false}
  958. :as opts}]
  959. (let [button-props (merge
  960. (dissoc opts
  961. :theme :background :href :variant :class :intent :small? :icon :icon-props :disabled? :button-props)
  962. button-props)
  963. props (merge {:variant (cond
  964. (= theme :gray) :ghost
  965. (= background "gray") :secondary
  966. (= background "red") :destructive
  967. (= intent "link") :ghost
  968. :else (or variant :default))
  969. :href href
  970. :size (if small? :xs (or size :sm))
  971. :icon icon
  972. :class (if (and (string? background)
  973. (not (contains? #{"gray" "red"} background)))
  974. (str class " primary-" background) class)
  975. :muted disabled?}
  976. button-props)
  977. icon (when icon (shui/tabler-icon icon icon-props))
  978. href? (not (string/blank? href))
  979. text (cond
  980. href? [:a {:href href :target "_blank"
  981. :style {:color "inherit"}} text]
  982. :else text)
  983. children [icon text]]
  984. (shui/button props children)))
  985. (defn button
  986. [text & {:keys []
  987. :as opts}]
  988. (if (map? text)
  989. (button-inner nil text)
  990. (button-inner text opts)))
  991. (rum/defc point
  992. ([] (point "bg-red-600" 5 nil))
  993. ([klass size {:keys [class style] :as opts}]
  994. [:span.ui__point.overflow-hidden.rounded-full.inline-block
  995. (merge {:class (str (util/hiccup->class klass) " " class)
  996. :style (merge {:width size :height size} style)}
  997. (dissoc opts :style :class))]))
  998. (rum/defc type-icon
  999. [{:keys [name class title extension?]}]
  1000. [:.type-icon {:class class
  1001. :title title}
  1002. (icon name {:extension? extension?})])
  1003. (rum/defc with-shortcut < rum/reactive
  1004. < {:key-fn (fn [key pos] (str "shortcut-" key pos))}
  1005. [shortcut-key position content]
  1006. (let [tooltip? (state/sub :ui/shortcut-tooltip?)]
  1007. (if tooltip?
  1008. (tippy
  1009. {:html [:div.text-sm.font-medium (keyboard-shortcut-from-config shortcut-key)]
  1010. :interactive true
  1011. :position position
  1012. :theme "monospace"
  1013. :delay [1000, 100]
  1014. :arrow true}
  1015. content)
  1016. content)))
  1017. (rum/defc progress-bar
  1018. [width]
  1019. {:pre (integer? width)}
  1020. [:div.w-full.rounded-full.h-2.5.animate-pulse.bg-gray-06-alpha
  1021. [:div.bg-gray-09-alpha.h-2.5.rounded-full {:style {:width (str width "%")}
  1022. :transition "width 1s"}]])
  1023. (rum/defc progress-bar-with-label
  1024. [width label-left label-right]
  1025. {:pre (integer? width)}
  1026. [:div
  1027. [:div.flex.justify-between.mb-1
  1028. [:span.text-base
  1029. label-left]
  1030. [:span.text-sm.font-medium
  1031. label-right]]
  1032. (progress-bar width)])
  1033. (rum/defc lazy-loading-placeholder
  1034. [height]
  1035. [:div {:style {:height height}}])
  1036. (rum/defc lazy-visible-inner
  1037. [visible? content-fn ref fade-in?]
  1038. (let [[set-ref rect] (r/use-bounding-client-rect)
  1039. placeholder-height (or (when rect (.-height rect)) 24)]
  1040. [:div.lazy-visibility {:ref ref}
  1041. [:div {:ref set-ref}
  1042. (if visible?
  1043. (when (fn? content-fn)
  1044. (if fade-in?
  1045. [:div.fade-enter
  1046. {:ref #(when-let [^js cls (and % (.-classList %))]
  1047. (.add cls "fade-enter-active"))}
  1048. (content-fn)]
  1049. (content-fn)))
  1050. (lazy-loading-placeholder placeholder-height))]]))
  1051. (rum/defc lazy-visible
  1052. ([content-fn]
  1053. (lazy-visible content-fn nil))
  1054. ([content-fn {:keys [initial-state trigger-once? fade-in? root-margin _debug-id]
  1055. :or {initial-state false
  1056. trigger-once? true
  1057. fade-in? true
  1058. root-margin 100}}]
  1059. (let [[visible? set-visible!] (rum/use-state initial-state)
  1060. inViewState (useInView #js {:initialInView initial-state
  1061. :rootMargin (str root-margin "px")
  1062. :triggerOnce trigger-once?
  1063. :onChange (fn [in-view? _entry]
  1064. (set-visible! in-view?))})
  1065. ref (.-ref inViewState)]
  1066. (lazy-visible-inner visible? content-fn ref fade-in?))))
  1067. (rum/defc portal
  1068. ([children]
  1069. (portal children {:attach-to (fn [] js/document.body)
  1070. :prepend? false}))
  1071. ([children {:keys [attach-to prepend?]}]
  1072. (let [[portal-anchor set-portal-anchor] (rum/use-state nil)]
  1073. (rum/use-effect!
  1074. (fn []
  1075. (let [div (js/document.createElement "div")
  1076. attached (or (if (fn? attach-to) (attach-to) attach-to) js/document.body)]
  1077. (.setAttribute div "data-logseq-portal" (str (d/squuid)))
  1078. (if prepend? (.prepend attached div) (.append attached div))
  1079. (set-portal-anchor div)
  1080. #(.remove div)))
  1081. [])
  1082. (when portal-anchor
  1083. (rum/portal (rum/fragment children) portal-anchor)))))
  1084. (rum/defc menu-heading
  1085. ([add-heading-fn auto-heading-fn rm-heading-fn]
  1086. (menu-heading nil add-heading-fn auto-heading-fn rm-heading-fn))
  1087. ([heading add-heading-fn auto-heading-fn rm-heading-fn]
  1088. [:div.flex.flex-row.justify-between.pb-2.pt-1.px-2.items-center
  1089. [:div.flex.flex-row.justify-between.flex-1.px-1
  1090. (for [i (range 1 7)]
  1091. (rum/with-key (button
  1092. ""
  1093. :disabled? (and (some? heading) (= heading i))
  1094. :icon (str "h-" i)
  1095. :title (t :heading i)
  1096. :class "to-heading-button"
  1097. :on-click #(add-heading-fn i)
  1098. :intent "link"
  1099. :small? true)
  1100. (str "key-h-" i)))
  1101. (button
  1102. ""
  1103. :icon "h-auto"
  1104. :disabled? (and (some? heading) (true? heading))
  1105. :icon-props {:extension? true}
  1106. :class "to-heading-button"
  1107. :title (t :auto-heading)
  1108. :on-click auto-heading-fn
  1109. :intent "link"
  1110. :small? true)
  1111. (button
  1112. ""
  1113. :icon "heading-off"
  1114. :disabled? (and (some? heading) (not heading))
  1115. :icon-props {:extension? true}
  1116. :class "to-heading-button"
  1117. :title (t :remove-heading)
  1118. :on-click rm-heading-fn
  1119. :intent "link"
  1120. :small? true)]]))
  1121. (comment
  1122. (rum/defc emoji-picker
  1123. [opts]
  1124. (EmojiPicker. (assoc opts :data emoji-data))))