page.cljs 22 KB

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