♻️ Change paste implementation to work with more browsers

This commit is contained in:
Andrés Moya 2020-12-02 08:44:39 +01:00 committed by Andrey Antukh
parent 264811c5ee
commit 6186d82151
4 changed files with 149 additions and 83 deletions

View file

@ -769,6 +769,14 @@
"es" : "Actualizado: %s" "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" : { "errors.auth.unauthorized" : {
"used-in" : [ "src/app/main/ui/auth/login.cljs:82" ], "used-in" : [ "src/app/main/ui/auth/login.cljs:82" ],
"translations" : { "translations" : {

View file

@ -24,6 +24,7 @@
[app.config :as cfg] [app.config :as cfg]
[app.main.constants :as c] [app.main.constants :as c]
[app.main.data.colors :as mdc] [app.main.data.colors :as mdc]
[app.main.data.messages :as dm]
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
[app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.notifications :as dwn] [app.main.data.workspace.notifications :as dwn]
@ -42,6 +43,7 @@
[app.util.timers :as ts] [app.util.timers :as ts]
[app.util.transit :as t] [app.util.transit :as t]
[app.util.webapi :as wapi] [app.util.webapi :as wapi]
[app.util.i18n :refer [tr] :as i18n]
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[clojure.set :as set] [clojure.set :as set]
@ -1016,7 +1018,6 @@
(watch [_ state stream] (watch [_ state stream]
(rx/of (dwc/update-shapes ids #(gsh/resize-rect % attr value) {:reg-objects? true}))))) (rx/of (dwc/update-shapes ids #(gsh/resize-rect % attr value) {:reg-objects? true})))))
;; --- Shape Proportions ;; --- Shape Proportions
(defn set-shape-proportion-lock (defn set-shape-proportion-lock
@ -1029,6 +1030,7 @@
(assoc shape :proportion-lock false) (assoc shape :proportion-lock false)
(-> (assoc shape :proportion-lock true) (-> (assoc shape :proportion-lock true)
(gpr/assign-proportions))))))))) (gpr/assign-proportions)))))))))
;; --- Update Shape Position ;; --- Update Shape Position
(s/def ::x number?) (s/def ::x number?)
@ -1053,6 +1055,21 @@
(rx/of (dwt/set-modifiers [id] {:displacement displ}) (rx/of (dwt/set-modifiers [id] {:displacement displ})
(dwt/apply-modifiers [id])))))) (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 ;; Navigation
@ -1195,6 +1212,68 @@
(rx/catch on-copy-error) (rx/catch on-copy-error)
(rx/ignore))))))) (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] (defn selected-frame? [state]
(let [selected (get-in state [:workspace-local :selected]) (let [selected (get-in state [:workspace-local :selected])
page-id (:current-page-id state) page-id (:current-page-id state)
@ -1202,9 +1281,9 @@
(and (and (= 1 (count selected)) (and (and (= 1 (count selected))
(= :frame (get-in objects [(first selected) :type])))))) (= :frame (get-in objects [(first selected) :type]))))))
(defn- paste-impl (defn- paste-shape
[{:keys [selected objects] :as data}] [{:keys [selected objects] :as data}]
(ptk/reify ::paste-impl (ptk/reify ::paste-shape
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [selected-objs (map #(get objects %) selected) (let [selected-objs (map #(get objects %) selected)
@ -1240,71 +1319,6 @@
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dwc/select-shapes selected)))))) (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] (defn as-content [text]
(let [paragraphs (->> (str/lines text) (let [paragraphs (->> (str/lines text)
(map str/trim) (map str/trim)
@ -1341,18 +1355,33 @@
(dwc/add-shape shape) (dwc/add-shape shape)
dwc/commit-undo-transaction))))) dwc/commit-undo-transaction)))))
(defn update-shape-flags (defn- image-uploaded
[id {:keys [blocked hidden] :as flags}] [image]
(s/assert ::us/uuid id) (let [{:keys [x y]} @ms/mouse-position
(s/assert ::shape-attrs flags) {:keys [width height]} image
(ptk/reify ::update-shape-flags 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 ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(letfn [(update-fn [obj] (let [file-id (get-in state [:workspace-file :id])
(cond-> obj params {:file-id file-id
(boolean? blocked) (assoc :blocked blocked) :local? true
(boolean? hidden) (assoc :hidden hidden)))] :js-files [image]}]
(rx/of (dwc/update-shapes-recursive [id] update-fn)))))) (rx/of (dwp/upload-media-objects
(with-meta params
{:on-success image-uploaded})))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GROUPS ;; GROUPS
@ -1662,7 +1691,6 @@
(dwd/select-for-drawing :text)) (dwd/select-for-drawing :text))
"p" #(st/emit! (dwd/select-for-drawing :path)) "p" #(st/emit! (dwd/select-for-drawing :path))
(c-mod "c") #(st/emit! copy-selected) (c-mod "c") #(st/emit! copy-selected)
(c-mod "v") #(st/emit! paste)
(c-mod "x") #(st/emit! copy-selected delete-selected) (c-mod "x") #(st/emit! copy-selected delete-selected)
"escape" #(st/emit! (esc-pressed)) "escape" #(st/emit! (esc-pressed))
"del" #(st/emit! delete-selected) "del" #(st/emit! delete-selected)

View file

@ -475,6 +475,10 @@
(with-meta params (with-meta params
{:on-success #(on-uploaded % viewport-coord)}))))))) {:on-success #(on-uploaded % viewport-coord)})))))))
on-paste
(fn [event]
(st/emit! (dw/paste-from-event event)))
on-resize on-resize
(fn [event] (fn [event]
(let [node (mf/ref-val viewport-ref) (let [node (mf/ref-val viewport-ref)
@ -496,7 +500,8 @@
;; bind with passive=false to allow the event to be cancelled ;; bind with passive=false to allow the event to be cancelled
;; https://stackoverflow.com/a/57582286/3219895 ;; https://stackoverflow.com/a/57582286/3219895
(events/listen js/window EventType.WHEEL on-mouse-wheel #js {:passive false}) (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 [] (fn []
(doseq [key keys] (doseq [key keys]

View file

@ -79,12 +79,15 @@
(let [cboard (unchecked-get js/navigator "clipboard")] (let [cboard (unchecked-get js/navigator "clipboard")]
(.writeText ^js cboard data))) (.writeText ^js cboard data)))
(defn- read-from-clipboard (defn read-from-clipboard
[] []
(let [cboard (unchecked-get js/navigator "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") (let [cboard (unchecked-get js/navigator "clipboard")
read-item (fn [item] read-item (fn [item]
@ -97,6 +100,28 @@
(rx/mapcat identity) ;; Convert each item into an emission (rx/mapcat identity) ;; Convert each item into an emission
(rx/switch-map read-item)))) (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 (defn request-fullscreen
[el] [el]
(cond (cond