right_sidebar.cljs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. (ns frontend.components.right-sidebar
  2. (:require [cljs-bean.core :as bean]
  3. [clojure.string :as string]
  4. [frontend.components.block :as block]
  5. [frontend.components.onboarding :as onboarding]
  6. [frontend.components.page :as page]
  7. [frontend.components.shortcut :as shortcut]
  8. [frontend.context.i18n :refer [t]]
  9. [frontend.date :as date]
  10. [frontend.db :as db]
  11. [frontend.db-mixins :as db-mixins]
  12. [frontend.db.model :as db-model]
  13. [frontend.extensions.slide :as slide]
  14. [frontend.handler.editor :as editor-handler]
  15. [frontend.handler.ui :as ui-handler]
  16. [frontend.state :as state]
  17. [frontend.ui :as ui]
  18. [frontend.util :as util]
  19. [frontend.config :as config]
  20. [frontend.modules.editor.undo-redo :as undo-redo]
  21. [medley.core :as medley]
  22. [reitit.frontend.easy :as rfe]
  23. [rum.core :as rum]
  24. [frontend.handler.common :as common-handler]))
  25. (rum/defc toggle
  26. []
  27. (when-not (util/sm-breakpoint?)
  28. (ui/with-shortcut :ui/toggle-right-sidebar "left"
  29. [:button.button.icon.toggle-right-sidebar
  30. {:title (t :right-side-bar/toggle-right-sidebar)
  31. :on-click ui-handler/toggle-right-sidebar!}
  32. (ui/icon "layout-sidebar-right" {:size 20})])))
  33. (rum/defc block-cp < rum/reactive
  34. [repo idx block]
  35. (let [id (:block/uuid block)]
  36. (page/page {:parameters {:path {:name (str id)}}
  37. :sidebar? true
  38. :sidebar/idx idx
  39. :repo repo})))
  40. (rum/defc page-cp < rum/reactive
  41. [repo page-name]
  42. (page/page {:parameters {:path {:name page-name}}
  43. :sidebar? true
  44. :repo repo}))
  45. (rum/defc contents < rum/reactive db-mixins/query
  46. []
  47. [:div.contents.flex-col.flex.ml-3
  48. (when-let [contents (db/entity [:block/name "contents"])]
  49. (page/contents-page contents))])
  50. (rum/defc shortcut-settings
  51. []
  52. [:div.contents.flex-col.flex.ml-3
  53. (shortcut/shortcut-page {:show-title? false})])
  54. (defn- block-with-breadcrumb
  55. [repo block idx sidebar-key ref?]
  56. (when-let [block-id (:block/uuid block)]
  57. [[:.flex.items-center {:class (when ref? "ml-8")}
  58. (ui/icon "block" {:class "text-md mr-2"})
  59. (block/breadcrumb {:id "block-parent"
  60. :block? true
  61. :sidebar-key sidebar-key} repo block-id {:indent? false})]
  62. (block-cp repo idx block)]))
  63. (rum/defc history-action-info
  64. [[k v]]
  65. (when v [:.ml-4 (ui/foldable
  66. [:div (str k)]
  67. [:.ml-4 (case k
  68. :tx-id
  69. [:.my-1 [:pre.code.pre-wrap-white-space.bg-base-4 (str v)]]
  70. :blocks
  71. (map (fn [block]
  72. [:.my-1 [:pre.code.pre-wrap-white-space.bg-base-4 (str block)]]) v)
  73. :txs
  74. (map (fn [[_ key val]]
  75. (when val
  76. [:pre.code.pre-wrap-white-space.bg-base-4
  77. [:span.font-bold (str key) " "] (str val)])) v)
  78. (map (fn [[key val]]
  79. (when val
  80. [:pre.code.pre-wrap-white-space.bg-base-4
  81. [:span.font-bold (str key) " "] (str val)])) v))]
  82. {:default-collapsed? true})]))
  83. (rum/defc history-stack
  84. [label stack]
  85. [:.ml-4 (ui/foldable
  86. [:div label " (" (count stack) ")"]
  87. (map-indexed (fn [index item]
  88. [:.ml-4 (ui/foldable [:div (str index " " (-> item :tx-meta :outliner-op))]
  89. (map history-action-info item)
  90. {:default-collapsed? true})]) stack)
  91. {:default-collapsed? true})])
  92. (rum/defc history < rum/reactive
  93. []
  94. (let [state (undo-redo/get-state)
  95. page-only-mode? (state/sub :history/page-only-mode?)]
  96. [:div.ml-4
  97. [:div.ml-3.font-bold (if page-only-mode? (t :right-side-bar/history-pageonly) (t :right-side-bar/history-global))]
  98. [:div.p-4 [:.ml-4.mb-2
  99. (history-stack (t :right-side-bar/history-undos) (rum/react (:undo-stack state)))
  100. (history-stack (t :right-side-bar/history-redos) (rum/react (:redo-stack state)))]]]))
  101. (defn build-sidebar-item
  102. [repo idx db-id block-type]
  103. (case (keyword block-type)
  104. :contents
  105. [[:.flex.items-center (ui/icon "list-details" {:class "text-md mr-2"}) (t :right-side-bar/contents)]
  106. (contents)]
  107. :help
  108. [[:.flex.items-center (ui/icon "help" {:class "text-md mr-2"}) (t :right-side-bar/help)] (onboarding/help)]
  109. :page-graph
  110. [[:.flex.items-center (ui/icon "hierarchy" {:class "text-md mr-2"}) (t :right-side-bar/page-graph)]
  111. (page/page-graph)]
  112. :history
  113. [[:.flex.items-center (ui/icon "history" {:class "text-md mr-2"}) (t :right-side-bar/history)]
  114. (history)]
  115. :block-ref
  116. #_:clj-kondo/ignore
  117. (let [lookup (if (integer? db-id) db-id [:block/uuid db-id])]
  118. (when-let [block (db/entity repo lookup)]
  119. [(t :right-side-bar/block-ref)
  120. (block-with-breadcrumb repo block idx [repo db-id block-type] true)]))
  121. :block
  122. #_:clj-kondo/ignore
  123. (let [lookup (if (integer? db-id) db-id [:block/uuid db-id])]
  124. (when-let [block (db/entity repo lookup)]
  125. (block-with-breadcrumb repo block idx [repo db-id block-type] false)))
  126. :page
  127. (let [lookup (if (integer? db-id) db-id [:block/uuid db-id])
  128. page (db/entity repo lookup)
  129. page-name (:block/name page)]
  130. [[:.flex.items-center.page-title
  131. (if-let [icon (get-in page [:block/properties :icon])]
  132. [:.text-md.mr-2 icon]
  133. (ui/icon (if (= "whiteboard" (:block/type page)) "whiteboard" "page") {:class "text-md mr-2"}))
  134. [:span.overflow-hidden.text-ellipsis (db-model/get-page-original-name page-name)]]
  135. (page-cp repo page-name)])
  136. :page-slide-view
  137. (let [page-name (:block/name (db/entity db-id))]
  138. [[:a.page-title {:href (rfe/href :page {:name page-name})}
  139. (db-model/get-page-original-name page-name)]
  140. [:div.ml-2.slide.mt-2
  141. (slide/slide page-name)]])
  142. :shortcut-settings
  143. [[:.flex.items-center (ui/icon "command" {:class "text-md mr-2"}) (t :help/shortcuts)]
  144. (shortcut-settings)]
  145. ["" [:span]]))
  146. (defonce *drag-to
  147. (atom nil))
  148. (defonce *drag-from
  149. (atom nil))
  150. (rum/defc context-menu-content
  151. [db-id idx type collapsed? block-count toggle-fn]
  152. [:.menu-links-wrapper.text-left
  153. {:on-click toggle-fn}
  154. (ui/menu-link {:on-click #(state/sidebar-remove-block! idx)} (t :right-side-bar/pane-close))
  155. (when (> block-count 1) (ui/menu-link {:on-click #(state/sidebar-remove-rest! db-id)} (t :right-side-bar/pane-clese-others)))
  156. (when (> block-count 1) (ui/menu-link {:on-click (fn []
  157. (state/clear-sidebar-blocks!)
  158. (state/hide-right-sidebar!))} (t :right-side-bar/pane-clese-all)))
  159. (when (or (not collapsed?) (> block-count 1)) [:hr.menu-separator])
  160. (when-not collapsed? (ui/menu-link {:on-click #(state/sidebar-block-toggle-collapse! db-id)} (t :right-side-bar/pane-collapse)))
  161. (when (> block-count 1) (ui/menu-link {:on-click #(state/sidebar-block-collapse-rest! db-id)} (t :right-side-bar/pane-collapse-others)))
  162. (when (> block-count 1) (ui/menu-link {:on-click #(state/sidebar-block-set-collapsed-all! true)} (t :right-side-bar/pane-collapse-all)))
  163. (when (or collapsed? (> block-count 1)) [:hr.menu-separator])
  164. (when collapsed? (ui/menu-link {:on-click #(state/sidebar-block-toggle-collapse! db-id)} (t :right-side-bar/pane-expand)))
  165. (when (> block-count 1) (ui/menu-link {:on-click #(state/sidebar-block-set-collapsed-all! false)} (t :right-side-bar/pane-expand-all)))
  166. (when (= type :page) [:hr.menu-separator])
  167. (when (= type :page)
  168. (let [name (:block/name (db/entity db-id))]
  169. (ui/menu-link {:href (if (db-model/whiteboard-page? name)
  170. (rfe/href :whiteboard {:name name})
  171. (rfe/href :page {:name name}))} (t :right-side-bar/pane-open-as-page))))])
  172. (rum/defc drop-indicator
  173. [idx drag-to]
  174. [:.sidebar-drop-indicator {:on-drag-enter #(when drag-to (reset! *drag-to idx))
  175. :on-drag-over util/stop
  176. :class (when (= idx drag-to) "drag-over")}])
  177. (rum/defc drop-area
  178. [idx]
  179. [:.sidebar-item-drop-area
  180. {:on-drag-over util/stop}
  181. [:.sidebar-item-drop-area-overlay.top
  182. {:on-drag-enter #(reset! *drag-to (dec idx))}]
  183. [:.sidebar-item-drop-area-overlay.bottom
  184. {:on-drag-enter #(reset! *drag-to idx)}]])
  185. (rum/defc inner-component <
  186. {:should-update (fn [_prev-state state] (last (:rum/args state)))}
  187. [component _should-update?]
  188. component)
  189. (rum/defc sidebar-item < rum/reactive
  190. [repo idx db-id block-type block-count]
  191. (let [drag-from (rum/react *drag-from)
  192. drag-to (rum/react *drag-to)
  193. item (build-sidebar-item repo idx db-id block-type)]
  194. (when item
  195. (let [collapsed? (state/sub [:ui/sidebar-collapsed-blocks db-id])]
  196. [:<>
  197. (when (zero? idx) (drop-indicator (dec idx) drag-to))
  198. [:div.flex.sidebar-item.content.color-level.shadow-md.rounded-md
  199. {:class [(str "item-type-" (name block-type))
  200. (when collapsed? "collapsed")]}
  201. (let [[title component] item]
  202. [:div.flex.flex-col.w-full.relative
  203. [:.flex.flex-row.justify-between.pr-2.sidebar-item-header.color-level.rounded-t-md
  204. {:class (when collapsed? "rounded-b-md")
  205. :draggable true
  206. :on-drag-start (fn [event]
  207. (editor-handler/block->data-transfer! (:block/name (db/entity db-id)) event)
  208. (reset! *drag-from idx))
  209. :on-drag-end (fn [_event]
  210. (when drag-to (state/sidebar-move-block! idx drag-to))
  211. (reset! *drag-to nil)
  212. (reset! *drag-from nil))
  213. :on-mouse-up (fn [event]
  214. (when (= (.-which (.-nativeEvent event)) 2)
  215. (state/sidebar-remove-block! idx)))
  216. :on-context-menu (fn [e]
  217. (util/stop e)
  218. (common-handler/show-custom-context-menu! e (context-menu-content db-id idx block-type collapsed? block-count #())))}
  219. [:button.flex.flex-row.p-2.items-center.w-full.overflow-hidden
  220. {:aria-expanded (str (not collapsed?))
  221. :id (str "sidebar-panel-header-" idx)
  222. :aria-controls (str "sidebar-panel-content-" idx)
  223. :on-click (fn [event]
  224. (util/stop event)
  225. (state/sidebar-block-toggle-collapse! db-id))}
  226. [:span.opacity-50.hover:opacity-100.flex.items-center.pr-1
  227. (ui/rotating-arrow collapsed?)]
  228. [:div.ml-1.font-medium.overflow-hidden
  229. title]]
  230. [:.item-actions.flex.items-center
  231. (ui/dropdown (fn [{:keys [toggle-fn]}]
  232. [:button.button {:title (t :right-side-bar/pane-more)
  233. :on-click (fn [e]
  234. (util/stop e)
  235. (toggle-fn))} (ui/icon "dots")])
  236. (fn [{:keys [close-fn]}]
  237. (context-menu-content db-id idx block-type collapsed? block-count close-fn)))
  238. [:button.button.close {:title (t :right-side-bar/pane-close)
  239. :on-click #(state/sidebar-remove-block! idx)} (ui/icon "x")]]]
  240. [:div.scrollbar-spacing.p-4 {:role "region"
  241. :id (str "sidebar-panel-content-" idx)
  242. :aria-labelledby (str "sidebar-panel-header-" idx)
  243. :class (if collapsed? "hidden" "initial")}
  244. (inner-component component (not drag-from))]
  245. (when drag-from (drop-area idx))])]
  246. (drop-indicator idx drag-to)]))))
  247. (defn- get-page
  248. [match]
  249. (let [route-name (get-in match [:data :name])
  250. page (case route-name
  251. :page
  252. (get-in match [:path-params :name])
  253. :file
  254. (get-in match [:path-params :path])
  255. (date/journal-name))]
  256. (when page
  257. (string/lower-case page))))
  258. (defn get-current-page
  259. []
  260. (let [match (:route-match @state/state)]
  261. (get-page match)))
  262. (rum/defc sidebar-resizer
  263. [sidebar-open? sidebar-id handler-position]
  264. (let [el-ref (rum/use-ref nil)
  265. min-px-width 144 ; Custom window controls width
  266. min-ratio 0.1
  267. max-ratio 0.7
  268. keyboard-step 5
  269. add-resizing-class #(.. js/document.documentElement -classList (add "is-resizing-buf"))
  270. remove-resizing-class (fn []
  271. (.. js/document.documentElement -classList (remove "is-resizing-buf"))
  272. (reset! ui-handler/*right-sidebar-resized-at (js/Date.now)))
  273. set-width! (fn [ratio]
  274. (when el-ref
  275. (let [value (* ratio 100)
  276. width (str value "%")]
  277. (.setAttribute (rum/deref el-ref) "aria-valuenow" value)
  278. (ui-handler/persist-right-sidebar-width! width))))]
  279. (rum/use-effect!
  280. (fn []
  281. (when-let [el (and (fn? js/window.interact) (rum/deref el-ref))]
  282. (-> (js/interact el)
  283. (.draggable
  284. (bean/->js
  285. {:listeners
  286. {:move
  287. (fn [^js/MouseEvent e]
  288. (let [width js/document.documentElement.clientWidth
  289. min-ratio (max min-ratio (/ min-px-width width))
  290. sidebar-el (js/document.getElementById sidebar-id)
  291. offset (.-pageX e)
  292. ratio (.toFixed (/ offset width) 6)
  293. ratio (if (= handler-position :west) (- 1 ratio) ratio)
  294. cursor-class (str "cursor-" (first (name handler-position)) "-resize")]
  295. (if (= (.getAttribute el "data-expanded") "true")
  296. (cond
  297. (< ratio (/ min-ratio 2))
  298. (state/hide-right-sidebar!)
  299. (< ratio min-ratio)
  300. (.. js/document.documentElement -classList (add cursor-class))
  301. (and (< ratio max-ratio) sidebar-el)
  302. (when sidebar-el
  303. (#(.. js/document.documentElement -classList (remove cursor-class))
  304. (set-width! ratio)))
  305. :else
  306. #(.. js/document.documentElement -classList (remove cursor-class)))
  307. (when (> ratio (/ min-ratio 2)) (state/open-right-sidebar!)))))}}))
  308. (.styleCursor false)
  309. (.on "dragstart" add-resizing-class)
  310. (.on "dragend" remove-resizing-class)
  311. (.on "keydown" (fn [e]
  312. (when-let [sidebar-el (js/document.getElementById sidebar-id)]
  313. (let [width js/document.documentElement.clientWidth
  314. min-ratio (max min-ratio (/ min-px-width width))
  315. keyboard-step (case (.-code e)
  316. "ArrowLeft" (- keyboard-step)
  317. "ArrowRight" keyboard-step
  318. 0)
  319. offset (+ (.-x (.getBoundingClientRect sidebar-el)) keyboard-step)
  320. ratio (.toFixed (/ offset width) 6)
  321. ratio (if (= handler-position :west) (- 1 ratio) ratio)]
  322. (when (and (> ratio min-ratio) (< ratio max-ratio) (not (zero? keyboard-step)))
  323. (do (add-resizing-class)
  324. (set-width! ratio)))))))
  325. (.on "keyup" remove-resizing-class)))
  326. #())
  327. [])
  328. (rum/use-effect!
  329. (fn []
  330. ;; sidebar animation duration
  331. (js/setTimeout
  332. #(reset! ui-handler/*right-sidebar-resized-at (js/Date.now)) 300))
  333. [sidebar-open?])
  334. [:.resizer {:ref el-ref
  335. :role "separator"
  336. :aria-orientation "vertical"
  337. :aria-label (t :right-side-bar/separator)
  338. :aria-valuemin (* min-ratio 100)
  339. :aria-valuemax (* max-ratio 100)
  340. :tabIndex "0"
  341. :data-expanded sidebar-open?}]))
  342. (rum/defcs sidebar-inner <
  343. (rum/local false ::anim-finished?)
  344. {:will-mount (fn [state]
  345. (js/setTimeout (fn [] (reset! (get state ::anim-finished?) true)) 300)
  346. state)}
  347. [state repo t blocks]
  348. (let [*anim-finished? (get state ::anim-finished?)
  349. block-count (count blocks)]
  350. [:div.cp__right-sidebar-inner.flex.flex-col.h-full#right-sidebar-container
  351. [:div.cp__right-sidebar-scrollable
  352. {:on-drag-over util/stop}
  353. [:div.cp__right-sidebar-topbar.flex.flex-row.justify-between.items-center.px-2.h-12
  354. [:div.cp__right-sidebar-settings.hide-scrollbar.gap-1 {:key "right-sidebar-settings"}
  355. [:div.text-sm
  356. [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
  357. (state/sidebar-add-block! repo "contents" :contents))}
  358. (t :right-side-bar/contents)]]
  359. [:div.text-sm
  360. [:button.button.cp__right-sidebar-settings-btn {:on-click (fn []
  361. (when-let [page (get-current-page)]
  362. (state/sidebar-add-block!
  363. repo
  364. page
  365. :page-graph)))}
  366. (t :right-side-bar/page-graph)]]
  367. [:div.text-sm
  368. [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
  369. (state/sidebar-add-block! repo "help" :help))}
  370. (t :right-side-bar/help)]]
  371. (when (and config/dev? (state/sub [:ui/developer-mode?]))
  372. [:div.text-sm
  373. [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
  374. (state/sidebar-add-block! repo "history" :history))}
  375. (t :right-side-bar/history)]])]]
  376. [:.sidebar-item-list.flex-1.scrollbar-spacing.ml-2.pr-3
  377. (if @*anim-finished?
  378. (for [[idx [repo db-id block-type]] (medley/indexed blocks)]
  379. (rum/with-key
  380. (sidebar-item repo idx db-id block-type block-count)
  381. (str "sidebar-block-" db-id)))
  382. [:div.p-4
  383. [:span.font-medium.opacity-50 "Loading ..."]])]]]))
  384. (rum/defcs sidebar < rum/reactive
  385. [state]
  386. (let [blocks (state/sub-right-sidebar-blocks)
  387. blocks (if (empty? blocks)
  388. [[(state/get-current-repo) "contents" :contents nil]]
  389. blocks)
  390. sidebar-open? (state/sub :ui/sidebar-open?)
  391. width (state/sub :ui/sidebar-width)
  392. repo (state/sub :git/current-repo)]
  393. [:div#right-sidebar.cp__right-sidebar.h-screen
  394. {:class (if sidebar-open? "open" "closed")
  395. :style {:width width}}
  396. (sidebar-resizer sidebar-open? "right-sidebar" :west)
  397. (when sidebar-open?
  398. (sidebar-inner repo t blocks))]))