sidebar.cljs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  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.repo :as repo]
  8. [frontend.components.right-sidebar :as right-sidebar]
  9. [frontend.components.settings :as settings]
  10. [frontend.components.theme :as theme]
  11. [frontend.components.widgets :as widgets]
  12. [frontend.components.plugins :as plugins]
  13. [frontend.components.select :as select]
  14. [frontend.config :as config]
  15. [frontend.context.i18n :as i18n]
  16. [frontend.db :as db]
  17. [frontend.db.model :as db-model]
  18. [frontend.components.svg :as svg]
  19. [frontend.db-mixins :as db-mixins]
  20. [frontend.handler.editor :as editor-handler]
  21. [frontend.handler.route :as route-handler]
  22. [frontend.handler.page :as page-handler]
  23. [frontend.mixins :as mixins]
  24. [frontend.modules.shortcut.data-helper :as shortcut-dh]
  25. [frontend.state :as state]
  26. [frontend.ui :as ui]
  27. [frontend.util :as util]
  28. [reitit.frontend.easy :as rfe]
  29. [goog.dom :as gdom]
  30. [goog.object :as gobj]
  31. [rum.core :as rum]
  32. [frontend.extensions.srs :as srs]
  33. [frontend.extensions.pdf.assets :as pdf-assets]
  34. [frontend.mobile.util :as mobile-util]
  35. [frontend.handler.mobile.swipe :as swipe]))
  36. (defn nav-item
  37. [title href svg-d _active? close-modal-fn]
  38. [:a.mb-1.group.flex.items-center.pl-4.py-2.text-base.leading-6.font-medium.hover:text-gray-200.transition.ease-in-out.duration-150.nav-item
  39. {:href href
  40. :on-click close-modal-fn}
  41. [:svg.mr-4.h-6.w-6.group-hover:text-gray-200.group-focus:text-gray-200.transition.ease-in-out.duration-150
  42. {:viewBox "0 0 24 24", :fill "none", :stroke "currentColor"}
  43. [:path
  44. {:d svg-d
  45. :stroke-width "2"
  46. :stroke-linejoin "round"
  47. :stroke-linecap "round"}]]
  48. title])
  49. (rum/defc nav-content-item
  50. [name {:keys [class]} child]
  51. [:div.nav-content-item.is-expand
  52. {:class class}
  53. [:div.header.items-center.mb-1
  54. {:on-click (fn [^js/MouseEvent e]
  55. (let [^js target (.-target e)
  56. ^js parent (.closest target ".nav-content-item")]
  57. (.toggle (.-classList parent) "is-expand")))}
  58. [:a.font-medium.fade-link name]
  59. [:span
  60. [:a.more svg/arrow-down-v2]]]
  61. [:div.bd child]])
  62. (defn- delta-y
  63. [e]
  64. (when-let [target (.. e -target)]
  65. (let [rect (.. target getBoundingClientRect)]
  66. (- (.. e -pageY) (.. rect -top)))))
  67. (defn- move-up?
  68. [e]
  69. (let [delta (delta-y e)]
  70. (< delta 14)))
  71. (rum/defc page-name
  72. [name icon]
  73. (let [original-name (db-model/get-page-original-name name)]
  74. [:a {:on-click (fn [e]
  75. (let [name (util/safe-page-name-sanity-lc name)]
  76. (if (gobj/get e "shiftKey")
  77. (when-let [page-entity (db/entity [:block/name name])]
  78. (state/sidebar-add-block!
  79. (state/get-current-repo)
  80. (:db/id page-entity)
  81. :page
  82. {:page page-entity}))
  83. (route-handler/redirect-to-page! name))))}
  84. [:span.page-icon icon]
  85. (pdf-assets/fix-local-asset-filename original-name)]))
  86. (defn get-page-icon [page-entity]
  87. (let [default-icon "◦"
  88. from-properties (get-in (into {} page-entity) [:block/properties :icon])]
  89. (or
  90. (when (not= from-properties "") from-properties)
  91. default-icon))) ;; Fall back to default if icon is undefined or empty
  92. (rum/defcs favorite-item <
  93. (rum/local nil ::up?)
  94. (rum/local nil ::dragging-over)
  95. [state _t name icon]
  96. (let [up? (get state ::up?)
  97. dragging-over (get state ::dragging-over)
  98. target (state/sub :favorites/dragging)]
  99. [:li.favorite-item
  100. {:key name
  101. :class (if (and target @dragging-over (not= target @dragging-over))
  102. "dragging-target"
  103. "")
  104. :draggable true
  105. :on-drag-start (fn [_event]
  106. (state/set-state! :favorites/dragging name))
  107. :on-drag-over (fn [e]
  108. (util/stop e)
  109. (reset! dragging-over name)
  110. (when-not (= name (get @state/state :favorites/dragging))
  111. (reset! up? (move-up? e))))
  112. :on-drag-leave (fn [_e]
  113. (reset! dragging-over nil))
  114. :on-drop (fn [e]
  115. (page-handler/reorder-favorites! {:to name
  116. :up? (move-up? e)})
  117. (reset! up? nil)
  118. (reset! dragging-over nil))}
  119. (page-name name icon)]))
  120. (rum/defc favorites < rum/reactive
  121. [t]
  122. (nav-content-item
  123. [:a.flex.items-center.text-sm.font-medium.rounded-md
  124. (ui/icon "star mr-1" {:style {:font-size 18}})
  125. [:span.flex-1.ml-1 {:style {:padding-top 2}}
  126. (t :left-side-bar/nav-favorites)]]
  127. {:class "favorites"
  128. :edit-fn
  129. (fn [e]
  130. (rfe/push-state :page {:name "Favorites"})
  131. (util/stop e))}
  132. (let [favorites (->> (:favorites (state/sub-graph-config))
  133. (remove string/blank?)
  134. (filter string?))]
  135. (when (seq favorites)
  136. [:ul.favorites.text-sm
  137. (for [name favorites]
  138. (when-not (string/blank? name)
  139. (when-let [entity (db/entity [:block/name (util/safe-page-name-sanity-lc name)])]
  140. (let [icon (get-page-icon entity)]
  141. (favorite-item t name icon)))))]))))
  142. (rum/defc recent-pages
  143. < rum/reactive db-mixins/query
  144. [t]
  145. (nav-content-item
  146. [:a.flex.items-center.text-sm.font-medium.rounded-md
  147. (ui/icon "history mr-2" {:style {:font-size 18}})
  148. [:span.flex-1 {:style {:padding-top 2}}
  149. (t :left-side-bar/nav-recent-pages)]]
  150. {:class "recent"}
  151. (let [pages (->> (db/sub-key-value :recent/pages)
  152. (remove string/blank?)
  153. (filter string?)
  154. (map (fn [page] {:lowercase (util/safe-page-name-sanity-lc page)
  155. :page page}))
  156. (util/distinct-by :lowercase)
  157. (map :page))]
  158. [:ul.text-sm
  159. (for [name pages]
  160. (when-let [entity (db/entity [:block/name (util/safe-page-name-sanity-lc name)])]
  161. [:li.recent-item {:key name}
  162. (page-name name (get-page-icon entity))]))])))
  163. (rum/defcs flashcards < db-mixins/query rum/reactive
  164. {:did-mount (fn [state]
  165. (srs/update-cards-due-count!)
  166. state)}
  167. [state]
  168. (let [num (state/sub :srs/cards-due-count)]
  169. (rum/with-context [[t] i18n/*tongue-context*]
  170. [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md {:on-click #(state/pub-event! [:modal/show-cards])}
  171. (ui/icon "infinity mr-3" {:style {:font-size 20}})
  172. [:span.flex-1 (t :right-side-bar/flashcards)]
  173. (when (and num (not (zero? num)))
  174. [:span.ml-3.inline-block.py-0.5.px-3.text-xs.font-medium.rounded-full.fade-in num])])))
  175. (defn get-default-home-if-valid
  176. []
  177. (when-let [default-home (state/get-default-home)]
  178. (let [page (:page default-home)
  179. page (when (and (string? page)
  180. (not (string/blank? page)))
  181. (db/entity [:block/name (util/safe-page-name-sanity-lc page)]))]
  182. (if page
  183. default-home
  184. (dissoc default-home :page)))))
  185. (defn sidebar-item
  186. [{on-click-handler :on-click-handler
  187. class :class
  188. title :title
  189. icon :icon
  190. href :href}]
  191. [:div
  192. {:class class}
  193. [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
  194. {:on-click on-click-handler
  195. :href href}
  196. (ui/icon (str icon " mr-3") {:style {:font-size 20}})
  197. [:span.flex-1 title]]])
  198. (rum/defc sidebar-nav
  199. [_route-match close-modal-fn]
  200. (rum/with-context [[t] i18n/*tongue-context*]
  201. (let [default-home (get-default-home-if-valid)]
  202. [:div.left-sidebar-inner.flex-1.flex.flex-col.min-h-0
  203. {:on-click #(when-let [^js target (and (util/sm-breakpoint?) (.-target %))]
  204. (when (some (fn [sel] (boolean (.closest target sel)))
  205. [".favorites .bd" ".recent .bd" ".dropdown-wrapper" ".nav-header"])
  206. (close-modal-fn)))}
  207. [:div.flex.flex-col.pb-4.wrap
  208. [:nav.px-2.space-y-1 {:aria-label "Sidebar"}
  209. (repo/repos-dropdown)
  210. [:div.nav-header
  211. (if (:page default-home)
  212. (sidebar-item
  213. {:class "home-nav"
  214. :title (:page default-home)
  215. :on-click-handler route-handler/redirect-to-home!
  216. :icon "home"})
  217. (sidebar-item
  218. {:class "journals-nav"
  219. :title (t :right-side-bar/journals)
  220. :on-click-handler route-handler/go-to-journals!
  221. :icon "calendar"}))
  222. [:div.flashcards-nav
  223. (flashcards)]
  224. (sidebar-item
  225. {:class "graph-view-nav"
  226. :title (t :right-side-bar/graph-view)
  227. :href (rfe/href :graph)
  228. :icon "hierarchy"})
  229. (sidebar-item
  230. {:class "all-pages-nav"
  231. :title (t :right-side-bar/all-pages)
  232. :href (rfe/href :all-pages)
  233. :icon "files"})]]
  234. (favorites t)
  235. (recent-pages t)
  236. [:nav.px-2 {:aria-label "Sidebar"
  237. :class "new-page"}
  238. (when-not config/publishing?
  239. [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
  240. {:on-click (fn []
  241. (and (util/sm-breakpoint?)
  242. (state/toggle-left-sidebar!))
  243. (state/pub-event! [:go/search]))}
  244. (ui/icon "circle-plus mr-3" {:style {:font-size 20}})
  245. [:span.flex-1 (t :right-side-bar/new-page)]])]]])))
  246. (rum/defc left-sidebar < rum/reactive
  247. [{:keys [left-sidebar-open? route-match]}]
  248. (let [close-fn #(state/set-left-sidebar-open! false)]
  249. [:div#left-sidebar.cp__sidebar-left-layout
  250. {:class (util/classnames [{:is-open left-sidebar-open?}])}
  251. ;; sidebar contents
  252. (sidebar-nav route-match close-fn)
  253. [:span.shade-mask {:on-click close-fn}]]))
  254. (rum/defc main <
  255. {:did-mount (fn [state]
  256. (when-let [element (gdom/getElement "main-content-container")]
  257. (dnd/subscribe!
  258. element
  259. :upload-files
  260. {:drop (fn [_e files]
  261. (when-let [id (state/get-edit-input-id)]
  262. (let [format (:block/format (state/get-edit-block))]
  263. (editor-handler/upload-asset id files format editor-handler/*asset-uploading? true))))}))
  264. state)}
  265. [{:keys [route-match global-graph-pages? route-name indexeddb-support? db-restoring? main-content]}]
  266. (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)]
  267. (rum/with-context [[t] i18n/*tongue-context*]
  268. [:div#main-container.cp__sidebar-main-layout.flex-1.flex
  269. {:class (util/classnames [{:is-left-sidebar-open left-sidebar-open?}])}
  270. ;; desktop left sidebar layout
  271. (left-sidebar {:left-sidebar-open? left-sidebar-open?
  272. :route-match route-match})
  273. [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center
  274. [:div.cp__sidebar-main-content
  275. {:data-is-global-graph-pages global-graph-pages?
  276. :data-is-full-width (or global-graph-pages?
  277. (contains? #{:all-files :all-pages :my-publishing} route-name))}
  278. (when-not (mobile-util/is-native-platform?)
  279. (widgets/demo-graph-alert))
  280. (widgets/github-integration-soon-deprecated-alert)
  281. (cond
  282. (not indexeddb-support?)
  283. nil
  284. db-restoring?
  285. [:div.mt-20
  286. [:div.ls-center
  287. (ui/loading (t :loading))]]
  288. :else
  289. [:div {:class (if global-graph-pages? "" (util/hiccup->class "max-w-7xl.mx-auto.pb-24"))
  290. :style {:margin-bottom (if global-graph-pages? 0 120)
  291. :padding-bottom (when (mobile-util/native-iphone?) "7rem")}}
  292. main-content])]]])))
  293. (rum/defc footer
  294. []
  295. (when-let [user-footer (and config/publishing? (get-in (state/get-config) [:publish-common-footer]))]
  296. [:div.p-6 user-footer]))
  297. (defonce sidebar-inited? (atom false))
  298. ;; TODO: simplify logic
  299. (rum/defc main-content < rum/reactive db-mixins/query
  300. {:init (fn [state]
  301. (when-not @sidebar-inited?
  302. (let [current-repo (state/sub :git/current-repo)
  303. default-home (get-default-home-if-valid)
  304. sidebar (:sidebar default-home)
  305. sidebar (if (string? sidebar) [sidebar] sidebar)]
  306. (when-let [pages (->> (seq sidebar)
  307. (remove string/blank?))]
  308. (doseq [page pages]
  309. (let [page (util/safe-page-name-sanity-lc page)
  310. [db-id block-type] (if (= page "contents")
  311. ["contents" :contents]
  312. [page :page])]
  313. (state/sidebar-add-block! current-repo db-id block-type nil)))
  314. (reset! sidebar-inited? true))))
  315. state)}
  316. []
  317. (let [cloning? (state/sub :repo/cloning?)
  318. default-home (get-default-home-if-valid)
  319. importing-to-db? (state/sub :repo/importing-to-db?)
  320. current-repo (state/sub :git/current-repo)
  321. loading-files? (when current-repo (state/sub [:repo/loading-files? current-repo]))
  322. me (state/sub :me)
  323. journals-length (state/sub :journals-length)
  324. latest-journals (db/get-latest-journals (state/get-current-repo) journals-length)
  325. preferred-format (state/sub [:me :preferred_format])
  326. logged? (:name me)]
  327. (rum/with-context [[t] i18n/*tongue-context*]
  328. [:div
  329. (cond
  330. (and default-home
  331. (= :home (state/get-current-route))
  332. (not (state/route-has-p?))
  333. (:page default-home))
  334. (route-handler/redirect-to-page! (:page default-home))
  335. (and config/publishing?
  336. (not default-home)
  337. (empty? latest-journals))
  338. (route-handler/redirect! {:to :all-pages})
  339. importing-to-db?
  340. (ui/loading (t :parsing-files))
  341. loading-files?
  342. (ui/loading (t :loading-files))
  343. (and (not logged?) (seq latest-journals))
  344. (journal/journals latest-journals)
  345. (and logged? (not preferred-format))
  346. (widgets/choose-preferred-format)
  347. ;; TODO: delay this
  348. (and logged? (nil? (:email me)))
  349. (settings/set-email)
  350. cloning?
  351. (ui/loading (t :cloning))
  352. (seq latest-journals)
  353. (journal/journals latest-journals)
  354. (or
  355. (and (mobile-util/is-native-platform?)
  356. (nil? (state/get-current-repo)))
  357. (and logged? (empty? (:repos me))))
  358. (widgets/add-graph)
  359. ;; FIXME: why will this happen?
  360. :else
  361. [:div])])))
  362. (rum/defc custom-context-menu < rum/reactive
  363. []
  364. (when (state/sub :custom-context-menu/show?)
  365. (when-let [links (state/sub :custom-context-menu/links)]
  366. (ui/css-transition
  367. {:class-names "fade"
  368. :timeout {:enter 500
  369. :exit 300}}
  370. links
  371. ;; (custom-context-menu-content)
  372. ))))
  373. (rum/defc new-block-mode < rum/reactive
  374. []
  375. (when (state/sub [:document/mode?])
  376. (ui/tippy {:html [:div.p-2
  377. [:p.mb-2 [:b "Document mode"]]
  378. [:ul
  379. [:li
  380. [:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :editor/new-line))]
  381. [:p.inline-block "to create new block"]]
  382. [:li
  383. [:p.inline-block.mr-1 "Click `D` or type"]
  384. [:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :ui/toggle-document-mode))]
  385. [:p.inline-block "to toggle document mode"]]]]}
  386. [:a.block.px-1.text-sm.font-medium.bg-base-2.rounded-md.mx-2
  387. {:on-click state/toggle-document-mode!}
  388. "D"])))
  389. (rum/defc help-button < rum/reactive
  390. []
  391. (when-not (state/sub :ui/sidebar-open?)
  392. ;; TODO: remove with-context usage
  393. (rum/with-context [[t] i18n/*tongue-context*]
  394. [:div.cp__sidebar-help-btn
  395. {:title (t :help-shortcut-title)
  396. :on-click (fn []
  397. (state/sidebar-add-block! (state/get-current-repo) "help" :help nil))}
  398. "?"])))
  399. (defn- hide-context-menu-and-clear-selection
  400. [e]
  401. (state/hide-custom-context-menu!)
  402. (when-not (gobj/get e "shiftKey")
  403. (editor-handler/clear-selection!)))
  404. (rum/defcs sidebar <
  405. (mixins/modal :modal/show?)
  406. rum/reactive
  407. (mixins/event-mixin
  408. (fn [state]
  409. (mixins/listen state js/window "click" hide-context-menu-and-clear-selection)
  410. (mixins/listen state js/window "keydown"
  411. (fn [e]
  412. (when (= 27 (.-keyCode e))
  413. (if (state/modal-opened?)
  414. (state/close-modal!)
  415. (hide-context-menu-and-clear-selection e)))))))
  416. {:did-mount (fn [state]
  417. (swipe/setup-listeners!)
  418. state)}
  419. [state route-match main-content]
  420. (let [{:keys [open-fn]} state
  421. me (state/sub :me)
  422. current-repo (state/sub :git/current-repo)
  423. granted? (state/sub [:nfs/user-granted? (state/get-current-repo)])
  424. theme (state/sub :ui/theme)
  425. system-theme? (state/sub :ui/system-theme?)
  426. white? (= "white" (state/sub :ui/theme))
  427. sidebar-open? (state/sub :ui/sidebar-open?)
  428. settings-open? (state/sub :ui/settings-open?)
  429. left-sidebar-open? (state/sub :ui/left-sidebar-open?)
  430. right-sidebar-blocks (state/sub-right-sidebar-blocks)
  431. route-name (get-in route-match [:data :name])
  432. global-graph-pages? (= :graph route-name)
  433. logged? (:name me)
  434. db-restoring? (state/sub :db/restoring?)
  435. indexeddb-support? (state/sub :indexeddb/support?)
  436. page? (= :page route-name)
  437. home? (= :home route-name)
  438. edit? (:editor/editing? @state/state)
  439. default-home (get-default-home-if-valid)]
  440. (rum/with-context [[t] i18n/*tongue-context*]
  441. (theme/container
  442. {:t t
  443. :theme theme
  444. :route route-match
  445. :current-repo current-repo
  446. :edit? edit?
  447. :nfs-granted? granted?
  448. :db-restoring? db-restoring?
  449. :sidebar-open? sidebar-open?
  450. :settings-open? settings-open?
  451. :sidebar-blocks-len (count right-sidebar-blocks)
  452. :system-theme? system-theme?
  453. :on-click (fn [e]
  454. (editor-handler/unhighlight-blocks!)
  455. (util/fix-open-external-with-shift! e))}
  456. [:div.theme-inner
  457. {:class (util/classnames [{:ls-left-sidebar-open left-sidebar-open?
  458. :ls-right-sidebar-open sidebar-open?}])}
  459. [:div.#app-container
  460. [:div#left-container
  461. {:class (if (state/sub :ui/sidebar-open?) "overflow-hidden" "w-full")}
  462. (header/header {:open-fn open-fn
  463. :white? white?
  464. :current-repo current-repo
  465. :logged? logged?
  466. :page? page?
  467. :route-match route-match
  468. :me me
  469. :default-home default-home
  470. :new-block-mode new-block-mode})
  471. (main {:route-match route-match
  472. :global-graph-pages? global-graph-pages?
  473. :logged? logged?
  474. :home? home?
  475. :route-name route-name
  476. :indexeddb-support? indexeddb-support?
  477. :white? white?
  478. :db-restoring? db-restoring?
  479. :main-content main-content})
  480. (footer)]
  481. (right-sidebar/sidebar)
  482. [:div#app-single-container]]
  483. (ui/notification)
  484. (ui/modal)
  485. (ui/sub-modal)
  486. (command-palette/command-palette-modal)
  487. (select/select-modal)
  488. (custom-context-menu)
  489. (plugins/custom-js-installer {:t t
  490. :current-repo current-repo
  491. :nfs-granted? granted?
  492. :db-restoring? db-restoring?})
  493. [:a#download.hidden]
  494. (when
  495. (and (not config/mobile?)
  496. (not config/publishing?))
  497. (help-button))]))))