| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493 |
- (ns frontend.components.page
- (:require [rum.core :as rum]
- [frontend.util :as util :refer-macros [profile]]
- [frontend.handler.file :as file]
- [frontend.handler.page :as page-handler]
- [frontend.handler.ui :as ui-handler]
- [frontend.handler.route :as route-handler]
- [frontend.handler.notification :as notification]
- [frontend.handler.editor :as editor-handler]
- [frontend.state :as state]
- [clojure.string :as string]
- [frontend.db :as db]
- [dommy.core :as d]
- [frontend.components.block :as block]
- [frontend.components.editor :as editor]
- [frontend.components.reference :as reference]
- [frontend.components.svg :as svg]
- [frontend.extensions.graph-2d :as graph-2d]
- [frontend.ui :as ui]
- [frontend.components.content :as content]
- [frontend.components.project :as project]
- [frontend.config :as config]
- [frontend.db :as db]
- [frontend.mixins :as mixins]
- [frontend.db-mixins :as db-mixins]
- [goog.dom :as gdom]
- [goog.object :as gobj]
- [frontend.utf8 :as utf8]
- [frontend.date :as date]
- [frontend.graph :as graph]
- [frontend.format.mldoc :as mldoc]
- [cljs-time.coerce :as tc]
- [cljs-time.core :as t]
- [cljs.pprint :as pprint]
- [frontend.context.i18n :as i18n]
- [reitit.frontend.easy :as rfe]))
- (defn- get-page-name
- [state]
- (let [route-match (first (:rum/args state))]
- (get-in route-match [:parameters :path :name])))
- (defn- get-blocks
- [repo page-name page-original-name block? block-id]
- (when page-name
- (if block?
- (db/get-block-and-children repo block-id)
- (do
- (db/add-page-to-recent! repo page-original-name)
- (db/get-page-blocks repo page-name)))))
- (rum/defc page-blocks-cp < rum/reactive
- db-mixins/query
- [repo page file-path page-name page-original-name encoded-page-name sidebar? journal? block? block-id format]
- (let [raw-page-blocks (get-blocks repo page-name page-original-name block? block-id)
- page-blocks (db/with-dummy-block raw-page-blocks format
- (if (empty? raw-page-blocks)
- (let [content (db/get-file repo file-path)]
- {:block/page {:db/id (:db/id page)}
- :block/file {:db/id (:db/id (:page/file page))}
- :block/meta
- (let [file-id (:db/id (:page/file page))]
- {:start-pos (utf8/length (utf8/encode content))
- :end-pos nil})}))
- journal?)
- start-level (or (:block/level (first page-blocks)) 1)
- hiccup-config {:id encoded-page-name
- :start-level start-level
- :sidebar? sidebar?
- :block? block?
- :editor-box editor/box}
- hiccup (block/->hiccup page-blocks hiccup-config {})]
- (rum/with-key
- (content/content encoded-page-name
- {:hiccup hiccup
- :sidebar? sidebar?})
- (str encoded-page-name "-hiccup"))))
- (defn contents-page
- [{:page/keys [name original-name file] :as contents}]
- (when-let [repo (state/get-current-repo)]
- (let [format (db/get-page-format name)
- file-path (:file/path file)]
- (page-blocks-cp repo contents file-path name original-name name true false false nil format))))
- (defn presentation
- [repo page journal?]
- [:a.opacity-50.hover:opacity-100.ml-4
- {:title "Presentation mode (Powered by Reveal.js)"
- :on-click (fn []
- (state/sidebar-add-block!
- repo
- (:db/id page)
- :page-presentation
- {:page page
- :journal? journal?}))}
- svg/slideshow])
- (rum/defc today-queries < rum/reactive
- [repo today? sidebar?]
- (when (and today? (not sidebar?))
- (let [queries (state/sub [:config repo :default-queries :journals])]
- (when (seq queries)
- [:div#today-queries.mt-10
- (for [{:keys [title] :as query} queries]
- (rum/with-key
- (block/custom-query {:start-level 2
- :attr {:class "mt-10"}
- :editor-box editor/box} query)
- (str repo "-custom-query-" (:query query))))]))))
- (defn- delete-page!
- [page-name]
- (page-handler/delete! page-name
- (fn []
- (notification/show! (str "Page " page-name " was deleted successfully!")
- :success)))
- (state/close-modal!)
- (route-handler/redirect-to-home!))
- (defn delete-page-dialog
- [page-name]
- (fn [close-fn]
- [:div
- [:div.sm:flex.sm:items-start
- [: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
- [:svg.h-6.w-6.text-red-600
- {:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
- [:path
- {:d
- "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",
- :stroke-width "2",
- :stroke-linejoin "round",
- :stroke-linecap "round"}]]]
- [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
- [:h3#modal-headline.text-lg.leading-6.font-medium.text-gray-900
- "Are you sure you want to delete this page?"]]]
- [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
- [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
- [: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
- {:type "button"
- :on-click (fn []
- (delete-page! page-name))}
- "Yes"]]
- [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
- [: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
- {:type "button"
- :on-click close-fn}
- "Cancel"]]]]))
- (rum/defcs rename-page-dialog-inner <
- (rum/local "" ::input)
- [state page-name close-fn]
- (let [input (get state ::input)]
- [:div
- [:div.sm:flex.sm:items-start
- [:div.mt-3.text-center.sm:mt-0.sm:text-left
- [:h3#modal-headline.text-lg.leading-6.font-medium.text-gray-900
- (str "Rename \"" page-name "\" to:")]]]
- [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
- {:auto-focus true
- :style {:color "#000"}
- :on-change (fn [e]
- (reset! input (util/evalue e)))}]
- [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
- [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
- [: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
- {:type "button"
- :on-click (fn []
- (let [value @input]
- (let [value (string/trim value)]
- (when-not (string/blank? value)
- (page-handler/rename! page-name value)
- (state/close-modal!)))))}
- "Submit"]]
- [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
- [: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
- {:type "button"
- :on-click close-fn}
- "Cancel"]]]]))
- (defn rename-page-dialog
- [page-name]
- (fn [close-fn]
- (rename-page-dialog-inner page-name close-fn)))
- (defn tagged-pages
- [repo tag]
- (let [pages (db/get-tag-pages repo tag)]
- (when (seq pages)
- [:div.references.mt-6.flex-1.flex-row
- [:div.content
- (ui/foldable
- [:h2.font-bold.opacity-50 (util/format "Pages tagged with \"%s\"" tag)]
- [:ul.mt-2
- (for [[original-name name] pages]
- [:li {:key (str "tagged-page-" name)}
- [:a {:href (str "/page/" (util/encode-str name))}
- original-name]])])]])))
- (defonce last-route (atom :home))
- ;; A page is just a logical block
- (rum/defcs page < rum/reactive
- {:did-mount (fn [state]
- (ui-handler/scroll-and-highlight! state)
- ;; only when route changed
- (when (not= @last-route (state/get-current-route))
- (editor-handler/open-last-block! false))
- (reset! last-route (state/get-current-route))
- state)
- :did-update (fn [state]
- (ui-handler/scroll-and-highlight! state)
- state)}
- [state {:keys [repo] :as option}]
- (let [current-repo (state/sub :git/current-repo)
- repo (or repo current-repo)
- encoded-page-name (or (get-page-name state)
- (state/get-current-page))
- page-name (string/lower-case (util/url-decode encoded-page-name))
- path-page-name page-name
- marker-page? (util/marker? page-name)
- priority-page? (contains? #{"a" "b" "c"} page-name)
- format (db/get-page-format page-name)
- journal? (db/journal-page? page-name)
- block? (util/uuid-string? page-name)
- block-id (and block? (uuid page-name))
- sidebar? (:sidebar? option)]
- (rum/with-context [[t] i18n/*tongue-context*]
- (cond
- priority-page?
- [:div.page
- [:h1.title
- (str "Priority \"" (string/upper-case page-name) "\"")]
- [:div.ml-2
- (reference/references page-name false true)]]
- marker-page?
- [:div.page
- [:h1.title
- (string/upper-case page-name)]
- [:div.ml-2
- (reference/references page-name true false)]]
- :else
- (let [route-page-name page-name
- page (if block?
- (->> (:db/id (:block/page (db/entity repo [:block/uuid block-id])))
- (db/entity repo))
- (db/entity repo [:page/name page-name]))
- properties (:page/properties page)
- page-name (:page/name page)
- page-original-name (:page/original-name page)
- file (:page/file page)
- file-path (and (:db/id file) (:file/path (db/entity repo (:db/id file))))
- today? (and
- journal?
- (= page-name (string/lower-case (date/journal-name))))
- developer-mode? (state/sub [:ui/developer-mode?])
- published? (= "true" (:published properties))
- public? (= "true" (:public properties))]
- [:div.flex-1.page.relative
- [:div.relative
- (when (and (not block?)
- (not sidebar?)
- (not config/publishing?))
- (let [links (->>
- [(when file
- {:title (t :page/re-index)
- :options {:on-click (fn []
- (file/re-index! file))}})
- {:title (t :page/add-to-contents)
- :options {:on-click (fn [] (page-handler/handle-add-page-to-contents! page-original-name))}}
- {:title (t :page/rename)
- :options {:on-click #(state/set-modal! (rename-page-dialog page-name))}}
- {:title (t :page/delete)
- :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}}
- {:title (t (if public? :page/make-private :page/make-public))
- :options {:on-click #(page-handler/update-public-attribute!
- page-name
- (if public? false true))}}
- {:title (t :page/publish)
- :options {:on-click (fn []
- (page-handler/publish-page! page-name project/add-project))}}
- {:title (t :page/publish-as-slide)
- :options {:on-click (fn []
- (page-handler/publish-page-as-slide! page-name project/add-project))}}
- (when published?
- {:title (t :page/unpublish)
- :options {:on-click (fn []
- (page-handler/unpublish-page! page-name))}})
- (when developer-mode?
- {:title "(Dev) Show page data"
- :options {:on-click (fn []
- (let [page-data (with-out-str (pprint/pprint (db/pull (:db/id page))))]
- (println page-data)
- (notification/show!
- [:div
- [:pre.code page-data]
- [:br]
- (ui/button "Copy to clipboard"
- :on-click #(.writeText js/navigator.clipboard page-data))]
- :success
- false)))}})]
- (remove nil?))]
- (when (seq links)
- (ui/dropdown-with-links
- (fn [{:keys [toggle-fn]}]
- [:a.opacity-70.hover:opacity-100
- {:style {:position "absolute"
- :right 0
- :top 20}
- :title "More options"
- :on-click toggle-fn}
- (svg/vertical-dots {:class (util/hiccup->class "opacity-50.hover:opacity-100.h-5.w-5")})])
- links
- {:modal-class (util/hiccup->class
- "origin-top-right.absolute.right-0.top-10.mt-2.rounded-md.shadow-lg.whitespace-no-wrap.dropdown-overflow-auto.page-drop-options")
- :z-index 1}))))
- (when (and (not sidebar?)
- (not block?))
- [:a {:on-click (fn [e]
- (util/stop e)
- (when (gobj/get e "shiftKey")
- (when-let [page (db/pull repo '[*] [:page/name page-name])]
- (state/sidebar-add-block!
- repo
- (:db/id page)
- :page
- {:page page}))))}
- [:h1.title {:style {:margin-left -2}}
- (if page-original-name
- (if (and (string/includes? page-original-name "[[")
- (string/includes? page-original-name "]]"))
- (let [ast (mldoc/->edn page-original-name (mldoc/default-config format))]
- (block/markup-element-cp {} (ffirst ast)))
- page-original-name)
- (or
- page-name
- path-page-name))]])
- [:div
- [:div.content
- (when (and file-path
- (not sidebar?)
- (not block?)
- (not (state/hide-file?))
- (not config/publishing?))
- [:div.text-sm.ml-1.mb-4.flex-1 {:key "page-file"}
- [:span.opacity-50 (t :file/file)]
- [:a.bg-base-2.p-1.ml-1 {:style {:border-radius 4}
- :href (str "/file/" (util/url-encode file-path))}
- file-path]])]
- (when (and repo (not journal?) (not block?))
- (let [alias (db/get-page-alias-names repo page-name)]
- (when (seq alias)
- [:div.text-sm.ml-1.mb-4 {:key "page-file"}
- [:span.opacity-50 "Alias: "]
- (for [item alias]
- [:a.p-1.ml-1 {:href (str "/page/" (util/encode-str item))}
- item])])))
- (when (and block? (not sidebar?))
- [:div.mb-4
- (block/block-parents repo block-id format)])
- ;; blocks
- (page-blocks-cp repo page file-path page-name page-original-name encoded-page-name sidebar? journal? block? block-id format)]]
- (when-not block?
- (today-queries repo today? sidebar?))
- (tagged-pages repo page-name)
- ;; referenced blocks
- [:div {:key "page-references"}
- (reference/references route-page-name false)]
- [:div {:key "page-unlinked-references"}
- (reference/unlinked-references route-page-name)]])))))
- (defonce layout (atom [js/window.outerWidth js/window.outerHeight]))
- (defonce graph-ref (atom nil))
- (defonce show-journal? (atom false))
- (defonce dot-mode? (atom false))
- (rum/defcs global-graph < rum/reactive
- [state]
- (let [theme (state/sub :ui/theme)
- sidebar-open? (state/sub :ui/sidebar-open?)
- [width height] (rum/react layout)
- dark? (= theme "dark")
- graph (db/build-global-graph theme (rum/react show-journal?))
- dot-mode-value? (rum/react dot-mode?)]
- (rum/with-context [[t] i18n/*tongue-context*]
- [:div.relative#global-graph
- (if (seq (:nodes graph))
- (graph-2d/graph
- (graph/build-graph-opts
- graph
- dark?
- dot-mode-value?
- {:width (if (and (> width 1280) sidebar-open?)
- (- width 24 600)
- (- width 24))
- :height (- height 120)
- :ref (fn [v] (reset! graph-ref v))
- :ref-atom graph-ref}))
- [:div.ls-center.mt-20
- [:p.opacity-70.font-medium "Empty"]])
- [:div.absolute.top-5.left-5
- [:div.flex.flex-col
- [:a.text-sm.font-medium
- {:on-click (fn [_e]
- (swap! show-journal? not))}
- (str (t :page/show-journals)
- (if @show-journal? " (ON)"))]
- [:a.text-sm.font-medium.mt-4
- {:title (if @dot-mode?
- (t :page/show-name)
- (t :page/hide-name))
- :on-click (fn [_e]
- (swap! dot-mode? not))}
- (str (t :dot-mode)
- (if @dot-mode? " (ON)"))]]]])))
- (rum/defc all-pages < rum/reactive
- ;; {:did-mount (fn [state]
- ;; (let [current-repo (state/sub :git/current-repo)]
- ;; (js/setTimeout #(db/remove-orphaned-pages! current-repo) 0))
- ;; state)}
- []
- (let [current-repo (state/sub :git/current-repo)]
- (rum/with-context [[t] i18n/*tongue-context*]
- [:div.flex-1
- [:h1.title (t :all-pages)]
- (when current-repo
- (let [pages (db/get-pages-with-modified-at current-repo)]
- [:table.table-auto
- [:thead
- [:tr
- [:th (t :page/name)]
- [:th (t :file/last-modified-at)]]]
- [:tbody
- (for [[page modified-at] pages]
- (let [encoded-page (util/encode-str page)]
- [:tr {:key encoded-page}
- [:td [:a {:on-click (fn [e]
- (util/stop e)
- (let [repo (state/get-current-repo)
- page (db/pull repo '[*] [:page/name (string/lower-case page)])]
- (when (gobj/get e "shiftKey")
- (state/sidebar-add-block!
- repo
- (:db/id page)
- :page
- {:page page}))))
- :href (rfe/href :page {:name encoded-page})}
- page]]
- [:td [:span.text-gray-500.text-sm
- (if (zero? modified-at)
- (t :file/no-data)
- (date/get-date-time-string
- (t/to-default-time-zone (tc/to-date-time modified-at))))]]]))]]))])))
- (rum/defcs new < rum/reactive
- (rum/local "" ::title)
- (mixins/event-mixin
- (fn [state]
- (mixins/on-enter state
- :node (gdom/getElement "page-title")
- :on-enter (fn []
- (let [title @(get state ::title)]
- (when-not (string/blank? title)
- (page-handler/create! title)))))))
- [state]
- (rum/with-context [[t] i18n/*tongue-context*]
- (let [title (get state ::title)]
- [:div#page-new.flex-1.flex-col {:style {:flex-wrap "wrap"}}
- [:div.mt-10.mb-2 {:style {:font-size "1.5rem"}}
- (t :page/new-title)]
- [:input#page-title.focus:outline-none.ml-1.text-gray-900
- {:style {:border "none"
- :font-size "1.8rem"
- :max-width 300}
- :auto-focus true
- :auto-complete "off"
- :on-change (fn [e]
- (reset! title (util/evalue e)))}]])))
|