editor.cljs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. (ns frontend.components.editor
  2. (:require [clojure.string :as string]
  3. [dommy.core :as d]
  4. [frontend.commands :as commands
  5. :refer [*angle-bracket-caret-pos *first-command-group *matched-block-commands *matched-commands *show-block-commands *show-commands *slash-caret-pos]]
  6. [frontend.components.block :as block]
  7. [frontend.components.datetime :as datetime-comp]
  8. [frontend.components.search :as search]
  9. [frontend.components.svg :as svg]
  10. [frontend.config :as config]
  11. [frontend.handler.notification :as notification]
  12. [frontend.db :as db]
  13. [frontend.extensions.zotero :as zotero]
  14. [frontend.handler.editor :as editor-handler :refer [get-state]]
  15. [frontend.handler.editor.lifecycle :as lifecycle]
  16. [frontend.handler.page :as page-handler]
  17. [frontend.mixins :as mixins]
  18. [frontend.modules.shortcut.core :as shortcut]
  19. [frontend.state :as state]
  20. [frontend.ui :as ui]
  21. [frontend.util :as util]
  22. [frontend.util.cursor :as cursor]
  23. [frontend.util.keycode :as keycode]
  24. [goog.dom :as gdom]
  25. [promesa.core :as p]
  26. [rum.core :as rum]))
  27. (rum/defc commands < rum/reactive
  28. [id format]
  29. (let [matched (util/react *matched-commands)]
  30. (when (util/react *show-commands)
  31. (ui/auto-complete
  32. matched
  33. {:get-group-name
  34. (fn [item]
  35. (get *first-command-group (first item)))
  36. :item-render
  37. (fn [item]
  38. (let [command-name (first item)
  39. command-doc (get item 2)
  40. doc (when (state/show-command-doc?) command-doc)]
  41. (cond
  42. (string? doc)
  43. [:div {:title doc}
  44. command-name]
  45. (vector? doc)
  46. [:div.has-help
  47. command-name
  48. (ui/tippy
  49. {:html doc
  50. :interactive true
  51. :fixed-position? true
  52. :position "right"}
  53. [:small (svg/help-circle)])]
  54. :else
  55. [:div command-name])))
  56. :on-chosen
  57. (fn [chosen-item]
  58. (let [command (first chosen-item)]
  59. (reset! commands/*current-command command)
  60. (let [command-steps (get (into {} matched) command)
  61. restore-slash? (or
  62. (contains? #{"Today" "Yesterday" "Tomorrow" "Current time"} command)
  63. (and
  64. (not (fn? command-steps))
  65. (not (contains? (set (map first command-steps)) :editor/input))
  66. (not (contains? #{"Date picker" "Template" "Deadline" "Scheduled" "Upload an image"} command))))]
  67. (editor-handler/insert-command! id command-steps
  68. format
  69. {:restore? restore-slash?})
  70. (state/pub-event! [:instrument {:type :editor/command-triggered
  71. :payload {:command command}}]))))
  72. :class
  73. "black"}))))
  74. (rum/defc block-commands < rum/reactive
  75. [id format]
  76. (when (util/react *show-block-commands)
  77. (let [matched (util/react *matched-block-commands)]
  78. (ui/auto-complete
  79. (map first matched)
  80. {:on-chosen (fn [chosen]
  81. (editor-handler/insert-command! id (get (into {} matched) chosen)
  82. format
  83. {:last-pattern commands/angle-bracket}))
  84. :class "black"}))))
  85. (defn- in-sidebar? [el]
  86. (not (.contains (.getElementById js/document "left-container") el)))
  87. (rum/defc page-search < rum/reactive
  88. {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
  89. [id format]
  90. (when (state/sub :editor/show-page-search?)
  91. (let [pos (:editor/last-saved-cursor @state/state)
  92. input (gdom/getElement id)]
  93. (when input
  94. (let [current-pos (cursor/pos input)
  95. edit-content (or (state/sub [:editor/content id]) "")
  96. sidebar? (in-sidebar? input)
  97. q (or
  98. @editor-handler/*selected-text
  99. (when (state/sub :editor/show-page-search-hashtag?)
  100. (util/safe-subs edit-content pos current-pos))
  101. (when (> (count edit-content) current-pos)
  102. (util/safe-subs edit-content pos current-pos))
  103. "")
  104. matched-pages (when-not (string/blank? q)
  105. (editor-handler/get-matched-pages q))
  106. matched-pages (cond
  107. (contains? (set (map string/lower-case matched-pages)) (string/trim q))
  108. matched-pages
  109. (empty? matched-pages)
  110. matched-pages
  111. :else
  112. (->>
  113. (cons (first matched-pages)
  114. (cons
  115. (str "New page: " q)
  116. (rest matched-pages)))
  117. (remove nil?)))]
  118. (ui/auto-complete
  119. matched-pages
  120. {:on-chosen (page-handler/on-chosen-handler input id q pos format)
  121. :on-enter #(page-handler/page-not-exists-handler input id q current-pos)
  122. :item-render (fn [page-name chosen?]
  123. [:div.preview-trigger-wrapper
  124. (block/page-preview-trigger
  125. {:children [:div (search/highlight-exact-query page-name q)]
  126. :open? chosen?
  127. :manual? true
  128. :fixed-position? true
  129. :tippy-distance 24
  130. :tippy-position (if sidebar? "left" "right")}
  131. page-name)])
  132. :empty-div [:div.text-gray-500.text-sm.px-4.py-2 "Search for a page"]
  133. :class "black"}))))))
  134. (rum/defcs block-search-auto-complete < rum/reactive
  135. {:init (fn [state]
  136. (assoc state ::result (atom nil)))
  137. :did-update (fn [state]
  138. (let [result (::result state)
  139. [edit-block _ _ q] (:rum/args state)]
  140. (p/let [matched-blocks (when-not (string/blank? q)
  141. (editor-handler/get-matched-blocks q (:block/uuid edit-block)))]
  142. (reset! result matched-blocks)))
  143. state)}
  144. [state edit-block input id q format]
  145. (let [result (rum/react (get state ::result))
  146. chosen-handler (editor-handler/block-on-chosen-handler input id q format)
  147. non-exist-block-handler (editor-handler/block-non-exist-handler input)]
  148. (when result
  149. (ui/auto-complete
  150. result
  151. {:on-chosen chosen-handler
  152. :on-enter non-exist-block-handler
  153. :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a block"]
  154. :item-render (fn [{:block/keys [content page uuid] :as item}]
  155. (let [page (or (:block/original-name page)
  156. (:block/name page))
  157. repo (state/sub :git/current-repo)
  158. format (db/get-page-format page)]
  159. [:.py-2 (search/block-search-result-item repo uuid format content q :block)]))
  160. :class "black"}))))
  161. (rum/defcs block-search < rum/reactive
  162. {:will-unmount (fn [state]
  163. (reset! editor-handler/*selected-text nil)
  164. (state/clear-search-result!)
  165. state)}
  166. [state id format]
  167. (when (state/sub :editor/show-block-search?)
  168. (let [pos (:editor/last-saved-cursor @state/state)
  169. input (gdom/getElement id)
  170. [id format] (:rum/args state)
  171. current-pos (cursor/pos input)
  172. edit-content (state/sub [:editor/content id])
  173. edit-block (state/get-edit-block)
  174. q (or
  175. @editor-handler/*selected-text
  176. (when (> (count edit-content) current-pos)
  177. (subs edit-content pos current-pos)))]
  178. (when input
  179. (block-search-auto-complete edit-block input id q format)))))
  180. (rum/defc template-search < rum/reactive
  181. {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
  182. [id format]
  183. (when (state/sub :editor/show-template-search?)
  184. (let [pos (:editor/last-saved-cursor @state/state)
  185. input (gdom/getElement id)]
  186. (when input
  187. (let [current-pos (cursor/pos input)
  188. edit-content (state/sub [:editor/content id])
  189. q (or
  190. (when (>= (count edit-content) current-pos)
  191. (subs edit-content pos current-pos))
  192. "")
  193. matched-templates (editor-handler/get-matched-templates q)
  194. non-exist-handler (fn [_state]
  195. (state/set-editor-show-template-search! false))]
  196. (ui/auto-complete
  197. matched-templates
  198. {:on-chosen (editor-handler/template-on-chosen-handler id)
  199. :on-enter non-exist-handler
  200. :empty-div [:div.text-gray-500.px-4.py-2.text-sm "Search for a template"]
  201. :item-render (fn [[template _block-db-id]]
  202. template)
  203. :class "black"}))))))
  204. (rum/defc mobile-bar < rum/reactive
  205. [parent-state parent-id]
  206. (let [vw-state (state/sub :ui/visual-viewport-state)
  207. vw-pending? (state/sub :ui/visual-viewport-pending?)]
  208. [:div#mobile-editor-toolbar.bg-base-2
  209. {:style {:bottom (if (and vw-state)
  210. (- (.-clientHeight js/document.documentElement)
  211. (:height vw-state)
  212. (:offset-top vw-state))
  213. 0)}
  214. :class (util/classnames [{:is-vw-pending (boolean vw-pending?)}])}
  215. [:div.flex.justify-around.w-full
  216. [:div
  217. [:button.bottom-action
  218. {:on-mouse-down (fn [e]
  219. (util/stop e)
  220. (editor-handler/indent-outdent true))}
  221. (ui/icon "arrow-bar-right"
  222. {:style {:fontSize ui/icon-size}})]]
  223. [:div
  224. [:button.bottom-action
  225. {:on-mouse-down (fn [e]
  226. (util/stop e)
  227. (editor-handler/indent-outdent false))}
  228. (ui/icon "arrow-bar-left"
  229. {:style {:fontSize ui/icon-size}})]]
  230. [:div
  231. [:button.bottom-action
  232. {:on-mouse-down (fn [e]
  233. (util/stop e)
  234. ((editor-handler/move-up-down true)))}
  235. (ui/icon "arrow-bar-to-up"
  236. {:style {:fontSize ui/icon-size}})]]
  237. [:div
  238. [:button.bottom-action
  239. {:on-mouse-down (fn [e]
  240. (util/stop e)
  241. ((editor-handler/move-up-down false)))}
  242. (ui/icon "arrow-bar-to-down"
  243. {:style {:fontSize ui/icon-size}})]]
  244. [:div
  245. [:button.bottom-action
  246. {:on-mouse-down (fn [e]
  247. (util/stop e)
  248. (commands/simple-insert! parent-id "\n"
  249. {:forward-pos 1})
  250. ;; TODO: should we add this focus step to `simple-insert!`?
  251. (when-let [input (gdom/getElement parent-id)]
  252. (.focus input)))}
  253. (ui/icon "arrow-back"
  254. {:style {:fontSize ui/icon-size}})]]
  255. [:div
  256. [:button.bottom-action
  257. {:on-mouse-down (fn [e]
  258. (util/stop e)
  259. (editor-handler/cycle-todo!))}
  260. (ui/icon "checkbox"
  261. {:style {:fontSize ui/icon-size}})]]
  262. [:div
  263. [:button.bottom-action
  264. {:on-mouse-down (fn [e]
  265. (util/stop e)
  266. (commands/simple-insert!
  267. parent-id "[[]]"
  268. {:backward-pos 2
  269. :check-fn (fn [_ _ new-pos]
  270. (reset! commands/*slash-caret-pos new-pos)
  271. (commands/handle-step [:editor/search-page]))})
  272. (when-let [input (gdom/getElement parent-id)]
  273. (.focus input)))}
  274. (ui/icon "brackets"
  275. {:style {:fontSize ui/icon-size}})]]
  276. [:div
  277. [:button.bottom-action
  278. {:on-mouse-down (fn [e]
  279. (util/stop e)
  280. (commands/simple-insert!
  281. parent-id "(())"
  282. {:backward-pos 2
  283. :check-fn (fn [_ _ new-pos]
  284. (reset! commands/*slash-caret-pos new-pos)
  285. (commands/handle-step [:editor/search-block]))})
  286. (when-let [input (gdom/getElement parent-id)]
  287. (.focus input)))}
  288. (ui/icon "parentheses"
  289. {:style {:fontSize ui/icon-size}})]]
  290. [:div
  291. [:button.bottom-action
  292. {:on-mouse-down (fn [e]
  293. (util/stop e)
  294. (commands/simple-insert! parent-id "/" {})
  295. (when-let [input (gdom/getElement parent-id)]
  296. (.focus input)))}
  297. (ui/icon "command"
  298. {:style {:fontSize ui/icon-size}})]]]]))
  299. (rum/defcs input < rum/reactive
  300. (rum/local {} ::input-value)
  301. (mixins/event-mixin
  302. (fn [state]
  303. (mixins/on-key-down
  304. state
  305. {;; enter
  306. 13 (fn [state e]
  307. (let [input-value (get state ::input-value)
  308. input-option (get @state/state :editor/show-input)]
  309. (when (seq @input-value)
  310. ;; no new line input
  311. (util/stop e)
  312. (let [[_id on-submit] (:rum/args state)
  313. {:keys [pos]} @*slash-caret-pos
  314. command (:command (first input-option))]
  315. (on-submit command @input-value pos))
  316. (reset! input-value nil))))})))
  317. [state id on-submit]
  318. (when-let [input-option (state/sub :editor/show-input)]
  319. (let [{:keys [pos]} (util/react *slash-caret-pos)
  320. input-value (get state ::input-value)]
  321. (when (seq input-option)
  322. (let [command (:command (first input-option))]
  323. [:div.p-2.rounded-md.shadow-lg
  324. (for [{:keys [id placeholder type autoFocus] :as input-item} input-option]
  325. [:div.my-3 {:key id}
  326. [:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
  327. (merge
  328. (cond->
  329. {:key (str "modal-input-" (name id))
  330. :id (str "modal-input-" (name id))
  331. :type (or type "text")
  332. :on-change (fn [e]
  333. (swap! input-value assoc id (util/evalue e)))
  334. :auto-complete (if (util/chrome?) "chrome-off" "off")}
  335. placeholder
  336. (assoc :placeholder placeholder)
  337. autoFocus
  338. (assoc :auto-focus true))
  339. (dissoc input-item :id))]])
  340. (ui/button
  341. "Submit"
  342. :on-click
  343. (fn [e]
  344. (util/stop e)
  345. (on-submit command @input-value pos)))])))))
  346. (rum/defc absolute-modal < rum/static
  347. [cp set-default-width? {:keys [top left rect]}]
  348. (let [max-height 370
  349. max-width 300
  350. offset-top 24
  351. vw-height js/window.innerHeight
  352. vw-width js/window.innerWidth
  353. to-max-height (if (and (seq rect) (> vw-height max-height))
  354. (let [delta-height (- vw-height (+ (:top rect) top offset-top))]
  355. (if (< delta-height max-height)
  356. (- (max (* 2 offset-top) delta-height) 16)
  357. max-height))
  358. max-height)
  359. right-sidebar? (:ui/sidebar-open? @state/state)
  360. editing-key (first (keys (:editor/editing? @state/state)))
  361. *el (rum/use-ref nil)
  362. _ (rum/use-effect! (fn []
  363. (when-let [^js/HTMLElement cnt
  364. (and right-sidebar? editing-key
  365. (js/document.querySelector "#main-container"))]
  366. (when (.contains cnt (js/document.querySelector (str "#" editing-key)))
  367. (let [el (rum/deref *el)
  368. ofx (- (.-scrollWidth cnt) (.-clientWidth cnt))]
  369. (when (> ofx 0)
  370. (set! (.-transform (.-style el)) (str "translateX(-" (+ ofx 20) "px)")))))))
  371. [right-sidebar? editing-key])
  372. ;; FIXME: for translateY layer
  373. x-overflow-vw? (when (and (seq rect) (> vw-width max-width))
  374. (let [delta-width (- vw-width (+ (:left rect) left))]
  375. (< delta-width (* max-width 0.5))))]
  376. [:div.absolute.rounded-md.shadow-lg.absolute-modal
  377. {:ref *el
  378. :class (if x-overflow-vw? "is-overflow-vw-x" "")
  379. :on-mouse-down (fn [e]
  380. (.stopPropagation e))
  381. :style (merge
  382. {:top (+ top offset-top)
  383. :max-height to-max-height
  384. :max-width 700
  385. ;; TODO: auto responsive fixed size
  386. :width "fit-content"
  387. :z-index 11}
  388. (when set-default-width?
  389. {:width max-width})
  390. (if config/mobile?
  391. {:left 0}
  392. {:left left}))}
  393. cp]))
  394. (rum/defc transition-cp < rum/reactive
  395. [cp set-default-width? pos]
  396. (when pos
  397. (when-let [pos (rum/react pos)]
  398. (ui/css-transition
  399. {:class-names "fade"
  400. :timeout {:enter 500
  401. :exit 300}}
  402. (absolute-modal cp set-default-width? pos)))))
  403. (rum/defc image-uploader < rum/reactive
  404. [id format]
  405. [:div.image-uploader
  406. [:input
  407. {:id "upload-file"
  408. :type "file"
  409. :on-change (fn [e]
  410. (let [files (.-files (.-target e))]
  411. (editor-handler/upload-asset id files format editor-handler/*asset-uploading? false)))
  412. :hidden true}]
  413. (when-let [uploading? (util/react editor-handler/*asset-uploading?)]
  414. (let [processing (util/react editor-handler/*asset-uploading-process)]
  415. (transition-cp
  416. [:div.flex.flex-row.align-center.rounded-md.shadow-sm.bg-base-2.px-1.py-1
  417. (ui/loading
  418. (util/format "Uploading %s%" (util/format "%2d" processing)))]
  419. false
  420. *slash-caret-pos)))])
  421. (defn- set-up-key-down!
  422. [repo state format]
  423. (mixins/on-key-down
  424. state
  425. {}
  426. {:not-matched-handler (editor-handler/keydown-not-matched-handler format)}))
  427. (defn- set-up-key-up!
  428. [state input input-id search-timeout]
  429. (mixins/on-key-up
  430. state
  431. {}
  432. (editor-handler/keyup-handler state input input-id search-timeout)))
  433. (def search-timeout (atom nil))
  434. (defn- setup-key-listener!
  435. [state]
  436. (let [{:keys [id format block]} (get-state)
  437. input-id id
  438. input (gdom/getElement input-id)
  439. repo (:block/repo block)]
  440. (set-up-key-down! repo state format)
  441. (set-up-key-up! state input input-id search-timeout)))
  442. (def starts-with? clojure.string/starts-with?)
  443. (defn get-editor-style-class
  444. "Get textarea css class according to it's content"
  445. [content format]
  446. (let [content (if content (str content) "")]
  447. ;; as the function is binding to the editor content, optimization is welcome
  448. (str
  449. (if (or (> (.-length content) 1000)
  450. (string/includes? content "\n"))
  451. "multiline-block"
  452. "uniline-block")
  453. " "
  454. (case format
  455. :markdown
  456. (cond
  457. (starts-with? content "# ") "h1"
  458. (starts-with? content "## ") "h2"
  459. (starts-with? content "### ") "h3"
  460. (starts-with? content "#### ") "h4"
  461. (starts-with? content "##### ") "h5"
  462. (starts-with? content "###### ") "h6"
  463. (and (starts-with? content "---\n") (.endsWith content "\n---")) "page-properties"
  464. :else "normal-block")
  465. ;; other formats
  466. (cond
  467. (and (starts-with? content "---\n") (.endsWith content "\n---")) "page-properties"
  468. :else "normal-block")))))
  469. (defn editor-row-height-unchanged?
  470. "Check if the row height of editor textarea is changed, which happens when font-size changed"
  471. []
  472. ;; FIXME: assuming enter key is the only trigger of the height changing (under markdown editing of headlines)
  473. ;; FIXME: looking for an elegant & robust way to track the change of font-size, or wait for our own WYSIWYG text area
  474. (let [last-key (state/get-last-key-code)]
  475. (and (not= keycode/enter (:key-code last-key))
  476. (not= keycode/enter-code (:code last-key)))))
  477. (rum/defc mock-textarea <
  478. rum/static
  479. {:did-update
  480. (fn [state]
  481. (when-not (:editor/on-paste? @state/state)
  482. (try (editor-handler/handle-last-input)
  483. (catch js/Error _e
  484. nil)))
  485. (state/set-state! :editor/on-paste? false)
  486. state)}
  487. [content]
  488. [:div#mock-text
  489. {:style {:width "100%"
  490. :height "100%"
  491. :position "absolute"
  492. :visibility "hidden"
  493. :top 0
  494. :left 0}}
  495. (let [content (str content "0")]
  496. (for [[idx c] (map-indexed
  497. vector
  498. (string/split content ""))]
  499. (if (= c "\n")
  500. [:span {:id (str "mock-text_" idx)
  501. :key idx} "0" [:br]]
  502. [:span {:id (str "mock-text_" idx)
  503. :key idx} c])))])
  504. (rum/defc mock-textarea-wrapper < rum/reactive
  505. []
  506. (let [content (state/sub-edit-content)]
  507. (mock-textarea content)))
  508. (defn animated-modal
  509. [key component set-default-width? *pos]
  510. (when *pos
  511. (ui/css-transition
  512. {:key key
  513. :class-names {:enter "origin-top-left opacity-0 transform scale-95"
  514. :enter-done "origin-top-left transition opacity-100 transform scale-100"
  515. :exit "origin-top-left transition opacity-0 transform scale-95"}
  516. :timeout {:enter 0
  517. :exit 150}}
  518. (fn [_]
  519. (absolute-modal
  520. component
  521. set-default-width?
  522. *pos)))))
  523. (rum/defc modals < rum/reactive
  524. "React to atom changes, find and render the correct modal"
  525. [id format]
  526. (ui/transition-group
  527. (cond
  528. (and (util/react *show-commands)
  529. (not (state/sub :editor/show-page-search?))
  530. (not (state/sub :editor/show-block-search?))
  531. (not (state/sub :editor/show-template-search?))
  532. (not (state/sub :editor/show-input))
  533. (not (state/sub :editor/show-zotero))
  534. (not (state/sub :editor/show-date-picker?)))
  535. (animated-modal "commands" (commands id format) true (util/react *slash-caret-pos))
  536. (and (util/react *show-block-commands) @*angle-bracket-caret-pos)
  537. (animated-modal "block-commands" (block-commands id format) true (util/react *angle-bracket-caret-pos))
  538. (state/sub :editor/show-page-search?)
  539. (animated-modal "page-search" (page-search id format) true (util/react *slash-caret-pos))
  540. (state/sub :editor/show-block-search?)
  541. (animated-modal "block-search" (block-search id format) false (util/react *slash-caret-pos))
  542. (state/sub :editor/show-template-search?)
  543. (animated-modal "template-search" (template-search id format) true (util/react *slash-caret-pos))
  544. (state/sub :editor/show-date-picker?)
  545. (animated-modal "date-picker" (datetime-comp/date-picker id format nil) false (util/react *slash-caret-pos))
  546. (state/sub :editor/show-input)
  547. (animated-modal "input" (input id
  548. (fn [command m pos]
  549. (editor-handler/handle-command-input command id format m)))
  550. true (util/react *slash-caret-pos))
  551. (state/sub :editor/show-zotero)
  552. (animated-modal "zotero-search" (zotero/zotero-search id) false (util/react *slash-caret-pos))
  553. :else
  554. nil)))
  555. (rum/defcs box < rum/reactive
  556. {:init (fn [state]
  557. (assoc state ::heading-level (:heading-level (first (:rum/args state)))))
  558. :did-mount (fn [state]
  559. (state/set-editor-args! (:rum/args state))
  560. state)}
  561. (mixins/event-mixin setup-key-listener!)
  562. (shortcut/mixin :shortcut.handler/block-editing-only)
  563. lifecycle/lifecycle
  564. [state {:keys [on-hide node format block block-parent-id heading-level]
  565. :as option} id config]
  566. (let [content (state/sub-edit-content)
  567. heading-class (get-editor-style-class content format)]
  568. [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
  569. (when config/mobile? (mobile-bar state id))
  570. (ui/ls-textarea
  571. {:id id
  572. :cacheMeasurements (editor-row-height-unchanged?) ;; check when content updated (as the content variable is binded)
  573. :default-value (or content "")
  574. :minRows (if (state/enable-grammarly?) 2 1)
  575. :on-click (editor-handler/editor-on-click! id)
  576. :on-change (editor-handler/editor-on-change! block id search-timeout)
  577. :on-paste (editor-handler/editor-on-paste! id)
  578. :auto-focus false
  579. :class heading-class})
  580. (mock-textarea-wrapper)
  581. (modals id format)
  582. (when format
  583. (image-uploader id format))]))