From 6186d82151d560fbc17923ad8ac5007521e97ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Wed, 2 Dec 2020 08:44:39 +0100 Subject: [PATCH] :recycle: Change paste implementation to work with more browsers --- frontend/resources/locales.json | 8 + frontend/src/app/main/data/workspace.cljs | 186 ++++++++++-------- .../src/app/main/ui/workspace/viewport.cljs | 7 +- frontend/src/app/util/webapi.cljs | 31 ++- 4 files changed, 149 insertions(+), 83 deletions(-) diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index 0bf7355417..ceddb5b7b7 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -769,6 +769,14 @@ "es" : "Actualizado: %s" } }, + "errors.clipboard-not-implemented" : { + "translations" : { + "en" : "Your browser cannot do this operation, please use Ctrl-V", + "fr" : "", + "ru" : "", + "es" : "Tu navegador no puede realizar esta operación, por favor usa Ctrl-V." + } + }, "errors.auth.unauthorized" : { "used-in" : [ "src/app/main/ui/auth/login.cljs:82" ], "translations" : { diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 31ec93f3f7..71fc3a70f1 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -24,6 +24,7 @@ [app.config :as cfg] [app.main.constants :as c] [app.main.data.colors :as mdc] + [app.main.data.messages :as dm] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.notifications :as dwn] @@ -42,6 +43,7 @@ [app.util.timers :as ts] [app.util.transit :as t] [app.util.webapi :as wapi] + [app.util.i18n :refer [tr] :as i18n] [beicon.core :as rx] [cljs.spec.alpha :as s] [clojure.set :as set] @@ -1016,7 +1018,6 @@ (watch [_ state stream] (rx/of (dwc/update-shapes ids #(gsh/resize-rect % attr value) {:reg-objects? true}))))) - ;; --- Shape Proportions (defn set-shape-proportion-lock @@ -1029,6 +1030,7 @@ (assoc shape :proportion-lock false) (-> (assoc shape :proportion-lock true) (gpr/assign-proportions))))))))) + ;; --- Update Shape Position (s/def ::x number?) @@ -1053,6 +1055,21 @@ (rx/of (dwt/set-modifiers [id] {:displacement displ}) (dwt/apply-modifiers [id])))))) +;; --- Update Shape Flags + +(defn update-shape-flags + [id {:keys [blocked hidden] :as flags}] + (s/assert ::us/uuid id) + (s/assert ::shape-attrs flags) + (ptk/reify ::update-shape-flags + ptk/WatchEvent + (watch [_ state stream] + (letfn [(update-fn [obj] + (cond-> obj + (boolean? blocked) (assoc :blocked blocked) + (boolean? hidden) (assoc :hidden hidden)))] + (rx/of (dwc/update-shapes-recursive [id] update-fn)))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Navigation @@ -1195,6 +1212,68 @@ (rx/catch on-copy-error) (rx/ignore))))))) +(declare paste-shape) +(declare paste-text) +(declare paste-image) + +(def paste + (ptk/reify ::paste + ptk/WatchEvent + (watch [_ state stream] + (try + (let [clipboard-str (wapi/read-from-clipboard) + + paste-transit-str + (->> clipboard-str + (rx/filter t/transit?) + (rx/map t/decode) + (rx/filter #(= :copied-shapes (:type %))) + (rx/map #(select-keys % [:selected :objects])) + (rx/map paste-shape)) + + paste-plain-text-str + (->> clipboard-str + (rx/filter (comp not empty?)) + (rx/map paste-text)) + + paste-image-str + (->> (wapi/read-image-from-clipboard) + (rx/map paste-image))] + + (->> (rx/concat paste-transit-str + paste-plain-text-str + paste-image-str) + (rx/first) + (rx/catch + (fn [err] + (js/console.error "Clipboard error:" err) + (rx/empty))))) + (catch :default e + (let [data (ex-data e)] + (if (:not-implemented data) + (rx/of (dm/warn (tr "errors.clipboard-not-implemented"))) + (js/console.error "ERROR" e)))))))) + +(defn paste-from-event + [event] + (ptk/reify ::paste-from-event + ptk/WatchEvent + (watch [_ state stream] + (try + (let [paste-data (wapi/read-from-paste-event event)] + (when paste-data + (let [text-data (wapi/extract-text paste-data) + decoded-data (when (and paste-data (t/transit? text-data)) + (t/decode text-data))] + (if (and decoded-data (= (:type decoded-data) :copied-shapes)) + (rx/of (paste-shape decoded-data)) + (if-not (empty? text-data) + (rx/of (paste-text text-data)) + (let [images (wapi/extract-images paste-data)] + (rx/from (map paste-image images)))))))) + (catch :default err + (js/console.error "Clipboard error:" err)))))) + (defn selected-frame? [state] (let [selected (get-in state [:workspace-local :selected]) page-id (:current-page-id state) @@ -1202,9 +1281,9 @@ (and (and (= 1 (count selected)) (= :frame (get-in objects [(first selected) :type])))))) -(defn- paste-impl +(defn- paste-shape [{:keys [selected objects] :as data}] - (ptk/reify ::paste-impl + (ptk/reify ::paste-shape ptk/WatchEvent (watch [_ state stream] (let [selected-objs (map #(get objects %) selected) @@ -1240,71 +1319,6 @@ (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) (dwc/select-shapes selected)))))) -(defn- image-uploaded - [image] - (let [{:keys [x y]} @ms/mouse-position - {:keys [width height]} image - shape {:name (:name image) - :width width - :height height - :x (- x (/ width 2)) - :y (- y (/ height 2)) - :metadata {:width width - :height height - :id (:id image) - :path (:path image)}}] - (st/emit! (create-and-add-shape :image x y shape)))) - -(defn- paste-image-impl - [image] - (ptk/reify ::paste-bin-impl - ptk/WatchEvent - (watch [_ state stream] - (let [file-id (get-in state [:workspace-file :id]) - params {:file-id file-id - :local? true - :js-files [image]}] - (rx/of (dwp/upload-media-objects - (with-meta params - {:on-success image-uploaded}))))))) - -(declare paste-text) - -(def paste - (ptk/reify ::paste - ptk/WatchEvent - (watch [_ state stream] - (try - (let [clipboard-str (wapi/read-from-clipboard) - - paste-transit-str - (->> clipboard-str - (rx/filter t/transit?) - (rx/map t/decode) - (rx/filter #(= :copied-shapes (:type %))) - (rx/map #(select-keys % [:selected :objects])) - (rx/map paste-impl)) - - paste-plain-text-str - (->> clipboard-str - (rx/filter (comp not empty?)) - (rx/map paste-text)) - - paste-image-str - (->> (wapi/read-image-from-clipboard) - (rx/map paste-image-impl))] - - (->> (rx/concat paste-transit-str - paste-plain-text-str - paste-image-str) - (rx/first) - (rx/catch - (fn [err] - (js/console.error "Clipboard error:" err) - (rx/empty))))) - (catch :default e - (.error js/console "ERROR" e)))))) - (defn as-content [text] (let [paragraphs (->> (str/lines text) (map str/trim) @@ -1341,18 +1355,33 @@ (dwc/add-shape shape) dwc/commit-undo-transaction))))) -(defn update-shape-flags - [id {:keys [blocked hidden] :as flags}] - (s/assert ::us/uuid id) - (s/assert ::shape-attrs flags) - (ptk/reify ::update-shape-flags +(defn- image-uploaded + [image] + (let [{:keys [x y]} @ms/mouse-position + {:keys [width height]} image + shape {:name (:name image) + :width width + :height height + :x (- x (/ width 2)) + :y (- y (/ height 2)) + :metadata {:width width + :height height + :id (:id image) + :path (:path image)}}] + (st/emit! (create-and-add-shape :image x y shape)))) + +(defn- paste-image + [image] + (ptk/reify ::paste-bin-impl ptk/WatchEvent (watch [_ state stream] - (letfn [(update-fn [obj] - (cond-> obj - (boolean? blocked) (assoc :blocked blocked) - (boolean? hidden) (assoc :hidden hidden)))] - (rx/of (dwc/update-shapes-recursive [id] update-fn)))))) + (let [file-id (get-in state [:workspace-file :id]) + params {:file-id file-id + :local? true + :js-files [image]}] + (rx/of (dwp/upload-media-objects + (with-meta params + {:on-success image-uploaded}))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; GROUPS @@ -1662,7 +1691,6 @@ (dwd/select-for-drawing :text)) "p" #(st/emit! (dwd/select-for-drawing :path)) (c-mod "c") #(st/emit! copy-selected) - (c-mod "v") #(st/emit! paste) (c-mod "x") #(st/emit! copy-selected delete-selected) "escape" #(st/emit! (esc-pressed)) "del" #(st/emit! delete-selected) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 21173e1c94..0620f626f7 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -475,6 +475,10 @@ (with-meta params {:on-success #(on-uploaded % viewport-coord)}))))))) + on-paste + (fn [event] + (st/emit! (dw/paste-from-event event))) + on-resize (fn [event] (let [node (mf/ref-val viewport-ref) @@ -496,7 +500,8 @@ ;; bind with passive=false to allow the event to be cancelled ;; https://stackoverflow.com/a/57582286/3219895 (events/listen js/window EventType.WHEEL on-mouse-wheel #js {:passive false}) - (events/listen js/window EventType.RESIZE on-resize)]] + (events/listen js/window EventType.RESIZE on-resize) + (events/listen js/window EventType.PASTE on-paste)]] (fn [] (doseq [key keys] diff --git a/frontend/src/app/util/webapi.cljs b/frontend/src/app/util/webapi.cljs index f18cc56082..a08e9889fd 100644 --- a/frontend/src/app/util/webapi.cljs +++ b/frontend/src/app/util/webapi.cljs @@ -79,12 +79,15 @@ (let [cboard (unchecked-get js/navigator "clipboard")] (.writeText ^js cboard data))) -(defn- read-from-clipboard +(defn read-from-clipboard [] (let [cboard (unchecked-get js/navigator "clipboard")] - (rx/from (.readText ^js cboard)))) + (if (.-readText ^js cboard) + (rx/from (.readText ^js cboard)) + (throw (ex-info "This browser does not implement read from clipboard protocol" + {:not-implemented true}))))) -(defn- read-image-from-clipboard +(defn read-image-from-clipboard [] (let [cboard (unchecked-get js/navigator "clipboard") read-item (fn [item] @@ -97,6 +100,28 @@ (rx/mapcat identity) ;; Convert each item into an emission (rx/switch-map read-item)))) +(defn read-from-paste-event + [event] + (let [target (.-target ^js event)] + (when (and (not (.-isContentEditable target)) ;; ignore when pasting into + (not= (.-tagName target) "INPUT")) ;; an editable control + (-> ^js event + (.getBrowserEvent) + (.-clipboardData))))) + +(defn extract-text + [clipboard-data] + (when clipboard-data + (.getData clipboard-data "text"))) + +(defn extract-images + [clipboard-data] + (when clipboard-data + (let [file-list (-> (.-files ^js clipboard-data))] + (->> (range (.-length file-list)) + (map #(.item file-list %)) + (filter #(str/starts-with? (.-type %) "image/")))))) + (defn request-fullscreen [el] (cond