editor.cljs 28 KB

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