content.cljs 17 KB

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