1001 formas de fazer Input de arquivos com JavaScript
Ok, ok, eu exagerei... Não são 1001 formas. Mas, felizmente, hoje temos diversas maneiras de inputarmos arquivos usando JavaScript e as APIs do DOM. É hora de discutirmos algumas delas.
Cliente vs Server
O que faremos aqui será útil para manipularmos no client side inicialmente. Mas, como veremos, teremos um blob em nossas mãos e, assim, poderemos fazer upload deste blob usando APIs como fetch ou submetendo um formulário mesmo.
URLs
De longe, a maneira mais simples é utilizarmos uma URL para carregarmos algum arquivo.
Infelizmente, isso só é útil se o usuário já tem a imagem armazenada em algum lugar, e é preciso levar em consideração também as permissões para carregarmos estes conteúdos caso venham de outro domínio.
Vamos, por exemplo, perguntar para nosso usuário por uma URL. Vamos conferir se trata-se de uma URL válida e vamos colocá-la no DOM.
Nosso HTML seria assim
<div id='container'></div> <input type='button' value='+' id='add-file-btn' />
E no JavaScript:
function appendTheFile (url) { url = new URL(url) // se for uma imagem, adicionamos uma imagem if (url.pathname.match(/\.(jpe?g|png|svg|webp|gif)/)) { let img = document.createElement('img') img.src = url.toString() document.getElementById('container').appendChild(img) } else { // se não for uma imagem, usamos um iframe let iframe = document.createElement('iframe') iframe.src = url.toString() document.getElementById('container').appendChild(iframe) } // iframes funcionam para qualquer arquivo praticamente, // mas poderíamos tratar outros formatos de arquivo, como vídeo, áudio, etc. } document.getElementById('add-file-btn').addEventListener('click', event => { // perguntamos qual a URL para o usuário let answer = window.prompt('Qual o endereço?') // usamos uma expressão regular para verificarmos se é uma URL válida if (answer.match(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/im)) { // podemos opcionalmente parsear esta url em um objeto do tipo URL appendTheFile(url) } })
Essa forma é prática, porém não nos dá tanto poder para manipular o arquivo - e não é tão trivial para o usuário.
FilesList
Para manipular os arquivos escolhidos pelo usuário nos métodos seguintes, receberemos um FilesList.
Usaremos uma função chamada appendFiles, que descreverei no final do artigo. Ela receberá o FilesList entregue por cada um dos diferentes métodos, que trata-se de um objeto Array like, com uma lista de objetos do tipo file.
Note que essa lista não é uma Array, apesar de ter um length. Caso queira iterar sobre seus itens, use algo como
Array.from(filesList)
.
Input do tipo file
Contamos também com o input do tipo file para solicitarmos arquivos ao usuário. Esta API nos dá algumas possibilidades a mais.
Por exemplo, podemos especificar os mimetypes que aceitaremos.
<input type='file' accept='image/*' id='file-input' />
Legal é que, se quisermos que o usuário tenha a possibilidade de escolher entre enviar um arquivo ou tirar uma foto na hora (caso esteja usando um celular), podemos utilizar o atributo capture.
<input type='file' accept='image/*' id='file-input' capture />
Quando o usuário clicar o botão do input, será exibido o prompt do Sistema Operacional para que o usuário possa escolher.
<script> const fileInput = document.getElementById('file-input'); fileInput.addEventListener('change', event => appendFiles(event.target.files)); </script>
Acha o input[type=file] feio? Tudo bem, você pode usar algum outro botão na tela para dispará-lo e esconder o input do tipo file.
Apenas lembre que ele não pode ter um display: none
.
Por exemplo:
<label for='real-input' ><div class='botao-bonito'>+</div></label> <input type='file' accept='image/*' id='real-input' name='real-input' capture style="visibility: hidden; position: fixed; left: -9000px" />
O atributo capture pode ser true, definido por estar presente no input (como no exemplo acima). Mas, também, pode receber os valores user
ou environment
para indicar uma preferência de qual câmera queremos usar - a frontal ou a traseira respectivamente.
<input type='file' accept='image/*' capture /> <input type='file' accept='image/*' capture='user' /> <input type='file' accept='image/*' capture='environment' />
Drop Files
Uma outra API muito legal é a de drag n drop para arquivos, que já tem um suporte bem legal em múltiplos browsers.
[video width="600" height="392" mp4="https://prd.braziljs.org/wp-content/uploads/2017/11/drop-file.mp4"][/video]
Essa API permite que o usuário arraste um ou mais arquivos e "drope-os" em sua página.
A ideia aqui é tratar estes arquivos e interceptar o evento de drop para que o browser não execute sua ação padrão, que seria substituir a página por um viewer do próprio navegador.
Essa API adiciona os seguintes eventos aos elementos do DOM:
dragenter
: o usuário entrou com o mouse sobre a droppable zone, enquanto arrastava arquivos (acontece uma única vez).dragleave
: o usuário estava arrastando arquivos para sua página, mas tirou o cursor de cima de sua droppable zone.dragover
: o usuário está com o cursor sobre sua droppable zone (disparado várias vezes).drop
: O usuário "droppou" os arquivos em droppable zone.
// Este é um elemento que ficará visível apenas quando o usuário estiver // levando algum elemento para sua droppable zone const droppableZoneSign = document.getElementById('droppable-zone-sign') document.addEventListener('dragenter', event => { droppableZoneSign.classList.add('droppable') }) document.addEventListener('dragleave', event => { droppableZoneSign.classList.remove('droppable') }) document.addEventListener('dragover', event => { event.stopPropagation(); event.preventDefault(); droppableZoneSign.classList.add('droppable') // isso adiciona o sinal de mais (+) ao lado do cursor para indicar ao usuário // que uma ação diferente será tomada event.dataTransfer.dropEffect = 'copy'; }) document.addEventListener('drop', event => { outputEl.classList.remove('droppable') event.stopPropagation(); event.preventDefault(); // trata o filesList appendFiles(event.dataTransfer.files) })
Estamos setando a propriedade event.dataTransfer.dropEffect
para indicar ao usuário que algo diferente da ação padrão acontecerá.
O dropEffect aceita os seguintes valores:
copy
: adiciona um sinal de mais (+) ao arquivo sendo arrastadomove
: em alguns sistemas operacionais e navegadores, adiciona um ícone indicando o ato de mover um arquivolink
: adiciona um sinal de link (normalmente uma seta)none
: informa que o arquivo não é válido para ser droppado. O evento de drop será anulado.
Paste / Colar arquivos
Esta é uma feature que eu uso bastante. Trata-se da possibilidade do usuário colar arquivos ou imagens que estavam na memória. Especialmente útil em casos como screenshots.
Felizmente, essa é uma API bem simples e intuitiva. :)
// escutamos o evento de "colar" myInputElement.addEventListener('paste', event => { // se haviam arquivos no clipboard (não era um texto ou valor simples) if (event.clipboardData.files && event.clipboardData.files.length) { // cancelamos o comportamento padrão do navegador event.preventDefault() event.stopPropagation() // usamos o filesList sendFiles(event.clipboardData.files) } })
O único ponto no qual precisamos prestar atenção aqui é que este evento apenas pode ser disparado em elementos que sejam do tipo input ou textarea ou, então, que tenham a contentEditable=true
.
sendFiles (tratando um filesList)
Agora nós vamos tratar o nosso fileList.
Como comentei anteriormente, o fileList é um objeto Array like, ou seja, se parece com uma Array, mas não é. Por exemplo, ele tem a propriedade length, mas não tem o método map.
Podemos contornar isso facilmente, usando uma chamada para Array.from(filesList)
.
Este é o momento em que criaremos nossa função sendFiles, que receberá o filesList gerado por cada um dos métodos citados acima.
function sendFiles (filesList) { let content = '' Array.from(filesList).forEach(file => { console.log(file) if (file.type.match(/^image\//)) { content += `<img src='${URL.createObjectURL(file)}'>` } else { // você pode tratar outros tipos de arquivos, como vídeos ou áudios // para este exemplo, vamos jogar o arquivo em um iframe content += `<iframe src='${URL.createObjectURL(file)}'></iframe>` } }) let div = document.createElement('div') div.innerHTML = content document.body.appendChild(div) }
Coming next
Em nosso próximo encontro, discutiremos como utilizar a câmera do usuário com a API de captura, o que será mais uma opção na hora de importarmos arquivos ou imagens do usuário.
Espero que tenha curtido e aproveite para deixar nos comentários sugestões de outras possibilidades de entrada de arquivos para que possamos discutir no futuro.