content.cljs 17 KB

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