editor.cljs 35 KB

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