1001 formas de fazer Input de arquivos com JavaScript

Em:

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\.)?[[email protected]:%._\+~#=]{2,256}\.[a-z]{2,6}\b([[email protected]:%_\+.~#?&//=]*)/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.

image-picker
<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.

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 arrastado
  • move: em alguns sistemas operacionais e navegadores, adiciona um ícone indicando o ato de mover um arquivo
  • link: 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.

  • Interação com Hardware usando JavaScript

    É isso mesmo que você leu! Já ouvimos falar que o JavaScript “roda em tudo”, certo? Existem diversos exemplos que provam que tal afirmação está correta, e hoje irei mostrar um deles. Inclusive, se contássemos o que anda acontecendo atualmente, faríamos alguém dos anos 90 rir muito. Primeiramente, existem frameworks e plataformas diferentes: O Cylon.js, […]

  • BrazilJS Weekly #195 – Palestrantes BrazilJS, Kotlin, JSON feed e Adeus Bower?

    Wow! Anunciados os primeiros palestrantes da BrazilJS Conf 2017, Kotlin, JSON feed e Adeus Bower?

  • HyperTerm agora é Hyper.app

    O time da Zeit anunciou na última semana uma mudança no terminal JS/HTML/CSS desenvolvido pela empresa. O HyperTerm agora é Hyper.app. Guillermo Rauch, um dos fundadores da empresa, comentou que o projeto iniciou apenas como um experimento e se tornou um dos projetos mais populares no GitHub neste ano, com mais de 9.000 estrelas. O […]

Patrocinadores BrazilJS

Gold

Silver

Bronze

Apoio

BrazilJS® é uma iniciativa NASC.     Hosted by Getup