Desbloqueando o acesso à área de transferência

Acesso mais seguro e sem bloqueios à área de transferência para texto e imagens

A maneira tradicional de acessar a área de transferência do sistema era usando document.execCommand() para interações com a área de transferência. Embora amplamente aceito, esse método de cortar e colar tinha um custo: o acesso à área de transferência era síncrono e só podia ler e gravar no DOM.

Isso funciona bem para pequenos trechos de texto, mas há muitos casos em que bloquear a página para transferência da área de transferência é uma experiência ruim. Pode ser necessário fazer uma limpeza demorada ou decodificação de imagem antes de colar o conteúdo com segurança. O navegador pode precisar carregar ou inserir recursos vinculados de um documento colado. Isso bloquearia a página enquanto aguarda o disco ou a rede. Imagine adicionar permissões à mistura, exigindo que o navegador bloqueie a página ao solicitar acesso à área de transferência. Ao mesmo tempo, as permissões implementadas em torno de document.execCommand() para interação com a área de transferência são definidas de forma imprecisa e variam entre navegadores.

A API Async Clipboard resolve esses problemas, fornecendo um modelo de permissões bem definido que não bloqueia a página. A API Async Clipboard é limitada ao processamento de texto e imagens na maioria dos navegadores, mas a compatibilidade varia. Estude com atenção a visão geral da compatibilidade do navegador em cada uma das seções a seguir.

Cópia: gravação de dados na área de transferência

writeText()

Para copiar texto para a área de transferência, chame writeText(). Como essa API é assíncrona, a função writeText() retorna uma promessa que é resolvida ou rejeitada, dependendo se o texto transmitido foi copiado com êxito:

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

Browser Support

  • Chrome: 66.
  • Edge: 79.
  • Firefox: 63.
  • Safari: 13.1.

Source

write()

Na verdade, writeText() é apenas um método prático para o método genérico write(), que também permite copiar imagens para a área de transferência. Assim como writeText(), ele é assíncrono e retorna uma promessa.

Para gravar uma imagem na área de transferência, você precisa dela como um blob. Uma maneira de fazer isso é solicitar a imagem de um servidor usando fetch() e chamar blob() na resposta.

Solicitar uma imagem do servidor pode não ser desejável ou possível por vários motivos. Felizmente, também é possível desenhar a imagem em uma tela e chamar o método toBlob() da tela.

Em seguida, transmita uma matriz de objetos ClipboardItem como um parâmetro para o método write(). No momento, só é possível transmitir uma imagem por vez, mas esperamos adicionar suporte para várias imagens no futuro. ClipboardItem usa um objeto com o tipo MIME da imagem como chave e o blob como valor. Para objetos blob obtidos de fetch() ou canvas.toBlob(), a propriedade blob.type contém automaticamente o tipo MIME correto para uma imagem.

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, você pode escrever uma promessa para o objeto ClipboardItem. Para esse padrão, é necessário saber o tipo MIME dos dados com antecedência.

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

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Source

O evento de cópia

No caso em que um usuário inicia uma cópia da área de transferência e não chama preventDefault(), o copy event inclui uma propriedade clipboardData com os itens já no formato certo. Se quiser implementar sua própria lógica, chame preventDefault() para impedir o comportamento padrão em favor da sua implementação. Nesse caso, clipboardData vai estar vazio. Considere uma página com texto e uma imagem. Quando o usuário selecionar tudo e iniciar uma cópia da área de transferência, sua solução personalizada vai descartar o texto e copiar apenas a imagem. Você pode fazer isso conforme mostrado na amostra de código abaixo. Este exemplo não mostra como usar APIs anteriores quando a API Clipboard não é compatível.

<!-- 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 o evento copy:

Browser Support

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 22.
  • Safari: 3.

Source

Para ClipboardItem:

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Source

Colar: leitura de dados da área de transferência

readText()

Para ler texto da área de transferência, chame navigator.clipboard.readText() e aguarde a resolução da promessa retornada:

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

Browser Support

  • Chrome: 66.
  • Edge: 79.
  • Firefox: 125.
  • Safari: 13.1.

Source

read()

O método navigator.clipboard.read() também é assíncrono e retorna uma promessa. Para ler uma imagem da área de transferência, extraia uma lista de objetos ClipboardItem e itere sobre eles.

Cada ClipboardItem pode armazenar conteúdo em tipos diferentes. Portanto, você precisa iterar a lista de tipos, novamente usando um loop for...of. Para cada tipo, chame o método getType() com o tipo atual como argumento para receber o blob correspondente. Como antes, esse código não está vinculado a imagens e vai funcionar com outros tipos de arquivos no futuro.

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

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Source

Como trabalhar com arquivos colados

É útil que os usuários possam usar atalhos de teclado da área de transferência, como ctrl+c e ctrl+v. O Chromium expõe arquivos somente leitura na área de transferência, conforme descrito abaixo. Isso é acionado quando o usuário pressiona o atalho de colagem padrão do sistema operacional ou quando ele clica em Editar e em Colar na barra de menus do navegador. Nenhum outro código de encanamento é necessário.

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

Browser Support

  • Chrome: 3.
  • Edge: 12.
  • Firefox: 3.6.
  • Safari: 4.

Source

O evento de colagem

Como observado antes, há planos de introduzir eventos para trabalhar com a API Clipboard, mas, por enquanto, use o evento paste atual. Ele funciona bem com os novos métodos assíncronos para leitura de texto da área de transferência. Assim como no evento copy, não se esqueça de chamar preventDefault().

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

Browser Support

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 22.
  • Safari: 3.

Source

Como processar vários tipos MIME

A maioria das implementações coloca vários formatos de dados na área de transferência para uma única operação de corte ou cópia. Há dois motivos para isso: como desenvolvedor de apps, você não tem como saber os recursos do app em que um usuário quer copiar texto ou imagens, e muitos aplicativos aceitam colar dados estruturados como texto simples. Normalmente, isso é apresentado aos usuários com um item de menu Editar com um nome como Colar e corresponder ao estilo ou Colar sem formatação.

O exemplo a seguir mostra como fazer isso. Este exemplo usa fetch() para receber dados de imagem, mas eles também podem vir de um <canvas> ou da API 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]);
}

Segurança e permissões

O acesso à área de transferência sempre foi uma questão de segurança para os navegadores. Sem permissões adequadas, uma página pode copiar silenciosamente todo tipo de conteúdo malicioso para a área de transferência de um usuário, o que produziria resultados catastróficos quando colado. Imagine uma página da Web que copia silenciosamente rm -rf / ou uma imagem de bomba de descompressão para a área de transferência.

Solicitação do navegador pedindo permissão de acesso à área de transferência.
A solicitação de permissão para a API Clipboard.

Dar às páginas da Web acesso de leitura irrestrito à área de transferência é ainda mais problemático. Os usuários costumam copiar informações sensíveis, como senhas e detalhes pessoais, para a área de transferência, que pode ser lida por qualquer página sem o conhecimento do usuário.

Assim como muitas APIs novas, a API Clipboard só é compatível com páginas veiculadas por HTTPS. Para ajudar a evitar abusos, o acesso à área de transferência só é permitido quando uma página é a guia ativa. As páginas em guias ativas podem gravar na área de transferência sem pedir permissão, mas a leitura sempre exige permissão.

As permissões de copiar e colar foram adicionadas à API Permissions. A permissão clipboard-write é concedida automaticamente às páginas quando elas são a guia ativa. A permissão clipboard-read precisa ser solicitada. Para isso, tente ler dados da área de transferência. O código abaixo mostra o último caso:

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

Também é possível controlar se um gesto do usuário é necessário para invocar o corte ou colagem usando a opção allowWithoutGesture. O padrão desse valor varia de acordo com o navegador, então sempre inclua esse atributo.

É aqui que a natureza assíncrona da API Clipboard é muito útil: tentar ler ou gravar dados da área de transferência solicita automaticamente a permissão do usuário, caso ela ainda não tenha sido concedida. Como a API é baseada em promessas, isso é completamente transparente, e um usuário que nega a permissão da área de transferência faz com que a promessa seja rejeitada para que a página possa responder adequadamente.

Como os navegadores só permitem o acesso à área de transferência quando uma página é a guia ativa, alguns dos exemplos aqui não serão executados se forem colados diretamente no console do navegador, já que as próprias ferramentas para desenvolvedores são a guia ativa. Há um truque: adie o acesso à área de transferência usando setTimeout() e clique rapidamente na página para focar nela antes que as funções sejam chamadas:

setTimeout(async () => {
  const text = await navigator.clipboard.readText();
  console.log(text);
}, 2000);

Integração da política de permissões

Para usar a API em iframes, é necessário ativá-la com a Política de permissões, que define um mecanismo para ativar e desativar seletivamente vários recursos e APIs do navegador. Especificamente, você precisa transmitir clipboard-read ou clipboard-write, ou ambos, dependendo das necessidades do seu app.

<iframe
    src="index.html"
    allow="clipboard-read; clipboard-write"
>
</iframe>

Detecção de recursos

Para usar a API Async Clipboard e oferecer suporte a todos os navegadores, teste navigator.clipboard e volte para métodos anteriores. Por exemplo, veja como implementar a ação de colar para incluir outros 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);
});

Mas essa não é a história toda. Antes da API Async Clipboard, havia uma mistura de diferentes implementações de copiar e colar em navegadores da Web. Na maioria dos navegadores, a função copiar e colar do navegador pode ser acionada usando document.execCommand('copy') e document.execCommand('paste'). Se o texto a ser copiado for uma string não presente no DOM, ele precisará ser injetado e selecionado no DOM:

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

Demonstrações

Você pode testar a API Async Clipboard nas demonstrações abaixo. O primeiro exemplo demonstra como mover texto para dentro e para fora da área de transferência.

Para testar a API com imagens, use esta demonstração. Lembre-se de que apenas PNGs são aceitos e apenas em alguns navegadores.

Agradecimentos

A API Async Clipboard foi implementada por Darwin Huang e Gary Kačmarčík. Darwin também fez a demonstração. Agradecemos a Kyarik e novamente a Gary Kačmarčík por revisarem partes deste artigo.

Imagem principal de Markus Winkler no Unsplash.