page.cljs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. (ns frontend.components.page
  2. (:require [rum.core :as rum]
  3. [frontend.util :as util :refer-macros [profile]]
  4. [frontend.handler.file :as file]
  5. [frontend.handler.page :as page-handler]
  6. [frontend.handler.ui :as ui-handler]
  7. [frontend.handler.common :as common-handler]
  8. [frontend.handler.route :as route-handler]
  9. [frontend.handler.graph :as graph-handler]
  10. [frontend.handler.notification :as notification]
  11. [frontend.handler.editor :as editor-handler]
  12. [frontend.state :as state]
  13. [clojure.string :as string]
  14. [frontend.db :as db]
  15. [dommy.core :as d]
  16. [frontend.components.block :as block]
  17. [frontend.components.editor :as editor]
  18. [frontend.components.reference :as reference]
  19. [frontend.components.svg :as svg]
  20. [frontend.extensions.graph-2d :as graph-2d]
  21. [frontend.ui :as ui]
  22. [frontend.components.content :as content]
  23. [frontend.components.project :as project]
  24. [frontend.config :as config]
  25. [frontend.db :as db]
  26. [frontend.mixins :as mixins]
  27. [frontend.db-mixins :as db-mixins]
  28. [goog.dom :as gdom]
  29. [goog.object :as gobj]
  30. [frontend.utf8 :as utf8]
  31. [frontend.date :as date]
  32. [frontend.graph :as graph]
  33. [frontend.format.mldoc :as mldoc]
  34. [cljs-time.coerce :as tc]
  35. [cljs-time.core :as t]
  36. [cljs.pprint :as pprint]
  37. [frontend.context.i18n :as i18n]
  38. [reitit.frontend.easy :as rfe]
  39. [frontend.handler.block :as block-handler]))
  40. (defn- get-page-name
  41. [state]
  42. (let [route-match (first (:rum/args state))]
  43. (get-in route-match [:parameters :path :name])))
  44. (defn- get-blocks
  45. [repo page-name page-original-name block? block-id]
  46. (when page-name
  47. (if block?
  48. (db/get-block-and-children repo block-id)
  49. (do
  50. (page-handler/add-page-to-recent! repo page-original-name)
  51. (db/get-page-blocks repo page-name)))))
  52. (rum/defc page-blocks-cp < rum/reactive
  53. db-mixins/query
  54. [repo page file-path page-name page-original-name encoded-page-name sidebar? journal? block? block-id format]
  55. (let [raw-page-blocks (get-blocks repo page-name page-original-name block? block-id)
  56. page-blocks (block-handler/with-dummy-block raw-page-blocks format
  57. (if (empty? raw-page-blocks)
  58. (let [content (db/get-file repo file-path)]
  59. {:block/page {:db/id (:db/id page)}
  60. :block/file {:db/id (:db/id (:page/file page))}
  61. :block/meta
  62. (let [file-id (:db/id (:page/file page))]
  63. {:start-pos (utf8/length (utf8/encode content))
  64. :end-pos nil})}))
  65. {:journal? journal?
  66. :page-name page-name})
  67. start-level (or (:block/level (first page-blocks)) 1)
  68. hiccup-config {:id (if block? (str block-id) page-name)
  69. :start-level start-level
  70. :sidebar? sidebar?
  71. :block? block?
  72. :editor-box editor/box}
  73. hiccup-config (common-handler/config-with-document-mode hiccup-config)
  74. hiccup (block/->hiccup page-blocks hiccup-config {})]
  75. (rum/with-key
  76. (content/content page-name
  77. {:hiccup hiccup
  78. :sidebar? sidebar?})
  79. (str encoded-page-name "-hiccup"))))
  80. (defn contents-page
  81. [{:page/keys [name original-name file] :as contents}]
  82. (when-let [repo (state/get-current-repo)]
  83. (let [format (db/get-page-format name)
  84. file-path (:file/path file)]
  85. (page-blocks-cp repo contents file-path name original-name name true false false nil format))))
  86. (defn presentation
  87. [repo page]
  88. [:a.opacity-50.hover:opacity-100
  89. {:title "Presentation mode (Powered by Reveal.js)"
  90. :on-click (fn []
  91. (state/sidebar-add-block!
  92. repo
  93. (:db/id page)
  94. :page-presentation
  95. {:page page}))}
  96. svg/slideshow])
  97. (rum/defc today-queries < rum/reactive
  98. [repo today? sidebar?]
  99. (when (and today? (not sidebar?))
  100. (let [queries (state/sub [:config repo :default-queries :journals])]
  101. (when (seq queries)
  102. [:div#today-queries.mt-10
  103. (for [{:keys [title] :as query} queries]
  104. (rum/with-key
  105. (block/custom-query {:start-level 2
  106. :attr {:class "mt-10"}
  107. :editor-box editor/box} query)
  108. (str repo "-custom-query-" (:query query))))]))))
  109. (defn- delete-page!
  110. [page-name]
  111. (page-handler/delete! page-name
  112. (fn []
  113. (notification/show! (str "Page " page-name " was deleted successfully!")
  114. :success)))
  115. (state/close-modal!)
  116. (route-handler/redirect-to-home!))
  117. (defn delete-page-dialog
  118. [page-name]
  119. (fn [close-fn]
  120. [:div
  121. [:div.sm:flex.sm:items-start
  122. [:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-red-100.sm:mx-0.sm:h-10.sm:w-10
  123. [:svg.h-6.w-6.text-red-600
  124. {:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
  125. [:path
  126. {:d
  127. "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
  128. :stroke-width "2"
  129. :stroke-linejoin "round"
  130. :stroke-linecap "round"}]]]
  131. [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
  132. [:h3#modal-headline.text-lg.leading-6.font-medium.text-gray-900
  133. "Are you sure you want to delete this page?"]]]
  134. [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
  135. [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
  136. [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
  137. {:type "button"
  138. :on-click (fn []
  139. (delete-page! page-name))}
  140. "Yes"]]
  141. [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
  142. [:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
  143. {:type "button"
  144. :on-click close-fn}
  145. "Cancel"]]]]))
  146. (rum/defcs rename-page-dialog-inner <
  147. (rum/local "" ::input)
  148. [state page-name close-fn]
  149. (let [input (get state ::input)]
  150. [:div
  151. [:div.sm:flex.sm:items-start
  152. [:div.mt-3.text-center.sm:mt-0.sm:text-left
  153. [:h3#modal-headline.text-lg.leading-6.font-medium.text-gray-900
  154. (str "Rename \"" page-name "\" to:")]]]
  155. [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
  156. {:auto-focus true
  157. :style {:color "#000"}
  158. :on-change (fn [e]
  159. (reset! input (util/evalue e)))}]
  160. [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
  161. [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
  162. [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
  163. {:type "button"
  164. :on-click (fn []
  165. (let [value @input]
  166. (let [value (string/trim value)]
  167. (when-not (string/blank? value)
  168. (page-handler/rename! page-name value)
  169. (state/close-modal!)))))}
  170. "Submit"]]
  171. [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
  172. [:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
  173. {:type "button"
  174. :on-click close-fn}
  175. "Cancel"]]]]))
  176. (defn rename-page-dialog
  177. [page-name]
  178. (fn [close-fn]
  179. (rename-page-dialog-inner page-name close-fn)))
  180. (defn tagged-pages
  181. [repo tag]
  182. (let [pages (db/get-tag-pages repo tag)]
  183. (when (seq pages)
  184. [:div.references.mt-6.flex-1.flex-row
  185. [:div.content
  186. (ui/foldable
  187. [:h2.font-bold.opacity-50 (util/format "Pages tagged with \"%s\"" tag)]
  188. [:ul.mt-2
  189. (for [[original-name name] pages]
  190. [:li {:key (str "tagged-page-" name)}
  191. [:a {:href (str "/page/" (util/encode-str name))}
  192. original-name]])])]])))
  193. (defonce last-route (atom :home))
  194. ;; A page is just a logical block
  195. (rum/defcs page < rum/reactive
  196. {:did-mount (fn [state]
  197. (ui-handler/scroll-and-highlight! state)
  198. ;; only when route changed
  199. (when (not= @last-route (state/get-current-route))
  200. (editor-handler/open-last-block! false))
  201. (reset! last-route (state/get-current-route))
  202. state)
  203. :did-update (fn [state]
  204. (ui-handler/scroll-and-highlight! state)
  205. state)}
  206. [state {:keys [repo] :as option}]
  207. (let [current-repo (state/sub :git/current-repo)
  208. repo (or repo current-repo)
  209. encoded-page-name (or (get-page-name state)
  210. (state/get-current-page))
  211. page-name (string/lower-case (util/url-decode encoded-page-name))
  212. path-page-name page-name
  213. marker-page? (util/marker? page-name)
  214. priority-page? (contains? #{"a" "b" "c"} page-name)
  215. format (db/get-page-format page-name)
  216. journal? (db/journal-page? page-name)
  217. block? (util/uuid-string? page-name)
  218. block-id (and block? (uuid page-name))
  219. sidebar? (:sidebar? option)]
  220. (rum/with-context [[t] i18n/*tongue-context*]
  221. (cond
  222. priority-page?
  223. [:div.page
  224. [:h1.title
  225. (str "Priority \"" (string/upper-case page-name) "\"")]
  226. [:div.ml-2
  227. (reference/references page-name false true)]]
  228. marker-page?
  229. [:div.page
  230. [:h1.title
  231. (string/upper-case page-name)]
  232. [:div.ml-2
  233. (reference/references page-name true false)]]
  234. :else
  235. (let [route-page-name page-name
  236. page (if block?
  237. (->> (:db/id (:block/page (db/entity repo [:block/uuid block-id])))
  238. (db/entity repo))
  239. (db/entity repo [:page/name page-name]))
  240. page (if page page (do
  241. (db/transact! repo [{:page/name page-name}])
  242. (db/entity repo [:page/name page-name])))
  243. properties (:page/properties page)
  244. page-name (:page/name page)
  245. page-original-name (:page/original-name page)
  246. file (:page/file page)
  247. file-path (and (:db/id file) (:file/path (db/entity repo (:db/id file))))
  248. today? (and
  249. journal?
  250. (= page-name (string/lower-case (date/journal-name))))
  251. developer-mode? (state/sub [:ui/developer-mode?])
  252. published? (= "true" (:published properties))
  253. public? (= "true" (:public properties))]
  254. [:div.flex-1.page.relative
  255. [:div.relative
  256. (when (and (not block?)
  257. (not sidebar?)
  258. (not config/publishing?))
  259. (let [links (->>
  260. [(when file
  261. {:title (t :page/re-index)
  262. :options {:on-click (fn []
  263. (file/re-index! file))}})
  264. {:title (t :page/add-to-contents)
  265. :options {:on-click (fn [] (page-handler/handle-add-page-to-contents! page-original-name))}}
  266. {:title (t :page/rename)
  267. :options {:on-click #(state/set-modal! (rename-page-dialog page-name))}}
  268. {:title (t :page/delete)
  269. :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}}
  270. {:title (t :page/action-publish)
  271. :options {:on-click
  272. (fn []
  273. (state/set-modal!
  274. (fn []
  275. [:div.cp__page-publish-actions
  276. (mapv (fn [{:keys [title options]}]
  277. (when title
  278. [:div.it
  279. {:on-click #(state/close-modal!)}
  280. (apply (partial ui/button title) (flatten (seq options)))]))
  281. [{:title (t :page/publish)
  282. :options {:on-click (fn []
  283. (page-handler/publish-page! page-name project/add-project))}}
  284. {:title (t :page/publish-as-slide)
  285. :options {:on-click (fn []
  286. (page-handler/publish-page-as-slide! page-name project/add-project))}}
  287. (when published?
  288. {:title (t :page/unpublish)
  289. :options {:on-click (fn []
  290. (page-handler/unpublish-page! page-name))}})
  291. {:title (t (if public? :page/make-private :page/make-public))
  292. :options {:background (if public? "gray" "indigo")
  293. :on-click #(page-handler/update-public-attribute!
  294. page-name
  295. (if public? false true))}}])])))}}
  296. (when developer-mode?
  297. {:title "(Dev) Show page data"
  298. :options {:on-click (fn []
  299. (let [page-data (with-out-str (pprint/pprint (db/pull (:db/id page))))]
  300. (println page-data)
  301. (notification/show!
  302. [:div
  303. [:pre.code page-data]
  304. [:br]
  305. (ui/button "Copy to clipboard"
  306. :on-click #(.writeText js/navigator.clipboard page-data))]
  307. :success
  308. false)))}})]
  309. (remove nil?))]
  310. (when (seq links)
  311. (ui/dropdown-with-links
  312. (fn [{:keys [toggle-fn]}]
  313. [:a.opacity-70.hover:opacity-100
  314. {:style {:position "absolute"
  315. :right 0
  316. :top 20}
  317. :title "More options"
  318. :on-click toggle-fn}
  319. (svg/vertical-dots {:class (util/hiccup->class "opacity-50.hover:opacity-100.h-5.w-5")})])
  320. links
  321. {:modal-class (util/hiccup->class
  322. "origin-top-right.absolute.right-0.top-10.mt-2.rounded-md.shadow-lg.whitespace-no-wrap.dropdown-overflow-auto.page-drop-options")
  323. :z-index 1}))))
  324. (when (and (not sidebar?)
  325. (not block?))
  326. [:a {:on-click (fn [e]
  327. (.preventDefault e)
  328. (when (gobj/get e "shiftKey")
  329. (when-let [page (db/pull repo '[*] [:page/name page-name])]
  330. (state/sidebar-add-block!
  331. repo
  332. (:db/id page)
  333. :page
  334. {:page page}))))}
  335. [:h1.title {:style {:margin-left -2}}
  336. (if page-original-name
  337. (if (and (string/includes? page-original-name "[[")
  338. (string/includes? page-original-name "]]"))
  339. (let [ast (mldoc/->edn page-original-name (mldoc/default-config format))]
  340. (block/markup-element-cp {} (ffirst ast)))
  341. page-original-name)
  342. (or
  343. page-name
  344. path-page-name))]])
  345. [:div
  346. [:div.content
  347. (when (and file-path
  348. (not sidebar?)
  349. (not block?)
  350. (not (state/hide-file?))
  351. (not config/publishing?))
  352. [:div.text-sm.ml-1.mb-4.flex-1.inline-flex
  353. {:key "page-file"}
  354. [:span.opacity-50 {:style {:margin-top 2}} (t :file/file)]
  355. [:a.bg-base-2.px-1.ml-1.mr-3 {:style {:border-radius 4}
  356. :href (str "/file/" (util/url-encode file-path))}
  357. file-path]
  358. (when (and (not config/mobile?)
  359. (not journal?))
  360. (presentation repo page))])]
  361. (when (and repo (not block?))
  362. (let [alias (db/get-page-alias-names repo page-name)]
  363. (when (seq alias)
  364. [:div.text-sm.ml-1.mb-4 {:key "page-file"}
  365. [:span.opacity-50 "Alias: "]
  366. (for [item alias]
  367. [:a.ml-1.mr-1 {:href (str "/page/" (util/encode-str item))}
  368. item])])))
  369. (when (and block? (not sidebar?))
  370. [:div.mb-4
  371. (block/block-parents repo block-id format)])
  372. ;; blocks
  373. (page-blocks-cp repo page file-path page-name page-original-name encoded-page-name sidebar? journal? block? block-id format)]]
  374. (when-not block?
  375. (today-queries repo today? sidebar?))
  376. (tagged-pages repo page-name)
  377. ;; referenced blocks
  378. [:div {:key "page-references"}
  379. (reference/references route-page-name false)]
  380. [:div {:key "page-unlinked-references"}
  381. (reference/unlinked-references route-page-name)]])))))
  382. (defonce layout (atom [js/window.outerWidth js/window.outerHeight]))
  383. (defonce graph-ref (atom nil))
  384. (defonce show-journal? (atom false))
  385. (defonce dot-mode? (atom false))
  386. (rum/defcs global-graph < rum/reactive
  387. [state]
  388. (let [theme (state/sub :ui/theme)
  389. sidebar-open? (state/sub :ui/sidebar-open?)
  390. [width height] (rum/react layout)
  391. dark? (= theme "dark")
  392. graph (graph-handler/build-global-graph theme (rum/react show-journal?))
  393. dot-mode-value? (rum/react dot-mode?)]
  394. (rum/with-context [[t] i18n/*tongue-context*]
  395. [:div.relative#global-graph
  396. (if (seq (:nodes graph))
  397. (graph-2d/graph
  398. (graph/build-graph-opts
  399. graph
  400. dark?
  401. dot-mode-value?
  402. {:width (if (and (> width 1280) sidebar-open?)
  403. (- width 24 600)
  404. (- width 24))
  405. :height (- height 120)
  406. :ref (fn [v] (reset! graph-ref v))
  407. :ref-atom graph-ref}))
  408. [:div.ls-center.mt-20
  409. [:p.opacity-70.font-medium "Empty"]])
  410. [:div.absolute.top-15.left-5
  411. [:div.flex.flex-col
  412. [:a.text-sm.font-medium
  413. {:on-click (fn [_e]
  414. (swap! show-journal? not))}
  415. (str (t :page/show-journals)
  416. (if @show-journal? " (ON)"))]
  417. [:a.text-sm.font-medium.mt-4
  418. {:title (if @dot-mode?
  419. (t :page/show-name)
  420. (t :page/hide-name))
  421. :on-click (fn [_e]
  422. (swap! dot-mode? not))}
  423. (str (t :dot-mode)
  424. (if @dot-mode? " (ON)"))]]]])))
  425. (rum/defc all-pages < rum/reactive
  426. ;; {:did-mount (fn [state]
  427. ;; (let [current-repo (state/sub :git/current-repo)]
  428. ;; (js/setTimeout #(db/remove-orphaned-pages! current-repo) 0))
  429. ;; state)}
  430. []
  431. (let [current-repo (state/sub :git/current-repo)]
  432. (rum/with-context [[t] i18n/*tongue-context*]
  433. [:div.flex-1
  434. [:h1.title (t :all-pages)]
  435. (when current-repo
  436. (let [pages (page-handler/get-pages-with-modified-at current-repo)]
  437. [:table.table-auto
  438. [:thead
  439. [:tr
  440. [:th (t :page/name)]
  441. [:th (t :file/last-modified-at)]]]
  442. [:tbody
  443. (for [[page modified-at] pages]
  444. (let [encoded-page (util/encode-str page)]
  445. [:tr {:key encoded-page}
  446. [:td [:a {:on-click (fn [e]
  447. (.preventDefault e)
  448. (let [repo (state/get-current-repo)
  449. page (db/pull repo '[*] [:page/name (string/lower-case page)])]
  450. (when (gobj/get e "shiftKey")
  451. (state/sidebar-add-block!
  452. repo
  453. (:db/id page)
  454. :page
  455. {:page page}))))
  456. :href (rfe/href :page {:name encoded-page})}
  457. page]]
  458. [:td [:span.text-gray-500.text-sm
  459. (if (zero? modified-at)
  460. (t :file/no-data)
  461. (date/get-date-time-string
  462. (t/to-default-time-zone (tc/to-date-time modified-at))))]]]))]]))])))
  463. (rum/defcs new < rum/reactive
  464. (rum/local "" ::title)
  465. (mixins/event-mixin
  466. (fn [state]
  467. (mixins/on-enter state
  468. :node (gdom/getElement "page-title")
  469. :on-enter (fn []
  470. (let [title @(get state ::title)]
  471. (when-not (string/blank? title)
  472. (page-handler/create! title)))))))
  473. [state]
  474. (rum/with-context [[t] i18n/*tongue-context*]
  475. (let [title (get state ::title)]
  476. [:div#page-new.flex-1.flex-col {:style {:flex-wrap "wrap"}}
  477. [:div.mt-10.mb-2 {:style {:font-size "1.5rem"}}
  478. (t :page/new-title)]
  479. [:input#page-title.focus:outline-none.ml-1.text-gray-900
  480. {:style {:border "none"
  481. :font-size "1.8rem"
  482. :max-width 300}
  483. :auto-focus true
  484. :auto-complete "off"
  485. :on-change (fn [e]
  486. (reset! title (util/evalue e)))}]])))