Acesso mais seguro e desbloqueado à área de transferência para texto e imagens
A maneira tradicional de acessar a área de transferência do sistema era por
document.execCommand()
para interações com a área de transferência. Embora tenha amplo suporte, esse método de corte e
colagem tem um custo: o acesso à área de transferência é síncrono e só pode 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. Pode ser necessária uma limpeza ou
decodificação de imagens demorada 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
bloquearia a página enquanto aguarda no disco ou na rede. Imagine adicionar permissões
à mistura, exigindo que o navegador bloqueie a página enquanto solicita
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 a textos e imagens na maioria dos navegadores, mas o suporte varia. Estude 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 de o texto transmitido ser 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);
}
}
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. 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 é solicitando a imagem de um servidor usando fetch()
e, em seguida, chamando
blob()
na
resposta.
Solicitar uma imagem do servidor pode não ser desejável ou possível por vários motivos. Felizmente, você também pode 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, você só pode transmitir uma imagem por vez, mas esperamos oferecer suporte a várias imagens no futuro. ClipboardItem
recebe 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, grave uma promessa no 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
evento copy
inclui uma propriedade clipboardData
com os itens já no formato correto.
Se você quiser implementar sua própria lógica, chame preventDefault()
para
evitar o comportamento padrão em favor da 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 na área de transferência, sua solução personalizada vai descartar o texto e
copiar apenas a imagem. Você pode fazer isso conforme mostrado no exemplo de código abaixo.
O que não é abordado nesse exemplo é como voltar a APIs
anteriores quando a API Clipboard não tem suporte.
<!-- 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 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);
}
}
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 faça iterações neles.
Cada ClipboardItem
pode armazenar o conteúdo em tipos diferentes. Portanto, será necessário
iterar a lista de tipos, usando novamente um loop for...of
. Para cada tipo,
chame o método getType()
com o tipo atual como um 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
Eles são úteis para os 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 padrão de colar do sistema operacional ou quando o usuário clica 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());
});
O evento "Colar"
Como mencionado anteriormente, há planos para introduzir eventos para trabalhar com a API Clipboard,
mas, por enquanto, você pode usar o evento paste
atual. Ele 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);
});
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 acontecer: como desenvolvedor de apps, você não tem como conhecer os recursos do app em que um usuário quer copiar texto ou imagens, e muitos aplicativos oferecem suporte à colagem de dados estruturados como texto simples. Isso geralmente é apresentado aos usuários com um item de menu Editar com um nome como Colar e combinar estilo ou Colar sem formatação.
O exemplo abaixo mostra como fazer isso. Este exemplo usa fetch()
para receber
dados de imagem, mas também pode 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 apresentou uma preocupação de segurança para os navegadores. Sem
as 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, 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 descompactaçã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 copiam rotineiramente 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 muitas outras APIs, a API Clipboard só tem suporte para páginas veiculadas por HTTPS. Para evitar abusos, o acesso à área de transferência só é permitido quando uma página está na 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.
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
a guia ativa. A permissão clipboard-read
precisa ser solicitada. Para isso,
tente ler os dados da área de transferência. O código abaixo mostra o último:
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 recorte ou
colagem usando a opção allowWithoutGesture
. O padrão desse valor
varia de acordo com o navegador. Portanto, sempre inclua esse valor.
É aqui que a natureza assíncrona da API Clipboard é muito útil: a tentativa de 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 para a área de transferência faz com que a promessa seja rejeitada para que a página possa responder de maneira adequada.
Como os navegadores só permitem acesso à área de transferência quando uma página está na guia ativa,
alguns dos exemplos mostrados 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 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, você precisa ativá-la com a
Política de permissões,
que define um mecanismo que permite ativar e
desativar seletivamente vários recursos e APIs do navegador. Especificamente, é necessário transmitir
clipboard-read
ou clipboard-write
, ou ambos, 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 com suporte a todos os navegadores, teste
navigator.clipboard
e volte a métodos anteriores. Por exemplo, veja como
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
implementações de copiar e colar diferentes em navegadores da Web. Na maioria dos navegadores,
a cópia e a colagem podem ser acionadas 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 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 de imagem para fazer experimentos com elas.
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 apenas em alguns navegadores.
Links relacionados
- MDN (em inglês)
- Formatos personalizados da Web
Agradecimentos
A API Async Clipboard foi implementada por Darwin Huang e Gary Kačmarčík (links em inglês). Darwin também forneceu a demonstração. Agradecemos a Kyarik e Gary Kačmarčík por reler partes deste artigo.
Imagem principal de Markus Winkler no Unsplash (links em inglês).