Carga de archivos con arrastrar y soltar | Programar Plus

Trabajo en una aplicación de lectura de RSS llamada Readerrr (nota del editor: el enlace se eliminó porque el sitio parece muerto). Quería enriquecer la experiencia de importación de feeds permitiendo la carga de archivos con la función de arrastrar y soltar junto con la entrada de archivos tradicional. A veces, arrastrar y soltar es una forma más cómoda de seleccionar un archivo, ¿no es así?

Ver demostración

Margen

Este marcado no tiene nada que ver específicamente con arrastrar y soltar. Es solo un normal, funcional <form>, aunque con algunos elementos HTML adicionales para estados potenciales.

<form class="box" method="post" action="" enctype="multipart/form-data">
  <div class="box__input">
    <input class="box__file" type="file" name="files[]" id="file" data-multiple-caption="{count} files selected" multiple />
    <label for="file"><strong>Choose a file</strong><span class="box__dragndrop"> or drag it here</span>.</label>
    <button class="box__button" type="submit">Upload</button>
  </div>
  <div class="box__uploading">Uploading…</div>
  <div class="box__success">Done!</div>
  <div class="box__error">Error! <span></span>.</div>
</form>

Ocultaremos esos estados hasta que los necesitemos:

.box__dragndrop,
.box__uploading,
.box__success,
.box__error {
  display: none;
}

Una pequeña explicación:

  • Respecto a los estados: .box__uploading El elemento será visible durante el proceso Ajax de carga de archivos (y los demás aún estarán ocultos). Entonces .box__success o .box__error se mostrará dependiendo de lo que suceda.
  • input[type="file"] y label son las partes funcionales del formulario. Escribí sobre el estilo de estos juntos en mi publicación sobre la personalización de entradas de archivos. En esa publicación también describí el propósito de [data-multiple-caption] atributo. La entrada y la etiqueta también sirven como una alternativa para seleccionar archivos de la forma estándar (o la única forma si no se admite la función de arrastrar y soltar).
  • .box__dragndrop se mostrará si un navegador admite la función de carga de archivos de arrastrar y soltar.

Detección de características

No podemos confiar al 100% en navegadores que admitan la función de arrastrar y soltar. Deberíamos proporcionar una solución alternativa. Y así: detección de características. La carga de archivos de arrastrar y soltar se basa en varias API de JavaScript diferentes, por lo que tendremos que verificarlas todas.

Primero, arrastre y suelte los eventos. Modernizr es una biblioteca en la que puede confiar en lo que respecta a la detección de características. Esta prueba es de ahí:

var div = document.createElement('div');
return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)

A continuación, debemos verificar la interfaz FormData, que es para formar un objeto programático de los archivos seleccionados para que puedan enviarse al servidor a través de Ajax:

return 'FormData' in window;

Por último, necesitamos el objeto DataTransfer. Este es un poco complicado porque no existe una forma a prueba de balas de detectar la disponibilidad del objeto antes de la primera interacción del usuario con la interfaz de arrastrar y soltar. No todos los navegadores exponen el objeto.

Idealmente, nos gustaría evitar UX como …

  • “¡Arrastra y suelta archivos aquí!”
  • [User drags and drops files]
  • “Vaya, no se admite la función de arrastrar y soltar”.

El truco aquí es verificar la disponibilidad de FileReader API justo cuando se carga el documento. La idea detrás de esto es que los navegadores que admiten FileReader apoyo DataTransfer también:

'FileReader' in window

Combinando el código anterior en una función anónima autoinvocada …

var isAdvancedUpload = function() {
  var div = document.createElement('div');
  return (('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)) && 'FormData' in window && 'FileReader' in window;
}();

… nos permitirá realizar una detección eficaz de soporte de funciones:

if (isAdvancedUpload) {
  // ...
}

Con esta función de detección de funciones, ahora podemos hacerles saber a los usuarios que pueden arrastrar y soltar sus archivos en nuestro formulario (o no). Podemos diseñar el formulario agregando una clase en el caso de soporte:

var $form = $('.box');

if (isAdvancedUpload) {
  $form.addClass('has-advanced-upload');
}
.box.has-advanced-upload {
  background-color: white;
  outline: 2px dashed black;
  outline-offset: -10px;
}
.box.has-advanced-upload .box__dragndrop {
  display: inline;
}

No hay ningún problema si no se admite la carga de archivos con la función de arrastrar y soltar. Wsers podrá cargar archivos a través de buenos input[type="file"]!

Nota sobre la compatibilidad con el navegador: Microsoft Edge tiene un error que impide que funcione la función de arrastrar y soltar. Parece que lo saben y esperan solucionarlo. (Actualización: el enlace al error se eliminó cuando el enlace dejó de funcionar. Ahora que Edge es Chromium, presumiblemente, ya no es un problema).

Arrastrar y soltar

Aquí vamos, aquí está lo bueno.

Esta parte trata de agregar y eliminar clases al formulario en los diferentes estados, como cuando el usuario arrastra un archivo sobre el formulario. Luego, captura esos archivos cuando se caen.

if (isAdvancedUpload) {

  var droppedFiles = false;

  $form.on('drag dragstart dragend dragover dragenter dragleave drop', function(e) {
    e.preventDefault();
    e.stopPropagation();
  })
  .on('dragover dragenter', function() {
    $form.addClass('is-dragover');
  })
  .on('dragleave dragend drop', function() {
    $form.removeClass('is-dragover');
  })
  .on('drop', function(e) {
    droppedFiles = e.originalEvent.dataTransfer.files;
  });

}
  • e.preventDefault() y e.stopPropagation() evitar comportamientos no deseados para los eventos asignados en los navegadores.
  • e.originalEvent.dataTransfer.files devuelve la lista de archivos que se eliminaron. Pronto verá cómo utilizar los datos para enviar estos archivos al servidor.

Agregar y quitar .is-dragover cuando sea necesario, nos permite indicar visualmente cuándo es seguro para un usuario soltar los archivos:

.box.is-dragover {
  background-color: grey;
}

Seleccionar archivos de forma tradicional

A veces, arrastrar y soltar archivos no es una forma muy cómoda de seleccionar archivos para cargar. Especialmente cuando un usuario está frente a una computadora con pantalla pequeña. Por lo tanto, sería bueno dejar que los usuarios elijan el método que prefieran. La entrada del archivo y la etiqueta están aquí para permitir esto. Diseñarlos a ambos de la forma que he descrito nos permite mantener la interfaz de usuario consistente:

Subida de Ajax

No existe una forma cruzada de navegador para cargar archivos arrastrados y soltados sin Ajax. Algunos navegadores (IE y Firefox) no permiten configurar el valor de la entrada de un archivo, que luego podría enviarse al servidor de la manera habitual.

Esta no funcionará:

$form.find('input[type="file"]').prop('files', droppedFiles);

En su lugar, usaremos Ajax cuando se envíe el formulario:

$form.on('submit', function(e) {
  if ($form.hasClass('is-uploading')) return false;

  $form.addClass('is-uploading').removeClass('is-error');

  if (isAdvancedUpload) {
    // ajax for modern browsers
  } else {
    // ajax for legacy browsers
  }
});

El .is-uploading La clase cumple una doble función: evita que el formulario se envíe repetidamente (return false) y ayuda a indicar a un usuario que el envío está en curso:

.box.is-uploading .box__input {
  visibility: none;
}
.box.is-uploading .box__uploading {
  display: block;
}

Ajax para navegadores modernos

Si este fuera un formulario sin la carga de un archivo, no necesitaríamos tener dos técnicas Ajax diferentes. Desafortunadamente, la carga de archivos a través de XMLHttpRequest en IE 9 y anteriores no es compatible.

Para distinguir qué método Ajax funcionará, podemos usar nuestro isAdvancedUpload prueba, porque los navegadores que admiten lo que escribí antes, también admiten la carga de archivos a través de XMLHttpRequest. Aquí está el código que funciona en IE 10+:

if (isAdvancedUpload) {
  e.preventDefault();

  var ajaxData = new FormData($form.get(0));

  if (droppedFiles) {
    $.each( droppedFiles, function(i, file) {
      ajaxData.append( $input.attr('name'), file );
    });
  }

  $.ajax({
    url: $form.attr('action'),
    type: $form.attr('method'),
    data: ajaxData,
    dataType: 'json',
    cache: false,
    contentType: false,
    processData: false,
    complete: function() {
      $form.removeClass('is-uploading');
    },
    success: function(data) {
      $form.addClass( data.success == true ? 'is-success' : 'is-error' );
      if (!data.success) $errorMsg.text(data.error);
    },
    error: function() {
      // Log the error, show an alert, whatever works for you
    }
  });
}
  • FormData($form.get(0)) recopila datos de todas las entradas del formulario
  • El $.each() bucle se ejecuta a través de los archivos arrastrados y soltados. ajaxData.append() los agrega a la pila de datos que se enviará a través de Ajax
  • data.success y data.error son una respuesta en formato JSON que se devolverá desde el servidor. Así sería en PHP:
<?php
  // ...
  die(json_encode([ 'success'=> $is_success, 'error'=> $error_msg]));
?>

Ajax para navegadores heredados

Esto es esencialmente para IE 9-. No necesitamos recopilar los archivos arrastrados y soltados porque en este caso (isAdvancedUpload = false), el navegador no admite la carga de archivos de arrastrar y soltar y el formulario se basa solo en el input[type="file"].

Curiosamente, apuntar al formulario en un iframe insertado dinámicamente funciona:

if (isAdvancedUpload) {
  // ...
} else {
  var iframeName="uploadiframe" + new Date().getTime();
    $iframe   = $('<iframe name="' + iframeName + '" style="display: none;"></iframe>');

  $('body').append($iframe);
  $form.attr('target', iframeName);

  $iframe.one('load', function() {
    var data = JSON.parse($iframe.contents().find('body' ).text());
    $form
      .removeClass('is-uploading')
      .addClass(data.success == true ? 'is-success' : 'is-error')
      .removeAttr('target');
    if (!data.success) $errorMsg.text(data.error);
    $form.removeAttr('target');
    $iframe.remove();
  });
}

Envío automático

Si tiene un formulario simple con solo un área de arrastrar y soltar o una entrada de archivo, puede ser conveniente para el usuario evitar tener que presionar el botón. En su lugar, puede enviar automáticamente el formulario en la colocación / selección de archivos activando el submit evento:

// ...

.on('drop', function(e) { // when drag & drop is supported
  droppedFiles = e.originalEvent.dataTransfer.files;
  $form.trigger('submit');
});

// ...

$input.on('change', function(e) { // when drag & drop is NOT supported
  $form.trigger('submit');
});

Si el área de arrastrar y soltar está visualmente bien diseñada (es obvio para el usuario qué hacer), podría considerar ocultar el botón de envío (menos UI puede ser bueno). Pero tenga cuidado al esconder un control como ese. El botón debe ser visible y funcional si por alguna razón JavaScript no está disponible (¡mejora progresiva!). Añadiendo un .no-js nombre de clase a y eliminarlo con JavaScript hará el truco:

<html class="no-js">
  <head>
    <!-- remove this if you use Modernizr -->
    <script>(function(e,t,n){var r=e.querySelectorAll("html")[0];r.className=r.className.replace(/(^|s)no-js(s|$)/,"$1js$2")})(document,window,0);</script>
  </head>
</html>
.box__button {
  display: none;
}
.no-js .box__button {
  display: block;
}

Visualización de los archivos seleccionados

Si no va a realizar el envío automático, debe haber una indicación para el usuario si ha seleccionado los archivos correctamente:

var $input    = $form.find('input[type="file"]'),
    $label    = $form.find('label'),
    showFiles = function(files) {
      $label.text(files.length > 1 ? ($input.attr('data-multiple-caption') || '').replace( '{count}', files.length ) : files[ 0 ].name);
    };

// ...

.on('drop', function(e) {
  droppedFiles = e.originalEvent.dataTransfer.files; // the files that were dropped
  showFiles( droppedFiles );
});

//...

$input.on('change', function(e) {
  showFiles(e.target.files);
});


Cuando JavaScript no está disponible

La mejora progresiva se trata de la idea de que un usuario debería poder completar las tareas principales en un sitio web sin importar qué. La carga de archivos no es una excepción. Si por alguna razón JavaScript no está disponible, la interfaz se verá así:

La página se actualizará al enviar el formulario. Nuestro JavaScript para indicar el resultado del envío es inútil. Eso significa que tenemos que confiar en la solución del lado del servidor. Así es como se ve y funciona en la página de demostración:

<?php

  $upload_success = null;
  $upload_error="";

  if (!empty($_FILES['files'])) {
    /*
      the code for file upload;
      $upload_success – becomes "true" or "false" if upload was unsuccessful;
      $upload_error – an error message of if upload was unsuccessful;
    */
  }

?>

Y algunos ajustes para el marcado:

<form class="box" method="post" action="" enctype="multipart/form-data">

  <?php if ($upload_success === null): ?>

  <div class="box__input">
    <!-- ... -->
  </div>

  <?php endif; ?>

  <!-- ... -->

  <div class="box__success"<?php if( $upload_success === true ): ?> style="display: block;"<?php endif; ?>>Done!</div>
  <div class="box__error"<?php if( $upload_success === false ): ?> style="display: block;"<?php endif; ?>>Error! <span><?=$upload_error?></span>.</div>

</form>

¡Eso es! Este artículo, que ya es extenso, podría haber sido aún más extenso, pero creo que esto le permitirá comenzar con una función de carga de archivos responsable de arrastrar y soltar en sus propios proyectos.

Consulte la demostración para obtener más información (consulte la fuente para ver el JavaScript sin dependencia de jQuery):

Ver demostración

(Visited 6 times, 1 visits today)