mirror of
https://github.com/logseq/logseq.git
synced 2026-05-25 13:14:39 +00:00
* feat: export to image * chore: export selection on whiteboards * fix: whiteboards zoom on export * fix: loading position * chore: support video thumb * core: add export to whiteboards context menu * fix: context menu entry * fix; copy image to clipboard * fix: copy / export label * fix: hide ui elements * fix: remove random character * fix: graph export * chore: remove console log and jpg format * style: run prettier * fix: disable on multiple selected blocks * fix: multiple blocks * enhance: restrict bounds of selected shapes * chore: export selection on whiteboards * fix: whiteboards zoom on export * chore: support video thumb * core: add export to whiteboards context menu * fix: context menu entry * fix; copy image to clipboard * fix: copy / export label * fix: hide ui elements * fix: remove random character * fix: graph export * chore: remove console log and jpg format * style: run prettier * fix: disable on multiple selected blocks * fix: multiple blocks * enhance: restrict bounds of selected shapes * Fix any html2canvas related functionality failing in publishing * fix: portal header gradient on export * chore: add comment about html2canvas-ignore attr * fix: use export padding constant * fix: export collapsed portals with size > medium * fix: reset export type * enhance: export filename --------- Co-authored-by: Gabriel Horner <gabriel@logseq.com> Co-authored-by: Tienson Qin <tiensonqin@gmail.com>
452 lines
17 KiB
Clojure
452 lines
17 KiB
Clojure
(ns frontend.components.content
|
|
(:require [clojure.string :as string]
|
|
[dommy.core :as d]
|
|
[frontend.commands :as commands]
|
|
[frontend.components.editor :as editor]
|
|
[frontend.components.page-menu :as page-menu]
|
|
[frontend.components.export :as export]
|
|
[frontend.context.i18n :refer [t]]
|
|
[frontend.db :as db]
|
|
[frontend.extensions.srs :as srs]
|
|
[frontend.handler.common :as common-handler]
|
|
[frontend.handler.editor :as editor-handler]
|
|
[frontend.handler.image :as image-handler]
|
|
[frontend.handler.notification :as notification]
|
|
[frontend.handler.page :as page-handler]
|
|
[frontend.handler.common.developer :as dev-common-handler]
|
|
[frontend.mixins :as mixins]
|
|
[frontend.state :as state]
|
|
[frontend.ui :as ui]
|
|
[frontend.util :as util]
|
|
[frontend.modules.shortcut.core :as shortcut]
|
|
[logseq.graph-parser.util :as gp-util]
|
|
[logseq.graph-parser.util.block-ref :as block-ref]
|
|
[frontend.util.url :as url-util]
|
|
[goog.dom :as gdom]
|
|
[goog.object :as gobj]
|
|
[rum.core :as rum]))
|
|
|
|
;; TODO i18n support
|
|
|
|
(rum/defc custom-context-menu-content
|
|
[]
|
|
[:.menu-links-wrapper
|
|
(ui/menu-background-color #(editor-handler/batch-add-block-property! (state/get-selection-block-ids) :background-color %)
|
|
#(editor-handler/batch-remove-block-property! (state/get-selection-block-ids) :background-color))
|
|
|
|
(ui/menu-heading #(editor-handler/batch-set-heading! (state/get-selection-block-ids) %)
|
|
#(editor-handler/batch-set-heading! (state/get-selection-block-ids) true)
|
|
#(editor-handler/batch-remove-heading! (state/get-selection-block-ids)))
|
|
|
|
[:hr.menu-separator]
|
|
|
|
(ui/menu-link
|
|
{:key "cut"
|
|
:on-click #(editor-handler/cut-selection-blocks true)}
|
|
(t :editor/cut)
|
|
(ui/keyboard-shortcut-from-config :editor/cut))
|
|
(ui/menu-link
|
|
{:key "delete"
|
|
:on-click #(do (editor-handler/delete-selection %)
|
|
(state/hide-custom-context-menu!))}
|
|
(t :editor/delete-selection)
|
|
(ui/keyboard-shortcut-from-config :editor/delete))
|
|
(ui/menu-link
|
|
{:key "copy"
|
|
:on-click editor-handler/copy-selection-blocks}
|
|
(t :editor/copy)
|
|
(ui/keyboard-shortcut-from-config :editor/copy))
|
|
(ui/menu-link
|
|
{:key "copy as"
|
|
:on-click (fn [_]
|
|
(let [block-uuids (editor-handler/get-selected-toplevel-block-uuids)]
|
|
(state/set-modal!
|
|
#(export/export-blocks block-uuids {:whiteboard? false}))))}
|
|
(t :content/copy-export-as)
|
|
nil)
|
|
(ui/menu-link
|
|
{:key "copy block refs"
|
|
:on-click editor-handler/copy-block-refs}
|
|
(t :content/copy-block-ref)
|
|
nil)
|
|
(ui/menu-link
|
|
{:key "copy block embeds"
|
|
:on-click editor-handler/copy-block-embeds}
|
|
(t :content/copy-block-emebed)
|
|
nil)
|
|
|
|
[:hr.menu-separator]
|
|
|
|
(when (state/enable-flashcards?)
|
|
(ui/menu-link
|
|
{:key "Make a Card"
|
|
:on-click #(srs/batch-make-cards!)}
|
|
(t :context-menu/make-a-flashcard)
|
|
nil))
|
|
|
|
(ui/menu-link
|
|
{:key "cycle todos"
|
|
:on-click editor-handler/cycle-todos!}
|
|
(t :editor/cycle-todo)
|
|
(ui/keyboard-shortcut-from-config :editor/cycle-todo))
|
|
|
|
[:hr.menu-separator]
|
|
|
|
(ui/menu-link
|
|
{:key "Expand all"
|
|
:on-click editor-handler/expand-all-selection!}
|
|
(t :editor/expand-block-children)
|
|
(ui/keyboard-shortcut-from-config :editor/expand-block-children))
|
|
|
|
(ui/menu-link
|
|
{:key "Collapse all"
|
|
:on-click editor-handler/collapse-all-selection!}
|
|
(t :editor/collapse-block-children)
|
|
(ui/keyboard-shortcut-from-config :editor/collapse-block-children))])
|
|
|
|
(defonce *template-including-parent? (atom nil))
|
|
|
|
(rum/defc template-checkbox
|
|
[template-including-parent?]
|
|
[:div.flex.flex-row.w-auto.items-center
|
|
[:p.text-medium.mr-2 (t :context-menu/template-include-parent-block)]
|
|
(ui/toggle template-including-parent?
|
|
#(swap! *template-including-parent? not))])
|
|
|
|
(rum/defcs block-template < rum/reactive
|
|
shortcut/disable-all-shortcuts
|
|
(rum/local false ::edit?)
|
|
(rum/local "" ::input)
|
|
{:will-unmount (fn [state]
|
|
(reset! *template-including-parent? nil)
|
|
state)}
|
|
[state block-id]
|
|
(let [edit? (get state ::edit?)
|
|
input (get state ::input)
|
|
template-including-parent? (rum/react *template-including-parent?)
|
|
block-id (if (string? block-id) (uuid block-id) block-id)
|
|
block (db/entity [:block/uuid block-id])
|
|
has-children? (seq (:block/_parent block))]
|
|
(when (and (nil? template-including-parent?) has-children?)
|
|
(reset! *template-including-parent? true))
|
|
|
|
(if @edit?
|
|
(do
|
|
(state/clear-edit!)
|
|
[:<>
|
|
[:div.px-4.py-2.text-sm {:on-click (fn [e] (util/stop e))}
|
|
[:p (t :context-menu/input-template-name)]
|
|
[:input#new-template.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
|
|
{:auto-focus true
|
|
:on-change (fn [e]
|
|
(reset! input (util/evalue e)))}]
|
|
(when has-children?
|
|
(template-checkbox template-including-parent?))
|
|
(ui/button (t :submit)
|
|
:on-click (fn []
|
|
(let [title (string/trim @input)]
|
|
(when (not (string/blank? title))
|
|
(if (page-handler/template-exists? title)
|
|
(notification/show!
|
|
[:p (t :context-menu/template-exists-warning)]
|
|
:error)
|
|
(do
|
|
(editor-handler/set-block-property! block-id :template title)
|
|
(when (false? template-including-parent?)
|
|
(editor-handler/set-block-property! block-id :template-including-parent false))
|
|
(state/hide-custom-context-menu!)))))))]
|
|
[:hr.menu-separator]])
|
|
(ui/menu-link
|
|
{:key "Make a Template"
|
|
:on-click (fn [e]
|
|
(util/stop e)
|
|
(reset! edit? true))}
|
|
(t :context-menu/make-a-template)
|
|
nil))))
|
|
|
|
(rum/defc ^:large-vars/cleanup-todo block-context-menu-content <
|
|
shortcut/disable-all-shortcuts
|
|
[_target block-id]
|
|
(when-let [block (db/entity [:block/uuid block-id])]
|
|
(let [heading (-> block :block/properties :heading (or false))]
|
|
[:.menu-links-wrapper
|
|
(ui/menu-background-color #(editor-handler/set-block-property! block-id :background-color %)
|
|
#(editor-handler/remove-block-property! block-id :background-color))
|
|
|
|
(ui/menu-heading heading
|
|
#(editor-handler/set-heading! block-id %)
|
|
#(editor-handler/set-heading! block-id true)
|
|
#(editor-handler/remove-heading! block-id))
|
|
|
|
[:hr.menu-separator]
|
|
|
|
(ui/menu-link
|
|
{:key "Open in sidebar"
|
|
:on-click (fn [_e]
|
|
(editor-handler/open-block-in-sidebar! block-id))}
|
|
(t :content/open-in-sidebar)
|
|
["⇧" "click"])
|
|
|
|
[:hr.menu-separator]
|
|
|
|
(ui/menu-link
|
|
{:key "Copy block ref"
|
|
:on-click (fn [_e]
|
|
(editor-handler/copy-block-ref! block-id block-ref/->block-ref))}
|
|
(t :content/copy-block-ref)
|
|
nil)
|
|
|
|
(ui/menu-link
|
|
{:key "Copy block embed"
|
|
:on-click (fn [_e]
|
|
(editor-handler/copy-block-ref! block-id #(util/format "{{embed ((%s))}}" %)))}
|
|
(t :content/copy-block-emebed)
|
|
nil)
|
|
|
|
;; TODO Logseq protocol mobile support
|
|
(when (util/electron?)
|
|
(ui/menu-link
|
|
{:key "Copy block URL"
|
|
:on-click (fn [_e]
|
|
(let [current-repo (state/get-current-repo)
|
|
tap-f (fn [block-id]
|
|
(url-util/get-logseq-graph-uuid-url nil current-repo block-id))]
|
|
(editor-handler/copy-block-ref! block-id tap-f)))}
|
|
(t :content/copy-block-url)
|
|
nil))
|
|
|
|
(ui/menu-link
|
|
{:key "Copy as"
|
|
:on-click (fn [_]
|
|
(state/set-modal! #(export/export-blocks [block-id] {:whiteboard? false})))}
|
|
(t :content/copy-export-as)
|
|
nil)
|
|
|
|
(ui/menu-link
|
|
{:key "Cut"
|
|
:on-click (fn [_e]
|
|
(editor-handler/cut-block! block-id))}
|
|
(t :editor/cut)
|
|
(ui/keyboard-shortcut-from-config :editor/cut))
|
|
|
|
(ui/menu-link
|
|
{:key "delete"
|
|
:on-click #(editor-handler/delete-block-aux! block true)}
|
|
(t :editor/delete-selection)
|
|
(ui/keyboard-shortcut-from-config :editor/delete))
|
|
|
|
[:hr.menu-separator]
|
|
|
|
(block-template block-id)
|
|
|
|
(cond
|
|
(srs/card-block? block)
|
|
(ui/menu-link
|
|
{:key "Preview Card"
|
|
:on-click #(srs/preview (:db/id block))}
|
|
(t :context-menu/preview-flashcard)
|
|
nil)
|
|
(state/enable-flashcards?)
|
|
(ui/menu-link
|
|
{:key "Make a Card"
|
|
:on-click #(srs/make-block-a-card! block-id)}
|
|
(t :context-menu/make-a-flashcard)
|
|
nil)
|
|
:else
|
|
nil)
|
|
|
|
[:hr.menu-separator]
|
|
|
|
(ui/menu-link
|
|
{:key "Expand all"
|
|
:on-click (fn [_e]
|
|
(editor-handler/expand-all! block-id))}
|
|
(t :editor/expand-block-children)
|
|
(ui/keyboard-shortcut-from-config :editor/expand-block-children))
|
|
|
|
(ui/menu-link
|
|
{:key "Collapse all"
|
|
:on-click (fn [_e]
|
|
(editor-handler/collapse-all! block-id {}))}
|
|
(t :editor/collapse-block-children)
|
|
(ui/keyboard-shortcut-from-config :editor/collapse-block-children))
|
|
|
|
(when (state/sub [:plugin/simple-commands])
|
|
(when-let [cmds (state/get-plugins-commands-with-type :block-context-menu-item)]
|
|
(for [[_ {:keys [key label] :as cmd} action pid] cmds]
|
|
(ui/menu-link
|
|
{:key key
|
|
:on-click #(commands/exec-plugin-simple-command!
|
|
pid (assoc cmd :uuid block-id) action)}
|
|
label
|
|
nil))))
|
|
|
|
(when (state/sub [:ui/developer-mode?])
|
|
(ui/menu-link
|
|
{:key "(Dev) Show block data"
|
|
:on-click (fn []
|
|
(dev-common-handler/show-entity-data [:block/uuid block-id]))}
|
|
(t :dev/show-block-data)
|
|
nil))
|
|
|
|
(when (state/sub [:ui/developer-mode?])
|
|
(ui/menu-link
|
|
{:key "(Dev) Show block AST"
|
|
:on-click (fn []
|
|
(let [block (db/pull [:block/uuid block-id])]
|
|
(dev-common-handler/show-content-ast (:block/content block) (:block/format block))))}
|
|
(t :dev/show-block-ast)
|
|
nil))])))
|
|
|
|
(rum/defc block-ref-custom-context-menu-content
|
|
[block block-ref-id]
|
|
(when (and block block-ref-id)
|
|
[:.menu-links-wrapper
|
|
(ui/menu-link
|
|
{:key "open-in-sidebar"
|
|
:on-click (fn []
|
|
(state/sidebar-add-block!
|
|
(state/get-current-repo)
|
|
block-ref-id
|
|
:block-ref))}
|
|
(t :content/open-in-sidebar)
|
|
["⇧" "click"])
|
|
(ui/menu-link
|
|
{:key "copy"
|
|
:on-click (fn [] (editor-handler/copy-current-ref block-ref-id))}
|
|
(t :content/copy-ref)
|
|
nil)
|
|
(ui/menu-link
|
|
{:key "delete"
|
|
:on-click (fn [] (editor-handler/delete-current-ref! block block-ref-id))}
|
|
(t :content/delete-ref)
|
|
nil)
|
|
(ui/menu-link
|
|
{:key "replace-with-text"
|
|
:on-click (fn [] (editor-handler/replace-ref-with-text! block block-ref-id))}
|
|
(t :content/replace-with-text)
|
|
nil)
|
|
(ui/menu-link
|
|
{:key "replace-with-embed"
|
|
:on-click (fn [] (editor-handler/replace-ref-with-embed! block block-ref-id))}
|
|
(t :content/replace-with-embed)
|
|
nil)]))
|
|
|
|
(rum/defc page-title-custom-context-menu-content
|
|
[page]
|
|
(when-not (string/blank? page)
|
|
(let [page-menu-options (page-menu/page-menu page)]
|
|
[:.menu-links-wrapper
|
|
(for [{:keys [title options]} page-menu-options]
|
|
(rum/with-key
|
|
(ui/menu-link options title nil)
|
|
title))])))
|
|
|
|
;; TODO: content could be changed
|
|
;; Also, keyboard bindings should only be activated after
|
|
;; blocks were already selected.
|
|
(rum/defc hiccup-content < rum/static
|
|
(mixins/event-mixin
|
|
(fn [state]
|
|
;; fixme: this mixin will register global event listeners on window
|
|
;; which might cause unexpected issues
|
|
(mixins/listen state js/window "contextmenu"
|
|
(fn [e]
|
|
(let [target (gobj/get e "target")
|
|
block-id (d/attr target "blockid")
|
|
{:keys [block block-ref]} (state/sub :block-ref/context)
|
|
{:keys [page]} (state/sub :page-title/context)]
|
|
(cond
|
|
page
|
|
(do
|
|
(common-handler/show-custom-context-menu!
|
|
e
|
|
(page-title-custom-context-menu-content page))
|
|
(state/set-state! :page-title/context nil))
|
|
|
|
block-ref
|
|
(do
|
|
(common-handler/show-custom-context-menu!
|
|
e
|
|
(block-ref-custom-context-menu-content block block-ref))
|
|
(state/set-state! :block-ref/context nil))
|
|
|
|
(and (state/selection?) (not (d/has-class? target "bullet")))
|
|
(common-handler/show-custom-context-menu!
|
|
e
|
|
(custom-context-menu-content))
|
|
|
|
(and block-id (parse-uuid block-id))
|
|
(let [block (.closest target ".ls-block")]
|
|
(when block
|
|
(state/clear-selection!)
|
|
(state/conj-selection-block! block :down))
|
|
(common-handler/show-custom-context-menu!
|
|
e
|
|
(block-context-menu-content target (uuid block-id))))
|
|
|
|
:else
|
|
nil))))))
|
|
[id {:keys [hiccup]}]
|
|
[:div {:id id}
|
|
(if hiccup
|
|
hiccup
|
|
[:div.cursor (t :content/click-to-edit)])])
|
|
|
|
(rum/defc non-hiccup-content < rum/reactive
|
|
[id content on-click on-hide config format]
|
|
(let [edit? (state/sub [:editor/editing? id])]
|
|
(if edit?
|
|
(editor/box {:on-hide on-hide
|
|
:format format}
|
|
id
|
|
config)
|
|
(let [on-click (fn [e]
|
|
(when-not (util/link? (gobj/get e "target"))
|
|
(util/stop e)
|
|
(editor-handler/reset-cursor-range! (gdom/getElement (str id)))
|
|
(state/set-edit-content! id content)
|
|
(state/set-edit-input-id! id)
|
|
(when on-click
|
|
(on-click e))))]
|
|
[:pre.cursor.content.pre-white-space
|
|
{:id id
|
|
:on-click on-click}
|
|
(if (string/blank? content)
|
|
[:div.cursor (t :content/click-to-edit)]
|
|
content)]))))
|
|
|
|
(defn- set-draw-iframe-style!
|
|
[]
|
|
(let [width (gobj/get js/window "innerWidth")]
|
|
(when (>= width 1024)
|
|
(let [draws (d/by-class "draw-iframe")
|
|
width (- width 200)]
|
|
(doseq [draw draws]
|
|
(d/set-style! draw :width (str width "px"))
|
|
(let [height (max 700 (/ width 2))]
|
|
(d/set-style! draw :height (str height "px")))
|
|
(d/set-style! draw :margin-left (str (- (/ (- width 570) 2)) "px")))))))
|
|
|
|
(rum/defcs content < rum/reactive
|
|
{:did-mount (fn [state]
|
|
(set-draw-iframe-style!)
|
|
(image-handler/render-local-images!)
|
|
state)
|
|
:did-update (fn [state]
|
|
(set-draw-iframe-style!)
|
|
(image-handler/render-local-images!)
|
|
state)}
|
|
[state id {:keys [format
|
|
config
|
|
hiccup
|
|
content
|
|
on-click
|
|
on-hide]
|
|
:as option}]
|
|
(if hiccup
|
|
[:div
|
|
(hiccup-content id option)]
|
|
(let [format (gp-util/normalize-format format)]
|
|
(non-hiccup-content id content on-click on-hide config format))))
|