DEV Community

Cover image for Creando un drag and drop con javascript y css y algo de python.
Hermann Pollack (hpollack95)
Hermann Pollack (hpollack95)

Posted on

Creando un drag and drop con javascript y css y algo de python.

Las aplicaciones web han evolucionado tanto que hoy no solo tenemos, herramientas y frameworks e distintos lenguajes, si no que además, se han hecho mas amigables con el usuario, si lo comparamos como era en los 2000.

Pues bien, una herramienta útil, es el "arrastrar y soltar". Este concepto no es nuevo, y han existido librerías Javascript para crear el efecto. Sin embargo, desde las especificaciones de Ecmascript 6, el usar Javascript puro (Vanilla Javascript para los conocedores), no solo se ha vuelto más fácil, si no que no necesitas depender de extensiones o librerías externas, como lo fue por ejemplo JQuery.

Como ejemplo, usaré un proyecto personal que he estado desarrollando este tiempo. Sin embargo, el concepto en general, se puede aplicar a cualquier aplicación web que estén desarrollando.

Estructura base.

HTML.

Vamos a crear el campo donde arrastraremos el archivo, en este caso, la imagen.

 <div class="drop-zone" id="fotoperfil"> <span class="fa fa-upload"> Arrastre la foto o haga click para seleccionar archivos</span> </div> <input type="file" name="imagen" id="imagen" class="form-control"> 
Enter fullscreen mode Exit fullscreen mode

El código de arriba, crea un <div> con la clase .drop-zone y el id fotoperfil, que es la que manejará el evento de arastre. Fuera de este, colocaremos el <input> con la propiedad file, el cual tendra el id imagen. Teniendo definido esto, le damos estilo con CSS.

En la zona donde se verá la imagen una vez cargada, al ser Flask, nuestro backend, usamos el motor Jinja2, para renderizar. Esto debe quedar asi

<div class="card-body profile-card pt-4 d-flex flex-column align-items-center"> {% for user in datausuario %} <!-- Aquí validamos si existe la url de la imagen. Si es el caso, se muestra --> {% if user[13] %} <img src="{{ url_for('static', filename='img/profile/' ~ user[13]) }}" alt="Profile" class="rounded-5 img-fluid profile-image"> {% else %} <img src="{{ url_for('static', filename='img/profile/avatar.png') }}" alt="Profile" class="rounded-5 img-fluid"> {% endif %} <h2>{{ user[1] }} {{ user[2] }}</h2> <h3>{{ user[3] }} </h3> {% if user[13] %} <button type="button" class="btn btn-danger mb-4" id="quitaimagen"><i class="fa fa-times"></i> Eliminar imagen</button> {% endif %} <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#nuevaimagen"><i class="fa fa-upload"></i> Cargar Nueva Imagen</button> {% endfor %} </div> 
Enter fullscreen mode Exit fullscreen mode

CSS.

Procedemos a darle estilo.

.drop-zone { height: 150px; border: 2px dashed #aaa; border-radius: 10px; display: flex; align-items: center; justify-content: center; color: #666; cursor: pointer; transition: 0.2s; } .drop-zone.draggable { border-color: #333; background-color: #fff; } /* Si solo quieres que sea un campo y no todos en el proyecto, puedes utilizar el id del campo */ input[type="file"] { display: none; } 
Enter fullscreen mode Exit fullscreen mode

En .drop-zone definimos el alto que tendrá el campo -el ancho lo dejamos a criterio-. con un borde segmentado de dos pixeles y un color gris. Le añadimos el cursor pointer -👆- para darle la visualización de que el campo es mas grande.

El truco de esto es que ocultamos el campo tipo "file", que es el que recibe la imagen. En este caso, el campo va dentro de un Modal de Bootstrap 5

modal con drop zone

Javascript.

Ahora, hay que programar el evento. Lo primero es definir los selectores html.

const zonaFoto = document.querySelector('#fotoperfil') const campoFoto = document.querySelector('#imagen') 
Enter fullscreen mode Exit fullscreen mode

Con estas constantes, escogemos #fotoperfil del div de la zona de arrastre vista mas arriba e #imagen, del campo tipo "file".

Lo siguiente es definir la función que tomara los archivos. Esto se puede hacer al final de script.

function tomaArchivos(archivos) { const transfer = new DataTransfer() for (const archivo of archivos) { if (archivo.type.startsWith('image/')) { const lector = new FileReader() lector.onload = function(e) { zonaFoto.style.backgroundImage = `url(${e.target.result})` zonaFoto.style.bacgroundSize = 'contain' zonaFoto.style.bacgroundPosition = 'center' zonaFoto.style.bacgroundRepeat = 'no-repeat' } lector.readAsDataURL(archivo) transfer.items.add(archivo) } else { alerta(3, 'Solo se permiten imagenes') } } campoFoto.files = transfer.files console.log(campoFoto.files) } 
Enter fullscreen mode Exit fullscreen mode

La función tomaArchivos() es la que evaluará y procesará la imagen, usando el método startsWith() con el tipo mime image/. Este solo dejará pasar los archivos que sean tipo imagen (se puede incluso definir la extensión de esta forma image/png si solo quieres de ese tipo).

El FileReader(), nos mostrará la imagen cargada en el campo de arrastre. Fuera del bucle for le pasamos la información con DataTransfer() al campo oculto.

Por último, nos queda definir los eventos.

// Creamos el evento click para el div drop-zone zonaFoto.addEventListener('click', () => { campoFoto.click() }) // Los dos eventos siguientes crean y deshacen el arrastrar zonaFoto.addEventListener('dragover', (e) => { e.preventDefault() e.stopPropagation() zonaFoto.classList.add('draggable') }) zonaFoto.addEventListener('dragleave', () =>{ zonaFoto.classList.remove('draggable') }) // Una vez que la imagen se deposita en la zona, se remueve la clase que genera el drag, toma el archivo y lo deja en el campo tipo file zonaFoto.addEventListener('drop', (e) => { e.preventDefault() e.stopPropagation() zonaFoto.classList.remove('draggable') const foto = e.dataTransfer.files tomaArchivos(foto) }) // Por último, el campo file, detecta si hubo un cambio y toma la imagen guardándola. campoFoto.addEventListener('change', () => { tomaArchivos(campoFoto.files) }) 
Enter fullscreen mode Exit fullscreen mode

Estos eventos, son dragover, que crea el "arrastrar" y dragleave que lo quita, esto moviendo la clase .dragabble, sea el caso. El siguiente, drop, desactiva el arrastrar, dejando el archivo depositado en la zona y transfiriéndolo al campo tipo file, que detecta el cambio y lo guarda.

Con esto, ahora creamos el evento para subirlo.

/* En este evento, invocaremos al método fetch API que subirá la imagen mas un dato adicional, esto para que el backend lo tome y procese */ document.getElementById('uploadimagen').addEventListener('submit', function(e) { e.preventDefault() cargaLoader() const form = new FormData() form.append('file', document.querySelector('input[type="file"]').files[0]) form.append('idusuario', usuario) fetch('/subirimagen', { method : 'POST', body : form }) .then(response => response.json()) .then(data => { modal.hide() if (data.resp == 1) { cierraLoader() jconfirm({ title: 'Realizado', content : data.msg, type : 'green', buttons : { aceptar : { btnClass : 'btn-success ripple', action : () => { window.location.reload() } } } }) } else { cierraLoader() alerta(3, data.msg) } }) .catch(error => { console.log(error) }) }) 
Enter fullscreen mode Exit fullscreen mode

Con este evento en el botón, el formulario donde se encuentra el drag and drop, subirá la imagen al backend.

Backend Python Flask.

Ahora iremos por el lado del backend. Ya definidos los eventos en el front, ahora debemos procesar la imagen que esta ya cargada en buffer.

@user.route('/subirimagen', methods = ['POST']) def upload_image(): if request.method == 'POST': idusuario = request.form['idusuario'] nombrearchivo = '' if 'file' not in request.files and 'idusuario' not in request.form: return json.dumps({'resp' : 0, 'msg' : 'No se encontraron datos'}) else: file = request.files['file'] if file.filename == '': return json.dumps({'resp' : 0, 'msg' : 'No se ha seleccionado ningun archivo'}) else: # Esta esa funcion aparte, que se describe mas abajo.  extension = Utils.validar_extension(file.filename) if not extension: return json.dumps({'resp' : 0, 'msg' : 'El archivo no es permitido'}) else: extension = file.filename.rsplit('.', 1)[1].lower() if extension not in {'png', 'jpg', 'jpeg'}: return json.dumps({'resp' : 0, 'msg' : 'Este archivo no es una imagen valida'}) directorio = UPLOAD_PROFILE_IMAGE upload = os.path.join(directorio, file.filename) try: file.save(upload) nombrearchivo = file.filename # Una vez arriba el archivo, se procede a guardar su nombre y extensión.  resp = usuarioModel.update_url_img_profile(idusuario, nombrearchivo) if resp == 1: Utils.set_history(session['usuario'][0], 'paginausuario', f'Se subio foto de perfil para usuario {str(idusuario)}') return json.dumps({'resp' : 1, 'msg' : 'Imagen subida correctamente'}) else: return json.dumps({'resp' : 0, 'msg' : 'Ocurrio un error al intentar subir el archivo'}) except Exception as e: Utils.errorLog(f'Error al guardar el archivo {str(e)}') return json.dumps({'resp' : 0, 'msg' : f'Error al guardar el archivo'}) 
Enter fullscreen mode Exit fullscreen mode

En este controlador, validamos la imagen y el parámetro asociado, para entregarselo al model que ingresara la url a la base de datos.

Definimos una constante UPLOAD_PROFILE_IMAGE, que guarda la url donde quedara. Se pone por lo general en el encabezado antes de las funciones o en caso de ser OPP, fuera de la clase. Para este caso, usaremos una ruta estática definida en el backend.

UPLOAD_PROFILE_IMAGE = 'static/img/profile/' 
Enter fullscreen mode Exit fullscreen mode

Lo otro a considerar es Utils.validar_extension(file.filename) que es una función parte de una librería interna para validar las extensiones almacenadas en un diccionario. Se detalla a continuación.

#Utils.py # Aqui se definen las extensiones permitidas. ALLOWED_EXTENSIONS = {'xslx', 'png', 'jpg', 'jpeg'} # Funcion que valida si la extensión está dentro del diccionario. def validar_extension(archivo): return '.' in archivo and archivo.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS 
Enter fullscreen mode Exit fullscreen mode

En cuanto al modelo, el método será de actualización del campo en específico, donde el parámetro idusuario es el identificador.

def update_url_img_profile(idusuario, ruta): conexion = conn() string = "update public.usuarios set imagenperfil = %s where id = %s;" try: sql = conexion.cursor() sql.execute(string, (ruta, idusuario)) sql.close() conexion.commit() return 1 except psycopg2.DatabaseError as e: conexion.rollback() Utils.errorLog(str(e)) return 0 finally: conexion.close() 
Enter fullscreen mode Exit fullscreen mode

El resultado final será:

  1. Se podrá arrastrar y soltar una imagen dentro del recuadro.
  2. Se subirá al backend, guardando la imagen y mostrándola en la zona donde va el avatar.

El resultado, pueden verlo en el siguiente video.

Top comments (0)