Acceso más seguro y sin bloqueos al portapapeles para imágenes y texto
La forma tradicional de acceder al portapapeles del sistema era a través de document.execCommand()
para las interacciones del portapapeles. Aunque es ampliamente compatible, este método de cortar y
el pegado tenía un costo: el acceso al portapapeles era síncrono y solo podía leer
y escribir en el DOM.
Eso está bien para fragmentos pequeños de texto, pero hay muchos casos en los que bloquear la página para la transferencia al portapapeles es una experiencia deficiente. Las limpiezas que consumen mucho tiempo o
es posible que se necesite la decodificación de imágenes para pegar el contenido de forma segura. El navegador
tal vez necesiten cargar o intercalar recursos vinculados desde un documento pegado. Eso
bloquear la página mientras esperas en el disco o la red. Imagina que agregas permisos a la mezcla, lo que requiere que el navegador bloquee la página mientras solicita acceso al portapapeles. Al mismo tiempo, se implementan los permisos
Las document.execCommand()
de la interacción del portapapeles están definidas de forma flexible y varían
entre navegadores.
El API de Async Clipboard soluciona estos problemas y proporciona un modelo de permisos bien definido que no bloquear la página. La API de Async Clipboard se limita a controlar imágenes y texto. en la mayoría de los navegadores, pero la compatibilidad varía. Asegúrate de estudiar cuidadosamente la descripción general de la compatibilidad con navegadores de cada una de las siguientes secciones.
Copiar: Escribe datos en el portapapeles.
writeText()
Para copiar texto en el portapapeles, llama a writeText()
. Dado que esta API es
asíncrona, la función writeText()
muestra una promesa que resuelve o
se rechaza en función de si el texto que se pasó se copia correctamente:
async function copyPageUrl() {
try {
await navigator.clipboard.writeText(location.href);
console.log('Page URL copied to clipboard');
} catch (err) {
console.error('Failed to copy: ', err);
}
}
write()
En realidad, writeText()
es solo un método útil para el elemento write()
genérico.
que también te permite copiar imágenes en el portapapeles. Al igual que writeText()
,
es asíncrona y muestra una promesa.
Para escribir una imagen en el portapapeles, necesitas la imagen como un blob
. Una forma de hacerlo
Para ello, solicita la imagen a un servidor mediante fetch()
y, luego, llama
blob()
en la
respuesta.
Solicitar una imagen al servidor puede no ser conveniente o posible para una
por diversos motivos. Por suerte, también puedes dibujar la imagen en un lienzo y
Llamar al lienzo
toBlob()
.
Luego, pasa un array de objetos ClipboardItem
como parámetro al método write()
. Por el momento, solo puedes pasar una imagen a la vez, pero esperamos admitir varias imágenes en el futuro. ClipboardItem
toma un objeto con
el tipo de MIME de la imagen como clave y el BLOB como valor. En el caso de los objetos BLOB obtenidos de fetch()
o canvas.toBlob()
, la propiedad blob.type
contiene automáticamente el tipo MIME correcto para una imagen.
try {
const imgURL = '/images/generic/file.png';
const data = await fetch(imgURL);
const blob = await data.blob();
await navigator.clipboard.write([
new ClipboardItem({
// The key is determined dynamically based on the blob's type.
[blob.type]: blob
})
]);
console.log('Image copied.');
} catch (err) {
console.error(err.name, err.message);
}
Como alternativa, puedes escribir una promesa en el objeto ClipboardItem
.
Para este patrón, necesitas conocer el tipo de MIME de los datos de antemano.
try {
const imgURL = '/images/generic/file.png';
await navigator.clipboard.write([
new ClipboardItem({
// Set the key beforehand and write a promise as the value.
'image/png': fetch(imgURL).then(response => response.blob()),
})
]);
console.log('Image copied.');
} catch (err) {
console.error(err.name, err.message);
}
El evento de copia
En el caso de que un usuario inicie una copia del portapapeles y no llame a preventDefault()
, el evento copy
incluye una propiedad clipboardData
con los elementos ya en el formato correcto.
Si deseas implementar tu propia lógica, debes llamar a preventDefault()
para evitar el comportamiento predeterminado en favor de tu propia implementación.
En este caso, clipboardData
estará vacío.
Considera una página con texto y una imagen. Cuando el usuario seleccione todo e inicie una copia en el portapapeles, tu solución personalizada debe descartar el texto y solo copiar la imagen. Puedes lograrlo como se muestra en la siguiente muestra de código.
En este ejemplo, no se explica cómo recurrir a APIs anteriores cuando la API de Clipboard no es compatible.
<!-- The image we want on the clipboard. -->
<img src="kitten.webp" alt="Cute kitten.">
<!-- Some text we're not interested in. -->
<p>Lorem ipsum</p>
document.addEventListener("copy", async (e) => {
// Prevent the default behavior.
e.preventDefault();
try {
// Prepare an array for the clipboard items.
let clipboardItems = [];
// Assume `blob` is the blob representation of `kitten.webp`.
clipboardItems.push(
new ClipboardItem({
[blob.type]: blob,
})
);
await navigator.clipboard.write(clipboardItems);
console.log("Image copied, text ignored.");
} catch (err) {
console.error(err.name, err.message);
}
});
Para el evento copy
:
Para ClipboardItem
:
Pegar: lectura de datos del portapapeles
readText()
Para leer texto del portapapeles, llama a navigator.clipboard.readText()
y espera a que se resuelva la promesa que se muestra:
async function getClipboardContents() {
try {
const text = await navigator.clipboard.readText();
console.log('Pasted content: ', text);
} catch (err) {
console.error('Failed to read clipboard contents: ', err);
}
}
read()
El método navigator.clipboard.read()
también es asíncrono y muestra un
muy prometedores. Para leer una imagen desde el portapapeles, obtén una lista de
ClipboardItem
objetos, y luego iterar sobre ellos.
Cada ClipboardItem
puede contener su contenido de diferentes tipos, por lo que deberás
iterar sobre la lista de tipos, nuevamente con un bucle for...of
. Para cada tipo,
llama al método getType()
con el tipo actual como argumento para obtener el
el BLOB correspondiente. Como antes, este código no está vinculado a imágenes y se
trabajará con otros tipos de archivos futuros.
async function getClipboardContents() {
try {
const clipboardItems = await navigator.clipboard.read();
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
const blob = await clipboardItem.getType(type);
console.log(URL.createObjectURL(blob));
}
}
} catch (err) {
console.error(err.name, err.message);
}
}
Cómo trabajar con archivos pegados
Es útil que los usuarios puedan usar combinaciones de teclas del portapapeles, como Ctrl + C y Ctrl + V. Chromium expone archivos de solo lectura en el portapapeles, como se describe a continuación. Esto se activa cuando el usuario presiona el atajo de pegado predeterminado del sistema operativo o cuando hace clic en Editar y, luego, en Pegar en la barra de menú del navegador. No se necesita ningún código de plomería adicional.
document.addEventListener("paste", async e => {
e.preventDefault();
if (!e.clipboardData.files.length) {
return;
}
const file = e.clipboardData.files[0];
// Read the file's contents, assuming it's a text file.
// There is no way to write back to it.
console.log(await file.text());
});
El evento para pegar
Como mencionamos antes, se planea agregar eventos para que funcionen con la API de Portapapeles.
pero, por ahora, puedes usar el evento paste
existente. Funciona a la perfección con el nuevo
métodos asíncronos para leer el texto del portapapeles. Al igual que con el evento copy
, no olvides llamar a preventDefault()
.
document.addEventListener('paste', async (e) => {
e.preventDefault();
const text = await navigator.clipboard.readText();
console.log('Pasted text: ', text);
});
Cómo administrar varios tipos de MIME
La mayoría de las implementaciones colocan varios formatos de datos en el portapapeles para una sola operación de corte o copia. Esto se debe a dos motivos: como desarrollador de apps, las capacidades de la aplicación a la que el usuario quiere copiar texto o imágenes, y muchas aplicaciones admiten el pegado de datos estructurados como texto sin formato. Normalmente, esto es presentar a los usuarios un elemento de menú Editar con un nombre como Pegar y estilo de concordancia o Pegar sin formato.
En el siguiente ejemplo, se muestra cómo hacerlo. En este ejemplo, se usa fetch()
para obtener
datos de una imagen, pero también podría provenir de un
<canvas>
o la API de File System Access.
async function copy() {
const image = await fetch('kitten.png').then(response => response.blob());
const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'});
const item = new ClipboardItem({
'text/plain': text,
'image/png': image
});
await navigator.clipboard.write([item]);
}
Seguridad y permisos
El acceso al portapapeles siempre ha planteado un problema de seguridad para los navegadores. Sin los permisos adecuados, una página podría copiar de forma silenciosa todo tipo de contenido malicioso en el portapapeles de un usuario, lo que generaría resultados catastróficos cuando se pegue.
Imagina una página web que copia rm -rf /
o una
imagen de la bomba de descompresión
en el portapapeles.
Permitir que las páginas web accedan sin restricciones al portapapeles es aún más problemático. Los usuarios copian de forma rutinaria la información sensible como contraseñas datos personales en el portapapeles, que luego cualquier página podría leer sin el conocimiento del usuario.
Al igual que ocurre con muchas APIs nuevas, la API de Clipboard solo es compatible con páginas publicadas en HTTPS Para evitar abusos, el acceso al portapapeles solo se permite cuando una página es la pestaña activa. Las páginas en pestañas activas pueden escribir en el portapapeles sin solicitar permiso, pero leer desde el portapapeles siempre requiere permiso.
Se agregaron permisos para copiar y pegar
API de Permissions.
El permiso clipboard-write
se otorga automáticamente a las páginas cuando son la pestaña activa. Se debe solicitar el permiso clipboard-read
, lo que puedes hacer si intentas leer datos del portapapeles. En el siguiente código, se muestra la última opción:
const queryOpts = { name: 'clipboard-read', allowWithoutGesture: false };
const permissionStatus = await navigator.permissions.query(queryOpts);
// Will be 'granted', 'denied' or 'prompt':
console.log(permissionStatus.state);
// Listen for changes to the permission state
permissionStatus.onchange = () => {
console.log(permissionStatus.state);
};
También puedes controlar si se requiere un gesto del usuario para invocar cortar o pegar con la opción allowWithoutGesture
. El valor predeterminado para este valor
varía según el navegador, por lo que siempre debes incluirlo.
Aquí es donde la naturaleza asíncrona de la API de Clipboard realmente resulta útil: cuando intentan leer o escribir datos del portapapeles, automáticamente se le solicita al usuario permiso si aún no se otorgó. Dado que la API se basa en promesas, esto es completamente transparente, y un usuario que deniega el permiso del portapapeles causa la promesa de rechazar para que la página pueda responder adecuadamente.
Debido a que los navegadores solo permiten el acceso al portapapeles cuando una página es la pestaña activa, verás que algunos de los ejemplos que se incluyen aquí no se ejecutan si se pegan directamente en la consola del navegador, ya que las herramientas para desarrolladores son la pestaña activa. Hay un truco: aplaza el acceso al portapapeles con setTimeout()
y, luego, haz clic rápidamente dentro de la página para enfocarla antes de que se llamen a las funciones:
setTimeout(async () => {
const text = await navigator.clipboard.readText();
console.log(text);
}, 2000);
Integración de la política de permisos
Para usar la API en iframes, debes habilitarla con
Política de Permisos,
que define un mecanismo que permite habilitar y
inhabilitar varias funciones y APIs del navegador. Concretamente, debes pasar ya sea
o ambos de clipboard-read
o clipboard-write
, según las necesidades de tu app.
<iframe
src="index.html"
allow="clipboard-read; clipboard-write"
>
</iframe>
Detección de atributos
Para usar la API de Async Clipboard y a la vez admitir todos los navegadores, prueba lo siguiente:
navigator.clipboard
y recurrir a los métodos anteriores. Por ejemplo, a continuación, se muestra cómo
podrías implementar el pegado para incluir otros navegadores.
document.addEventListener('paste', async (e) => {
e.preventDefault();
let text;
if (navigator.clipboard) {
text = await navigator.clipboard.readText();
}
else {
text = e.clipboardData.getData('text/plain');
}
console.log('Got pasted text: ', text);
});
Esa no es toda la historia. Antes de la API de Async Clipboard, había una mezcla de
diferentes implementaciones de copiar y pegar en navegadores web. En la mayoría de los navegadores,
la acción de copiar y pegar del navegador se puede activar mediante
document.execCommand('copy')
y document.execCommand('paste')
. Si el texto
que se copiará es una cadena que no está presente en el DOM, se debe insertar
DOM y seleccionados:
button.addEventListener('click', (e) => {
const input = document.createElement('input');
input.style.display = 'none';
document.body.appendChild(input);
input.value = text;
input.focus();
input.select();
const result = document.execCommand('copy');
if (result === 'unsuccessful') {
console.error('Failed to copy text.');
}
input.remove();
});
Demostraciones
Puedes jugar con la API de Async Clipboard en las siguientes demostraciones. En Glitch, puedes hacer un remix de la demo de texto o la demo de imágenes para experimentar con ellas.
En el primer ejemplo, se muestra cómo mover texto dentro y fuera del portapapeles.
Para probar la API con imágenes, usa esta demostración. Recuerda que solo se admiten archivos PNG y solo en algunos navegadores.
Vínculos relacionados
Agradecimientos
La API del portapapeles asíncrono fue implementada por Darwin Huang y Gary Kačmarčík Darwin también proporcionó la demostración. Gracias a Kyarik y, nuevamente, a Gary Kačmarčík por revisar partes de este artículo.
Imagen hero de Markus Winkler en Retiro: