diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index 704b0efbc6..bbaba9517d 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -288,7 +288,7 @@ (ptk/reify ::paste-from-clipboard ptk/WatchEvent (watch [_ _ _] - (->> (clipboard/from-clipboard) + (->> (clipboard/from-navigator) (rx/mapcat default-paste-from-blob) (rx/take 1))))) @@ -476,7 +476,7 @@ (js/console.error "Clipboard error:" cause)) (rx/empty)))] - (->> (clipboard/from-clipboard) + (->> (clipboard/from-navigator) (rx/mapcat #(.text %)) (rx/map decode-entry) (rx/take 1) @@ -944,8 +944,7 @@ (defn- paste-html-text [html] - (js/console.log html) - (dm/assert! (string? html)) + (assert (string? html)) (ptk/reify ::paste-html-text ptk/WatchEvent (watch [_ state _] @@ -953,9 +952,7 @@ root (dwtxt/create-root-from-html html style) text (.-textContent root) content (tc/dom->cljs root)] - (js/console.log "root" root "content" content) (when (types.text/valid-content? content) - (js/console.log "valid-content") (let [id (uuid/next) width (max 8 (min (* 7 (count text)) 700)) height 16 diff --git a/frontend/src/app/main/ui/debug/playground.cljs b/frontend/src/app/main/ui/debug/playground.cljs index 16b428af64..c96c6c4dca 100644 --- a/frontend/src/app/main/ui/debug/playground.cljs +++ b/frontend/src/app/main/ui/debug/playground.cljs @@ -31,7 +31,7 @@ on-click (mf/use-fn (fn [e] (js/console.log "event" e) - (let [stream (clipboard/from-clipboard)] + (let [stream (clipboard/from-navigator)] (rx/sub! stream (fn [data] (js/console.log "data" data))))))] diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index afa34710e2..75caa6318f 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -181,7 +181,7 @@ handle-hover-copy-paste (mf/use-callback (fn [] - (->> (clipboard/from-clipboard) + (->> (clipboard/from-navigator) (rx/mapcat #(.text %)) (rx/take 1) (rx/subs! @@ -802,7 +802,6 @@ ;; FIXME: optimize because it is rendered always - (mf/defc context-menu* [] (let [mdata (mf/deref menu-ref) diff --git a/frontend/src/app/util/clipboard.cljs b/frontend/src/app/util/clipboard.cljs index 63d632aa76..7c7b56da26 100644 --- a/frontend/src/app/util/clipboard.cljs +++ b/frontend/src/app/util/clipboard.cljs @@ -6,8 +6,9 @@ (ns app.util.clipboard (:require - ["./clipboard.js" :as clipboard] + ["./clipboard.js" :as impl] [app.common.transit :as t] + [app.util.dom :as dom] [beicon.v2.core :as rx])) (def image-types @@ -16,31 +17,50 @@ "image/jpeg" "image/svg+xml"]) -(def clipboard-settings #js {:decodeTransit t/decode-str}) +(def ^:private default-options + #js {:decodeTransit t/decode-str}) -(defn from-clipboard [] - (->> (rx/from (clipboard/fromClipboard clipboard-settings)) +(defn- from-data-transfer + "Get clipboard stream from DataTransfer instance" + [data-transfer] + (->> (rx/from (impl/fromDataTransfer data-transfer default-options)) (rx/mapcat #(rx/from %)))) -(defn from-data-transfer [data-transfer] - (->> (rx/from (clipboard/fromDataTransfer data-transfer clipboard-settings)) +(defn from-navigator + [] + (->> (rx/from (impl/fromNavigator default-options)) (rx/mapcat #(rx/from %)))) -(defn from-clipboard-data [clipboard-data] - (from-data-transfer clipboard-data)) +(defn from-clipboard-event + "Get clipboard stream from clipboard event" + [event] + (let [cdata (.-clipboardData ^js event)] + (from-data-transfer cdata))) -(defn from-clipboard-event [event] - (from-clipboard-data (.-clipboardData event))) +(defn from-synthetic-clipboard-event + "Get clipboard stream from syntetic clipboard event" + [event] + (let [target + (dom/get-target event) -(defn from-synthetic-clipboard-event [event] - (let [target (.-target ^js event)] - (when (and (not (.-isContentEditable ^js target)) ;; ignore when pasting into - (not= (.-tagName ^js target) "INPUT")) ;; an editable control - (from-clipboard-event (. ^js event getBrowserEvent))))) + content-editable? + (dom/is-content-editable? target) -(defn from-drop-event [event] - (from-data-transfer (.-dataTransfer event))) + is-input? + (= (dom/get-tag-name target) "INPUT")] + ;; ignore when pasting into an editable control + (when-not (or content-editable? is-input?) + (-> event + (dom/event->browser-event) + (from-clipboard-event))))) + +(defn from-drop-event + "Get clipboard stream from drop event" + [event] + (from-data-transfer (.-dataTransfer ^js event))) + +;; FIXME: rename to `write-text` (defn to-clipboard [data] (assert (string? data) "`data` should be string") @@ -52,6 +72,7 @@ (js/ClipboardItem. (js-obj mimetype promise))) +;; FIXME: this API is very confuse (defn to-clipboard-promise [mimetype promise] (let [clipboard (unchecked-get js/navigator "clipboard") diff --git a/frontend/src/app/util/clipboard.js b/frontend/src/app/util/clipboard.js index fc1f994a53..5fe4d88865 100644 --- a/frontend/src/app/util/clipboard.js +++ b/frontend/src/app/util/clipboard.js @@ -36,20 +36,25 @@ const exclusiveTypes = [ * @param {Blob} [defaultReturn] * @returns {Blob} */ -function parseClipboardItemText( - text, - options, - defaultReturn = new Blob([text], { type: "text/plain" }), -) { - let decodedTransit = false; - try { decodedTransit = options?.decodeTransit?.(text) ?? false } - catch (error) { /* NOOP */ } +function parseText(text, options) { + options = options || {}; + + const decodeTransit = options["decodeTransit"]; + + if (decodeTransit) { + try { + decodeTransit(text); + return new Blob([text], { type: "application/transit+json" }); + } catch (_error) { + // NOOP + } + } + if (/^]/i.test(text)) { return new Blob([text], { type: "image/svg+xml" }); - } else if (decodedTransit) { - return new Blob([text], { type: "application/transit+json" }); + } else { + return new Blob([text], { type: "text/plain" }); } - return defaultReturn; } /** @@ -57,9 +62,8 @@ function parseClipboardItemText( * @param {ClipboardSettings} [options] * @returns {Promise>} */ -export async function fromClipboard(options) { +export async function fromNavigator(options) { const items = await navigator.clipboard.read(); - console.log("items", items); return Promise.all( Array.from(items).map(async (item) => { const itemAllowedTypes = Array.from(item.types) @@ -73,9 +77,10 @@ export async function fromClipboard(options) { const blob = await item.getType("text/plain"); if (blob.size < maxParseableSize) { const text = await blob.text(); - return parseClipboardItemText(text, options); + return parseText(text, options); + } else { + return blob; } - return blob; } const type = itemAllowedTypes.at(0); @@ -105,9 +110,10 @@ export async function fromDataTransfer(dataTransfer, options) { const type = item.type; item.getAsString((text) => { if (type === "text/plain") { - return resolve(parseClipboardItemText(text, options)); + return resolve(parseText(text, options)); + } else { + return resolve(new Blob([text], { type })); } - return resolve(new Blob([text], { type })); }); }); } @@ -119,9 +125,7 @@ export async function fromDataTransfer(dataTransfer, options) { .reduce((filtered, item) => { if ( exclusiveTypes.includes(item.type) && - filtered.find((filteredItem) => - exclusiveTypes.includes(filteredItem.type), - ) + filtered.find((filteredItem) => exclusiveTypes.includes(filteredItem.type)) ) { return filtered; } diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index d8c50d61b1..2faa6cc1c0 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -23,7 +23,7 @@ (extend-type BrowserEvent cljs.core/IDeref - (-deref [it] (.getBrowserEvent it))) + (-deref [it] (.getBrowserEvent ^js it))) (declare get-window-size) @@ -360,6 +360,10 @@ (when (some? el) (.-innerText el))) +(defn is-content-editable? + [^js el] + (.-isContentEditable ^js el)) + (defn query ([^string selector] (query globals/document selector))