right_sidebar.cljs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  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.components.svg :as svg]
  9. [frontend.context.i18n :refer [t]]
  10. [frontend.date :as date]
  11. [frontend.db :as db]
  12. [frontend.db-mixins :as db-mixins]
  13. [frontend.db.model :as db-model]
  14. [frontend.extensions.slide :as slide]
  15. [frontend.handler.editor :as editor-handler]
  16. [frontend.handler.ui :as ui-handler]
  17. [frontend.state :as state]
  18. [frontend.ui :as ui]
  19. [frontend.util :as util]
  20. [frontend.config :as config]
  21. [frontend.modules.editor.undo-redo :as undo-redo]
  22. [goog.object :as gobj]
  23. [medley.core :as medley]
  24. [reitit.frontend.easy :as rfe]
  25. [rum.core :as rum]))
  26. (rum/defc toggle
  27. []
  28. (when-not (util/sm-breakpoint?)
  29. (ui/with-shortcut :ui/toggle-right-sidebar "left"
  30. [:button.button.icon.toggle-right-sidebar
  31. {:title (t :right-side-bar/toggle-right-sidebar)
  32. :on-click ui-handler/toggle-right-sidebar!}
  33. (ui/icon "layout-sidebar-right" {:size 20})])))
  34. (rum/defc block-cp < rum/reactive
  35. [repo idx block]
  36. (let [id (:block/uuid block)]
  37. (page/page {:parameters {:path {:name (str id)}}
  38. :sidebar? true
  39. :sidebar/idx idx
  40. :repo repo})))
  41. (rum/defc page-cp < rum/reactive
  42. [repo page-name]
  43. (page/page {:parameters {:path {:name page-name}}
  44. :sidebar? true
  45. :repo repo}))
  46. (rum/defc contents < rum/reactive db-mixins/query
  47. []
  48. [:div.contents.flex-col.flex.ml-3
  49. (when-let [contents (db/entity [:block/name "contents"])]
  50. (page/contents-page contents))])
  51. (rum/defc shortcut-settings
  52. []
  53. [:div.contents.flex-col.flex.ml-3
  54. (shortcut/shortcut-page {:show-title? false})])
  55. (defn- block-with-breadcrumb
  56. [repo block idx sidebar-key ref?]
  57. (when-let [block-id (:block/uuid block)]
  58. [[:div.mt-1 {:class (if ref? "ml-8" "ml-1")}
  59. (block/breadcrumb {:id "block-parent"
  60. :block? true
  61. :sidebar-key sidebar-key} repo block-id {})]
  62. [:div.ml-2
  63. (block-cp repo idx block)]]))
  64. (rum/defc history-action-info
  65. [[k v]]
  66. (when v [:.ml-4 (ui/foldable
  67. [:div (str k)]
  68. [:.ml-4 (case k
  69. :tx-id
  70. [:.my-1 [:pre.code.pre-wrap-white-space.bg-base-4 (str v)]]
  71. :blocks
  72. (map (fn [block]
  73. [:.my-1 [:pre.code.pre-wrap-white-space.bg-base-4 (str block)]]) v)
  74. :txs
  75. (map (fn [[_ key val]]
  76. (when val
  77. [:pre.code.pre-wrap-white-space.bg-base-4
  78. [:span.font-bold (str key) " "] (str val)])) v)
  79. (map (fn [[key val]]
  80. (when val
  81. [:pre.code.pre-wrap-white-space.bg-base-4
  82. [:span.font-bold (str key) " "] (str val)])) v))]
  83. {:default-collapsed? true})]))
  84. (rum/defc history-stack
  85. [label stack]
  86. [:.ml-4 (ui/foldable
  87. [:div label " (" (count stack) ")"]
  88. (map-indexed (fn [index item]
  89. [:.ml-4 (ui/foldable [:div (str index " " (-> item :tx-meta :outliner-op))]
  90. (map history-action-info item)
  91. {:default-collapsed? true})]) stack)
  92. {:default-collapsed? true})])
  93. (rum/defc history < rum/reactive
  94. []
  95. (let [state (undo-redo/get-state)
  96. page-only-mode? (state/sub :history/page-only-mode?)]
  97. [:div.ml-4
  98. [:div.ml-3.font-bold (if page-only-mode? "page only" "global")]
  99. [:div.p-4 [:.ml-4.mb-2
  100. (history-stack "Undos" (rum/react (:undo-stack state)))
  101. (history-stack "Redos" (rum/react (:redo-stack state)))]]]))
  102. (defn build-sidebar-item
  103. [repo idx db-id block-type]
  104. (case block-type
  105. :contents
  106. [(t :right-side-bar/contents)
  107. (contents)]
  108. :help
  109. [(t :right-side-bar/help) (onboarding/help)]
  110. :page-graph
  111. [(t :right-side-bar/page-graph)
  112. (page/page-graph)]
  113. :history
  114. [(t :right-side-bar/history)
  115. (history)]
  116. :block-ref
  117. #_:clj-kondo/ignore
  118. (let [lookup (if (integer? db-id) db-id [:block/uuid db-id])]
  119. (when-let [block (db/entity repo lookup)]
  120. [(t :right-side-bar/block-ref)
  121. (block-with-breadcrumb repo block idx [repo db-id block-type] true)]))
  122. :block
  123. #_:clj-kondo/ignore
  124. (let [lookup (if (integer? db-id) db-id [:block/uuid db-id])]
  125. (when-let [block (db/entity repo lookup)]
  126. (block-with-breadcrumb repo block idx [repo db-id block-type] false)))
  127. :page
  128. (when-let [page-name (if (integer? db-id)
  129. (:block/name (db/entity db-id))
  130. db-id)]
  131. [[:a.page-title {:href (if (db-model/whiteboard-page? page-name)
  132. (rfe/href :whiteboard {:name page-name})
  133. (rfe/href :page {:name page-name}))
  134. :draggable true
  135. :on-drag-start (fn [event] (editor-handler/block->data-transfer! page-name event))
  136. :on-click (fn [e]
  137. (when (gobj/get e "shiftKey")
  138. (.preventDefault e)))}
  139. (db-model/get-page-original-name page-name)]
  140. [:div.ml-2
  141. (page-cp repo page-name)]])
  142. :page-presentation
  143. (let [page-name (:block/name (db/entity db-id))]
  144. [[:a.page-title {:href (rfe/href :page {:name page-name})}
  145. (db-model/get-page-original-name page-name)]
  146. [:div.ml-2.slide.mt-2
  147. (slide/slide page-name)]])
  148. :shortcut-settings
  149. [(t :help/shortcuts) (shortcut-settings)]
  150. ["" [:span]]))
  151. (defn close
  152. ([on-close]
  153. (close nil on-close))
  154. ([class on-close]
  155. [:a.close.flex.items-center
  156. (cond-> {:on-click on-close
  157. :style {:margin-right -4}}
  158. class
  159. (assoc :class class))
  160. svg/close]))
  161. (rum/defc sidebar-item < rum/reactive
  162. [repo idx db-id block-type]
  163. (let [item (build-sidebar-item repo idx db-id block-type)]
  164. (when item
  165. (let [collapse? (state/sub [:ui/sidebar-collapsed-blocks db-id])]
  166. [:div.sidebar-item.content.color-level.px-4.shadow-md
  167. (let [[title component] item]
  168. [:div.flex.flex-col
  169. [:div.flex.flex-row.justify-between
  170. [:div.flex.flex-row.justify-center
  171. [:a.opacity-50.hover:opacity-100.flex.items-center.pr-1
  172. {:on-click #(state/sidebar-block-toggle-collapse! db-id)}
  173. (ui/rotating-arrow collapse?)]
  174. [:div.ml-1.font-medium
  175. title]]
  176. (close #(state/sidebar-remove-block! idx))]
  177. [:div {:class (if collapse? "hidden" "initial")}
  178. component]])]))))
  179. (defn- get-page
  180. [match]
  181. (let [route-name (get-in match [:data :name])
  182. page (case route-name
  183. :page
  184. (get-in match [:path-params :name])
  185. :file
  186. (get-in match [:path-params :path])
  187. (date/journal-name))]
  188. (when page
  189. (string/lower-case page))))
  190. (defn get-current-page
  191. []
  192. (let [match (:route-match @state/state)]
  193. (get-page match)))
  194. (rum/defc sidebar-resizer
  195. [sidebar-open? sidebar-id handler-position]
  196. (let [el-ref (rum/use-ref nil)
  197. min-px-width 144 ; Custom window controls width
  198. min-ratio 0.1
  199. max-ratio 0.7
  200. keyboard-step 5
  201. add-resizing-class #(.. js/document.documentElement -classList (add "is-resizing-buf"))
  202. remove-resizing-class (fn []
  203. (.. js/document.documentElement -classList (remove "is-resizing-buf"))
  204. (reset! ui-handler/*right-sidebar-resized-at (js/Date.now)))
  205. set-width! (fn [ratio]
  206. (when el-ref
  207. (let [value (* ratio 100)
  208. width (str value "%")]
  209. (.setAttribute (rum/deref el-ref) "aria-valuenow" value)
  210. (ui-handler/persist-right-sidebar-width! width))))]
  211. (rum/use-effect!
  212. (fn []
  213. (when-let [el (and (fn? js/window.interact) (rum/deref el-ref))]
  214. (-> (js/interact el)
  215. (.draggable
  216. (bean/->js
  217. {:listeners
  218. {:move
  219. (fn [^js/MouseEvent e]
  220. (let [width js/document.documentElement.clientWidth
  221. min-ratio (max min-ratio (/ min-px-width width))
  222. sidebar-el (js/document.getElementById sidebar-id)
  223. offset (.-pageX e)
  224. ratio (.toFixed (/ offset width) 6)
  225. ratio (if (= handler-position :west) (- 1 ratio) ratio)
  226. cursor-class (str "cursor-" (first (name handler-position)) "-resize")]
  227. (if (= (.getAttribute el "data-expanded") "true")
  228. (cond
  229. (< ratio (/ min-ratio 2))
  230. (state/hide-right-sidebar!)
  231. (< ratio min-ratio)
  232. (.. js/document.documentElement -classList (add cursor-class))
  233. (and (< ratio max-ratio) sidebar-el)
  234. (when sidebar-el
  235. (#(.. js/document.documentElement -classList (remove cursor-class))
  236. (set-width! ratio)))
  237. :else
  238. #(.. js/document.documentElement -classList (remove cursor-class)))
  239. (when (> ratio (/ min-ratio 2)) (state/open-right-sidebar!)))))}}))
  240. (.styleCursor false)
  241. (.on "dragstart" add-resizing-class)
  242. (.on "dragend" remove-resizing-class)
  243. (.on "keydown" (fn [e]
  244. (when-let [sidebar-el (js/document.getElementById sidebar-id)]
  245. (let [width js/document.documentElement.clientWidth
  246. min-ratio (max min-ratio (/ min-px-width width))
  247. keyboard-step (case (.-code e)
  248. "ArrowLeft" (- keyboard-step)
  249. "ArrowRight" keyboard-step
  250. 0)
  251. offset (+ (.-x (.getBoundingClientRect sidebar-el)) keyboard-step)
  252. ratio (.toFixed (/ offset width) 6)
  253. ratio (if (= handler-position :west) (- 1 ratio) ratio)]
  254. (when (and (> ratio min-ratio) (< ratio max-ratio) (not (zero? keyboard-step)))
  255. ((add-resizing-class)
  256. (set-width! ratio)))))))
  257. (.on "keyup" remove-resizing-class)))
  258. #())
  259. [])
  260. (rum/use-effect!
  261. (fn []
  262. ;; sidebar animation duration
  263. (js/setTimeout
  264. #(reset! ui-handler/*right-sidebar-resized-at (js/Date.now)) 300))
  265. [sidebar-open?])
  266. [:.resizer {:ref el-ref
  267. :role "separator"
  268. :aria-orientation "vertical"
  269. :aria-label (t :right-side-bar/separator)
  270. :aria-valuemin (* min-ratio 100)
  271. :aria-valuemax (* max-ratio 100)
  272. :tabIndex "0"
  273. :data-expanded sidebar-open?}]))
  274. (rum/defcs sidebar-inner <
  275. (rum/local false ::anim-finished?)
  276. {:will-mount (fn [state]
  277. (js/setTimeout (fn [] (reset! (get state ::anim-finished?) true)) 300)
  278. state)}
  279. [state repo t blocks]
  280. (let [*anim-finished? (get state ::anim-finished?)]
  281. [:div.cp__right-sidebar-inner.flex.flex-col.h-full#right-sidebar-container
  282. [:div.cp__right-sidebar-scrollable
  283. [:div.cp__right-sidebar-topbar.flex.flex-row.justify-between.items-center.px-2.h-12
  284. [:div.cp__right-sidebar-settings.hide-scrollbar.gap-1 {:key "right-sidebar-settings"}
  285. [:div.text-sm
  286. [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
  287. (state/sidebar-add-block! repo "contents" :contents))}
  288. (t :right-side-bar/contents)]]
  289. [:div.text-sm
  290. [:button.button.cp__right-sidebar-settings-btn {:on-click (fn []
  291. (when-let [page (get-current-page)]
  292. (state/sidebar-add-block!
  293. repo
  294. page
  295. :page-graph)))}
  296. (t :right-side-bar/page-graph)]]
  297. [:div.text-sm
  298. [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
  299. (state/sidebar-add-block! repo "help" :help))}
  300. (t :right-side-bar/help)]]
  301. (when config/dev? [:div.text-sm
  302. [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
  303. (state/sidebar-add-block! repo "history" :history))}
  304. (t :right-side-bar/history)]])]]
  305. [:.sidebar-item-list.flex-1.scrollbar-spacing.flex.flex-col.gap-2
  306. (if @*anim-finished?
  307. (for [[idx [repo db-id block-type]] (medley/indexed blocks)]
  308. (rum/with-key
  309. (sidebar-item repo idx db-id block-type)
  310. (str "sidebar-block-" idx)))
  311. [:div.p-4
  312. [:span.font-medium.opacity-50 "Loading ..."]])]]]))
  313. (rum/defcs sidebar < rum/reactive
  314. [state]
  315. (let [blocks (state/sub-right-sidebar-blocks)
  316. blocks (if (empty? blocks)
  317. [[(state/get-current-repo) "contents" :contents nil]]
  318. blocks)
  319. sidebar-open? (state/sub :ui/sidebar-open?)
  320. width (state/sub :ui/sidebar-width)
  321. repo (state/sub :git/current-repo)]
  322. [:div#right-sidebar.cp__right-sidebar.h-screen
  323. {:class (if sidebar-open? "open" "closed")
  324. :style {:width width}}
  325. (sidebar-resizer sidebar-open? "right-sidebar" :west)
  326. (when sidebar-open?
  327. (sidebar-inner repo t blocks))]))