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
- Crie uma área com texto que diga o que fazer e o que é aceito: "Arraste os PDFs para cá, até 10 MB cada".
- Coloque dentro dela um
<input type="file">real, com<label>, para teclado, celular e quem prefere o diálogo do sistema. - Na janela inteira, cancele o padrão de
dragoveredroppara o navegador não abrir o arquivo, sem processar nada. - Na zona, destaque a área enquanto um arquivo está sobre ela, com um contador para os filhos não fazerem piscar.
- No
dropda zona e nochangedo input, chame a mesma função que valida e põe na fila. - Mostre a fila com o nome de cada arquivo e o motivo de cada recusa.
- 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
dropna janela. O arquivo solto fora da zona abre na aba e o formulário preenchido se perde. - Tratar o arquivo no
documentinteiro. Qualquer arrasto acidental sobre a aba vira upload. - Começar o envio no
dragoverou nodrop. 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?
Referência: http://yuilibrary.com/yui/docs/uploader/uploader-dd.html