Desbloqueando el acceso al portapapeles

Acceso más seguro 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.

Está bien para fragmentos de texto pequeños, pero hay muchos casos en los que bloquear el para la transferencia del portapapeles es una mala experiencia. 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 en 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 detenidamente el uso del navegador descripción general de la compatibilidad para cada una de las siguientes secciones.

Copiar: escribir 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);
  }
}

Navegadores compatibles

  • 66
  • 79
  • 63
  • 13.1

Origen

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 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() .

A continuación, pasa un array de objetos ClipboardItem como parámetro a write(). . Actualmente, solo puede pasar una imagen a la vez, pero esperamos agregar la compatibilidad con 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. Para BLOB objetos obtenidos de fetch() o canvas.toBlob(), la propiedad blob.type contenga automáticamente el tipo de MIME correcto de 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);
}

Navegadores compatibles

  • 66
  • 79
  • 127
  • 13.1

Origen

El evento de copia

Cuando un usuario inicia una copia del portapapeles y no llama 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 a favor de tu propia implementación. En este caso, clipboardData estará vacío. Piensa en una página con texto e imagen, y cuando el usuario selecciona todo y inicia una copia en el portapapeles, tu solución personalizada debe descartar el texto y solo copiar la imagen. Puedes lograrlo tal como se muestra en la siguiente muestra de código. Lo que no se aborda en este ejemplo es cómo recurrir APIs 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:

Navegadores compatibles

  • 1
  • 12
  • 22
  • 3

Origen

Para ClipboardItem:

Navegadores compatibles

  • 76
  • 79
  • 127
  • 13.1

Origen

Pegar: lectura de datos del portapapeles

readText()

Para leer texto del portapapeles, llama a navigator.clipboard.readText() y espera para que 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);
  }
}

Navegadores compatibles

  • 66
  • 79
  • 125
  • 13.1

Origen

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);
  }
}

Navegadores compatibles

  • 66
  • 79
  • 127
  • 13.1

Origen

Cómo trabajar con archivos pegados

Resulta útil que los usuarios puedan utilizar las combinaciones de teclas del portapapeles, como Ctrl + C y Ctrl + v. Chromium expone los archivos de solo lectura en el portapapeles, como se describe a continuación. Este método se activa cuando el usuario selecciona el acceso directo de pegado predeterminado del sistema operativo. o cuando el usuario hace clic en Editar y, luego, en Pegar en la barra de menú del navegador. No se necesita más código de mantenimiento.

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());
});

Navegadores compatibles

  • 3
  • 12
  • 3.6
  • 4

Origen

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 olvidas llamar a preventDefault().

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  const text = await navigator.clipboard.readText();
  console.log('Pasted text: ', text);
});

Navegadores compatibles

  • 1
  • 12
  • 22
  • 3

Origen

Cómo administrar varios tipos de MIME

La mayoría de las implementaciones colocan varios formatos de datos en el portapapeles para realizar un solo corte. o una operación de 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 en silencio todo tipo de contenido en el portapapeles de un usuario, lo que produciría resultados catastróficos cuando se pegara. Imagina una página web que copia rm -rf / o una imagen de la bomba de descompresión en el portapapeles.

Mensaje del navegador que le solicita al usuario el permiso del portapapeles.
La solicitud de permiso para la API del portapapeles

Otorgar a las páginas web acceso de lectura ilimitado 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 está la pestaña activa. Las páginas de las 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 la pestaña activa. Se debe solicitar el permiso clipboard-read, que puedes al intentar leer los 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 el corte o pegando 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.

Como los navegadores solo permiten el acceso al portapapeles cuando una página es la pestaña activa, descubrirás que algunos de los ejemplos 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: aplazar acceso al portapapeles con setTimeout() y, luego, haz clic rápidamente en la página para enfocarlo antes de que se llame 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 funciones

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, aquí te mostramos cómo puedes 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. De Glitch pueden mezclar la demostración de texto o la demostración de imagen para experimentar con ellos.

En el primer ejemplo, se muestra el movimiento de 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.

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: