mirror of https://github.com/logseq/logseq
Add :heading/children
parent
c748f83617
commit
fe96858e19
|
@ -166,6 +166,10 @@
|
|||
new-pos (- (+ (count prefix)
|
||||
(or forward-pos 0))
|
||||
(or backward-pos 0))]
|
||||
(prn {:id id
|
||||
:new-value new-value
|
||||
:value value
|
||||
:new-pos new-pos})
|
||||
(state/set-heading-content-and-last-pos! id new-value new-pos)
|
||||
(util/move-cursor-to input
|
||||
(if (or backward-pos forward-pos)
|
||||
|
|
|
@ -249,6 +249,13 @@
|
|||
chosen-handler (fn [chosen _click?]
|
||||
(state/set-editor-show-block-search false)
|
||||
(let [uuid-string (str (:heading/uuid chosen))]
|
||||
;; block reference
|
||||
(insert-command! id
|
||||
(util/format "((%s))" uuid-string)
|
||||
format
|
||||
{:last-pattern (str "((" q)
|
||||
:postfix-fn (fn [s] (util/replace-first "))" s ""))})
|
||||
;; block embed
|
||||
(insert-command! id
|
||||
(str "@@embed: " uuid-string)
|
||||
format
|
||||
|
|
|
@ -0,0 +1,775 @@
|
|||
(ns frontend.components.editor
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.handler :as handler]
|
||||
[frontend.util :as util]
|
||||
[frontend.date :as date]
|
||||
[frontend.state :as state]
|
||||
[frontend.mixins :as mixins]
|
||||
[frontend.image :as image]
|
||||
[frontend.ui :as ui]
|
||||
[frontend.db :as db]
|
||||
[frontend.config :as config]
|
||||
[dommy.core :as d]
|
||||
[goog.object :as gobj]
|
||||
[goog.dom :as gdom]
|
||||
[clojure.string :as string]
|
||||
[frontend.commands :as commands
|
||||
:refer [*show-commands
|
||||
*matched-commands
|
||||
*slash-caret-pos
|
||||
*angle-bracket-caret-pos
|
||||
*matched-block-commands
|
||||
*show-block-commands]]
|
||||
[medley.core :as medley]
|
||||
[cljs-time.core :as t]
|
||||
[cljs-time.coerce :as tc]
|
||||
[cljs-drag-n-drop.core :as dnd]
|
||||
[frontend.search :as search]
|
||||
["/frontend/utils" :as utils]))
|
||||
|
||||
;; TODO: refactor the state, it is already too complex.
|
||||
(defonce *last-edit-heading (atom nil))
|
||||
|
||||
;; FIXME: should support multiple images concurrently uploading
|
||||
(defonce *image-uploading? (atom false))
|
||||
(defonce *image-uploading-process (atom 0))
|
||||
|
||||
(defn set-last-edit-heading!
|
||||
[id value]
|
||||
(reset! *last-edit-heading [id value]))
|
||||
|
||||
(defn- insert-command!
|
||||
[id command-output format {:keys [restore?]
|
||||
:or {restore? true}
|
||||
:as option}]
|
||||
(cond
|
||||
;; replace string
|
||||
(string? command-output)
|
||||
(commands/insert! id command-output option)
|
||||
|
||||
;; steps
|
||||
(vector? command-output)
|
||||
(commands/handle-steps command-output format)
|
||||
|
||||
:else
|
||||
nil)
|
||||
|
||||
(when restore?
|
||||
(commands/restore-state restore?)))
|
||||
|
||||
(def autopair-map
|
||||
{"[" "]"
|
||||
"{" "}"
|
||||
"(" ")"
|
||||
"$" "$" ; math
|
||||
"`" "`"
|
||||
"~" "~"
|
||||
"*" "*"
|
||||
"_" "_"
|
||||
"^" "^"})
|
||||
|
||||
(def reversed-autopair-map
|
||||
(zipmap (vals autopair-map)
|
||||
(keys autopair-map)))
|
||||
|
||||
(defn- autopair
|
||||
[input-id prefix format {:keys [restore?]
|
||||
:or {restore? true}
|
||||
:as option}]
|
||||
(let [value (get autopair-map prefix)
|
||||
value (str prefix value)
|
||||
input (gdom/getElement input-id)]
|
||||
(when value
|
||||
(let [[prefix pos] (commands/simple-insert! input-id value
|
||||
{:backward-pos 1
|
||||
:check-fn (fn [new-value prefix-pos]
|
||||
(when (>= prefix-pos 0)
|
||||
[(subs new-value prefix-pos (+ prefix-pos 2))
|
||||
(+ prefix-pos 2)]))})]
|
||||
(case prefix
|
||||
"[["
|
||||
(do
|
||||
(commands/handle-step [:editor/search-page])
|
||||
(reset! commands/*slash-caret-pos (util/get-caret-pos input)))
|
||||
|
||||
"(("
|
||||
(do
|
||||
(commands/handle-step [:editor/search-block])
|
||||
(reset! commands/*slash-caret-pos (util/get-caret-pos input)))
|
||||
|
||||
nil))
|
||||
)))
|
||||
|
||||
(defn- upload-image
|
||||
[id files format uploading? drop?]
|
||||
(image/upload
|
||||
files
|
||||
(fn [file file-name file-type]
|
||||
(handler/request-presigned-url
|
||||
file file-name file-type
|
||||
uploading?
|
||||
(fn [signed-url]
|
||||
(insert-command! id
|
||||
(util/format "[[%s][%s]]"
|
||||
signed-url
|
||||
file-name)
|
||||
format
|
||||
{:last-pattern (if drop? "" commands/slash)})
|
||||
|
||||
(reset! *image-uploading-process 0))
|
||||
(fn [e]
|
||||
(let [process (* (/ (gobj/get e "loaded")
|
||||
(gobj/get e "total"))
|
||||
100)]
|
||||
(reset! *image-uploading-process process)))))))
|
||||
|
||||
(defn with-levels
|
||||
[text format level]
|
||||
(let [pattern (config/get-heading-pattern format)]
|
||||
(str (apply str (repeat level pattern))
|
||||
" "
|
||||
(string/triml text))))
|
||||
|
||||
(rum/defc commands < rum/reactive
|
||||
[id format]
|
||||
(when (and (util/react *show-commands)
|
||||
@*slash-caret-pos
|
||||
(not (state/sub :editor/show-page-search?))
|
||||
(not (state/sub :editor/show-input))
|
||||
(not (state/sub :editor/show-date-picker?)))
|
||||
(let [matched (util/react *matched-commands)]
|
||||
(ui/auto-complete
|
||||
(map first matched)
|
||||
{:on-chosen (fn [chosen]
|
||||
(let [restore-slash? (not (contains? #{"Page Reference"
|
||||
"Link"
|
||||
"Image Link"
|
||||
"Date Picker"} chosen))]
|
||||
(insert-command! id (get (into {} matched) chosen)
|
||||
format
|
||||
{:restore? restore-slash?})))
|
||||
:class "black"}))))
|
||||
|
||||
(rum/defc block-commands < rum/reactive
|
||||
[id format]
|
||||
(when (and (util/react *show-block-commands)
|
||||
@*angle-bracket-caret-pos)
|
||||
(let [matched (util/react *matched-block-commands)]
|
||||
(ui/auto-complete
|
||||
(map first matched)
|
||||
{:on-chosen (fn [chosen]
|
||||
(insert-command! id (get (into {} matched) chosen)
|
||||
format
|
||||
{:last-pattern commands/angle-bracket}))
|
||||
:class "black"}))))
|
||||
|
||||
(defn get-matched-pages
|
||||
[q]
|
||||
(let [pages (db/get-pages (state/get-current-repo))]
|
||||
(filter
|
||||
(fn [page]
|
||||
(string/index-of
|
||||
(string/lower-case page)
|
||||
(string/lower-case q)))
|
||||
pages)))
|
||||
|
||||
(defn get-previous-input-char
|
||||
[input]
|
||||
(when-let [pos (:pos (util/get-caret-pos input))]
|
||||
(let [value (gobj/get input "value")]
|
||||
(when (and (>= (count value) pos)
|
||||
(>= pos 1))
|
||||
(nth value (- pos 1))))))
|
||||
|
||||
(defn get-previous-input-chars
|
||||
[input length]
|
||||
(when-let [pos (:pos (util/get-caret-pos input))]
|
||||
(let [value (gobj/get input "value")]
|
||||
(when (and (>= (count value) pos)
|
||||
(>= pos 1))
|
||||
(subs value (- pos length) pos)))))
|
||||
|
||||
(defn get-current-input-char
|
||||
[input]
|
||||
(when-let [pos (:pos (util/get-caret-pos input))]
|
||||
(let [value (gobj/get input "value")]
|
||||
(when (and (>= (count value) (inc pos))
|
||||
(>= pos 1))
|
||||
(nth value pos)))))
|
||||
|
||||
(rum/defc page-search < rum/reactive
|
||||
[id format]
|
||||
(when (state/sub :editor/show-page-search?)
|
||||
(let [pos (:editor/last-saved-cursor @state/state)
|
||||
input (gdom/getElement id)]
|
||||
(when input
|
||||
(let [current-pos (:pos (util/get-caret-pos input))
|
||||
edit-content (state/sub [:editor/content id])
|
||||
q (when (> (count edit-content)
|
||||
(+ current-pos))
|
||||
(subs edit-content pos current-pos))
|
||||
matched-pages (when-not (string/blank? q)
|
||||
(map util/capitalize-all (get-matched-pages q)))
|
||||
chosen-handler (fn [chosen _click?]
|
||||
(state/set-editor-show-page-search false)
|
||||
(insert-command! id
|
||||
(util/format "[[%s]]" chosen)
|
||||
format
|
||||
{:last-pattern (str "[[" q)
|
||||
:postfix-fn (fn [s] (util/replace-first "]]" s ""))}))
|
||||
non-exist-page-handler (fn [_state]
|
||||
(state/set-editor-show-page-search false)
|
||||
(util/cursor-move-forward input 2))]
|
||||
(ui/auto-complete
|
||||
matched-pages
|
||||
{:on-chosen chosen-handler
|
||||
:on-enter non-exist-page-handler
|
||||
:empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a page"]
|
||||
:class "black"}))))))
|
||||
|
||||
(defn get-matched-blocks
|
||||
[q]
|
||||
(search/search q 5))
|
||||
|
||||
(rum/defc block-search < rum/reactive
|
||||
[id format]
|
||||
(when (state/sub :editor/show-block-search?)
|
||||
(let [pos (:editor/last-saved-cursor @state/state)
|
||||
input (gdom/getElement id)]
|
||||
(when input
|
||||
(let [current-pos (:pos (util/get-caret-pos input))
|
||||
edit-content (state/sub [:editor/content id])
|
||||
q (when (> (count edit-content)
|
||||
(+ current-pos))
|
||||
(subs edit-content pos current-pos))
|
||||
matched-blocks (when-not (string/blank? q)
|
||||
(get-matched-blocks q))
|
||||
chosen-handler (fn [chosen _click?]
|
||||
(state/set-editor-show-block-search false)
|
||||
(let [uuid-string (str (:heading/uuid chosen))]
|
||||
(insert-command! id
|
||||
(util/format "((%s))" uuid-string)
|
||||
format
|
||||
{:last-pattern (str "((" q)
|
||||
:postfix-fn (fn [s] (util/replace-first "))" s ""))})
|
||||
;; Save it so it'll be remembered when next time it got parsing
|
||||
(handler/set-heading-property! (:heading/uuid chosen)
|
||||
"CUSTOM_ID"
|
||||
uuid-string)))
|
||||
non-exist-block-handler (fn [_state]
|
||||
(state/set-editor-show-block-search false)
|
||||
(util/cursor-move-forward input 2))]
|
||||
(ui/auto-complete
|
||||
matched-blocks
|
||||
{:on-chosen chosen-handler
|
||||
:on-enter non-exist-block-handler
|
||||
:empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a block"]
|
||||
:item-render (fn [{:heading/keys [content]}]
|
||||
(subs content 0 64))
|
||||
:class "black"}))))))
|
||||
|
||||
(rum/defc date-picker < rum/reactive
|
||||
[id format]
|
||||
(when (state/sub :editor/show-date-picker?)
|
||||
(ui/datepicker
|
||||
(t/today)
|
||||
{:on-change
|
||||
(fn [e date]
|
||||
(util/stop e)
|
||||
(let [date (t/to-default-time-zone date)
|
||||
journal (date/journal-name date)]
|
||||
;; similar to page reference
|
||||
(insert-command! id
|
||||
(util/format "[[%s]]" journal)
|
||||
format
|
||||
nil)
|
||||
(state/set-editor-show-date-picker false)))})))
|
||||
|
||||
(rum/defcs input < rum/reactive
|
||||
(rum/local {} ::input-value)
|
||||
(mixins/event-mixin
|
||||
(fn [state]
|
||||
(mixins/on-key-down
|
||||
state
|
||||
{
|
||||
;; enter
|
||||
13 (fn [state e]
|
||||
(let [input-value (get state ::input-value)]
|
||||
(when (seq @input-value)
|
||||
;; no new line input
|
||||
(util/stop e)
|
||||
(let [[_id on-submit] (:rum/args state)
|
||||
{:keys [pos]} @*slash-caret-pos]
|
||||
(on-submit @input-value pos))
|
||||
(reset! input-value nil))))}
|
||||
nil)))
|
||||
{:did-update
|
||||
(fn [state]
|
||||
(when-let [show-input (state/get-editor-show-input)]
|
||||
(let [id (str "modal-input-"
|
||||
(name (:id (first show-input))))
|
||||
first-input (gdom/getElement id)]
|
||||
(when (and first-input
|
||||
(not (d/has-class? first-input "focused")))
|
||||
(.focus first-input)
|
||||
(d/add-class! first-input "focused"))))
|
||||
state)}
|
||||
[state id on-submit]
|
||||
(when-let [input-option (state/sub :editor/show-input)]
|
||||
(let [{:keys [pos]} (util/react *slash-caret-pos)
|
||||
input-value (get state ::input-value)]
|
||||
(when (seq input-option)
|
||||
[:div.p-2.mt-2.rounded-md.shadow-sm.bg-base-2
|
||||
(for [{:keys [id] :as input-item} input-option]
|
||||
[:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5.mb-2
|
||||
(merge
|
||||
{:key (str "modal-input-" (name id))
|
||||
:id (str "modal-input-" (name id))
|
||||
:value (get @input-value id "")
|
||||
:on-change (fn [e]
|
||||
(swap! input-value assoc id (util/evalue e)))
|
||||
:auto-complete "off"}
|
||||
(dissoc input-item :id))])
|
||||
(ui/button
|
||||
"Submit"
|
||||
:on-click
|
||||
(fn [e]
|
||||
(util/stop e)
|
||||
(on-submit @input-value pos)))]))))
|
||||
|
||||
;; TODO: refactor
|
||||
(defn get-state
|
||||
[state]
|
||||
(let [[content {:keys [on-hide heading heading-id heading-parent-id dummy? format sidebar?]} id] (:rum/args state)
|
||||
node (gdom/getElement id)
|
||||
value (gobj/get node "value")
|
||||
pos (gobj/get node "selectionStart")]
|
||||
{:on-hide on-hide
|
||||
:content content
|
||||
:dummy? dummy?
|
||||
:sidebar? sidebar?
|
||||
:format format
|
||||
:id id
|
||||
:heading heading
|
||||
:heading-id heading-id
|
||||
:heading-parent-id heading-parent-id
|
||||
:node node
|
||||
:value value
|
||||
:pos pos}))
|
||||
|
||||
(defn on-up-down
|
||||
[state e up?]
|
||||
(let [{:keys [id heading-id heading heading-parent-id dummy? value pos format]} (get-state state)
|
||||
element (gdom/getElement id)
|
||||
line-height (util/get-textarea-line-height element)]
|
||||
(when (and heading-id
|
||||
(or (and up? (util/textarea-cursor-first-row? element line-height))
|
||||
(and (not up?) (util/textarea-cursor-end-row? element line-height))))
|
||||
(util/stop e)
|
||||
(let [f (if up? util/get-prev-heading util/get-next-heading)
|
||||
sibling-heading (f (gdom/getElement heading-parent-id))]
|
||||
(when sibling-heading
|
||||
(when-let [sibling-heading-id (d/attr sibling-heading "headingid")]
|
||||
(handler/edit-heading! (uuid sibling-heading-id) pos format id)))))))
|
||||
|
||||
(defn delete-heading!
|
||||
[state e]
|
||||
(let [{:keys [id heading-id heading-parent-id dummy? value pos format]} (get-state state)]
|
||||
(when (and heading-id (= value ""))
|
||||
(do
|
||||
(util/stop e)
|
||||
;; delete heading, edit previous heading
|
||||
(let [heading (db/pull [:heading/uuid heading-id])
|
||||
heading-parent (gdom/getElement heading-parent-id)
|
||||
sibling-heading (util/get-prev-heading heading-parent)]
|
||||
(handler/delete-heading! heading dummy?)
|
||||
(when sibling-heading
|
||||
(when-let [sibling-heading-id (d/attr sibling-heading "headingid")]
|
||||
(handler/edit-heading! (uuid sibling-heading-id) :max format id))))))))
|
||||
|
||||
(defn get-matched-commands
|
||||
[input]
|
||||
(try
|
||||
(let [edit-content (gobj/get input "value")
|
||||
pos (:pos (util/get-caret-pos input))
|
||||
last-slash-caret-pos (:pos @*slash-caret-pos)
|
||||
last-command (and last-slash-caret-pos (subs edit-content last-slash-caret-pos pos))]
|
||||
(when (> pos 0)
|
||||
(or
|
||||
(and (= \/ (nth edit-content (dec pos)))
|
||||
(commands/commands-map))
|
||||
(and last-command
|
||||
(commands/get-matched-commands last-command)))))
|
||||
(catch js/Error e
|
||||
nil)))
|
||||
|
||||
(defn get-matched-block-commands
|
||||
[input]
|
||||
(try
|
||||
(let [edit-content (gobj/get input "value")
|
||||
pos (:pos (util/get-caret-pos input))
|
||||
last-command (subs edit-content
|
||||
(:pos @*angle-bracket-caret-pos)
|
||||
pos)]
|
||||
(when (> pos 0)
|
||||
(or
|
||||
(and (= \< (nth edit-content (dec pos)))
|
||||
(commands/block-commands-map))
|
||||
(and last-command
|
||||
(commands/get-matched-commands
|
||||
last-command
|
||||
(commands/block-commands-map))))))
|
||||
(catch js/Error e
|
||||
nil)))
|
||||
|
||||
(defn in-auto-complete?
|
||||
[input]
|
||||
(or (seq (get-matched-commands input))
|
||||
(state/get-editor-show-page-search)
|
||||
(state/get-editor-show-block-search)
|
||||
(state/get-editor-show-date-picker)))
|
||||
|
||||
(rum/defc absolute-modal < rum/reactive
|
||||
[cp set-default-width? pos]
|
||||
(let [{:keys [top left pos]} (rum/react pos)]
|
||||
[:div.absolute.rounded-md.shadow-lg
|
||||
{:style (merge
|
||||
{:top (+ top 24)
|
||||
:left left
|
||||
:max-height 600
|
||||
:z-index 11}
|
||||
(if set-default-width?
|
||||
{:width 400}))}
|
||||
cp]))
|
||||
|
||||
(rum/defc transition-cp
|
||||
[cp set-default-width? pos]
|
||||
(ui/css-transition
|
||||
{:class-names "fade"
|
||||
:timeout {:enter 500
|
||||
:exit 300}}
|
||||
(absolute-modal cp set-default-width? pos)))
|
||||
|
||||
(rum/defc image-uploader < rum/reactive
|
||||
[id format]
|
||||
[:<>
|
||||
[:input
|
||||
{:id "upload-file"
|
||||
:type "file"
|
||||
:on-change (fn [e]
|
||||
(let [files (.-files (.-target e))]
|
||||
(upload-image id files format *image-uploading? false)))
|
||||
:hidden true}]
|
||||
(when-let [uploading? (util/react *image-uploading?)]
|
||||
(let [processing (util/react *image-uploading-process)]
|
||||
(transition-cp
|
||||
[:div.flex.flex-row.align-center.rounded-md.shadow-sm.bg-base-2.pl-1.pr-1
|
||||
[:span.lds-dual-ring.mr-2]
|
||||
[:span {:style {:margin-top 2}}
|
||||
(util/format "Uploading %s%" (util/format "%2d" processing))]]
|
||||
*slash-caret-pos)))])
|
||||
|
||||
(defn- clear-when-saved!
|
||||
[]
|
||||
(state/set-editor-show-input nil)
|
||||
(state/set-editor-show-date-picker false)
|
||||
(state/set-editor-show-page-search false)
|
||||
(state/set-editor-show-block-search false)
|
||||
(commands/restore-state true))
|
||||
|
||||
(defn- insert-new-heading!
|
||||
[state]
|
||||
(let [{:keys [heading value format id]} (get-state state)
|
||||
heading-id (:heading/uuid heading)
|
||||
heading (or (db/pull [:heading/uuid heading-id])
|
||||
heading)]
|
||||
(set-last-edit-heading! (:heading/uuid heading) value)
|
||||
;; save the current heading and insert a new heading
|
||||
(let [value-with-levels (with-levels value format (:heading/level heading))
|
||||
[_first-heading last-heading _new-heading-content] (handler/insert-new-heading! heading value-with-levels)
|
||||
last-id (:heading/uuid last-heading)]
|
||||
(handler/edit-heading! last-id :max format id)
|
||||
(clear-when-saved!))))
|
||||
|
||||
(defn get-previous-heading-level
|
||||
[current-id]
|
||||
(when-let [input (gdom/getElement current-id)]
|
||||
(when-let [prev-heading (util/get-prev-heading input)]
|
||||
(util/parse-int (d/attr prev-heading "level")))))
|
||||
|
||||
(defn- adjust-heading-level!
|
||||
[state direction]
|
||||
(let [{:keys [heading heading-parent-id value]} (get-state state)
|
||||
format (:heading/format heading)
|
||||
heading-pattern (config/get-heading-pattern format)
|
||||
level (:heading/level heading)
|
||||
previous-level (or (get-previous-heading-level heading-parent-id) 1)
|
||||
[add? remove?] (case direction
|
||||
:left [false true]
|
||||
:right [true false]
|
||||
[(<= level previous-level)
|
||||
(and (> level previous-level)
|
||||
(> level 2))])
|
||||
final-level (cond
|
||||
add? (inc level)
|
||||
remove? (if (> level 2)
|
||||
(dec level)
|
||||
level)
|
||||
:else level)
|
||||
new-value (with-levels value format final-level)]
|
||||
(set-last-edit-heading! (:heading/uuid heading) value)
|
||||
(handler/save-heading-if-changed! heading new-value)))
|
||||
|
||||
(defn- get-input
|
||||
[state]
|
||||
(when-let [input-id (last (:rum/args state))]
|
||||
(gdom/getElement input-id)))
|
||||
|
||||
(defn edit-heading?
|
||||
[state]
|
||||
(some? (:heading (nth (:rum/args state) 1))))
|
||||
|
||||
(rum/defc box < rum/reactive
|
||||
;; TODO: Overwritten by user's configuration
|
||||
(mixins/keyboard-mixin "alt+enter" insert-new-heading! edit-heading? get-input)
|
||||
(mixins/event-mixin
|
||||
(fn [state]
|
||||
(let [{:keys [id format]} (get-state state)
|
||||
input-id id
|
||||
input (gdom/getElement input-id)]
|
||||
(let [{:keys [format heading]} (get-state state)]
|
||||
(mixins/hide-when-esc-or-outside
|
||||
state
|
||||
:on-hide
|
||||
(fn [state e event]
|
||||
(let [{:keys [on-hide format value heading id]} (get-state state)
|
||||
current-edit-id (state/get-edit-input-id)]
|
||||
(state/set-edit-input-id! nil)
|
||||
(when on-hide (on-hide value event))
|
||||
(when (and heading (= current-edit-id id))
|
||||
(state/set-edit-heading! nil))))))
|
||||
(mixins/on-key-down
|
||||
state
|
||||
{
|
||||
;; up
|
||||
38 (fn [state e]
|
||||
(when-not (in-auto-complete? input)
|
||||
(on-up-down state e true)))
|
||||
;; down
|
||||
40 (fn [state e]
|
||||
(when-not (in-auto-complete? input)
|
||||
(on-up-down state e false)))
|
||||
;; backspace
|
||||
8 (fn [state e]
|
||||
(let [node (gdom/getElement input-id)
|
||||
current-pos (:pos (util/get-caret-pos node))
|
||||
value (gobj/get node "value")
|
||||
deleted (and (> current-pos 0)
|
||||
(nth value (dec current-pos)))]
|
||||
(cond
|
||||
(= value "")
|
||||
(delete-heading! state e)
|
||||
|
||||
(and (> current-pos 1)
|
||||
(= (nth value (dec current-pos)) commands/slash))
|
||||
(do
|
||||
(reset! *slash-caret-pos nil)
|
||||
(reset! *show-commands false))
|
||||
|
||||
(and (> current-pos 1)
|
||||
(= (nth value (dec current-pos)) commands/angle-bracket))
|
||||
(do
|
||||
(reset! *angle-bracket-caret-pos nil)
|
||||
(reset! *show-block-commands false))
|
||||
|
||||
;; pair
|
||||
(and
|
||||
deleted
|
||||
(contains?
|
||||
(set (keys autopair-map))
|
||||
deleted)
|
||||
(>= (count value) (inc current-pos))
|
||||
(= (nth value current-pos)
|
||||
(get autopair-map deleted)))
|
||||
|
||||
(do
|
||||
(util/stop e)
|
||||
(commands/delete-pair! id))
|
||||
|
||||
:else
|
||||
nil)))
|
||||
;; tab
|
||||
9 (fn [state e]
|
||||
(when-not (state/get-editor-show-input)
|
||||
(util/stop e)
|
||||
(let [direction (if (gobj/get e "shiftKey") ; shift+tab move to left
|
||||
:left
|
||||
:right)]
|
||||
(adjust-heading-level! state direction))))}
|
||||
(fn [e key-code]
|
||||
(let [key (gobj/get e "key")]
|
||||
(cond
|
||||
(and
|
||||
(contains? (set (keys reversed-autopair-map)) key)
|
||||
(= (get-previous-input-chars input 2) (str key key)))
|
||||
nil
|
||||
|
||||
(and
|
||||
(contains? (set (keys reversed-autopair-map)) key)
|
||||
(or
|
||||
(= (get-previous-input-char input) key)
|
||||
(= (get-current-input-char input) key)))
|
||||
(do
|
||||
(util/stop e)
|
||||
(util/cursor-move-forward input 1))
|
||||
|
||||
(contains? (set (keys autopair-map)) key)
|
||||
(do
|
||||
(util/stop e)
|
||||
(autopair input-id key format nil))
|
||||
|
||||
:else
|
||||
nil))
|
||||
;; (swap! state/state assoc
|
||||
;; :editor/last-saved-cursor nil)
|
||||
))
|
||||
(mixins/on-key-up
|
||||
state
|
||||
{
|
||||
;; /
|
||||
191 (fn [state e]
|
||||
(when-let [matched-commands (seq (get-matched-commands input))]
|
||||
(reset! *slash-caret-pos (util/get-caret-pos input))
|
||||
(reset! *show-commands true)))
|
||||
|
||||
;; <
|
||||
188 (fn [state e]
|
||||
(when-let [matched-commands (seq (get-matched-block-commands input))]
|
||||
(reset! *angle-bracket-caret-pos (util/get-caret-pos input))
|
||||
(reset! *show-block-commands true)))}
|
||||
(fn [e key-code]
|
||||
(let [format (:format (get-state state))]
|
||||
(when (not= key-code 191) ; not /
|
||||
(let [matched-commands (get-matched-commands input)]
|
||||
(if (seq matched-commands)
|
||||
(do
|
||||
(cond
|
||||
(= key-code 9) ;tab
|
||||
(when @*show-commands
|
||||
(util/stop e)
|
||||
(insert-command! input-id
|
||||
(last (first matched-commands))
|
||||
format
|
||||
nil))
|
||||
|
||||
:else
|
||||
(do
|
||||
(reset! *show-commands true)
|
||||
(reset! *matched-commands matched-commands))))
|
||||
(reset! *show-commands false))))
|
||||
(when (not= key-code 188) ; not <
|
||||
(let [matched-block-commands (get-matched-block-commands input)]
|
||||
(if (seq matched-block-commands)
|
||||
(cond
|
||||
(= key-code 9) ;tab
|
||||
(when @*show-block-commands
|
||||
(util/stop e)
|
||||
(insert-command! input-id
|
||||
(last (first matched-block-commands))
|
||||
format
|
||||
{:last-pattern commands/angle-bracket}))
|
||||
|
||||
:else
|
||||
(reset! *matched-block-commands matched-block-commands))
|
||||
(reset! *show-block-commands false))))))))))
|
||||
{:did-mount (fn [state]
|
||||
(let [[content {:keys [heading format dummy? format]} id] (:rum/args state)]
|
||||
(let [content (handler/remove-level-spaces content format)]
|
||||
(state/set-edit-content! id (string/trim (or content "")) true)
|
||||
(handler/restore-cursor-pos! id content dummy?))
|
||||
(when-let [input (gdom/getElement id)]
|
||||
(dnd/subscribe!
|
||||
input
|
||||
:upload-images
|
||||
{:drop (fn [e files]
|
||||
(upload-image id files format *image-uploading? true))})))
|
||||
state)
|
||||
:will-unmount (fn [state]
|
||||
(let [{:keys [id value format heading]} (get-state state)]
|
||||
(when-let [input (gdom/getElement id)]
|
||||
(dnd/unsubscribe!
|
||||
input
|
||||
:upload-images))
|
||||
(when (and heading (not= value ""))
|
||||
(let [new-value (with-levels
|
||||
value
|
||||
format
|
||||
(:heading/level heading))]
|
||||
(let [cache [(:heading/uuid heading) value]]
|
||||
(when (not= @*last-edit-heading cache)
|
||||
(handler/save-heading-if-changed! heading new-value)
|
||||
(reset! *last-edit-heading cache)))))
|
||||
(clear-when-saved!))
|
||||
state)}
|
||||
[content {:keys [on-hide dummy? node format heading]
|
||||
:or {dummy? false}
|
||||
:as option} id]
|
||||
(let [edit-content (state/sub [:editor/content id])]
|
||||
[:div.editor {:style {:position "relative"
|
||||
:display "flex"
|
||||
:flex "1 1 0%"}
|
||||
:class (if heading "heading-editor" "non-heading-editor")}
|
||||
(ui/textarea
|
||||
{:id id
|
||||
:value (or edit-content content)
|
||||
:on-change (fn [e]
|
||||
(let [value (util/evalue e)]
|
||||
(state/set-edit-content! id value false)))
|
||||
:auto-focus true})
|
||||
(transition-cp
|
||||
(commands id format)
|
||||
true
|
||||
*slash-caret-pos)
|
||||
|
||||
(transition-cp
|
||||
(block-commands id format)
|
||||
true
|
||||
*angle-bracket-caret-pos)
|
||||
|
||||
(transition-cp
|
||||
(page-search id format)
|
||||
true
|
||||
*slash-caret-pos)
|
||||
|
||||
(transition-cp
|
||||
(block-search id format)
|
||||
true
|
||||
*slash-caret-pos)
|
||||
|
||||
(transition-cp
|
||||
(date-picker id format)
|
||||
false
|
||||
*slash-caret-pos)
|
||||
|
||||
(transition-cp
|
||||
(input id
|
||||
(fn [{:keys [link label]} pos]
|
||||
(if (and (string/blank? link)
|
||||
(string/blank? label))
|
||||
nil
|
||||
(insert-command! id
|
||||
(util/format "[[%s][%s]]"
|
||||
(or link "")
|
||||
(or label ""))
|
||||
format
|
||||
{:last-pattern (str commands/slash "link")}))
|
||||
(state/set-editor-show-input nil)
|
||||
(when-let [saved-cursor (get @state/state :editor/last-saved-cursor)]
|
||||
(when-let [input (gdom/getElement id)]
|
||||
(.focus input)
|
||||
(util/move-cursor-to input saved-cursor)))))
|
||||
true
|
||||
*slash-caret-pos)
|
||||
|
||||
(when format
|
||||
(image-uploader id format))]))
|
|
@ -0,0 +1,399 @@
|
|||
(ns frontend.components.editor
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.handler :as handler]
|
||||
[frontend.util :as util]
|
||||
[frontend.state :as state]
|
||||
[frontend.mixins :as mixins]
|
||||
[frontend.image :as image]
|
||||
[frontend.ui :as ui]
|
||||
[frontend.db :as db]
|
||||
[dommy.core :as d]
|
||||
[goog.object :as gobj]
|
||||
[goog.dom :as gdom]
|
||||
[clojure.string :as string]
|
||||
[frontend.commands :as commands]
|
||||
[medley.core :as medley]
|
||||
[cljs-time.core :as t]
|
||||
[cljs-time.coerce :as tc]))
|
||||
|
||||
(defonce *show-commands (atom false))
|
||||
(defonce *matched-commands (atom nil))
|
||||
(defonce *slash-caret-pos (atom nil))
|
||||
|
||||
(defn- insert-command!
|
||||
[id command-output]
|
||||
(cond
|
||||
;; replace string
|
||||
(string? command-output)
|
||||
(commands/insert! id command-output *slash-caret-pos *show-commands *matched-commands)
|
||||
|
||||
;; steps
|
||||
(vector? command-output)
|
||||
(commands/handle-steps command-output *show-commands *matched-commands)
|
||||
|
||||
:else
|
||||
nil))
|
||||
|
||||
(rum/defc commands < rum/reactive
|
||||
{:will-mount (fn [state]
|
||||
(reset! *matched-commands (commands/commands-map))
|
||||
state)}
|
||||
[id]
|
||||
(when (rum/react *show-commands)
|
||||
(let [matched (rum/react *matched-commands)]
|
||||
(ui/auto-complete
|
||||
(map first matched)
|
||||
(fn [chosen]
|
||||
(insert-command! id (get (into {} matched) chosen)))))))
|
||||
|
||||
(rum/defc page-search < rum/reactive
|
||||
[id]
|
||||
(when (state/sub :editor/show-page-search?)
|
||||
(let [{:keys [pos]} (rum/react *slash-caret-pos)
|
||||
input (gdom/getElement id)
|
||||
current-pos (:pos (util/get-caret-pos input))
|
||||
edit-content (state/sub :edit-content)
|
||||
q (subs edit-content (inc pos) current-pos)
|
||||
matched-pages (when-not (string/blank? q)
|
||||
(let [pages (db/get-pages (state/get-current-repo))]
|
||||
(filter
|
||||
(fn [page]
|
||||
(string/index-of
|
||||
(string/lower-case page)
|
||||
(string/lower-case q)))
|
||||
pages)))]
|
||||
(ui/auto-complete
|
||||
matched-pages
|
||||
(fn [chosen click?]
|
||||
(commands/insert! id (str "[[" chosen)
|
||||
*slash-caret-pos
|
||||
*show-commands
|
||||
*matched-commands
|
||||
:last-pattern "[["
|
||||
:forward-pos 2)
|
||||
(state/set-editor-show-page-search false))
|
||||
:empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a page"]))))
|
||||
|
||||
(rum/defc date-picker < rum/reactive
|
||||
[id]
|
||||
(when (state/sub :editor/show-date-picker?)
|
||||
(ui/datepicker
|
||||
(t/today)
|
||||
{:on-change
|
||||
(fn [e date]
|
||||
(util/stop e)
|
||||
(let [journal (util/journal-name (tc/to-date date))]
|
||||
;; similar to page reference
|
||||
(commands/insert! id (str "[[" journal)
|
||||
*slash-caret-pos
|
||||
*show-commands
|
||||
*matched-commands
|
||||
:last-pattern "[["
|
||||
:forward-pos 2)
|
||||
(state/set-editor-show-date-picker false)))})))
|
||||
|
||||
(rum/defcs input < rum/reactive
|
||||
(rum/local {} ::input-value)
|
||||
(mixins/event-mixin
|
||||
(fn [state]
|
||||
(mixins/on-key-down
|
||||
state
|
||||
{
|
||||
;; enter
|
||||
13 (fn [state e]
|
||||
(let [input-value (get state ::input-value)]
|
||||
(when (seq @input-value)
|
||||
;; no new line input
|
||||
(util/stop e)
|
||||
(let [[_id on-submit] (:rum/args state)
|
||||
{:keys [pos]} @*slash-caret-pos]
|
||||
(on-submit @input-value pos))
|
||||
(reset! input-value nil))))}
|
||||
nil)))
|
||||
{:did-update
|
||||
(fn [state]
|
||||
(when-let [show-input (state/get-editor-show-input)]
|
||||
(let [id (str "modal-input-"
|
||||
(name (:id (first show-input))))
|
||||
first-input (gdom/getElement id)]
|
||||
(when (and first-input
|
||||
(not (d/has-class? first-input "focused")))
|
||||
(.focus first-input)
|
||||
(d/add-class! first-input "focused"))))
|
||||
state)}
|
||||
[state id on-submit]
|
||||
(when-let [input-option (state/sub :editor/show-input)]
|
||||
(let [{:keys [pos]} (rum/react *slash-caret-pos)
|
||||
input-value (get state ::input-value)]
|
||||
(when (seq input-option)
|
||||
[:div.p-2.mt-2.mb-2.rounded-md.shadow-sm {:style {:background "#d3d3d3"}},
|
||||
(for [{:keys [id] :as input-item} input-option]
|
||||
[:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5.mb-1
|
||||
(merge
|
||||
{:key (str "modal-input-" (name id))
|
||||
:id (str "modal-input-" (name id))
|
||||
:value (get @input-value id "")
|
||||
:on-change (fn [e]
|
||||
(swap! input-value assoc id (util/evalue e)))
|
||||
:auto-complete "off"}
|
||||
(dissoc input-item :id))])
|
||||
(ui/button
|
||||
"Submit"
|
||||
(fn [e]
|
||||
(util/stop e)
|
||||
(on-submit @input-value pos)))]))))
|
||||
|
||||
(defn get-state
|
||||
[state]
|
||||
(let [[_ {:keys [on-hide dummy?]} id] (:rum/args state)
|
||||
node (gdom/getElement id)
|
||||
value (gobj/get node "value")
|
||||
pos (gobj/get node "selectionStart")]
|
||||
{:on-hide on-hide
|
||||
:dummy? dummy?
|
||||
:id id
|
||||
:node node
|
||||
:value value
|
||||
:pos pos}))
|
||||
|
||||
(defn on-up-down
|
||||
[state e up?]
|
||||
(let [{:keys [id dummy? on-hide value pos]} (get-state state)
|
||||
heading? (string/starts-with? id "edit-heading-")
|
||||
element (gdom/getElement id)
|
||||
line-height (util/get-textarea-line-height element)]
|
||||
(when (and heading?
|
||||
(or (and up? (util/textarea-cursor-first-row? element line-height))
|
||||
(and (not up?) (util/textarea-cursor-end-row? element line-height))))
|
||||
(util/stop e)
|
||||
(let [f (if up? gdom/getPreviousElementSibling gdom/getNextElementSibling)
|
||||
heading-id (string/replace id "edit-heading-" "")
|
||||
heading-parent (str "ls-heading-parent-" heading-id)
|
||||
sibling-heading (f (gdom/getElement heading-parent))
|
||||
id (gobj/get sibling-heading "id")]
|
||||
(when id
|
||||
(let [id (uuid (string/replace id "ls-heading-parent-" ""))]
|
||||
(on-hide value)
|
||||
;; FIXME: refactor later
|
||||
;; (let [heading (db/entity [:heading/uuid (uuid heading-id)])]
|
||||
;; (handler/save-heading-if-changed! heading value))
|
||||
(handler/edit-heading! id pos)))))))
|
||||
|
||||
(defn on-backspace
|
||||
[state e]
|
||||
(let [{:keys [id dummy? value on-hide pos]} (get-state state)
|
||||
heading? (string/starts-with? id "edit-heading-")]
|
||||
(when (and heading? (= value ""))
|
||||
(util/stop e)
|
||||
;; delete heading, edit previous heading
|
||||
(let [heading-id (string/replace id "edit-heading-" "")
|
||||
heading (db/entity [:heading/uuid (uuid heading-id)])
|
||||
heading-parent (str "ls-heading-parent-" heading-id)
|
||||
heading-parent (gdom/getElement heading-parent)
|
||||
current-idx (util/parse-int (gobj/get heading-parent "idx"))
|
||||
sibling-heading (gdom/getPreviousElementSibling heading-parent)
|
||||
id (gobj/get sibling-heading "id")]
|
||||
|
||||
(let [heading (db/entity [:heading/uuid (uuid heading-id)])]
|
||||
(handler/delete-heading! heading dummy?))
|
||||
|
||||
(when id
|
||||
(let [id (uuid (string/replace id "ls-heading-parent-" ""))]
|
||||
(handler/edit-heading! id :max)))))))
|
||||
|
||||
(defn get-matched-commands
|
||||
[input]
|
||||
(try
|
||||
(let [edit-content (gobj/get input "value")
|
||||
pos (:pos (util/get-caret-pos input))
|
||||
last-command (subs edit-content
|
||||
(:pos @*slash-caret-pos)
|
||||
pos)]
|
||||
(when (> pos 0)
|
||||
(or
|
||||
(and (= \/ (nth edit-content (dec pos)))
|
||||
;; (or
|
||||
;; (and
|
||||
;; (>= (count edit-content) 2)
|
||||
;; (contains? #{" " "\r" "\n" "\t"} (nth edit-content (- (count edit-content) 2))))
|
||||
;; (= edit-content "/"))
|
||||
(commands/commands-map))
|
||||
(and last-command
|
||||
(commands/get-matched-commands last-command)))))
|
||||
(catch js/Error e
|
||||
nil)))
|
||||
|
||||
(defn in-auto-complete?
|
||||
[input]
|
||||
(or (seq (get-matched-commands input))
|
||||
(state/get-editor-show-page-search)
|
||||
(state/get-editor-show-date-picker)))
|
||||
|
||||
(rum/defc absolute-modal < rum/reactive
|
||||
[cp set-default-width?]
|
||||
(let [{:keys [top left pos]} (rum/react *slash-caret-pos)]
|
||||
[:div.absolute.rounded-md.shadow-lg
|
||||
{:style (merge
|
||||
{:top (+ top 20)
|
||||
:left left}
|
||||
(if set-default-width?
|
||||
{:width 400}))}
|
||||
cp]))
|
||||
|
||||
(rum/defc transition-cp
|
||||
[cp set-default-width?]
|
||||
(ui/css-transition
|
||||
{:class-names "fade"
|
||||
:timeout {:enter 500
|
||||
:exit 300}}
|
||||
(absolute-modal cp set-default-width?)))
|
||||
|
||||
(rum/defc box < rum/reactive
|
||||
(mixins/event-mixin
|
||||
(fn [state]
|
||||
(let [input-id (last (:rum/args state))
|
||||
input (gdom/getElement input-id)]
|
||||
(mixins/hide-when-esc-or-outside
|
||||
state
|
||||
:on-hide (fn []
|
||||
(let [{:keys [value on-hide]} (get-state state)]
|
||||
(on-hide value)
|
||||
(state/set-editor-show-input nil)
|
||||
(state/set-editor-show-date-picker false)
|
||||
(state/set-editor-show-page-search false)
|
||||
(state/set-edit-input-id! nil)
|
||||
(reset! *slash-caret-pos nil)
|
||||
(reset! *show-commands false)
|
||||
(reset! *matched-commands (commands/commands-map)))))
|
||||
(mixins/on-key-down
|
||||
state
|
||||
{
|
||||
;; up
|
||||
38 (fn [state e]
|
||||
(when-not (in-auto-complete? input)
|
||||
(on-up-down state e true)))
|
||||
;; down
|
||||
40 (fn [state e]
|
||||
(when-not (in-auto-complete? input)
|
||||
(on-up-down state e false)))
|
||||
;; backspace
|
||||
8 (fn [state e]
|
||||
(let [node (gdom/getElement input-id)
|
||||
current-pos (:pos (util/get-caret-pos node))
|
||||
value (gobj/get node "value")]
|
||||
(when (and (> current-pos 1)
|
||||
(= (nth value (dec current-pos)) "/"))
|
||||
(reset! *slash-caret-pos nil)
|
||||
(reset! *show-commands false))))
|
||||
}
|
||||
(fn [e key-code]
|
||||
(swap! state/state assoc
|
||||
:editor/last-saved-cursor nil)))
|
||||
(mixins/on-key-up
|
||||
state
|
||||
{
|
||||
;; /
|
||||
191 (fn [state e]
|
||||
(when-let [matched-commands (seq (get-matched-commands input))]
|
||||
(reset! *show-commands true)
|
||||
(reset! *slash-caret-pos (util/get-caret-pos input))))
|
||||
;; backspace
|
||||
8 on-backspace}
|
||||
(fn [e key-code]
|
||||
(when (not= key-code 191) ; not /
|
||||
(let [matched-commands (get-matched-commands input)]
|
||||
(if (seq matched-commands)
|
||||
(if (= key-code 9) ;tab
|
||||
(do
|
||||
(util/stop e)
|
||||
(insert-command! input-id (last (first matched-commands))))
|
||||
(do
|
||||
(reset! *matched-commands matched-commands)
|
||||
(reset! *show-commands true)))
|
||||
(reset! *show-commands false)))))))))
|
||||
{:init (fn [state _props]
|
||||
(let [[content {:keys [dummy?]}] (:rum/args state)]
|
||||
(state/set-edit-content!
|
||||
(if dummy?
|
||||
(string/triml content)
|
||||
(string/trim content)))
|
||||
(swap! state/state assoc
|
||||
:editor/last-saved-cursor nil))
|
||||
state)
|
||||
:did-mount (fn [state]
|
||||
(let [[content opts id] (:rum/args state)]
|
||||
(handler/restore-cursor-pos! id content (:dummy? opts)))
|
||||
state)
|
||||
:did-update (fn [state]
|
||||
(when-let [saved-cursor (get @state/state :editor/last-saved-cursor)]
|
||||
(let [[_content _opts id] (:rum/args state)
|
||||
input (gdom/getElement id)]
|
||||
(when input
|
||||
(.focus input)
|
||||
(util/move-cursor-to input saved-cursor))))
|
||||
state)}
|
||||
[content {:keys [on-hide dummy? node]
|
||||
:or {dummy? false}} id]
|
||||
(let [value (state/sub :edit-content)]
|
||||
[:div.editor {:style {:position "relative"
|
||||
:display "flex"
|
||||
:flex "1 1 0%"}}
|
||||
(ui/textarea
|
||||
{:id id
|
||||
:on-change (fn [e]
|
||||
(state/set-edit-content! (util/evalue e)))
|
||||
:value value
|
||||
:auto-focus true
|
||||
:style {:border "none"
|
||||
:border-radius 0
|
||||
:background "transparent"
|
||||
:padding 0}})
|
||||
(transition-cp
|
||||
(commands id)
|
||||
true)
|
||||
|
||||
(transition-cp
|
||||
(page-search id)
|
||||
true)
|
||||
|
||||
(transition-cp
|
||||
(date-picker id)
|
||||
false)
|
||||
|
||||
(transition-cp
|
||||
(input id
|
||||
(fn [{:keys [link label]} pos]
|
||||
(when-not (and (string/blank? link)
|
||||
(string/blank? label))
|
||||
(commands/insert! id
|
||||
(util/format "[[%s][%s]]"
|
||||
(or link "")
|
||||
(or label ""))
|
||||
*slash-caret-pos
|
||||
*show-commands
|
||||
*matched-commands
|
||||
:last-pattern "[["
|
||||
:postfix-fn (fn [s]
|
||||
(util/replace-first "][]]" s ""))))
|
||||
(state/set-editor-show-input nil)))
|
||||
true)
|
||||
|
||||
[:input
|
||||
{:id "upload-file"
|
||||
:type "file"
|
||||
:on-change (fn [e]
|
||||
(let [files (.-files (.-target e))]
|
||||
(image/upload
|
||||
files
|
||||
(fn [file file-name file-type]
|
||||
(handler/request-presigned-url
|
||||
file file-name file-type
|
||||
(fn [signed-url]
|
||||
(commands/insert! id
|
||||
(util/format "[[%s][%s]]"
|
||||
signed-url
|
||||
file-name)
|
||||
*slash-caret-pos
|
||||
*show-commands
|
||||
*matched-commands)))))))
|
||||
:hidden true}]]))
|
|
@ -0,0 +1,21 @@
|
|||
(ns frontend.components.heading
|
||||
(:require [frontend.db :as db]
|
||||
[frontend.handler :as handler]
|
||||
[clojure.string :as string]))
|
||||
|
||||
(defn heading-parents
|
||||
[repo heading-id format]
|
||||
(let [parents (db/get-heading-parents repo heading-id 3)]
|
||||
(when (seq parents)
|
||||
[:div.heading-parents.mt-4.mb-2.flex-row.flex
|
||||
(for [[id content] parents]
|
||||
(let [title (->> (take 24
|
||||
(-> (string/split content #"\n")
|
||||
first
|
||||
(handler/remove-level-spaces format)))
|
||||
(apply str))]
|
||||
(when-not (string/blank? title)
|
||||
[:div
|
||||
[:span.mx-1 ">"]
|
||||
[:a {:href (str "/page/" id)}
|
||||
title]])))])))
|
|
@ -1094,8 +1094,9 @@
|
|||
|
||||
(rum/defc headings-container < rum/static
|
||||
[headings config]
|
||||
[:div.headings-container {:style {:margin-left -24}}
|
||||
(build-headings headings config)])
|
||||
(let [headings (map #(dissoc % :heading/children) headings)]
|
||||
[:div.headings-container {:style {:margin-left -24}}
|
||||
(build-headings headings config)]))
|
||||
|
||||
;; headers to hiccup
|
||||
(rum/defc ->hiccup < rum/reactive
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
[clojure.string :as string]
|
||||
[frontend.db :as db]
|
||||
[frontend.components.hiccup :as hiccup]
|
||||
[frontend.components.heading :as heading]
|
||||
[frontend.components.reference :as reference]
|
||||
[frontend.components.svg :as svg]
|
||||
[frontend.extensions.graph-2d :as graph-2d]
|
||||
|
@ -175,6 +176,9 @@
|
|||
[:a {:href (str "/page/" (util/encode-str item))}
|
||||
[:span.mr-1 (util/capitalize-all item)]])])))
|
||||
|
||||
(when heading?
|
||||
(heading/heading-parents repo heading-id format))
|
||||
|
||||
;; headings
|
||||
(rum/with-key
|
||||
(page-headings-cp repo page raw-page-headings file-path page-name encoded-page-name sidebar? journal? heading? format)
|
||||
|
|
|
@ -34,9 +34,13 @@
|
|||
(heading-cp repo idx (:heading block-data))]]
|
||||
|
||||
:heading
|
||||
["Block"
|
||||
[:div.ml-2
|
||||
(heading-cp repo idx block-data)]]
|
||||
(let [page-name (:page/name
|
||||
(db/entity repo
|
||||
(get-in block-data [:heading/page :db/id])))]
|
||||
[[:a {:href (str "/page/" (util/url-encode page-name))}
|
||||
(util/capitalize-all page-name)]
|
||||
[:div.ml-2
|
||||
(heading-cp repo idx block-data)]])
|
||||
|
||||
:page
|
||||
(let [page-name (get-in block-data [:page :page/name])]
|
||||
|
|
|
@ -174,8 +174,8 @@
|
|||
:heading/body {}
|
||||
:heading/pre-heading? {}
|
||||
:heading/collapsed? {}
|
||||
;; :heading/children {:db/valueType :db.type/ref
|
||||
;; :db/cardinality :db.cardinality/many}})
|
||||
:heading/children {:db/cardinality :db.cardinality/many}
|
||||
|
||||
:tag/name {:db/unique :db.unique/identity}})
|
||||
|
||||
;; transit serialization
|
||||
|
@ -632,14 +632,14 @@
|
|||
[repo]
|
||||
(when-let [conn (get-conn repo)]
|
||||
(->> (q repo [:files] {:use-cache? false}
|
||||
'[:find ?path ?modified-at
|
||||
:where
|
||||
[?file :file/path ?path]
|
||||
[(get-else $ ?file :file/last-modified-at 0) ?modified-at]])
|
||||
(react)
|
||||
(seq)
|
||||
(sort-by last)
|
||||
(reverse))))
|
||||
'[:find ?path ?modified-at
|
||||
:where
|
||||
[?file :file/path ?path]
|
||||
[(get-else $ ?file :file/last-modified-at 0) ?modified-at]])
|
||||
(react)
|
||||
(seq)
|
||||
(sort-by last)
|
||||
(reverse))))
|
||||
|
||||
(defn get-files-headings
|
||||
[repo-url paths]
|
||||
|
@ -1608,6 +1608,54 @@
|
|||
[?h :heading/collapsed? true]]
|
||||
(get-conn)))
|
||||
|
||||
;; recursive query might be slow, need benchmarks
|
||||
;; Could replace this with a recursive call, see below
|
||||
;; (defn get-heading-parents
|
||||
;; [repo heading-id depth]
|
||||
;; (when-let [conn (get-conn repo)]
|
||||
;; (let [ids (->> (d/q
|
||||
;; '[:find ?e2
|
||||
;; :in $ ?e1 %
|
||||
;; :where (parent ?e2 ?e1)]
|
||||
;; conn
|
||||
;; heading-id
|
||||
;; ;; recursive rules
|
||||
;; '[[(parent ?e2 ?e1)
|
||||
;; [?e2 :heading/children ?e1]]
|
||||
;; [(parent ?e2 ?e1)
|
||||
;; [?t :heading/children ?e1]
|
||||
;; [?t :heading/uuid ?tid]
|
||||
;; (parent ?e2 ?tid)]])
|
||||
;; (seq-flatten))]
|
||||
;; (when (seq ids)
|
||||
;; (d/pull-many conn '[:heading/uuid :heading/title] ids)))))
|
||||
|
||||
(defn get-heading-parent
|
||||
[conn heading-id]
|
||||
(-> (d/q
|
||||
'[:find ?e2-id ?e2-content
|
||||
:in $ ?e1 %
|
||||
:where
|
||||
[?e2 :heading/children ?e1]
|
||||
[?e2 :heading/content ?e2-content]
|
||||
[?e2 :heading/uuid ?e2-id]]
|
||||
conn
|
||||
heading-id)
|
||||
first))
|
||||
|
||||
;; non recursive query
|
||||
(defn get-heading-parents
|
||||
[repo heading-id depth]
|
||||
(when-let [conn (get-conn repo)]
|
||||
(loop [heading-id heading-id
|
||||
parents (list)
|
||||
d 1]
|
||||
(if (> d depth)
|
||||
parents
|
||||
(if-let [parent (get-heading-parent conn heading-id)]
|
||||
(recur (first parent) (conj parents parent) (inc d))
|
||||
parents)))))
|
||||
|
||||
(comment
|
||||
|
||||
|
||||
|
|
|
@ -120,14 +120,7 @@
|
|||
(defn safe-headings
|
||||
[headings]
|
||||
(map (fn [heading]
|
||||
(let [id (or (when-let [custom-id (get-in heading [:properties "CUSTOM_ID"])]
|
||||
(when (util/uuid-string? custom-id)
|
||||
(uuid custom-id)))
|
||||
;; editing old headings
|
||||
(:heading/uuid heading)
|
||||
(d/squuid))
|
||||
heading (util/remove-nils heading)
|
||||
heading (assoc heading :heading/uuid id)]
|
||||
(let [heading (util/remove-nils heading)]
|
||||
(medley/map-keys
|
||||
(fn [k] (keyword "heading" k))
|
||||
heading)))
|
||||
|
@ -147,7 +140,9 @@
|
|||
blocks (reverse blocks)
|
||||
timestamps {}
|
||||
properties {}
|
||||
last-pos last-pos]
|
||||
last-pos last-pos
|
||||
last-level 1000
|
||||
children []]
|
||||
(if (seq blocks)
|
||||
(let [block (first blocks)
|
||||
level (:level (second block))]
|
||||
|
@ -155,28 +150,47 @@
|
|||
(paragraph-timestamp-block? block)
|
||||
(let [timestamp (extract-timestamp block)
|
||||
timestamps' (conj timestamps timestamp)]
|
||||
(recur headings heading-body (rest blocks) timestamps' properties last-pos))
|
||||
(recur headings heading-body (rest blocks) timestamps' properties last-pos last-level children))
|
||||
|
||||
(properties-block? block)
|
||||
(let [properties (extract-properties block)]
|
||||
(recur headings heading-body (rest blocks) timestamps properties last-pos))
|
||||
(recur headings heading-body (rest blocks) timestamps properties last-pos last-level children))
|
||||
|
||||
(heading-block? block)
|
||||
(let [id (or (when-let [custom-id (get-in properties [:properties "CUSTOM_ID"])]
|
||||
(when (util/uuid-string? custom-id)
|
||||
(uuid custom-id)))
|
||||
(d/squuid))
|
||||
heading (second block)
|
||||
level (:level heading)
|
||||
[children current-heading-children]
|
||||
(cond
|
||||
(>= level last-level)
|
||||
[(conj children [id level])
|
||||
#{}]
|
||||
|
||||
(let [heading (-> (assoc (second block)
|
||||
(< level last-level)
|
||||
(let [current-heading-children (set (->> (filter #(< level (second %)) children)
|
||||
(map first)))
|
||||
others (vec (remove #(< level (second %)) children))]
|
||||
[(conj others [id level])
|
||||
current-heading-children]))
|
||||
heading (-> (assoc heading
|
||||
:uuid id
|
||||
:body (vec (reverse heading-body))
|
||||
:timestamps timestamps
|
||||
:properties (:properties properties)
|
||||
:properties-meta (dissoc properties :properties))
|
||||
:properties-meta (dissoc properties :properties)
|
||||
:children (or current-heading-children []))
|
||||
(assoc-in [:meta :end-pos] last-pos))
|
||||
heading (collect-heading-tags heading)
|
||||
heading (with-refs heading)
|
||||
last-pos' (get-in heading [:meta :pos])]
|
||||
(recur (conj headings heading) [] (rest blocks) {} {} last-pos'))
|
||||
(recur (conj headings heading) [] (rest blocks) {} {} last-pos' (:level heading) children))
|
||||
|
||||
:else
|
||||
(let [heading-body' (conj heading-body block)]
|
||||
(recur headings heading-body' (rest blocks) timestamps properties last-pos))))
|
||||
(recur headings heading-body' (rest blocks) timestamps properties last-pos last-level children))))
|
||||
(-> (reverse headings)
|
||||
safe-headings)))]
|
||||
(let [first-heading (first headings)
|
||||
|
@ -202,31 +216,6 @@
|
|||
headings)
|
||||
headings))))
|
||||
|
||||
;; marker: DOING | IN-PROGRESS > TODO > WAITING | WAIT > DONE > CANCELED | CANCELLED
|
||||
;; priority: A > B > C
|
||||
(defn sort-tasks
|
||||
[headings]
|
||||
(let [markers ["NOW" "LATER" "DOING" "IN-PROGRESS" "TODO" "WAITING" "WAIT" "DONE" "CANCELED" "CANCELLED"]
|
||||
markers (zipmap markers (reverse (range 1 (count markers))))
|
||||
priorities ["A" "B" "C" "D" "E" "F" "G"]
|
||||
priorities (zipmap priorities (reverse (range 1 (count priorities))))]
|
||||
(sort (fn [t1 t2]
|
||||
(let [m1 (get markers (:heading/marker t1) 0)
|
||||
m2 (get markers (:heading/marker t2) 0)
|
||||
p1 (get priorities (:heading/priority t1) 0)
|
||||
p2 (get priorities (:heading/priority t2) 0)]
|
||||
(cond
|
||||
(and (= m1 m2)
|
||||
(= p1 p2))
|
||||
(compare (str (:heading/title t1))
|
||||
(str (:heading/title t2)))
|
||||
|
||||
(= m1 m2)
|
||||
(> p1 p2)
|
||||
:else
|
||||
(> m1 m2))))
|
||||
headings)))
|
||||
|
||||
(defn parse-heading
|
||||
[{:heading/keys [uuid content meta file page] :as heading} format]
|
||||
(when-not (string/blank? content)
|
||||
|
@ -281,3 +270,27 @@
|
|||
(let [pattern (config/get-heading-pattern format)
|
||||
prefix (if pre-heading? "" (str (apply str (repeat level pattern)) " "))]
|
||||
(str prefix (string/triml text))))
|
||||
|
||||
(comment
|
||||
(defn sort-tasks
|
||||
[headings]
|
||||
(let [markers ["NOW" "LATER" "DOING" "IN-PROGRESS" "TODO" "WAITING" "WAIT" "DONE" "CANCELED" "CANCELLED"]
|
||||
markers (zipmap markers (reverse (range 1 (count markers))))
|
||||
priorities ["A" "B" "C" "D" "E" "F" "G"]
|
||||
priorities (zipmap priorities (reverse (range 1 (count priorities))))]
|
||||
(sort (fn [t1 t2]
|
||||
(let [m1 (get markers (:heading/marker t1) 0)
|
||||
m2 (get markers (:heading/marker t2) 0)
|
||||
p1 (get priorities (:heading/priority t1) 0)
|
||||
p2 (get priorities (:heading/priority t2) 0)]
|
||||
(cond
|
||||
(and (= m1 m2)
|
||||
(= p1 p2))
|
||||
(compare (str (:heading/title t1))
|
||||
(str (:heading/title t2)))
|
||||
|
||||
(= m1 m2)
|
||||
(> p1 p2)
|
||||
:else
|
||||
(> m1 m2))))
|
||||
headings))))
|
||||
|
|
Loading…
Reference in New Issue