Copiar en el portapapeles con JavaScript y ClipboardJs

Programar botones para copiar contenido en el portapapeles con JavaScript y ClipboardJs puede mejorar mucho la experiencia de tus usuarios. Sigue este tutorial para implementarlo en tu próxima aplicación web.

https://clipboardjs.com/ A modern approach to copy text to clipboard.”

He creado un ejercicio práctico al final de esta publicación que hace uso de la librería. Haz click en la siguiente imagen para ver el ejercicio finalizado en una ventana nueva:

Ejercicio con Clipboard
Haz click sobre la imagen para ver el ejercicio acabado de éste tutorial

¿Quieres saltarte la teoría? Ve directamente al tutorial de este ejercicio

Sin duda, uno de los comandos más conocidos por cualquier programador JavaScript es el instalador de dependencias NPM.

npm install …

Copiar y pegar el código del comando, para ejecutarlo en la consola CLI, se ha convertido en una acción casi diaria para todo desarrollador. Ignoro la cantidad de veces que lo habré hecho, pero seguro que han sido miles

Por eso, añadir un simple botón de copiado automático, me parece una pequeña (gran) mejora en la experiencia de usuario

Botón de copiado automático en el portapapeles en npmjs
Es posible que NPMJS.org use esta misma librería

El objetivo de una buena UX es, en gran parte, simplificar y facilitar la interacción del usuario con la interfaz. Por eso, aunque a priori pueda parecer una simpleza, ahorrarle la tarea de seleccionar y copiar el texto manualmente, contribuye a lograr dicho objetivo.

De modo que hoy veremos cómo copiar texto en el portapapeles con JavaScript y la librería Clipboard.js.

Copiar y pegar en el portapapeles con JavasScript y Clipboard.js
Copiar y pegar en el portapapeles con JavasScript y Clipboard.js

Efectivamente, existe una librería JavaScript dedicada a copiar contenido de texto directamente en el portapapeles.

Clipboard.js fué creada por Zeno Rocha. A pesar de que ésta micro librería ocupa solo 3kb en su forma comprimida, tiene más de 32k estrellas en su repositorio de Github. Y una media de 2.330.085 descargas semanales.

Antes de entrar en detalle a ver cómo implementar su API, quiero detenerme brevemente en destacar una parte de su descripción. 

Modern copy to clipboard. No Flash. Just 3kb gzipped.

El significado de estas dos palabras concentra décadas de evolución web

Así es, hace unos años, implementar funciones en el navegador, como el copiado automático, sólo era posible mediante addons Flash.

Y, a pesar de que, Adobe Flash fué una tecnología revolucionaria en su momento, hoy en día se considera obsoleta, y potencialmente peligrosa.

Pero basta de historia, vamos a ver cómo copiar contenido en el portapapeles con JavaScript y ClipboardJs en el 2022.

La API JavaScript de ClipboardJs para copiar en el portapapeles

Como es habitual, y validando lo dicho al inicio, hay que empezar instalando la librería. Para ello ejecutaremos el siguiente comando:

npm install clipboard --save

Clipboard.js es extremadamente sencilla de utilizar, basta con importar la clase ClipboardJS, e instanciar un objeto a partir de ésta.

A pesar de que el constructor de la clase, admite dos argumentos, de momento nos centraremos únicamente en el primero. El segundo es opcional, y veremos para qué sirve más adelante. 

Al instanciar la clase, pasamos un texto que actúa de selector CSS. La librería lo utilizará para encontrar un elemento en el DOM, y agregarle la funcionalidad de copiar en el portapapeles, al hacer click.

const copyBtn = new ClipboardJS(‘.copy-btn’);

En ese elemento HTML es necesario declarar una serie de parámetros, para el correcto funcionamiento. A continuación, veremos qué parámetros existen, y cómo implementarlos.

A través del atributo “data-clipboard-target” indicamos a la herramienta, de dónde debe obtener el texto a copiar. Por consiguiente, el valor de este atributo debe ser otro selector CSS.

<button class="copy-btn" data-clipboard-target="#element-container-text">Copiar</button>

Clipboard, obtendrá el contenido de un elemento destino, y lo guardará en el portapapeles.

Adicionalmente, mencionar que si el elemento destino es una etiqueta “input” o “textarea”, se puede establecer que la acción sea de tipo “cut”.

Me explico, por defecto, siempre se copiará el contenido. Sin embargo, si se incluye el atributo data-clipboard-action=”cut”, la librería cortará el contenido del input, y lo guardará en el portapapeles.

<!-- Target -->
<textarea id="copy-btn">Lorem ipsum...</textarea>
<!-- Trigger -->
<button class="copy-btn" data-clipboard-action="cut" data-clipboard-target="#bar">
  Cortar al portapapeles
</button>

Por otra parte, también existe la posibilidad de copiar un contenido incluído en el mismo botón. Para ello, solo hace falta añadir el atributo data-clipboard-text, y guardar en él, la información que se desea copiar.

<button class="copy-btn" data-clipboard-text=”Contenido de texto no visible que se va a guardar">
  Copiar al portapapeles
</button>

Sabiendo esto, es hora de hacer un vistazo al segundo parámetro de la clase. Se trata de un objeto JSON opcional, y sirve para definir los comportamientos descritos, sin necesidad de alterar el DOM con atributos data.

new ClipboardJS('.btn', {
    target: function(trigger) {
        return trigger.nextElementSibling;
    }
});
new ClipboardJS('.btn', {
    text: function(trigger) {
        return trigger.getAttribute('aria-label');
    }
});
new ClipboardJS('.btn', {
    container: document.getElementById('modal')
});

La instancia devuelta por la clase ClipboardJS, dispone del método “.on()”. Como es habitual, grácias a éste método, se pueden escuchar eventos, y vincular funciones de respuesta.

Los dos eventos que la API expone són “success” y “error”. Tal y como sus nombres indican, se ejecutan si se completa o falla la acción de copiar.

const copyAction = new ClipboardJS('.copy-btn');
copyAction.on('success', function(e) {
    console.info('Action:', e.action);
    console.info('Text:', e.text);
    console.info('Trigger:', e.trigger);
    e.clearSelection();
});
copyAction.on('error', function(e) {
    console.error('Action:', e.action);
    console.error('Trigger:', e.trigger);
});

“Destroy” es otro método que el objeto ofrece. A través de éste, se destruye la instancia y se libera memoria. Especialmente útil para páginas o aplicaciones tipo SPA (single page application).

Con la teoría aprendida, vamos a crear un ejercicio que ponga en práctica esta nueva herramienta.

¡Vamos a poner en práctica lo aprendido!

Imagina el siguiente escenario. Estás trabajando en una nueva aplicación de mensajería instantánea llamada iCopyU!. Y quieres añadir un botón en cada mensaje, que copie en el portapapeles el contenido.

Para ello, vamos a general la estructura HTML base. Una etiqueta div con la clase «chat-container” engloba el resto de elementos.

<div class="chat-container"></div>

Dentro de este contenedor principal, incluiremos 3 tipos distintos de bloques. En primer lugar, añadiremos un contenedor con el logo de la app.

<div class="container-logo">
     <img src="logo_copy_u.png" alt="" class="logo-app" />
</div>

El segundo bloque representa un mensaje enviado o recibido. Éste código se compone de un conjunto de “divs” y “spans” que contienen la información del emisor, el mensaje y la hora. Adicionalmente, también añadimos un botón con la clase “copy-btn”.

En este botón es importante incluir el atributo data-clipboard-target apuntando al contenedor del mensaje. Asegúrate también de que el selector del botón es único

<div class="conversation-bubble l">
  <div class="wrapper-bubble">
    <span class="name">Mamá</span>
    <span class="content" id="john-content-1651743889218">Hola Anne, tengo
      tuppers recién hechos listos para entregar</span>
    <span class="time">11:45</span>
    <button
      class="copy-btn"
      data-clipboard-target="#john-content-1651743889218"
    >
      <span class="tooltip">Copied!</span>
    </button>
  </div>
</div>

Puedes incluir y alternar tantos de estos bloques como quieras, pero ten en cuenta lo mencionado. Si deseas alternar la posición a derecha e izquierda, solo tienes que modificar la clase “l” por “r” (left / right).

Por último, incluimos un contenedor con un textarea dentro.

<div class="chat-box__container">
  <textarea
    name=""
    id=""
    cols="30"
    rows="10"
    placeholder="Escribe o copia algun mensaje anterior"
  ></textarea>
</div>

Con esto tenemos resuelta la parte HTML. Pasamos a definir estilos con SCSS.

La primera instrucción sirve para añadir una fuente de Google Fonts.

@import url("https://fonts.googleapis.com/css2?family=Work+Sans:wght@300;500&display=swap");

Seguidamente declaramos variables y estilos globales. Por supuesto, puedes editarlos para personalizar tu aplicación. Entre los recursos, te he dejado algunos fondos adicionales para que elijas el que mejor se adapte a tu diseño.

* {
  outline: none;
}
:root {
  --main-bubble-color: darkslateblue;
  --second-bubble-color: rgb(107, 97, 170);
  --white-bubble-color: rgb(234, 230, 255);
  --bubble-radius: 20px;
}
body {
  margin: 0;
  padding: 0;
  font-family: "Work Sans", sans-serif;
  background-image: url("../resources/bgs/memphis-mini-dark.png");
}

Ten en cuenta que el resto de clases se anidan en el selector .chat-container. Sin embargo, aquí lo iremos viendo com bloques independientes. Te recomiendo que sigas estas instrucciones con el código fuente del repositorio al lado.

.chat-container {
  padding: 10px 10px 120px 10px;
  max-width: 600px;
  margin: 0 auto;
  position: relative;
}

El contenedor del logo responderá al siguiente conjunto de estilos

.container-logo {
  background-color: rgba(71, 61, 139, 0.757);
  margin-bottom: 10px;
  padding: 20px 10px;
  border-radius: 10px;
  .logo-app {
    display: block;
    max-width: 50%;
    filter: drop-shadow(0px 0px 1px rgba(255, 255, 255, 0.879));
    margin: 0 auto;
  }
}

El grupo de estilos de los mensajes es el más amplio. Te lo dejo a continuación, para que puedas analizarlo detenidamente.

.conversation-bubble {
  margin-bottom: 10px;
  margin-left: 10px;
  .wrapper-bubble {
    display: inline-block;
    background-color: rgb(107, 97, 170);
    padding: 8px 15px;
    border-top-left-radius: var(--bubble-radius);
    border-bottom-left-radius: 0px;
    border-top-right-radius: var(--bubble-radius);
    border-bottom-right-radius: var(--bubble-radius);
    color: #f8f7ff;
    position: relative;
    font-weight: 300;
    box-shadow: 0px 4px 4px 0px rgb(0, 0, 0);
    .name {
      display: block;
      text-align: left;
      font-weight: 500;
      font-size: 0.8rem;
      padding-bottom: 2px;
    }
    .time {
      display: inline-block;
      margin-left: 5px;
      font-size: 0.7rem;
      color: #c8c3e2;
    }
    .copy-btn {
      display: block;
      height: 20px;
      width: 20px;
      background-image: url("../resources/icons/copy.svg");
      border: none;
      background-color: transparent;
      background-size: 80%;
      background-repeat: no-repeat;
      background-position: center center;
      position: relative;
      background-color: var(--second-bubble-color);
      border-radius: 3px;
      margin-top: 5px;
      cursor: pointer;
      .tooltip {
        background-color: #fff;
        position: absolute;
        top: -25px;
        left: -30px;
        padding: 3px 5px;
        margin-left: 10px;
        font-size: 0.8rem;
        border-radius: 3px;
        color: rgb(78, 78, 78);
        border: solid 1px rgb(209, 209, 209);
        opacity: 0;
        transition: opacity 1.8s ease;
        &:after {
          content: "";
          position: absolute;
          bottom: -3px;
          left: 50%;
          margin-left: -3px;
          width: 6px;
          height: 6px;
          background-color: #fff;
          transform: rotate(45deg);
        }
      }
      &:active {
        background-color: darkslateblue;
        .tooltip {
          transition: opacity 0s linear;
          opacity: 1;
        }
      }
    }
  }
  &.r {
    text-align: right;
    margin-right: 10px;
    .wrapper-bubble {
      background-color: var(--main-bubble-color);
      border-bottom-left-radius: var(--bubble-radius);
      border-bottom-right-radius: 0px;
      .time {
        background-color: var(--main-bubble-color);
      }
      .copy-btn {
        background-color: var(--main-bubble-color);
        &:active {
          background-color: rgb(45, 39, 84);
        }
      }
    }
  }
}

Por último, pero no por eso menos importante, las clases que dan estilo a la caja de mensajes.

.chat-box__container {
  position: fixed;
  bottom: 20px;
  left: 0;
  width: 100%;
  textarea {
    display: block;
    border-radius: 10px;
    width: 95%;
    max-width: 600px;
    margin: 0 auto;
    resize: none;
    padding: 20px;
    height: 100px;
    font-size: 1.2rem;
    color: var(--main-bubble-color);
    background-color: #ffffffec;
    font-family: "Work Sans", sans-serif;
  }
}

Con esto tendrás un diseño convincente. Ahora es momento de cerrar el ejercicio programando el comportamiento con JavaScript.

Compensando la complejidad de los estilos, verás que ésta parte es increíblemente fácil.

Importamos la librería y los estilos.

import ClipboardJS from "clipboard";
import "./SCSS/index.scss";

Guardamos una referencia al DOM del textarea, e instanciamos ClipboardJS pasando el selector “.copy-btn”.

const textblock = document.querySelector(".chat-box__container textarea");
const clipboard = new ClipboardJS(".copy-btn");

Finalmente, escuchamos el evento “success” y en el “callback” actualizamos el valor del campo de texto con el contenido copiado en el portapapeles.

clipboard.on("success", (copiedContent) => {
  textblock.value = copiedContent.text;
});

¿Te has quedado con ganas de convertir esta aplicación en una app real de mensajería instantánea? En el artículo que te dejo a continuación aprenderás los pasos necesario para hacerlo.

Mensajería instantánea con JavaScript y Socket.io

Pequeñas mejoras, grandes resultados

No es la primera vez que manifiesto mi predilección por esas librerías que son capaces de resolver necesidades muy específicas, con muy poco peso. En su momento ya lo dije de la librería TinyColor o de CountUp.

Si no las conoces y quieres ampliar tu catálogo de herramientas te dejo los enlaces a continuación.

Controlar colores con JavaScript y TinyColor

Efecto contador con JavaScript y CountUp

Eso ha sido todo por hoy, te dejo algunos recursos adicionales en este listado, por si te has quedado con ganas de más.

Nos vemos pronto, un abrazo desarrolladores.

2 comentarios en «Copiar en el portapapeles con JavaScript y ClipboardJs»

  1. Perfecto amigo, muchas gracias por el contenido ahora, que hay para un simple mortal que solo desea copiar el contenido de un textarea y «eso es todo, sin css ni esas cosas, se entiende»?

    a proposito, el demo que pones esta genial!

    Responder
    • Hola Ernesto, gracias por escribir, a ver si puedo resolver tu duda. Toda la parte de CSS es completamente opcional, si lo que deseas es agregar un botón que copie el contenido de un textarea, la forma mas sencilla es añadir el atributo data-clipboard-target=»#foo» al botón en cuestión. Por otra parte, asegúrate de que el textarea tenga el id referenciado (en este caso id=»foo»). Con eso debería ser suficiente. Saludos!

      Responder

Deja un comentario