sidebar.cljs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. (ns frontend.components.sidebar
  2. (:require [cljs-drag-n-drop.core :as dnd]
  3. [clojure.string :as string]
  4. [frontend.components.command-palette :as command-palette]
  5. [frontend.components.header :as header]
  6. [frontend.components.journal :as journal]
  7. [frontend.components.onboarding :as onboarding]
  8. [frontend.components.plugins :as plugins]
  9. [frontend.components.repo :as repo]
  10. [frontend.components.right-sidebar :as right-sidebar]
  11. [frontend.components.select :as select]
  12. [frontend.components.svg :as svg]
  13. [frontend.components.theme :as theme]
  14. [frontend.components.widgets :as widgets]
  15. [frontend.components.find-in-page :as find-in-page]
  16. [frontend.config :as config]
  17. [frontend.context.i18n :refer [t]]
  18. [frontend.db :as db]
  19. [frontend.db-mixins :as db-mixins]
  20. [frontend.db.model :as db-model]
  21. [frontend.extensions.pdf.assets :as pdf-assets]
  22. [frontend.extensions.srs :as srs]
  23. [frontend.handler.common :as common-handler]
  24. [frontend.handler.editor :as editor-handler]
  25. [frontend.handler.mobile.swipe :as swipe]
  26. [frontend.handler.page :as page-handler]
  27. [frontend.handler.route :as route-handler]
  28. [frontend.handler.user :as user-handler]
  29. [frontend.handler.whiteboard :as whiteboard-handler]
  30. [frontend.mixins :as mixins]
  31. [frontend.mobile.action-bar :as action-bar]
  32. [frontend.mobile.footer :as footer]
  33. [frontend.mobile.mobile-bar :refer [mobile-bar]]
  34. [frontend.mobile.util :as mobile-util]
  35. [frontend.modules.shortcut.data-helper :as shortcut-dh]
  36. [frontend.state :as state]
  37. [frontend.ui :as ui]
  38. [frontend.util :as util]
  39. [frontend.util.cursor :as cursor]
  40. [goog.dom :as gdom]
  41. [goog.object :as gobj]
  42. [logseq.graph-parser.util :as gp-util]
  43. [react-draggable]
  44. [reitit.frontend.easy :as rfe]
  45. [rum.core :as rum]))
  46. (rum/defc nav-content-item < rum/reactive
  47. [name {:keys [class]} child]
  48. [:div.nav-content-item
  49. {:class (util/classnames [class {:is-expand (not (state/sub [:ui/navigation-item-collapsed? class]))}])}
  50. [:div.nav-content-item-inner
  51. [:div.header.items-center.mb-1
  52. {:on-click (fn [^js/MouseEvent _e]
  53. (state/toggle-navigation-item-collapsed! class))}
  54. [:div.font-medium name]
  55. [:span
  56. [:a.more svg/arrow-down-v2]]]
  57. [:div.bd child]]])
  58. (defn- delta-y
  59. [e]
  60. (when-let [target (.. e -target)]
  61. (let [rect (.. target getBoundingClientRect)]
  62. (- (.. e -pageY) (.. rect -top)))))
  63. (defn- move-up?
  64. [e]
  65. (let [delta (delta-y e)]
  66. (< delta 14)))
  67. (rum/defc page-name
  68. [name icon recent?]
  69. (let [original-name (db-model/get-page-original-name name)
  70. whiteboard-page? (db-model/whiteboard-page? name)]
  71. [:a.flex.items-center
  72. {:on-click
  73. (fn [e]
  74. (let [name (util/safe-page-name-sanity-lc name)
  75. source-page (db-model/get-alias-source-page (state/get-current-repo) name)
  76. name (if (empty? source-page) name (:block/name source-page))]
  77. (if (and (gobj/get e "shiftKey") (not whiteboard-page?))
  78. (when-let [page-entity (if (empty? source-page) (db/entity [:block/name name]) source-page)]
  79. (state/sidebar-add-block!
  80. (state/get-current-repo)
  81. (:db/id page-entity)
  82. :page))
  83. (if whiteboard-page?
  84. (route-handler/redirect-to-whiteboard! name)
  85. (route-handler/redirect-to-page! name {:click-from-recent? recent?})))))}
  86. [:span.page-icon (if whiteboard-page? (ui/icon "whiteboard" {:extension? true}) icon)]
  87. [:span.page-title (pdf-assets/fix-local-asset-pagename original-name)]]))
  88. (defn get-page-icon [page-entity]
  89. (let [default-icon (ui/icon "page" {:extension? true})
  90. from-properties (get-in (into {} page-entity) [:block/properties :icon])]
  91. (or
  92. (when (not= from-properties "") from-properties)
  93. default-icon))) ;; Fall back to default if icon is undefined or empty
  94. (rum/defcs favorite-item <
  95. (rum/local nil ::up?)
  96. (rum/local nil ::dragging-over)
  97. [state _t name icon]
  98. (let [up? (get state ::up?)
  99. dragging-over (get state ::dragging-over)
  100. target (state/sub :favorites/dragging)]
  101. [:li.favorite-item
  102. {:key name
  103. :title name
  104. :data-ref name
  105. :class (if (and target @dragging-over (not= target @dragging-over))
  106. "dragging-target"
  107. "")
  108. :draggable true
  109. :on-drag-start (fn [_event]
  110. (state/set-state! :favorites/dragging name))
  111. :on-drag-over (fn [e]
  112. (util/stop e)
  113. (reset! dragging-over name)
  114. (when-not (= name (get @state/state :favorites/dragging))
  115. (reset! up? (move-up? e))))
  116. :on-drag-leave (fn [_e]
  117. (reset! dragging-over nil))
  118. :on-drop (fn [e]
  119. (page-handler/reorder-favorites! {:to name
  120. :up? (move-up? e)})
  121. (reset! up? nil)
  122. (reset! dragging-over nil))}
  123. (page-name name icon false)]))
  124. (rum/defc favorites < rum/reactive
  125. [t]
  126. (nav-content-item
  127. [:a.flex.items-center.text-sm.font-medium.rounded-md.wrap-th
  128. (ui/icon "star" {:size 16})
  129. [:span.flex-1.ml-2 (string/upper-case (t :left-side-bar/nav-favorites))]]
  130. {:class "favorites"
  131. :edit-fn
  132. (fn [e]
  133. (rfe/push-state :page {:name "Favorites"})
  134. (util/stop e))}
  135. (let [favorites (->> (:favorites (state/sub-config))
  136. (remove string/blank?)
  137. (filter string?))]
  138. (when (seq favorites)
  139. [:ul.favorites.text-sm
  140. (for [name favorites]
  141. (when-not (string/blank? name)
  142. (when-let [entity (db/entity [:block/name (util/safe-page-name-sanity-lc name)])]
  143. (let [icon (get-page-icon entity)]
  144. (favorite-item t name icon)))))]))))
  145. (rum/defc recent-pages < rum/reactive db-mixins/query
  146. [t]
  147. (nav-content-item
  148. [:a.flex.items-center.text-sm.font-medium.rounded-md.wrap-th
  149. (ui/icon "history" {:size 16})
  150. [:span.flex-1.ml-2
  151. (string/upper-case (t :left-side-bar/nav-recent-pages))]]
  152. {:class "recent"}
  153. (let [pages (->> (db/sub-key-value :recent/pages)
  154. (remove string/blank?)
  155. (filter string?)
  156. (map (fn [page] {:lowercase (util/safe-page-name-sanity-lc page)
  157. :page page}))
  158. (util/distinct-by :lowercase)
  159. (map :page))]
  160. [:ul.text-sm
  161. (for [name pages]
  162. (when-let [entity (db/entity [:block/name (util/safe-page-name-sanity-lc name)])]
  163. [:li.recent-item.select-none
  164. {:key name
  165. :title name
  166. :data-ref name}
  167. (page-name name (get-page-icon entity) true)]))])))
  168. (rum/defcs flashcards < db-mixins/query rum/reactive
  169. {:did-mount (fn [state]
  170. (srs/update-cards-due-count!)
  171. state)}
  172. [_state srs-open?]
  173. (let [num (state/sub :srs/cards-due-count)]
  174. [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
  175. {:class (util/classnames [{:active srs-open?}])
  176. :on-click #(do
  177. (srs/update-cards-due-count!)
  178. (state/pub-event! [:modal/show-cards]))}
  179. (ui/icon "infinity")
  180. [:span.flex-1 (t :right-side-bar/flashcards)]
  181. (when (and num (not (zero? num)))
  182. [:span.ml-3.inline-block.py-0.5.px-3.text-xs.font-medium.rounded-full.fade-in num])]))
  183. (defn get-default-home-if-valid
  184. []
  185. (when-let [default-home (state/get-default-home)]
  186. (let [page (:page default-home)
  187. page (when (and (string? page)
  188. (not (string/blank? page)))
  189. (db/entity [:block/name (util/safe-page-name-sanity-lc page)]))]
  190. (if page
  191. default-home
  192. (dissoc default-home :page)))))
  193. (defn sidebar-item
  194. [{on-click-handler :on-click-handler
  195. class :class
  196. title :title
  197. icon :icon
  198. icon-extension? :icon-extension?
  199. active :active
  200. href :href}]
  201. [:div
  202. {:class class}
  203. [:a.item.group.flex.items-center.text-sm.font-medium.rounded-md
  204. {:on-click on-click-handler
  205. :class (when active "active")
  206. :href href}
  207. (ui/icon (str icon) {:extension? icon-extension?})
  208. [:span.flex-1 title]]])
  209. (defn close-sidebar-on-mobile!
  210. []
  211. (and (util/sm-breakpoint?)
  212. (state/toggle-left-sidebar!)))
  213. (defn create-dropdown
  214. []
  215. (ui/dropdown-with-links
  216. (fn [{:keys [toggle-fn]}]
  217. [:button#create-button
  218. {:on-click toggle-fn}
  219. [:<>
  220. (ui/icon "plus" {:font? "true"})
  221. [:span.mx-1 (t :left-side-bar/create)]]])
  222. (->>
  223. [{:title (t :left-side-bar/new-page)
  224. :class "new-page-link"
  225. :shortcut (ui/keyboard-shortcut-from-config :go/search)
  226. :options {:on-click #(do (close-sidebar-on-mobile!)
  227. (state/pub-event! [:go/search]))}
  228. :icon (ui/type-icon {:name "new-page"
  229. :class "highlight"
  230. :extension? true})}
  231. {:title (t :left-side-bar/new-whiteboard)
  232. :class "new-whiteboard-link"
  233. :shortcut (ui/keyboard-shortcut-from-config :editor/new-whiteboard)
  234. :options {:on-click #(do (close-sidebar-on-mobile!)
  235. (whiteboard-handler/create-new-whiteboard-and-redirect!))}
  236. :icon (ui/type-icon {:name "new-whiteboard"
  237. :class "highlight"
  238. :extension? true})}])
  239. {}))
  240. (rum/defc sidebar-nav < rum/reactive
  241. [route-match close-modal-fn left-sidebar-open? srs-open?]
  242. (let [default-home (get-default-home-if-valid)
  243. route-name (get-in route-match [:data :name])
  244. enable-whiteboards? (state/enable-whiteboards?)
  245. on-contents-scroll #(when-let [^js el (.-target %)]
  246. (let [top (.-scrollTop el)
  247. cls (.-classList el)
  248. cls' "is-scrolled"]
  249. (if (> top 2)
  250. (.add cls cls')
  251. (.remove cls cls'))))]
  252. [:div.left-sidebar-inner.flex-1.flex.flex-col.min-h-0
  253. {:on-click #(when-let [^js target (and (util/sm-breakpoint?) (.-target %))]
  254. (when (some (fn [sel] (boolean (.closest target sel)))
  255. [".favorites .bd" ".recent .bd" ".dropdown-wrapper" ".nav-header"])
  256. (close-modal-fn)))}
  257. [:div.flex.flex-col.wrap.gap-1.relative
  258. (when (mobile-util/native-platform?)
  259. [:div.fake-bar.absolute
  260. [:button
  261. {:on-click state/toggle-left-sidebar!}
  262. (ui/icon "menu-2" {:size ui/icon-size})]])
  263. [:nav.px-4.flex.flex-col.gap-1.cp__menubar-repos
  264. {:aria-label "Navigation menu"}
  265. (repo/repos-dropdown)
  266. [:div.nav-header.flex.gap-1.flex-col
  267. (let [page (:page default-home)]
  268. (if (and page (not (state/enable-journals? (state/get-current-repo))))
  269. (sidebar-item
  270. {:class "home-nav"
  271. :title page
  272. :on-click-handler route-handler/redirect-to-home!
  273. :active (and (not srs-open?)
  274. (= route-name :page)
  275. (= page (get-in route-match [:path-params :name])))
  276. :icon "home"})
  277. (sidebar-item
  278. {:class "journals-nav"
  279. :active (and (not srs-open?)
  280. (or (= route-name :all-journals) (= route-name :home)))
  281. :title (t :left-side-bar/journals)
  282. :on-click-handler (fn [e]
  283. (if (gobj/get e "shiftKey")
  284. (route-handler/sidebar-journals!)
  285. (route-handler/go-to-journals!)))
  286. :icon "calendar"})))
  287. (when (state/enable-flashcards? (state/get-current-repo))
  288. [:div.flashcards-nav
  289. (flashcards srs-open?)])
  290. (sidebar-item
  291. {:class "graph-view-nav"
  292. :title (t :right-side-bar/graph-view)
  293. :href (rfe/href :graph)
  294. :active (and (not srs-open?) (= route-name :graph))
  295. :icon "hierarchy"})
  296. (sidebar-item
  297. {:class "all-pages-nav"
  298. :title (t :right-side-bar/all-pages)
  299. :href (rfe/href :all-pages)
  300. :active (and (not srs-open?) (= route-name :all-pages))
  301. :icon "files"})
  302. (when enable-whiteboards?
  303. (sidebar-item
  304. {:class "whiteboard"
  305. :title (t :right-side-bar/whiteboards)
  306. :href (rfe/href :whiteboards)
  307. :active (and (not srs-open?) (#{:whiteboard :whiteboards} route-name))
  308. :icon "whiteboard"
  309. :icon-extension? true}))]]
  310. [:div.nav-contents-container.flex.flex-col.gap-1.pt-1
  311. {:on-scroll on-contents-scroll}
  312. (when left-sidebar-open?
  313. (favorites t))
  314. (when (and left-sidebar-open? (not config/publishing?))
  315. (recent-pages t))]
  316. [:footer.px-2 {:class "create"}
  317. (when-not config/publishing?
  318. (if enable-whiteboards?
  319. (create-dropdown)
  320. [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md.new-page-link
  321. {:on-click (fn []
  322. (and (util/sm-breakpoint?)
  323. (state/toggle-left-sidebar!))
  324. (state/pub-event! [:go/search]))}
  325. (ui/icon "circle-plus" {:style {:font-size 20}})
  326. [:span.flex-1 (t :right-side-bar/new-page)]]))]]]))
  327. (rum/defc left-sidebar < rum/reactive
  328. [{:keys [left-sidebar-open? route-match]}]
  329. (let [close-fn #(state/set-left-sidebar-open! false)
  330. srs-open? (= :srs (state/sub :modal/id))]
  331. [:div#left-sidebar.cp__sidebar-left-layout
  332. {:class (util/classnames [{:is-open left-sidebar-open?}])}
  333. ;; sidebar contents
  334. (sidebar-nav route-match close-fn left-sidebar-open? srs-open?)
  335. [:span.shade-mask {:on-click close-fn}]]))
  336. (rum/defc recording-bar
  337. []
  338. [:> react-draggable
  339. {:onStart (fn [_event]
  340. (when-let [pos (some-> (state/get-input) cursor/pos)]
  341. (state/set-editor-last-pos! pos)))
  342. :onStop (fn [_event]
  343. (when-let [block (get-in @state/state [:editor/block :block/uuid])]
  344. (editor-handler/edit-block! block :max (:block/uuid block))
  345. (when-let [input (state/get-input)]
  346. (when-let [saved-cursor (state/get-editor-last-pos)]
  347. (cursor/move-cursor-to input saved-cursor)))))}
  348. [:div#audio-record-toolbar
  349. {:style {:bottom (+ @util/keyboard-height 45)}}
  350. (footer/audio-record-cp)]])
  351. (rum/defc main <
  352. {:did-mount (fn [state]
  353. (when-let [element (gdom/getElement "main-content-container")]
  354. (dnd/subscribe!
  355. element
  356. :upload-files
  357. {:drop (fn [_e files]
  358. (when-let [id (state/get-edit-input-id)]
  359. (let [format (:block/format (state/get-edit-block))]
  360. (editor-handler/upload-asset id files format editor-handler/*asset-uploading? true))))})
  361. (common-handler/listen-to-scroll! element))
  362. state)}
  363. [{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content show-action-bar? show-recording-bar?]}]
  364. (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)
  365. onboarding-and-home? (and (or (nil? (state/get-current-repo)) (config/demo-graph?))
  366. (not config/publishing?)
  367. (= :home route-name))
  368. margin-less-pages? (or onboarding-and-home? margin-less-pages?)]
  369. [:div#main-container.cp__sidebar-main-layout.flex-1.flex
  370. {:class (util/classnames [{:is-left-sidebar-open left-sidebar-open?}])}
  371. ;; desktop left sidebar layout
  372. (left-sidebar {:left-sidebar-open? left-sidebar-open?
  373. :route-match route-match})
  374. [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row.outline-none
  375. {:tabIndex "-1"
  376. :data-is-margin-less-pages margin-less-pages?}
  377. (when (util/electron?)
  378. (find-in-page/search))
  379. (when show-action-bar?
  380. (action-bar/action-bar))
  381. [:div.cp__sidebar-main-content
  382. {:data-is-margin-less-pages margin-less-pages?
  383. :data-is-full-width (or margin-less-pages?
  384. (contains? #{:all-files :all-pages :my-publishing} route-name))}
  385. (when show-recording-bar?
  386. (recording-bar))
  387. (mobile-bar)
  388. (footer/footer)
  389. (when (and (not (mobile-util/native-platform?))
  390. (contains? #{:page :home} route-name))
  391. (widgets/demo-graph-alert))
  392. (cond
  393. (not indexeddb-support?)
  394. nil
  395. db-restoring?
  396. [:div.mt-20
  397. [:div.ls-center
  398. (ui/loading (t :loading))]]
  399. :else
  400. [:div
  401. {:class (if (or onboarding-and-home? margin-less-pages?) "" (util/hiccup->class "mx-auto.pb-24"))
  402. :style {:margin-bottom (cond
  403. margin-less-pages? 0
  404. onboarding-and-home? 0
  405. :else 120)}}
  406. main-content])
  407. (when onboarding-and-home?
  408. (onboarding/intro onboarding-and-home?))]]]))
  409. (defonce sidebar-inited? (atom false))
  410. ;; TODO: simplify logic
  411. (rum/defc parsing-progress < rum/static
  412. [state]
  413. (let [finished (or (:finished state) 0)
  414. total (:total state)
  415. width (js/Math.round (* (.toFixed (/ finished total) 2) 100))
  416. file-basename (util/node-path.basename
  417. (:current-parsing-file state))
  418. display-filename (if (mobile-util/native-platform?)
  419. (gp-util/safe-decode-uri-component file-basename)
  420. file-basename)
  421. left-label [:div.flex.flex-row.font-bold
  422. (t :parsing-files)
  423. [:div.hidden.md:flex.flex-row
  424. [:span.mr-1 ": "]
  425. [:div.text-ellipsis-wrapper {:style {:max-width 300}}
  426. display-filename]]]]
  427. (ui/progress-bar-with-label width left-label (str finished "/" total))))
  428. (rum/defc main-content < rum/reactive db-mixins/query
  429. {:init (fn [state]
  430. (when-not @sidebar-inited?
  431. (let [current-repo (state/sub :git/current-repo)
  432. default-home (get-default-home-if-valid)
  433. sidebar (:sidebar default-home)
  434. sidebar (if (string? sidebar) [sidebar] sidebar)]
  435. (when-let [pages (->> (seq sidebar)
  436. (remove string/blank?))]
  437. (doseq [page pages]
  438. (let [page (util/safe-page-name-sanity-lc page)
  439. [db-id block-type] (if (= page "contents")
  440. ["contents" :contents]
  441. [page :page])]
  442. (state/sidebar-add-block! current-repo db-id block-type)))
  443. (reset! sidebar-inited? true))))
  444. (when (state/mobile?)
  445. (state/set-state! :mobile/show-tabbar? true))
  446. state)}
  447. []
  448. (let [default-home (get-default-home-if-valid)
  449. current-repo (state/sub :git/current-repo)
  450. loading-files? (when current-repo (state/sub [:repo/loading-files? current-repo]))
  451. journals-length (state/sub :journals-length)
  452. latest-journals (db/get-latest-journals (state/get-current-repo) journals-length)
  453. graph-parsing-state (state/sub [:graph/parsing-state current-repo])]
  454. (cond
  455. (or
  456. (:graph-loading? graph-parsing-state)
  457. (not= (:total graph-parsing-state) (:finished graph-parsing-state)))
  458. [:div.flex.items-center.justify-center.full-height-without-header
  459. [:div.flex-1
  460. (parsing-progress graph-parsing-state)]]
  461. :else
  462. [:div
  463. (cond
  464. (and default-home
  465. (= :home (state/get-current-route))
  466. (not (state/route-has-p?))
  467. (:page default-home))
  468. (route-handler/redirect-to-page! (:page default-home))
  469. (and config/publishing?
  470. (not default-home)
  471. (empty? latest-journals))
  472. (route-handler/redirect! {:to :all-pages})
  473. loading-files?
  474. (ui/loading (t :loading-files))
  475. (seq latest-journals)
  476. (journal/journals latest-journals)
  477. ;; FIXME: why will this happen?
  478. :else
  479. [:div])])))
  480. (defn- hide-context-menu-and-clear-selection
  481. [e]
  482. (state/hide-custom-context-menu!)
  483. (let [block (.closest (.-target e) ".ls-block")]
  484. (when-not (or (gobj/get e "shiftKey")
  485. (util/meta-key? e)
  486. (state/get-edit-input-id)
  487. (and block
  488. (or (= block (.-target e))
  489. (.contains block (.-target e)))))
  490. (editor-handler/clear-selection!))))
  491. (rum/defc render-custom-context-menu
  492. [links position]
  493. (let [ref (rum/use-ref nil)]
  494. (rum/use-effect!
  495. #(let [el (rum/deref ref)
  496. {:keys [x y]} (util/calc-delta-rect-offset el js/document.documentElement)]
  497. (set! (.. el -style -transform)
  498. (str "translate3d(" (if (neg? x) x 0) "px," (if (neg? y) (- y 10) 0) "px" ",0)"))))
  499. [:<>
  500. [:div.menu-backdrop {:on-mouse-down (fn [e] (hide-context-menu-and-clear-selection e))}]
  501. [:div#custom-context-menu
  502. {:ref ref
  503. :style {:left (str (first position) "px")
  504. :top (str (second position) "px")}} links]]))
  505. (rum/defc custom-context-menu < rum/reactive
  506. []
  507. (let [show? (state/sub :custom-context-menu/show?)
  508. links (state/sub :custom-context-menu/links)
  509. position (state/sub :custom-context-menu/position)]
  510. (when (and show? links position)
  511. (ui/css-transition
  512. {:class-names "fade"
  513. :timeout {:enter 500
  514. :exit 300}}
  515. (render-custom-context-menu links position)))))
  516. (rum/defc new-block-mode < rum/reactive
  517. []
  518. (when (state/sub [:document/mode?])
  519. (ui/tippy {:html [:div.p-2
  520. [:p.mb-2 [:b "Document mode"]]
  521. [:ul
  522. [:li
  523. [:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :editor/new-line))]
  524. [:p.inline-block "to create new block"]]
  525. [:li
  526. [:p.inline-block.mr-1 "Click `D` or type"]
  527. [:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :ui/toggle-document-mode))]
  528. [:p.inline-block "to toggle document mode"]]]]}
  529. [:a.block.px-1.text-sm.font-medium.bg-base-2.rounded-md.mx-2
  530. {:on-click state/toggle-document-mode!}
  531. "D"])))
  532. (rum/defc help-button < rum/reactive
  533. []
  534. (when-not (state/sub :ui/sidebar-open?)
  535. [:div.cp__sidebar-help-btn
  536. [:div.inner
  537. {:title (t :help-shortcut-title)
  538. :on-click (fn []
  539. (state/sidebar-add-block! (state/get-current-repo) "help" :help))}
  540. "?"]]))
  541. (rum/defcs ^:large-vars/cleanup-todo sidebar <
  542. (mixins/modal :modal/show?)
  543. rum/reactive
  544. (mixins/event-mixin
  545. (fn [state]
  546. (mixins/listen state js/window "click" hide-context-menu-and-clear-selection)
  547. (mixins/listen state js/window "keydown"
  548. (fn [e]
  549. (when (= 27 (.-keyCode e))
  550. (if (and (state/modal-opened?)
  551. (not
  552. (and
  553. ;; FIXME: this does not work on CI tests
  554. util/node-test?
  555. (:editor/editing? @state/state))))
  556. (state/close-modal!)
  557. (hide-context-menu-and-clear-selection e)))))))
  558. {:did-mount (fn [state]
  559. (swipe/setup-listeners!)
  560. state)}
  561. [state route-match main-content]
  562. (let [{:keys [open-fn]} state
  563. current-repo (state/sub :git/current-repo)
  564. granted? (state/sub [:nfs/user-granted? (state/get-current-repo)])
  565. theme (state/sub :ui/theme)
  566. system-theme? (state/sub :ui/system-theme?)
  567. light? (= "light" (state/sub :ui/theme))
  568. sidebar-open? (state/sub :ui/sidebar-open?)
  569. settings-open? (state/sub :ui/settings-open?)
  570. left-sidebar-open? (state/sub :ui/left-sidebar-open?)
  571. wide-mode? (state/sub :ui/wide-mode?)
  572. ls-block-hl-colored? (state/sub :pdf/block-highlight-colored?)
  573. onboarding-state (state/sub :file-sync/onboarding-state)
  574. right-sidebar-blocks (state/sub-right-sidebar-blocks)
  575. route-name (get-in route-match [:data :name])
  576. margin-less-pages? (boolean (#{:graph :whiteboard} route-name))
  577. db-restoring? (state/sub :db/restoring?)
  578. indexeddb-support? (state/sub :indexeddb/support?)
  579. page? (= :page route-name)
  580. home? (= :home route-name)
  581. edit? (:editor/editing? @state/state)
  582. default-home (get-default-home-if-valid)
  583. logged? (user-handler/logged-in?)
  584. show-action-bar? (state/sub :mobile/show-action-bar?)
  585. show-recording-bar? (state/sub :mobile/show-recording-bar?)
  586. preferred-language (state/sub [:preferred-language])]
  587. (theme/container
  588. {:t t
  589. :theme theme
  590. :route route-match
  591. :current-repo current-repo
  592. :edit? edit?
  593. :nfs-granted? granted?
  594. :db-restoring? db-restoring?
  595. :sidebar-open? sidebar-open?
  596. :settings-open? settings-open?
  597. :sidebar-blocks-len (count right-sidebar-blocks)
  598. :system-theme? system-theme?
  599. :onboarding-state onboarding-state
  600. :preferred-language preferred-language
  601. :on-click (fn [e]
  602. (editor-handler/unhighlight-blocks!)
  603. (util/fix-open-external-with-shift! e))}
  604. [:main.theme-inner
  605. {:class (util/classnames [{:ls-left-sidebar-open left-sidebar-open?
  606. :ls-right-sidebar-open sidebar-open?
  607. :ls-wide-mode wide-mode?
  608. :ls-hl-colored ls-block-hl-colored?}])}
  609. [:button#skip-to-main
  610. {:on-click #(ui/focus-element (ui/main-node))
  611. :on-key-up (fn [e]
  612. (when (= (.-key e) "Enter")
  613. (ui/focus-element (ui/main-node))))}
  614. (t :accessibility/skip-to-main-content)]
  615. [:div.#app-container
  616. [:div#left-container
  617. {:class (if (state/sub :ui/sidebar-open?) "overflow-hidden" "w-full")}
  618. (header/header {:open-fn open-fn
  619. :light? light?
  620. :current-repo current-repo
  621. :logged? logged?
  622. :page? page?
  623. :route-match route-match
  624. :default-home default-home
  625. :new-block-mode new-block-mode})
  626. (main {:route-match route-match
  627. :margin-less-pages? margin-less-pages?
  628. :logged? logged?
  629. :home? home?
  630. :route-name route-name
  631. :indexeddb-support? indexeddb-support?
  632. :light? light?
  633. :db-restoring? db-restoring?
  634. :main-content main-content
  635. :show-action-bar? show-action-bar?
  636. :show-recording-bar? show-recording-bar?})]
  637. (right-sidebar/sidebar)
  638. [:div#app-single-container]]
  639. (ui/notification)
  640. (ui/modal)
  641. (ui/sub-modal)
  642. (command-palette/command-palette-modal)
  643. (select/select-modal)
  644. (custom-context-menu)
  645. (plugins/custom-js-installer {:t t
  646. :current-repo current-repo
  647. :nfs-granted? granted?
  648. :db-restoring? db-restoring?})
  649. [:a#download.hidden]
  650. (when
  651. (and (not config/mobile?)
  652. (not config/publishing?))
  653. (help-button))])))