Hoy vamos a ver varias formas como se puede mejorar un formulario para subir archivos a una página web. La forma mas simple de hacerlo es añadir un nuevo INPUT tipo file cada vez que el usuario selecciona un archivo. Así el usuario puede añadir uno por uno el número de archivos que quiera. Hay dos problemas con esta solución:

  1. Es una tarea muy pesada seleccionar muchos archivos, ya que los navegadores sólo permiten seleccionar un archivo por cada INPUT.
  2. Puede ser difícil comprobar que archivos están seleccionados en cada uno de los INPUTs. Firefox, por ejemplo, muestra la ruta entera del archivo en el INPUT. El resultado es que vemos el principio de un texto tipo 'C:/Users/....' pero lo que de verdad nos interesa, el final con el nombre del archivo, no se ve.

Subir varios archivos

Primero intentamos solucionar el problema 2, ya que es bastante fácil. ¿Que vamos a hacer? Pues, cuando el usuario selecciona un archivo, escondemos el INPUT y creamos un nuevo elemento como un SPAN o LI para mostrar sólo el nombre del archivo seleccionado. Este truco no cambia el comportamiento del formulario, porque los INPUTs siguen allí, simplemente no se ven. En caso de que el usuario cambie de opinión y no quiera subir un archivo ya seleccionado, le dejamos borrarlo con un enlace que simplemente elimina el INPUT. Puedes verlo en acción en el ejemplo 1.

La funcionalidad básica de este ejemplo viene del objeto demo.base, creado en la linea 33 del código ejemplo. Lo primero que hacemos es crear un INPUT:


creaFileInputVacio: function(){
var nuevo_archivo = document.createElement('input');
nuevo_archivo.name = "archivo[]";
nuevo_archivo.type = "file";
var that = this;
nuevo_archivo.onchange = function(){return that.haSeleccionadoUnArchivo(this)};

var form = document.getElementById(this.formulario_id);
form.getElementsByTagName('fieldset')[0].appendChild(nuevo_archivo);
}

Este INPUT llama a nuestro método haSeleccionadoUnArchivo cuando el usuario ha seleccionado un archivo. En este método hacemos 3 cosas: Creamos el elemento con el nombre del archivo (lo llamamos miniatura en el código), escondemos el INPUT y creamos un nuevo INPUT vacío para que el usuario pueda seleccionar otros archivos.


creaMiniaturaDescripcion: function(miniatura, elemento){
miniatura.innerHTML = elemento.value;
},
creaMiniatura: function(elemento){
var miniatura = document.createElement('li');
this.creaMiniaturaDescripcion(miniatura, elemento);

var enlaceEliminar = document.createElement('a');
enlaceEliminar.href = '#';
enlaceEliminar.innerHTML = 'Eliminar';
enlaceEliminar.onclick = function(){
miniatura.parentNode.removeChild(miniatura);
return false;
};
miniatura.appendChild(enlaceEliminar);

var archivos_miniaturas = this.childrenByClass(document.getElementById(this.formulario_id), 'archivos_miniaturas')[0];
return archivos_miniaturas.appendChild(miniatura);
}

En el código de arriba vemos los métodos creaMiniaturaDescripcion y creaMiniatura. Lo importante para nosotros pasa en creaMiniaturaDescripcion. Leemos el nombre del archivo seleccionado con la propiedad value del elemento INPUT y lo metemos dentro de nuestra miniatura. Si el usuario decide no subir el archivo, puede eliminarlo usando el enlace creado en creaMiniatura.

Seleccionar varios archivos a la vez

Bien, hemos conseguido que el usuario puede comprobar mejor los archivos que ya ha seleccionado, pero nos queda el problema 1, que seleccionar muchos archivos de esta manera es muy pesado. Podríamos usar un pequeño programa en Flash pero hoy me gustaría mostrar otra solución sólo usando HTML5 y Javascript, que funciona en las últimas versiones de Firefox y Google Chrome. Si usamos un navegador sin soporte de HTML5 la funcionalidad será la misma como en el ejemplo 1 de arriba.

¿Porque usar algo que no funciona en IE, ya que todavía forma la mayor parte del mercado de navegadores? Bueno, la solución presentada aquí no será apta en cualquier caso. Si sabes que la mayoría de tus usuarios están usando IE, mejor usar Flash o algo parecido. Pero yo hace poco estuve mejorando la edición de plantillas en 3sellers. Un usuario normal de 3sellers no va a editar el código de su plantilla. Es una sección a donde normalmente sólo acceden los diseñadores. Y como es mucho mas probable que un diseñador web usa Firefox que IE, la solución presentada aquí nos ha resultado mucho mas sencilla.

En teoría lo único que tenemos que hacer es añadir el atributo multiple al INPUT. Con esto, un navegador con soporte de HTML5 permitirá al usuario seleccionar varios archivos. Un navegador sin soporte simplemente se comportará como siempre. Esta es la razón por la que a mi me gusta tanto. Con un simple cambio en nuestro HTML facilitamos subir muchos archivos sin cambiar el comportamiento de nuestra web. Puedes ver como funciona (usando Firefox 3.6, por ejemplo) en el ejemplo 2.

En el ejemplo sobreescribimos el método para crear el INPUT de forma que lo crea con multiple activado.


demo.ejemplo2.creaFileInputVacio = function(){
var nuevo_archivo = document.createElement('input');
nuevo_archivo.name = "archivo[]";
nuevo_archivo.type = "file";
nuevo_archivo.multiple = "multiple"; // Con esto el navegador, si tiene soporte, deja al usuario seleccionar varios archivos.
var that = this;
nuevo_archivo.onchange = function(){return that.haSeleccionadoUnArchivo(this)};

var form = document.getElementById(this.formulario_id);
form.getElementsByTagName('fieldset')[0].appendChild(nuevo_archivo);
};

El otro cambio en el código está en la generación de la miniatura. Como ahora es posible que el usuario haya seleccionado mas que un archivo, es posible que tengamos que mostrar mas que un nombre en el listado.


demo.ejemplo2.creaMiniaturaDescripcion = function(miniatura, elemento){
if (typeof(elemento.files) == 'undefined') {
// Navegador sin soporte para seleccionar varios archivos
miniatura.innerHTML = elemento.value;
} else {
var nombres = [];
for (var i = 0; i < elemento.files.length; i++) {
nombres[i] = typeof(elemento.files[i].name) == 'undefined' ? elemento.files[i].fileName : elemento.files[i].name;
}
miniatura.innerHTML = nombres.join(', ');
}
};

En Firefox y Chrome un elemento INPUT con soporte para múltiples archivos tiene una propiedad files (que es simplemente una lista de objetos). En el código de arriba comprobamos primero si esta propiedad existe. Si no existe, suponemos que el navegador no funciona con HTML5 y simplemente accedemos a la propiedad value para coger el nombre del archivo. Si files existe, recorremos esta lista, obtenemos el nombre de cada archivo, y los juntamos para mostrar en la miniatura.

Mostrar vistas previas de imágenes seleccionadas

En el ejemplo 3 podemos ver otra variación de la misma funcionalidad. Esta vez mostramos una pequeña vista previa para cada imagen seleccionada. Aquí utilizamos otra funcionalidad nueva (por ahora parece que sólo funciona con Firefox) que permite leer archivos seleccionados con Javascript.


demo.ejemplo3.creaVistaPrevia = function(miniatura, archivo){
// Comprobamos si podemos acceder al contenido del archivo (ver https://developer.mozilla.org/en/DOM/File) y si el archivo es una imagen
if (!(typeof(archivo.getAsDataURL) != 'undefined' && archivo.type.match(/image.*/))) {
return false;
}

var img = document.createElement("img");
img.style.maxWidth = '150px';
img.style.maxHeight = '150px';
miniatura.appendChild(img);

// Lee la imagen de forma asincrónica. Ver https://developer.mozilla.org/en/Using_files_from_web_applications#Example.3a.c2.a0Showing_thumbnails_of_user-selected_images
var reader = new FileReader();
reader.onloadend = (function(aImg) { return function(e) { aImg.src = e.target.result; }; })(img);
reader.readAsDataURL(archivo);

return true;
};

En el código de arriba vemos como lo hacemos. La función creaMiniaturaDescripcion llama a creaVistaPrevia intentando crear la vista previa, pasando como argumentos el elemento que contiene la miniatura y el archivo seleccionado por el usuario. Primero comprobamos que podemos leer el contenido de este archivo (getAsDataURL) y si el archivo es una imagen (archivo.type.match(/image.*/)). Si no lo podemos leer o el archivo no es una imagen, devolvemos false y creaMiniaturaDescripcion pondrá simplemente el nombre del archivo como hacía antes.

Si el archivo es una imagen, creamos primero el elemento IMG para mostrarla. Le ponemos un ancho y alto máximo para no desmontar el formulario por culpa de imágenes grandes. Luego usamos una técnica mostrada en el blog de desarrolladores de Mozilla. Un objeto FileReader nos permite leer el archivo de forma asincrónica. Con esto el navegador del usuario no se queda bloqueado mientras generamos las vistas previas.

Al FileReader le pasamos en onloadend una función que llamará cuando haya leído el archivo entero. Lo único que hacemos en esta función es aplicar la imagen leída al elemento IMG creado antes, y ya puede ver el usuario la vista previa de la imagen.