Desbloqueando o acesso à área de transferência

Acesso mais seguro e desbloqueado à área de transferência para textos e imagens

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

Isso é bom 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. Uma limpeza ou decodificação de imagens demoradas pode ser necessária para que o conteúdo possa ser colado com segurança. O navegador pode precisar carregar recursos vinculados inline de um documento colado. Isso bloquearia a página enquanto aguardava 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 aplicadas em torno de document.execCommand() para a interação da área de transferência são definidas vagamente e variam de acordo com o navegador.

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 o suporte varia. Analise com atenção a visão geral da compatibilidade do navegador para cada uma das seções a seguir.

Copiar: gravar dados na área de transferência

writeText()

Para copiar o 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 ou não:

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

Compatibilidade com navegadores

  • 66
  • 79
  • 63
  • 13.1

Origem

write()

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

Para gravar uma imagem na área de transferência, você precisa dela como blob. Uma maneira de fazer isso é solicitar a imagem de um servidor usando fetch() e, em seguida, 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(). Atualmente, 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 a chave e o blob como o valor. Para objetos blob recebidos de fetch() ou canvas.toBlob(), a propriedade blob.type contém automaticamente o tipo MIME correto de 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, escreva uma promessa no objeto ClipboardItem. Para esse padrão, é preciso conhecer 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);
}

Compatibilidade com navegadores

  • 66
  • 79
  • 13.1

Origem

O evento de cópia

Quando um usuário inicia uma cópia da área de transferência e não chama preventDefault(), o evento copy inclui uma propriedade clipboardData com os itens já no formato correto. Caso queira implementar sua própria lógica, chame preventDefault() para evitar o comportamento padrão e favorecer sua própria 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 precisará descartar o texto e copiar apenas a imagem. É possível fazer isso conforme mostrado no exemplo de código abaixo. Este exemplo não aborda como retornar a APIs anteriores quando não há suporte para a API Clipboard.

<!-- 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:

Compatibilidade com navegadores

  • 1
  • 12
  • 22
  • 3

Origem

Para ClipboardItem:

Compatibilidade com navegadores

  • 76
  • 79
  • 13.1

Origem

Colar: lendo dados da área de transferência

readText()

Para ler o 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);
  }
}

Compatibilidade com navegadores

  • 66
  • 79
  • 13.1

Origem

read()

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

Cada ClipboardItem pode conter o conteúdo em tipos diferentes. Portanto, será necessário iterar a lista de tipos, novamente usando uma repetição 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 funcionará com outros tipos de arquivo 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);
  }
}

Compatibilidade com navegadores

  • 66
  • 79
  • 13.1

Origem

Como trabalhar com arquivos colados

Os usuários podem utilizar atalhos do 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 clica no atalho padrão do sistema operacional ou em Editar e depois 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());
});

Compatibilidade com navegadores

  • 3
  • 12
  • 3.6
  • 4

Origem

O evento de colar

Conforme observado anteriormente, há planos de introduzir eventos para trabalhar com a API Clipboard, mas, por enquanto, você pode usar o evento paste já existente. Isso funciona bem com os novos métodos assíncronos para ler o 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);
});

Compatibilidade com navegadores

  • 1
  • 12
  • 22
  • 3

Origem

Gerenciamento de 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 conhecer os recursos do app para os quais um usuário quer copiar texto ou imagens, e muitos aplicativos oferecem suporte à colagem de dados estruturados como texto simples. Geralmente, isso é apresentado aos usuários com um item de menu Editar com um nome, como Colar e corresponder estilo ou Colar sem formatação.

O exemplo abaixo mostra como fazer isso. Esse exemplo usa fetch() para receber dados de imagem, mas também pode vir de uma <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 preocupação de segurança para os navegadores. Sem as 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 e produzir resultados catastróficos quando colada. Imagine uma página da Web que copia silenciosamente rm -rf / ou uma imagem de bomba de descompactação para a área de transferência.

Solicitação do navegador solicitando a permissão da área de transferência ao usuário.
A solicitação de permissão da API Clipboard.

Oferecer acesso irrestrito de leitura à área de transferência é ainda mais difícil. Os usuários sempre copiam informações sensíveis, como senhas e detalhes pessoais, para a área de transferência, que podem ser lidas por qualquer página sem o conhecimento do usuário.

Assim como em muitas APIs novas, a API Clipboard só oferece suporte a páginas exibidas por HTTPS. Para 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 solicitar permissão, mas a leitura da área de transferência sempre exige permissão.

Adicionamos permissões para copiar e colar à 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, o que você pode fazer tentando ler os dados da área de transferência. O código abaixo mostra a segunda opção:

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 gesto de cortar ou colar usando a opção allowWithoutGesture. O padrão para esse valor varia de acordo com o navegador, portanto, sempre inclua-o.

É aqui que a natureza assíncrona da API Clipboard realmente ajuda: tentar ler ou gravar dados da área de transferência solicita automaticamente a permissão do usuário, caso isso 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 de forma adequada.

Como os navegadores só permitem acesso à área de transferência quando uma página está na guia ativa, alguns dos exemplos aqui não sã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 dentro da página para focá-la antes que as funções sejam chamadas:

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

Integração com a 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 que permite ativar e desativar vários recursos do navegador e APIs. Concretamente, você precisa transmitir clipboard-read ou clipboard-write, dependendo das necessidades do 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 o navigator.clipboard e volte aos 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);
});

Essa não é a história completa. Antes da API Async Clipboard, havia uma mistura de diferentes implementações de copiar e colar nos navegadores da Web. Na maioria dos navegadores, o recurso de copiar e colar pode ser acionado usando document.execCommand('copy') e document.execCommand('paste'). Se o texto a ser copiado não estiver presente no DOM, ele precisará ser injetado no DOM e selecionado:

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. No Glitch, é possível remixar a demonstração de texto ou a demonstração de imagem para fazer testes.

O primeiro exemplo demonstra como mover o 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 em alguns navegadores.

Agradecimentos

Essa API foi implementada por Darwin Huang e Gary Kačmarčík (links em inglês). Darwin também fez a demonstração. Agradecemos a Kyarik e, novamente, Gary Kačmarčík pela revisão de partes deste artigo.

Imagem principal de Markus Winkler no Unsplash (links em inglês).