Files
logseq/src/main/frontend/components/editor.cljs
2023-11-23 14:01:19 +08:00

761 lines
33 KiB
Clojure

(ns frontend.components.editor
(:require [clojure.string :as string]
[frontend.commands :as commands
:refer [*first-command-group *matched-block-commands *matched-commands]]
[frontend.components.block :as block]
[frontend.components.datetime :as datetime-comp]
[frontend.components.svg :as svg]
[frontend.components.search :as search]
[frontend.context.i18n :refer [t]]
[frontend.db :as db]
[frontend.db.model :as db-model]
[frontend.extensions.zotero :as zotero]
[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.search :refer [fuzzy-search]]
[frontend.mixins :as mixins]
[frontend.modules.shortcut.core :as shortcut]
[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.graph-parser.property :as gp-property]
[logseq.graph-parser.util :as gp-util]
[promesa.core :as p]
[react-draggable]
[rum.core :as rum]
[frontend.config :as config]))
(rum/defc commands < rum/reactive
[id format]
(when (= :commands (state/sub :editor/action))
(let [matched (util/react *matched-commands)]
(ui/auto-complete
matched
{:get-group-name
(fn [item]
(get *first-command-group (first 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)]
(cond
(or plugin-id (vector? doc))
[:div.has-help
command-name
(when doc (ui/tippy
{:html doc
:interactive true
:fixed-position? true
:position "right"}
[:small (svg/help-circle)]))
(when plugin-id
[:small {:title (str plugin-id)} (ui/icon "puzzle")])]
(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
"black"}))))
(rum/defc block-commands < rum/reactive
[id format]
(when (= :block-commands (state/get-editor-action))
(let [matched (util/react *matched-block-commands)]
(ui/auto-complete
(map first matched)
{:on-chosen (fn [chosen]
(editor-handler/insert-command! id (get (into {} matched) chosen)
format
{:last-pattern commands/angle-bracket
:command :block-commands}))
:class "black"}))))
(defn- in-sidebar? [el]
(not (.contains (.getElementById js/document "left-container") el)))
(defn- page-on-chosen-handler
[embed? input id q pos format]
(if embed?
(fn [chosen-item _e]
(let [value (.-value input)
value' (str (gp-util/safe-subs value 0 q)
(gp-util/safe-subs value (+ (count q) 4 pos)))]
(state/set-edit-content! (.-id input) value')
(state/clear-editor-action!)
(let [page-name (util/page-name-sanity-lc chosen-item)
page (db/entity [:block/name page-name])
_ (when-not page (page-handler/create! chosen-item {:redirect? false
:create-first-block? false}))
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 (db/entity [:block/name page-name]))}}))))
(page-handler/on-chosen-handler input id q pos format)))
(rum/defcs page-search < rum/reactive
{:will-unmount (fn [state]
(reset! commands/*current-command nil)
state)}
"Page or tag searching popup"
[state 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?)
create-page? (state/sub :editor/create-page?)]
(when (contains? #{:page-search :page-search-hashtag} action)
(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)
sidebar? (in-sidebar? input)
q (or
(editor-handler/get-selected-text)
(when (= action :page-search-hashtag)
(gp-util/safe-subs edit-content pos current-pos))
(when (> (count edit-content) current-pos)
(gp-util/safe-subs edit-content pos current-pos))
"")
matched-pages (if db-tag?
(editor-handler/get-matched-classes q)
;; FIXME: display refed pages recentedly or frequencyly used
(when-not (string/blank? q)
(editor-handler/get-matched-pages q)))
matched-pages (cond
(contains? (set (map util/page-name-sanity-lc matched-pages))
(util/page-name-sanity-lc (string/trim q))) ;; if there's a page name fully matched
(sort-by (fn [m]
[(count m) m])
matched-pages)
(and (string/blank? q) (not db-tag?))
nil
(and (string/blank? q) db-tag?)
matched-pages
(empty? matched-pages)
(cons q matched-pages)
;; reorder, shortest and starts-with first.
:else
(let [matched-pages (remove nil? matched-pages)
matched-pages (sort-by
(fn [m]
[(not (gstring/caseInsensitiveStartsWith m q)) (count m) m])
matched-pages)]
(if (gstring/caseInsensitiveStartsWith (first matched-pages) q)
(cons (first matched-pages)
(cons q (rest matched-pages)))
(cons q matched-pages))))]
[:div
(when (and db-tag?
;; Don't display in heading
(not (some->> edit-content (re-find #"^\s*#"))))
[:div.flex.flex-row.items-center.px-4.py-1.text-sm.opacity-70.gap-2
"Turn this block into a page:"
(ui/toggle create-page?
(fn [_e]
(swap! (:editor/create-page? @state/state) not))
true)])
(ui/auto-complete
matched-pages
{:on-chosen (page-on-chosen-handler embed? input id q pos format)
:on-enter #(page-handler/page-not-exists-handler input id q current-pos)
:item-render (fn [page-name chosen?]
[:div.preview-trigger-wrapper
(block/page-preview-trigger
{:children
[:div.flex
(when (db-model/whiteboard-page? page-name) [:span.mr-1 (ui/icon "whiteboard" {:extension? true})])
[:div.flex.space-x-1
[:div (when-not (db/page-exists? page-name)
(if db-tag?
(t :new-class)
(t :new-page)))]
(search-handler/highlight-exact-query page-name q)]]
:open? chosen?
:manual? true
:fixed-position? true
:tippy-distance 24
:tippy-position (if sidebar? "left" "right")}
page-name)])
:empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 (if db-tag?
"Search for a class"
"Search for a page")]
:class "black"})]))))))
(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 (:block/uuid edit-block)))]
(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 (gp-util/safe-subs value 0 q)
(gp-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)]
(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)))
(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] (string/blank? (:block/content (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 (or (:block/original-name page)
(:block/name page))
repo (state/sub :git/current-repo)
format (db/get-page-format page)
block (db-model/query-block-by-uuid uuid)
content (:block/content 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]
(when (= :block-search (state/sub :editor/action))
(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
(block-search-auto-complete edit-block input id q format selected-text)))))
(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))
"")
matched-templates (editor-handler/get-matched-templates q)
non-exist-handler (fn [_state]
(state/clear-editor-action!))]
(ui/auto-complete
matched-templates
{:on-chosen (editor-handler/template-on-chosen-handler id)
:on-enter non-exist-handler
:empty-placeholder [:div.text-gray-500.px-4.py-2.text-sm "Search for a template"]
:item-render (fn [[template _block-db-id]]
template)
:class "black"})))))
(rum/defc property-search < rum/reactive
[id]
(let [input (gdom/getElement id)]
(when input
(let [q (or (:searching-property (editor-handler/get-searching-property input))
"")
matched-properties (editor-handler/get-matched-properties q)
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 < 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)
matched-values (editor-handler/get-matched-property-values property q)
non-exist-handler (fn [_state]
((editor-handler/property-value-on-chosen-handler id q) nil))]
(ui/auto-complete
matched-values
{:on-chosen (editor-handler/property-value-on-chosen-handler id q)
:on-enter non-exist-handler
: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 code-block-mode-keyup-listener
[_q _edit-content last-pos current-pos]
(rum/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 [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)
(gp-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})
(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 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 (= :input (state/sub :editor/action))
(when-let [action-data (state/sub :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.shadow-lg
(for [{:keys [id placeholder type autoFocus] :as input-item} options]
[:div.my-3 {:key id}
[:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
(merge
(cond->
{:key (str "modal-input-" (name id))
:id (str "modal-input-" (name id))
:type (or type "text")
:on-change (fn [e]
(swap! input-value assoc id (util/evalue e)))
:auto-complete (if (util/chrome?) "chrome-off" "off")}
placeholder
(assoc :placeholder placeholder)
autoFocus
(assoc :auto-focus true))
(dissoc input-item :id))]])
(ui/button
"Submit"
:on-click
(fn [e]
(util/stop e)
(on-submit command @input-value pos)))]))))))
(rum/defc absolute-modal < rum/static
[cp modal-name set-default-width? {:keys [top left rect]}]
(let [MAX-HEIGHT 700
MAX-HEIGHT' 600
MAX-WIDTH 600
SM-MAX-WIDTH 300
Y-BOUNDARY-HEIGHT 150
vw-width js/window.innerWidth
vw-height js/window.innerHeight
vw-max-width (- vw-width (:left rect))
vw-max-height (- vw-height (:top rect))
vw-max-height' (:top rect)
sm? (< vw-width 415)
max-height (min (- vw-max-height 20) MAX-HEIGHT)
max-height' (min (- vw-max-height' 70) MAX-HEIGHT')
max-width (if sm? SM-MAX-WIDTH (min (max 400 (/ vw-max-width 2)) MAX-WIDTH))
offset-top 24
to-max-height (cond-> (if (and (seq rect) (> vw-height max-height))
(let [delta-height (- vw-height (+ (:top rect) top offset-top))]
(if (< delta-height max-height)
(- (max (* 2 offset-top) delta-height) 16)
max-height))
max-height)
(= modal-name "commands")
(min 500))
right-sidebar? (:ui/sidebar-open? @state/state)
editing-key (state/get-edit-input-id)
*el (rum/use-ref nil)
y-overflow-vh? (or (< to-max-height Y-BOUNDARY-HEIGHT)
(> (- max-height' to-max-height) Y-BOUNDARY-HEIGHT))
to-max-height (if y-overflow-vh? max-height' to-max-height)
pos-rect (when (and (seq rect) editing-key)
(:rect (cursor/get-caret-pos (state/get-input))))
y-diff (when pos-rect (- (:height pos-rect) (:height rect)))
style (merge
{:top (+ top offset-top (if (int? y-diff) y-diff 0))
:max-height to-max-height
:max-width 700
;; TODO: auto responsive fixed size
:width "fit-content"
:z-index 11}
(when set-default-width?
{:width max-width})
(if (<= vw-max-width (+ left (if set-default-width? max-width 500)))
{:right 0}
{:left 0}))]
(rum/use-effect!
(fn []
(when-let [^js/HTMLElement cnt
(and right-sidebar? editing-key
(js/document.querySelector "#main-content-container"))]
(when (.contains cnt (js/document.querySelector (str "#" editing-key)))
(let [el (rum/deref *el)
ofx (- (.-scrollWidth cnt) (.-clientWidth cnt))]
(when (> ofx 0)
(set! (.-transform (.-style el))
(util/format "translate(-%spx, %s)" (+ ofx 20) (if y-overflow-vh? "calc(-100% - 2rem)" 0))))))))
[right-sidebar? editing-key y-overflow-vh?])
[:div.absolute.rounded-md.shadow-lg.absolute-modal
{:ref *el
:data-modal-name modal-name
:class (if y-overflow-vh? "is-overflow-vh-y" "")
:on-mouse-down (fn [e]
(.stopPropagation e))
:style style}
cp]))
(rum/defc transition-cp < rum/reactive
[cp modal-name set-default-width?]
(when-let [pos (:pos (state/sub :editor/action-data))]
(ui/css-transition
{:class-names "fade"
:timeout {:enter 500
:exit 300}}
(absolute-modal cp modal-name set-default-width? 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}]
#_:clj-kondo/ignore
(when-let [uploading? (util/react editor-handler/*asset-uploading?)]
(let [processing (util/react editor-handler/*asset-uploading-process)]
(transition-cp
[:div.flex.flex-row.align-center.rounded-md.shadow-sm.bg-base-2.px-1.py-1
(ui/loading
(util/format "Uploading %s%" (util/format "%2d" processing)))]
"upload-file"
false)))])
(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 input-id]
(mixins/on-key-up
state
{}
(editor-handler/keyup-handler state input input-id)))
(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 input-id)))
(defn get-editor-style-class
"Get textarea css class according to it's content"
[block content format]
(let [content (if content (str content) "")
properties (:block/properties block)
heading (pu/lookup properties :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")]
(for [[idx c] (map-indexed
vector
(string/split content ""))]
(if (= c "\n")
[:span {:id (str "mock-text_" idx)
:key idx} "0" [:br]]
[:span {:id (str "mock-text_" idx)
:key idx} c])))])
(rum/defc animated-modal < rum/reactive
[modal-name component set-default-width?]
(when-let [pos (:pos (state/get-editor-action-data))]
(ui/css-transition
{:key modal-name
:class-names {:enter "origin-top-left opacity-0 transform scale-95"
:enter-done "origin-top-left transition opacity-100 transform scale-100"
:exit "origin-top-left transition opacity-0 transform scale-95"}
:timeout {:enter 0
:exit 150}}
(fn [_]
(absolute-modal
component
modal-name
set-default-width?
pos)))))
(rum/defc modals < rum/reactive
"React to atom changes, find and render the correct modal"
[id format]
(let [action (state/sub :editor/action)]
(cond
(= action :commands)
(animated-modal "commands" (commands id format) true)
(= action :block-commands)
(animated-modal "block-commands" (block-commands id format) true)
(contains? #{:page-search :page-search-hashtag} action)
(animated-modal "page-search" (page-search id format) true)
(= :block-search action)
(animated-modal "block-search" (block-search id format) true)
(= :template-search action)
(animated-modal "template-search" (template-search id format) true)
(= :property-search action)
(animated-modal "property-search" (property-search id) true)
(= :property-value-search action)
(animated-modal "property-value-search" (property-value-search id) true)
;; date-picker in editing-mode
(= :datepicker action)
(animated-modal "date-picker" (datetime-comp/date-picker id format nil) false)
(= :select-code-block-mode action)
(animated-modal "select-code-block-mode" (code-block-mode-picker id format) true)
(= :input action)
(animated-modal "input" (input id
(fn [command m]
(editor-handler/handle-command-input command id format m))
(fn []
(editor-handler/handle-command-input-close id)))
true)
(= :zotero action)
(animated-modal "zotero-search" (zotero/zotero-search id) false)
:else
nil)))
(rum/defcs box < rum/reactive
{:init (fn [state]
(assoc state
::id (str (random-uuid))))
:will-unmount (fn [state]
(reset! (:editor/create-page? @state/state) false)
state)
:did-mount (fn [state]
(state/set-editor-args! (:rum/args state))
state)}
(mixins/event-mixin setup-key-listener!)
(shortcut/mixin :shortcut.handler/block-editing-only)
lifecycle/lifecycle
[state {:keys [format block parent-block]} id config]
(let [content (state/sub-edit-content (:block/uuid block))
heading-class (get-editor-style-class block content format)
opts (cond->
{:id id
: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)
:auto-focus false
: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)
(modals id format)
(when format
(image-uploader id format))]))