editor.cljs 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828
  1. (ns frontend.components.editor
  2. (:require [rum.core :as rum]
  3. [frontend.components.svg :as svg]
  4. [frontend.config :as config]
  5. [frontend.handler.editor :as editor-handler :refer [get-state]]
  6. [frontend.util :as util :refer-macros [profile]]
  7. [frontend.handler.file :as file]
  8. [frontend.handler.block :as block-handler]
  9. [frontend.handler.page :as page-handler]
  10. [frontend.handler.editor.keyboards :as keyboards-handler]
  11. [frontend.components.datetime :as datetime-comp]
  12. [promesa.core :as p]
  13. [frontend.state :as state]
  14. [frontend.mixins :as mixins]
  15. [frontend.ui :as ui]
  16. [frontend.db :as db]
  17. [frontend.config :as config]
  18. [frontend.handler.web.nfs :as nfs]
  19. [dommy.core :as d]
  20. [goog.object :as gobj]
  21. [goog.dom :as gdom]
  22. [clojure.string :as string]
  23. [clojure.set :as set]
  24. [cljs.core.match :refer-macros [match]]
  25. [frontend.commands :as commands
  26. :refer [*show-commands
  27. *matched-commands
  28. *slash-caret-pos
  29. *angle-bracket-caret-pos
  30. *matched-block-commands
  31. *show-block-commands]]
  32. [medley.core :as medley]
  33. [cljs-drag-n-drop.core :as dnd]
  34. [frontend.text :as text]
  35. ["/frontend/utils" :as utils]))
  36. (rum/defc commands < rum/reactive
  37. [id format]
  38. (when (and (util/react *show-commands)
  39. @*slash-caret-pos
  40. (not (state/sub :editor/show-page-search?))
  41. (not (state/sub :editor/show-block-search?))
  42. (not (state/sub :editor/show-template-search?))
  43. (not (state/sub :editor/show-input))
  44. (not (state/sub :editor/show-date-picker?)))
  45. (let [matched (util/react *matched-commands)]
  46. (ui/auto-complete
  47. (map first matched)
  48. {:on-chosen (fn [chosen]
  49. (reset! commands/*current-command chosen)
  50. (let [command-steps (get (into {} matched) chosen)
  51. restore-slash? (or
  52. (contains? #{"Today" "Yesterday" "Tomorrow"} chosen)
  53. (and
  54. (not (fn? command-steps))
  55. (not (contains? (set (map first command-steps)) :editor/input))
  56. (not (contains? #{"Date Picker" "Template" "Deadline" "Scheduled" "Upload an image"} chosen))))]
  57. (editor-handler/insert-command! id command-steps
  58. format
  59. {:restore? restore-slash?})))
  60. :class "black"}))))
  61. (rum/defc block-commands < rum/reactive
  62. [id format]
  63. (when (and (util/react *show-block-commands)
  64. @*angle-bracket-caret-pos)
  65. (let [matched (util/react *matched-block-commands)]
  66. (ui/auto-complete
  67. (map first matched)
  68. {:on-chosen (fn [chosen]
  69. (editor-handler/insert-command! id (get (into {} matched) chosen)
  70. format
  71. {:last-pattern commands/angle-bracket}))
  72. :class "black"}))))
  73. (rum/defc page-search < rum/reactive
  74. {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
  75. [id format]
  76. (when (state/sub :editor/show-page-search?)
  77. (let [pos (:editor/last-saved-cursor @state/state)
  78. input (gdom/getElement id)]
  79. (when input
  80. (let [current-pos (:pos (util/get-caret-pos input))
  81. edit-content (state/sub [:editor/content id])
  82. edit-block (state/sub :editor/block)
  83. q (or
  84. @editor-handler/*selected-text
  85. (when (state/sub :editor/show-page-search-hashtag?)
  86. (util/safe-subs edit-content pos current-pos))
  87. (when (> (count edit-content) current-pos)
  88. (util/safe-subs edit-content pos current-pos)))
  89. matched-pages (when-not (string/blank? q)
  90. (editor-handler/get-matched-pages q))
  91. chosen-handler (if (state/sub :editor/show-page-search-hashtag?)
  92. (fn [chosen _click?]
  93. (state/set-editor-show-page-search! false)
  94. (let [chosen (if (re-find #"\s+" chosen)
  95. (util/format "[[%s]]" chosen)
  96. chosen)]
  97. (editor-handler/insert-command! id
  98. (str "#" chosen)
  99. format
  100. {:last-pattern (str "#" (if @editor-handler/*selected-text "" q))})))
  101. (fn [chosen _click?]
  102. (state/set-editor-show-page-search! false)
  103. (let [page-ref-text (page-handler/get-page-ref-text chosen)]
  104. (editor-handler/insert-command! id
  105. page-ref-text
  106. format
  107. {:last-pattern (str "[[" (if @editor-handler/*selected-text "" q))
  108. :postfix-fn (fn [s] (util/replace-first "]]" s ""))}))))
  109. non-exist-page-handler (fn [_state]
  110. (state/set-editor-show-page-search! false)
  111. (if (state/org-mode-file-link? (state/get-current-repo))
  112. (let [page-ref-text (page-handler/get-page-ref-text q)
  113. value (gobj/get input "value")
  114. old-page-ref (util/format "[[%s]]" q)
  115. new-value (string/replace value
  116. old-page-ref
  117. page-ref-text)]
  118. (state/set-edit-content! id new-value)
  119. (let [new-pos (+ current-pos
  120. (- (count page-ref-text)
  121. (count old-page-ref))
  122. 2)]
  123. (util/move-cursor-to input new-pos)))
  124. (util/cursor-move-forward input 2)))]
  125. (ui/auto-complete
  126. matched-pages
  127. {:on-chosen chosen-handler
  128. :on-enter non-exist-page-handler
  129. :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a page"]
  130. :class "black"}))))))
  131. (rum/defcs block-search < rum/reactive
  132. {:will-unmount (fn [state]
  133. (reset! editor-handler/*selected-text nil)
  134. (state/clear-search-result!)
  135. state)}
  136. [state id format]
  137. (when (state/sub :editor/show-block-search?)
  138. (let [pos (:editor/last-saved-cursor @state/state)
  139. input (gdom/getElement id)
  140. [id format] (:rum/args state)
  141. current-pos (:pos (util/get-caret-pos input))
  142. edit-content (state/sub [:editor/content id])
  143. edit-block (state/get-edit-block)
  144. q (or
  145. @editor-handler/*selected-text
  146. (when (> (count edit-content) current-pos)
  147. (subs edit-content pos current-pos)))
  148. _ (p/let [matched-blocks (when-not (string/blank? q)
  149. (editor-handler/get-matched-blocks q (:block/uuid edit-block)))]
  150. (state/set-search-result! matched-blocks))
  151. matched-blocks (state/sub :search/result)]
  152. (when input
  153. (let [chosen-handler (fn [chosen _click?]
  154. (state/set-editor-show-block-search! false)
  155. (let [uuid-string (str (:block/uuid chosen))]
  156. ;; block reference
  157. (editor-handler/insert-command! id
  158. (util/format "((%s))" uuid-string)
  159. format
  160. {:last-pattern (str "((" (if @editor-handler/*selected-text "" q))
  161. :postfix-fn (fn [s] (util/replace-first "))" s ""))})
  162. ;; Save it so it'll be parsed correctly in the future
  163. (editor-handler/set-block-property! (:block/uuid chosen)
  164. "ID"
  165. uuid-string)
  166. (when-let [input (gdom/getElement id)]
  167. (.focus input))))
  168. non-exist-block-handler (fn [_state]
  169. (state/set-editor-show-block-search! false)
  170. (util/cursor-move-forward input 2))]
  171. (ui/auto-complete
  172. matched-blocks
  173. {:on-chosen chosen-handler
  174. :on-enter non-exist-block-handler
  175. :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a block"]
  176. :item-render (fn [{:block/keys [content]}]
  177. (subs content 0 64))
  178. :class "black"}))))))
  179. (rum/defc template-search < rum/reactive
  180. {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
  181. [id format]
  182. (when (state/sub :editor/show-template-search?)
  183. (let [pos (:editor/last-saved-cursor @state/state)
  184. input (gdom/getElement id)]
  185. (when input
  186. (let [current-pos (:pos (util/get-caret-pos input))
  187. edit-content (state/sub [:editor/content id])
  188. edit-block (state/sub :editor/block)
  189. q (or
  190. (when (>= (count edit-content) current-pos)
  191. (subs edit-content pos current-pos))
  192. "")
  193. matched-templates (editor-handler/get-matched-templates q)
  194. chosen-handler (fn [[template db-id] _click?]
  195. (if-let [block (db/entity db-id)]
  196. (let [new-level (:block/level edit-block)
  197. template-parent-level (:block/level block)
  198. pattern (config/get-block-pattern format)
  199. content
  200. (block-handler/get-block-full-content
  201. (state/get-current-repo)
  202. (:block/uuid block)
  203. (fn [{:block/keys [level content properties] :as block}]
  204. (let [new-level (+ new-level (- level template-parent-level))
  205. properties' (dissoc (into {} properties) "id" "custom_id" "template")]
  206. (-> content
  207. (string/replace-first (apply str (repeat level pattern))
  208. (apply str (repeat new-level pattern)))
  209. text/remove-properties!
  210. (text/rejoin-properties properties')))))
  211. content (if (string/includes? (string/trim edit-content) "\n")
  212. content
  213. (text/remove-level-spaces content format))
  214. content (editor-handler/resolve-dynamic-template! content)]
  215. (state/set-editor-show-template-search! false)
  216. (editor-handler/insert-command! id
  217. content
  218. format
  219. {})))
  220. (when-let [input (gdom/getElement id)]
  221. (.focus input)))
  222. non-exist-handler (fn [_state]
  223. (state/set-editor-show-template-search! false))]
  224. (ui/auto-complete
  225. matched-templates
  226. {:on-chosen chosen-handler
  227. :on-enter non-exist-handler
  228. :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a template"]
  229. :item-render (fn [[template _block-db-id]]
  230. template)
  231. :class "black"}))))))
  232. (rum/defc mobile-bar < rum/reactive
  233. [parent-state parent-id]
  234. [:div#mobile-editor-toolbar.bg-base-2.fix-ios-fixed-bottom
  235. [:button.bottom-action
  236. {:on-click #(editor-handler/adjust-block-level! parent-state :right)}
  237. svg/indent-block]
  238. [:button.bottom-action
  239. {:on-click #(editor-handler/adjust-block-level! parent-state :left)}
  240. svg/outdent-block]
  241. [:button.bottom-action
  242. {:on-click #(editor-handler/move-up-down % true)}
  243. svg/move-up-block]
  244. [:button.bottom-action
  245. {:on-click #(editor-handler/move-up-down % false)}
  246. svg/move-down-block]
  247. [:button.bottom-action
  248. {:on-click #(commands/simple-insert! parent-id "\n" {})}
  249. svg/multi-line-input]
  250. [:button.bottom-action
  251. {:on-click #(commands/insert-before! parent-id "TODO " {})}
  252. svg/checkbox]
  253. [:button.font-extrabold.bottom-action.-mt-1
  254. {:on-click #(commands/simple-insert!
  255. parent-id "[[]]"
  256. {:backward-pos 2
  257. :check-fn (fn [_ _ new-pos]
  258. (reset! commands/*slash-caret-pos new-pos)
  259. (commands/handle-step [:editor/search-page]))})}
  260. "[[]]"]
  261. [:button.font-extrabold.bottom-action.-mt-1
  262. {:on-click #(commands/simple-insert!
  263. parent-id "(())"
  264. {:backward-pos 2
  265. :check-fn (fn [_ _ new-pos]
  266. (reset! commands/*slash-caret-pos new-pos)
  267. (commands/handle-step [:editor/search-block]))})}
  268. "(())"]
  269. [:button.font-extrabold.bottom-action.-mt-1
  270. {:on-click #(commands/simple-insert! parent-id "/" {})}
  271. "/"]])
  272. (rum/defcs input < rum/reactive
  273. (rum/local {} ::input-value)
  274. (mixins/event-mixin
  275. (fn [state]
  276. (mixins/on-key-down
  277. state
  278. {;; enter
  279. 13 (fn [state e]
  280. (let [input-value (get state ::input-value)
  281. input-option (get @state/state :editor/show-input)]
  282. (when (seq @input-value)
  283. ;; no new line input
  284. (util/stop e)
  285. (let [[_id on-submit] (:rum/args state)
  286. {:keys [pos]} @*slash-caret-pos
  287. command (:command (first input-option))]
  288. (on-submit command @input-value pos))
  289. (reset! input-value nil))))})))
  290. {:did-update
  291. (fn [state]
  292. (when-let [show-input (state/get-editor-show-input)]
  293. (let [id (str "modal-input-"
  294. (name (:id (first show-input))))
  295. first-input (gdom/getElement id)]
  296. (when (and first-input
  297. (not (d/has-class? first-input "focused")))
  298. (.focus first-input)
  299. (d/add-class! first-input "focused"))))
  300. state)}
  301. [state id on-submit]
  302. (when-let [input-option (state/sub :editor/show-input)]
  303. (let [{:keys [pos]} (util/react *slash-caret-pos)
  304. input-value (get state ::input-value)]
  305. (when (seq input-option)
  306. (let [command (:command (first input-option))]
  307. [:div.p-2.mt-2.rounded-md.shadow-sm.bg-base-2
  308. (for [{:keys [id placeholder type] :as input-item} input-option]
  309. [:div.my-3
  310. [:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
  311. (merge
  312. (cond->
  313. {:key (str "modal-input-" (name id))
  314. :id (str "modal-input-" (name id))
  315. :type (or type "text")
  316. :on-change (fn [e]
  317. (swap! input-value assoc id (util/evalue e)))
  318. :auto-complete (if (util/chrome?) "chrome-off" "off")}
  319. placeholder
  320. (assoc :placeholder placeholder))
  321. (dissoc input-item :id))]])
  322. (ui/button
  323. "Submit"
  324. :on-click
  325. (fn [e]
  326. (util/stop e)
  327. (on-submit command @input-value pos)))])))))
  328. (rum/defc absolute-modal < rum/static
  329. [cp set-default-width? {:keys [top left rect]}]
  330. (let [max-height 500
  331. max-width 300
  332. offset-top 24
  333. vw-height js/window.innerHeight
  334. vw-width js/window.innerWidth
  335. to-max-height (if (and (seq rect) (> vw-height max-height))
  336. (let [delta-height (- vw-height (+ (:top rect) top offset-top))]
  337. (if (< delta-height max-height)
  338. (- (max (* 2 offset-top) delta-height) 16)
  339. max-height))
  340. max-height)
  341. x-overflow? (if (and (seq rect) (> vw-width max-width))
  342. (let [delta-width (- vw-width (+ (:left rect) left))]
  343. (< delta-width (* max-width 0.5))))] ;; FIXME: for translateY layer
  344. [:div.absolute.rounded-md.shadow-lg.absolute-modal
  345. {:class (if x-overflow? "is-overflow-vw-x" "")
  346. :style (merge
  347. {:top (+ top offset-top)
  348. :max-height to-max-height
  349. :z-index 11}
  350. (if set-default-width?
  351. {:width max-width})
  352. (if config/mobile?
  353. {:left 0}
  354. {:left left}))}
  355. cp]))
  356. (rum/defc transition-cp < rum/reactive
  357. [cp set-default-width? pos]
  358. (when pos
  359. (when-let [pos (rum/react pos)]
  360. (ui/css-transition
  361. {:class-names "fade"
  362. :timeout {:enter 500
  363. :exit 300}}
  364. (absolute-modal cp set-default-width? pos)))))
  365. (rum/defc image-uploader < rum/reactive
  366. {:did-mount (fn [state]
  367. (let [[id format] (:rum/args state)]
  368. (add-watch editor-handler/*asset-pending-file ::pending-asset
  369. (fn [_ _ _ f]
  370. (reset! *slash-caret-pos (util/get-caret-pos (gdom/getElement id)))
  371. (editor-handler/upload-asset id #js[f] format editor-handler/*asset-uploading? true))))
  372. state)
  373. :will-unmount (fn [state]
  374. (remove-watch editor-handler/*asset-pending-file ::pending-asset))}
  375. [id format]
  376. [:div.image-uploader
  377. [:input
  378. {:id "upload-file"
  379. :type "file"
  380. :on-change (fn [e]
  381. (let [files (.-files (.-target e))]
  382. (editor-handler/upload-asset id files format editor-handler/*asset-uploading? false)))
  383. :hidden true}]
  384. (when-let [uploading? (util/react editor-handler/*asset-uploading?)]
  385. (let [processing (util/react editor-handler/*asset-uploading-process)]
  386. (transition-cp
  387. [:div.flex.flex-row.align-center.rounded-md.shadow-sm.bg-base-2.px-1.py-1
  388. (ui/loading
  389. (util/format "Uploading %s%" (util/format "%2d" processing)))]
  390. false
  391. *slash-caret-pos)))])
  392. (rum/defcs box < rum/reactive
  393. (mixins/event-mixin
  394. (fn [state]
  395. (let [{:keys [id format block]} (get-state state)
  396. input-id id
  397. input (gdom/getElement input-id)
  398. repo (:block/repo block)]
  399. (mixins/on-key-down
  400. state
  401. {;; enter
  402. 13 (fn [state e]
  403. (when (and (not (gobj/get e "ctrlKey"))
  404. (not (gobj/get e "metaKey"))
  405. (not (editor-handler/in-auto-complete? input)))
  406. (let [{:keys [block config]} (get-state state)]
  407. (when (and block
  408. (not (:ref? config))
  409. (not (:custom-query? config))) ; in reference section
  410. (let [content (state/get-edit-content)]
  411. (if (and
  412. (> (:block/level block) 2)
  413. (string/blank? content))
  414. (do
  415. (util/stop e)
  416. (editor-handler/adjust-block-level! state :left))
  417. (let [shortcut (state/get-new-block-shortcut)
  418. insert? (cond
  419. config/mobile?
  420. true
  421. (and (= shortcut "alt+enter") (not (gobj/get e "altKey")))
  422. false
  423. (gobj/get e "shiftKey")
  424. false
  425. :else
  426. true)]
  427. (when (and
  428. insert?
  429. (not (editor-handler/in-auto-complete? input)))
  430. (util/stop e)
  431. (profile
  432. "Insert block"
  433. (editor-handler/insert-new-block! state))))))))))
  434. ;; up
  435. 38 (fn [state e]
  436. (when (and
  437. (not (gobj/get e "ctrlKey"))
  438. (not (gobj/get e "metaKey"))
  439. (not (editor-handler/in-auto-complete? input)))
  440. (editor-handler/on-up-down state e true)))
  441. ;; down
  442. 40 (fn [state e]
  443. (when (and
  444. (not (gobj/get e "ctrlKey"))
  445. (not (gobj/get e "metaKey"))
  446. (not (editor-handler/in-auto-complete? input)))
  447. (editor-handler/on-up-down state e false)))
  448. ;; backspace
  449. 8 (fn [state e]
  450. (let [node (gdom/getElement input-id)
  451. current-pos (:pos (util/get-caret-pos node))
  452. value (gobj/get node "value")
  453. deleted (and (> current-pos 0)
  454. (util/nth-safe value (dec current-pos)))
  455. selected-start (gobj/get node "selectionStart")
  456. selected-end (gobj/get node "selectionEnd")
  457. block-id (:block-id (first (:rum/args state)))
  458. page (state/get-current-page)]
  459. (cond
  460. (not= selected-start selected-end)
  461. nil
  462. (and (zero? current-pos)
  463. ;; not the top block in a block page
  464. (not (and page
  465. (util/uuid-string? page)
  466. (= (medley/uuid page) block-id))))
  467. (editor-handler/delete-block! state repo e)
  468. (and (> current-pos 1)
  469. (= (util/nth-safe value (dec current-pos)) commands/slash))
  470. (do
  471. (reset! *slash-caret-pos nil)
  472. (reset! *show-commands false))
  473. (and (> current-pos 1)
  474. (= (util/nth-safe value (dec current-pos)) commands/angle-bracket))
  475. (do
  476. (reset! *angle-bracket-caret-pos nil)
  477. (reset! *show-block-commands false))
  478. ;; pair
  479. (and
  480. deleted
  481. (contains?
  482. (set (keys editor-handler/delete-map))
  483. deleted)
  484. (>= (count value) (inc current-pos))
  485. (= (util/nth-safe value current-pos)
  486. (get editor-handler/delete-map deleted)))
  487. (do
  488. (util/stop e)
  489. (commands/delete-pair! id)
  490. (cond
  491. (and (= deleted "[") (state/get-editor-show-page-search?))
  492. (state/set-editor-show-page-search! false)
  493. (and (= deleted "(") (state/get-editor-show-block-search?))
  494. (state/set-editor-show-block-search! false)
  495. :else
  496. nil))
  497. ;; deleting hashtag
  498. (and (= deleted "#") (state/get-editor-show-page-search-hashtag?))
  499. (state/set-editor-show-page-search-hashtag! false)
  500. :else
  501. nil)))
  502. ;; tab
  503. 9 (fn [state e]
  504. (let [input-id (state/get-edit-input-id)
  505. input (and input-id (gdom/getElement id))
  506. pos (and input (:pos (util/get-caret-pos input)))]
  507. (when (and (not (state/get-editor-show-input))
  508. (not (state/get-editor-show-date-picker?))
  509. (not (state/get-editor-show-template-search?)))
  510. (util/stop e)
  511. (let [direction (if (gobj/get e "shiftKey") ; shift+tab move to left
  512. :left
  513. :right)]
  514. (p/let [_ (editor-handler/adjust-block-level! state direction)]
  515. (and input pos (js/setTimeout #(when-let [input (gdom/getElement input-id)]
  516. (util/move-cursor-to input pos))
  517. 0)))))))}
  518. {:not-matched-handler
  519. (fn [e key-code]
  520. (let [key (gobj/get e "key")
  521. value (gobj/get input "value")
  522. ctrlKey (gobj/get e "ctrlKey")
  523. metaKey (gobj/get e "metaKey")
  524. pos (util/get-input-pos input)]
  525. (cond
  526. (or ctrlKey metaKey)
  527. nil
  528. (or
  529. (and (= key "#")
  530. (and
  531. (> pos 0)
  532. (= "#" (util/nth-safe value (dec pos)))))
  533. (and (= key " ")
  534. (state/get-editor-show-page-search-hashtag?)))
  535. (state/set-editor-show-page-search-hashtag! false)
  536. (or
  537. (editor-handler/surround-by? input "#" " ")
  538. (editor-handler/surround-by? input "#" :end)
  539. (= key "#"))
  540. (do
  541. (commands/handle-step [:editor/search-page-hashtag])
  542. (state/set-last-pos! (:pos (util/get-caret-pos input)))
  543. (reset! commands/*slash-caret-pos (util/get-caret-pos input)))
  544. (and
  545. (= key " ")
  546. (state/get-editor-show-page-search-hashtag?))
  547. (state/set-editor-show-page-search-hashtag! false)
  548. (and
  549. (contains? (set/difference (set (keys editor-handler/reversed-autopair-map))
  550. #{"`"})
  551. key)
  552. (= (editor-handler/get-current-input-char input) key))
  553. (do
  554. (util/stop e)
  555. (util/cursor-move-forward input 1))
  556. (contains? (set (keys editor-handler/autopair-map)) key)
  557. (do
  558. (util/stop e)
  559. (editor-handler/autopair input-id key format nil)
  560. (cond
  561. (editor-handler/surround-by? input "[[" "]]")
  562. (do
  563. (commands/handle-step [:editor/search-page])
  564. (reset! commands/*slash-caret-pos (util/get-caret-pos input)))
  565. (editor-handler/surround-by? input "((" "))")
  566. (do
  567. (commands/handle-step [:editor/search-block :reference])
  568. (reset! commands/*slash-caret-pos (util/get-caret-pos input)))
  569. :else
  570. nil))
  571. (let [sym "$"]
  572. (and (= key sym)
  573. (>= (count value) 1)
  574. (> pos 0)
  575. (= (nth value (dec pos)) sym)
  576. (if (> (count value) pos)
  577. (not= (nth value pos) sym)
  578. true)))
  579. (commands/simple-insert! input-id "$$" {:backward-pos 2})
  580. (let [sym "^"]
  581. (and (= key sym)
  582. (>= (count value) 1)
  583. (> pos 0)
  584. (= (nth value (dec pos)) sym)
  585. (if (> (count value) pos)
  586. (not= (nth value pos) sym)
  587. true)))
  588. (commands/simple-insert! input-id "^^" {:backward-pos 2})
  589. :else
  590. nil)))})
  591. (mixins/on-key-up
  592. state
  593. {}
  594. (fn [e key-code]
  595. (let [k (gobj/get e "key")
  596. format (:format (get-state state))]
  597. (when-not (state/get-editor-show-input)
  598. (when (and @*show-commands (not= key-code 191)) ; not /
  599. (let [matched-commands (editor-handler/get-matched-commands input)]
  600. (if (seq matched-commands)
  601. (do
  602. (reset! *show-commands true)
  603. (reset! *matched-commands matched-commands))
  604. (reset! *show-commands false))))
  605. (when (and @*show-block-commands (not= key-code 188)) ; not <
  606. (let [matched-block-commands (editor-handler/get-matched-block-commands input)]
  607. (if (seq matched-block-commands)
  608. (cond
  609. (= key-code 9) ;tab
  610. (when @*show-block-commands
  611. (util/stop e)
  612. (editor-handler/insert-command! input-id
  613. (last (first matched-block-commands))
  614. format
  615. {:last-pattern commands/angle-bracket}))
  616. :else
  617. (reset! *matched-block-commands matched-block-commands))
  618. (reset! *show-block-commands false))))
  619. (editor-handler/close-autocomplete-if-outside input))))))))
  620. {:did-mount (fn [state]
  621. (let [[{:keys [dummy? format block-parent-id]} id] (:rum/args state)
  622. content (get-in @state/state [:editor/content id])
  623. input (gdom/getElement id)]
  624. (when block-parent-id
  625. (state/set-editing-block-dom-id! block-parent-id))
  626. (if (= :indent-outdent (state/get-editor-op))
  627. (when input
  628. (when-let [pos (state/get-edit-pos)]
  629. (util/set-caret-pos! input pos)))
  630. (editor-handler/restore-cursor-pos! id content dummy?))
  631. (when input
  632. (dnd/subscribe!
  633. input
  634. :upload-images
  635. {:drop (fn [e files]
  636. (editor-handler/upload-asset id files format editor-handler/*asset-uploading? true))}))
  637. ;; Here we delay this listener, otherwise the click to edit event will trigger a outside click event,
  638. ;; which will hide the editor so no way for editing.
  639. (js/setTimeout #(keyboards-handler/esc-save! state) 100)
  640. (when-let [element (gdom/getElement id)]
  641. (.focus element)))
  642. state)
  643. :did-remount (fn [_old-state state]
  644. (keyboards-handler/esc-save! state)
  645. state)
  646. :will-unmount (fn [state]
  647. (let [{:keys [id value format block repo dummy? config]} (get-state state)
  648. file? (:file? config)]
  649. (when-let [input (gdom/getElement id)]
  650. ;; (.removeEventListener input "paste" (fn [event]
  651. ;; (append-paste-doc! format event)))
  652. (let [s (str "cljs-drag-n-drop." :upload-images)
  653. a (gobj/get input s)
  654. timer (:timer a)]
  655. (and timer
  656. (dnd/unsubscribe!
  657. input
  658. :upload-images))))
  659. (editor-handler/clear-when-saved!)
  660. (if file?
  661. (let [path (:file-path config)
  662. content (db/get-file-no-sub path)
  663. value (some-> (gdom/getElement path)
  664. (gobj/get "value"))]
  665. (when (and
  666. (not (string/blank? value))
  667. (not= (string/trim value) (string/trim content)))
  668. (let [old-page-name (db/get-file-page path false)]
  669. (page-handler/rename-when-alter-title-property! old-page-name path format content value)
  670. (file/alter-file (state/get-current-repo) path (string/trim value)
  671. {:re-render-root? true}))))
  672. (when-not (contains? #{:insert :indent-outdent :auto-save} (state/get-editor-op))
  673. (editor-handler/save-block! (get-state state) value))))
  674. state)}
  675. [state {:keys [on-hide dummy? node format block block-parent-id]
  676. :or {dummy? false}
  677. :as option} id config]
  678. (let [content (state/get-edit-content)]
  679. [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
  680. (when config/mobile? (mobile-bar state id))
  681. (ui/ls-textarea
  682. {:id id
  683. :class "mousetrap"
  684. :cacheMeasurements true
  685. :default-value (or content "")
  686. :minRows (if (state/enable-grammarly?) 2 1)
  687. :on-click (fn [_e]
  688. (let [input (gdom/getElement id)
  689. current-pos (:pos (util/get-caret-pos input))]
  690. (state/set-edit-pos! current-pos)
  691. (editor-handler/close-autocomplete-if-outside input)))
  692. :on-change (fn [e]
  693. (let [value (util/evalue e)
  694. current-pos (:pos (util/get-caret-pos (gdom/getElement id)))]
  695. (state/set-edit-content! id value false)
  696. (state/set-edit-pos! current-pos)
  697. (when-let [repo (or (:block/repo block)
  698. (state/get-current-repo))]
  699. (state/set-editor-last-input-time! repo (util/time-ms))
  700. (db/clear-repo-persistent-job! repo))
  701. (let [input (gdom/getElement id)
  702. native-e (gobj/get e "nativeEvent")
  703. last-input-char (util/nth-safe value (dec current-pos))]
  704. (case last-input-char
  705. "/"
  706. ;; TODO: is it cross-browser compatible?
  707. (when (not= (gobj/get native-e "inputType") "insertFromPaste")
  708. (when-let [matched-commands (seq (editor-handler/get-matched-commands input))]
  709. (reset! *slash-caret-pos (util/get-caret-pos input))
  710. (reset! *show-commands true)))
  711. "<"
  712. (when-let [matched-commands (seq (editor-handler/get-matched-block-commands input))]
  713. (reset! *angle-bracket-caret-pos (util/get-caret-pos input))
  714. (reset! *show-block-commands true))
  715. nil))))
  716. :on-paste (fn [e]
  717. (when-let [handled
  718. (let [pick-one-allowed-item
  719. (fn [items]
  720. (if (util/electron?)
  721. (let [existed-file-path (js/window.apis.getFilePathFromClipboard)
  722. existed-file-path (if (and
  723. (string? existed-file-path)
  724. (not util/mac?)
  725. (not util/win32?)) ; FIXME: linuxcx
  726. (when (re-find #"^(/[^/ ]*)+/?$" existed-file-path)
  727. existed-file-path)
  728. existed-file-path)
  729. has-file-path? (not (string/blank? existed-file-path))
  730. has-image? (js/window.apis.isClipboardHasImage)]
  731. (if (or has-image? has-file-path?)
  732. [:asset (js/File. #js[] (if has-file-path? existed-file-path "image.png"))]))
  733. (when (and items (.-length items))
  734. (let [files (. (js/Array.from items) (filter #(= (.-kind %) "file")))
  735. it (gobj/get files 0) ;;; TODO: support multiple files
  736. mime (and it (.-type it))]
  737. (cond
  738. (contains? #{"image/jpeg" "image/png" "image/jpg" "image/gif"} mime) [:asset (. it getAsFile)])))))
  739. clipboard-data (gobj/get e "clipboardData")
  740. items (or (.-items clipboard-data)
  741. (.-files clipboard-data))
  742. picked (pick-one-allowed-item items)]
  743. (if (get picked 1)
  744. (match picked
  745. [:asset file] (editor-handler/set-asset-pending-file file))))]
  746. (util/stop e)))
  747. :auto-focus false})
  748. ;; TODO: how to render the transitions asynchronously?
  749. (transition-cp
  750. (commands id format)
  751. true
  752. *slash-caret-pos)
  753. (transition-cp
  754. (block-commands id format)
  755. true
  756. *angle-bracket-caret-pos)
  757. (transition-cp
  758. (page-search id format)
  759. true
  760. *slash-caret-pos)
  761. (transition-cp
  762. (block-search id format)
  763. true
  764. *slash-caret-pos)
  765. (transition-cp
  766. (template-search id format)
  767. true
  768. *slash-caret-pos)
  769. (transition-cp
  770. (datetime-comp/date-picker id format nil)
  771. false
  772. *slash-caret-pos)
  773. (transition-cp
  774. (input id
  775. (fn [command m pos]
  776. (editor-handler/handle-command-input command id format m pos)))
  777. true
  778. *slash-caret-pos)
  779. (when format
  780. (image-uploader id format))]))