| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820 |
- (ns frontend.components.editor
- (:require [rum.core :as rum]
- [frontend.components.svg :as svg]
- [frontend.config :as config]
- [frontend.handler.editor :as editor-handler :refer [get-state]]
- [frontend.util :as util :refer-macros [profile]]
- [frontend.handler.file :as file]
- [frontend.handler.block :as block-handler]
- [frontend.handler.page :as page-handler]
- [frontend.handler.editor.keyboards :as keyboards-handler]
- [frontend.components.datetime :as datetime-comp]
- [promesa.core :as p]
- [frontend.state :as state]
- [frontend.mixins :as mixins]
- [frontend.ui :as ui]
- [frontend.db :as db]
- [frontend.config :as config]
- [frontend.handler.web.nfs :as nfs]
- [dommy.core :as d]
- [goog.object :as gobj]
- [goog.dom :as gdom]
- [clojure.string :as string]
- [clojure.set :as set]
- [cljs.core.match :refer-macros [match]]
- [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-drag-n-drop.core :as dnd]
- [frontend.text :as text]
- ["/frontend/utils" :as utils]))
- (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-block-search?))
- (not (state/sub :editor/show-template-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]
- (reset! commands/*current-command chosen)
- (let [command-steps (get (into {} matched) chosen)
- restore-slash? (or
- (contains? #{"Today" "Yesterday" "Tomorrow"} chosen)
- (and
- (not (fn? command-steps))
- (not (contains? (set (map first command-steps)) :editor/input))
- (not (contains? #{"Date Picker" "Template" "Deadline" "Scheduled" "Upload an image"} chosen))))]
- (editor-handler/insert-command! id command-steps
- 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]
- (editor-handler/insert-command! id (get (into {} matched) chosen)
- format
- {:last-pattern commands/angle-bracket}))
- :class "black"}))))
- (rum/defc page-search < rum/reactive
- {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
- [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])
- edit-block (state/sub :editor/block)
- q (or
- @editor-handler/*selected-text
- (when (state/sub :editor/show-page-search-hashtag?)
- (util/safe-subs edit-content pos current-pos))
- (when (> (count edit-content) current-pos)
- (util/safe-subs edit-content pos current-pos)))
- matched-pages (when-not (string/blank? q)
- (editor-handler/get-matched-pages q))
- chosen-handler (if (state/sub :editor/show-page-search-hashtag?)
- (fn [chosen _click?]
- (state/set-editor-show-page-search! false)
- (let [chosen (if (re-find #"\s+" chosen)
- (util/format "[[%s]]" chosen)
- chosen)]
- (editor-handler/insert-command! id
- (str "#" chosen)
- format
- {:last-pattern (str "#" (if @editor-handler/*selected-text "" q))})))
- (fn [chosen _click?]
- (state/set-editor-show-page-search! false)
- (let [page-ref-text (page-handler/get-page-ref-text chosen)]
- (editor-handler/insert-command! id
- page-ref-text
- format
- {:last-pattern (str "[[" (if @editor-handler/*selected-text "" q))
- :postfix-fn (fn [s] (util/replace-first "]]" s ""))}))))
- non-exist-page-handler (fn [_state]
- (state/set-editor-show-page-search! false)
- (if (state/org-mode-file-link? (state/get-current-repo))
- (let [page-ref-text (page-handler/get-page-ref-text q)
- value (gobj/get input "value")
- old-page-ref (util/format "[[%s]]" q)
- new-value (string/replace value
- old-page-ref
- page-ref-text)]
- (state/set-edit-content! id new-value)
- (let [new-pos (+ current-pos
- (- (count page-ref-text)
- (count old-page-ref))
- 2)]
- (util/move-cursor-to input new-pos)))
- (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"}))))))
- (rum/defc block-search < rum/reactive
- {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
- [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])
- edit-block (state/get-edit-block)
- q (or
- @editor-handler/*selected-text
- (when (> (count edit-content) current-pos)
- (subs edit-content pos current-pos)))
- matched-blocks (when-not (string/blank? q)
- (editor-handler/get-matched-blocks q (:block/uuid edit-block)))
- chosen-handler (fn [chosen _click?]
- (state/set-editor-show-block-search! false)
- (let [uuid-string (str (:block/uuid chosen))]
- ;; block reference
- (editor-handler/insert-command! id
- (util/format "((%s))" uuid-string)
- format
- {:last-pattern (str "((" (if @editor-handler/*selected-text "" q))
- :postfix-fn (fn [s] (util/replace-first "))" s ""))})
- ;; Save it so it'll be parsed correctly in the future
- (editor-handler/set-block-property! (:block/uuid chosen)
- "ID"
- uuid-string)
- (when-let [input (gdom/getElement id)]
- (.focus input))))
- 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 [{:block/keys [content]}]
- (subs content 0 64))
- :class "black"}))))))
- (rum/defc template-search < rum/reactive
- {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
- [id format]
- (when (state/sub :editor/show-template-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])
- edit-block (state/sub :editor/block)
- q (or
- (when (>= (count edit-content) current-pos)
- (subs edit-content pos current-pos))
- "")
- matched-templates (editor-handler/get-matched-templates q)
- chosen-handler (fn [[template db-id] _click?]
- (if-let [block (db/entity db-id)]
- (let [new-level (:block/level edit-block)
- template-parent-level (:block/level block)
- pattern (config/get-block-pattern format)
- content
- (block-handler/get-block-full-content
- (state/get-current-repo)
- (:block/uuid block)
- (fn [{:block/keys [level content properties] :as block}]
- (let [new-level (+ new-level (- level template-parent-level))
- properties' (dissoc (into {} properties) "id" "custom_id" "template")]
- (-> content
- (string/replace-first (apply str (repeat level pattern))
- (apply str (repeat new-level pattern)))
- text/remove-properties!
- (text/rejoin-properties properties')))))
- content (if (string/includes? (string/trim edit-content) "\n")
- content
- (text/remove-level-spaces content format))]
- (state/set-editor-show-template-search! false)
- (editor-handler/insert-command! id
- content
- format
- {})))
- (when-let [input (gdom/getElement id)]
- (.focus input)))
- non-exist-handler (fn [_state]
- (state/set-editor-show-template-search! false))]
- (ui/auto-complete
- matched-templates
- {:on-chosen chosen-handler
- :on-enter non-exist-handler
- :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a template"]
- :item-render (fn [[template _block-db-id]]
- template)
- :class "black"}))))))
- (rum/defc mobile-bar < rum/reactive
- [parent-state parent-id]
- [:div#mobile-editor-toolbar.bg-base-2.fix-ios-fixed-bottom
- [:button.bottom-action
- {:on-click #(editor-handler/adjust-block-level! parent-state :right)}
- svg/indent-block]
- [:button.bottom-action
- {:on-click #(editor-handler/adjust-block-level! parent-state :left)}
- svg/outdent-block]
- [:button.bottom-action
- {:on-click #(editor-handler/move-up-down % true)}
- svg/move-up-block]
- [:button.bottom-action
- {:on-click #(editor-handler/move-up-down % false)}
- svg/move-down-block]
- [:button.bottom-action
- {:on-click #(commands/simple-insert! parent-id "\n" {})}
- svg/multi-line-input]
- [:button.bottom-action
- {:on-click #(commands/insert-before! parent-id "TODO " {})}
- svg/checkbox]
- [:button.font-extrabold.bottom-action.-mt-1
- {:on-click #(commands/simple-insert!
- parent-id "[[]]"
- {:backward-pos 2
- :check-fn (fn [_ _ new-pos]
- (reset! commands/*slash-caret-pos new-pos)
- (commands/handle-step [:editor/search-page]))})}
- "[[]]"]
- [:button.font-extrabold.bottom-action.-mt-1
- {:on-click #(commands/simple-insert!
- parent-id "(())"
- {:backward-pos 2
- :check-fn (fn [_ _ new-pos]
- (reset! commands/*slash-caret-pos new-pos)
- (commands/handle-step [:editor/search-block]))})}
- "(())"]
- [:button.font-extrabold.bottom-action.-mt-1
- {:on-click #(commands/simple-insert! parent-id "/" {})}
- "/"]])
- (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 (get @state/state :editor/show-input)]
- (when (seq @input-value)
- ;; no new line input
- (util/stop e)
- (let [[_id on-submit] (:rum/args state)
- {:keys [pos]} @*slash-caret-pos
- command (:command (first input-option))]
- (on-submit command @input-value pos))
- (reset! input-value 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)
- (let [command (:command (first input-option))]
- [:div.p-2.mt-2.rounded-md.shadow-sm.bg-base-2
- (for [{:keys [id placeholder type] :as input-item} input-option]
- [:div.my-3
- [: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))
- (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 set-default-width? {:keys [top left rect]}]
- (let [max-height 500
- max-width 300
- offset-top 24
- vw-height js/window.innerHeight
- vw-width js/window.innerWidth
- to-max-height (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)
- x-overflow? (if (and (seq rect) (> vw-width max-width))
- (let [delta-width (- vw-width (+ (:left rect) left))]
- (< delta-width (* max-width 0.5))))] ;; FIXME: for translateY layer
- [:div.absolute.rounded-md.shadow-lg.absolute-modal
- {:class (if x-overflow? "is-overflow-vw-x" "")
- :style (merge
- {:top (+ top offset-top)
- :max-height to-max-height
- :z-index 11}
- (if set-default-width?
- {:width max-width})
- (if config/mobile?
- {:left 0}
- {:left left}))}
- cp]))
- (rum/defc transition-cp < rum/reactive
- [cp set-default-width? pos]
- (when pos
- (when-let [pos (rum/react 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
- {:did-mount (fn [state]
- (let [[id format] (:rum/args state)]
- (add-watch editor-handler/*asset-pending-file ::pending-asset
- (fn [_ _ _ f]
- (reset! *slash-caret-pos (util/get-caret-pos (gdom/getElement id)))
- (editor-handler/upload-asset id #js[f] format editor-handler/*asset-uploading? true))))
- state)
- :will-unmount (fn [state]
- (remove-watch editor-handler/*asset-pending-file ::pending-asset))}
- [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}]
- (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)))]
- false
- *slash-caret-pos)))])
- (rum/defcs box < rum/reactive
- (mixins/event-mixin
- (fn [state]
- (let [{:keys [id format block]} (get-state state)
- input-id id
- input (gdom/getElement input-id)
- repo (:block/repo block)]
- (mixins/on-key-down
- state
- {;; enter
- 13 (fn [state e]
- (when (and (not (gobj/get e "ctrlKey"))
- (not (gobj/get e "metaKey"))
- (not (editor-handler/in-auto-complete? input)))
- (let [{:keys [block config]} (get-state state)]
- (when (and block
- (not (:ref? config))
- (not (:custom-query? config))) ; in reference section
- (let [content (state/get-edit-content)]
- (if (and
- (> (:block/level block) 2)
- (string/blank? content))
- (do
- (util/stop e)
- (editor-handler/adjust-block-level! state :left))
- (let [shortcut (state/get-new-block-shortcut)
- insert? (cond
- config/mobile?
- true
- (and (= shortcut "alt+enter") (not (gobj/get e "altKey")))
- false
- (gobj/get e "shiftKey")
- false
- :else
- true)]
- (when (and
- insert?
- (not (editor-handler/in-auto-complete? input)))
- (util/stop e)
- (profile
- "Insert block"
- (editor-handler/insert-new-block! state))))))))))
- ;; up
- 38 (fn [state e]
- (when (and
- (not (gobj/get e "ctrlKey"))
- (not (gobj/get e "metaKey"))
- (not (editor-handler/in-auto-complete? input)))
- (editor-handler/on-up-down state e true)))
- ;; down
- 40 (fn [state e]
- (when (and
- (not (gobj/get e "ctrlKey"))
- (not (gobj/get e "metaKey"))
- (not (editor-handler/in-auto-complete? input)))
- (editor-handler/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)
- (util/nth-safe value (dec current-pos)))
- selected-start (gobj/get node "selectionStart")
- selected-end (gobj/get node "selectionEnd")
- block-id (:block-id (first (:rum/args state)))
- page (state/get-current-page)]
- (cond
- (not= selected-start selected-end)
- nil
- (and (zero? current-pos)
- ;; not the top block in a block page
- (not (and page
- (util/uuid-string? page)
- (= (medley/uuid page) block-id))))
- (editor-handler/delete-block! state repo e)
- (and (> current-pos 1)
- (= (util/nth-safe value (dec current-pos)) commands/slash))
- (do
- (reset! *slash-caret-pos nil)
- (reset! *show-commands false))
- (and (> current-pos 1)
- (= (util/nth-safe 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 editor-handler/delete-map))
- deleted)
- (>= (count value) (inc current-pos))
- (= (util/nth-safe value current-pos)
- (get editor-handler/delete-map deleted)))
- (do
- (util/stop e)
- (commands/delete-pair! id)
- (cond
- (and (= deleted "[") (state/get-editor-show-page-search?))
- (state/set-editor-show-page-search! false)
- (and (= deleted "(") (state/get-editor-show-block-search?))
- (state/set-editor-show-block-search! false)
- :else
- nil))
- ;; deleting hashtag
- (and (= deleted "#") (state/get-editor-show-page-search-hashtag?))
- (state/set-editor-show-page-search-hashtag! false)
- :else
- nil)))
- ;; tab
- 9 (fn [state e]
- (let [input-id (state/get-edit-input-id)
- input (and input-id (gdom/getElement id))
- pos (and input (:pos (util/get-caret-pos input)))]
- (when (and (not (state/get-editor-show-input))
- (not (state/get-editor-show-date-picker?))
- (not (state/get-editor-show-template-search?)))
- (util/stop e)
- (let [direction (if (gobj/get e "shiftKey") ; shift+tab move to left
- :left
- :right)]
- (p/let [_ (editor-handler/adjust-block-level! state direction)]
- (and input pos (js/setTimeout #(when-let [input (gdom/getElement input-id)]
- (util/move-cursor-to input pos))
- 0)))))))}
- {:not-matched-handler
- (fn [e key-code]
- (let [key (gobj/get e "key")
- value (gobj/get input "value")
- ctrlKey (gobj/get e "ctrlKey")
- metaKey (gobj/get e "metaKey")
- pos (util/get-input-pos input)]
- (cond
- (or ctrlKey metaKey)
- nil
- (or
- (and (= key "#")
- (and
- (> pos 0)
- (= "#" (util/nth-safe value (dec pos)))))
- (and (= key " ")
- (state/get-editor-show-page-search-hashtag?)))
- (state/set-editor-show-page-search-hashtag! false)
- (or
- (editor-handler/surround-by? input "#" " ")
- (editor-handler/surround-by? input "#" :end)
- (= key "#"))
- (do
- (commands/handle-step [:editor/search-page-hashtag])
- (state/set-last-pos! (:pos (util/get-caret-pos input)))
- (reset! commands/*slash-caret-pos (util/get-caret-pos input)))
- (and
- (= key " ")
- (state/get-editor-show-page-search-hashtag?))
- (state/set-editor-show-page-search-hashtag! false)
- (and
- (contains? (set/difference (set (keys editor-handler/reversed-autopair-map))
- #{"`"})
- key)
- (= (editor-handler/get-current-input-char input) key))
- (do
- (util/stop e)
- (util/cursor-move-forward input 1))
- (contains? (set (keys editor-handler/autopair-map)) key)
- (do
- (util/stop e)
- (editor-handler/autopair input-id key format nil)
- (cond
- (editor-handler/surround-by? input "[[" "]]")
- (do
- (commands/handle-step [:editor/search-page])
- (reset! commands/*slash-caret-pos (util/get-caret-pos input)))
- (editor-handler/surround-by? input "((" "))")
- (do
- (commands/handle-step [:editor/search-block :reference])
- (reset! commands/*slash-caret-pos (util/get-caret-pos input)))
- :else
- nil))
- (let [sym "$"]
- (and (= key sym)
- (>= (count value) 1)
- (> pos 0)
- (= (nth value (dec pos)) sym)
- (if (> (count value) pos)
- (not= (nth value pos) sym)
- true)))
- (commands/simple-insert! input-id "$$" {:backward-pos 2})
- (let [sym "^"]
- (and (= key sym)
- (>= (count value) 1)
- (> pos 0)
- (= (nth value (dec pos)) sym)
- (if (> (count value) pos)
- (not= (nth value pos) sym)
- true)))
- (commands/simple-insert! input-id "^^" {:backward-pos 2})
- :else
- nil)))})
- (mixins/on-key-up
- state
- {}
- (fn [e key-code]
- (let [k (gobj/get e "key")
- format (:format (get-state state))]
- (when-not (state/get-editor-show-input)
- (when (and @*show-commands (not= key-code 191)) ; not /
- (let [matched-commands (editor-handler/get-matched-commands input)]
- (if (seq matched-commands)
- (do
- (reset! *show-commands true)
- (reset! *matched-commands matched-commands))
- (reset! *show-commands false))))
- (when (and @*show-block-commands (not= key-code 188)) ; not <
- (let [matched-block-commands (editor-handler/get-matched-block-commands input)]
- (if (seq matched-block-commands)
- (cond
- (= key-code 9) ;tab
- (when @*show-block-commands
- (util/stop e)
- (editor-handler/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))))
- (editor-handler/close-autocomplete-if-outside input))))))))
- {:did-mount (fn [state]
- (let [[{:keys [dummy? format block-parent-id]} id] (:rum/args state)
- content (get-in @state/state [:editor/content id])
- input (gdom/getElement id)]
- (when block-parent-id
- (state/set-editing-block-dom-id! block-parent-id))
- (if (= :indent-outdent (state/get-editor-op))
- (when input
- (when-let [pos (state/get-edit-pos)]
- (util/set-caret-pos! input pos)))
- (editor-handler/restore-cursor-pos! id content dummy?))
- (when input
- (dnd/subscribe!
- input
- :upload-images
- {:drop (fn [e files]
- (editor-handler/upload-asset id files format editor-handler/*asset-uploading? true))}))
- ;; Here we delay this listener, otherwise the click to edit event will trigger a outside click event,
- ;; which will hide the editor so no way for editing.
- (js/setTimeout #(keyboards-handler/esc-save! state) 100)
- (when-let [element (gdom/getElement id)]
- (.focus element)))
- state)
- :did-remount (fn [_old-state state]
- (keyboards-handler/esc-save! state)
- state)
- :will-unmount (fn [state]
- (let [{:keys [id value format block repo dummy? config]} (get-state state)
- file? (:file? config)]
- (when-let [input (gdom/getElement id)]
- ;; (.removeEventListener input "paste" (fn [event]
- ;; (append-paste-doc! format event)))
- (let [s (str "cljs-drag-n-drop." :upload-images)
- a (gobj/get input s)
- timer (:timer a)]
- (and timer
- (dnd/unsubscribe!
- input
- :upload-images))))
- (editor-handler/clear-when-saved!)
- (if file?
- (let [path (:file-path config)
- content (db/get-file-no-sub path)
- value (some-> (gdom/getElement path)
- (gobj/get "value"))]
- (when (and
- (not (string/blank? value))
- (not= (string/trim value) (string/trim content)))
- (let [old-page-name (db/get-file-page path false)]
- (page-handler/rename-when-alter-title-property! old-page-name path format content value)
- (file/alter-file (state/get-current-repo) path (string/trim value)
- {:re-render-root? true}))))
- (when-not (contains? #{:insert :indent-outdent :auto-save} (state/get-editor-op))
- (editor-handler/save-block! (get-state state) value))))
- state)}
- [state {:keys [on-hide dummy? node format block block-parent-id]
- :or {dummy? false}
- :as option} id config]
- (let [content (state/get-edit-content)]
- [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
- (when config/mobile? (mobile-bar state id))
- (ui/ls-textarea
- {:id id
- :cacheMeasurements true
- :default-value (or content "")
- :minRows (if (state/enable-grammarly?) 2 1)
- :on-click (fn [_e]
- (let [input (gdom/getElement id)
- current-pos (:pos (util/get-caret-pos input))]
- (state/set-edit-pos! current-pos)
- (editor-handler/close-autocomplete-if-outside input)))
- :on-change (fn [e]
- (let [value (util/evalue e)
- current-pos (:pos (util/get-caret-pos (gdom/getElement id)))]
- (state/set-edit-content! id value false)
- (state/set-edit-pos! current-pos)
- (when-let [repo (or (:block/repo block)
- (state/get-current-repo))]
- (state/set-editor-last-input-time! repo (util/time-ms))
- (db/clear-repo-persistent-job! repo))
- (let [input (gdom/getElement id)
- native-e (gobj/get e "nativeEvent")
- last-input-char (util/nth-safe value (dec current-pos))]
- (case last-input-char
- "/"
- ;; TODO: is it cross-browser compatible?
- (when (not= (gobj/get native-e "inputType") "insertFromPaste")
- (when-let [matched-commands (seq (editor-handler/get-matched-commands input))]
- (reset! *slash-caret-pos (util/get-caret-pos input))
- (reset! *show-commands true)))
- "<"
- (when-let [matched-commands (seq (editor-handler/get-matched-block-commands input))]
- (reset! *angle-bracket-caret-pos (util/get-caret-pos input))
- (reset! *show-block-commands true))
- nil))))
- :on-paste (fn [e]
- (when-let [handled
- (let [pick-one-allowed-item
- (fn [items]
- (if (util/electron?)
- (let [existed-file-path (js/window.apis.getFilePathFromClipboard)
- existed-file-path (if (and
- (string? existed-file-path)
- (not util/mac?)
- (not util/win32?)) ; FIXME: linuxcx
- (when (re-find #"^(/[^/ ]*)+/?$" existed-file-path)
- existed-file-path)
- existed-file-path)
- has-file-path? (not (string/blank? existed-file-path))
- has-image? (js/window.apis.isClipboardHasImage)]
- (if (or has-image? has-file-path?)
- [:asset (js/File. #js[] (if has-file-path? existed-file-path "image.png"))]))
- (when (and items (.-length items))
- (let [files (. (js/Array.from items) (filter #(= (.-kind %) "file")))
- it (gobj/get files 0) ;;; TODO: support multiple files
- mime (and it (.-type it))]
- (cond
- (contains? #{"image/jpeg" "image/png" "image/jpg" "image/gif"} mime) [:asset (. it getAsFile)])))))
- clipboard-data (gobj/get e "clipboardData")
- items (or (.-items clipboard-data)
- (.-files clipboard-data))
- picked (pick-one-allowed-item items)]
- (if (get picked 1)
- (match picked
- [:asset file] (editor-handler/set-asset-pending-file file))))]
- (util/stop e)))
- :auto-focus false})
- ;; TODO: how to render the transitions asynchronously?
- (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
- (template-search id format)
- true
- *slash-caret-pos)
- (transition-cp
- (datetime-comp/date-picker id format nil)
- false
- *slash-caret-pos)
- (transition-cp
- (input id
- (fn [command m pos]
- (editor-handler/handle-command-input command id format m pos)))
- true
- *slash-caret-pos)
- (when format
- (image-uploader id format))]))
|