mirror of
https://github.com/penpot/penpot.git
synced 2025-12-12 06:24:17 +01:00
♻️ Refactor clipboard
This commit is contained in:
committed by
Andrey Antukh
parent
9532dea2c6
commit
32e1b55658
@@ -90,6 +90,10 @@
|
||||
[{:fill-color clr/black
|
||||
:fill-opacity 1}])
|
||||
|
||||
(def default-paragraph-attrs
|
||||
{:text-align "left"
|
||||
:text-direction "ltr"})
|
||||
|
||||
(def default-text-attrs
|
||||
{:font-id "sourcesanspro"
|
||||
:font-family "sourcesanspro"
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
[app.main.features :as features]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.storage :as storage]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[clojure.string :as str]
|
||||
[potok.v2.core :as ptk]))
|
||||
@@ -417,7 +417,7 @@
|
||||
(rx/map (fn [fragment]
|
||||
(assoc cf/public-uri :fragment fragment)))
|
||||
(rx/tap (fn [uri]
|
||||
(wapi/write-to-clipboard (str uri))))
|
||||
(clipboard/to-clipboard (str uri))))
|
||||
(rx/tap on-success)
|
||||
(rx/ignore)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.main.streams :as ms]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.code-gen.markup-svg :as svg]
|
||||
[app.util.code-gen.style-css :as css]
|
||||
[app.util.globals :as ug]
|
||||
@@ -59,7 +60,6 @@
|
||||
[potok.v2.core :as ptk]
|
||||
[promesa.core :as p]))
|
||||
|
||||
|
||||
(defn copy-selected
|
||||
[]
|
||||
(letfn [(sort-selected [state data]
|
||||
@@ -183,7 +183,7 @@
|
||||
(let [text (wapi/get-current-selected-text)]
|
||||
(if-not (str/empty? text)
|
||||
(try
|
||||
(wapi/write-to-clipboard text)
|
||||
(clipboard/to-clipboard text)
|
||||
(catch :default e
|
||||
(on-copy-error e)))
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
(rx/map #(t/encode-str % {:type :json-verbose}))
|
||||
(rx/map #(wapi/create-blob % "text/plain"))
|
||||
(rx/subs! resolve reject))))]
|
||||
(->> (rx/from (wapi/write-to-clipboard-promise "text/plain" resolve-data-promise))
|
||||
(->> (rx/from (clipboard/to-clipboard-promise "text/plain" resolve-data-promise))
|
||||
(rx/catch on-copy-error)
|
||||
(rx/ignore)))
|
||||
|
||||
@@ -240,7 +240,7 @@
|
||||
(rx/map (partial sort-selected state))
|
||||
(rx/map (partial advance-copies state selected))
|
||||
(rx/map #(t/encode-str % {:type :json-verbose}))
|
||||
(rx/map wapi/write-to-clipboard)
|
||||
(rx/map clipboard/to-clipboard)
|
||||
(rx/catch on-copy-error)
|
||||
(rx/ignore))))))))))
|
||||
|
||||
@@ -252,49 +252,45 @@
|
||||
(declare ^:private paste-svg-text)
|
||||
(declare ^:private paste-shapes)
|
||||
|
||||
(defn create-paste-from-blob
|
||||
[in-viewport?]
|
||||
(fn [blob]
|
||||
(let [type (.-type blob)
|
||||
result (cond
|
||||
(= type "image/svg+xml")
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map paste-svg-text))
|
||||
|
||||
(some #(= type %) clipboard/image-types)
|
||||
(rx/of (paste-image blob))
|
||||
|
||||
(= type "text/html")
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map paste-html-text))
|
||||
|
||||
(= type "application/transit+json")
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map (fn [text]
|
||||
(let [transit-data (t/decode-str text)]
|
||||
(assoc transit-data :in-viewport in-viewport?))))
|
||||
(rx/map paste-transit-shapes))
|
||||
|
||||
:else
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map paste-text)))]
|
||||
result)))
|
||||
|
||||
(def default-paste-from-blob (create-paste-from-blob false))
|
||||
|
||||
(defn paste-from-clipboard
|
||||
"Perform a `paste` operation using the Clipboard API."
|
||||
[]
|
||||
(letfn [(decode-entry [entry]
|
||||
(try
|
||||
[:transit (t/decode-str entry)]
|
||||
(catch :default _cause
|
||||
[:text entry])))
|
||||
|
||||
(process-entry [[type data]]
|
||||
(case type
|
||||
:text
|
||||
(cond
|
||||
(str/empty? data)
|
||||
(rx/empty)
|
||||
|
||||
(re-find #"<svg\s" data)
|
||||
(rx/of (paste-svg-text data))
|
||||
|
||||
:else
|
||||
(rx/of (paste-text data)))
|
||||
|
||||
:transit
|
||||
(rx/of (paste-transit-shapes data))))
|
||||
|
||||
(on-error [cause]
|
||||
(let [data (ex-data cause)]
|
||||
(if (:not-implemented data)
|
||||
(rx/of (ntf/warn (tr "errors.clipboard-not-implemented")))
|
||||
(js/console.error "Clipboard error:" cause))
|
||||
(rx/empty)))]
|
||||
|
||||
(ptk/reify ::paste-from-clipboard
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rx/concat
|
||||
(->> (wapi/read-from-clipboard)
|
||||
(rx/map decode-entry)
|
||||
(rx/mapcat process-entry))
|
||||
(->> (wapi/read-image-from-clipboard)
|
||||
(rx/map paste-image)))
|
||||
(rx/take 1)
|
||||
(rx/catch on-error))))))
|
||||
(->> (clipboard/from-clipboard)
|
||||
(rx/mapcat default-paste-from-blob)
|
||||
(rx/take 1)))))
|
||||
|
||||
(defn paste-from-event
|
||||
"Perform a `paste` operation from user emmited event."
|
||||
@@ -310,30 +306,8 @@
|
||||
;; we forbid that scenario so the default behaviour is executed
|
||||
(if is-editing?
|
||||
(rx/empty)
|
||||
(let [pdata (wapi/read-from-paste-event event)
|
||||
image-data (some-> pdata wapi/extract-images)
|
||||
text-data (some-> pdata wapi/extract-text)
|
||||
html-data (some-> pdata wapi/extract-html-text)
|
||||
transit-data (ex/ignoring (some-> text-data t/decode-str))]
|
||||
(cond
|
||||
(and (string? text-data) (re-find #"<svg\s" text-data))
|
||||
(rx/of (paste-svg-text text-data))
|
||||
|
||||
(seq image-data)
|
||||
(->> (rx/from image-data)
|
||||
(rx/map paste-image))
|
||||
|
||||
(coll? transit-data)
|
||||
(rx/of (paste-transit-shapes (assoc transit-data :in-viewport in-viewport?)))
|
||||
|
||||
(and (string? html-data) (d/not-empty? html-data))
|
||||
(rx/of (paste-html-text html-data text-data))
|
||||
|
||||
(and (string? text-data) (d/not-empty? text-data))
|
||||
(rx/of (paste-text text-data))
|
||||
|
||||
:else
|
||||
(rx/empty))))))))
|
||||
(->> (clipboard/from-synthetic-clipboard-event event)
|
||||
(rx/mapcat (create-paste-from-blob in-viewport?))))))))
|
||||
|
||||
(defn copy-selected-svg
|
||||
[]
|
||||
@@ -352,7 +326,7 @@
|
||||
|
||||
shapes (mapv maybe-translate selected)
|
||||
svg-formatted (svg/generate-formatted-markup objects shapes)]
|
||||
(wapi/write-to-clipboard svg-formatted)))))
|
||||
(clipboard/to-clipboard svg-formatted)))))
|
||||
|
||||
(defn copy-selected-css
|
||||
[]
|
||||
@@ -362,7 +336,7 @@
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
selected (->> (dsh/lookup-selected state) (mapv (d/getf objects)))
|
||||
css (css/generate-style objects selected selected {:with-prelude? false})]
|
||||
(wapi/write-to-clipboard css)))))
|
||||
(clipboard/to-clipboard css)))))
|
||||
|
||||
(defn copy-selected-css-nested
|
||||
[]
|
||||
@@ -374,7 +348,7 @@
|
||||
(cfh/selected-with-children objects)
|
||||
(mapv (d/getf objects)))
|
||||
css (css/generate-style objects selected selected {:with-prelude? false})]
|
||||
(wapi/write-to-clipboard css)))))
|
||||
(clipboard/to-clipboard css)))))
|
||||
|
||||
(defn copy-selected-text
|
||||
[]
|
||||
@@ -405,7 +379,7 @@
|
||||
(-> shape :content txt/content->text))))
|
||||
(str/join "\n"))]
|
||||
|
||||
(wapi/write-to-clipboard text)))))
|
||||
(clipboard/to-clipboard text)))))
|
||||
|
||||
(defn copy-selected-props
|
||||
[]
|
||||
@@ -474,7 +448,7 @@
|
||||
(rx/map #(wapi/create-blob % "text/plain"))
|
||||
(rx/subs! resolve reject))))]
|
||||
|
||||
(->> (rx/from (wapi/write-to-clipboard-promise "text/plain" resolve-data-promise))
|
||||
(->> (rx/from (clipboard/to-clipboard-promise "text/plain" resolve-data-promise))
|
||||
(rx/catch on-copy-error)
|
||||
(rx/ignore)))
|
||||
;; FIXME: this is to support Firefox versions below 116 that don't support
|
||||
@@ -482,7 +456,7 @@
|
||||
;; https://caniuse.com/?search=ClipboardItem
|
||||
(->> (rx/of copy-data)
|
||||
(rx/mapcat resolve-images)
|
||||
(rx/map #(wapi/write-to-clipboard (t/encode-str % {:type :json-verbose})))
|
||||
(rx/map #(clipboard/to-clipboard (t/encode-str % {:type :json-verbose})))
|
||||
(rx/catch on-copy-error)
|
||||
(rx/ignore))))))))))))
|
||||
|
||||
@@ -502,7 +476,8 @@
|
||||
(js/console.error "Clipboard error:" cause))
|
||||
(rx/empty)))]
|
||||
|
||||
(->> (wapi/read-from-clipboard)
|
||||
(->> (clipboard/from-clipboard)
|
||||
(rx/mapcat #(.text %))
|
||||
(rx/map decode-entry)
|
||||
(rx/take 1)
|
||||
(rx/catch on-error)))))))
|
||||
@@ -968,15 +943,19 @@
|
||||
(deref ms/mouse-position)))
|
||||
|
||||
(defn- paste-html-text
|
||||
[html text]
|
||||
[html]
|
||||
(js/console.log html)
|
||||
(dm/assert! (string? html))
|
||||
(ptk/reify ::paste-html-text
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [style (deref refs/workspace-clipboard-style)
|
||||
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
|
||||
@@ -1051,4 +1030,4 @@
|
||||
(ptk/reify ::copy-link-to-clipboard
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(wapi/write-to-clipboard (rt/get-current-href)))))
|
||||
(clipboard/to-clipboard (rt/get-current-href)))))
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.debug.icons-preview :refer [icons-preview]]
|
||||
[app.main.ui.debug.playground :refer [playground]]
|
||||
[app.main.ui.ds.product.loader :refer [loader*]]
|
||||
[app.main.ui.error-boundary :refer [error-boundary*]]
|
||||
[app.main.ui.exports.files]
|
||||
@@ -209,6 +210,10 @@
|
||||
(when *assert*
|
||||
[:& icons-preview])
|
||||
|
||||
:debug-playground
|
||||
(when *assert*
|
||||
[:& playground])
|
||||
|
||||
(:dashboard-search
|
||||
:dashboard-recent
|
||||
:dashboard-files
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.event :as-alias ev]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.timers :as tm]
|
||||
[app.util.webapi :as wapi]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc copy-button*
|
||||
@@ -34,7 +34,7 @@
|
||||
(reset! active* true)
|
||||
(tm/schedule 1000 #(reset! active* false))
|
||||
(when (fn? on-copied) (on-copied event))
|
||||
(wapi/write-to-clipboard
|
||||
(clipboard/to-clipboard
|
||||
(if (fn? data) (data) data)))))]
|
||||
|
||||
[:button {:class class
|
||||
|
||||
51
frontend/src/app/main/ui/debug/playground.cljs
Normal file
51
frontend/src/app/main/ui/debug/playground.cljs
Normal file
@@ -0,0 +1,51 @@
|
||||
(ns app.main.ui.debug.playground
|
||||
#_(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.util.clipboard :as clipboard]
|
||||
[beicon.v2.core :as rx]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc playground-clipboard
|
||||
{::mf/wrap-props false
|
||||
::mf/private true}
|
||||
[]
|
||||
(let [on-paste (mf/use-fn
|
||||
(fn [e]
|
||||
(let [stream (clipboard/from-clipboard-event e)]
|
||||
(rx/sub! stream
|
||||
(fn [data]
|
||||
(js/console.log "data" data))))))
|
||||
|
||||
on-dragover (mf/use-fn
|
||||
(fn [e]
|
||||
(.preventDefault e)))
|
||||
|
||||
on-drop (mf/use-fn
|
||||
(fn [e]
|
||||
(.preventDefault e)
|
||||
(let [stream (clipboard/from-drop-event e)]
|
||||
(rx/sub! stream
|
||||
(fn [data]
|
||||
(js/console.log "data" data))))))
|
||||
|
||||
on-click (mf/use-fn
|
||||
(fn [e]
|
||||
(js/console.log "event" e)
|
||||
(let [stream (clipboard/from-clipboard)]
|
||||
(rx/sub! stream
|
||||
(fn [data]
|
||||
(js/console.log "data" data))))))]
|
||||
|
||||
(.addEventListener js/window "paste" on-paste)
|
||||
(.addEventListener js/window "drop" on-drop)
|
||||
(.addEventListener js/window "dragover" on-dragover)
|
||||
|
||||
[:button#paste {:on-click on-click} "Paste"]))
|
||||
|
||||
(mf/defc playground
|
||||
{::mf/wrap-props false
|
||||
::mf/private true}
|
||||
[]
|
||||
[:& playground-clipboard])
|
||||
|
||||
|
||||
0
frontend/src/app/main/ui/debug/playground.scss
Normal file
0
frontend/src/app/main/ui/debug/playground.scss
Normal file
@@ -23,11 +23,11 @@
|
||||
[app.main.ui.hooks.resize :refer [use-resize-hook]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.shapes.text.fontfaces :refer [shapes->fonts]]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.code-beautify :as cb]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.http :as http]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
@@ -202,7 +202,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps style-code markup-code images-data)
|
||||
(fn []
|
||||
(wapi/write-to-clipboard (gen-all-code style-code markup-code images-data))
|
||||
(clipboard/to-clipboard (gen-all-code style-code markup-code images-data))
|
||||
(let [origin (if (= :workspace from)
|
||||
"workspace"
|
||||
"viewer")]
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
[app.main.ui.inspect.styles.property-detail-copiable :refer [property-detail-copiable*]]
|
||||
[app.main.ui.inspect.styles.rows.color-properties-row :refer [color-properties-row*]]
|
||||
[app.main.ui.inspect.styles.rows.properties-row :refer [properties-row*]]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.timers :as tm]
|
||||
[app.util.webapi :as wapi]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
(.toUpperCase text)
|
||||
text)]
|
||||
(reset! copied* true)
|
||||
(wapi/write-to-clipboard formatted-text)
|
||||
(clipboard/to-clipboard formatted-text)
|
||||
(tm/schedule 1000 #(reset! copied* false)))))
|
||||
composite-typography-token (get-resolved-token :typography shape resolved-tokens)]
|
||||
[:div {:class (stl/css :text-properties)}
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
[app.main.ui.ds.tooltip :refer [tooltip*]]
|
||||
[app.main.ui.formats :as fmt]
|
||||
[app.main.ui.inspect.styles.property-detail-copiable :refer [property-detail-copiable*]]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.color :as uc]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.timers :as tm]
|
||||
[app.util.webapi :as wapi]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
(mf/deps copied formatted-color-value)
|
||||
(fn []
|
||||
(reset! copied* true)
|
||||
(wapi/write-to-clipboard copiable-value)
|
||||
(clipboard/to-clipboard copiable-value)
|
||||
(tm/schedule 1000 #(reset! copied* false))))]
|
||||
[:*
|
||||
[:dl {:class [(stl/css :property-row) class]
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
format-token-value]]
|
||||
[app.main.ui.ds.tooltip :refer [tooltip*]]
|
||||
[app.main.ui.inspect.styles.property-detail-copiable :refer [property-detail-copiable*]]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.timers :as tm]
|
||||
[app.util.webapi :as wapi]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
(mf/deps copied)
|
||||
(fn []
|
||||
(reset! copied* true)
|
||||
(wapi/write-to-clipboard copiable-value)
|
||||
(clipboard/to-clipboard copiable-value)
|
||||
(tm/schedule 1000 #(reset! copied* false))))]
|
||||
[:dl {:class [(stl/css :property-row) class]
|
||||
:data-testid "property-row"}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
[app.common.data :as d]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.webapi :as wapi]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- panel->title
|
||||
@@ -50,7 +50,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps shorthand)
|
||||
(fn []
|
||||
(wapi/write-to-clipboard (str shorthand))))]
|
||||
(clipboard/to-clipboard (str shorthand))))]
|
||||
[:article {:class (stl/css :style-box)}
|
||||
[:header {:class (stl/css :disclosure-header)}
|
||||
[:button {:class (stl/css :disclosure-button)
|
||||
|
||||
@@ -48,6 +48,9 @@
|
||||
(when *assert*
|
||||
["/debug/icons-preview" :debug-icons-preview])
|
||||
|
||||
(when *assert*
|
||||
["/debug/playground" :debug-playground])
|
||||
|
||||
;; Used for export
|
||||
["/render-sprite/:file-id" :render-sprite]
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.webapi :as wapi]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
(mf/deps created)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(wapi/write-to-clipboard (:token created))
|
||||
(clipboard/to-clipboard (:token created))
|
||||
(st/emit! (ntf/show {:level :info
|
||||
:type :toast
|
||||
:content (tr "dashboard.access-tokens.copied-success")
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.select :refer [select]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.webapi :as wapi]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
|
||||
copy-link
|
||||
(fn [_]
|
||||
(wapi/write-to-clipboard current-link)
|
||||
(clipboard/to-clipboard current-link)
|
||||
(st/emit! (ntf/show {:level :info
|
||||
:type :toast
|
||||
:content (tr "common.share-link.link-copied-success")
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.workspace.sidebar.assets.common :as cmm]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr] :as i18n]
|
||||
[app.util.shape-icon :as usi]
|
||||
[app.util.timers :as timers]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[okulary.core :as l]
|
||||
[potok.v2.core :as ptk]
|
||||
@@ -181,7 +181,8 @@
|
||||
handle-hover-copy-paste
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(->> (wapi/read-from-clipboard)
|
||||
(->> (clipboard/from-clipboard)
|
||||
(rx/mapcat #(.text %))
|
||||
(rx/take 1)
|
||||
(rx/subs!
|
||||
(fn [data]
|
||||
|
||||
59
frontend/src/app/util/clipboard.cljs
Normal file
59
frontend/src/app/util/clipboard.cljs
Normal file
@@ -0,0 +1,59 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.util.clipboard
|
||||
(:require
|
||||
["./clipboard.js" :as clipboard]
|
||||
[app.common.transit :as t]
|
||||
[beicon.v2.core :as rx]))
|
||||
|
||||
(def image-types
|
||||
["image/webp"
|
||||
"image/png"
|
||||
"image/jpeg"
|
||||
"image/svg+xml"])
|
||||
|
||||
(def clipboard-settings #js {:decodeTransit t/decode-str})
|
||||
|
||||
(defn from-clipboard []
|
||||
(->> (rx/from (clipboard/fromClipboard clipboard-settings))
|
||||
(rx/mapcat #(rx/from %))))
|
||||
|
||||
(defn from-data-transfer [data-transfer]
|
||||
(->> (rx/from (clipboard/fromDataTransfer data-transfer clipboard-settings))
|
||||
(rx/mapcat #(rx/from %))))
|
||||
|
||||
(defn from-clipboard-data [clipboard-data]
|
||||
(from-data-transfer clipboard-data))
|
||||
|
||||
(defn from-clipboard-event [event]
|
||||
(from-clipboard-data (.-clipboardData 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)))))
|
||||
|
||||
(defn from-drop-event [event]
|
||||
(from-data-transfer (.-dataTransfer event)))
|
||||
|
||||
(defn to-clipboard
|
||||
[data]
|
||||
(assert (string? data) "`data` should be string")
|
||||
(let [clipboard (unchecked-get js/navigator "clipboard")]
|
||||
(.writeText ^js clipboard data)))
|
||||
|
||||
(defn- create-clipboard-item
|
||||
[mimetype promise]
|
||||
(js/ClipboardItem.
|
||||
(js-obj mimetype promise)))
|
||||
|
||||
(defn to-clipboard-promise
|
||||
[mimetype promise]
|
||||
(let [clipboard (unchecked-get js/navigator "clipboard")
|
||||
data (create-clipboard-item mimetype promise)]
|
||||
(.write ^js clipboard #js [data])))
|
||||
151
frontend/src/app/util/clipboard.js
Normal file
151
frontend/src/app/util/clipboard.js
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright (c) KALEIDOS INC
|
||||
*/
|
||||
const maxParseableSize = 16 * 1024 * 1024;
|
||||
|
||||
const allowedTypes = [
|
||||
"image/webp",
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/svg+xml",
|
||||
"application/transit+json",
|
||||
"text/html",
|
||||
"text/plain",
|
||||
];
|
||||
|
||||
const exclusiveTypes = [
|
||||
"application/transit+json",
|
||||
"text/html",
|
||||
"text/plain"
|
||||
];
|
||||
|
||||
/**
|
||||
* @typedef {Object} ClipboardSettings
|
||||
* @property {Function} [decodeTransit]
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} text
|
||||
* @param {ClipboardSettings} options
|
||||
* @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 */ }
|
||||
if (/^<svg[\s>]/i.test(text)) {
|
||||
return new Blob([text], { type: "image/svg+xml" });
|
||||
} else if (decodedTransit) {
|
||||
return new Blob([text], { type: "application/transit+json" });
|
||||
}
|
||||
return defaultReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ClipboardSettings} [options]
|
||||
* @returns {Promise<Array<Blob>>}
|
||||
*/
|
||||
export async function fromClipboard(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)
|
||||
.filter((type) => allowedTypes.includes(type))
|
||||
.sort((a, b) => allowedTypes.indexOf(a) - allowedTypes.indexOf(b));
|
||||
|
||||
if (
|
||||
itemAllowedTypes.length === 1 &&
|
||||
itemAllowedTypes.at(0) === "text/plain"
|
||||
) {
|
||||
const blob = await item.getType("text/plain");
|
||||
if (blob.size < maxParseableSize) {
|
||||
const text = await blob.text();
|
||||
return parseClipboardItemText(text, options);
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
|
||||
const type = itemAllowedTypes.at(0);
|
||||
return item.getType(type);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DataTransfer} dataTransfer
|
||||
* @param {ClipboardSettings} [options]
|
||||
* @returns {Promise<Array<Blob>>}
|
||||
*/
|
||||
export async function fromDataTransfer(dataTransfer, options) {
|
||||
const items = await Promise.all(
|
||||
Array.from(dataTransfer.items)
|
||||
.filter((item) => allowedTypes.includes(item.type))
|
||||
.sort(
|
||||
(a, b) => allowedTypes.indexOf(a.type) - allowedTypes.indexOf(b.type),
|
||||
)
|
||||
.map(async (item) => {
|
||||
if (item.kind === "file") {
|
||||
return Promise.resolve(item.getAsFile());
|
||||
} else if (item.kind === "string") {
|
||||
return new Promise((resolve) => {
|
||||
const type = item.type;
|
||||
item.getAsString((text) => {
|
||||
if (type === "text/plain") {
|
||||
return resolve(parseClipboardItemText(text, options));
|
||||
}
|
||||
return resolve(new Blob([text], { type }));
|
||||
});
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}),
|
||||
);
|
||||
return items
|
||||
.filter((item) => !!item)
|
||||
.reduce((filtered, item) => {
|
||||
if (
|
||||
exclusiveTypes.includes(item.type) &&
|
||||
filtered.find((filteredItem) =>
|
||||
exclusiveTypes.includes(filteredItem.type),
|
||||
)
|
||||
) {
|
||||
return filtered;
|
||||
}
|
||||
filtered.push(item);
|
||||
return filtered;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} clipboardData
|
||||
* @param {ClipboardSettings} [options]
|
||||
* @returns {Promise<Array<Blob>>}
|
||||
*/
|
||||
export function fromClipboardData(clipboardData, options) {
|
||||
return fromDataTransfer(clipboardData, options);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} e
|
||||
* @param {ClipboardSettings} [options]
|
||||
* @returns {Promise<Array<Blob>>}
|
||||
*/
|
||||
export function fromClipboardEvent(e, options) {
|
||||
return fromClipboardData(e.clipboardData, options);
|
||||
}
|
||||
@@ -37,34 +37,36 @@
|
||||
(.-textContent element)))
|
||||
|
||||
(defn get-attrs-from-styles
|
||||
[element attrs]
|
||||
[element attrs defaults]
|
||||
(reduce (fn [acc key]
|
||||
(let [style (.-style element)]
|
||||
(if (contains? styles/mapping key)
|
||||
(let [style-name (styles/get-style-name-as-css-variable key)
|
||||
[_ style-decode] (get styles/mapping key)
|
||||
value (style-decode (.getPropertyValue style style-name))]
|
||||
(assoc acc key value))
|
||||
(let [style-name (styles/get-style-name key)]
|
||||
(assoc acc key (styles/normalize-attr-value key (.getPropertyValue style style-name))))))) {} attrs))
|
||||
(assoc acc key (if (empty? value) (get defaults key) value)))
|
||||
(let [style-name (styles/get-style-name key)
|
||||
value (styles/normalize-attr-value key (.getPropertyValue style style-name))]
|
||||
(assoc acc key (if (empty? value) (get defaults key) value)))))) {} attrs))
|
||||
|
||||
(defn get-inline-styles
|
||||
[element]
|
||||
(get-attrs-from-styles element txt/text-node-attrs))
|
||||
(get-attrs-from-styles element txt/text-node-attrs (txt/get-default-text-attrs)))
|
||||
|
||||
(defn get-paragraph-styles
|
||||
[element]
|
||||
(get-attrs-from-styles element (d/concat-set txt/paragraph-attrs txt/text-node-attrs)))
|
||||
(get-attrs-from-styles element (d/concat-set txt/paragraph-attrs txt/text-node-attrs) (d/merge txt/default-paragraph-attrs txt/default-text-attrs)))
|
||||
|
||||
(defn get-root-styles
|
||||
[element]
|
||||
(get-attrs-from-styles element txt/root-attrs))
|
||||
(get-attrs-from-styles element txt/root-attrs txt/default-root-attrs))
|
||||
|
||||
(defn create-inline
|
||||
[element]
|
||||
(d/merge {:text (get-inline-text element)
|
||||
(let [text (get-inline-text element)]
|
||||
(d/merge {:text text
|
||||
:key (.-id element)}
|
||||
(get-inline-styles element)))
|
||||
(get-inline-styles element))))
|
||||
|
||||
(defn create-paragraph
|
||||
[element]
|
||||
@@ -76,7 +78,7 @@
|
||||
(defn create-root
|
||||
[element]
|
||||
(let [root-styles (get-root-styles element)]
|
||||
(d/merge {:type "root",
|
||||
(d/merge {:type "root"
|
||||
:key (.-id element)
|
||||
:children [{:type "paragraph-set"
|
||||
:children (mapv create-paragraph (.-children element))}]}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
(ns app.util.webapi
|
||||
"HTML5 web api helpers."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as log]
|
||||
[app.util.globals :as globals]
|
||||
@@ -115,68 +114,6 @@
|
||||
[]
|
||||
(.. js/window getSelection toString))
|
||||
|
||||
(defn write-to-clipboard
|
||||
[data]
|
||||
(assert (string? data) "`data` should be string")
|
||||
(let [cboard (unchecked-get js/navigator "clipboard")]
|
||||
(.writeText ^js cboard data)))
|
||||
|
||||
(defn write-to-clipboard-promise
|
||||
[mimetype promise]
|
||||
(let [cboard (unchecked-get js/navigator "clipboard")
|
||||
data (js/ClipboardItem.
|
||||
(-> (obj/create)
|
||||
(obj/set! mimetype promise)))]
|
||||
(.write ^js cboard #js [data])))
|
||||
|
||||
(defn read-from-clipboard
|
||||
[]
|
||||
(try
|
||||
(let [cboard (unchecked-get js/navigator "clipboard")]
|
||||
(if (.-readText ^js cboard)
|
||||
(rx/from (.readText ^js cboard))
|
||||
(rx/throw (ex-info "This browser does not implement read from clipboard protocol"
|
||||
{:not-implemented true}))))
|
||||
(catch :default cause
|
||||
(rx/throw cause))))
|
||||
|
||||
(defn read-image-from-clipboard
|
||||
[]
|
||||
(try
|
||||
(let [cboard (unchecked-get js/navigator "clipboard")
|
||||
read-item (fn [item]
|
||||
(let [img-type (->> (.-types ^js item)
|
||||
(d/seek #(str/starts-with? % "image/")))]
|
||||
(if img-type
|
||||
(rx/from (.getType ^js item img-type))
|
||||
(rx/empty))))]
|
||||
(->> (rx/from (.read ^js cboard)) ;; Get a stream of item lists
|
||||
(rx/mapcat identity) ;; Convert each item into an emission
|
||||
(rx/switch-map read-item)))
|
||||
(catch :default cause
|
||||
(rx/throw cause))))
|
||||
|
||||
(defn read-from-paste-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
|
||||
(.. ^js event getBrowserEvent -clipboardData))))
|
||||
|
||||
(defn extract-html-text
|
||||
[clipboard-data]
|
||||
(.getData clipboard-data "text/html"))
|
||||
|
||||
(defn extract-text
|
||||
[clipboard-data]
|
||||
(.getData clipboard-data "text"))
|
||||
|
||||
(defn extract-images
|
||||
"Get image files from clipboard data. Returns a native js array."
|
||||
[clipboard-data]
|
||||
(let [files (obj/into-array (.-files ^js clipboard-data))]
|
||||
(.filter ^js files #(str/starts-with? (obj/get % "type") "image/"))))
|
||||
|
||||
(defn create-canvas-element
|
||||
[width height]
|
||||
(let [canvas (.createElement js/document "canvas")]
|
||||
|
||||
Reference in New Issue
Block a user