diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 73d568f55b..1f258dc2ac 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -55,3 +55,5 @@ jobs: with: name: test-results path: frontend/test-results/ + overwrite: true + retention-days: 7 diff --git a/frontend/package.json b/frontend/package.json index 9b3629e4d4..e7517eed4a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,7 +26,7 @@ "build:wasm": "../render-wasm/build", "build:storybook:cljs": "clojure -M:dev:shadow-cljs compile storybook", "build:app:libs": "node ./scripts/build-libs.js", - "build:app:main": "clojure -M:dev:shadow-cljs release main worker", + "build:app:main": "clojure -M:dev:shadow-cljs release main worker --debug", "build:app": "yarn run clear:shadow-cache && yarn run build:app:main && yarn run build:app:libs", "e2e:server": "node ./scripts/e2e-server.js", "fmt:clj": "cljfmt fix --parallel=true src/ test/", diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index 704b0efbc6..0458faeb11 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -255,6 +255,7 @@ (defn create-paste-from-blob [in-viewport?] (fn [blob] + (js/console.log "create-paste-from-blob" blob) (let [type (.-type blob) result (cond (= type "image/svg+xml") @@ -288,7 +289,8 @@ (ptk/reify ::paste-from-clipboard ptk/WatchEvent (watch [_ _ _] - (->> (clipboard/from-clipboard) + (prn "paste-from-clipboard") + (->> (clipboard/from-dom-api) (rx/mapcat default-paste-from-blob) (rx/take 1))))) @@ -298,6 +300,7 @@ (ptk/reify ::paste-from-event ptk/WatchEvent (watch [_ state _] + (prn "paste-from-event") (let [objects (dsh/lookup-page-objects state) edit-id (dm/get-in state [:workspace-local :edition]) is-editing? (and edit-id (= :text (get-in objects [edit-id :type])))] @@ -306,7 +309,7 @@ ;; we forbid that scenario so the default behaviour is executed (if is-editing? (rx/empty) - (->> (clipboard/from-synthetic-clipboard-event event) + (->> (clipboard/from-synthetic-event event) (rx/mapcat (create-paste-from-blob in-viewport?)))))))) (defn copy-selected-svg @@ -476,7 +479,7 @@ (js/console.error "Clipboard error:" cause)) (rx/empty)))] - (->> (clipboard/from-clipboard) + (->> (clipboard/from-dom-api) (rx/mapcat #(.text %)) (rx/map decode-entry) (rx/take 1) diff --git a/frontend/src/app/main/ui/debug/playground.cljs b/frontend/src/app/main/ui/debug/playground.cljs index 16b428af64..17d3c5ea1f 100644 --- a/frontend/src/app/main/ui/debug/playground.cljs +++ b/frontend/src/app/main/ui/debug/playground.cljs @@ -11,7 +11,7 @@ [] (let [on-paste (mf/use-fn (fn [e] - (let [stream (clipboard/from-clipboard-event e)] + (let [stream (clipboard/from-event e)] (rx/sub! stream (fn [data] (js/console.log "data" data)))))) @@ -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-dom-api)] (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..a0b468d650 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -181,7 +181,8 @@ handle-hover-copy-paste (mf/use-callback (fn [] - (->> (clipboard/from-clipboard) + (->> (clipboard/from-dom-api) + ;: FIXME: use specific API for access .text (rx/mapcat #(.text %)) (rx/take 1) (rx/subs! diff --git a/frontend/src/app/util/clipboard.cljs b/frontend/src/app/util/clipboard.cljs index 63d632aa76..3199825cc9 100644 --- a/frontend/src/app/util/clipboard.cljs +++ b/frontend/src/app/util/clipboard.cljs @@ -6,41 +6,142 @@ (ns app.util.clipboard (:require - ["./clipboard.js" :as clipboard] + [app.common.data :as d] + [app.common.exceptions :as ex] [app.common.transit :as t] + [app.util.dom :as dom] + [app.util.object :as obj] [beicon.v2.core :as rx])) +(def max-parseable-size + (* 16 1024 1024)) + (def image-types ["image/webp" "image/png" "image/jpeg" "image/svg+xml"]) -(def clipboard-settings #js {:decodeTransit t/decode-str}) +(def allowed-types + (d/ordered-set + "image/webp", + "image/png", + "image/jpeg", + "image/svg+xml", + "application/transit+json", + "text/html", + "text/plain")) -(defn from-clipboard [] - (->> (rx/from (clipboard/fromClipboard clipboard-settings)) - (rx/mapcat #(rx/from %)))) +(def exclusive-types + (d/ordered-set + "application/transit+json", + "text/html", + "text/plain")) -(defn from-data-transfer [data-transfer] - (->> (rx/from (clipboard/fromDataTransfer data-transfer clipboard-settings)) - (rx/mapcat #(rx/from %)))) +(def clipboard-settings + #js {:decodeTransit t/decode-str}) -(defn from-clipboard-data [clipboard-data] - (from-data-transfer clipboard-data)) +(defn- parse-pain-text + [text] + (or (when (ex/ignoring (t/decode-str text)) + (new js/Blob #js [text] #js {:type "application/transit+json"})) + (when (re-seq #"^]" text) + (new js/Blob #js [text] #js {:type "image/svg+xml"})) + (new js/Blob #js [text] #js {:type "text/plain"}))) -(defn from-clipboard-event [event] - (from-clipboard-data (.-clipboardData event))) +(defn from-dom-api + [] + (let [api (.-clipboard js/navigator)] + (->> (rx/from (.read ^js api)) + (rx/mapcat (comp rx/from obj/into-array)) + (rx/mapcat (fn [item] + (let [allowed-types' + (->> (seq (.-types item)) + (filter (fn [type] (contains? allowed-types type))) + (sort-by (fn [type] (d/index-of allowed-types type))) + (into (d/ordered-set))) -(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))))) + main-type + (first allowed-types')] -(defn from-drop-event [event] - (from-data-transfer (.-dataTransfer event))) + (cond->> (rx/from (.getType ^js item main-type)) + (and (= (count allowed-types') 1) + (= "text/plain" main-type)) + (rx/mapcat (fn [blob] + (if (>= max-parseable-size (.-size ^js blob)) + (->> (rx/from (.text ^js blob)) + (rx/map parse-pain-text)) + (rx/of blob))))))))))) +(defn- from-data-transfer + "Get clipboard stream from DataTransfer instance" + [data-transfer] + (let [sorted-items + (->> (seq (.-items ^js data-transfer)) + (filter (fn [item] + (contains? allowed-types (.-type ^js item)))) + (sort-by (fn [item] (d/index-of allowed-types (.-type item)))))] + (->> (rx/from sorted-items) + (rx/mapcat (fn [item] + (let [kind (.-kind ^js item) + type (.-type ^js item)] + (cond + (= kind "file") + (rx/of (.getAsFile ^js item)) + + (= kind "string") + (->> (rx/create (fn [subs] + (.getAsString ^js item + (fn [text] + (rx/push! subs (d/vec2 type text)) + (rx/end! subs))))) + (rx/map (fn [[type text]] + (if (= type "text/plain") + (parse-pain-text text) + (new js/Blob #js [text] #js {:type type}))))) + :else + (rx/empty))))) + (rx/filter some?) + (rx/reduce (fn [filtered item] + (js/console.log "AAA" item) + (let [type (.-type ^js item)] + (if (and (contains? exclusive-types type) + (some (fn [item] (contains? exclusive-types type)) filtered)) + filtered + (conj filtered item)))) + (d/ordered-set)) + (rx/mapcat (comp rx/from seq))))) + +(defn from-event + "Get clipboard stream from event" + [event] + (let [cdata (.-clipboardData ^js event)] + (from-data-transfer cdata))) + +(defn from-synthetic-event + "Get clipboard stream from syntetic event" + [event] + (let [target + (dom/get-target event) + + content-editable? + (dom/is-content-editable? target) + + 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-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 +153,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/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))