content.cljs 17 KB

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