container.cljs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  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. [frontend.components.content :as cp-content]
  6. [frontend.components.find-in-page :as find-in-page]
  7. [frontend.components.handbooks :as handbooks]
  8. [frontend.components.header :as header]
  9. [frontend.components.journal :as journal]
  10. [frontend.components.left-sidebar :as app-left-sidebar]
  11. [frontend.components.plugins :as plugins]
  12. [frontend.components.right-sidebar :as right-sidebar]
  13. [frontend.components.theme :as theme]
  14. [frontend.components.window-controls :as window-controls]
  15. [frontend.config :as config]
  16. [frontend.context.i18n :refer [t]]
  17. [frontend.db :as db]
  18. [frontend.db-mixins :as db-mixins]
  19. [frontend.db.async :as db-async]
  20. [frontend.db.model :as db-model]
  21. [frontend.handler.common :as common-handler]
  22. [frontend.handler.editor :as editor-handler]
  23. [frontend.handler.route :as route-handler]
  24. [frontend.handler.user :as user-handler]
  25. [frontend.mixins :as mixins]
  26. [frontend.mobile.footer :as footer]
  27. [frontend.mobile.util :as mobile-util]
  28. [frontend.modules.shortcut.data-helper :as shortcut-dh]
  29. [frontend.state :as state]
  30. [frontend.ui :as ui]
  31. [frontend.util :as util]
  32. [frontend.version :refer [version]]
  33. [goog.dom :as gdom]
  34. [goog.object :as gobj]
  35. [logseq.common.path :as path]
  36. [logseq.shui.dialog.core :as shui-dialog]
  37. [logseq.shui.hooks :as hooks]
  38. [logseq.shui.popup.core :as shui-popup]
  39. [logseq.shui.toaster.core :as shui-toaster]
  40. [logseq.shui.ui :as shui]
  41. [medley.core :as medley]
  42. [promesa.core :as p]
  43. [react-draggable]
  44. [reitit.frontend.easy :as rfe]
  45. [rum.core :as rum]))
  46. (rum/defc main <
  47. {:did-mount (fn [state]
  48. (when-let [element (gdom/getElement "main-content-container")]
  49. (dnd/subscribe!
  50. element
  51. :upload-files
  52. {:drop (fn [_e files]
  53. (when-let [id (state/get-edit-input-id)]
  54. (let [format (get (state/get-edit-block) :block/format :markdown)]
  55. (editor-handler/upload-asset! id files format editor-handler/*asset-uploading? true))))})
  56. (common-handler/listen-to-scroll! element)
  57. (when (:margin-less-pages? (first (:rum/args state))) ;; makes sure full screen pages displaying without scrollbar
  58. (set! (.. element -scrollTop) 0)))
  59. state)
  60. :will-unmount (fn [state]
  61. (when-let [el (gdom/getElement "main-content-container")]
  62. (dnd/unsubscribe! el :upload-files))
  63. state)}
  64. [{:keys [route-match margin-less-pages? route-name db-restoring? main-content]}]
  65. (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)
  66. onboarding-and-home? (and (or (nil? (state/get-current-repo)) (config/demo-graph?))
  67. (not config/publishing?)
  68. (= :home route-name))
  69. margin-less-pages? (or (and (mobile-util/native-platform?) onboarding-and-home?) margin-less-pages?)]
  70. [:div#main-container.cp__sidebar-main-layout.flex-1.flex
  71. {:class (util/classnames [{:is-left-sidebar-open left-sidebar-open?}])}
  72. ;; desktop left sidebar layout
  73. (app-left-sidebar/left-sidebar
  74. {:left-sidebar-open? left-sidebar-open?
  75. :route-match route-match})
  76. [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row.outline-none.relative
  77. {:tabIndex "-1"
  78. :data-is-margin-less-pages margin-less-pages?}
  79. [:div.cp__sidebar-main-content
  80. {:data-is-margin-less-pages margin-less-pages?
  81. :data-is-full-width (or margin-less-pages?
  82. (contains? #{:all-files :all-pages :my-publishing} route-name))}
  83. (footer/footer)
  84. (cond
  85. db-restoring?
  86. (if config/publishing?
  87. [:div.space-y-2
  88. (shui/skeleton {:class "h-8 w-1/3 mb-8 bg-gray-400"})
  89. (shui/skeleton {:class "h-6 w-full bg-gray-400"})
  90. (shui/skeleton {:class "h-6 w-full bg-gray-400"})]
  91. [:div.space-y-2
  92. (shui/skeleton {:class "h-8 w-1/3 mb-8"})
  93. (shui/skeleton {:class "h-6 w-full"})
  94. (shui/skeleton {:class "h-6 w-full"})])
  95. :else
  96. [:div
  97. {:class (if (or onboarding-and-home? margin-less-pages?) "" (util/hiccup->class "mx-auto.pb-24"))
  98. :style {:margin-bottom (cond
  99. margin-less-pages? 0
  100. onboarding-and-home? 0
  101. :else 120)}}
  102. main-content])
  103. (comment
  104. (when onboarding-and-home?
  105. (onboarding/intro onboarding-and-home?)))]]]))
  106. (defonce sidebar-inited? (atom false))
  107. ;; TODO: simplify logic
  108. (rum/defc parsing-progress < rum/static
  109. [state]
  110. (let [finished (or (:finished state) 0)
  111. total (:total state)
  112. width (js/Math.round (* (.toFixed (/ finished total) 2) 100))
  113. display-filename (some-> (:current-parsing-file state)
  114. not-empty
  115. path/filename)
  116. left-label [:div.flex.flex-row.font-bold
  117. (t :parsing-files)
  118. [:div.hidden.md:flex.flex-row
  119. [:span.mr-1 ": "]
  120. [:div.text-ellipsis-wrapper {:style {:max-width 300}}
  121. display-filename]]]]
  122. (ui/progress-bar-with-label width left-label (str finished "/" total))))
  123. (rum/defc main-content < rum/reactive db-mixins/query
  124. {:init (fn [state]
  125. (when-not @sidebar-inited?
  126. (let [current-repo (state/sub :git/current-repo)
  127. default-home (app-left-sidebar/get-default-home-if-valid)
  128. sidebar (:sidebar default-home)
  129. sidebar (if (string? sidebar) [sidebar] sidebar)]
  130. (when-let [pages (->> (seq sidebar)
  131. (remove string/blank?))]
  132. (doseq [page pages]
  133. (let [page (util/safe-page-name-sanity-lc page)
  134. [db-id block-type] (if (= page "contents")
  135. [(or (:db/id (db/get-page page)) "contents") :contents]
  136. [(:db/id (db/get-page page)) :page])]
  137. (state/sidebar-add-block! current-repo db-id block-type)))
  138. (reset! sidebar-inited? true))))
  139. state)}
  140. []
  141. (let [default-home (app-left-sidebar/get-default-home-if-valid)
  142. current-repo (state/sub :git/current-repo)
  143. loading-files? (when current-repo (state/sub [:repo/loading-files? current-repo]))
  144. graph-parsing-state (state/sub [:graph/parsing-state current-repo])]
  145. (cond
  146. (or
  147. (:graph-loading? graph-parsing-state)
  148. (not= (:total graph-parsing-state) (:finished graph-parsing-state)))
  149. [:div.flex.items-center.justify-center.full-height-without-header
  150. [:div.flex-1
  151. (parsing-progress graph-parsing-state)]]
  152. :else
  153. [:div
  154. (cond
  155. (and default-home
  156. (= :home (state/get-current-route))
  157. (not (state/route-has-p?))
  158. (:page default-home))
  159. (route-handler/redirect-to-page! (:page default-home))
  160. (or (not (state/enable-journals? current-repo))
  161. (let [latest-journals (db/get-latest-journals (state/get-current-repo) 1)]
  162. (and config/publishing?
  163. (not default-home)
  164. (empty? latest-journals))))
  165. (route-handler/redirect! {:to :all-pages})
  166. loading-files?
  167. (ui/loading (t :loading-files))
  168. :else
  169. (journal/all-journals))])))
  170. (defn- hide-context-menu-and-clear-selection
  171. [e & {:keys [esc?]}]
  172. (state/hide-custom-context-menu!)
  173. (when-not (or (gobj/get e "shiftKey")
  174. (util/meta-key? e)
  175. (state/get-edit-input-id)
  176. (some-> (.-target e) util/input?)
  177. (= (shui-dialog/get-last-modal-id) :property-dialog)
  178. (some-> (.-target e) (.closest ".ls-block"))
  179. (some-> (.-target e) (.closest "[data-keep-selection]")))
  180. (if (and esc? (editor-handler/popup-exists? :selection-action-bar))
  181. (state/pub-event! [:editor/hide-action-bar])
  182. (editor-handler/clear-selection!))))
  183. (rum/defc render-custom-context-menu
  184. [links position]
  185. (let [ref (rum/use-ref nil)]
  186. (hooks/use-effect!
  187. #(let [el (rum/deref ref)
  188. {:keys [x y]} (util/calc-delta-rect-offset el js/document.documentElement)]
  189. (set! (.. el -style -transform)
  190. (str "translate3d(" (if (neg? x) x 0) "px," (if (neg? y) (- y 10) 0) "px" ",0)"))))
  191. [:<>
  192. [:div.menu-backdrop {:on-pointer-down (fn [e] (hide-context-menu-and-clear-selection e))}]
  193. [:div#custom-context-menu
  194. {:ref ref
  195. :style {:z-index 999
  196. :left (str (first position) "px")
  197. :top (str (second position) "px")}} links]]))
  198. (rum/defc custom-context-menu < rum/reactive
  199. []
  200. (let [show? (state/sub :custom-context-menu/show?)
  201. links (state/sub :custom-context-menu/links)
  202. position (state/sub :custom-context-menu/position)]
  203. (when (and show? links position)
  204. (render-custom-context-menu links position))))
  205. (rum/defc new-block-mode < rum/reactive
  206. []
  207. (when (state/sub [:document/mode?])
  208. (ui/tooltip
  209. [:a.block.px-1.text-sm.font-medium.bg-base-2.rounded-md.mx-2
  210. {:on-click state/toggle-document-mode!}
  211. "D"]
  212. [:div.p-2
  213. [:p.mb-2 [:b "Document mode"]]
  214. [:ul
  215. [:li
  216. [:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :editor/new-line))]
  217. [:p.inline-block "to create new block"]]
  218. [:li
  219. [:p.inline-block.mr-1 "Click `D` or type"]
  220. [:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :ui/toggle-document-mode))]
  221. [:p.inline-block "to toggle document mode"]]]])))
  222. (def help-menu-items
  223. [{:title "Handbook" :icon "book-2" :on-click #(handbooks/toggle-handbooks)}
  224. {:title "Keyboard shortcuts" :icon "command" :on-click #(state/sidebar-add-block! (state/get-current-repo) "shortcut-settings" :shortcut-settings)}
  225. {:title "Documentation" :icon "help" :href "https://docs.logseq.com/"}
  226. :hr
  227. {:title "Report bug" :icon "bug" :on-click #(rfe/push-state :bug-report)}
  228. {:title "Request feature" :icon "git-pull-request" :href "https://discuss.logseq.com/c/feedback/feature-requests/"}
  229. {:title "Submit feedback" :icon "messages" :href "https://discuss.logseq.com/c/feedback/13"}
  230. :hr
  231. {:title "Ask the community" :icon "brand-discord" :href "https://discord.com/invite/KpN4eHY"}
  232. {:title "Support forum" :icon "message" :href "https://discuss.logseq.com/"}
  233. :hr
  234. {:title "Release notes" :icon "asterisk" :href "https://docs.logseq.com/#/page/changelog"}])
  235. (rum/defc help-menu-popup
  236. []
  237. (hooks/use-effect!
  238. (fn []
  239. (state/set-state! :ui/handbooks-open? false))
  240. [])
  241. (hooks/use-effect!
  242. (fn []
  243. (let [h #(state/set-state! :ui/help-open? false)]
  244. (.addEventListener js/document.body "click" h)
  245. #(.removeEventListener js/document.body "click" h)))
  246. [])
  247. [:div.cp__sidebar-help-menu-popup
  248. [:div.list-wrap
  249. (for [[idx {:keys [title icon href on-click] :as item}] (medley/indexed help-menu-items)]
  250. (case item
  251. :hr
  252. [:hr.my-2 {:key idx}]
  253. ;; default
  254. [:a.it.flex.items-center.px-4.py-1.select-none
  255. {:key title
  256. :on-click (fn []
  257. (cond
  258. (fn? on-click) (on-click)
  259. (string? href) (util/open-url href))
  260. (state/set-state! :ui/help-open? false))}
  261. [:span.flex.items-center.pr-2.opacity-40 (ui/icon icon {:size 20})]
  262. [:strong.font-normal title]]))]
  263. [:div.ft.pl-11.pb-3
  264. [:span.opacity.text-xs.opacity-30 "Logseq " version]]])
  265. (rum/defc help-button < rum/reactive
  266. []
  267. (let [help-open? (state/sub :ui/help-open?)
  268. handbooks-open? (state/sub :ui/handbooks-open?)]
  269. [:<>
  270. [:div.cp__sidebar-help-btn
  271. [:div.inner
  272. {:title (t :help-shortcut-title)
  273. :on-click #(state/toggle! :ui/help-open?)}
  274. [: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"}
  275. [:path {:stroke "none", :d "M0 0h24v24H0z", :fill "none"}]
  276. [:path {:d "M12 16v.01"}]
  277. [:path {:d "M12 13a2 2 0 0 0 .914 -3.782a1.98 1.98 0 0 0 -2.414 .483"}]]]]
  278. (when help-open?
  279. (help-menu-popup))
  280. (when handbooks-open?
  281. (handbooks/handbooks-popup))]))
  282. (rum/defc app-context-menu-observer
  283. < rum/static
  284. (mixins/event-mixin
  285. (fn [state]
  286. ;; fixme: this mixin will register global event listeners on window
  287. ;; which might cause unexpected issues
  288. (mixins/listen state js/window "contextmenu"
  289. (fn [^js e]
  290. (let [target (gobj/get e "target")
  291. block-el (.closest target ".bullet-container[blockid]")
  292. block-id (some-> block-el (.getAttribute "blockid"))
  293. {:keys [block block-ref]} (state/sub :block-ref/context)
  294. {:keys [page page-entity]} (state/sub :page-title/context)]
  295. (let [show!
  296. (fn [content & {:as option}]
  297. (shui/popup-show! e
  298. (fn [{:keys [id]}]
  299. [:div {:on-click #(shui/popup-hide! id)
  300. :data-keep-selection true}
  301. content])
  302. (merge
  303. {:on-before-hide state/dom-clear-selection!
  304. :on-after-hide state/state-clear-selection!
  305. :content-props {:class "w-[280px] ls-context-menu-content"}
  306. :as-dropdown? true}
  307. option)))
  308. handled
  309. (cond
  310. (and page (not block-id))
  311. (do
  312. (show! (cp-content/page-title-custom-context-menu-content page-entity))
  313. (state/set-state! :page-title/context nil))
  314. block-ref
  315. (do
  316. (show! (cp-content/block-ref-custom-context-menu-content block block-ref))
  317. (state/set-state! :block-ref/context nil))
  318. ;; block selection
  319. (and (state/selection?) (not (d/has-class? target "bullet")))
  320. (show! (cp-content/custom-context-menu-content)
  321. {:id :blocks-selection-context-menu})
  322. ;; block bullet
  323. (and block-id (parse-uuid block-id))
  324. (let [block (.closest target ".ls-block")
  325. property-default-value? (when block
  326. (= "true" (d/attr block "data-is-property-default-value")))]
  327. (when block
  328. (state/clear-selection!)
  329. (state/conj-selection-block! block :down))
  330. (p/do!
  331. (db-async/<get-block (state/get-current-repo) (uuid block-id) {:children? false})
  332. (show! (cp-content/block-context-menu-content target (uuid block-id) property-default-value?))))
  333. :else
  334. false)]
  335. (when (not (false? handled))
  336. (util/stop e))))))))
  337. []
  338. nil)
  339. (defn- on-mouse-up
  340. [e]
  341. (when-not (or (.closest (.-target e) ".block-control-wrap")
  342. (.closest (.-target e) "button")
  343. (.closest (.-target e) "input")
  344. (.closest (.-target e) "textarea")
  345. (.closest (.-target e) "a"))
  346. (editor-handler/show-action-bar!)))
  347. (rum/defcs ^:large-vars/cleanup-todo root-container < rum/reactive
  348. (mixins/event-mixin
  349. (fn [state]
  350. (mixins/listen state js/window "pointerdown" hide-context-menu-and-clear-selection)
  351. (mixins/listen state js/window "keydown"
  352. (fn [e]
  353. (cond
  354. (= 27 (.-keyCode e))
  355. (if (and (state/modal-opened?)
  356. (not
  357. (and
  358. ;; FIXME: this does not work on CI tests
  359. util/node-test?
  360. (state/editing?))))
  361. (state/close-modal!)
  362. (hide-context-menu-and-clear-selection e {:esc? true})))
  363. (state/set-ui-last-key-code! (.-key e))))
  364. (mixins/listen state js/window "keyup"
  365. (fn [_e]
  366. (state/set-state! :editor/latest-shortcut nil)))))
  367. [state route-match main-content']
  368. (let [current-repo (state/sub :git/current-repo)
  369. theme (state/sub :ui/theme)
  370. accent-color (some-> (state/sub :ui/radix-color) (name))
  371. editor-font (state/sub :ui/editor-font)
  372. system-theme? (state/sub :ui/system-theme?)
  373. light? (= "light" (state/sub :ui/theme))
  374. sidebar-open? (state/sub :ui/sidebar-open?)
  375. settings-open? (state/sub :ui/settings-open?)
  376. left-sidebar-open? (state/sub :ui/left-sidebar-open?)
  377. wide-mode? (state/sub :ui/wide-mode?)
  378. ls-block-hl-colored? (state/sub :pdf/block-highlight-colored?)
  379. onboarding-state (state/sub :file-sync/onboarding-state)
  380. right-sidebar-blocks (state/sub-right-sidebar-blocks)
  381. route-name (get-in route-match [:data :name])
  382. margin-less-pages? (or (boolean (#{:graph} route-name))
  383. (db-model/whiteboard-page? (state/get-current-page)))
  384. db-restoring? (state/sub :db/restoring?)
  385. page? (= :page route-name)
  386. home? (= :home route-name)
  387. native-titlebar? (state/sub [:electron/user-cfgs :window/native-titlebar?])
  388. window-controls? (and (util/electron?) (not util/mac?) (not native-titlebar?))
  389. edit? (state/editing?)
  390. default-home (app-left-sidebar/get-default-home-if-valid)
  391. logged? (user-handler/logged-in?)
  392. fold-button-on-right? (state/enable-fold-button-right?)
  393. show-action-bar? (state/sub :mobile/show-action-bar?)
  394. preferred-language (state/sub [:preferred-language])]
  395. (theme/container
  396. {:t t
  397. :theme theme
  398. :accent-color accent-color
  399. :editor-font editor-font
  400. :route route-match
  401. :current-repo current-repo
  402. :edit? edit?
  403. :db-restoring? db-restoring?
  404. :sidebar-open? sidebar-open?
  405. :settings-open? settings-open?
  406. :sidebar-blocks-len (count right-sidebar-blocks)
  407. :system-theme? system-theme?
  408. :onboarding-state onboarding-state
  409. :preferred-language preferred-language
  410. :on-click (fn [e]
  411. (editor-handler/unhighlight-blocks!)
  412. (util/fix-open-external-with-shift! e))}
  413. [:main.theme-container-inner#app-container-wrapper
  414. {:class (util/classnames
  415. [{:ls-left-sidebar-open left-sidebar-open?
  416. :ls-right-sidebar-open sidebar-open?
  417. :ls-wide-mode wide-mode?
  418. :ls-window-controls window-controls?
  419. :ls-fold-button-on-right fold-button-on-right?
  420. :ls-hl-colored ls-block-hl-colored?}])
  421. :on-pointer-up (fn []
  422. (when-let [container (gdom/getElement "app-container-wrapper")]
  423. (d/remove-class! container "blocks-selection-mode")
  424. (when (and (> (count (state/get-selection-blocks)) 1)
  425. (not (util/input? js/document.activeElement)))
  426. (util/clear-selection!))))}
  427. [:button#skip-to-main
  428. {:on-click #(ui/focus-element (ui/main-node))
  429. :on-key-up (fn [e]
  430. (when (= "Enter" (.-key e))
  431. (ui/focus-element (ui/main-node))))}
  432. (t :accessibility/skip-to-main-content)]
  433. [:div.#app-container
  434. {:on-mouse-up on-mouse-up}
  435. [:div#left-container
  436. {:class (if (state/sub :ui/sidebar-open?) "overflow-hidden" "w-full")}
  437. (header/header {:light? light?
  438. :current-repo current-repo
  439. :logged? logged?
  440. :page? page?
  441. :route-match route-match
  442. :default-home default-home
  443. :new-block-mode new-block-mode})
  444. (when (util/electron?)
  445. (find-in-page/search))
  446. (if (state/sub :rtc/uploading?)
  447. [:div.flex.items-center.justify-center.full-height-without-header
  448. (ui/loading "Creating remote graph...")]
  449. (main {:route-match route-match
  450. :margin-less-pages? margin-less-pages?
  451. :logged? logged?
  452. :home? home?
  453. :route-name route-name
  454. :light? light?
  455. :db-restoring? db-restoring?
  456. :main-content main-content'
  457. :show-action-bar? show-action-bar?}))]
  458. (when window-controls?
  459. (window-controls/container))
  460. (right-sidebar/sidebar)
  461. [:div#app-single-container]]
  462. (ui/notification)
  463. (shui-toaster/install-toaster)
  464. (shui-dialog/install-modals)
  465. (shui-popup/install-popups)
  466. (custom-context-menu)
  467. (plugins/custom-js-installer
  468. {:t t
  469. :current-repo current-repo
  470. :db-restoring? db-restoring?})
  471. (app-context-menu-observer)
  472. [:a#download.hidden]
  473. [:a#download-as-edn-v2.hidden]
  474. [:a#download-as-json-v2.hidden]
  475. [:a#download-as-transit-debug.hidden]
  476. [:a#download-as-sqlite-db.hidden]
  477. [:a#download-as-db-edn.hidden]
  478. [:a#download-as-roam-json.hidden]
  479. [:a#download-as-html.hidden]
  480. [:a#download-as-zip.hidden]
  481. [:a#export-as-markdown.hidden]
  482. [:a#export-as-opml.hidden]
  483. [:a#convert-markdown-to-unordered-list-or-heading.hidden]
  484. (when (and (not config/mobile?)
  485. (not config/publishing?))
  486. (help-button))])))