El mes pasado os hablaba aquí de una manera sencilla y soportada por todos los navegadores para acceder al portapapeles desde código JavaScript en el navegador. Es un método simple pero un poco engorroso de implementar, puesto que hay que seleccionar rangos en la página para poder copiar su contenido. Además, su implementación varía ligeramente de un navegador a otro porque no está estandarizada. 

Otra pega importante que tiene el método "clásico" es que es síncrono. Es decir, mientras se efectúa la operación de copiado o pegado se bloquea el hilo principal de ejecución del navegador. En condiciones normales no importa demasiado ya que es una operación muy rápida, pero si la operación involucrase un contenido muy extenso podría llegar a bloquearse la interfaz de la página, con lo que ello conlleva en cuanto a la usabilidad y experiencia de usuario.

Para solucionar todos estos problemas la W3C ha creado la Async Clipboard API, que e el momento de escribir esto se encuentra todavía como borrador, pero es muy estable. Esta API unifica el modelo de permisos para que todos los navegadores lo implemente igual y además es asíncrona, lo cual hace que no se bloquee la página al utilizarla. Para la parte asíncrona podemos utilizar tanto promesas como la API async/await de ECMAScript 2017.

En el momento de escribir esto, esta API solo está soportada por Google Chrome, pero es de suponer que en breve los demás navegadores la soporten también. Como veremos, es fácil añadir una comprobación y usar el método más antiguo en caso de no estar soportada, de modo que así nuestra aplicación esté preparada para el futuro y soporte también el presente y el pasado.

Vamos a ver cómo funciona.

El objeto clipboard

Este objeto es una nueva propiedad del objeto navigator y podemos, por tanto, acceder a ella simplemente escribiendo:

if (!navigator.clipboard) {
alert('¡Tu navegador no soporta la API Asíncrona del Portapapeles!')
}

De esta manera, por ejemplo, comprobamos si la API está soportada o no por el navegador actual.

Este objeto posee dos métodos para leer y otros dos para escribir en el portapapeles. Veamos cómo se utilizan.

Escribir al portapapeles con la API asíncrona

La manera más sencilla de escribir texto plano al portapapeles es utilizando el método writeText del objeto anterior. Su uso es muy sencillo puesto que lo único que tenemos que hacer es llamarlo y gestionar el resultado de la llamada de manera asíncrona.

Para ello tenemos dos formas básicas de conseguirlo. La primera es el uso de promesas. Explicar las promesas no es el objeto de este artículo, así que te refiero a la MDN (o a nuestro fabuloso curso de JavaScript y ECMAScript avanzado de campusMVP) si necesitas aprender sobre ellas.

Con promesas la escritura consiste en hacer algo como esto:

navigator.clipboard.writeText(contenidoACopiar).then(function() {
exito();
mostrarAlerta();
});

Es decir, llamas a la función y con el método then de la promesa resultante gestionas lo que quieres hacer cuando haya funcionado (en este caso mostrar un mensaje de alerta).

También podrías gestionar un posible fallo con catch(), claro.

Si prefieres utilizar la asincronía, algo que todos los navegadores que soportan esta API de portapapeles deberían soportar también, entonces es todavía más sencillo y directo:

await navigator.clipboard.writeText(contenidoACopiar)
exito();
mostrarAlerta();

Evidentemente la función que contenga a este fragmento de código debería estar marcada con async para poder funcionar. De esta manera se ve más claro todo y la ejecución se detiene hasta que vuelve la llamada a writeText. Para controlar posibles errores usaríamos una gestión estructurada normal y corriente, con try-catch.

Bien, con este método conseguimos de manera muy sencilla copiar texto al portapapeles.

No es necesario permiso especial alguno ya que, aunque existe uno específico según el estándar, Chrome no lo solicita y siempre te da permiso para copiar al portapapeles en la pestaña activa. Ya veremos que para leer desde el portapapeles la cosa cambia.

Además del método writeText que acabamos de ver existe otro más genérico llamado write que permite escribir cualquier cosa de manera genérica al portapapeles (por ejemplo las versiones de texto y HTML del mismo contenido), para lo cual utiliza el mismo objeto DataTransfer que la API de arrastrar y soltar.

No entraré en mucho detalle sobre su uso, pero este ejemplo sirve para ver que tampoco es muy complicado:

var data = new DataTransfer();
data.items.add("Hola <b>amiguetes</b>", "text/html");
data.items.add("Hola amiguetes", "text/plain");
await navigator.clipboard.write(data);

Se trata de crear el objeto de transferencia de datos, rellenarlo con los formatos y llamar al método. Es bastante directo, sobre todo en su versión con async.

Leer desde el portapapeles

El proceso de lectura desde el portapapeles (que sería equivalente a "pegar" desde él), es idéntico al anterior, solo que se utilizan los métodos read() y readText() para leer todos los formatos que haya o solo el posible texto. Ambos métodos funcionan de la misma manera, solo que no toman parámetro alguno y reciben como resultado de la llamada respectivamente el objeto DataTransfer o el texto con lo que haya en el portapapeles.

Por ejemplo, con una promesa haríamos:

navigator.clipboard.readText().then(function(contenido) {
zonaDondePegar.innerText = contenido;
}).catch(function(ex) {
excepcion();
mostrarAlerta();
});

Fíjate como en este caso recibimos el contenido del portapapeles como parámetro de la función de callback para el método then de la promesa. También capturamos los posibles errores con el método catch, ya que es muy fácil que se produzca uno cuando no tenemos permiso (más sobre esto enseguida).

La versión con async sería más sencilla:

try {
var contenido = await navigator.clipboard.readText();
zonaDondePegar.innerText = contenido;
}
catch(ex) {
excepcion();
mostrarAlerta();
}

ya que se gestiona como código lineal normal.

La parte más interesante del "pegado" es que, ahora sí, necesitaremos que el usuario de la páginas nos conceda permisos, o en caso contrario podríamos robarle la información del portapapeles sin que lo supiese, con las terribles implicaciones de privacidad y seguridad que ello tendría.

Por eso, cuando intentamos usar el código anterior nos saldría un mensaje como este:

En este caso como la estaba usando desde disco directamente por eso sale ese URL tan rato (file:///), pero en condiciones normales saldría el dominio actual.

Por cierto, aunque te funcionará desde el sistema de archivos y desde localhost, si la colocas en un servidor en Internet, al igual que la mayoría de las APIs de HTML5, solo te funcionará si estás bajo HTTPS. Tenlo muy en cuenta si aún eres de los que piensa que "mi dominio no necesita HTTPS porque no manejo datos privados ni claves".

Permisos

Cuando aceptamos y se ejecuta la lectura de datos, veremos en la barra del navegador un iconito de una carpeta que indicará que hemos concedido permisos de acceso al portapapeles:

en caso de bloquear el acceso aparecerá uno similar, pero tachado:

Si el usuario lo pulsa siempre puede cambiar el permiso que haya otorgado previamente:

De este modo tenemos un modelo de permisos coherente con el de las demás APIs del navegador, y no algo que cada cual implementa como le parece. Además le cedemos control al usuario para que decida en cada momento si quiere otorgar o no los permisos correspondientes.

Existe una API del navegador (accesible a través del objeto permissions) que nos permite comprobar los diferentes permisos de un usuario antes de, por ejemplo, realizar una acción concreta, como la de lectura de información desde el portapapeles. En el caso concreto del portapapeles los permisos que se pueden comprobar son dos:

  • clipboard-read
  • clipboard-write

El importante ahora mismo (mientras no cambie la cosa) es el primero, que es el que permite leer desde el portapapeles.

Podemos comprobarlo con un código similar a este:

if (navigator.permissions) {
var estadoAct = await navigator.permissions.query({
name: 'clipboard-read'
})

switch(estadoAct.state) {
case "prompt":
alert("Permisos sin establecer todavía")
break;
case "denied":
alert("Permiso denegado")
break;
case "granted":
alert("Permiso concedido")
break;
default:
alert("Estado desconocido: " + estadoAct.state)
}
}

Lo que se hace es leer el estado actual del permiso clipboard-read. En el objeto recibido consultamos la propiedad state que devuelve una cadena con tres posibles valores para el permiso en cuestión:

  • prompt: que quiere decir que no se le ha pedido permiso todavía al usuario, o sea, que está sin definir explícitamente.
  • denied: que se le ha preguntado al usuario y este lo ha denegado explícitamente.
  • granted: que se le ha preguntado al usuario y este lo ha concedido explícitamente.

Así podremos saber si tenemos ya o no un permiso y pedirlo si es necesario, aunque como es el propio navegador quien lo hará la primera vez que lo intentemos no será necesario la mayor parte de las veces. Eso sí, si ya sabemos de antemano que está denegado podemos deshabilitar ya los botones que tengamos para permitir leer desde el portapapeles.

En resumen

Ya hemos visto como será la futura API para manejar los contenidos del portapapeles. Es estándar, asíncrona y con un modelo de permisos coherente, por lo que será la forma preferida de implementar estas funcionalidades, frente a la manera clásica.

Te he dejado un ejemplo completo (ZIP, 2.24KB), equivalente al del post anterior, para que puedas descargarlo y jugar con él:

Fíjate en el código: tienes comentadas las versiones con async para que puedas probarlas. Comenta las versiones con Promise y descomenta las otras para probarlas. Y si lo pones online tiene que ser con HTTPS.

De momento esta API solo la implementa Chrome pero en los próximos meses espero que ya la implementen los demás. Y mientras tanto podemos hacer que coexistan fácilmente las dos.

¡Espero que te resulte útil!

Escrito por un humano, no por una IA