Merida Design Blog

Publicado el | Tutoriales / , , , ,

Como crear un input file personalizado, con vista previa y drag & drop

Los input de tipo file son uno de los elementos de html que cuentan con menor capacidad de personalización a través de CSS, sin embargo, es posible crear un control completamente personalizado que use las funciones del input file. En éste tutorial vamos a ver como realizar justamente eso.

Los 3 objetivos principales que voy a cubrir en este tutorial son los siguientes:

Al final del tutorial este será el resultado:

Selecciona o arrastra una imagen sobre el control para ver el funcionamiento


Personalizar el control con CSS

Actualmente los input de tipo file no se pueden personalizar con css mas que algunas propiedades, sin embargo, la etiqueta label puede ser ligada directamente a un input, de manera que si presionamos el label, es como si estuviésemos presionando el input directamente y el label puede ser personalizado como cualquier elemento de tipo bloque (div), lo que nos permite crear el control personalizado sin mayor problemas.

Crea un archivo con nombre index.html y coloca el siguiente código:

    <!DOCTYPE html>
    <html lang="es">
        <head>
            <title>File Uploader</title>
            <link href="https://file.myfontastic.com/SLzQsLcd7FmmzjBYTcyVW3/icons.css" rel="stylesheet">
            <link rel="stylesheet" href="style.css">
        </head>
        
        <body>
            <label class="uploader">
                <i class="icon icon-upload"></i>
                <img src="">
                <input type="file" accept="image/*">
            </label>
        </body>
    </html>

Crea un archivo con nombre style.css y coloca el siguiente código:

    /*Demo Styles  */

    html, body {
        height: 100%;
        margin: 0;
        padding: 0; 
    }

    body {
        align-items: center;
        background-color: #eee;
        display: flex;
        flex-direction: column;
        justify-content: center; 
    }

El label contiene un elemento de imagen que nos servirá para mostrar la vista previa y un elemento al que le asignamos la clase icon-upload que obtenemos de Fontastic (línea 5).
El input también lo colocamos dentro del label pero esto es opcional, ya que podemos colocarlo fuera y relacionarlos a través del atributo for del label y un id para el input solamente colocando el mismo valor en ambos.

Los estilos colocados en el archivo style.css solo sirven para centrar el contenido para fines del demo.

Con el fin de no hacer mas extenso el código para el artículo, no estoy colocando prefijos para distintos navegadores pero es necesario colocarlos para una mayor compatibilidad. El código del demo si contiene los prefijos.

Si abrimos en este momento el index.html en el navegador debe verse de la siguiente forma:

image-1

 

Crea un archivo de estilos con el nombre fileuploader.css y coloca el siguente código:

    /* File Uploader Styles  */

    .uploader input {
        display: none; 
    }

    .uploader {
        align-items: center;
        background-color: rgba(0, 0, 0, 0.02);
        display: flex;
        height: 300px;
        justify-content: center;
        outline: 3px dashed #ccc;
        outline-offset: 5px;
        position: relative;
        width: 300px; 
    }
    
    .uploader img,
    .uploader .icon {
        pointer-events: none; 
    }

    .uploader, 
    .uploader .icon {
        transition: all 100ms ease-in; 
    }

    .uploader .icon {
        color: rgba(0, 0, 0, 0.2);
        font-size: 5em;
    }

    .uploader.dragging {
        outline-color: orangered; 
    }

    .uploader.dragging .icon {
        color: orangered; 
    }

    .uploader.loaded .icon {
        color: rgba(255, 255, 255, 0.5); 
    }

    .uploader img {
        left: 50%;
        opacity: 0;
        max-height: 100%;
        max-width: 100%;
        position: absolute;
        top: 50%;
        transition: all 300ms ease-in;
        transform: translate(-50%,-50%);
        z-index: -1; 
    }
    
    .uploader img.loaded {
        opacity: 1; 
    }

A continuación describo los puntos que considero mas importantes del código.

  • En la línea 4 ocultamos el input, ya que la apariencia del control la crearemos a través del label .
  • En la línea 21 cancelamos la respuesta a los eventos del mouse para los elementos que están dentro del label para que no haya problemas al momento de implementar la función de arrastrar y soltar.
  • Las clases dragging y loaded las asignaremos dinámicamente para controlar la retroalimentación visual al usuario según la acción realizada.
  • La imagen para la vista previa permanecerá oculta (linea 48) y se mostrará una vez que se haya terminado de cargar en el DOM (linea 59).

 

Ahora solo tenemos que cargar este archivo en el html.

    ...
    <link rel="stylesheet" href="fileuploader.css">
    <link rel="stylesheet" href="style.css">

Si recargas el archivo en el navegador, ahora debe verse igual al demo que se encuentra al principio, incluso puedes hacer click al control y se abrirá la ventana para seleccionar el archivo, sin embargo, al seleccionar el archivo no pasará nada ya que aún no implementamos la funcionalidad para mostrar la vista previa.


Vista previa de la imagen seleccionada

Abre tu editor y crea un archivo con el nombre FileUploader.js y coloca el siguiente código:

    (function() {        
        // 1
        function FileUploader(selector) {
            if (undefined !== selector) {
                this.init(selector);
            }
        }
        
        // 2
        FileUploader.prototype.init = function(selector) {
            if (undefined !== this.$el) {
                this.unsuscribe();
            }
            
            this.$el = document.querySelector(selector);
            this.$fileInput = this.$el.querySelector('input');
            this.$img = this.$el.querySelector('img');
            
            this.suscribe();
        }

        // 3
        FileUploader.prototype.suscribe = function() {
            this.$fileInput.addEventListener('change', _handleInputChange.bind(this));
            this.$img.addEventListener('load', _handleImageLoaded.bind(this));
        }
        
        // 4
        FileUploader.prototype.unsuscribe = function() {
            this.$fileInput.removeEventListener('change', _handleInputChange.bind(this));
            this.$img.removeEventListener('load', _handleImageLoaded.bind(this));
        }

        // 5
        function _handleImageLoaded() {
            if (!this.$img.classList.contains('loaded')) {
                this.$img.classList.add('loaded');
            }
        }

        // 6
        function _handleInputChange(e) {
            // 6.1
            var file = (undefined !== e)
                ? e.target.files[0]
                : this.$img.files[0];

            var pattern = /image-*/;
            var reader = new FileReader();

            // 6.2
            if (!file.type.match(pattern)) {
                alert('invalid format');
                return;
            }

            if (this.$el.classList.contains('loaded')) {
                this.$el.classList.remove('loaded');
            }

            // 6.3
            reader.onload = _handleReaderLoaded.bind(this);
            reader.readAsDataURL(file);
        }

        // 7
        function _handleReaderLoaded(e) {
            var reader = e.target;
            this.$img.src = reader.result;
            this.$el.classList.add('loaded');
        }
        
        // 8
        window.FileUploader = FileUploader;
        
    } ());

Punto 1: Se declara la función principal recibiendo un parámetro que sería el contenedor del control personalizado. Si la función recibe dicho parámetro se ejecuta la función que se encarga de guardar las variables en memoria y suscribirse a los eventos.

Punto 2: La función init se encarga de almacenar las referencias a los elementos del DOM que se usan en mas de una ocasión. Esto se hace para evitar inspeccionar el DOM contínuamente ya que es una operación costosa en cuanto a rendimiento.
Si el elemento principal this.$el ya había sido definido, entonces se ejecuta la función para eliminar las suscripciones almacenadas en memoria.

Punto 3: Se registran los eventos que necesitamos escuchar para efectuar algo cuando ocurran. En el caso de este plugin, solo necesitamos saber cuando el valor del input ha cambiado y cuando la imagen de la vista previa a cargado por completo.

La función bind(this) que se coloca después del nombre de la función asignada al evento, sirve para que el valor de this siempre haga referencia a contexto de la función FileUploader, de lo contrario su valor sería el del objeto Event que se pasa como parámetro de manera automática.

Punto 4: Eliminamos de la memoria las funciones registradas a los eventos, de esta forma evitamos la creación de zombies o funciones que se quedan en memoria sin que vuelvan a ser llamadas.

Solo las funciones antes mencionadas serán parte del FileUploader cada vez que se cree una nueva instancia, ya que éstas funciones se declararon directamente en la propiedad prototype. Las siguientes funciones sirven para escuchar los eventos, por lo que solo necesitamos accederlas de manera interna (privada).

Punto 5: Cuando la imagen carga por completo se asigna la clase que cambia su opacidad para que se muestre la imagen con una leve animación.

Punto 6: Una vez seleccionado el archivo de la imagen se ejecuta esta función que se encarga de realizar lo siguiente:

Punto 6.1: Se obtiene el objeto File del input.
Punto 6.2: Validamos que el archivo seleccionado sea una imagen.
Punto 6.3: Ejecutamos la función readAsURL del objeto FileReader y suscribimos una función para ejecutarse cuando el archivo termine de leerse (reader.onload).

Punto 7: Una vez que el archivo ha sido leído podemos acceder al valor de la imagen en base64, mismo que usamos para asignar como src de la imagen de vista previa.

Punto 8: Finalmente exponemos la función principal como una propiedad global para que pueda instanciarse desde cualquier parte de tu aplicación.

Una vez terminado el plugin ahora solo que agregarlo al html e iniciarlo.

Abre el archivo index.html y agrega el siguiente código.

        ...
        </label>
    
        <script src="FileUploader.js"></script>
        <script type="text/javascript">
            var fileUploader = new FileUploader('.uploader');
            
            // ó
            
            var fileUploader = new FileUploader();
            fileUploader.init('.uploader');
        </script>
    </body>

Listo, ahora si cargas de nuevo la página en tu navegador y seleccionas una imagen debes poder ver como ésta se muestra dentro del control.


Soporte para Drag & Drop

Ya casi hemos terminado, ahora solo nos queda hacer que el control obtenga el valor de la imagen cuando se arrastre y suelte directamente sobre él.

El primer paso es indicarle al navegador que el control es un elemento con soporte para soltar otros elementos encima y para lograrlo solo necesitamos cancelar el comportamiento por defecto para el evento dragover.

Añade el atributo ondragover="return false" a la etiqueta label de la siguiente forma:

    ...
    <body>
        <label class="uploader" ondragover="return false">

Ahora en el archivo FileUploader.js añade el siguiente código.

    ...
    FileUploader.prototype.suscribe = function() {
      this.$fileInput.addEventListener('change', _handleInputChange.bind(this));
      this.$img.addEventListener('load', _handleImageLoaded.bind(this));
      this.$el.addEventListener('dragenter', _handleDragEnter.bind(this));
      this.$el.addEventListener('dragleave', _handleDragLeave.bind(this));
      this.$el.addEventListener('drop', _handleDrop.bind(this));
    }
    
    FileUploader.prototype.unsuscribe = function() {
      this.$fileInput.removeEventListener('change', _handleInputChange.bind(this));
      this.$img.removeEventListener('load', _handleImageLoaded.bind(this));
      this.$el.removeEventListener('dragenter', _handleDragEnter.bind(this));
      this.$el.removeEventListener('dragleave', _handleDragLeave.bind(this));
      this.$el.removeEventListener('drop', _handleDrop.bind(this));
    }

    function _handleDragEnter(e){
      e.preventDefault();

      if (!this.$el.classList.contains('dragging')) {
          this.$el.classList.add('dragging');
      }
    }

    function _handleDragLeave(e) {
      e.preventDefault();

      if (this.$el.classList.contains('dragging')) {
          this.$el.classList.remove('dragging');
      }
    }

    function _handleDrop(e) {
      e.preventDefault();
      this.$el.classList.remove('dragging');

      this.$img.files = e.dataTransfer.files;
      _handleInputChange.call(this);
    }

Como puedes ver en el código, hemos añadido la suscripción a los eventos para detectar cuando arrastramos algo sobre el elemento principal así como el evento cuando se suelta estando sobre éste.

Para los eventos dragenter y dragleave solamente añadimos y removemos clases para cambiar el aspecto visual e indicar al usuario que se está interactuando con el elemento.

Cuando el usuario suelta (drop) la imagen dentro del control, podemos acceder al objeto Files a través de la propiedad dataTransfer del evento, y como este objeto es igual al que obtenemos cuando se selecciona un archivo directamente, solo asignamos el valor al objeto de imagen de la vista previa (línea 57) y ejecutamos la función que procesa el cambio de ese valor y muestra la imagen (linea 58).

Actualiza la página y arrastra una imagen sobre el elemento para ver el resultado.


El código completo lo puedes consultar a través del demo colocado al principio o accediendo a su enlace directo en plunkr.


Conclusión

Gracias a la constante evolución de las capacidades del navegador como plataforma, es posible añadir a las aplicaciones web funciones y características que antes solo eran posible en aplicaciones nativas, si hay alguna funcionalidad o característica que sea parte de las APIs de HTML5 que te gustaría aprender, no dudes en dejar tus sugerencias en los comentarios.



Publicaciones que pueden interesarte

    Deja un comentario

      tope
    Derechos Reservados, Merida Design 2017
    %d bloggers like this: