ui.cljs 46 KB

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