editor.cljs 33 KB

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