123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807 |
- (ns frontend.components.editor
- (:require [clojure.string :as string]
- [dommy.core :as dom]
- [frontend.commands :as commands :refer [*matched-commands]]
- [frontend.components.file-based.datetime :as datetime-comp]
- [frontend.components.search :as search]
- [frontend.components.svg :as svg]
- [frontend.config :as config]
- [frontend.context.i18n :refer [t]]
- [frontend.date :as date]
- [frontend.db :as db]
- [frontend.db.model :as db-model]
- [frontend.extensions.zotero :as zotero]
- [frontend.handler.block :as block-handler]
- [frontend.handler.editor :as editor-handler :refer [get-state]]
- [frontend.handler.editor.lifecycle :as lifecycle]
- [frontend.handler.page :as page-handler]
- [frontend.handler.paste :as paste-handler]
- [frontend.handler.property.util :as pu]
- [frontend.handler.search :as search-handler]
- [frontend.mixins :as mixins]
- [frontend.search :refer [fuzzy-search]]
- [frontend.state :as state]
- [frontend.ui :as ui]
- [frontend.util :as util]
- [frontend.util.cursor :as cursor]
- [frontend.util.keycode :as keycode]
- [goog.dom :as gdom]
- [goog.string :as gstring]
- [logseq.common.util :as common-util]
- [logseq.common.util.page-ref :as page-ref]
- [logseq.db :as ldb]
- [logseq.db.frontend.class :as db-class]
- [logseq.graph-parser.property :as gp-property]
- [logseq.shui.hooks :as hooks]
- [logseq.shui.ui :as shui]
- [promesa.core :as p]
- [react-draggable]
- [rum.core :as rum]))
- (defn filter-commands
- [page? commands]
- (if page?
- (filter (fn [item]
- (or
- (= "Add new property" (first item))
- (when (= (count item) 5)
- (contains? #{"TASK" "PRIORITY"} (last item))))) commands)
- commands))
- (rum/defcs commands < rum/reactive
- (rum/local [] ::matched-commands)
- [s id format]
- (let [matched' (util/react *matched-commands)
- *matched (::matched-commands s)
- _ (when (state/get-editor-action)
- (reset! *matched matched'))
- page? (db/page? (db/entity (:db/id (state/get-edit-block))))
- matched (filter-commands page? @*matched)]
- (ui/auto-complete
- matched
- {:get-group-name
- (fn [item]
- (when (= (count item) 5) (last item)))
- :item-render
- (fn [item]
- (let [command-name (first item)
- command-doc (get item 2)
- plugin-id (get-in item [1 1 1 :pid])
- doc (when (state/show-command-doc?) command-doc)
- options (some-> item (get 3))
- icon-name (some-> (if (map? options) (:icon options) options) (name))
- command-name (if icon-name
- [:span.flex.items-center.gap-1
- (shui/tabler-icon icon-name)
- [:strong.font-normal command-name]]
- command-name)]
- (cond
- (or plugin-id (vector? doc))
- [:div.has-help
- {:title plugin-id}
- command-name
- (when doc (ui/tooltip [:small (svg/help-circle)] doc))]
- (string? doc)
- [:div {:title doc}
- command-name]
- :else
- [:div command-name])))
- :on-chosen
- (fn [chosen-item]
- (let [command (first chosen-item)]
- (reset! commands/*current-command command)
- (let [command-steps (get (into {} matched) command)
- restore-slash? (or
- (contains? #{"Today" "Yesterday" "Tomorrow" "Current time"} command)
- (and
- (not (fn? command-steps))
- (not (contains? (set (map first command-steps)) :editor/input))
- (not (contains? #{"Date picker" "Template" "Deadline" "Scheduled" "Upload an image"} command))))]
- (editor-handler/insert-command! id command-steps
- format
- {:restore? restore-slash?
- :command command}))))
- :class
- "cp__commands-slash"})))
- (defn- page-on-chosen-handler
- [embed? input id q pos format]
- (if embed?
- (fn [chosen-item _e]
- (let [value (.-value input)
- value' (str (common-util/safe-subs value 0 q)
- (common-util/safe-subs value (+ (count q) 4 pos)))]
- (state/set-edit-content! (.-id input) value')
- (state/clear-editor-action!)
- (p/let [page (db/get-page chosen-item)
- _ (when-not page (page-handler/<create! chosen-item {:redirect? false
- :create-first-block? false}))
- page' (db/get-page chosen-item)
- current-block (state/get-edit-block)]
- (editor-handler/api-insert-new-block! chosen-item
- {:block-uuid (:block/uuid current-block)
- :sibling? true
- :replace-empty-target? true
- :other-attrs {:block/link (:db/id page')}}))))
- (page-handler/on-chosen-handler input id pos format)))
- (rum/defc page-search-aux
- [id format embed? db-tag? q current-pos input pos]
- (let [db-based? (config/db-based-graph? (state/get-current-repo))
- q (string/trim q)
- [matched-pages set-matched-pages!] (rum/use-state nil)
- search-f (fn []
- (when-not (string/blank? q)
- (p/let [result (if db-tag?
- (editor-handler/get-matched-classes q)
- (editor-handler/<get-matched-blocks q {:nlp-pages? true
- :page-only? (not db-based?)}))]
- (set-matched-pages! result))))]
- (hooks/use-effect! search-f [(hooks/use-debounced-value q 150)])
- (let [matched-pages' (if (string/blank? q)
- (if db-tag?
- (db-model/get-all-classes (state/get-current-repo) {:except-root-class? true})
- (->> (map (fn [title] {:block/title title
- :nlp-date? true})
- date/nlp-pages)
- (take 10)))
- ;; reorder, shortest and starts-with first.
- (let [matched-pages-with-new-page
- (fn [partial-matched-pages]
- (if (or (db/page-exists? q (if db-tag?
- #{:logseq.class/Tag}
- ;; Page existence here should be the same as entity-util/page?.
- ;; Don't show 'New page' if a page has any of these tags
- db-class/page-classes))
- (and db-tag? (some ldb/class? (:block/_alias (db/get-page q)))))
- partial-matched-pages
- (if db-tag?
- (concat [{:block/title (str (t :new-tag) " " q)}]
- partial-matched-pages)
- (cons {:block/title (str (t :new-page) " " q)}
- partial-matched-pages))))]
- (if (and (seq matched-pages)
- (gstring/caseInsensitiveStartsWith (:block/title (first matched-pages)) q))
- (cons (first matched-pages)
- (matched-pages-with-new-page (rest matched-pages)))
- (matched-pages-with-new-page matched-pages))))]
- [:<>
- (ui/auto-complete
- matched-pages'
- {:on-chosen (page-on-chosen-handler embed? input id q pos format)
- :on-enter (fn []
- (page-handler/page-not-exists-handler input id q current-pos))
- :item-render (fn [block _chosen?]
- (let [block' (if-let [id (:block/uuid block)]
- (or (db/entity [:block/uuid id]) block)
- block)]
- [:div.flex.flex-col
- (when (and (not (:page? block)) (:block/uuid block'))
- (when-let [breadcrumb (state/get-component :block/breadcrumb)]
- [:div.text-xs.opacity-70.mb-1 {:style {:margin-left 3}}
- (breadcrumb {:search? true} (state/get-current-repo) (:block/uuid block') {})]))
- [:div.flex.flex-row.items-center.gap-1
- (when-not (or db-tag? (not db-based?))
- [:div.flex.items-center
- (cond
- (:nlp-date? block')
- (ui/icon "calendar" {:size 14})
- (ldb/class? block')
- (ui/icon "hash" {:size 14})
- (ldb/property? block')
- (ui/icon "letter-p" {:size 14})
- (db-model/whiteboard-page? block')
- (ui/icon "whiteboard" {:extension? true})
- (:page? block')
- (ui/icon "page" {:extension? true})
- (or (string/starts-with? (str (:block/title block')) (t :new-tag))
- (string/starts-with? (str (:block/title block')) (t :new-page)))
- (ui/icon "plus" {:size 14})
- :else
- (ui/icon "letter-n" {:size 14}))])
- (let [title (if db-tag?
- (let [target (first (:block/_alias block'))]
- (if (ldb/class? target)
- (str (:block/title block') " -> alias: " (:block/title target))
- (:block/title block')))
- (block-handler/block-unique-title block'))]
- (search-handler/highlight-exact-query title q))]]))
- :empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 (if db-tag?
- "Search for a tag"
- "Search for a node")]
- :class "black"})
- (when (and db-based? db-tag? (not (string/blank? q)))
- [:p.px-1.opacity-50.text-sm
- [:code (if util/mac? "Cmd+Enter" "Ctrl+Enter")]
- [:span " to display this tag inline instead of at the end of this node."]])])))
- (rum/defc page-search < rum/reactive
- {:will-unmount (fn [state]
- (reset! commands/*current-command nil)
- state)}
- "Page or tag searching popup"
- [id format]
- (let [action (state/sub :editor/action)
- db? (config/db-based-graph? (state/get-current-repo))
- embed? (and db? (= @commands/*current-command "Page embed"))
- tag? (= action :page-search-hashtag)
- db-tag? (and db? tag?)]
- (let [pos (state/get-editor-last-pos)
- input (gdom/getElement id)]
- (when input
- (let [current-pos (cursor/pos input)
- edit-content (state/sub-edit-content)
- q (or
- (editor-handler/get-selected-text)
- (when (= action :page-search-hashtag)
- (common-util/safe-subs edit-content pos current-pos))
- (when (> (count edit-content) current-pos)
- (common-util/safe-subs edit-content pos current-pos))
- "")]
- (page-search-aux id format embed? db-tag? q current-pos input pos))))))
- (defn- search-blocks!
- [state result]
- (let [[_edit-block _ _ q] (:rum/args state)]
- (p/let [matched-blocks (when-not (string/blank? q)
- (editor-handler/<get-matched-blocks q))]
- (reset! result matched-blocks))))
- (defn- block-on-chosen-handler
- [embed? input id q format selected-text]
- (if embed?
- (fn [chosen-item]
- (let [pos (state/get-editor-last-pos)
- value (.-value input)
- value' (str (common-util/safe-subs value 0 q)
- (common-util/safe-subs value (+ (count q) 4 pos)))]
- (state/set-edit-content! (.-id input) value')
- (state/clear-editor-action!)
- (let [current-block (state/get-edit-block)
- id (:block/uuid chosen-item)
- id (if (string? id) (uuid id) id)]
- (p/do!
- (editor-handler/api-insert-new-block! ""
- {:block-uuid (:block/uuid current-block)
- :sibling? true
- :replace-empty-target? true
- :other-attrs {:block/link (:db/id (db/entity [:block/uuid id]))}})
- (state/clear-edit!)))))
- (editor-handler/block-on-chosen-handler id q format selected-text)))
- ;; TODO: use rum/use-effect instead
- (rum/defcs block-search-auto-complete < rum/reactive
- {:init (fn [state]
- (let [result (atom nil)]
- (search-blocks! state result)
- (assoc state ::result result)))
- :did-update (fn [state]
- (search-blocks! state (::result state))
- state)}
- [state _edit-block input id q format selected-text]
- (let [result (->> (rum/react (get state ::result))
- (remove (fn [b] (or (nil? (:block/uuid b))
- (string/blank? (:block/title (db-model/query-block-by-uuid (:block/uuid b))))))))
- db? (config/db-based-graph? (state/get-current-repo))
- embed? (and db? (= @commands/*current-command "Block embed"))
- chosen-handler (block-on-chosen-handler embed? input id q format selected-text)
- non-exist-block-handler (editor-handler/block-non-exist-handler input)]
- (ui/auto-complete
- result
- {:on-chosen chosen-handler
- :on-enter non-exist-block-handler
- :empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 (t :editor/block-search)]
- :item-render (fn [{:block/keys [page uuid]}] ;; content returned from search engine is normalized
- (let [page-entity (db/entity [:block/uuid page])
- repo (state/sub :git/current-repo)
- format (get page-entity :block/format :markdown)
- block (db-model/query-block-by-uuid uuid)
- content (:block/title block)]
- (when-not (string/blank? content)
- [:.py-2 (search/block-search-result-item repo uuid format content q :block)])))
- :class "ac-block-search"})))
- (rum/defcs block-search < rum/reactive
- {:will-unmount (fn [state]
- (reset! commands/*current-command nil)
- (state/clear-search-result!)
- state)}
- [state id _format]
- (let [pos (state/get-editor-last-pos)
- input (gdom/getElement id)
- [id format] (:rum/args state)
- current-pos (cursor/pos input)
- edit-content (state/sub-edit-content)
- edit-block (state/get-edit-block)
- selected-text (editor-handler/get-selected-text)
- q (or
- selected-text
- (when (>= (count edit-content) current-pos)
- (subs edit-content pos current-pos)))]
- (when input
- (let [db? (config/db-based-graph? (state/get-current-repo))
- embed? (and db? (= @commands/*current-command "Block embed"))
- page (when embed? (page-ref/get-page-name edit-content))
- embed-block-id (when (and embed? page (common-util/uuid-string? page))
- (uuid page))]
- (if embed-block-id
- (let [f (block-on-chosen-handler true input id q format nil)
- block (db/entity embed-block-id)]
- (when block (f block))
- nil)
- (block-search-auto-complete edit-block input id q format selected-text))))))
- (rum/defc template-search-aux
- [id q]
- (let [db-based? (config/db-based-graph?)
- [matched-templates set-matched-templates!] (rum/use-state nil)]
- (hooks/use-effect! (fn []
- (p/let [result (editor-handler/<get-matched-templates q)]
- (set-matched-templates! result)))
- [q])
- (ui/auto-complete
- matched-templates
- {:on-chosen (editor-handler/template-on-chosen-handler id)
- :on-enter (fn [_state] (state/clear-editor-action!))
- :empty-placeholder [:div.text-gray-500.px-4.py-2.text-sm "Search for a template"]
- :item-render (fn [template]
- (if db-based? (:block/title template) (:template template)))
- :class "black"})))
- (rum/defc template-search < rum/reactive
- [id _format]
- (let [pos (state/get-editor-last-pos)
- input (gdom/getElement id)]
- (when input
- (let [current-pos (cursor/pos input)
- edit-content (state/sub-edit-content)
- q (or
- (when (>= (count edit-content) current-pos)
- (subs edit-content pos current-pos))
- "")]
- (template-search-aux id q)))))
- (rum/defc property-search
- [id]
- (let [input (gdom/getElement id)
- [matched-properties set-matched-properties!] (rum/use-state nil)
- [q set-q!] (rum/use-state "")]
- (when input
- (hooks/use-effect!
- (fn []
- (.addEventListener input "input" (fn [_e]
- (set-q! (or (:searching-property (editor-handler/get-searching-property input)) "")))))
- [])
- (hooks/use-effect!
- (fn []
- (p/let [matched-properties (editor-handler/<get-matched-properties q)]
- (set-matched-properties! matched-properties)))
- [q])
- (let [q-property (string/replace (string/lower-case q) #"\s+" "-")
- non-exist-handler (fn [_state]
- ((editor-handler/property-on-chosen-handler id q-property) nil))]
- (ui/auto-complete
- matched-properties
- {:on-chosen (editor-handler/property-on-chosen-handler id q-property)
- :on-enter non-exist-handler
- :empty-placeholder [:div.px-4.py-2.text-sm (str "Create a new property: " q-property)]
- :header [:div.px-4.py-2.text-sm.font-medium "Matched properties: "]
- :item-render (fn [property] property)
- :class "black"})))))
- (rum/defc property-value-search-aux
- [id property q]
- (let [[values set-values!] (rum/use-state nil)]
- (hooks/use-effect!
- (fn []
- (p/let [result (editor-handler/get-matched-property-values property q)]
- (set-values! result)))
- [property q])
- (ui/auto-complete
- values
- {:on-chosen (editor-handler/property-value-on-chosen-handler id q)
- :on-enter (fn [_state]
- ((editor-handler/property-value-on-chosen-handler id q) nil))
- :empty-placeholder [:div.px-4.py-2.text-sm (str "Create a new property value: " q)]
- :header [:div.px-4.py-2.text-sm.font-medium "Matched property values: "]
- :item-render (fn [property-value] property-value)
- :class "black"})))
- (rum/defc property-value-search < rum/reactive
- [id]
- (let [property (:property (state/get-editor-action-data))
- input (gdom/getElement id)]
- (when (and input
- (not (string/blank? property)))
- (let [current-pos (cursor/pos input)
- edit-content (state/sub-edit-content)
- start-idx (string/last-index-of (subs edit-content 0 current-pos)
- gp-property/colons)
- q (or
- (when (>= current-pos (+ start-idx 2))
- (subs edit-content (+ start-idx 2) current-pos))
- "")
- q (string/triml q)]
- (property-value-search-aux id property q)))))
- (rum/defc code-block-mode-keyup-listener
- [_q _edit-content last-pos current-pos]
- (hooks/use-effect!
- (fn []
- (when (< current-pos last-pos)
- (state/clear-editor-action!)))
- [last-pos current-pos])
- [:<>])
- (rum/defc code-block-mode-picker < rum/reactive
- [id format]
- (when-let [modes (some->> js/window.CodeMirror (.-modes) (js/Object.keys) (js->clj) (remove #(= "null" %)))]
- (when-let [^js input (gdom/getElement id)]
- (let [pos (state/get-editor-last-pos)
- current-pos (cursor/pos input)
- edit-content (or (state/sub-edit-content) "")
- q (or (editor-handler/get-selected-text)
- (common-util/safe-subs edit-content pos current-pos)
- "")
- matched (seq (fuzzy-search modes q))
- matched (or matched (if (string/blank? q) modes [q]))]
- [:div
- (code-block-mode-keyup-listener q edit-content pos current-pos)
- (ui/auto-complete matched
- {:on-chosen (fn [chosen _click?]
- (state/clear-editor-action!)
- (let [prefix (str "```" chosen)
- last-pattern (str "```" q)]
- (editor-handler/insert-command! id
- prefix format {:last-pattern last-pattern})
- (-> (editor-handler/save-block!
- (state/get-current-repo)
- (:block/uuid (state/get-edit-block))
- (.-value input))
- (p/then #(commands/handle-step [:codemirror/focus])))))
- :on-enter (fn []
- (state/clear-editor-action!)
- (commands/handle-step [:codemirror/focus]))
- :item-render (fn [mode _chosen?]
- [:strong mode])
- :class "code-block-mode-picker"})]))))
- (rum/defcs editor-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)
- input-option (:options (state/get-editor-show-input))]
- (when (seq @input-value)
- ;; no new line input
- (util/stop e)
- (let [[_id on-submit] (:rum/args state)
- command (:command (first input-option))]
- (on-submit command @input-value))
- (reset! input-value nil))))
- ;; escape
- 27 (fn [_state _e]
- (let [[id _on-submit on-cancel] (:rum/args state)]
- (on-cancel id)))})))
- [state _id on-submit _on-cancel]
- (when-let [action-data (state/get-editor-action-data)]
- (let [{:keys [pos options]} action-data
- input-value (get state ::input-value)]
- (when (seq options)
- (let [command (:command (first options))]
- [:div.p-2.rounded-md.flex.flex-col.gap-2
- (for [{:keys [id placeholder type]} options]
- (shui/input
- (cond->
- {:key (str "modal-input-" (name id))
- :type (or type "text")
- :auto-complete (if (util/chrome?) "chrome-off" "off")
- :on-change (fn [e]
- (swap! input-value assoc id (util/evalue e)))}
- placeholder
- (assoc :placeholder placeholder))))
- (ui/button
- "Submit"
- :on-click
- (fn [e]
- (util/stop e)
- (on-submit command @input-value pos)))])))))
- (rum/defc image-uploader < rum/reactive
- [id format]
- [:div.image-uploader
- [:input
- {:id "upload-file"
- :type "file"
- :on-change (fn [e]
- (let [files (.-files (.-target e))]
- (editor-handler/upload-asset! id files format editor-handler/*asset-uploading? false)))
- :hidden true}]])
- (defn- set-up-key-down!
- [state format]
- (mixins/on-key-down
- state
- {}
- {:not-matched-handler (editor-handler/keydown-not-matched-handler format)}))
- (defn- set-up-key-up!
- [state input']
- (mixins/on-key-up
- state
- {}
- (editor-handler/keyup-handler state input')))
- (def search-timeout (atom nil))
- (defn- setup-key-listener!
- [state]
- (let [{:keys [id format]} (get-state)
- input-id id
- input' (gdom/getElement input-id)]
- (set-up-key-down! state format)
- (set-up-key-up! state input')))
- (defn get-editor-style-class
- "Get textarea css class according to it's content"
- [block content format]
- (let [content (if content (str content) "")
- heading (pu/get-block-property-value block :logseq.property/heading)
- heading (if (true? heading)
- (min (inc (:block/level block)) 6)
- heading)]
- ;; as the function is binding to the editor content, optimization is welcome
- (str
- (if (or (> (.-length content) 1000)
- (string/includes? content "\n"))
- "multiline-block"
- "uniline-block")
- " "
- (case format
- :markdown
- (cond
- heading (str "h" heading)
- (string/starts-with? content "# ") "h1"
- (string/starts-with? content "## ") "h2"
- (string/starts-with? content "### ") "h3"
- (string/starts-with? content "#### ") "h4"
- (string/starts-with? content "##### ") "h5"
- (string/starts-with? content "###### ") "h6"
- (and (string/starts-with? content "---\n") (.endsWith content "\n---")) "page-properties"
- :else "normal-block")
- ;; other formats
- (cond
- heading (str "h" heading)
- (and (string/starts-with? content "---\n") (.endsWith content "\n---")) "page-properties"
- :else "normal-block")))))
- (defn editor-row-height-unchanged?
- "Check if the row height of editor textarea is changed, which happens when font-size changed"
- []
- ;; FIXME: assuming enter key is the only trigger of the height changing (under markdown editing of headlines)
- ;; FIXME: looking for an elegant & robust way to track the change of font-size, or wait for our own WYSIWYG text area
- (let [last-key (state/get-last-key-code)]
- (and (not= keycode/enter (:key-code last-key))
- (not= keycode/enter-code (:code last-key)))))
- (rum/defc mock-textarea <
- rum/static
- {:did-update
- (fn [state]
- (when-not @(:editor/on-paste? @state/state)
- (try (editor-handler/handle-last-input)
- (catch :default _e
- nil)))
- (state/set-state! :editor/on-paste? false)
- state)}
- [content]
- [:div#mock-text
- {:style {:width "100%"
- :height "100%"
- :position "absolute"
- :visibility "hidden"
- :top 0
- :left 0}}
- (let [content (str content "0")
- graphemes (util/split-graphemes content)
- graphemes-char-index (reductions #(+ %1 (count %2)) 0 graphemes)]
- (for [[idx c] (into (sorted-map) (zipmap graphemes-char-index graphemes))]
- (if (= c "\n")
- [:span {:id (str "mock-text_" idx)
- :key idx} "0" [:br]]
- [:span {:id (str "mock-text_" idx)
- :key idx} c])))])
- (defn- open-editor-popup!
- [id content opts]
- (let [input (state/get-input)
- line-height (or (when input
- (some-> (.-lineHeight (js/window.getComputedStyle input))
- (js/parseFloat)
- (- 4)))
- 20)
- {:keys [left top rect]} (cursor/get-caret-pos input)
- pos [(+ left (:left rect) -20) (+ top (:top rect) line-height)]
- {:keys [root-props content-props]} opts]
- (shui/popup-show!
- pos content
- (merge
- {:id (keyword :editor.commands id)
- :align :start
- :root-props (merge {:onOpenChange #(when-not % (state/clear-editor-action!))} root-props)
- :content-props (merge {:onOpenAutoFocus #(.preventDefault %)
- :onCloseAutoFocus #(.preventDefault %)
- :data-editor-popup-ref (name id)} content-props)
- :force-popover? true}
- (dissoc opts :root-props :content-props)))))
- (rum/defc shui-editor-popups
- [id format action _data]
- (hooks/use-effect!
- (fn []
- (let [pid (case action
- :commands
- (open-editor-popup! :commands
- (commands id format)
- {:content-props {:withoutAnimation false}})
- (:block-search :page-search :page-search-hashtag)
- (open-editor-popup! action
- (if (= :block-search action)
- (block-search id format)
- (page-search id format))
- {:root-props {:onOpenChange
- #(when-not %
- (when (contains?
- #{:block-search :page-search :page-search-hashtag}
- (state/get-editor-action))
- (state/clear-editor-action!)))}})
- :datepicker
- (open-editor-popup! :datepicker
- (datetime-comp/date-picker id format nil) {})
- :input
- (open-editor-popup! :input
- (editor-input id
- ;; on-submit
- (fn [command m]
- (editor-handler/handle-command-input command id format m))
- ;; on-cancel
- (fn []
- (editor-handler/handle-command-input-close id)))
- {:content-props {:onOpenAutoFocus #()}})
- :select-code-block-mode
- (open-editor-popup! :code-block-mode-picker
- (code-block-mode-picker id format) {})
- :template-search
- (open-editor-popup! :template-search
- (template-search id format) {})
- (:property-search :property-value-search)
- (open-editor-popup! action
- (if (= :property-search action)
- (property-search id) (property-value-search id))
- {})
- :zotero
- (open-editor-popup! :zotero
- (zotero/zotero-search id) {})
- ;; TODO: try remove local model state
- false)]
- #(when pid
- (shui/popup-hide! pid))))
- [action])
- [:<>])
- (rum/defc command-popups <
- rum/reactive
- "React to atom changes, find and render the correct popup"
- [id format]
- (let [action (state/sub :editor/action)]
- (shui-editor-popups id format action nil)))
- (defn- editor-on-hide
- [state type e]
- (let [action (state/get-editor-action)
- [_id config] (:rum/args state)]
- (cond
- (and (= type :esc) (editor-handler/editor-commands-popup-exists?))
- nil
- (or (contains?
- #{:commands :page-search :page-search-hashtag :block-search :template-search
- :property-search :property-value-search :datepicker}
- action)
- (and (keyword? action)
- (= (namespace action) "editor.action")))
- (when e (util/stop e))
- ;; editor/input component handles Escape directly, so just prevent handling it here
- (= :input action)
- nil
- ;; exit editing mode
- :else
- (let [select? (= type :esc)]
- (when-let [container (gdom/getElement "app-container")]
- (dom/remove-class! container "blocks-selection-mode"))
- (p/do!
- (editor-handler/escape-editing {:select? select?})
- (some-> config :on-escape-editing
- (apply [(str uuid) (= type :esc)])))))))
- (rum/defcs box < rum/reactive
- {:init (fn [state]
- (assoc state
- ::id (str (random-uuid))
- ::ref (atom nil)))
- :did-mount (fn [state]
- (state/set-editor-args! (:rum/args state))
- state)
- :will-unmount (fn [state]
- (state/set-state! :editor/raw-mode-block nil)
- state)}
- (mixins/event-mixin
- (fn [state]
- (mixins/hide-when-esc-or-outside
- state
- {:node @(::ref state)
- :on-hide (fn [_state e type]
- (when-not (= type :esc)
- (editor-on-hide state type e)))})))
- (mixins/event-mixin setup-key-listener!)
- lifecycle/lifecycle
- [state {:keys [format block parent-block]} id config]
- (let [*ref (::ref state)
- content (state/sub-edit-content (:block/uuid block))
- heading-class (get-editor-style-class block content format)
- opts (cond->
- {:id id
- :ref #(reset! *ref %)
- :cacheMeasurements (editor-row-height-unchanged?) ;; check when content updated (as the content variable is binded)
- :default-value (or content "")
- :minRows (if (state/enable-grammarly?) 2 1)
- :on-click (editor-handler/editor-on-click! id)
- :on-change (editor-handler/editor-on-change! block id search-timeout)
- :on-paste (paste-handler/editor-on-paste! id)
- :on-key-down (fn [e]
- (if-let [on-key-down (:on-key-down config)]
- (on-key-down e)
- (when (= (util/ekey e) "Escape")
- (editor-on-hide state :esc e))))
- :auto-focus true
- :class heading-class}
- (some? parent-block)
- (assoc :parentblockid (str (:block/uuid parent-block)))
- true
- (merge (:editor-opts config)))]
- [:div.editor-inner.flex.flex-1 {:class (if block "block-editor" "non-block-editor")}
- (ui/ls-textarea opts)
- (mock-textarea content)
- (command-popups id format)
- (when format
- (image-uploader id format))]))
|