diff --git a/CHANGES.md b/CHANGES.md index 929385dda7..9c2ed4c55a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -71,6 +71,7 @@ example. It's still usable as before, we just removed the example. - Add new shape validation mechanism for shapes [Github #7696](https://github.com/penpot/penpot/pull/7696) - Apply color tokens from sidebar [Taiga #11353](https://tree.taiga.io/project/penpot/us/11353) - Display tokens in the inspect tab [Taiga #9313](https://tree.taiga.io/project/penpot/us/9313) +- Refactor clipboard behavior to assess some minor inconsistencies and make pasting binary data faster. [Taiga #12571](https://tree.taiga.io/project/penpot/task/12571) ### :bug: Bugs fixed @@ -85,6 +86,7 @@ example. It's still usable as before, we just removed the example. - Fix shortcut conflict in text editor (increase/decrease font size vs word selection) - Fix problem with plugins generating code for pages different than current one [Taiga #12312](https://tree.taiga.io/project/penpot/issue/12312) - Fix input confirmation behavior is not uniform [Taiga #12294](https://tree.taiga.io/project/penpot/issue/12294) +- Fix copy/pasting application/transit+json [Taiga #12721](https://tree.taiga.io/project/penpot/issue/12721) ## 2.11.1 diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index c850b014e5..9e95f9c769 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -254,6 +254,10 @@ (declare ^:private paste-svg-text) (declare ^:private paste-shapes) +(def ^:private default-options + #js {:decodeTransit t/decode-str + :allowHTMLPaste (features/active-feature? @st/state "text-editor/v2-html-paste")}) + (defn create-paste-from-blob [in-viewport?] (fn [blob] @@ -290,7 +294,7 @@ (ptk/reify ::paste-from-clipboard ptk/WatchEvent (watch [_ _ _] - (->> (clipboard/from-navigator) + (->> (clipboard/from-navigator default-options) (rx/mapcat default-paste-from-blob) (rx/take 1))))) @@ -308,7 +312,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-clipboard-event event default-options) (rx/mapcat (create-paste-from-blob in-viewport?)))))))) (defn copy-selected-svg @@ -478,7 +482,7 @@ (js/console.error "Clipboard error:" cause)) (rx/empty)))] - (->> (clipboard/from-navigator) + (->> (clipboard/from-navigator default-options) (rx/mapcat #(.text %)) (rx/map decode-entry) (rx/take 1) diff --git a/frontend/src/app/util/clipboard.cljs b/frontend/src/app/util/clipboard.cljs index 7c7b56da26..e70c567881 100644 --- a/frontend/src/app/util/clipboard.cljs +++ b/frontend/src/app/util/clipboard.cljs @@ -18,47 +18,56 @@ "image/svg+xml"]) (def ^:private default-options - #js {:decodeTransit t/decode-str}) + #js {:decodeTransit t/decode-str + :allowHTMLPaste false}) (defn- from-data-transfer "Get clipboard stream from DataTransfer instance" - [data-transfer] - (->> (rx/from (impl/fromDataTransfer data-transfer default-options)) - (rx/mapcat #(rx/from %)))) + ([data-transfer] + (from-data-transfer data-transfer default-options)) + ([data-transfer options] + (->> (rx/from (impl/fromDataTransfer data-transfer options)) + (rx/mapcat #(rx/from %))))) (defn from-navigator - [] - (->> (rx/from (impl/fromNavigator default-options)) - (rx/mapcat #(rx/from %)))) + ([] + (from-navigator default-options)) + ([options] + (->> (rx/from (impl/fromNavigator options)) + (rx/mapcat #(rx/from %))))) (defn from-clipboard-event "Get clipboard stream from clipboard event" - [event] - (let [cdata (.-clipboardData ^js event)] - (from-data-transfer cdata))) + ([event] + (from-clipboard-event event default-options)) + ([event options] + (let [cdata (.-clipboardData ^js event)] + (from-data-transfer cdata options)))) (defn from-synthetic-clipboard-event "Get clipboard stream from syntetic clipboard event" - [event] - (let [target - (dom/get-target event) + ([event options] + (let [target + (dom/get-target event) - content-editable? - (dom/is-content-editable? target) + content-editable? + (dom/is-content-editable? target) - is-input? - (= (dom/get-tag-name target) "INPUT")] + 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))))) + (when-not (or content-editable? is-input?) + (-> event + (dom/event->browser-event) + (from-clipboard-event options)))))) (defn from-drop-event "Get clipboard stream from drop event" - [event] - (from-data-transfer (.-dataTransfer ^js event))) + ([event] + (from-drop-event event default-options)) + ([event options] + (from-data-transfer (.-dataTransfer ^js event) options))) ;; FIXME: rename to `write-text` (defn to-clipboard diff --git a/frontend/src/app/util/clipboard.js b/frontend/src/app/util/clipboard.js index 5fe4d88865..96f4080a7e 100644 --- a/frontend/src/app/util/clipboard.js +++ b/frontend/src/app/util/clipboard.js @@ -27,6 +27,7 @@ const exclusiveTypes = [ /** * @typedef {Object} ClipboardSettings * @property {Function} [decodeTransit] + * @property {boolean} [allowHTMLPaste] */ /** @@ -38,9 +39,7 @@ const exclusiveTypes = [ */ function parseText(text, options) { options = options || {}; - const decodeTransit = options["decodeTransit"]; - if (decodeTransit) { try { decodeTransit(text); @@ -57,18 +56,85 @@ function parseText(text, options) { } } +/** + * Filters ClipboardItem types + * + * @param {ClipboardSettings} options + * @returns {Function} + */ +function filterAllowedTypes(options) { + /** + * @param {string} type + * @returns {boolean} + */ + return function filter(type) { + if ( + (!("allowHTMLPaste" in options) || !options["allowHTMLPaste"]) && + type === "text/html" + ) { + return false; + } + return allowedTypes.includes(type); + }; +} + +/** + * Filters DataTransferItems + * + * @param {ClipboardSettings} options + * @returns {Function} + */ +function filterAllowedItems(options) { + /** + * @param {DataTransferItem} + * @returns {boolean} + */ + return function filter(item) { + if ( + (!("allowHTMLPaste" in options) || !options["allowHTMLPaste"]) && + item.type === "text/html" + ) { + return false; + } + return allowedTypes.includes(item.type); + }; +} + +/** + * Sorts ClipboardItem types + * + * @param {string} a + * @param {string} b + * @returns {number} + */ +function sortTypes(a, b) { + return allowedTypes.indexOf(a) - allowedTypes.indexOf(b); +} + +/** + * Sorts DataTransferItems + * + * @param {DataTransferItem} a + * @param {DataTransferItem} b + * @returns {number} + */ +function sortItems(a, b) { + return allowedTypes.indexOf(a.type) - allowedTypes.indexOf(b.type); +} + /** * * @param {ClipboardSettings} [options] * @returns {Promise>} */ export async function fromNavigator(options) { + options = options || {}; const items = await navigator.clipboard.read(); 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)); + .filter(filterAllowedTypes(options)) + .sort(sortTypes); if ( itemAllowedTypes.length === 1 && @@ -96,12 +162,11 @@ export async function fromNavigator(options) { * @returns {Promise>} */ export async function fromDataTransfer(dataTransfer, options) { + options = 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), - ) + .filter(filterAllowedItems(options)) + .sort(sortItems) .map(async (item) => { if (item.kind === "file") { return Promise.resolve(item.getAsFile());