container.cljs 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089
  1. (ns frontend.components.container
  2. (:require [cljs-drag-n-drop.core :as dnd]
  3. [clojure.string :as string]
  4. [dommy.core :as d]
  5. [electron.ipc :as ipc]
  6. [frontend.components.block :as block]
  7. [frontend.components.content :as cp-content]
  8. [frontend.components.dnd :as dnd-component]
  9. [frontend.components.find-in-page :as find-in-page]
  10. [frontend.components.handbooks :as handbooks]
  11. [frontend.components.header :as header]
  12. [frontend.components.icon :as icon]
  13. [frontend.components.journal :as journal]
  14. [frontend.components.plugins :as plugins]
  15. [frontend.components.repo :as repo]
  16. [frontend.components.right-sidebar :as right-sidebar]
  17. [frontend.components.theme :as theme]
  18. [frontend.components.window-controls :as window-controls]
  19. [frontend.config :as config]
  20. [frontend.context.i18n :refer [t tt]]
  21. [frontend.db :as db]
  22. [frontend.db-mixins :as db-mixins]
  23. [frontend.db.async :as db-async]
  24. [frontend.db.model :as db-model]
  25. [frontend.extensions.fsrs :as fsrs]
  26. [frontend.extensions.pdf.utils :as pdf-utils]
  27. [frontend.handler.block :as block-handler]
  28. [frontend.handler.common :as common-handler]
  29. [frontend.handler.editor :as editor-handler]
  30. [frontend.handler.page :as page-handler]
  31. [frontend.handler.recent :as recent-handler]
  32. [frontend.handler.route :as route-handler]
  33. [frontend.handler.user :as user-handler]
  34. [frontend.handler.whiteboard :as whiteboard-handler]
  35. [frontend.mixins :as mixins]
  36. [frontend.mobile.action-bar :as action-bar]
  37. [frontend.mobile.footer :as footer]
  38. [frontend.mobile.mobile-bar :refer [mobile-bar]]
  39. [frontend.mobile.util :as mobile-util]
  40. [frontend.modules.shortcut.data-helper :as shortcut-dh]
  41. [frontend.modules.shortcut.utils :as shortcut-utils]
  42. [frontend.state :as state]
  43. [frontend.storage :as storage]
  44. [frontend.ui :as ui]
  45. [frontend.util :as util]
  46. [frontend.util.cursor :as cursor]
  47. [frontend.util.page :as page-util]
  48. [frontend.version :refer [version]]
  49. [goog.dom :as gdom]
  50. [goog.object :as gobj]
  51. [logseq.common.path :as path]
  52. [logseq.common.util.namespace :as ns-util]
  53. [logseq.db :as ldb]
  54. [logseq.shui.dialog.core :as shui-dialog]
  55. [logseq.shui.hooks :as hooks]
  56. [logseq.shui.popup.core :as shui-popup]
  57. [logseq.shui.toaster.core :as shui-toaster]
  58. [logseq.shui.ui :as shui]
  59. [medley.core :as medley]
  60. [promesa.core :as p]
  61. [react-draggable]
  62. [reitit.frontend.easy :as rfe]
  63. [rum.core :as rum]))
  64. (rum/defc sidebar-content-group < rum/reactive
  65. [name {:keys [class count more header-props enter-show-more? collapsable?]} child]
  66. (let [collapsed? (state/sub [:ui/navigation-item-collapsed? class])]
  67. [:div.sidebar-content-group
  68. {:class (util/classnames [class {:is-expand (not collapsed?)
  69. :has-children (and (number? count) (> count 0))}])}
  70. [:div.sidebar-content-group-inner
  71. [:div.hd.items-center
  72. (cond-> (merge header-props
  73. {:class (util/classnames [(:class header-props)
  74. {:non-collapsable (false? collapsable?)
  75. :enter-show-more (true? enter-show-more?)}])})
  76. (not (false? collapsable?))
  77. (assoc :on-click (fn [^js/MouseEvent _e]
  78. (state/toggle-navigation-item-collapsed! class))))
  79. [:span.a name]
  80. [:span.b (or more (ui/icon "chevron-right" {:class "more" :size 15}))]]
  81. (when child [:div.bd child])]]))
  82. (rum/defc page-name
  83. [page icon recent?]
  84. (let [repo (state/get-current-repo)
  85. db-based? (config/db-based-graph? repo)
  86. title (:block/title page)
  87. untitled? (db-model/untitled-page? title)
  88. name (:block/name page)
  89. file-rpath (when (util/electron?) (page-util/get-page-file-rpath name))
  90. ctx-icon #(shui/tabler-icon %1 {:class "scale-90 pr-1 opacity-80"})
  91. open-in-sidebar #(state/sidebar-add-block!
  92. (state/get-current-repo)
  93. (:db/id page)
  94. :page)
  95. x-menu-content (fn []
  96. (let [x-menu-item shui/dropdown-menu-item
  97. x-menu-shortcut shui/dropdown-menu-shortcut]
  98. [:<>
  99. (when-not recent?
  100. (x-menu-item
  101. {:key "unfavorite"
  102. :on-click #(page-handler/<unfavorite-page! (if db-based? (str (:block/uuid page)) title))}
  103. (ctx-icon "star-off")
  104. (t :page/unfavorite)
  105. (x-menu-shortcut (when-let [binding (shortcut-dh/shortcut-binding :command/toggle-favorite)]
  106. (some-> binding
  107. (first)
  108. (shortcut-utils/decorate-binding))))))
  109. (when-let [page-fpath (and (util/electron?) file-rpath
  110. (config/get-repo-fpath (state/get-current-repo) file-rpath))]
  111. [:<>
  112. (x-menu-item
  113. {:key "open-in-folder"
  114. :on-click #(ipc/ipc :openFileInFolder page-fpath)}
  115. (ctx-icon "folder")
  116. (t :page/open-in-finder))
  117. (x-menu-item
  118. {:key "open with default app"
  119. :on-click #(js/window.apis.openPath page-fpath)}
  120. (ctx-icon "file")
  121. (t :page/open-with-default-app))])
  122. (x-menu-item
  123. {:key "open in sidebar"
  124. :on-click open-in-sidebar}
  125. (ctx-icon "layout-sidebar-right")
  126. (t :content/open-in-sidebar)
  127. (x-menu-shortcut (shortcut-utils/decorate-binding "shift+click")))]))]
  128. ;; TODO: move to standalone component
  129. [:a.link-item.group
  130. (cond->
  131. {:on-click
  132. (fn [e]
  133. (if (gobj/get e "shiftKey")
  134. (open-in-sidebar)
  135. (route-handler/redirect-to-page! (:block/uuid page) {:click-from-recent? recent?})))
  136. :on-context-menu (fn [^js e]
  137. (shui/popup-show! e (x-menu-content)
  138. {:as-dropdown? true
  139. :content-props {:on-click (fn [] (shui/popup-hide!))
  140. :class "w-60"}})
  141. (util/stop e))}
  142. (ldb/object? page)
  143. (assoc :title (block-handler/block-unique-title page)))
  144. [:span.page-icon {:key "page-icon"} icon]
  145. [:span.page-title {:key "title"
  146. :class (when untitled? "opacity-50")
  147. :style {:display "ruby"}}
  148. (cond
  149. (not (db/page? page))
  150. (block/inline-text :markdown (:block/title page))
  151. untitled? (t :untitled)
  152. :else (let [title' (pdf-utils/fix-local-asset-pagename title)
  153. parent (:logseq.property/parent page)]
  154. (if (and parent (not (ldb/class? page)))
  155. (str (:block/title parent) ns-util/parent-char title')
  156. title')))]
  157. ;; dots trigger
  158. (shui/button
  159. {:key "more actions"
  160. :size :sm
  161. :variant :ghost
  162. :class "absolute !bg-transparent right-0 top-0 px-1.5 scale-75 opacity-40 hidden group-hover:block hover:opacity-80 active:opacity-100"
  163. :on-click #(do
  164. (shui/popup-show! (.-target %) (x-menu-content)
  165. {:as-dropdown? true
  166. :content-props {:on-click (fn [] (shui/popup-hide!))
  167. :class "w-60"}})
  168. (util/stop %))}
  169. [:i.relative {:style {:top "4px"}} (shui/tabler-icon "dots")])]))
  170. (defn sidebar-item
  171. [{:keys [on-click-handler class title icon icon-extension? active href shortcut more]}]
  172. [:div
  173. {:key class
  174. :class (util/classnames [class {:active active}])}
  175. [:a.item.group.flex.items-center.text-sm.rounded-md.font-medium
  176. {:on-click on-click-handler
  177. :class (when active "active")
  178. :href href}
  179. (ui/icon (str icon) {:extension? icon-extension? :size 16})
  180. [:span.flex-1 title]
  181. (when shortcut
  182. [:span.ml-1
  183. (ui/render-keyboard-shortcut
  184. (ui/keyboard-shortcut-from-config shortcut {:pick-first? true}))])
  185. more]])
  186. (rum/defc sidebar-graphs
  187. []
  188. [:div.sidebar-graphs
  189. (repo/graphs-selector)])
  190. (rum/defc sidebar-navigations-edit-content
  191. [{:keys [_id navs checked-navs set-checked-navs!]}]
  192. (let [[local-navs set-local-navs!] (rum/use-state checked-navs)]
  193. (hooks/use-effect!
  194. (fn []
  195. (set-checked-navs! local-navs))
  196. [local-navs])
  197. (for [nav navs
  198. :let [name' (name nav)]]
  199. (shui/dropdown-menu-checkbox-item
  200. {:checked (contains? (set local-navs) nav)
  201. :onCheckedChange (fn [v] (set-local-navs!
  202. (fn []
  203. (if v
  204. (conj local-navs nav)
  205. (filterv #(not= nav %) local-navs)))))}
  206. (tt (keyword "left-side-bar" name')
  207. (keyword "right-side-bar" name'))))))
  208. (rum/defc ^:large-vars/cleanup-todo sidebar-navigations
  209. [{:keys [default-home route-match route-name srs-open? db-based? enable-whiteboards?]}]
  210. (let [navs (cond-> [:flashcards :graph-view :all-pages]
  211. db-based?
  212. (concat [:tag/tasks :tag/assets])
  213. (not db-based?)
  214. (#(cons :whiteboards %)))
  215. [checked-navs set-checked-navs!] (rum/use-state (or (storage/get :ls-sidebar-navigations)
  216. [:whiteboards :flashcards :graph-view :all-pages]))]
  217. (hooks/use-effect!
  218. (fn []
  219. (when (vector? checked-navs)
  220. (storage/set :ls-sidebar-navigations checked-navs)))
  221. [checked-navs])
  222. (sidebar-content-group
  223. [:a.wrap-th [:strong.flex-1 "Navigations"]]
  224. {:collapsable? false
  225. :enter-show-more? true
  226. :header-props {:on-click (fn [^js e] (when-let [^js _el (some-> (.-target e) (.closest ".as-edit"))]
  227. (shui/popup-show! _el
  228. #(sidebar-navigations-edit-content
  229. {:id (:id %) :navs navs
  230. :checked-navs checked-navs
  231. :set-checked-navs! set-checked-navs!})
  232. {:as-dropdown? false})))}
  233. :more [:a.as-edit {:class "!opacity-60 hover:!opacity-80 relative -top-0.5 -right-0.5"}
  234. (shui/tabler-icon "filter-edit" {:size 14})]}
  235. [:div.sidebar-navigations.flex.flex-col.mt-1
  236. ;; required custom home page
  237. (let [page (:page default-home)
  238. enable-journals? (state/enable-journals? (state/get-current-repo))]
  239. (if (and page (not enable-journals?))
  240. (sidebar-item
  241. {:class "home-nav"
  242. :title page
  243. :on-click-handler route-handler/redirect-to-home!
  244. :active (and (not srs-open?)
  245. (= route-name :page)
  246. (= page (get-in route-match [:path-params :name])))
  247. :icon "home"
  248. :shortcut :go/home})
  249. (when enable-journals?
  250. (sidebar-item
  251. {:class "journals-nav"
  252. :active (and (not srs-open?)
  253. (or (= route-name :all-journals) (= route-name :home)))
  254. :title (t :left-side-bar/journals)
  255. :on-click-handler (fn [e]
  256. (if (gobj/get e "shiftKey")
  257. (route-handler/sidebar-journals!)
  258. (route-handler/go-to-journals!)))
  259. :icon "calendar"
  260. :shortcut :go/journals}))))
  261. (for [nav checked-navs]
  262. (cond
  263. (= nav :whiteboards)
  264. (when enable-whiteboards?
  265. (when (not db-based?)
  266. (sidebar-item
  267. {:class "whiteboard"
  268. :title (t :right-side-bar/whiteboards)
  269. :href (rfe/href :whiteboards)
  270. :on-click-handler (fn [_e] (whiteboard-handler/onboarding-show))
  271. :active (and (not srs-open?) (#{:whiteboard :whiteboards} route-name))
  272. :icon "writing"
  273. :shortcut :go/whiteboards})))
  274. (= nav :flashcards)
  275. (when (state/enable-flashcards? (state/get-current-repo))
  276. (let [num (state/sub :srs/cards-due-count)]
  277. (sidebar-item
  278. {:class "flashcards-nav"
  279. :title (t :right-side-bar/flashcards)
  280. :icon "infinity"
  281. :shortcut :go/flashcards
  282. :active srs-open?
  283. :on-click-handler #(do (fsrs/update-due-cards-count)
  284. (state/pub-event! [:modal/show-cards]))
  285. :more (when (and num (not (zero? num)))
  286. [:span.ml-1.inline-block.py-0.5.px-3.text-xs.font-medium.rounded-full.fade-in num])})))
  287. (= nav :graph-view)
  288. (sidebar-item
  289. {:class "graph-view-nav"
  290. :title (t :right-side-bar/graph-view)
  291. :href (rfe/href :graph)
  292. :active (and (not srs-open?) (= route-name :graph))
  293. :icon "hierarchy"
  294. :shortcut :go/graph-view})
  295. (= nav :all-pages)
  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. (= (namespace nav) "tag")
  303. (when db-based?
  304. (let [name'' (name nav)
  305. class-ident (get {"assets" :logseq.class/Asset "tasks" :logseq.class/Task} name'')]
  306. (when-let [tag-uuid (and class-ident (:block/uuid (db/entity class-ident)))]
  307. (sidebar-item
  308. {:class (str "tag-view-nav " name'')
  309. :title (tt (keyword "left-side-bar" name'')
  310. (keyword "right-side-bar" name''))
  311. :href (rfe/href :page {:name tag-uuid})
  312. :active (= (str tag-uuid) (get-in route-match [:path-params :name]))
  313. :icon "hash"}))))))])))
  314. (rum/defc sidebar-favorites < rum/reactive
  315. []
  316. (let [_favorites-updated? (state/sub :favorites/updated?)
  317. favorite-entities (page-handler/get-favorites)]
  318. (sidebar-content-group
  319. [:a.wrap-th
  320. [:strong.flex-1 (t :left-side-bar/nav-favorites)]]
  321. {:class "favorites"
  322. :count (count favorite-entities)
  323. :edit-fn
  324. (fn [e]
  325. (rfe/push-state :page {:name "Favorites"})
  326. (util/stop e))}
  327. (when (seq favorite-entities)
  328. (let [favorite-items (map
  329. (fn [e]
  330. (let [icon (icon/get-node-icon-cp e {:size 16})]
  331. {:id (str (:db/id e))
  332. :value (:block/uuid e)
  333. :content [:li.favorite-item.font-medium (page-name e icon false)]}))
  334. favorite-entities)]
  335. (dnd-component/items favorite-items
  336. {:on-drag-end (fn [favorites']
  337. (page-handler/<reorder-favorites! favorites'))
  338. :parent-node :ul.favorites.text-sm}))))))
  339. (rum/defc sidebar-recent-pages < rum/reactive db-mixins/query
  340. []
  341. (let [pages (recent-handler/get-recent-pages)]
  342. (sidebar-content-group
  343. [:a.wrap-th [:strong.flex-1 (t :left-side-bar/nav-recent-pages)]]
  344. {:class "recent"
  345. :count (count pages)}
  346. [:ul.text-sm
  347. (for [page pages]
  348. [:li.recent-item.select-none.font-medium
  349. {:key (str "recent-" (:db/id page))
  350. :title (block-handler/block-unique-title page)}
  351. (page-name page (icon/get-node-icon-cp page {:size 16}) true)])])))
  352. (defn get-default-home-if-valid
  353. []
  354. (when-let [default-home (state/get-default-home)]
  355. (let [page (:page default-home)
  356. page (when (and (string? page)
  357. (not (string/blank? page)))
  358. (db/get-page page))]
  359. (if page
  360. default-home
  361. (dissoc default-home :page)))))
  362. (rum/defc ^:large-vars/cleanup-todo sidebar-container
  363. [route-match close-modal-fn left-sidebar-open? enable-whiteboards? srs-open?
  364. *closing? close-signal touching-x-offset]
  365. (let [[local-closing? set-local-closing?] (rum/use-state false)
  366. [el-rect set-el-rect!] (rum/use-state nil)
  367. ref-el (rum/use-ref nil)
  368. ref-open? (rum/use-ref left-sidebar-open?)
  369. db-based? (config/db-based-graph? (state/get-current-repo))
  370. default-home (get-default-home-if-valid)
  371. route-name (get-in route-match [:data :name])
  372. on-contents-scroll #(when-let [^js el (.-target %)]
  373. (let [top (.-scrollTop el)
  374. cls (.-classList el)
  375. cls' "is-scrolled"]
  376. (if (> top 2)
  377. (.add cls cls')
  378. (.remove cls cls'))))
  379. close-fn #(set-local-closing? true)
  380. touching-x-offset (when (number? touching-x-offset)
  381. (if-not left-sidebar-open?
  382. (when (> touching-x-offset 0)
  383. (min touching-x-offset (:width el-rect)))
  384. (when (< touching-x-offset 0)
  385. (max touching-x-offset (- 0 (:width el-rect))))))
  386. offset-ratio (and (number? touching-x-offset)
  387. (some->> (:width el-rect)
  388. (/ touching-x-offset)))]
  389. (hooks/use-effect!
  390. #(js/setTimeout
  391. (fn [] (some-> (rum/deref ref-el)
  392. (.getBoundingClientRect)
  393. (.toJSON)
  394. (js->clj :keywordize-keys true)
  395. (set-el-rect!)))
  396. 16)
  397. [])
  398. (hooks/use-layout-effect!
  399. (fn []
  400. (when (and (rum/deref ref-open?) local-closing?)
  401. (reset! *closing? true))
  402. (rum/set-ref! ref-open? left-sidebar-open?)
  403. #())
  404. [local-closing? left-sidebar-open?])
  405. (hooks/use-effect!
  406. (fn []
  407. (when-not (neg? close-signal)
  408. (close-fn)))
  409. [close-signal])
  410. [:<>
  411. [:div.left-sidebar-inner.flex-1.flex.flex-col.min-h-0
  412. {:key "left-sidebar"
  413. :ref ref-el
  414. :style (cond-> {}
  415. (and (number? offset-ratio)
  416. (> touching-x-offset 0))
  417. (assoc :transform (str "translate3d(calc(" touching-x-offset "px - 100%), 0, 0)"))
  418. (and (number? offset-ratio)
  419. (< touching-x-offset 0))
  420. (assoc :transform (str "translate3d(" (* offset-ratio 100) "%, 0, 0)")))
  421. :on-transition-end (fn []
  422. (when local-closing?
  423. (reset! *closing? false)
  424. (set-local-closing? false)
  425. (close-modal-fn)))
  426. :on-click #(when-let [^js target (and (util/sm-breakpoint?) (.-target %))]
  427. (when (some (fn [sel] (boolean (.closest target sel)))
  428. [".favorites .bd" ".recent .bd" ".dropdown-wrapper" ".nav-header"])
  429. (close-fn)))}
  430. [:div.wrap
  431. [:div.sidebar-header-container
  432. ;; sidebar graphs
  433. (sidebar-graphs)
  434. ;; sidebar sticky navigations
  435. (sidebar-navigations
  436. {:default-home default-home
  437. :route-match route-match
  438. :db-based? db-based?
  439. :enable-whiteboards? enable-whiteboards?
  440. :route-name route-name
  441. :srs-open? srs-open?})]
  442. [:div.sidebar-contents-container
  443. {:on-scroll on-contents-scroll}
  444. (sidebar-favorites)
  445. (when (not config/publishing?)
  446. (sidebar-recent-pages))]]]
  447. [:span.shade-mask
  448. (cond-> {:on-click close-fn
  449. :key "shade-mask"}
  450. (number? offset-ratio)
  451. (assoc :style {:opacity (cond-> offset-ratio
  452. (neg? offset-ratio)
  453. (+ 1))}))]]))
  454. (rum/defc sidebar-resizer
  455. []
  456. (let [*el-ref (rum/use-ref nil)
  457. ^js el-doc js/document.documentElement
  458. adjust-size! (fn [width]
  459. (.setProperty (.-style el-doc) "--ls-left-sidebar-width" width)
  460. (storage/set :ls-left-sidebar-width width))]
  461. ;; restore size
  462. (hooks/use-layout-effect!
  463. (fn []
  464. (when-let [width (storage/get :ls-left-sidebar-width)]
  465. (.setProperty (.-style el-doc) "--ls-left-sidebar-width" width))))
  466. ;; draggable handler
  467. (hooks/use-effect!
  468. (fn []
  469. (when-let [el (and (fn? js/window.interact) (rum/deref *el-ref))]
  470. (let [^js sidebar-el (.querySelector el-doc "#left-sidebar")]
  471. (-> (js/interact el)
  472. (.draggable
  473. #js {:listeners
  474. #js {:move (fn [^js/MouseEvent e]
  475. (when-let [offset (.-left (.-rect e))]
  476. (let [width (.toFixed (max (min offset 460) 240) 2)]
  477. (adjust-size! (str width "px")))))}})
  478. (.styleCursor false)
  479. (.on "dragstart" (fn []
  480. (.. sidebar-el -classList (add "is-resizing"))
  481. (.. el-doc -classList (add "is-resizing-buf"))))
  482. (.on "dragend" (fn []
  483. (.. sidebar-el -classList (remove "is-resizing"))
  484. (.. el-doc -classList (remove "is-resizing-buf"))))))
  485. #()))
  486. [])
  487. [:span.left-sidebar-resizer {:ref *el-ref}]))
  488. (rum/defcs left-sidebar < rum/reactive
  489. (rum/local false ::closing?)
  490. (rum/local -1 ::close-signal)
  491. (rum/local nil ::touch-state)
  492. [s {:keys [left-sidebar-open? route-match]}]
  493. (let [close-fn #(state/set-left-sidebar-open! false)
  494. *closing? (::closing? s)
  495. *touch-state (::touch-state s)
  496. *close-signal (::close-signal s)
  497. enable-whiteboards? (state/enable-whiteboards?)
  498. touch-point-fn (fn [^js e] (some-> (gobj/get e "touches") (aget 0) (#(hash-map :x (.-clientX %) :y (.-clientY %)))))
  499. srs-open? (= :srs (state/sub :modal/id))
  500. touching-x-offset (and (some-> @*touch-state :after)
  501. (some->> @*touch-state
  502. ((juxt :after :before))
  503. (map :x) (apply -)))
  504. touch-pending? (> (abs touching-x-offset) 20)]
  505. [:div#left-sidebar.cp__sidebar-left-layout
  506. {:class (util/classnames [{:is-open left-sidebar-open?
  507. :is-closing @*closing?
  508. :is-touching touch-pending?}])
  509. :on-touch-start
  510. (fn [^js e]
  511. (reset! *touch-state {:before (touch-point-fn e)}))
  512. :on-touch-move
  513. (fn [^js e]
  514. (when @*touch-state
  515. (some-> *touch-state (swap! assoc :after (touch-point-fn e)))))
  516. :on-touch-end
  517. (fn []
  518. (when touch-pending?
  519. (cond
  520. (and (not left-sidebar-open?) (> touching-x-offset 40))
  521. (state/set-left-sidebar-open! true)
  522. (and left-sidebar-open? (< touching-x-offset -30))
  523. (reset! *close-signal (inc @*close-signal))))
  524. (reset! *touch-state nil))}
  525. ;; sidebar contents
  526. (sidebar-container route-match close-fn left-sidebar-open? enable-whiteboards? srs-open? *closing?
  527. @*close-signal (and touch-pending? touching-x-offset))
  528. ;; resizer
  529. (sidebar-resizer)]))
  530. (rum/defc recording-bar
  531. []
  532. [:> react-draggable
  533. {:onStart (fn [_event]
  534. (when-let [pos (some-> (state/get-input) cursor/pos)]
  535. (state/set-editor-last-pos! pos)))
  536. :onStop (fn [_event]
  537. (when-let [block (get @(get @state/state :editor/block) :block/uuid)]
  538. (editor-handler/edit-block! block :max)
  539. (when-let [input (state/get-input)]
  540. (when-let [saved-cursor (state/get-editor-last-pos)]
  541. (cursor/move-cursor-to input saved-cursor)))))}
  542. [:div#audio-record-toolbar
  543. {:style {:bottom (+ @util/keyboard-height 45)}}
  544. (footer/audio-record-cp)]])
  545. (rum/defc main <
  546. {:did-mount (fn [state]
  547. (when-let [element (gdom/getElement "main-content-container")]
  548. (dnd/subscribe!
  549. element
  550. :upload-files
  551. {:drop (fn [_e files]
  552. (when-let [id (state/get-edit-input-id)]
  553. (let [format (get (state/get-edit-block) :block/format :markdown)]
  554. (editor-handler/upload-asset! id files format editor-handler/*asset-uploading? true))))})
  555. (common-handler/listen-to-scroll! element)
  556. (when (:margin-less-pages? (first (:rum/args state))) ;; makes sure full screen pages displaying without scrollbar
  557. (set! (.. element -scrollTop) 0)))
  558. state)
  559. :will-unmount (fn [state]
  560. (when-let [el (gdom/getElement "main-content-container")]
  561. (dnd/unsubscribe! el :upload-files))
  562. state)}
  563. [{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content show-action-bar? show-recording-bar?]}]
  564. (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)
  565. onboarding-and-home? (and (or (nil? (state/get-current-repo)) (config/demo-graph?))
  566. (not config/publishing?)
  567. (= :home route-name))
  568. margin-less-pages? (or (and (mobile-util/native-platform?) onboarding-and-home?) margin-less-pages?)]
  569. [:div#main-container.cp__sidebar-main-layout.flex-1.flex
  570. {:class (util/classnames [{:is-left-sidebar-open left-sidebar-open?}])}
  571. ;; desktop left sidebar layout
  572. (left-sidebar {:left-sidebar-open? left-sidebar-open?
  573. :route-match route-match})
  574. [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row.outline-none.relative
  575. {:tabIndex "-1"
  576. :data-is-margin-less-pages margin-less-pages?}
  577. (when show-action-bar?
  578. (action-bar/action-bar))
  579. [:div.cp__sidebar-main-content
  580. {:data-is-margin-less-pages margin-less-pages?
  581. :data-is-full-width (or margin-less-pages?
  582. (contains? #{:all-files :all-pages :my-publishing} route-name))}
  583. (when show-recording-bar?
  584. (recording-bar))
  585. (mobile-bar)
  586. (footer/footer)
  587. (cond
  588. (not indexeddb-support?)
  589. nil
  590. db-restoring?
  591. (if config/publishing?
  592. [:div.space-y-2
  593. (shui/skeleton {:class "h-8 w-1/3 mb-8 bg-gray-400"})
  594. (shui/skeleton {:class "h-6 w-full bg-gray-400"})
  595. (shui/skeleton {:class "h-6 w-full bg-gray-400"})]
  596. [:div.space-y-2
  597. (shui/skeleton {:class "h-8 w-1/3 mb-8"})
  598. (shui/skeleton {:class "h-6 w-full"})
  599. (shui/skeleton {:class "h-6 w-full"})])
  600. :else
  601. [:div
  602. {:class (if (or onboarding-and-home? margin-less-pages?) "" (util/hiccup->class "mx-auto.pb-24"))
  603. :style {:margin-bottom (cond
  604. margin-less-pages? 0
  605. onboarding-and-home? 0
  606. :else 120)}}
  607. main-content])
  608. (comment
  609. (when onboarding-and-home?
  610. (onboarding/intro onboarding-and-home?)))]]]))
  611. (defonce sidebar-inited? (atom false))
  612. ;; TODO: simplify logic
  613. (rum/defc parsing-progress < rum/static
  614. [state]
  615. (let [finished (or (:finished state) 0)
  616. total (:total state)
  617. width (js/Math.round (* (.toFixed (/ finished total) 2) 100))
  618. display-filename (some-> (:current-parsing-file state)
  619. not-empty
  620. path/filename)
  621. left-label [:div.flex.flex-row.font-bold
  622. (t :parsing-files)
  623. [:div.hidden.md:flex.flex-row
  624. [:span.mr-1 ": "]
  625. [:div.text-ellipsis-wrapper {:style {:max-width 300}}
  626. display-filename]]]]
  627. (ui/progress-bar-with-label width left-label (str finished "/" total))))
  628. (rum/defc main-content < rum/reactive db-mixins/query
  629. {:init (fn [state]
  630. (when-not @sidebar-inited?
  631. (let [current-repo (state/sub :git/current-repo)
  632. default-home (get-default-home-if-valid)
  633. sidebar (:sidebar default-home)
  634. sidebar (if (string? sidebar) [sidebar] sidebar)]
  635. (when-let [pages (->> (seq sidebar)
  636. (remove string/blank?))]
  637. (doseq [page pages]
  638. (let [page (util/safe-page-name-sanity-lc page)
  639. [db-id block-type] (if (= page "contents")
  640. [(or (:db/id (db/get-page page)) "contents") :contents]
  641. [(:db/id (db/get-page page)) :page])]
  642. (state/sidebar-add-block! current-repo db-id block-type)))
  643. (reset! sidebar-inited? true))))
  644. state)}
  645. []
  646. (let [default-home (get-default-home-if-valid)
  647. current-repo (state/sub :git/current-repo)
  648. loading-files? (when current-repo (state/sub [:repo/loading-files? current-repo]))
  649. graph-parsing-state (state/sub [:graph/parsing-state current-repo])]
  650. (cond
  651. (or
  652. (:graph-loading? graph-parsing-state)
  653. (not= (:total graph-parsing-state) (:finished graph-parsing-state)))
  654. [:div.flex.items-center.justify-center.full-height-without-header
  655. [:div.flex-1
  656. (parsing-progress graph-parsing-state)]]
  657. :else
  658. [:div
  659. (cond
  660. (and default-home
  661. (= :home (state/get-current-route))
  662. (not (state/route-has-p?))
  663. (:page default-home))
  664. (route-handler/redirect-to-page! (:page default-home))
  665. (or (not (state/enable-journals? current-repo))
  666. (let [latest-journals (db/get-latest-journals (state/get-current-repo) 1)]
  667. (and config/publishing?
  668. (not default-home)
  669. (empty? latest-journals))))
  670. (route-handler/redirect! {:to :all-pages})
  671. loading-files?
  672. (ui/loading (t :loading-files))
  673. :else
  674. (journal/all-journals))])))
  675. (defn- hide-context-menu-and-clear-selection
  676. [e & {:keys [esc?]}]
  677. (state/hide-custom-context-menu!)
  678. (when-not (or (gobj/get e "shiftKey")
  679. (util/meta-key? e)
  680. (state/get-edit-input-id)
  681. (some-> (.-target e) util/input?)
  682. (= (shui-dialog/get-last-modal-id) :property-dialog)
  683. (some-> (.-target e) (.closest ".ls-block"))
  684. (some-> (.-target e) (.closest "[data-keep-selection]")))
  685. (if (and esc? (editor-handler/popup-exists? :selection-action-bar))
  686. (state/pub-event! [:editor/hide-action-bar])
  687. (editor-handler/clear-selection!))))
  688. (rum/defc render-custom-context-menu
  689. [links position]
  690. (let [ref (rum/use-ref nil)]
  691. (hooks/use-effect!
  692. #(let [el (rum/deref ref)
  693. {:keys [x y]} (util/calc-delta-rect-offset el js/document.documentElement)]
  694. (set! (.. el -style -transform)
  695. (str "translate3d(" (if (neg? x) x 0) "px," (if (neg? y) (- y 10) 0) "px" ",0)"))))
  696. [:<>
  697. [:div.menu-backdrop {:on-pointer-down (fn [e] (hide-context-menu-and-clear-selection e))}]
  698. [:div#custom-context-menu
  699. {:ref ref
  700. :style {:z-index 999
  701. :left (str (first position) "px")
  702. :top (str (second position) "px")}} links]]))
  703. (rum/defc custom-context-menu < rum/reactive
  704. []
  705. (let [show? (state/sub :custom-context-menu/show?)
  706. links (state/sub :custom-context-menu/links)
  707. position (state/sub :custom-context-menu/position)]
  708. (when (and show? links position)
  709. (render-custom-context-menu links position))))
  710. (rum/defc new-block-mode < rum/reactive
  711. []
  712. (when (state/sub [:document/mode?])
  713. (ui/tooltip
  714. [:a.block.px-1.text-sm.font-medium.bg-base-2.rounded-md.mx-2
  715. {:on-click state/toggle-document-mode!}
  716. "D"]
  717. [:div.p-2
  718. [:p.mb-2 [:b "Document mode"]]
  719. [:ul
  720. [:li
  721. [:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :editor/new-line))]
  722. [:p.inline-block "to create new block"]]
  723. [:li
  724. [:p.inline-block.mr-1 "Click `D` or type"]
  725. [:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :ui/toggle-document-mode))]
  726. [:p.inline-block "to toggle document mode"]]]])))
  727. (def help-menu-items
  728. [{:title "Handbook" :icon "book-2" :on-click #(handbooks/toggle-handbooks)}
  729. {:title "Keyboard shortcuts" :icon "command" :on-click #(state/sidebar-add-block! (state/get-current-repo) "shortcut-settings" :shortcut-settings)}
  730. {:title "Documentation" :icon "help" :href "https://docs.logseq.com/"}
  731. :hr
  732. {:title "Report bug" :icon "bug" :on-click #(rfe/push-state :bug-report)}
  733. {:title "Request feature" :icon "git-pull-request" :href "https://discuss.logseq.com/c/feedback/feature-requests/"}
  734. {:title "Submit feedback" :icon "messages" :href "https://discuss.logseq.com/c/feedback/13"}
  735. :hr
  736. {:title "Ask the community" :icon "brand-discord" :href "https://discord.com/invite/KpN4eHY"}
  737. {:title "Support forum" :icon "message" :href "https://discuss.logseq.com/"}
  738. :hr
  739. {:title "Release notes" :icon "asterisk" :href "https://docs.logseq.com/#/page/changelog"}])
  740. (rum/defc help-menu-popup
  741. []
  742. (hooks/use-effect!
  743. (fn []
  744. (state/set-state! :ui/handbooks-open? false))
  745. [])
  746. (hooks/use-effect!
  747. (fn []
  748. (let [h #(state/set-state! :ui/help-open? false)]
  749. (.addEventListener js/document.body "click" h)
  750. #(.removeEventListener js/document.body "click" h)))
  751. [])
  752. [:div.cp__sidebar-help-menu-popup
  753. [:div.list-wrap
  754. (for [[idx {:keys [title icon href on-click] :as item}] (medley/indexed help-menu-items)]
  755. (case item
  756. :hr
  757. [:hr.my-2 {:key idx}]
  758. ;; default
  759. [:a.it.flex.items-center.px-4.py-1.select-none
  760. {:key title
  761. :on-click (fn []
  762. (cond
  763. (fn? on-click) (on-click)
  764. (string? href) (util/open-url href))
  765. (state/set-state! :ui/help-open? false))}
  766. [:span.flex.items-center.pr-2.opacity-40 (ui/icon icon {:size 20})]
  767. [:strong.font-normal title]]))]
  768. [:div.ft.pl-11.pb-3
  769. [:span.opacity.text-xs.opacity-30 "Logseq " version]]])
  770. (rum/defc help-button < rum/reactive
  771. []
  772. (let [help-open? (state/sub :ui/help-open?)
  773. handbooks-open? (state/sub :ui/handbooks-open?)]
  774. [:<>
  775. [:div.cp__sidebar-help-btn
  776. [:div.inner
  777. {:title (t :help-shortcut-title)
  778. :on-click #(state/toggle! :ui/help-open?)}
  779. [:svg.scale-125 {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "24", :view-box "0 0 24 24", :xmlns "http://www.w3.org/2000/svg", :stroke-linecap "round", :stroke-width "2", :class "icon icon-tabler icon-tabler-help-small", :height "24"}
  780. [:path {:stroke "none", :d "M0 0h24v24H0z", :fill "none"}]
  781. [:path {:d "M12 16v.01"}]
  782. [:path {:d "M12 13a2 2 0 0 0 .914 -3.782a1.98 1.98 0 0 0 -2.414 .483"}]]]]
  783. (when help-open?
  784. (help-menu-popup))
  785. (when handbooks-open?
  786. (handbooks/handbooks-popup))]))
  787. (rum/defc app-context-menu-observer
  788. < rum/static
  789. (mixins/event-mixin
  790. (fn [state]
  791. ;; fixme: this mixin will register global event listeners on window
  792. ;; which might cause unexpected issues
  793. (mixins/listen state js/window "contextmenu"
  794. (fn [^js e]
  795. (let [target (gobj/get e "target")
  796. block-el (.closest target ".bullet-container[blockid]")
  797. block-id (some-> block-el (.getAttribute "blockid"))
  798. {:keys [block block-ref]} (state/sub :block-ref/context)
  799. {:keys [page page-entity]} (state/sub :page-title/context)]
  800. (let [show!
  801. (fn [content & {:as option}]
  802. (shui/popup-show! e
  803. (fn [{:keys [id]}]
  804. [:div {:on-click #(shui/popup-hide! id)
  805. :data-keep-selection true}
  806. content])
  807. (merge
  808. {:on-before-hide state/dom-clear-selection!
  809. :on-after-hide state/state-clear-selection!
  810. :content-props {:class "w-[280px] ls-context-menu-content"}
  811. :as-dropdown? true}
  812. option)))
  813. handled
  814. (cond
  815. (and page (not block-id))
  816. (do
  817. (show! (cp-content/page-title-custom-context-menu-content page-entity))
  818. (state/set-state! :page-title/context nil))
  819. block-ref
  820. (do
  821. (show! (cp-content/block-ref-custom-context-menu-content block block-ref))
  822. (state/set-state! :block-ref/context nil))
  823. ;; block selection
  824. (and (state/selection?) (not (d/has-class? target "bullet")))
  825. (show! (cp-content/custom-context-menu-content)
  826. {:id :blocks-selection-context-menu})
  827. ;; block bullet
  828. (and block-id (parse-uuid block-id))
  829. (let [block (.closest target ".ls-block")
  830. property-default-value? (when block
  831. (= "true" (d/attr block "data-is-property-default-value")))]
  832. (when block
  833. (state/clear-selection!)
  834. (state/conj-selection-block! block :down))
  835. (p/do!
  836. (db-async/<get-block (state/get-current-repo) (uuid block-id) {:children? false})
  837. (show! (cp-content/block-context-menu-content target (uuid block-id) property-default-value?))))
  838. :else
  839. false)]
  840. (when (not (false? handled))
  841. (util/stop e))))))))
  842. []
  843. nil)
  844. (defn- on-mouse-up
  845. [e]
  846. (when-not (or (.closest (.-target e) ".block-control-wrap")
  847. (.closest (.-target e) "button")
  848. (.closest (.-target e) "input")
  849. (.closest (.-target e) "textarea")
  850. (.closest (.-target e) "a"))
  851. (editor-handler/show-action-bar!)))
  852. (rum/defcs ^:large-vars/cleanup-todo root-container < rum/reactive
  853. (mixins/event-mixin
  854. (fn [state]
  855. (mixins/listen state js/window "pointerdown" hide-context-menu-and-clear-selection)
  856. (mixins/listen state js/window "keydown"
  857. (fn [e]
  858. (cond
  859. (= 27 (.-keyCode e))
  860. (if (and (state/modal-opened?)
  861. (not
  862. (and
  863. ;; FIXME: this does not work on CI tests
  864. util/node-test?
  865. (state/editing?))))
  866. (state/close-modal!)
  867. (hide-context-menu-and-clear-selection e {:esc? true})))
  868. (state/set-ui-last-key-code! (.-key e))))
  869. (mixins/listen state js/window "keyup"
  870. (fn [_e]
  871. (state/set-state! :editor/latest-shortcut nil)))))
  872. [state route-match main-content']
  873. (let [current-repo (state/sub :git/current-repo)
  874. theme (state/sub :ui/theme)
  875. accent-color (some-> (state/sub :ui/radix-color) (name))
  876. editor-font (some-> (state/sub :ui/editor-font) (name))
  877. system-theme? (state/sub :ui/system-theme?)
  878. light? (= "light" (state/sub :ui/theme))
  879. sidebar-open? (state/sub :ui/sidebar-open?)
  880. settings-open? (state/sub :ui/settings-open?)
  881. left-sidebar-open? (state/sub :ui/left-sidebar-open?)
  882. wide-mode? (state/sub :ui/wide-mode?)
  883. ls-block-hl-colored? (state/sub :pdf/block-highlight-colored?)
  884. onboarding-state (state/sub :file-sync/onboarding-state)
  885. right-sidebar-blocks (state/sub-right-sidebar-blocks)
  886. route-name (get-in route-match [:data :name])
  887. margin-less-pages? (or (boolean (#{:graph} route-name))
  888. (db-model/whiteboard-page? (state/get-current-page)))
  889. db-restoring? (state/sub :db/restoring?)
  890. indexeddb-support? (state/sub :indexeddb/support?)
  891. page? (= :page route-name)
  892. home? (= :home route-name)
  893. native-titlebar? (state/sub [:electron/user-cfgs :window/native-titlebar?])
  894. window-controls? (and (util/electron?) (not util/mac?) (not native-titlebar?))
  895. edit? (state/editing?)
  896. default-home (get-default-home-if-valid)
  897. logged? (user-handler/logged-in?)
  898. fold-button-on-right? (state/enable-fold-button-right?)
  899. show-action-bar? (state/sub :mobile/show-action-bar?)
  900. show-recording-bar? (state/sub :mobile/show-recording-bar?)
  901. preferred-language (state/sub [:preferred-language])]
  902. (theme/container
  903. {:t t
  904. :theme theme
  905. :accent-color accent-color
  906. :editor-font editor-font
  907. :route route-match
  908. :current-repo current-repo
  909. :edit? edit?
  910. :db-restoring? db-restoring?
  911. :sidebar-open? sidebar-open?
  912. :settings-open? settings-open?
  913. :sidebar-blocks-len (count right-sidebar-blocks)
  914. :system-theme? system-theme?
  915. :onboarding-state onboarding-state
  916. :preferred-language preferred-language
  917. :on-click (fn [e]
  918. (editor-handler/unhighlight-blocks!)
  919. (util/fix-open-external-with-shift! e))}
  920. [:main.theme-container-inner#app-container-wrapper
  921. {:class (util/classnames
  922. [{:ls-left-sidebar-open left-sidebar-open?
  923. :ls-right-sidebar-open sidebar-open?
  924. :ls-wide-mode wide-mode?
  925. :ls-window-controls window-controls?
  926. :ls-fold-button-on-right fold-button-on-right?
  927. :ls-hl-colored ls-block-hl-colored?}])
  928. :on-pointer-up (fn []
  929. (when-let [container (gdom/getElement "app-container-wrapper")]
  930. (d/remove-class! container "blocks-selection-mode")
  931. (when (> (count (state/get-selection-blocks)) 1)
  932. (util/clear-selection!))))}
  933. [:button#skip-to-main
  934. {:on-click #(ui/focus-element (ui/main-node))
  935. :on-key-up (fn [e]
  936. (when (= "Enter" (.-key e))
  937. (ui/focus-element (ui/main-node))))}
  938. (t :accessibility/skip-to-main-content)]
  939. [:div.#app-container
  940. {:on-mouse-up on-mouse-up}
  941. [:div#left-container
  942. {:class (if (state/sub :ui/sidebar-open?) "overflow-hidden" "w-full")}
  943. (header/header {:light? light?
  944. :current-repo current-repo
  945. :logged? logged?
  946. :page? page?
  947. :route-match route-match
  948. :default-home default-home
  949. :new-block-mode new-block-mode})
  950. (when (util/electron?)
  951. (find-in-page/search))
  952. (main {:route-match route-match
  953. :margin-less-pages? margin-less-pages?
  954. :logged? logged?
  955. :home? home?
  956. :route-name route-name
  957. :indexeddb-support? indexeddb-support?
  958. :light? light?
  959. :db-restoring? db-restoring?
  960. :main-content main-content'
  961. :show-action-bar? show-action-bar?
  962. :show-recording-bar? show-recording-bar?})]
  963. (when window-controls?
  964. (window-controls/container))
  965. (right-sidebar/sidebar)
  966. [:div#app-single-container]]
  967. (ui/notification)
  968. (shui-toaster/install-toaster)
  969. (shui-dialog/install-modals)
  970. (shui-popup/install-popups)
  971. (custom-context-menu)
  972. (plugins/custom-js-installer
  973. {:t t
  974. :current-repo current-repo
  975. :db-restoring? db-restoring?})
  976. (app-context-menu-observer)
  977. [:a#download.hidden]
  978. [:a#download-as-edn-v2.hidden]
  979. [:a#download-as-json-v2.hidden]
  980. [:a#download-as-transit-debug.hidden]
  981. [:a#download-as-sqlite-db.hidden]
  982. [:a#download-as-db-edn.hidden]
  983. [:a#download-as-roam-json.hidden]
  984. [:a#download-as-html.hidden]
  985. [:a#download-as-zip.hidden]
  986. [:a#export-as-markdown.hidden]
  987. [:a#export-as-opml.hidden]
  988. [:a#convert-markdown-to-unordered-list-or-heading.hidden]
  989. (when (and (not config/mobile?)
  990. (not config/publishing?))
  991. (help-button))])))