A partir de Chrome 120, hay una nueva opción unsanitized
disponible en la API de Async Clipboard. Esta opción puede ser útil en situaciones especiales con HTML, en las que necesitas pegar el contenido del portapapeles de la misma manera que estaba cuando se copió.
Es decir, sin ningún paso de limpieza intermedio que los navegadores suelen aplicar, y con razón. Obtén información para usarla en esta guía.
Cuando se trabaja con la API de Async Clipboard, en la mayoría de los casos, los desarrolladores no tienen que preocuparse por la integridad del contenido del portapapeles y pueden suponer que lo que escriben en el portapapeles (copiar) es lo mismo que obtendrán cuando lean los datos del portapapeles (pegar).
Esto es cierto en el caso del texto. Intenta pegar el siguiente código en la Consola de DevTools y, luego, vuelve a enfocar la página de inmediato. (El setTimeout()
es necesario para que tengas suficiente tiempo para enfocar la página, que es un requisito de la API de Async Clipboard). Como ves, la entrada es exactamente igual que la salida.
setTimeout(async () => {
const input = 'Hello';
await navigator.clipboard.writeText(input);
const output = await navigator.clipboard.readText();
console.log(input, output, input === output);
// Logs "Hello Hello true".
}, 3000);
Con las imágenes, es un poco diferente. Para evitar los llamados ataques de bomba de compresión, los navegadores vuelven a codificar imágenes como las PNG, pero las imágenes de entrada y salida son visualmente exactamente iguales, píxel por píxel.
setTimeout(async () => {
const dataURL =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=';
const input = await fetch(dataURL).then((response) => response.blob());
await navigator.clipboard.write([
new ClipboardItem({
[input.type]: input,
}),
]);
const [clipboardItem] = await navigator.clipboard.read();
const output = await clipboardItem.getType(input.type);
console.log(input.size, output.size, input.type === output.type);
// Logs "68 161 true".
}, 3000);
Sin embargo, ¿qué sucede con el texto HTML? Como habrás adivinado, con HTML, la situación es diferente. Aquí, el navegador limpia el código HTML para evitar que sucedan eventos no deseados, por ejemplo, quitando las etiquetas <script>
del código HTML (y otras como <meta>
, <head>
y <style>
) y aplicando CSS intercalado.
Considera el siguiente ejemplo y pruébalo en la consola de DevTools. Notarás que el resultado difiere bastante de la entrada.
setTimeout(async () => {
const input = `<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="ProgId" content="Excel.Sheet" />
<meta name="Generator" content="Microsoft Excel 15" />
<style>
body {
font-family: HK Grotesk;
background-color: var(--color-bg);
}
</style>
</head>
<body>
<div>hello</div>
</body>
</html>`;
const inputBlob = new Blob([input], { type: 'text/html' });
await navigator.clipboard.write([
new ClipboardItem({
'text/html': inputBlob,
}),
]);
const [clipboardItem] = await navigator.clipboard.read();
const outputBlob = await clipboardItem.getType('text/html');
const output = await outputBlob.text();
console.log(input, output);
}, 3000);
Por lo general, la limpieza de HTML es una buena práctica. No quieres exponerte a problemas de seguridad si permites HTML no desinfectado en la mayoría de los casos. Sin embargo, existen situaciones en las que el desarrollador sabe exactamente lo que está haciendo y en las que la integridad del HTML de entrada y salida es fundamental para el funcionamiento correcto de la app. En estas circunstancias, tienes dos opciones:
- Si controlas el final de la copia y el pegado, por ejemplo, si copias desde tu app para pegarlo de la misma manera en tu app, debes usar formatos personalizados web para la API de Async Clipboard. Deja de leer aquí y consulta el artículo vinculado.
- Si solo controlas el extremo de pegado en tu app, pero no el extremo de copia, quizás porque la operación de copia se realiza en una app nativa que no admite formatos personalizados web, debes usar la opción
unsanitized
, que se explica en el resto de este artículo.
La limpieza incluye acciones como quitar etiquetas script
, intercalar estilos y garantizar que el HTML tenga el formato correcto. Esta lista no es exhaustiva y es posible que se agreguen más pasos en el futuro.
Copiar y pegar código HTML no depurado
Cuando write()
(copias) HTML en el portapapeles con la API de Async Clipboard, el navegador se asegura de que tenga el formato correcto ejecutándolo a través de un analizador de DOM y serializando la cadena HTML resultante, pero no se realiza ninguna limpieza en este paso. No necesitas hacer nada. Cuando otra aplicación coloca read()
HTML en el portapapeles y tu app web habilita la obtención del contenido de alta fidelidad y necesita realizar una limpieza en tu propio código, puedes pasar un objeto de opciones al método read()
con una propiedad unsanitized
y un valor de ['text/html']
. De forma aislada, se ve de la siguiente manera: navigator.clipboard.read({ unsanitized: ['text/html'] })
. La siguiente muestra de código es casi la misma que la que se mostró anteriormente, pero esta vez con la opción unsanitized
. Cuando lo pruebes en la consola de DevTools, verás que la entrada y la salida son las mismas.
setTimeout(async () => {
const input = `<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="ProgId" content="Excel.Sheet" />
<meta name="Generator" content="Microsoft Excel 15" />
<style>
body {
font-family: HK Grotesk;
background-color: var(--color-bg);
}
</style>
</head>
<body>
<div>hello</div>
</body>
</html>`;
const inputBlob = new Blob([input], { type: 'text/html' });
await navigator.clipboard.write([
new ClipboardItem({
'text/html': inputBlob,
}),
]);
const [clipboardItem] = await navigator.clipboard.read({
unsanitized: ['text/html'],
});
const outputBlob = await clipboardItem.getType('text/html');
const output = await outputBlob.text();
console.log(input, output);
}, 3000);
Compatibilidad con navegadores y detección de funciones
No hay una forma directa de verificar si la función es compatible, por lo que la detección de funciones se basa en observar el comportamiento. Por lo tanto, el siguiente ejemplo se basa en la detección de si sobrevive una etiqueta <style>
, lo que indica compatibilidad, o si se intercala, lo que indica que no es compatible. Ten en cuenta que, para que esto funcione, la página ya debe haber obtenido el permiso del portapapeles.
const supportsUnsanitized = async () => {
const input = `<style>p{color:red}</style><p>a`;
const inputBlob = new Blob([input], { type: 'text/html' });
await navigator.clipboard.write([
new ClipboardItem({
'text/html': inputBlob,
}),
]);
const [clipboardItem] = await navigator.clipboard.read({
unsanitized: ['text/html],
});
const outputBlob = await clipboardItem.getType('text/html');
const output = await outputBlob.text();
return /<style>/.test(output);
};
Demostración
Para ver la opción unsanitized
en acción, consulta la demostración en Glitch y revisa su código fuente.
Conclusiones
Como se describe en la introducción, la mayoría de los desarrolladores nunca necesitarán preocuparse por la limpieza de la plataforma de intercambio y pueden trabajar con las opciones de limpieza predeterminadas que realiza el navegador. En los casos excepcionales en los que los desarrolladores deben prestar atención, existe la opción unsanitized
.
Vínculos útiles
Agradecimientos
Anupam Snigdha y Rachel Andrew revisaron este artículo. El equipo de Microsoft Edge especificó y implementó la API.