editor.cljs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754
  1. (ns frontend.components.editor
  2. (:require [clojure.string :as string]
  3. [frontend.commands :as commands
  4. :refer [*matched-block-commands *matched-commands]]
  5. [frontend.components.datetime :as datetime-comp]
  6. [frontend.components.svg :as svg]
  7. [frontend.components.search :as search]
  8. [frontend.components.title :as title]
  9. [frontend.context.i18n :refer [t]]
  10. [frontend.db :as db]
  11. [logseq.db :as ldb]
  12. [frontend.db.model :as db-model]
  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.handler.paste :as paste-handler]
  18. [frontend.handler.property.util :as pu]
  19. [frontend.handler.search :as search-handler]
  20. [frontend.search :refer [fuzzy-search]]
  21. [frontend.mixins :as mixins]
  22. [frontend.state :as state]
  23. [frontend.ui :as ui]
  24. [logseq.shui.ui :as shui]
  25. [logseq.shui.popup.core :as shui-popup]
  26. [frontend.util :as util]
  27. [frontend.util.cursor :as cursor]
  28. [frontend.util.keycode :as keycode]
  29. [goog.dom :as gdom]
  30. [goog.string :as gstring]
  31. [dommy.core :as dom]
  32. [logseq.graph-parser.property :as gp-property]
  33. [logseq.common.util :as common-util]
  34. [promesa.core :as p]
  35. [react-draggable]
  36. [rum.core :as rum]
  37. [frontend.config :as config]))
  38. (rum/defcs commands < rum/reactive
  39. (rum/local [] ::matched-commands)
  40. [s id format]
  41. (let [matched' (util/react *matched-commands)
  42. *matched (::matched-commands s)
  43. _ (when (state/get-editor-action)
  44. (reset! *matched matched'))
  45. matched @*matched]
  46. (ui/auto-complete
  47. matched
  48. {:get-group-name
  49. (fn [item]
  50. (when (= (count item) 5) (last item)))
  51. :item-render
  52. (fn [item]
  53. (let [command-name (first item)
  54. command-doc (get item 2)
  55. plugin-id (get-in item [1 1 1 :pid])
  56. doc (when (state/show-command-doc?) command-doc)
  57. options (some-> item (get 3))
  58. icon-name (some-> (if (map? options) (:icon options) options) (name))
  59. command-name (if icon-name
  60. [:span.flex.items-center.gap-1
  61. (shui/tabler-icon icon-name)
  62. [:strong.font-normal command-name]]
  63. command-name)]
  64. (cond
  65. (or plugin-id (vector? doc))
  66. [:div.has-help
  67. {:title plugin-id}
  68. command-name
  69. (when doc (ui/tippy
  70. {:html doc
  71. :interactive true
  72. :fixed-position? true
  73. :position "right"}
  74. [:small (svg/help-circle)]))]
  75. (string? doc)
  76. [:div {:title doc}
  77. command-name]
  78. :else
  79. [:div command-name])))
  80. :on-chosen
  81. (fn [chosen-item]
  82. (let [command (first chosen-item)]
  83. (reset! commands/*current-command command)
  84. (let [command-steps (get (into {} matched) command)
  85. restore-slash? (or
  86. (contains? #{"Today" "Yesterday" "Tomorrow" "Current time"} command)
  87. (and
  88. (not (fn? command-steps))
  89. (not (contains? (set (map first command-steps)) :editor/input))
  90. (not (contains? #{"Date picker" "Template" "Deadline" "Scheduled" "Upload an image"} command))))]
  91. (editor-handler/insert-command! id command-steps
  92. format
  93. {:restore? restore-slash?
  94. :command command}))))
  95. :class
  96. "cp__commands-slash"})))
  97. (rum/defc block-commands < rum/reactive
  98. [id format]
  99. (when (= :block-commands (state/get-editor-action))
  100. (let [matched (util/react *matched-block-commands)]
  101. (ui/auto-complete
  102. (map first matched)
  103. {:on-chosen (fn [chosen]
  104. (editor-handler/insert-command! id (get (into {} matched) chosen)
  105. format
  106. {:last-pattern commands/angle-bracket
  107. :command :block-commands}))
  108. :class "black"}))))
  109. (defn- page-on-chosen-handler
  110. [embed? input id q pos format]
  111. (if embed?
  112. (fn [chosen-item _e]
  113. (let [value (.-value input)
  114. value' (str (common-util/safe-subs value 0 q)
  115. (common-util/safe-subs value (+ (count q) 4 pos)))]
  116. (state/set-edit-content! (.-id input) value')
  117. (state/clear-editor-action!)
  118. (p/let [page (db/get-page chosen-item)
  119. _ (when-not page (page-handler/<create! chosen-item {:redirect? false
  120. :create-first-block? false}))
  121. page' (db/get-page chosen-item)
  122. current-block (state/get-edit-block)]
  123. (editor-handler/api-insert-new-block! chosen-item
  124. {:block-uuid (:block/uuid current-block)
  125. :sibling? true
  126. :replace-empty-target? true
  127. :other-attrs {:block/link (:db/id page')}}))))
  128. (page-handler/on-chosen-handler input id q pos format)))
  129. (rum/defc page-search-aux
  130. [id format embed? db-tag? q current-pos input pos]
  131. (let [[matched-pages set-matched-pages!] (rum/use-state nil)]
  132. (rum/use-effect! (fn []
  133. (when-not (string/blank? q)
  134. (p/let [result (if db-tag?
  135. (editor-handler/get-matched-classes q)
  136. (editor-handler/<get-matched-blocks q))]
  137. (set-matched-pages! result))))
  138. [q])
  139. (let [matched-pages (when-not (string/blank? q)
  140. ;; reorder, shortest and starts-with first.
  141. (let [matched-pages-with-new-page
  142. (fn [partial-matched-pages]
  143. (if (or (db/page-exists? q)
  144. (some (fn [p] (= (string/lower-case q)
  145. (string/lower-case (:block/title p)))) matched-pages))
  146. partial-matched-pages
  147. (if db-tag?
  148. (concat [{:block/title (str (t :new-tag) " " q)}]
  149. partial-matched-pages)
  150. (cons {:block/title (str (t :new-page) " " q)}
  151. partial-matched-pages))))]
  152. (if (and (seq matched-pages)
  153. (gstring/caseInsensitiveStartsWith (:block/title (first matched-pages)) q))
  154. (cons (first matched-pages)
  155. (matched-pages-with-new-page (rest matched-pages)))
  156. (matched-pages-with-new-page matched-pages))))]
  157. (ui/auto-complete
  158. matched-pages
  159. {:on-chosen (page-on-chosen-handler embed? input id q pos format)
  160. :on-enter (fn []
  161. (page-handler/page-not-exists-handler input id q current-pos))
  162. :item-render (fn [block _chosen?]
  163. [:div.flex.flex-row.items-center.gap-1
  164. (when-not db-tag?
  165. (cond
  166. (ldb/class? block)
  167. [:div (ui/icon "hash" {:size 14})]
  168. (ldb/property? block)
  169. [:div (ui/icon "letter-p" {:size 14})]
  170. (db-model/whiteboard-page? block)
  171. [:div (ui/icon "whiteboard" {:extension? true})]
  172. (db/page? block)
  173. [:div (ui/icon "page" {:extension? true})]
  174. (or (string/starts-with? (:block/title block) (t :new-tag))
  175. (string/starts-with? (:block/title block) (t :new-page)))
  176. nil
  177. :else
  178. [:div (ui/icon "letter-n" {:size 14})]))
  179. (let [title (if db-tag?
  180. (:block/title block)
  181. (title/block-unique-title block))]
  182. (search-handler/highlight-exact-query title q))])
  183. :empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 (if db-tag?
  184. "Search for a tag"
  185. "Search for a node")]
  186. :class "black"}))))
  187. (rum/defc page-search < rum/reactive
  188. {:will-unmount (fn [state]
  189. (reset! commands/*current-command nil)
  190. state)}
  191. "Page or tag searching popup"
  192. [id format]
  193. (let [action (state/sub :editor/action)
  194. db? (config/db-based-graph? (state/get-current-repo))
  195. embed? (and db? (= @commands/*current-command "Page embed"))
  196. tag? (= action :page-search-hashtag)
  197. db-tag? (and db? tag?)]
  198. (let [pos (state/get-editor-last-pos)
  199. input (gdom/getElement id)]
  200. (when input
  201. (let [current-pos (cursor/pos input)
  202. edit-content (state/sub-edit-content)
  203. q (or
  204. (editor-handler/get-selected-text)
  205. (when (= action :page-search-hashtag)
  206. (common-util/safe-subs edit-content pos current-pos))
  207. (when (> (count edit-content) current-pos)
  208. (common-util/safe-subs edit-content pos current-pos))
  209. "")]
  210. (page-search-aux id format embed? db-tag? q current-pos input pos))))))
  211. (defn- search-blocks!
  212. [state result]
  213. (let [[_edit-block _ _ q] (:rum/args state)]
  214. (p/let [matched-blocks (when-not (string/blank? q)
  215. (editor-handler/<get-matched-blocks q))]
  216. (reset! result matched-blocks))))
  217. (defn- block-on-chosen-handler
  218. [embed? input id q format selected-text]
  219. (if embed?
  220. (fn [chosen-item]
  221. (let [pos (state/get-editor-last-pos)
  222. value (.-value input)
  223. value' (str (common-util/safe-subs value 0 q)
  224. (common-util/safe-subs value (+ (count q) 4 pos)))]
  225. (state/set-edit-content! (.-id input) value')
  226. (state/clear-editor-action!)
  227. (let [current-block (state/get-edit-block)
  228. id (:block/uuid chosen-item)
  229. id (if (string? id) (uuid id) id)]
  230. (p/do!
  231. (editor-handler/api-insert-new-block! ""
  232. {:block-uuid (:block/uuid current-block)
  233. :sibling? true
  234. :replace-empty-target? true
  235. :other-attrs {:block/link (:db/id (db/entity [:block/uuid id]))}})
  236. (state/clear-edit!)))))
  237. (editor-handler/block-on-chosen-handler id q format selected-text)))
  238. ;; TODO: use rum/use-effect instead
  239. (rum/defcs block-search-auto-complete < rum/reactive
  240. {:init (fn [state]
  241. (let [result (atom nil)]
  242. (search-blocks! state result)
  243. (assoc state ::result result)))
  244. :did-update (fn [state]
  245. (search-blocks! state (::result state))
  246. state)}
  247. [state _edit-block input id q format selected-text]
  248. (let [result (->> (rum/react (get state ::result))
  249. (remove (fn [b] (string/blank? (:block/title (db-model/query-block-by-uuid (:block/uuid b)))))))
  250. db? (config/db-based-graph? (state/get-current-repo))
  251. embed? (and db? (= @commands/*current-command "Block embed"))
  252. chosen-handler (block-on-chosen-handler embed? input id q format selected-text)
  253. non-exist-block-handler (editor-handler/block-non-exist-handler input)]
  254. (ui/auto-complete
  255. result
  256. {:on-chosen chosen-handler
  257. :on-enter non-exist-block-handler
  258. :empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 (t :editor/block-search)]
  259. :item-render (fn [{:block/keys [page uuid]}] ;; content returned from search engine is normalized
  260. (let [page-entity (db/entity [:block/uuid page])
  261. repo (state/sub :git/current-repo)
  262. format (get page-entity :block/format :markdown)
  263. block (db-model/query-block-by-uuid uuid)
  264. content (:block/title block)]
  265. (when-not (string/blank? content)
  266. [:.py-2 (search/block-search-result-item repo uuid format content q :block)])))
  267. :class "ac-block-search"})))
  268. (rum/defcs block-search < rum/reactive
  269. {:will-unmount (fn [state]
  270. (reset! commands/*current-command nil)
  271. (state/clear-search-result!)
  272. state)}
  273. [state id _format]
  274. (let [pos (state/get-editor-last-pos)
  275. input (gdom/getElement id)
  276. [id format] (:rum/args state)
  277. current-pos (cursor/pos input)
  278. edit-content (state/sub-edit-content)
  279. edit-block (state/get-edit-block)
  280. selected-text (editor-handler/get-selected-text)
  281. q (or
  282. selected-text
  283. (when (> (count edit-content) current-pos)
  284. (subs edit-content pos current-pos)))]
  285. (when input
  286. (block-search-auto-complete edit-block input id q format selected-text))))
  287. (rum/defc template-search-aux
  288. [id q]
  289. (let [[matched-templates set-matched-templates!] (rum/use-state nil)]
  290. (rum/use-effect! (fn []
  291. (p/let [result (editor-handler/<get-matched-templates q)]
  292. (set-matched-templates! result)))
  293. [q])
  294. (ui/auto-complete
  295. matched-templates
  296. {:on-chosen (editor-handler/template-on-chosen-handler id)
  297. :on-enter (fn [_state] (state/clear-editor-action!))
  298. :empty-placeholder [:div.text-gray-500.px-4.py-2.text-sm "Search for a template"]
  299. :item-render (fn [[template _block-db-id]]
  300. template)
  301. :class "black"})))
  302. (rum/defc template-search < rum/reactive
  303. [id _format]
  304. (let [pos (state/get-editor-last-pos)
  305. input (gdom/getElement id)]
  306. (when input
  307. (let [current-pos (cursor/pos input)
  308. edit-content (state/sub-edit-content)
  309. q (or
  310. (when (>= (count edit-content) current-pos)
  311. (subs edit-content pos current-pos))
  312. "")]
  313. (template-search-aux id q)))))
  314. (rum/defc property-search
  315. [id]
  316. (let [input (gdom/getElement id)
  317. [matched-properties set-matched-properties!] (rum/use-state nil)]
  318. (when input
  319. (let [q (or (:searching-property (editor-handler/get-searching-property input))
  320. "")]
  321. (rum/use-effect!
  322. (fn []
  323. (p/let [matched-properties (editor-handler/<get-matched-properties q)]
  324. (set-matched-properties! matched-properties)))
  325. [q])
  326. (let [q-property (string/replace (string/lower-case q) #"\s+" "-")
  327. non-exist-handler (fn [_state]
  328. ((editor-handler/property-on-chosen-handler id q-property) nil))]
  329. (ui/auto-complete
  330. matched-properties
  331. {:on-chosen (editor-handler/property-on-chosen-handler id q-property)
  332. :on-enter non-exist-handler
  333. :empty-placeholder [:div.px-4.py-2.text-sm (str "Create a new property: " q-property)]
  334. :header [:div.px-4.py-2.text-sm.font-medium "Matched properties: "]
  335. :item-render (fn [property] property)
  336. :class "black"}))))))
  337. (rum/defc property-value-search-aux
  338. [id property q]
  339. (let [[values set-values!] (rum/use-state nil)]
  340. (rum/use-effect!
  341. (fn []
  342. (p/let [result (editor-handler/get-matched-property-values property q)]
  343. (set-values! result)))
  344. [property q])
  345. (ui/auto-complete
  346. values
  347. {:on-chosen (editor-handler/property-value-on-chosen-handler id q)
  348. :on-enter (fn [_state]
  349. ((editor-handler/property-value-on-chosen-handler id q) nil))
  350. :empty-placeholder [:div.px-4.py-2.text-sm (str "Create a new property value: " q)]
  351. :header [:div.px-4.py-2.text-sm.font-medium "Matched property values: "]
  352. :item-render (fn [property-value] property-value)
  353. :class "black"})))
  354. (rum/defc property-value-search < rum/reactive
  355. [id]
  356. (let [property (:property (state/get-editor-action-data))
  357. input (gdom/getElement id)]
  358. (when (and input
  359. (not (string/blank? property)))
  360. (let [current-pos (cursor/pos input)
  361. edit-content (state/sub-edit-content)
  362. start-idx (string/last-index-of (subs edit-content 0 current-pos)
  363. gp-property/colons)
  364. q (or
  365. (when (>= current-pos (+ start-idx 2))
  366. (subs edit-content (+ start-idx 2) current-pos))
  367. "")
  368. q (string/triml q)]
  369. (property-value-search-aux id property q)))))
  370. (rum/defc code-block-mode-keyup-listener
  371. [_q _edit-content last-pos current-pos]
  372. (rum/use-effect!
  373. (fn []
  374. (when (< current-pos last-pos)
  375. (state/clear-editor-action!)))
  376. [last-pos current-pos])
  377. [:<>])
  378. (rum/defc code-block-mode-picker < rum/reactive
  379. [id format]
  380. (when-let [modes (some->> js/window.CodeMirror (.-modes) (js/Object.keys) (js->clj) (remove #(= "null" %)))]
  381. (when-let [^js input (gdom/getElement id)]
  382. (let [pos (state/get-editor-last-pos)
  383. current-pos (cursor/pos input)
  384. edit-content (or (state/sub-edit-content) "")
  385. q (or (editor-handler/get-selected-text)
  386. (common-util/safe-subs edit-content pos current-pos)
  387. "")
  388. matched (seq (fuzzy-search modes q))
  389. matched (or matched (if (string/blank? q) modes [q]))]
  390. [:div
  391. (code-block-mode-keyup-listener q edit-content pos current-pos)
  392. (ui/auto-complete matched
  393. {:on-chosen (fn [chosen _click?]
  394. (state/clear-editor-action!)
  395. (let [prefix (str "```" chosen)
  396. last-pattern (str "```" q)]
  397. (editor-handler/insert-command! id
  398. prefix format {:last-pattern last-pattern})
  399. (-> (editor-handler/save-block!
  400. (state/get-current-repo)
  401. (:block/uuid (state/get-edit-block))
  402. (.-value input))
  403. (p/then #(commands/handle-step [:codemirror/focus])))
  404. ))
  405. :on-enter (fn []
  406. (state/clear-editor-action!)
  407. (commands/handle-step [:codemirror/focus]))
  408. :item-render (fn [mode _chosen?]
  409. [:strong mode])
  410. :class "code-block-mode-picker"})]))))
  411. (rum/defcs input < rum/reactive
  412. (rum/local {} ::input-value)
  413. (mixins/event-mixin
  414. (fn [state]
  415. (mixins/on-key-down
  416. state
  417. {;; enter
  418. 13 (fn [state e]
  419. (let [input-value (get state ::input-value)
  420. input-option (:options (state/get-editor-show-input))]
  421. (when (seq @input-value)
  422. ;; no new line input
  423. (util/stop e)
  424. (let [[_id on-submit] (:rum/args state)
  425. command (:command (first input-option))]
  426. (on-submit command @input-value))
  427. (reset! input-value nil))))
  428. ;; escape
  429. 27 (fn [_state _e]
  430. (let [[id _on-submit on-cancel] (:rum/args state)]
  431. (on-cancel id)))})))
  432. [state _id on-submit _on-cancel]
  433. (when-let [action-data (state/get-editor-action-data)]
  434. (let [{:keys [pos options]} action-data
  435. input-value (get state ::input-value)]
  436. (when (seq options)
  437. (let [command (:command (first options))]
  438. [:div.p-2.rounded-md
  439. (for [{:keys [id placeholder type autoFocus] :as input-item} options]
  440. [:div.my-3 {:key id}
  441. [:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
  442. (merge
  443. (cond->
  444. {:key (str "modal-input-" (name id))
  445. :id (str "modal-input-" (name id))
  446. :type (or type "text")
  447. :on-change (fn [e]
  448. (swap! input-value assoc id (util/evalue e)))
  449. :auto-complete (if (util/chrome?) "chrome-off" "off")}
  450. placeholder
  451. (assoc :placeholder placeholder)
  452. autoFocus
  453. (assoc :auto-focus true))
  454. (dissoc input-item :id))]])
  455. (ui/button
  456. "Submit"
  457. :on-click
  458. (fn [e]
  459. (util/stop e)
  460. (on-submit command @input-value pos)))])))))
  461. (defn- set-up-key-down!
  462. [state format]
  463. (mixins/on-key-down
  464. state
  465. {}
  466. {:not-matched-handler (editor-handler/keydown-not-matched-handler format)}))
  467. (defn- set-up-key-up!
  468. [state input input-id]
  469. (mixins/on-key-up
  470. state
  471. {}
  472. (editor-handler/keyup-handler state input input-id)))
  473. (def search-timeout (atom nil))
  474. (defn- setup-key-listener!
  475. [state]
  476. (let [{:keys [id format]} (get-state)
  477. input-id id
  478. input (gdom/getElement input-id)]
  479. (set-up-key-down! state format)
  480. (set-up-key-up! state input input-id)))
  481. (defn get-editor-style-class
  482. "Get textarea css class according to it's content"
  483. [block content format]
  484. (let [content (if content (str content) "")
  485. heading (pu/get-block-property-value block :logseq.property/heading)
  486. heading (if (true? heading)
  487. (min (inc (:block/level block)) 6)
  488. heading)]
  489. ;; as the function is binding to the editor content, optimization is welcome
  490. (str
  491. (if (or (> (.-length content) 1000)
  492. (string/includes? content "\n"))
  493. "multiline-block"
  494. "uniline-block")
  495. " "
  496. (case format
  497. :markdown
  498. (cond
  499. heading (str "h" heading)
  500. (string/starts-with? content "# ") "h1"
  501. (string/starts-with? content "## ") "h2"
  502. (string/starts-with? content "### ") "h3"
  503. (string/starts-with? content "#### ") "h4"
  504. (string/starts-with? content "##### ") "h5"
  505. (string/starts-with? content "###### ") "h6"
  506. (and (string/starts-with? content "---\n") (.endsWith content "\n---")) "page-properties"
  507. :else "normal-block")
  508. ;; other formats
  509. (cond
  510. heading (str "h" heading)
  511. (and (string/starts-with? content "---\n") (.endsWith content "\n---")) "page-properties"
  512. :else "normal-block")))))
  513. (defn editor-row-height-unchanged?
  514. "Check if the row height of editor textarea is changed, which happens when font-size changed"
  515. []
  516. ;; FIXME: assuming enter key is the only trigger of the height changing (under markdown editing of headlines)
  517. ;; FIXME: looking for an elegant & robust way to track the change of font-size, or wait for our own WYSIWYG text area
  518. (let [last-key (state/get-last-key-code)]
  519. (and (not= keycode/enter (:key-code last-key))
  520. (not= keycode/enter-code (:code last-key)))))
  521. (rum/defc mock-textarea <
  522. rum/static
  523. {:did-update
  524. (fn [state]
  525. (when-not @(:editor/on-paste? @state/state)
  526. (try (editor-handler/handle-last-input)
  527. (catch :default _e
  528. nil)))
  529. (state/set-state! :editor/on-paste? false)
  530. state)}
  531. [content]
  532. [:div#mock-text
  533. {:style {:width "100%"
  534. :height "100%"
  535. :position "absolute"
  536. :visibility "hidden"
  537. :top 0
  538. :left 0}}
  539. (let [content (str content "0")
  540. graphemes (util/split-graphemes content)
  541. graphemes-char-index (reductions #(+ %1 (count %2)) 0 graphemes)]
  542. (for [[idx c] (into (sorted-map) (zipmap graphemes-char-index graphemes))]
  543. (if (= c "\n")
  544. [:span {:id (str "mock-text_" idx)
  545. :key idx} "0" [:br]]
  546. [:span {:id (str "mock-text_" idx)
  547. :key idx} c])))])
  548. (defn- exist-editor-commands-popup?
  549. []
  550. (some->> (shui-popup/get-popups)
  551. (some #(some-> % (:id) (str) (string/starts-with? ":editor.commands")))))
  552. (defn- open-editor-popup!
  553. [id content opts]
  554. (let [{:keys [left top rect]} (cursor/get-caret-pos (state/get-input))
  555. pos [(+ left (:left rect) -20) (+ top (:top rect) 20)]
  556. {:keys [root-props content-props]} opts]
  557. (shui/popup-show!
  558. pos content
  559. (merge
  560. {:id (keyword :editor.commands id)
  561. :align :start
  562. :root-props (merge {:onOpenChange #(when-not % (state/clear-editor-action!))} root-props)
  563. :content-props (merge {:onOpenAutoFocus #(.preventDefault %)
  564. :onCloseAutoFocus #(.preventDefault %)
  565. :data-editor-popup-ref (name id)} content-props)
  566. :force-popover? true}
  567. (dissoc opts :root-props :content-props)))))
  568. (rum/defc shui-editor-popups
  569. [id format action _data]
  570. (rum/use-effect!
  571. (fn []
  572. (let [pid (case action
  573. :commands
  574. (open-editor-popup! :commands
  575. (commands id format)
  576. {:content-props {:withoutAnimation false}})
  577. :block-commands
  578. (open-editor-popup! :block-commands
  579. (block-commands id format)
  580. {:content-props {:withoutAnimation true}})
  581. (:block-search :page-search :page-search-hashtag)
  582. (open-editor-popup! action
  583. (if (= :block-search action)
  584. (block-search id format)
  585. (page-search id format))
  586. {:root-props {:onOpenChange
  587. #(when-not %
  588. (when (contains?
  589. #{:block-search :page-search :page-search-hashtag}
  590. (state/get-editor-action))
  591. (state/clear-editor-action!)))}})
  592. :datepicker
  593. (open-editor-popup! :datepicker
  594. (datetime-comp/date-picker id format nil) {})
  595. :input
  596. (open-editor-popup! :input
  597. (input id
  598. (fn [command m]
  599. (editor-handler/handle-command-input command id format m))
  600. (fn []
  601. (editor-handler/handle-command-input-close id))) {})
  602. :select-code-block-mode
  603. (open-editor-popup! :code-block-mode-picker
  604. (code-block-mode-picker id format) {})
  605. :template-search
  606. (open-editor-popup! :template-search
  607. (template-search id format) {})
  608. (:property-search :property-value-search)
  609. (open-editor-popup! action
  610. (if (= :property-search action)
  611. (property-search id) (property-value-search id))
  612. {})
  613. :zotero
  614. (open-editor-popup! :zotero
  615. (zotero/zotero-search id) {})
  616. ;; TODO: try remove local model state
  617. false)]
  618. #(when pid
  619. (shui/popup-hide! pid))))
  620. [action])
  621. [:<>])
  622. (rum/defc command-popups <
  623. rum/reactive
  624. "React to atom changes, find and render the correct popup"
  625. [id format]
  626. (let [action (state/sub :editor/action)]
  627. (shui-editor-popups id format action nil)))
  628. (defn- editor-on-hide
  629. [state value* type e]
  630. (let [repo (state/get-current-repo)
  631. action (state/get-editor-action)
  632. [opts _id config] (:rum/args state)
  633. block (:block opts)
  634. value (or value* "")]
  635. (cond
  636. (and (= type :esc) (exist-editor-commands-popup?))
  637. nil
  638. (or (contains?
  639. #{:commands :block-commands
  640. :page-search :page-search-hashtag :block-search :template-search
  641. :property-search :property-value-search
  642. :datepicker} action)
  643. (and (keyword? action)
  644. (= (namespace action) "editor.action")))
  645. (when e (util/stop e))
  646. ;; editor/input component handles Escape directly, so just prevent handling it here
  647. (= :input action)
  648. nil
  649. ;; exit editing mode
  650. :else
  651. (let [select? (= type :esc)]
  652. (when-let [container (gdom/getElement "app-container")]
  653. (dom/remove-class! container "blocks-selection-mode"))
  654. (p/do!
  655. (editor-handler/save-block! repo (:block/uuid block) value)
  656. (editor-handler/escape-editing select?)
  657. (some-> config :on-escape-editing
  658. (apply [(str uuid) (= type :esc)])))))))
  659. (rum/defcs box < rum/reactive
  660. {:init (fn [state]
  661. (assoc state
  662. ::id (str (random-uuid))
  663. ::ref (atom nil)))
  664. :did-mount (fn [state]
  665. (state/set-editor-args! (:rum/args state))
  666. state)}
  667. (mixins/event-mixin
  668. (fn [state]
  669. (mixins/hide-when-esc-or-outside
  670. state
  671. {:node @(::ref state)
  672. :on-hide (fn [_state e type]
  673. (when-not (= type :esc)
  674. (editor-on-hide state (:value (editor-handler/get-state)) type e)))})))
  675. (mixins/event-mixin setup-key-listener!)
  676. lifecycle/lifecycle
  677. [state {:keys [format block parent-block]} id config]
  678. (let [*ref (::ref state)
  679. content (state/sub-edit-content (:block/uuid block))
  680. heading-class (get-editor-style-class block content format)
  681. opts (cond->
  682. {:id id
  683. :ref #(reset! *ref %)
  684. :cacheMeasurements (editor-row-height-unchanged?) ;; check when content updated (as the content variable is binded)
  685. :default-value (or content "")
  686. :minRows (if (state/enable-grammarly?) 2 1)
  687. :on-click (editor-handler/editor-on-click! id)
  688. :on-change (editor-handler/editor-on-change! block id search-timeout)
  689. :on-paste (paste-handler/editor-on-paste! id)
  690. :on-key-down (fn [e]
  691. (if-let [on-key-down (:on-key-down config)]
  692. (on-key-down e)
  693. (when (= (util/ekey e) "Escape")
  694. (editor-on-hide state content :esc e))))
  695. :auto-focus true
  696. :class heading-class}
  697. (some? parent-block)
  698. (assoc :parentblockid (str (:block/uuid parent-block)))
  699. true
  700. (merge (:editor-opts config)))]
  701. [:div.editor-inner.flex.flex-1 {:class (if block "block-editor" "non-block-editor")}
  702. (ui/ls-textarea opts)
  703. (mock-textarea content)
  704. (command-popups id format)]))