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);
}
}
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);
}
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
:
Para ClipboardItem
:
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);
}
}
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);
}
}
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());
});
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);
});
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.

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.
Links relacionados
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.