🐛 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)
- 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

View File

@@ -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)

View File

@@ -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

View File

@@ -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<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]
* @returns {Promise<Array<Blob>>}
*/
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<Array<Blob>>}
*/
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());