editor.cljs 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954
  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. [logseq.shui.ui :as shui]
  23. [logseq.shui.popup.core :as shui-popup]
  24. [frontend.util :as util]
  25. [frontend.util.cursor :as cursor]
  26. [frontend.util.keycode :as keycode]
  27. [goog.dom :as gdom]
  28. [goog.string :as gstring]
  29. [logseq.graph-parser.property :as gp-property]
  30. [logseq.common.util :as common-util]
  31. [promesa.core :as p]
  32. [react-draggable]
  33. [rum.core :as rum]
  34. [frontend.config :as config]))
  35. (rum/defcs commands < rum/reactive
  36. (rum/local [] ::matched-commands)
  37. [s id format]
  38. (let [matched' (util/react *matched-commands)
  39. *matched (::matched-commands s)
  40. _ (when (state/get-editor-action)
  41. (reset! *matched matched'))
  42. matched @*matched]
  43. (ui/auto-complete
  44. matched
  45. {:get-group-name
  46. (fn [item]
  47. (when (= (count item) 4) (last item)))
  48. :item-render
  49. (fn [item]
  50. (let [command-name (first item)
  51. command-doc (get item 2)
  52. plugin-id (get-in item [1 1 1 :pid])
  53. doc (when (state/show-command-doc?) command-doc)]
  54. (cond
  55. (or plugin-id (vector? doc))
  56. [:div.has-help
  57. command-name
  58. (when doc (ui/tippy
  59. {:html doc
  60. :interactive true
  61. :fixed-position? true
  62. :position "right"}
  63. [:small (svg/help-circle)]))
  64. (when plugin-id
  65. [:small {:title (str plugin-id)} (ui/icon "puzzle")])]
  66. (string? doc)
  67. [:div {:title doc}
  68. command-name]
  69. :else
  70. [:div command-name])))
  71. :on-chosen
  72. (fn [chosen-item]
  73. (let [command (first chosen-item)]
  74. (reset! commands/*current-command command)
  75. (let [command-steps (get (into {} matched) command)
  76. restore-slash? (or
  77. (contains? #{"Today" "Yesterday" "Tomorrow" "Current time"} command)
  78. (and
  79. (not (fn? command-steps))
  80. (not (contains? (set (map first command-steps)) :editor/input))
  81. (not (contains? #{"Date picker" "Template" "Deadline" "Scheduled" "Upload an image"} command))))]
  82. (editor-handler/insert-command! id command-steps
  83. format
  84. {:restore? restore-slash?
  85. :command command}))))
  86. :class
  87. "black"})))
  88. (rum/defc block-commands < rum/reactive
  89. [id format]
  90. (when (= :block-commands (state/get-editor-action))
  91. (let [matched (util/react *matched-block-commands)]
  92. (ui/auto-complete
  93. (map first matched)
  94. {:on-chosen (fn [chosen]
  95. (editor-handler/insert-command! id (get (into {} matched) chosen)
  96. format
  97. {:last-pattern commands/angle-bracket
  98. :command :block-commands}))
  99. :class "black"}))))
  100. (defn- page-on-chosen-handler
  101. [embed? input id q pos format]
  102. (if embed?
  103. (fn [chosen-item _e]
  104. (let [value (.-value input)
  105. value' (str (common-util/safe-subs value 0 q)
  106. (common-util/safe-subs value (+ (count q) 4 pos)))]
  107. (state/set-edit-content! (.-id input) value')
  108. (state/clear-editor-action!)
  109. (p/let [page-name (util/page-name-sanity-lc chosen-item)
  110. page (db/entity [:block/name page-name])
  111. _ (when-not page (page-handler/<create! chosen-item {:redirect? false
  112. :create-first-block? false}))
  113. current-block (state/get-edit-block)]
  114. (editor-handler/api-insert-new-block! chosen-item
  115. {:block-uuid (:block/uuid current-block)
  116. :sibling? true
  117. :replace-empty-target? true
  118. :other-attrs {:block/link (:db/id (db/entity [:block/name page-name]))}}))))
  119. (page-handler/on-chosen-handler input id q pos format)))
  120. (rum/defc page-search-aux
  121. [id format embed? db-tag? q current-pos input pos]
  122. (let [[matched-pages set-matched-pages!] (rum/use-state nil)]
  123. (rum/use-effect! (fn []
  124. (when-not (string/blank? q)
  125. (p/let [result (editor-handler/<get-matched-pages q)]
  126. (set-matched-pages! result))))
  127. [q])
  128. (let [matched-pages (cond
  129. (contains? (set (map util/page-name-sanity-lc matched-pages))
  130. (util/page-name-sanity-lc (string/trim q))) ;; if there's a page name fully matched
  131. (sort-by (fn [m]
  132. [(count m) m])
  133. matched-pages)
  134. (string/blank? q)
  135. nil
  136. (empty? matched-pages)
  137. (when-not (db/page-exists? q)
  138. (if db-tag?
  139. (concat [(str (t :new-class) " " q)]
  140. matched-pages)
  141. (cons q matched-pages)))
  142. ;; reorder, shortest and starts-with first.
  143. :else
  144. (let [matched-pages (remove nil? matched-pages)
  145. matched-pages (sort-by
  146. (fn [m]
  147. [(not (gstring/caseInsensitiveStartsWith m q)) (count m) m])
  148. matched-pages)]
  149. (if (gstring/caseInsensitiveStartsWith (first matched-pages) q)
  150. (cons (first matched-pages)
  151. (cons q (rest matched-pages)))
  152. (cons q matched-pages))))]
  153. (ui/auto-complete
  154. matched-pages
  155. {:on-chosen (page-on-chosen-handler embed? input id q pos format)
  156. :on-enter (fn []
  157. (page-handler/page-not-exists-handler input id q current-pos))
  158. :item-render (fn [page-name _chosen?]
  159. [:div.flex
  160. (when (db-model/whiteboard-page? page-name) [:span.mr-1 (ui/icon "whiteboard" {:extension? true})])
  161. (search-handler/highlight-exact-query page-name q)])
  162. :empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 (if db-tag?
  163. "Search for a page or a class"
  164. "Search for a page")]
  165. :class "black"}))))
  166. (rum/defc page-search < rum/reactive
  167. {:will-unmount (fn [state]
  168. (reset! commands/*current-command nil)
  169. state)}
  170. "Page or tag searching popup"
  171. [id format]
  172. (let [action (state/sub :editor/action)
  173. db? (config/db-based-graph? (state/get-current-repo))
  174. embed? (and db? (= @commands/*current-command "Page embed"))
  175. tag? (= action :page-search-hashtag)
  176. db-tag? (and db? tag?)]
  177. (when (contains? #{:page-search :page-search-hashtag} action)
  178. (let [pos (state/get-editor-last-pos)
  179. input (gdom/getElement id)]
  180. (when input
  181. (let [current-pos (cursor/pos input)
  182. edit-content (state/sub-edit-content)
  183. q (or
  184. (editor-handler/get-selected-text)
  185. (when (= action :page-search-hashtag)
  186. (common-util/safe-subs edit-content pos current-pos))
  187. (when (> (count edit-content) current-pos)
  188. (common-util/safe-subs edit-content pos current-pos))
  189. "")]
  190. (page-search-aux id format embed? db-tag? q current-pos input pos)))))))
  191. (defn- search-blocks!
  192. [state result]
  193. (let [[edit-block _ _ q] (:rum/args state)]
  194. (p/let [matched-blocks (when-not (string/blank? q)
  195. (editor-handler/get-matched-blocks q (:block/uuid edit-block)))]
  196. (reset! result matched-blocks))))
  197. (defn- block-on-chosen-handler
  198. [embed? input id q format selected-text]
  199. (if embed?
  200. (fn [chosen-item]
  201. (let [pos (state/get-editor-last-pos)
  202. value (.-value input)
  203. value' (str (common-util/safe-subs value 0 q)
  204. (common-util/safe-subs value (+ (count q) 4 pos)))]
  205. (state/set-edit-content! (.-id input) value')
  206. (state/clear-editor-action!)
  207. (let [current-block (state/get-edit-block)
  208. id (:block/uuid chosen-item)
  209. id (if (string? id) (uuid id) id)]
  210. (p/do!
  211. (editor-handler/api-insert-new-block! ""
  212. {:block-uuid (:block/uuid current-block)
  213. :sibling? true
  214. :replace-empty-target? true
  215. :other-attrs {:block/link (:db/id (db/entity [:block/uuid id]))}})
  216. (state/clear-edit!)))))
  217. (editor-handler/block-on-chosen-handler id q format selected-text)))
  218. ;; TODO: use rum/use-effect instead
  219. (rum/defcs block-search-auto-complete < rum/reactive
  220. {:init (fn [state]
  221. (let [result (atom nil)]
  222. (search-blocks! state result)
  223. (assoc state ::result result)))
  224. :did-update (fn [state]
  225. (search-blocks! state (::result state))
  226. state)}
  227. [state _edit-block input id q format selected-text]
  228. (let [result (->> (rum/react (get state ::result))
  229. (remove (fn [b] (string/blank? (:block/content (db-model/query-block-by-uuid (:block/uuid b)))))))
  230. db? (config/db-based-graph? (state/get-current-repo))
  231. embed? (and db? (= @commands/*current-command "Block embed"))
  232. chosen-handler (block-on-chosen-handler embed? input id q format selected-text)
  233. non-exist-block-handler (editor-handler/block-non-exist-handler input)]
  234. (ui/auto-complete
  235. result
  236. {:on-chosen chosen-handler
  237. :on-enter non-exist-block-handler
  238. :empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 (t :editor/block-search)]
  239. :item-render (fn [{:block/keys [page uuid]}] ;; content returned from search engine is normalized
  240. (let [page (or (:block/original-name page)
  241. (:block/name page))
  242. repo (state/sub :git/current-repo)
  243. format (db/get-page-format page)
  244. block (db-model/query-block-by-uuid uuid)
  245. content (:block/content block)]
  246. (when-not (string/blank? content)
  247. [:.py-2 (search/block-search-result-item repo uuid format content q :block)])))
  248. :class "ac-block-search"})))
  249. (rum/defcs block-search < rum/reactive
  250. {:will-unmount (fn [state]
  251. (reset! commands/*current-command nil)
  252. (state/clear-search-result!)
  253. state)}
  254. [state id _format]
  255. (when (= :block-search (state/sub :editor/action))
  256. (let [pos (state/get-editor-last-pos)
  257. input (gdom/getElement id)
  258. [id format] (:rum/args state)
  259. current-pos (cursor/pos input)
  260. edit-content (state/sub-edit-content)
  261. edit-block (state/get-edit-block)
  262. selected-text (editor-handler/get-selected-text)
  263. q (or
  264. selected-text
  265. (when (> (count edit-content) current-pos)
  266. (subs edit-content pos current-pos)))]
  267. (when input
  268. (block-search-auto-complete edit-block input id q format selected-text)))))
  269. (rum/defc template-search-aux
  270. [id q]
  271. (let [[matched-templates set-matched-templates!] (rum/use-state nil)]
  272. (rum/use-effect! (fn []
  273. (p/let [result (editor-handler/<get-matched-templates q)]
  274. (set-matched-templates! result)))
  275. [q])
  276. (ui/auto-complete
  277. matched-templates
  278. {:on-chosen (editor-handler/template-on-chosen-handler id)
  279. :on-enter (fn [_state] (state/clear-editor-action!))
  280. :empty-placeholder [:div.text-gray-500.px-4.py-2.text-sm "Search for a template"]
  281. :item-render (fn [[template _block-db-id]]
  282. template)
  283. :class "black"})))
  284. (rum/defc template-search < rum/reactive
  285. [id _format]
  286. (let [pos (state/get-editor-last-pos)
  287. input (gdom/getElement id)]
  288. (when input
  289. (let [current-pos (cursor/pos input)
  290. edit-content (state/sub-edit-content)
  291. q (or
  292. (when (>= (count edit-content) current-pos)
  293. (subs edit-content pos current-pos))
  294. "")]
  295. (template-search-aux id q)))))
  296. (rum/defc property-search
  297. [id]
  298. (let [input (gdom/getElement id)
  299. [matched-properties set-matched-properties!] (rum/use-state nil)]
  300. (when input
  301. (let [q (or (:searching-property (editor-handler/get-searching-property input))
  302. "")]
  303. (rum/use-effect!
  304. (fn []
  305. (p/let [matched-properties (editor-handler/<get-matched-properties q)]
  306. (set-matched-properties! matched-properties)))
  307. [q])
  308. (let [q-property (string/replace (string/lower-case q) #"\s+" "-")
  309. non-exist-handler (fn [_state]
  310. ((editor-handler/property-on-chosen-handler id q-property) nil))]
  311. (ui/auto-complete
  312. matched-properties
  313. {:on-chosen (editor-handler/property-on-chosen-handler id q-property)
  314. :on-enter non-exist-handler
  315. :empty-placeholder [:div.px-4.py-2.text-sm (str "Create a new property: " q-property)]
  316. :header [:div.px-4.py-2.text-sm.font-medium "Matched properties: "]
  317. :item-render (fn [property] property)
  318. :class "black"}))))))
  319. (rum/defc property-value-search-aux
  320. [id property q]
  321. (let [[values set-values!] (rum/use-state nil)]
  322. (rum/use-effect!
  323. (fn []
  324. (p/let [result (editor-handler/get-matched-property-values property q)]
  325. (set-values! result)))
  326. [property q])
  327. (ui/auto-complete
  328. values
  329. {:on-chosen (editor-handler/property-value-on-chosen-handler id q)
  330. :on-enter (fn [_state]
  331. ((editor-handler/property-value-on-chosen-handler id q) nil))
  332. :empty-placeholder [:div.px-4.py-2.text-sm (str "Create a new property value: " q)]
  333. :header [:div.px-4.py-2.text-sm.font-medium "Matched property values: "]
  334. :item-render (fn [property-value] property-value)
  335. :class "black"})))
  336. (rum/defc property-value-search < rum/reactive
  337. [id]
  338. (let [property (:property (state/get-editor-action-data))
  339. input (gdom/getElement id)]
  340. (when (and input
  341. (not (string/blank? property)))
  342. (let [current-pos (cursor/pos input)
  343. edit-content (state/sub-edit-content)
  344. start-idx (string/last-index-of (subs edit-content 0 current-pos)
  345. gp-property/colons)
  346. q (or
  347. (when (>= current-pos (+ start-idx 2))
  348. (subs edit-content (+ start-idx 2) current-pos))
  349. "")
  350. q (string/triml q)]
  351. (property-value-search-aux id property q)))))
  352. (rum/defc code-block-mode-keyup-listener
  353. [_q _edit-content last-pos current-pos]
  354. (rum/use-effect!
  355. (fn []
  356. (when (< current-pos last-pos)
  357. (state/clear-editor-action!)))
  358. [last-pos current-pos])
  359. [:<>])
  360. (rum/defc code-block-mode-picker < rum/reactive
  361. [id format]
  362. (when-let [modes (some->> js/window.CodeMirror (.-modes) (js/Object.keys) (js->clj) (remove #(= "null" %)))]
  363. (when-let [input (gdom/getElement id)]
  364. (let [pos (state/get-editor-last-pos)
  365. current-pos (cursor/pos input)
  366. edit-content (or (state/sub-edit-content) "")
  367. q (or (editor-handler/get-selected-text)
  368. (common-util/safe-subs edit-content pos current-pos)
  369. "")
  370. matched (seq (fuzzy-search modes q))
  371. matched (or matched (if (string/blank? q) modes [q]))]
  372. [:div
  373. (code-block-mode-keyup-listener q edit-content pos current-pos)
  374. (ui/auto-complete matched
  375. {:on-chosen (fn [chosen _click?]
  376. (state/clear-editor-action!)
  377. (let [prefix (str "```" chosen)
  378. last-pattern (str "```" q)]
  379. (editor-handler/insert-command! id
  380. prefix format {:last-pattern last-pattern})
  381. (commands/handle-step [:codemirror/focus])))
  382. :on-enter (fn []
  383. (state/clear-editor-action!)
  384. (commands/handle-step [:codemirror/focus]))
  385. :item-render (fn [mode _chosen?]
  386. [:strong mode])
  387. :class "code-block-mode-picker"})]))))
  388. (rum/defcs input < rum/reactive
  389. (rum/local {} ::input-value)
  390. (mixins/event-mixin
  391. (fn [state]
  392. (mixins/on-key-down
  393. state
  394. {;; enter
  395. 13 (fn [state e]
  396. (let [input-value (get state ::input-value)
  397. input-option (:options (state/get-editor-show-input))]
  398. (when (seq @input-value)
  399. ;; no new line input
  400. (util/stop e)
  401. (let [[_id on-submit] (:rum/args state)
  402. command (:command (first input-option))]
  403. (on-submit command @input-value))
  404. (reset! input-value nil))))
  405. ;; escape
  406. 27 (fn [_state _e]
  407. (let [[id _on-submit on-cancel] (:rum/args state)]
  408. (on-cancel id)))})))
  409. [state _id on-submit _on-cancel]
  410. (when-let [action-data (state/get-editor-action-data)]
  411. (let [{:keys [pos options]} action-data
  412. input-value (get state ::input-value)]
  413. (when (seq options)
  414. (let [command (:command (first options))]
  415. [:div.p-2.rounded-md
  416. (for [{:keys [id placeholder type autoFocus] :as input-item} options]
  417. [:div.my-3 {:key id}
  418. [:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
  419. (merge
  420. (cond->
  421. {:key (str "modal-input-" (name id))
  422. :id (str "modal-input-" (name id))
  423. :type (or type "text")
  424. :on-change (fn [e]
  425. (swap! input-value assoc id (util/evalue e)))
  426. :auto-complete (if (util/chrome?) "chrome-off" "off")}
  427. placeholder
  428. (assoc :placeholder placeholder)
  429. autoFocus
  430. (assoc :auto-focus true))
  431. (dissoc input-item :id))]])
  432. (ui/button
  433. "Submit"
  434. :on-click
  435. (fn [e]
  436. (util/stop e)
  437. (on-submit command @input-value pos)))])))))
  438. (rum/defc absolute-modal < rum/static
  439. [cp modal-name set-default-width? {:keys [top left rect]}]
  440. (let [MAX-HEIGHT 700
  441. MAX-HEIGHT' 600
  442. MAX-WIDTH 600
  443. SM-MAX-WIDTH 300
  444. Y-BOUNDARY-HEIGHT 150
  445. vw-width js/window.innerWidth
  446. vw-height js/window.innerHeight
  447. vw-max-width (- vw-width (:left rect))
  448. vw-max-height (- vw-height (:top rect))
  449. vw-max-height' (:top rect)
  450. sm? (< vw-width 415)
  451. max-height (min (- vw-max-height 20) MAX-HEIGHT)
  452. max-height' (min (- vw-max-height' 70) MAX-HEIGHT')
  453. max-width (if sm? SM-MAX-WIDTH (min (max 400 (/ vw-max-width 2)) MAX-WIDTH))
  454. offset-top 24
  455. to-max-height (cond-> (if (and (seq rect) (> vw-height max-height))
  456. (let [delta-height (- vw-height (+ (:top rect) top offset-top))]
  457. (if (< delta-height max-height)
  458. (- (max (* 2 offset-top) delta-height) 16)
  459. max-height))
  460. max-height)
  461. (= modal-name "commands")
  462. (min 500))
  463. right-sidebar? (:ui/sidebar-open? @state/state)
  464. editing-key (state/get-edit-input-id)
  465. *el (rum/use-ref nil)
  466. y-overflow-vh? (or (< to-max-height Y-BOUNDARY-HEIGHT)
  467. (> (- max-height' to-max-height) Y-BOUNDARY-HEIGHT))
  468. to-max-height (if y-overflow-vh? max-height' to-max-height)
  469. pos-rect (when (and (seq rect) editing-key)
  470. (:rect (cursor/get-caret-pos (state/get-input))))
  471. y-diff (when pos-rect (- (:height pos-rect) (:height rect)))
  472. style (merge
  473. {:top (+ top offset-top (if (int? y-diff) y-diff 0))
  474. :max-height to-max-height
  475. :max-width 700
  476. ;; TODO: auto responsive fixed size
  477. :width "fit-content"
  478. :z-index 11}
  479. (when set-default-width?
  480. {:width max-width})
  481. (if (<= vw-max-width (+ left (if set-default-width? max-width 500)))
  482. {:right 0}
  483. {:left 0}))]
  484. (rum/use-effect!
  485. (fn []
  486. (when-let [^js/HTMLElement cnt
  487. (and right-sidebar? editing-key
  488. (js/document.querySelector "#main-content-container"))]
  489. (when (.contains cnt (js/document.querySelector (str "#" editing-key)))
  490. (let [el (rum/deref *el)
  491. ofx (- (.-scrollWidth cnt) (.-clientWidth cnt))]
  492. (when (> ofx 0)
  493. (set! (.-transform (.-style el))
  494. (util/format "translate(-%spx, %s)" (+ ofx 20) (if y-overflow-vh? "calc(-100% - 2rem)" 0))))))))
  495. [right-sidebar? editing-key y-overflow-vh?])
  496. ;; HACK: close when click outside for classic editing models (popup)
  497. (rum/use-effect!
  498. (fn []
  499. (let [^js cnt js/document.body
  500. handle (fn [^js e]
  501. (when-not (some->> (.-target e) (.contains (rum/deref *el)))
  502. (state/clear-editor-action!)))]
  503. (.addEventListener cnt "click" handle false)
  504. #(.removeEventListener cnt "click" handle)))
  505. [])
  506. [:div.absolute.rounded-md.shadow-lg.absolute-modal
  507. {:ref *el
  508. :data-modal-name modal-name
  509. :class (if y-overflow-vh? "is-overflow-vh-y" "")
  510. :on-pointer-down (fn [e]
  511. (.stopPropagation e))
  512. :on-key-down (fn [^js e]
  513. (case (.-key e)
  514. "Escape"
  515. (do (state/clear-editor-action!)
  516. (some-> (state/get-input)
  517. (.focus)))
  518. :dune)
  519. (util/stop-propagation e))
  520. :style style}
  521. cp]))
  522. (rum/defc transition-cp < rum/reactive
  523. [cp modal-name set-default-width?]
  524. (when-let [pos (:pos (state/sub :editor/action-data))]
  525. (ui/css-transition
  526. {:class-names "fade"
  527. :timeout {:enter 500
  528. :exit 300}}
  529. (absolute-modal cp modal-name set-default-width? pos))))
  530. (rum/defc image-uploader < rum/reactive
  531. [id format]
  532. [:div.image-uploader
  533. [:input
  534. {:id "upload-file"
  535. :type "file"
  536. :on-change (fn [e]
  537. (let [files (.-files (.-target e))]
  538. (editor-handler/upload-asset id files format editor-handler/*asset-uploading? false)))
  539. :hidden true}]
  540. #_:clj-kondo/ignore
  541. (when-let [uploading? (util/react editor-handler/*asset-uploading?)]
  542. (let [processing (util/react editor-handler/*asset-uploading-process)]
  543. (transition-cp
  544. [:div.flex.flex-row.align-center.rounded-md.shadow-sm.bg-base-2.px-1.py-1
  545. (ui/loading
  546. (util/format "Uploading %s%" (util/format "%2d" processing)))]
  547. "upload-file"
  548. false)))])
  549. (defn- set-up-key-down!
  550. [state format]
  551. (mixins/on-key-down
  552. state
  553. {}
  554. {:not-matched-handler (editor-handler/keydown-not-matched-handler format)}))
  555. (defn- set-up-key-up!
  556. [state input input-id]
  557. (mixins/on-key-up
  558. state
  559. {}
  560. (editor-handler/keyup-handler state input input-id)))
  561. (def search-timeout (atom nil))
  562. (defn- setup-key-listener!
  563. [state]
  564. (let [{:keys [id format]} (get-state)
  565. input-id id
  566. input (gdom/getElement input-id)]
  567. (set-up-key-down! state format)
  568. (set-up-key-up! state input input-id)))
  569. (defn get-editor-style-class
  570. "Get textarea css class according to it's content"
  571. [block content format]
  572. (let [content (if content (str content) "")
  573. properties (:block/properties block)
  574. heading (pu/lookup properties :logseq.property/heading)
  575. heading (if (true? heading)
  576. (min (inc (:block/level block)) 6)
  577. heading)]
  578. ;; as the function is binding to the editor content, optimization is welcome
  579. (str
  580. (if (or (> (.-length content) 1000)
  581. (string/includes? content "\n"))
  582. "multiline-block"
  583. "uniline-block")
  584. " "
  585. (case format
  586. :markdown
  587. (cond
  588. heading (str "h" heading)
  589. (string/starts-with? content "# ") "h1"
  590. (string/starts-with? content "## ") "h2"
  591. (string/starts-with? content "### ") "h3"
  592. (string/starts-with? content "#### ") "h4"
  593. (string/starts-with? content "##### ") "h5"
  594. (string/starts-with? content "###### ") "h6"
  595. (and (string/starts-with? content "---\n") (.endsWith content "\n---")) "page-properties"
  596. :else "normal-block")
  597. ;; other formats
  598. (cond
  599. heading (str "h" heading)
  600. (and (string/starts-with? content "---\n") (.endsWith content "\n---")) "page-properties"
  601. :else "normal-block")))))
  602. (defn editor-row-height-unchanged?
  603. "Check if the row height of editor textarea is changed, which happens when font-size changed"
  604. []
  605. ;; FIXME: assuming enter key is the only trigger of the height changing (under markdown editing of headlines)
  606. ;; FIXME: looking for an elegant & robust way to track the change of font-size, or wait for our own WYSIWYG text area
  607. (let [last-key (state/get-last-key-code)]
  608. (and (not= keycode/enter (:key-code last-key))
  609. (not= keycode/enter-code (:code last-key)))))
  610. (rum/defc mock-textarea <
  611. rum/static
  612. {:did-update
  613. (fn [state]
  614. (when-not @(:editor/on-paste? @state/state)
  615. (try (editor-handler/handle-last-input)
  616. (catch :default _e
  617. nil)))
  618. (state/set-state! :editor/on-paste? false)
  619. state)}
  620. [content]
  621. [:div#mock-text
  622. {:style {:width "100%"
  623. :height "100%"
  624. :position "absolute"
  625. :visibility "hidden"
  626. :top 0
  627. :left 0}}
  628. (let [content (str content "0")
  629. graphemes (util/split-graphemes content)
  630. graphemes-char-index (reductions #(+ %1 (count %2)) 0 graphemes)]
  631. (for [[idx c] (into (sorted-map) (zipmap graphemes-char-index graphemes))]
  632. (if (= c "\n")
  633. [:span {:id (str "mock-text_" idx)
  634. :key idx} "0" [:br]]
  635. [:span {:id (str "mock-text_" idx)
  636. :key idx} c])))])
  637. (rum/defc animated-modal < rum/reactive
  638. [modal-name component set-default-width?]
  639. (when-let [pos (:pos (state/get-editor-action-data))]
  640. (ui/css-transition
  641. {:key modal-name
  642. :class-names {:enter "origin-top-left opacity-0 transform scale-95"
  643. :enter-done "origin-top-left transition opacity-100 transform scale-100"
  644. :exit "origin-top-left transition opacity-0 transform scale-95"}
  645. :timeout {:enter 0
  646. :exit 150}}
  647. (fn [_]
  648. (absolute-modal
  649. component
  650. modal-name
  651. set-default-width?
  652. pos)))))
  653. (rum/defc editor-action-query-wrap
  654. [trigger children & {:keys [on-input-keydown sub-input-keydown?]}]
  655. (let [[q set-q!] (rum/use-state "")
  656. [keydown-e set-keydown-e!] (rum/use-state nil)]
  657. (rum/use-effect!
  658. (fn []
  659. (when-let [^js input (state/get-input)]
  660. (let [keyup-handle (fn []
  661. (let [content (.-value input)
  662. pos (some-> (cursor/get-caret-pos input) :pos)
  663. content (subs content 0 pos)
  664. pos (string/last-index-of content trigger)
  665. content (subs content (inc pos))]
  666. (-> (p/delay 300)
  667. (p/then #(set-q! content)))))
  668. keydown-handle (fn [^js e]
  669. (when-not (false? (when (fn? on-input-keydown)
  670. (on-input-keydown e)))
  671. (case (.-key e)
  672. ("ArrowUp" "ArrowDown")
  673. (.preventDefault e)
  674. :dune))
  675. (when sub-input-keydown?
  676. (set-keydown-e! e)))]
  677. (doto input
  678. (.addEventListener "keyup" keyup-handle false)
  679. (.addEventListener "keydown" keydown-handle false))
  680. #(doto input
  681. (.removeEventListener "keyup" keyup-handle)
  682. (.removeEventListener "keydown" keydown-handle)))))
  683. [])
  684. (children q keydown-e)))
  685. (rum/defc ask-ai-content
  686. [query {:keys [id format action ^js keydown-e]}]
  687. (let [*el (rum/use-ref nil)]
  688. (rum/use-effect!
  689. (fn []
  690. (when keydown-e
  691. (when (contains? #{"ArrowUp" "ArrowDown"} (.-key keydown-e))
  692. (some-> (rum/deref *el)
  693. (.querySelector ".ui__button")
  694. (.focus)))))
  695. [keydown-e])
  696. [:h1
  697. {:ref *el}
  698. [:p.text-sm [:blockquote id]]
  699. [:p "TODO: " (str action) [:code query]]
  700. [:p (shui/button
  701. {:size :sm
  702. :on-click (fn []
  703. (editor-handler/insert-command!
  704. id #(util/format " [[%s]] " query)
  705. format
  706. {:restore? true
  707. :backward-truncate-number (inc (count query))
  708. :command action})
  709. )} query)]
  710. [:p "input key: " (shui/badge (some-> keydown-e (.-key)))]]))
  711. (defn- exist-editor-commands-popup?
  712. []
  713. (some->> (shui-popup/get-popups)
  714. (some #(some-> % (:id) (str) (string/starts-with? ":editor.commands")))))
  715. ;; TODO: [WIP]
  716. (rum/defc shui-models
  717. [id format action _data]
  718. (rum/use-effect!
  719. (fn []
  720. (let [{:keys [left top rect]} (cursor/get-caret-pos (state/get-input))
  721. pos [(+ left (:left rect) -20) (+ top (:top rect) 20)]]
  722. (let [pid (case action
  723. :commands
  724. (shui/popup-show! pos
  725. (commands id format)
  726. {:id :editor.commands/commands
  727. :align :start
  728. :root-props {:onOpenChange
  729. #(when-not %
  730. (when (= :commands (state/get-editor-action))
  731. (state/clear-editor-action!)))}
  732. :content-props {:onOpenAutoFocus #(.preventDefault %)
  733. :onCloseAutoFocus #(.preventDefault %)
  734. :data-editor-popup-ref "commands"}
  735. :force-popover? true})
  736. :block-commands
  737. (shui/popup-show! pos
  738. (block-commands id format)
  739. {:id :editor.commands/block-commands
  740. :align :start
  741. :root-props {:onOpenChange
  742. #(when-not %
  743. (when (= :block-commands (state/get-editor-action))
  744. (state/clear-editor-action!)))}
  745. :content-props {:onOpenAutoFocus #(.preventDefault %)
  746. :onCloseAutoFocus #(.preventDefault %)
  747. :data-editor-popup-ref "commands"}
  748. :force-popover? true})
  749. :datepicker
  750. (shui/popup-show!
  751. pos (datetime-comp/date-picker id format nil)
  752. {:id :editor.commands/datepicker
  753. :align :start
  754. :root-props {:onOpenChange #(when-not % (state/clear-editor-action!))}
  755. :content-props {:onOpenAutoFocus #(.preventDefault %)
  756. :data-editor-popup-ref "datepicker"}
  757. :force-popover? true})
  758. :input
  759. (shui/popup-show!
  760. pos (input id
  761. (fn [command m]
  762. (editor-handler/handle-command-input command id format m))
  763. (fn []
  764. (editor-handler/handle-command-input-close id)))
  765. {:id :editor.commands/input
  766. :align :start
  767. :root-props {:onOpenChange #(when-not % (state/clear-editor-action!))}
  768. :content-props {:onOpenAutoFocus #(.preventDefault %)
  769. :onCloseAutoFocus #(.preventDefault %)
  770. :data-editor-popup-ref "input"}})
  771. :select-code-block-mode
  772. (shui/popup-show!
  773. pos (code-block-mode-picker id format)
  774. {:id :editor.commands/code-block-mode-picker
  775. :align :start
  776. :root-props {:onOpenChange #(when-not % (state/clear-editor-action!))}
  777. :content-props {:onOpenAutoFocus #(.preventDefault %)
  778. :data-editor-popup-ref "code-block-mode-picker"}
  779. :force-popover? true})
  780. :editor.action/ask-ai
  781. (shui/popup-show!
  782. pos (editor-action-query-wrap
  783. commands/command-ask
  784. (fn [query ^js keydown-e]
  785. (ask-ai-content query
  786. {:id id :format format :action action :keydown-e keydown-e}))
  787. {:sub-input-keydown? true})
  788. {:id :editor.commands/ask-ai
  789. :align :start
  790. :root-props {:onOpenChange #(when-not % (state/clear-editor-action!))}
  791. :content-props {:onOpenAutoFocus #(.preventDefault %)}
  792. :force-popover? true})
  793. ;; TODO: try remove local model state
  794. false)]
  795. #(when pid
  796. (shui/popup-hide! pid)))))
  797. [action])
  798. [:<>])
  799. (rum/defc modals < rum/reactive
  800. "React to atom changes, find and render the correct modal"
  801. [id format]
  802. (let [action (state/sub :editor/action)]
  803. [:<>
  804. (shui-models id format action nil)
  805. (cond
  806. (= action :commands-classic)
  807. (animated-modal "commands" (commands id format) true)
  808. (= action :block-commands-classic)
  809. (animated-modal "block-commands" (block-commands id format) true)
  810. (contains? #{:page-search :page-search-hashtag} action)
  811. (animated-modal "page-search" (page-search id format) true)
  812. (= :block-search action)
  813. (animated-modal "block-search" (block-search id format) true)
  814. (= :template-search action)
  815. (animated-modal "template-search" (template-search id format) true)
  816. (= :property-search action)
  817. (animated-modal "property-search" (property-search id) true)
  818. (= :property-value-search action)
  819. (animated-modal "property-value-search" (property-value-search id) true)
  820. (= :zotero action)
  821. (animated-modal "zotero-search" (zotero/zotero-search id) false)
  822. :else
  823. nil)]))
  824. (defn- editor-on-blur
  825. [^js e]
  826. (cond
  827. (let [action (state/get-editor-action)]
  828. (or (contains?
  829. #{:commands :block-commands
  830. :page-search :page-search-hashtag :block-search :template-search
  831. :property-search :property-value-search
  832. :datepicker} action)
  833. (and (keyword? action)
  834. (= (namespace action) "editor.action"))))
  835. ;; FIXME: This should probably be handled as a keydown handler in editor, but this handler intercepts Esc first
  836. (util/stop e)
  837. ;; editor/input component handles Escape directly, so just prevent handling it here
  838. (= :input (state/get-editor-action))
  839. nil
  840. :else
  841. (let [{:keys [on-hide value]} (editor-handler/get-state)]
  842. (when on-hide
  843. (on-hide value nil)))))
  844. (rum/defcs box < rum/reactive
  845. {:init (fn [state]
  846. (assoc state ::id (str (random-uuid))))
  847. :did-mount (fn [state]
  848. (state/set-editor-args! (:rum/args state))
  849. state)}
  850. (mixins/event-mixin setup-key-listener!)
  851. lifecycle/lifecycle
  852. [state {:keys [format block parent-block on-hide]} id config]
  853. (let [content (state/sub-edit-content (:block/uuid block))
  854. heading-class (get-editor-style-class block content format)
  855. opts (cond->
  856. {:id id
  857. :cacheMeasurements (editor-row-height-unchanged?) ;; check when content updated (as the content variable is binded)
  858. :default-value (or content "")
  859. :minRows (if (state/enable-grammarly?) 2 1)
  860. :on-click (editor-handler/editor-on-click! id)
  861. :on-change (editor-handler/editor-on-change! block id search-timeout)
  862. :on-paste (paste-handler/editor-on-paste! id)
  863. :on-blur (fn [e]
  864. (if-let [on-blur (:on-blur config)]
  865. (on-blur e)
  866. (editor-on-blur e)))
  867. :on-key-down (fn [e]
  868. (if-let [on-key-down (:on-key-down config)]
  869. (on-key-down e)
  870. (when (and (= (util/ekey e) "Escape") on-hide)
  871. (when-not (exist-editor-commands-popup?)
  872. (on-hide content :esc)))))
  873. :auto-focus true
  874. :class heading-class}
  875. (some? parent-block)
  876. (assoc :parentblockid (str (:block/uuid parent-block)))
  877. true
  878. (merge (:editor-opts config)))]
  879. [:div.editor-inner.flex.flex-1 {:class (if block "block-editor" "non-block-editor")}
  880. (ui/ls-textarea opts)
  881. (mock-textarea content)
  882. (modals id format)
  883. (when format
  884. (image-uploader id format))]))