Programação Por Equipe DevBras

Capa do artigo: Arrastar e soltar arquivo: uma zona clara e o mesmo caminho do botão

Arrastar e soltar arquivo: uma zona clara e o mesmo caminho do botão

Monte uma zona de soltar arquivos que diz o que aceita, não deixa o navegador abrir o PDF por cima do formulário e usa a mesma validação do botão de escolher arquivo.

A pessoa preenche o pedido de reembolso, arrasta o PDF da nota fiscal e erra a área por dois centímetros. O navegador abre o PDF no lugar da página, e o formulário inteiro some. Em outro site, o contrário: a página toda aceita arquivo, e quem só estava movendo uma planilha entre janelas soltou em cima da aba e começou um upload que não queria.

A resposta curta: a zona de soltar é um lugar delimitado, com texto dizendo o que aceita, e o resto da janela não abre nem envia o arquivo solto. A mesma zona tem um <input type="file"> de verdade para quem não arrasta, e os dois caminhos passam pela mesma validação e pela mesma fila. O envio começa num clique de "Enviar", nunca no simples arrastar por cima.

Por que o arquivo solto foge do controle

Quando você solta um arquivo numa página que não trata o evento, o navegador faz o que faria com qualquer arquivo: abre. PDF e imagem aparecem na aba, substituindo a página; outros tipos podem ser baixados. Para a pessoa, o formulário desapareceu.

Para aceitar o arquivo, a página precisa cancelar o comportamento padrão de dois eventos: dragover, que dispara continuamente enquanto o arquivo está sobre um elemento, e drop, no momento em que ele é solto. Muita implementação faz isso no document inteiro e trata o arquivo ali, e é assim que a janela toda vira zona de upload.

Há ainda o problema do contorno que pisca. Os eventos dragenter e dragleave disparam também ao entrar e sair dos elementos filhos da zona. Se você apaga o destaque no primeiro dragleave, ele some cada vez que o arquivo passa por cima do texto dentro da zona.

Como montar a zona passo a passo

  1. Crie uma área com texto que diga o que fazer e o que é aceito: "Arraste os PDFs para cá, até 10 MB cada".
  2. Coloque dentro dela um <input type="file"> real, com <label>, para teclado, celular e quem prefere o diálogo do sistema.
  3. Na janela inteira, cancele o padrão de dragover e drop para o navegador não abrir o arquivo, sem processar nada.
  4. Na zona, destaque a área enquanto um arquivo está sobre ela, com um contador para os filhos não fazerem piscar.
  5. No drop da zona e no change do input, chame a mesma função que valida e põe na fila.
  6. Mostre a fila com o nome de cada arquivo e o motivo de cada recusa.
  7. Envie só quando a pessoa confirmar, e valide de novo no servidor.

O HTML da zona

<div data-zona-upload>
  <p>Arraste os PDFs da nota fiscal para cá.</p>
  <label>
    Ou escolha no computador:
    <input type="file" name="documentos" accept="application/pdf,.pdf" multiple>
  </label>
  <p>Só PDF, até 10 MB cada.</p>
</div>
<ul data-fila aria-live="polite"></ul>
<button type="button" data-enviar>Enviar arquivos</button>

O input fica visível e funcional. Se o design pede um botão estilizado, há jeitos de esconder o input sem tirá-lo do teclado, explicados em botão de escolher arquivo e em input file ainda é input.

O JavaScript

const zona = document.querySelector('[data-zona-upload]');
const input = zona.querySelector('input[type="file"]');
const fila = document.querySelector('[data-fila]');
const botaoEnviar = document.querySelector('[data-enviar]');
const TIPOS_ACEITOS = ['application/pdf'];
const TAMANHO_MAXIMO = 10 * 1024 * 1024;
const pendentes = [];

function motivoDeRecusa(arquivo) {
  if (!TIPOS_ACEITOS.includes(arquivo.type)) return 'não é PDF';
  if (arquivo.size > TAMANHO_MAXIMO) return 'maior que 10 MB';
  return null;
}

function adicionar(lista) {
  for (const arquivo of lista) {
    const motivo = motivoDeRecusa(arquivo);
    const item = document.createElement('li');
    item.textContent = motivo
      ? arquivo.name + ': recusado, ' + motivo
      : arquivo.name + ': pronto para enviar';
    fila.append(item);
    if (!motivo) pendentes.push(arquivo);
  }
}

input.addEventListener('change', () => {
  adicionar(input.files);
  input.value = '';
});

window.addEventListener('dragover', (evento) => evento.preventDefault());
window.addEventListener('drop', (evento) => evento.preventDefault());

let profundidade = 0;

zona.addEventListener('dragenter', (evento) => {
  if (!evento.dataTransfer.types.includes('Files')) return;
  profundidade++;
  zona.dataset.ativa = 'true';
});

zona.addEventListener('dragleave', () => {
  profundidade = Math.max(0, profundidade - 1);
  if (profundidade === 0) delete zona.dataset.ativa;
});

zona.addEventListener('dragover', (evento) => {
  evento.preventDefault();
  evento.dataTransfer.dropEffect = 'copy';
});

zona.addEventListener('drop', (evento) => {
  evento.preventDefault();
  profundidade = 0;
  delete zona.dataset.ativa;
  adicionar(evento.dataTransfer.files);
});

botaoEnviar.addEventListener('click', async () => {
  if (pendentes.length === 0) return;
  const dados = new FormData();
  for (const arquivo of pendentes) dados.append('documentos[]', arquivo);
  const resposta = await fetch('/reembolso/anexos', { method: 'POST', body: dados });
  fila.append(Object.assign(document.createElement('li'), {
    textContent: resposta.ok ? 'Arquivos enviados.' : 'O envio falhou. Os arquivos continuam na fila.'
  }));
  if (resposta.ok) pendentes.length = 0;
});

Os listeners da janela só impedem o navegador de abrir o arquivo; eles não fazem upload de nada. Quem trata o arquivo é a zona. Um arquivo solto fora dela simplesmente não acontece, e o formulário fica onde estava.

O input.value = '' depois do change permite escolher o mesmo arquivo de novo, caso a pessoa o remova da fila e mude de ideia. Sem isso, o navegador não dispara change para a mesma seleção.

O destaque usa data-ativa, que o CSS pode estilizar com [data-zona-upload][data-ativa]: borda mais grossa, fundo levemente colorido e, de preferência, uma mudança de texto, porque cor sozinha não basta para todo mundo.

Validação que não engana

O tipo do navegador é uma pista, não uma prova

O arquivo.type vem da extensão do nome, na maioria dos sistemas. Um executável renomeado para .pdf passa por ele. A checagem no navegador serve para dar resposta rápida a quem errou o arquivo; a checagem que protege o sistema é a do servidor, que lê o conteúdo. Como fazer isso no PHP está em upload de arquivo em PHP, e os limites de tamanho que se somam no caminho em limite de upload de verdade.

Pasta solta e tipo vazio

Algumas pessoas soltam uma pasta inteira. Dependendo do navegador e do sistema, ela pode chegar na lista como um item sem tipo, que a regra de tipo já recusa. Para arquivos que o sistema não reconhece, type vem como texto vazio, e a mensagem "não é PDF" continua correta.

Mostrar o progresso real

Se os arquivos são grandes, mostre o progresso, mas não confunda bytes enviados com arquivo gravado. A diferença está em barra de progresso do upload.

Erros comuns

  • Não cancelar o drop na janela. O arquivo solto fora da zona abre na aba e o formulário preenchido se perde.
  • Tratar o arquivo no document inteiro. Qualquer arrasto acidental sobre a aba vira upload.
  • Começar o envio no dragover ou no drop. A pessoa não tem chance de conferir a fila nem de remover o arquivo errado.
  • Zona sem input de arquivo. Quem usa teclado, celular ou leitor de tela não tem como enviar nada.
  • Validação diferente para arrastar e para escolher. O mesmo arquivo é recusado num caminho e aceito no outro.
  • Apagar o destaque no primeiro dragleave. A borda pisca a cada filho e a zona parece instável.

Checklist

  • A zona diz em texto o que aceita e qual o tamanho máximo.
  • Há um <input type="file"> funcional dentro dela.
  • Soltar fora da zona não abre o arquivo nem envia nada.
  • O destaque não pisca ao passar sobre o texto interno.
  • Arrastar e escolher chamam a mesma função de validação.
  • A fila mostra cada arquivo e cada motivo de recusa.
  • O envio só começa por ação explícita da pessoa.
  • O servidor valida tipo e tamanho de novo, pelo conteúdo.

Preencha o formulário do seu site, arraste um PDF e solte de propósito fora da área marcada. O formulário continua lá, com tudo preenchido? E dá para enviar o mesmo arquivo sem arrastar nada, só com o teclado?

Avalie este artigo

Referência: http://yuilibrary.com/yui/docs/uploader/uploader-dd.html