Desbloqueando o acesso à área de transferência

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

A forma tradicional de obter acesso à área de transferência do sistema era através de document.execCommand() para interações com a área de transferência. Embora amplamente suportado, esse método de cortar e o processo de colar teve um custo: o acesso à área de transferência era síncrono e só podia e gravar no DOM.

Isso é bom para pequenas partes de texto, mas há muitos casos em que o bloqueio da para a transferência da área de transferência é uma experiência ruim. Limpeza demorada ou a decodificação da imagem pode ser necessária antes que o conteúdo possa ser colado com segurança. O navegador pode precisar carregar ou in-line recursos vinculados de um documento colado. Isso bloquear a página enquanto aguarda no disco ou na rede. Imagine adicionar permissões ao conjunto, exigindo que o navegador bloqueie a página enquanto solicita acesso à área de transferência. Ao mesmo tempo, as permissões exigidas As document.execCommand() para interação com a área de transferência são vagamente definidas e variam entre os navegadores.

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

Copiar: gravação de 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 resolve ou é rejeitada dependendo de o texto transmitido ser copiado com sucesso:

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

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

Origem

write()

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

Para gravar uma imagem na área de transferência, você precisa ter a imagem como um blob Uma maneira de fazer isso é solicitando a imagem de um servidor usando fetch() e, em seguida, chamando blob() no resposta.

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

Em seguida, transmita uma matriz de objetos ClipboardItem como um parâmetro ao write(). . No momento, só é possível passar uma imagem por vez, mas esperamos adicionar suporte a 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 blob objetos recebidos de fetch() ou canvas.toBlob(), a propriedade blob.type; contém automaticamente o tipo MIME correto para a 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, grave uma promessa no objeto ClipboardItem. Para esse padrão, você precisa 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);
}

Compatibilidade com navegadores

  • Chrome: 66.
  • Borda: 79.
  • Firefox: 127
  • Safari: 13.1.

Origem

O evento de cópia

Quando um usuário inicia uma cópia para a área de transferência e não chama preventDefault(), o copy evento inclui uma propriedade clipboardData com os itens já no formato correto. Se você quiser implementar sua própria lógica, chame preventDefault() para impedem o comportamento padrão em favor da sua própria implementação. Neste caso, clipboardData vai estar vazio. Considere uma página com texto e uma imagem e, quando o usuário selecionar tudo e iniciar uma cópia da área de transferência, sua solução personalizada deverá descartar o texto e apenas copiar a imagem. É possível fazer isso conforme mostrado no exemplo de código abaixo. O que não é abordado neste exemplo é como voltar ao APIs quando não houver 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

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

Origem

Para ClipboardItem:

Compatibilidade com navegadores

  • Chrome: 76.
  • Borda: 79.
  • Firefox: 127
  • Safari: 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 para que a promessa retornada seja resolvida:

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

  • Chrome: 66.
  • Borda: 79.
  • Firefox: 125
  • Safari: 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, consiga uma lista com ClipboardItem objetos e itere neles.

Cada ClipboardItem pode ter o próprio conteúdo em diferentes tipos, então você precisará 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 um argumento para conseguir o blob correspondente. Como antes, esse código não está vinculado a imagens trabalhar com outros tipos de arquivos 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

  • Chrome: 66.
  • Borda: 79.
  • Firefox: 127
  • Safari: 13.1.

Origem

Como trabalhar com arquivos colados

É útil para os usuários poderem 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. A ação é acionada quando o usuário pressiona o atalho padrão para colar no sistema operacional ou quando o usuário clicar em Editar e depois em Colar na barra de menus do navegador. Não é necessário nenhum outro código de encanamento.

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

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

Origem

O evento "Colar"

Conforme observado anteriormente, há planos de introduzir eventos para funcionar com a API Clipboard, mas, por enquanto, é possível usar o evento paste atual. Funciona muito bem com o novo métodos assíncronos para leitura de texto na á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

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

Origem

Lidar com vários tipos MIME

A maioria das implementações coloca vários formatos de dados na área de transferência para um único corte ou operação de cópia. Há dois motivos para isso: como desenvolvedor de apps, você tem uma maneira de conhecer os recursos do app para o qual um usuário quer copiar texto ou imagens, e muitos aplicativos oferecem suporte para colar dados estruturados como texto simples. Isso normalmente ocorre apresentado aos usuários com um item de menu Editar com um nome, como Colar e estilo de correspondência ou em Colar sem formatação.

O exemplo abaixo mostra como fazer isso. Este exemplo usa fetch() para extrair dados de imagem, mas também podem vir de uma <canvas> ou a 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 apresentou uma preocupação de segurança para os navegadores. Sem permissões adequadas, uma página poderia copiar silenciosamente todo tipo de conteúdo malicioso para a área de transferência de um usuário que produziria resultados catastróficos quando colados. Imagine uma página da Web que copia silenciosamente rm -rf / ou uma imagem da bomba de descompressão para a área de transferência.

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

Dar acesso irrestrito de leitura à área de transferência a páginas da Web é ainda mais problemáticos. Os usuários sempre copiam informações sensíveis, como senhas e dados pessoais para a área de transferência, que poderiam ser lidos por qualquer página sem conhecimento do usuário.

Assim como muitas APIs novas, a API Clipboard é suportada apenas para páginas exibidas em HTTPS. Para ajudar a evitar abusos, o acesso à área de transferência só é permitido quando uma página é na guia ativa. Páginas em guias ativas podem ser gravadas na área de transferência sem solicitando permissão, mas a leitura da área de transferência sempre exige permissão.

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

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

Você também pode controlar se um gesto do usuário é necessário para invocar o corte ou colar usando a opção allowWithoutGesture. O padrão para esse valor varia de acordo com o navegador, portanto, você deve sempre incluí-lo.

É aqui que a natureza assíncrona da API Clipboard é útil: a tentativa de ler ou gravar dados da área de transferência solicita ao usuário automaticamente se ela ainda não tiver sido concedida. Como a API é baseada em promessas, isso é totalmente transparente, e um usuário que nega a permissão para a área de transferência causa a promessa de rejeitar para que a página possa responder adequadamente.

Como os navegadores só permitem acesso à área de transferência quando uma página está na guia ativa, alguns exemplos não serão executados se forem colados diretamente console do navegador, já que as próprias ferramentas do desenvolvedor são a guia ativa. Há um truque: adiar acesso à área de transferência usando setTimeout() e, em seguida, clicar rapidamente na página para focalize-o 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, é preciso ativá-la com Política de permissões, que define um mecanismo que permite habilitar e desativar vários recursos do navegador e APIs. Concretamente, você precisa passar ou ambos de 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, ao mesmo tempo, oferecer suporte a todos os navegadores, faça o teste da navigator.clipboard e voltar aos métodos anteriores. Por exemplo, veja como você pode implementar a colagem 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);
});

E isso não é tudo. Antes da API Async Clipboard, havia uma mistura de diferentes implementações de copiar e colar em navegadores da Web. Na maioria dos navegadores, o recurso de copiar e colar do próprio navegador pode ser acionado usando document.execCommand('copy') e document.execCommand('paste'). Se o texto a ser copiada é uma string ausente no DOM, que deve ser injetada 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 pode remixar a demonstração em texto ou a demonstração da imagem para experimentá-los.

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 compatíveis. e somente em alguns navegadores.

Agradecimentos

A API async Clipboard foi implementada por Darwin Huang e Gary Kačmarčík (em inglês). Darwin também fez uma demonstração. Agradecemos a Kyarik e novamente Gary Kačmarčík por revisar partes deste artigo.

Imagem principal de Markus Winkler no Abrir a página.