Índice
- Eventos de arrastrar y soltar
- Configurando nuestro formulario
- Agregar la funcionalidad de arrastrar y soltar
- Características adicionales
- Conclusión
Es difícil diseñar las entradas de selección de archivos de la manera que los desarrolladores desean, por lo que muchos simplemente las ocultan y crean un botón que abre el cuadro de diálogo de selección de archivos. Sin embargo, hoy en día existe una forma aún más sofisticada de manejar la selección de archivos: arrastrar y soltar. En este artículo, Joseph Zimmerman utilizará JavaScript ES2015+ “vainilla” (sin marcos ni bibliotecas) para completar este proyecto, y se supone que tiene conocimientos prácticos de JavaScript en el navegador. Este ejemplo debería ser compatible con todos los navegadores actuales más IE 10 y 11.
Es un hecho conocido que las entradas de selección de archivos son difíciles de diseñar de la manera que los desarrolladores desean, por lo que muchos simplemente las ocultan y crean un botón que abre el cuadro de diálogo de selección de archivos. Sin embargo, hoy en día tenemos una forma aún más sofisticada de manejar la selección de archivos: arrastrar y soltar.
Técnicamente, esto ya era posible porque la mayoría (si no todas ) las implementaciones de la entrada de selección de archivos le permitían arrastrar archivos sobre él para seleccionarlos, pero esto requiere que realmente muestre el file
elemento. Entonces, usemos las API que nos proporciona el navegador para implementar un selector y cargador de archivos de arrastrar y soltar.
En este artículo, usaremos JavaScript ES2015+ “vainilla” (sin marcos ni bibliotecas) para completar este proyecto, y se supone que tiene conocimientos prácticos de JavaScript en el navegador. Este ejemplo, aparte de la sintaxis de ES2015+, que puede cambiarse fácilmente a la sintaxis de ES5 o transpilarse con Babel, debería ser compatible con todos los navegadores actuales, además de IE 10 y 11.
Aquí hay un vistazo rápido a lo que harás:
Eventos de arrastrar y soltar
Lo primero que debemos discutir son los eventos relacionados con arrastrar y soltar porque son la fuerza impulsora detrás de esta característica. En total, hay ocho eventos que el navegador activa relacionados con arrastrar y soltar: drag
, dragend
, dragenter
, dragexit
, dragleave
, dragover
, dragstart
y drop
. No los repasaremos todos porque drag
, dragend
, dragexit
y dragstart
se activan en el elemento que se está arrastrando y, en nuestro caso, arrastraremos archivos desde nuestro sistema de archivos en lugar de elementos DOM, por lo que estos eventos nunca aparecerá.
Si tienes curiosidad acerca de ellos, puedes leer alguna documentación sobre estos eventos en MDN .
Como es de esperar, puede registrar controladores de eventos para estos eventos de la misma manera que registra controladores de eventos para la mayoría de los eventos del navegador: a través de addEventListener
.
let dropArea = document.getElementById('drop-area') dropArea.addEventListener('dragenter', handlerFunction, false) dropArea.addEventListener('dragleave', handlerFunction, false) dropArea.addEventListener('dragover', handlerFunction, false) dropArea.addEventListener('drop', handlerFunction, false)
Aquí hay una pequeña tabla que describe lo que hacen estos eventos, utilizando dropArea
el ejemplo de código para que el lenguaje sea más claro:
Evento | ¿Cuándo se despide? |
---|---|
dragenter | El elemento arrastrado se arrastra sobre dropArea, lo que lo convierte en el destino del evento de colocación si el usuario lo suelta allí. |
dragleave | El elemento arrastrado se arrastra fuera de dropArea y hacia otro elemento, convirtiéndolo en el destino del evento de colocación. |
dragover | Cada pocos cientos de milisegundos, mientras el elemento arrastrado se encuentra sobre dropArea y se mueve. |
drop | El usuario suelta el botón del mouse y suelta el elemento arrastrado en dropArea. |
Tenga en cuenta que el elemento arrastrado se arrastra sobre un elemento secundario de dropArea
, dragleave
se activará dropArea
y dragenter
se activará en ese elemento secundario porque es el nuevo target
. El drop
evento se propagará hasta dropArea
(a menos que un detector de eventos diferente detenga la propagación antes de llegar allí), por lo que aún se activará dropArea
a pesar de que no sea el evento target
para el evento.
También tenga en cuenta que para crear interacciones personalizadas de arrastrar y soltar, deberá llamar event.preventDefault()
a cada uno de los oyentes para estos eventos. Si no lo hace, el navegador terminará abriendo el archivo que soltó en lugar de enviarlo al drop
controlador de eventos.
Configurando nuestro formulario
Antes de comenzar a agregar la funcionalidad de arrastrar y soltar, necesitaremos un formulario básico con una file
entrada estándar. Técnicamente esto no es necesario, pero es una buena idea proporcionarlo como alternativa en caso de que el usuario tenga un navegador sin soporte para la API de arrastrar y soltar.
div form pUpload multiple files with the file dialog or by dragging and dropping images onto the dashed region/p input type="file" multiple accept="image/*" onchange="handleFiles(this.files)" label for="fileElem"Select some files/label /form/div
Estructura bastante simple. Es posible que observe un onchange
controlador en el archivo input
. Veremos eso más tarde. También sería una buena idea agregar action
un botón form
y submit
para ayudar a aquellas personas que no tienen JavaScript habilitado. Luego puedes usar JavaScript para deshacerte de ellos y obtener una forma más limpia. En cualquier caso, necesitará un script del lado del servidor para aceptar la carga, ya sea algo desarrollado internamente o si está utilizando un servicio como Cloudinary para hacerlo por usted. Aparte de esas notas, no hay nada especial aquí, así que agreguemos algunos estilos:
#drop-area { border: 2px dashed #ccc; border-radius: 20px; width: 480px; font-family: sans-serif; margin: 100px auto; padding: 20px;}#drop-area.highlight { border-color: purple;}p { margin-top: 0;}.my-form { margin-bottom: 10px;}#gallery { margin-top: 10px;}#gallery img { width: 150px; margin-bottom: 10px; margin-right: 10px; vertical-align: middle;}.button { display: inline-block; padding: 10px; background: #ccc; cursor: pointer; border-radius: 5px; border: 1px solid #ccc;}.button:hover { background: #ddd;}#fileElem { display: none;}
Muchos de estos estilos aún no entran en juego, pero está bien. Lo más destacado, por ahora, es que la file
entrada está oculta, pero tiene label
un estilo que parece un botón, por lo que la gente se dará cuenta de que pueden hacer clic en él para abrir el cuadro de diálogo de selección de archivos. También seguimos una convención al delinear el área de colocación con líneas discontinuas.
Agregar la funcionalidad de arrastrar y soltar
Ahora llegamos al meollo de la situación: arrastrar y soltar. Incluyamos un script al final de la página, o en un archivo separado, como le apetezca hacerlo. Lo primero que necesitamos en el script es una referencia al área de colocación para que podamos adjuntarle algunos eventos:
let dropArea = document.getElementById('drop-area')
Ahora agreguemos algunos eventos. Comenzaremos agregando controladores a todos los eventos para evitar comportamientos predeterminados y evitar que los eventos aumenten más de lo necesario:
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName = { dropArea.addEventListener(eventName, preventDefaults, false)})function preventDefaults (e) { e.preventDefault() e.stopPropagation()}
Ahora agreguemos un indicador para que el usuario sepa que efectivamente ha arrastrado el elemento sobre el área correcta usando CSS para cambiar el color del borde del área de colocación. Los estilos ya deberían estar ahí debajo del #drop-area.highlight
selector, así que usemos JS para agregar y eliminar esa highlight
clase cuando sea necesario.
;['dragenter', 'dragover'].forEach(eventName = { dropArea.addEventListener(eventName, highlight, false)});['dragleave', 'drop'].forEach(eventName = { dropArea.addEventListener(eventName, unhighlight, false)})function highlight(e) { dropArea.classList.add('highlight')}function unhighlight(e) { dropArea.classList.remove('highlight')}
Tuvimos que usar ambos dragenter
y dragover
para resaltar por lo que mencioné antes. Si comienza flotando directamente sobre dropArea
uno de sus hijos y luego pasa el cursor sobre uno de sus hijos, dragleave
se activará y se eliminará el resaltado. El dragover
evento se activa después de los eventos dragenter
y dragleave
, por lo que el resaltado se volverá a agregar dropArea
antes de que veamos que se elimina.
También eliminamos el resaltado cuando el elemento arrastrado sale del área designada o cuando suelta el elemento.
Ahora todo lo que tenemos que hacer es descubrir qué hacer cuando se eliminan algunos archivos:
dropArea.addEventListener('drop', handleDrop, false)function handleDrop(e) { let dt = e.dataTransfer let files = dt.files handleFiles(files)}
Esto no nos acerca a la finalización, pero hace dos cosas importantes:
- Muestra cómo obtener los datos de los archivos que se eliminaron.
- Nos lleva al mismo lugar en el que
file
input
estaba suonchange
controlador: esperandohandleFiles
.
Tenga en cuenta que files
no es una matriz, sino un archivo FileList
. Entonces, cuando implementemos handleFiles
, necesitaremos convertirlo en una matriz para poder iterarlo más fácilmente:
function handleFiles(files) { ([...files]).forEach(uploadFile)}
Eso fue decepcionante. Entremos uploadFile
en lo realmente carnoso. Recetas de cocina tradicionales y comodas
function uploadFile(file) { let url = 'YOUR URL HERE' let formData = new FormData() formData.append('file', file) fetch(url, { method: 'POST', body: formData }) .then(() = { /* Done. Inform the user */ }) .catch(() = { /* Error. Inform the user */ })}
Aquí utilizamos FormData
, una API de navegador integrada para crear datos de formulario y enviarlos al servidor. Luego usamos la fetch
API para enviar la imagen al servidor. Asegúrese de cambiar la URL para que funcione con su back-end o servicio, y formData.append
cualquier dato de formulario adicional que pueda necesitar para brindarle al servidor toda la información que necesita. Alternativamente, si desea admitir Internet Explorer, es posible que desee utilizar XMLHttpRequest
, lo que significa uploadFile
que se vería así:
function uploadFile(file) { var url = 'YOUR URL HERE' var xhr = new XMLHttpRequest() var formData = new FormData() xhr.open('POST', url, true) xhr.addEventListener('readystatechange', function(e) { if (xhr.readyState == 4 xhr.status == 200) { // Done. Inform the user } else if (xhr.readyState == 4 xhr.status != 200) { // Error. Inform the user } }) formData.append('file', file) xhr.send(formData)}
Dependiendo de cómo esté configurado su servidor, es posible que desee verificar diferentes rangos de status
números en lugar de solo 200
, pero para nuestros propósitos, esto funcionará.
Características adicionales
Esa es toda la funcionalidad básica, pero a menudo queremos más funcionalidad. Específicamente, en este tutorial, agregaremos un panel de vista previa que muestra todas las imágenes elegidas al usuario, luego agregaremos una barra de progreso que le permitirá al usuario ver el progreso de las cargas. Entonces, comencemos con la vista previa de imágenes.
Vista previa de imagen
Hay un par de formas de hacer esto: puede esperar hasta que se haya cargado la imagen y pedirle al servidor que envíe la URL de la imagen, pero eso significa que debe esperar y las imágenes pueden ser bastante grandes a veces. La alternativa, que exploraremos hoy, es utilizar la API FileReader en los datos del archivo que recibimos del evento. Esto es asincrónico y, alternativamente, podría usar FileReaderSync , pero podríamos estar intentando leer varios archivos grandes seguidos, por lo que esto podría bloquear el hilo durante bastante tiempo y realmente arruinar la experiencia. Entonces, creemos una función y veamos cómo funciona:drop
previewFile
function previewFile(file) { let reader = new FileReader() reader.readAsDataURL(file) reader.onloadend = function() { let img = document.createElement('img') img.src = reader.result document.getElementById('gallery').appendChild(img) }}
Aquí creamos un new FileReader
y readAsDataURL
lo invocamos con el File
objeto. Como se mencionó, esto es asincrónico, por lo que debemos agregar un onloadend
controlador de eventos para obtener el resultado de la lectura. Luego usamos la URL de datos base 64 como src
para un nuevo elemento de imagen y la agregamos al gallery
elemento. Sólo hay dos cosas que deben hacerse para que esto funcione ahora: agregar el gallery
elemento y asegurarse de previewFile
que realmente se llame.
Primero, agregue el siguiente HTML justo después del final de la form
etiqueta:
div/div
Nada especial; es solo un div. Los estilos ya están especificados para él y las imágenes que contiene, por lo que no queda nada por hacer allí. Ahora cambiemos la handleFiles
función a la siguiente:
function handleFiles(files) { files = [...files] files.forEach(uploadFile) files.forEach(previewFile)}
Hay algunas formas en que podría haber hecho esto, como la composición o una única devolución de llamada forEach
que se ejecutó uploadFile
y previewFile
en él, pero esto también funciona. Y con eso, cuando sueltas o seleccionas algunas imágenes, deberían aparecer casi instantáneamente debajo del formulario. Lo interesante de esto es que, en ciertas aplicaciones, es posible que en realidad no desees cargar imágenes, sino almacenar sus URL de datos localStorage
o en algún otro caché del lado del cliente para que la aplicación pueda acceder a ellas más tarde. Personalmente, no puedo pensar en ningún buen caso de uso para esto, pero estoy dispuesto a apostar que hay algunos.
Seguimiento del progreso
Si algo puede tardar un poco, una barra de progreso puede ayudar al usuario a darse cuenta de que realmente se está avanzando y dar una indicación de cuánto tiempo llevará completarlo. Agregar un indicador de progreso es bastante fácil gracias a la progress
etiqueta HTML5. Comencemos agregando eso al código HTML esta vez.
progress max=100 value=0/progress
Puedes colocarlo justo después label
o entre la form
galería y div
, lo que prefieras. De hecho, puedes colocarlo donde quieras dentro de las body
etiquetas. No se agregaron estilos para este ejemplo, por lo que mostrará la implementación predeterminada del navegador, que es útil. Ahora trabajemos para agregar JavaScript. Primero veremos la implementación usando fetch
y luego mostraremos una versión para XMLHttpRequest
. Para comenzar, necesitaremos un par de variables nuevas en la parte superior del script:
let filesDone = 0let filesToDo = 0let progressBar = document.getElementById('progress-bar')
Al usarlo, fetch
solo podemos determinar cuándo finaliza una carga, por lo que la única información que rastreamos es cuántos archivos se seleccionan para cargar (como filesToDo
) y la cantidad de archivos que terminaron de cargarse (como filesDone
). También mantenemos una referencia al #progress-bar
elemento para poder actualizarlo rápidamente. Ahora creemos un par de funciones para gestionar el progreso:
function initializeProgress(numfiles) { progressBar.value = 0 filesDone = 0 filesToDo = numfiles}function progressDone() { filesDone++ progressBar.value = filesDone / filesToDo * 100}
Cuando comencemos a cargar, initializeProgress
se nos llamará para restablecer la barra de progreso. Luego, con cada carga completa, llamaremos progressDone
para incrementar la cantidad de cargas completadas y actualizar la barra de progreso para mostrar el progreso actual. Así que llamemos a estas funciones actualizando un par de funciones antiguas:
function handleFiles(files) { files = [...files] initializeProgress(files.length) // - Add this line files.forEach(uploadFile) files.forEach(previewFile)}function uploadFile(file) { let url = 'YOUR URL HERE' let formData = new FormData() formData.append('file', file) fetch(url, { method: 'POST', body: formData }) .then(progressDone) // - Add `progressDone` call here .catch(() = { /* Error. Inform the user */ })}
Y eso es. Ahora echemos un vistazo a la XMLHttpRequest
implementación. Podríamos simplemente hacer una actualización rápida a uploadFile
, pero XMLHttpRequest
en realidad nos brinda más funcionalidad que fetch
, es decir, podemos agregar un detector de eventos para el progreso de la carga en cada solicitud, lo que periódicamente nos brindará información sobre qué parte de la solicitud ha finalizado. Debido a esto, necesitamos realizar un seguimiento del porcentaje de finalización de cada solicitud en lugar de solo cuántas se realizan. Entonces, comencemos reemplazando las declaraciones de filesDone
y filesToDo
por lo siguiente:
let uploadProgress = []
Entonces necesitamos actualizar nuestras funciones también. Cambiaremos progressDone
el nombre updateProgress
y los cambiaremos para que sean los siguientes:
function initializeProgress(numFiles) { progressBar.value = 0 uploadProgress = [] for(let i = numFiles; i 0; i--) { uploadProgress.push(0) }}function updateProgress(fileNumber, percent) { uploadProgress[fileNumber] = percent let total = uploadProgress.reduce((tot, curr) = tot + curr, 0) / uploadProgress.length progressBar.value = total}
Ahora initializeProgress
inicializa una matriz con una longitud igual a numFiles
la que se llena con ceros, lo que indica que cada archivo está completo al 0%. Descubrimos updateProgress
qué imagen tiene su progreso actualizado y cambiamos el valor en ese índice al proporcionado percent
. Luego calculamos el porcentaje de progreso total tomando un promedio de todos los porcentajes y actualizamos la barra de progreso para reflejar el total calculado. Seguimos llamando initializeProgress
igual handleFiles
que en el fetch
ejemplo, por lo que ahora todo lo que necesitamos actualizar es uploadFile
llamar updateProgress
.
function uploadFile(file, i) { // - Add `i` parameter var url = 'YOUR URL HERE' var xhr = new XMLHttpRequest() var formData = new FormData() xhr.open('POST', url, true) // Add following event listener xhr.upload.addEventListener("progress", function(e) { updateProgress(i, (e.loaded * 100.0 / e.total) || 100) }) xhr.addEventListener('readystatechange', function(e) { if (xhr.readyState == 4 xhr.status == 200) { // Done. Inform the user } else if (xhr.readyState == 4 xhr.status != 200) { // Error. Inform the user } }) formData.append('file', file) xhr.send(formData)}
Lo primero a tener en cuenta es que agregamos un i
parámetro. Este es el índice del archivo en la lista de archivos. No necesitamos actualizar handleFiles
para pasar este parámetro porque está usando forEach
, que ya proporciona el índice del elemento como segundo parámetro para las devoluciones de llamada. También agregamos el progress
detector de eventos xhr.upload
para que podamos llamar updateProgress
con el progreso. El objeto de evento (al que se hace referencia e
en el código) tiene dos datos pertinentes: loaded
que contiene la cantidad de bytes que se han cargado hasta el momento y total
que contiene la cantidad de bytes que tiene el archivo en total.
La || 100
pieza está ahí porque a veces, si hay un error, e.loaded
será e.total
cero, lo que significa que el cálculo saldrá como NaN
, por lo 100
que se usa para informar que el archivo está listo. También podrías usar 0
. En cualquier caso, el error aparecerá en el readystatechange
controlador para que pueda informar al usuario sobre él. Esto es simplemente para evitar que se lancen excepciones al intentar hacer cálculos con NaN
.
Conclusión
Esa es la pieza final. Ahora tiene una página web donde puede cargar imágenes arrastrando y soltando, obtener una vista previa de las imágenes que se cargan inmediatamente y ver el progreso de la carga en una barra de progreso. Puedes ver la versión final (con XMLHttpRequest
) en acción en CodePen , pero ten en cuenta que el servicio al que subo los archivos tiene límites, por lo que si mucha gente lo prueba, puede fallar por un tiempo.
Otras lecturas
- SolidStart: una clase diferente de metamarco
- El desarrollo web se está volviendo demasiado complejo y puede ser culpa nuestra
- Sube de nivel tus habilidades CSS con el selector :has()
- Consejos y trucos útiles de DevTools
(rb, ra, il, mrn)Explora más en
- Mecanografiado
- javascript
- Navegadores
Tal vez te puede interesar:
- ¿Deberían abrirse los enlaces en ventanas nuevas?
- 24 excelentes tutoriales de AJAX
- 70 técnicas nuevas y útiles de AJAX y JavaScript
- Más de 45 excelentes recursos y repositorios de fragmentos de código
Cómo crear un cargador de archivos de arrastrar y soltar con Vanilla JavaScript
Eventos de arrastrar y soltarConfigurando nuestro formularioAgregar la funcionalidad de arrastrar y soltarCaracterísticas adicionalesConclusión¡Registro! C
programar
es
https://aprendeprogramando.es/static/images/programar-como-crear-un-cargador-de-archivos-de-arrastrar-y-soltar-con-vanilla-javascript-926-0.jpg
2024-12-03
Si crees que alguno de los contenidos (texto, imagenes o multimedia) en esta página infringe tus derechos relativos a propiedad intelectual, marcas registradas o cualquier otro de tus derechos, por favor ponte en contacto con nosotros en el mail [email protected] y retiraremos este contenido inmediatamente