🐛 Fix pasting application/transit+json (#7812)

This commit is contained in:
Aitor Moreno
2025-11-24 14:36:24 +01:00
committed by GitHub
parent 00bbb0bfb6
commit f58475a7c9
4 changed files with 114 additions and 34 deletions

View File

@@ -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) - 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) - 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) - 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 ### :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 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 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 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 ## 2.11.1

View File

@@ -254,6 +254,10 @@
(declare ^:private paste-svg-text) (declare ^:private paste-svg-text)
(declare ^:private paste-shapes) (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 (defn create-paste-from-blob
[in-viewport?] [in-viewport?]
(fn [blob] (fn [blob]
@@ -290,7 +294,7 @@
(ptk/reify ::paste-from-clipboard (ptk/reify ::paste-from-clipboard
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(->> (clipboard/from-navigator) (->> (clipboard/from-navigator default-options)
(rx/mapcat default-paste-from-blob) (rx/mapcat default-paste-from-blob)
(rx/take 1))))) (rx/take 1)))))
@@ -308,7 +312,7 @@
;; we forbid that scenario so the default behaviour is executed ;; we forbid that scenario so the default behaviour is executed
(if is-editing? (if is-editing?
(rx/empty) (rx/empty)
(->> (clipboard/from-synthetic-clipboard-event event) (->> (clipboard/from-synthetic-clipboard-event event default-options)
(rx/mapcat (create-paste-from-blob in-viewport?)))))))) (rx/mapcat (create-paste-from-blob in-viewport?))))))))
(defn copy-selected-svg (defn copy-selected-svg
@@ -478,7 +482,7 @@
(js/console.error "Clipboard error:" cause)) (js/console.error "Clipboard error:" cause))
(rx/empty)))] (rx/empty)))]
(->> (clipboard/from-navigator) (->> (clipboard/from-navigator default-options)
(rx/mapcat #(.text %)) (rx/mapcat #(.text %))
(rx/map decode-entry) (rx/map decode-entry)
(rx/take 1) (rx/take 1)

View File

@@ -18,28 +18,35 @@
"image/svg+xml"]) "image/svg+xml"])
(def ^:private default-options (def ^:private default-options
#js {:decodeTransit t/decode-str}) #js {:decodeTransit t/decode-str
:allowHTMLPaste false})
(defn- from-data-transfer (defn- from-data-transfer
"Get clipboard stream from DataTransfer instance" "Get clipboard stream from DataTransfer instance"
[data-transfer] ([data-transfer]
(->> (rx/from (impl/fromDataTransfer data-transfer default-options)) (from-data-transfer data-transfer default-options))
(rx/mapcat #(rx/from %)))) ([data-transfer options]
(->> (rx/from (impl/fromDataTransfer data-transfer options))
(rx/mapcat #(rx/from %)))))
(defn from-navigator (defn from-navigator
[] ([]
(->> (rx/from (impl/fromNavigator default-options)) (from-navigator default-options))
(rx/mapcat #(rx/from %)))) ([options]
(->> (rx/from (impl/fromNavigator options))
(rx/mapcat #(rx/from %)))))
(defn from-clipboard-event (defn from-clipboard-event
"Get clipboard stream from clipboard event" "Get clipboard stream from clipboard event"
[event] ([event]
(from-clipboard-event event default-options))
([event options]
(let [cdata (.-clipboardData ^js event)] (let [cdata (.-clipboardData ^js event)]
(from-data-transfer cdata))) (from-data-transfer cdata options))))
(defn from-synthetic-clipboard-event (defn from-synthetic-clipboard-event
"Get clipboard stream from syntetic clipboard event" "Get clipboard stream from syntetic clipboard event"
[event] ([event options]
(let [target (let [target
(dom/get-target event) (dom/get-target event)
@@ -53,12 +60,14 @@
(when-not (or content-editable? is-input?) (when-not (or content-editable? is-input?)
(-> event (-> event
(dom/event->browser-event) (dom/event->browser-event)
(from-clipboard-event))))) (from-clipboard-event options))))))
(defn from-drop-event (defn from-drop-event
"Get clipboard stream from drop event" "Get clipboard stream from drop event"
[event] ([event]
(from-data-transfer (.-dataTransfer ^js event))) (from-drop-event event default-options))
([event options]
(from-data-transfer (.-dataTransfer ^js event) options)))
;; FIXME: rename to `write-text` ;; FIXME: rename to `write-text`
(defn to-clipboard (defn to-clipboard

View File

@@ -27,6 +27,7 @@ const exclusiveTypes = [
/** /**
* @typedef {Object} ClipboardSettings * @typedef {Object} ClipboardSettings
* @property {Function} [decodeTransit] * @property {Function} [decodeTransit]
* @property {boolean} [allowHTMLPaste]
*/ */
/** /**
@@ -38,9 +39,7 @@ const exclusiveTypes = [
*/ */
function parseText(text, options) { function parseText(text, options) {
options = options || {}; options = options || {};
const decodeTransit = options["decodeTransit"]; const decodeTransit = options["decodeTransit"];
if (decodeTransit) { if (decodeTransit) {
try { try {
decodeTransit(text); decodeTransit(text);
@@ -57,18 +56,85 @@ function parseText(text, options) {
} }
} }
/**
* Filters ClipboardItem types
*
* @param {ClipboardSettings} options
* @returns {Function<AllowedTypesFilterFunction>}
*/
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<AllowedTypesFilterFunction>}
*/
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] * @param {ClipboardSettings} [options]
* @returns {Promise<Array<Blob>>} * @returns {Promise<Array<Blob>>}
*/ */
export async function fromNavigator(options) { export async function fromNavigator(options) {
options = options || {};
const items = await navigator.clipboard.read(); const items = await navigator.clipboard.read();
return Promise.all( return Promise.all(
Array.from(items).map(async (item) => { Array.from(items).map(async (item) => {
const itemAllowedTypes = Array.from(item.types) const itemAllowedTypes = Array.from(item.types)
.filter((type) => allowedTypes.includes(type)) .filter(filterAllowedTypes(options))
.sort((a, b) => allowedTypes.indexOf(a) - allowedTypes.indexOf(b)); .sort(sortTypes);
if ( if (
itemAllowedTypes.length === 1 && itemAllowedTypes.length === 1 &&
@@ -96,12 +162,11 @@ export async function fromNavigator(options) {
* @returns {Promise<Array<Blob>>} * @returns {Promise<Array<Blob>>}
*/ */
export async function fromDataTransfer(dataTransfer, options) { export async function fromDataTransfer(dataTransfer, options) {
options = options || {};
const items = await Promise.all( const items = await Promise.all(
Array.from(dataTransfer.items) Array.from(dataTransfer.items)
.filter((item) => allowedTypes.includes(item.type)) .filter(filterAllowedItems(options))
.sort( .sort(sortItems)
(a, b) => allowedTypes.indexOf(a.type) - allowedTypes.indexOf(b.type),
)
.map(async (item) => { .map(async (item) => {
if (item.kind === "file") { if (item.kind === "file") {
return Promise.resolve(item.getAsFile()); return Promise.resolve(item.getAsFile());