Add :heading/children

pull/645/head
Tienson Qin 2020-07-09 22:27:53 +08:00
parent c748f83617
commit fe96858e19
10 changed files with 1331 additions and 55 deletions

View File

@ -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)

View File

@ -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

View File

@ -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))]))

View File

@ -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}]]))

View File

@ -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]])))])))

View File

@ -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

View File

@ -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)

View File

@ -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])]

View File

@ -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

View File

@ -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))))