content.cljs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. (ns frontend.components.content
  2. (:require [cljs-bean.core :as bean]
  3. [cljs-time.coerce :as tc]
  4. [cljs.pprint :as pp]
  5. [clojure.string :as string]
  6. [cognitect.transit :as transit]
  7. [dommy.core :as d]
  8. [frontend.commands :as commands]
  9. [frontend.components.editor :as editor]
  10. [frontend.components.export :as export]
  11. [frontend.components.page-menu :as page-menu]
  12. [frontend.config :as config]
  13. [frontend.context.i18n :refer [t]]
  14. [frontend.db :as db]
  15. [frontend.extensions.srs :as srs]
  16. [frontend.handler.common :as common-handler]
  17. [frontend.handler.common.developer :as dev-common-handler]
  18. [frontend.handler.editor :as editor-handler]
  19. [frontend.handler.notification :as notification]
  20. [frontend.handler.page :as page-handler]
  21. [frontend.handler.property :as property-handler]
  22. [frontend.handler.property.util :as pu]
  23. [frontend.mixins :as mixins]
  24. [frontend.modules.shortcut.core :as shortcut]
  25. [frontend.persist-db.browser :as db-browser]
  26. [frontend.state :as state]
  27. [frontend.ui :as ui]
  28. [logseq.shui.ui :as shui]
  29. [frontend.util :as util]
  30. [frontend.util.url :as url-util]
  31. [goog.dom :as gdom]
  32. [goog.object :as gobj]
  33. [logseq.common.util :as common-util]
  34. [logseq.common.util.block-ref :as block-ref]
  35. [promesa.core :as p]
  36. [rum.core :as rum]))
  37. (defonce transit-r (transit/reader :json))
  38. ;; TODO i18n support
  39. (rum/defc custom-context-menu-content
  40. []
  41. (let [repo (state/get-current-repo)]
  42. [:.menu-links-wrapper
  43. (ui/menu-background-color #(property-handler/batch-set-block-property! repo (state/get-selection-block-ids) :background-color %)
  44. #(property-handler/batch-remove-block-property! repo (state/get-selection-block-ids) :background-color))
  45. (ui/menu-heading #(editor-handler/batch-set-heading! (state/get-selection-block-ids) %)
  46. #(editor-handler/batch-set-heading! (state/get-selection-block-ids) true)
  47. #(editor-handler/batch-remove-heading! (state/get-selection-block-ids)))
  48. [:hr.menu-separator]
  49. (ui/menu-link
  50. {:key "cut"
  51. :on-click #(editor-handler/cut-selection-blocks true)
  52. :shortcut (ui/keyboard-shortcut-from-config :editor/cut)}
  53. (t :editor/cut))
  54. (ui/menu-link
  55. {:key "delete"
  56. :on-click #(do (editor-handler/delete-selection %)
  57. (state/hide-custom-context-menu!))
  58. :shortcut (ui/keyboard-shortcut-from-config :editor/delete)}
  59. (t :editor/delete-selection))
  60. (ui/menu-link
  61. {:key "copy"
  62. :on-click editor-handler/copy-selection-blocks
  63. :shortcut (ui/keyboard-shortcut-from-config :editor/copy)}
  64. (t :editor/copy))
  65. (ui/menu-link
  66. {:key "copy as"
  67. :on-click (fn [_]
  68. (let [block-uuids (editor-handler/get-selected-toplevel-block-uuids)]
  69. (state/set-modal!
  70. #(export/export-blocks block-uuids {:whiteboard? false}))))}
  71. (t :content/copy-export-as))
  72. (ui/menu-link
  73. {:key "copy block refs"
  74. :on-click editor-handler/copy-block-refs}
  75. (t :content/copy-block-ref))
  76. (ui/menu-link
  77. {:key "copy block embeds"
  78. :on-click editor-handler/copy-block-embeds}
  79. (t :content/copy-block-emebed))
  80. [:hr.menu-separator]
  81. (when (state/enable-flashcards?)
  82. (ui/menu-link
  83. {:key "Make a Card"
  84. :on-click #(srs/batch-make-cards!)}
  85. (t :context-menu/make-a-flashcard)))
  86. (ui/menu-link
  87. {:key "Toggle number list"
  88. :on-click #(state/pub-event! [:editor/toggle-own-number-list (state/get-selection-block-ids)])}
  89. (t :context-menu/toggle-number-list))
  90. (ui/menu-link
  91. {:key "cycle todos"
  92. :on-click editor-handler/cycle-todos!
  93. :shortcut (ui/keyboard-shortcut-from-config :editor/cycle-todo)}
  94. (t :editor/cycle-todo))
  95. [:hr.menu-separator]
  96. (ui/menu-link
  97. {:key "Expand all"
  98. :on-click editor-handler/expand-all-selection!
  99. :shortcut (ui/keyboard-shortcut-from-config :editor/expand-block-children)}
  100. (t :editor/expand-block-children))
  101. (ui/menu-link
  102. {:key "Collapse all"
  103. :on-click editor-handler/collapse-all-selection!
  104. :shortcut (ui/keyboard-shortcut-from-config :editor/collapse-block-children)}
  105. (t :editor/collapse-block-children))]))
  106. (defonce *template-including-parent? (atom nil))
  107. (rum/defc template-checkbox
  108. [template-including-parent?]
  109. [:div.flex.flex-row.w-auto.items-center
  110. [:p.text-medium.mr-2 (t :context-menu/template-include-parent-block)]
  111. (ui/toggle template-including-parent?
  112. #(swap! *template-including-parent? not))])
  113. (rum/defcs block-template < rum/reactive
  114. shortcut/disable-all-shortcuts
  115. (rum/local false ::edit?)
  116. (rum/local "" ::input)
  117. {:will-unmount (fn [state]
  118. (reset! *template-including-parent? nil)
  119. state)}
  120. [state block-id]
  121. (let [edit? (get state ::edit?)
  122. input (get state ::input)
  123. template-including-parent? (rum/react *template-including-parent?)
  124. block-id (if (string? block-id) (uuid block-id) block-id)
  125. block (db/entity [:block/uuid block-id])
  126. has-children? (seq (:block/_parent block))
  127. repo (state/get-current-repo)]
  128. (when (and (nil? template-including-parent?) has-children?)
  129. (reset! *template-including-parent? true))
  130. (if @edit?
  131. (do
  132. (state/clear-edit!)
  133. [:<>
  134. [:div.px-4.py-2.text-sm {:on-click (fn [e] (util/stop e))}
  135. [:p (t :context-menu/input-template-name)]
  136. [:input#new-template.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
  137. {:auto-focus true
  138. :on-change (fn [e]
  139. (reset! input (util/evalue e)))}]
  140. (when has-children?
  141. (template-checkbox template-including-parent?))
  142. (ui/button (t :submit)
  143. :on-click (fn []
  144. (let [title (string/trim @input)]
  145. (when (not (string/blank? title))
  146. (p/let [exists? (page-handler/<template-exists? title)]
  147. (if exists?
  148. (notification/show!
  149. [:p (t :context-menu/template-exists-warning)]
  150. :error)
  151. (p/do!
  152. (property-handler/set-block-property! repo block-id :template title)
  153. (when (false? template-including-parent?)
  154. (property-handler/set-block-property! repo block-id :template-including-parent false))
  155. (state/hide-custom-context-menu!))))))))]
  156. [:hr.menu-separator]])
  157. (ui/menu-link
  158. {:key "Make a Template"
  159. :on-click (fn [e]
  160. (util/stop e)
  161. (reset! edit? true))}
  162. (t :context-menu/make-a-template)))))
  163. (rum/defc ^:large-vars/cleanup-todo block-context-menu-content <
  164. shortcut/disable-all-shortcuts
  165. [_target block-id]
  166. (let [repo (state/get-current-repo)
  167. db? (config/db-based-graph? repo)]
  168. (when-let [block (db/entity [:block/uuid block-id])]
  169. (let [properties (:block/properties block)
  170. heading (or (pu/lookup properties :heading)
  171. false)]
  172. [:.menu-links-wrapper
  173. (ui/menu-background-color #(property-handler/set-block-property! repo block-id :background-color %)
  174. #(property-handler/remove-block-property! repo block-id :background-color))
  175. (ui/menu-heading heading
  176. #(editor-handler/set-heading! block-id %)
  177. #(editor-handler/set-heading! block-id true)
  178. #(editor-handler/remove-heading! block-id))
  179. [:hr.menu-separator]
  180. (ui/menu-link
  181. {:key "Open in sidebar"
  182. :on-click (fn [_e]
  183. (editor-handler/open-block-in-sidebar! block-id))
  184. :shortcut ["⇧+click"]}
  185. (t :content/open-in-sidebar))
  186. [:hr.menu-separator]
  187. (ui/menu-link
  188. {:key "Copy block ref"
  189. :on-click (fn [_e]
  190. (editor-handler/copy-block-ref! block-id block-ref/->block-ref))}
  191. (t :content/copy-block-ref))
  192. (ui/menu-link
  193. {:key "Copy block embed"
  194. :on-click (fn [_e]
  195. (editor-handler/copy-block-ref! block-id #(util/format "{{embed ((%s))}}" %)))}
  196. (t :content/copy-block-emebed))
  197. ;; TODO Logseq protocol mobile support
  198. (when (util/electron?)
  199. (ui/menu-link
  200. {:key "Copy block URL"
  201. :on-click (fn [_e]
  202. (let [current-repo (state/get-current-repo)
  203. tap-f (fn [block-id]
  204. (url-util/get-logseq-graph-uuid-url nil current-repo block-id))]
  205. (editor-handler/copy-block-ref! block-id tap-f)))}
  206. (t :content/copy-block-url)))
  207. (ui/menu-link
  208. {:key "Copy as"
  209. :on-click (fn [_]
  210. (state/set-modal! #(export/export-blocks [block-id] {:whiteboard? false})))}
  211. (t :content/copy-export-as))
  212. (ui/menu-link
  213. {:key "Cut"
  214. :on-click (fn [_e]
  215. (editor-handler/cut-block! block-id))
  216. :shortcut (ui/keyboard-shortcut-from-config :editor/cut)}
  217. (t :editor/cut))
  218. (ui/menu-link
  219. {:key "delete"
  220. :on-click #(editor-handler/delete-block-aux! block true)
  221. :shortcut (ui/keyboard-shortcut-from-config :editor/delete)}
  222. (t :editor/delete-selection))
  223. [:hr.menu-separator]
  224. (when-not db?
  225. (block-template block-id))
  226. (cond
  227. (srs/card-block? block)
  228. (ui/menu-link
  229. {:key "Preview Card"
  230. :on-click #(srs/preview (:db/id block))}
  231. (t :context-menu/preview-flashcard))
  232. (state/enable-flashcards?)
  233. (ui/menu-link
  234. {:key "Make a Card"
  235. :on-click #(srs/make-block-a-card! block-id)}
  236. (t :context-menu/make-a-flashcard))
  237. :else
  238. nil)
  239. (ui/menu-link
  240. {:key "Toggle number list"
  241. :on-click #(state/pub-event! [:editor/toggle-own-number-list (state/get-selection-block-ids)])}
  242. (t :context-menu/toggle-number-list))
  243. [:hr.menu-separator]
  244. (ui/menu-link
  245. {:key "Expand all"
  246. :on-click (fn [_e]
  247. (editor-handler/expand-all! block-id))
  248. :shortcut (ui/keyboard-shortcut-from-config :editor/expand-block-children)}
  249. (t :editor/expand-block-children))
  250. (ui/menu-link
  251. {:key "Collapse all"
  252. :on-click (fn [_e]
  253. (editor-handler/collapse-all! block-id {}))
  254. :shortcut (ui/keyboard-shortcut-from-config :editor/collapse-block-children)}
  255. (t :editor/collapse-block-children))
  256. (when (state/sub [:plugin/simple-commands])
  257. (when-let [cmds (state/get-plugins-commands-with-type :block-context-menu-item)]
  258. (for [[_ {:keys [key label] :as cmd} action pid] cmds]
  259. (ui/menu-link
  260. {:key key
  261. :on-click #(commands/exec-plugin-simple-command!
  262. pid (assoc cmd :uuid block-id) action)}
  263. label))))
  264. (when (state/sub [:ui/developer-mode?])
  265. (ui/menu-link
  266. {:key "(Dev) Show block data"
  267. :on-click (fn []
  268. (dev-common-handler/show-entity-data [:block/uuid block-id]))}
  269. (t :dev/show-block-data)))
  270. (when (state/sub [:ui/developer-mode?])
  271. (ui/menu-link
  272. {:key "(Dev) Show block AST"
  273. :on-click (fn []
  274. (let [block (db/pull [:block/uuid block-id])]
  275. (dev-common-handler/show-content-ast (:block/content block) (:block/format block))))}
  276. (t :dev/show-block-ast)))
  277. (when (state/sub [:ui/developer-mode?])
  278. (ui/menu-link
  279. {:key "(Dev) Show block content history"
  280. :on-click
  281. (fn []
  282. (let [^object worker @db-browser/*worker]
  283. (p/let [result (.rtc-get-block-content-versions worker block-id)
  284. blocks-versions (bean/->clj result)]
  285. (prn :Dev-show-block-content-history)
  286. (doseq [[block-uuid versions] blocks-versions]
  287. (prn :block-uuid block-uuid)
  288. (pp/print-table [:content :created-at]
  289. (map (fn [version]
  290. {:created-at (tc/from-long (* (:created-at version) 1000))
  291. :content (:value version)})
  292. versions))))))}
  293. "(Dev) Show block content history"))
  294. (when (state/sub [:ui/developer-mode?])
  295. (ui/menu-link
  296. {:key "(Dev) Show block RTC log"
  297. :on-click
  298. (fn []
  299. (let [^object worker @db-browser/*worker]
  300. (p/let [result (.rtc-get-block-update-log worker (str block-id))
  301. logs (transit/read transit-r result)]
  302. (prn :Dev-show-block-RTC-log block-id)
  303. (apply js/console.log logs))))}
  304. "(Dev) Show block RTC log"))]))))
  305. (rum/defc block-ref-custom-context-menu-content
  306. [block block-ref-id]
  307. (when (and block block-ref-id)
  308. [:.menu-links-wrapper
  309. (ui/menu-link
  310. {:key "open-in-sidebar"
  311. :on-click (fn []
  312. (state/sidebar-add-block!
  313. (state/get-current-repo)
  314. block-ref-id
  315. :block-ref))
  316. :shortcut ["⇧+click"]}
  317. (t :content/open-in-sidebar))
  318. (ui/menu-link
  319. {:key "copy"
  320. :on-click (fn [] (editor-handler/copy-current-ref block-ref-id))}
  321. (t :content/copy-ref))
  322. (ui/menu-link
  323. {:key "delete"
  324. :on-click (fn [] (editor-handler/delete-current-ref! block block-ref-id))}
  325. (t :content/delete-ref))
  326. (ui/menu-link
  327. {:key "replace-with-text"
  328. :on-click (fn [] (editor-handler/replace-ref-with-text! block block-ref-id))}
  329. (t :content/replace-with-text))
  330. (ui/menu-link
  331. {:key "replace-with-embed"
  332. :on-click (fn [] (editor-handler/replace-ref-with-embed! block block-ref-id))}
  333. (t :content/replace-with-embed))]))
  334. (rum/defc page-title-custom-context-menu-content
  335. [page]
  336. (when-not (string/blank? page)
  337. (let [page-menu-options (page-menu/page-menu page)]
  338. [:.menu-links-wrapper
  339. (for [{:keys [title options]} page-menu-options]
  340. (rum/with-key
  341. (ui/menu-link options title)
  342. title))])))
  343. ;; TODO: content could be changed
  344. ;; Also, keyboard bindings should only be activated after
  345. ;; blocks were already selected.
  346. (rum/defc hiccup-content < rum/static
  347. (mixins/event-mixin
  348. (fn [state]
  349. ;; fixme: this mixin will register global event listeners on window
  350. ;; which might cause unexpected issues
  351. (mixins/listen state js/window "contextmenu"
  352. (fn [e]
  353. (let [target (gobj/get e "target")
  354. block-el (.closest target ".bullet-container[blockid]")
  355. block-id (some-> block-el (.getAttribute "blockid"))
  356. {:keys [block block-ref]} (state/sub :block-ref/context)
  357. {:keys [page]} (state/sub :page-title/context)]
  358. (cond
  359. page
  360. (do
  361. (shui/popup-show!
  362. e
  363. (fn [{:keys [id]}]
  364. [:div
  365. {:on-click #(shui/popup-hide! id)}
  366. (page-title-custom-context-menu-content page)])
  367. {:content-props {:class "ls-context-menu-content"}})
  368. (state/set-state! :page-title/context nil))
  369. block-ref
  370. (do
  371. (shui/popup-show!
  372. e
  373. (fn [{:keys [id]}]
  374. [:div
  375. {:on-click #(shui/popup-hide! id)}
  376. (block-ref-custom-context-menu-content block block-ref)])
  377. {:content-props {:class "ls-context-menu-content"}})
  378. (state/set-state! :block-ref/context nil))
  379. (and (state/selection?) (not (d/has-class? target "bullet")))
  380. (shui/popup-show!
  381. e
  382. (fn [{:keys [id]}]
  383. [:div
  384. {:on-click #(shui/popup-hide! id)}
  385. (custom-context-menu-content)])
  386. {:content-props {:class "ls-context-menu-content"}})
  387. (and block-id (parse-uuid block-id))
  388. (let [block (.closest target ".ls-block")]
  389. (when block
  390. (state/clear-selection!)
  391. (state/conj-selection-block! block :down))
  392. (shui/popup-show!
  393. e
  394. (fn [{:keys [id]}]
  395. [:div
  396. {:on-click #(shui/popup-hide! id)}
  397. (block-context-menu-content target (uuid block-id))])
  398. {:content-props {:class "ls-context-menu-content"}}))
  399. :else
  400. nil))))))
  401. [id {:keys [hiccup]}]
  402. [:div {:id id}
  403. (if hiccup
  404. hiccup
  405. [:div.cursor (t :content/click-to-edit)])])
  406. (rum/defc non-hiccup-content < rum/reactive
  407. [id content on-click on-hide config format]
  408. (let [edit? (state/sub-editing? id)]
  409. (if edit?
  410. (editor/box {:on-hide on-hide
  411. :format format}
  412. id
  413. config)
  414. (let [on-click (fn [e]
  415. (when-not (util/link? (gobj/get e "target"))
  416. (util/stop e)
  417. (editor-handler/reset-cursor-range! (gdom/getElement (str id)))
  418. (state/set-edit-content! id content)
  419. (when on-click
  420. (on-click e))))]
  421. [:pre.cursor.content.pre-white-space
  422. {:id id
  423. :on-click on-click}
  424. (if (string/blank? content)
  425. [:div.cursor (t :content/click-to-edit)]
  426. content)]))))
  427. (defn- set-draw-iframe-style!
  428. []
  429. (let [width (gobj/get js/window "innerWidth")]
  430. (when (>= width 1024)
  431. (let [draws (d/by-class "draw-iframe")
  432. width (- width 200)]
  433. (doseq [draw draws]
  434. (d/set-style! draw :width (str width "px"))
  435. (let [height (max 700 (/ width 2))]
  436. (d/set-style! draw :height (str height "px")))
  437. (d/set-style! draw :margin-left (str (- (/ (- width 570) 2)) "px")))))))
  438. (rum/defcs content < rum/reactive
  439. {:did-mount (fn [state]
  440. (set-draw-iframe-style!)
  441. state)
  442. :did-update (fn [state]
  443. (set-draw-iframe-style!)
  444. state)}
  445. [state id {:keys [format
  446. config
  447. hiccup
  448. content
  449. on-click
  450. on-hide]
  451. :as option}]
  452. (if hiccup
  453. [:div
  454. (hiccup-content id option)]
  455. ;; TODO: remove this
  456. (let [format (common-util/normalize-format format)]
  457. (non-hiccup-content id content on-click on-hide config format))))