JavaScript reordenando lista arrastrando y soltando

8 minutos

Javascript

Una funcionalidad que más aprecio cuando estoy editando un contenido es poder arrastrar y soltar, o Drag and Drop, para ordenar una lista de elementos. Por ello he creado un sistema en JavaScript Vainilla para generar una lista a partir de cualquier Array que puede ser reordenado arrastrando y soltando.

El HTML no tiene ninguna complejidad. Una simple lista desordenada.

<ul id="list" class="menu-list"></ul>

El CSS solo te destaco zone que definirá la zona donde se puede soltar el elemento.

li {
  background: #209cee;
  color: white;
  padding: 1rem;
  border: 1px solid black;
  transition: 0.2s all;
  cursor: move;
}
li.zone {
  opacity: 0.8;
  height: 3rem;
}

Y aquí puedes leer todo el JavaScript. Las única variables destacables son listElements que debe ser el Array que pretendes ordenar y menuList que conecta con el HTML.

/*
 * Variables
 */
// Conjunto de datos a orden arrastrando y soltando
let listElements = [
    "gato 🐈",
    "loro 🦜",
    "elefante 🐘",
    "serpiente 🐍"
];
// Elemento <ul> que será ordenado
const menuList = document.querySelector("#list");
// --- Funcionalidad interna -- //
// Elementos <li> que será creados dentro de la lista desordenada
let menuItems = [];
// Clase que será usada para marcar la zona donde se puede soltar el elemento arrastable
const classZone = "zone";
/*
 * Funciones
 */
/**
 * Renderiza los elementos <li> en cada cambio de datos
 */
function renderUpdateList(list, target) {
    // Limpia todos los <li> anteriores
    target.textContent = "";
    // Vacia el array donde se guardarán los objetos <li>
    menuItems = [];
    // Se itera cada elemento de la lista para crear un <li>
    list.forEach((value, index) => {
        const myLi = document.createElement("li");
        myLi.textContent = value;
        myLi.setAttribute("draggable", "true");
        // Se añade una propiedad data-key con la posición de list para manipularla en el futuro
        myLi.dataset.key = index;
        // Si existe un elemento undefined, se añade una clase para marcarlo como zona soltable
        if (value === undefined)
            myLi.classList.add(classZone);
        // Si se esta arrastrando el elemento, no se renderiza su <li>
        if (myDragElement !== undefined &&
            myDragElement.dataset.key ==
                (eventDragOverIndex < index ? index - 1 : index))
            myLi.style.display = "none";
        // Eventos
        myLi.addEventListener("drop", eventDrop);
        myLi.addEventListener("dragover", eventDragOver);
        // Se añade al documento
        target.appendChild(myLi);
        // Se guarda en menuItems para gestionar
        menuItems.push(myLi);
    });
}
/**
 * Devuelve una copia de la lista donde se ha movidoun indice a otra posicion.
 * @param {number} indexFrom
 * @param {number} indexTo
 * @param {Array<any>} list
 * @return {Array<any>}
 */
function arrayMoveIndex(indexFrom, indexTo, list) {
    // Guarda el valor a mover
    const moveValue = list[indexFrom];
    // Borra de la lista el valor a mover
    const listNotValue = list.filter((currentValue, currentIndex) => currentIndex != indexFrom);
    // Concadena todos los fragmentos
    return listNotValue
        .slice(0, indexTo)
        .concat(moveValue, listNotValue.slice(indexTo));
}
/**
 * Añade en un array un valor a una posición concreta
 * @param {number} index
 * @param {any} value
 * @param {Array<any>} list
 * @return {Array<any>}
 */
function arrayAddValuePosition(index, value, list) {
    // Concat all fragments: start to position + moveValue + rest array
    return list.slice(0, index).concat(value, list.slice(index));
}
/*
 * Eventos Drag and drop
 */
// Drag Start - <li> que se esta arrastrando.
let myDragElement = undefined;
menuList.addEventListener("dragstart", (event) => {
    // Saves which element is moving.
    myDragElement = event.target;
    // Safari fix
    //event.dataTransfer.setData('text/html', myDragElement.innerHTML);
    //event.dataTransfer.setData("text/plain", event.target.textContent);
});
// Drag over - <li> que esta debajo del elemento que se esta arrastrando.
let eventDragOverIndex = -1;
function eventDragOver(event) {
    event.preventDefault();
    // Añade un elemento undefined en el mismo indice donde se esta arrastando con el objetivo de mostrar donde se puede soltar.
    // Guarda el indice
    eventDragOverIndex = event.target.dataset.key;
    // Quita cualquier undefined anteriores
    listElements = listElements.filter((item) => item !== undefined);
    // Añade undefined en la posición donde se encuentra el arrastre
    listElements = arrayAddValuePosition(event.target.dataset.key, undefined, listElements);
    // Renderiza
    renderUpdateList(listElements, menuList);
}
// Drop - <li> donde se ha soltado.
function eventDrop(event) {
    // Sustituye el elemento soltado por el elemento que estaba debajo
    const myDropElement = event.target;
    // Se arregla el indice sobrante por el elemento undefined de la zona soltable
    const undefinedIndex = listElements.indexOf(undefined);
    const myDropElementIndex = undefinedIndex > myDragElement.dataset.key
        ? myDropElement.dataset.key - 1
        : myDropElement.dataset.key;
    listElements = listElements.filter((item) => item !== undefined);
    listElements = arrayMoveIndex(myDragElement.dataset.key, myDropElementIndex, listElements);
    myDragElement = undefined;
    renderUpdateList(listElements, menuList);
}
// Init
renderUpdateList(listElements, menuList);

También dispones de la versión en Typescript.

/*
 * Variables
 */
// Conjunto de datos a orden arrastrando y soltando
let listElements: Array<any> = [
    "gato 🐈",
    "loro 🦜",
    "elefante 🐘",
    "serpiente 🐍"
];
// Elemento <ul> que será ordenado
const menuList: HTMLUListElement = document.querySelector("#list");

// --- Funcionalidad interna -- //

// Elementos <li> que será creados dentro de la lista desordenada
let menuItems: Array<HTMLLIElement> = [];
// Clase que será usada para marcar la zona donde se puede soltar el elemento arrastable
const classZone: string = "zone";

/*
 * Funciones
 */

/**
 * Renderiza los elementos <li> en cada cambio de datos
 */
function renderUpdateList(list: Array<any>, target: HTMLUListElement) {
    // Limpia todos los <li> anteriores
    target.textContent = "";
    // Vacia el array donde se guardarán los objetos <li>
    menuItems = [];
    // Se itera cada elemento de la lista para crear un <li>
    list.forEach((value, index) => {
        const myLi = document.createElement("li");
        myLi.textContent = value;
        myLi.setAttribute("draggable", "true");
        // Se añade una propiedad data-key con la posición de list para manipularla en el futuro
        myLi.dataset.key = index;
        // Si existe un elemento undefined, se añade una clase para marcarlo como zona soltable
        if (value === undefined) myLi.classList.add(classZone);
        // Si se esta arrastrando el elemento, no se renderiza su <li>
        if (
            myDragElement !== undefined &&
            myDragElement.dataset.key ==
                (eventDragOverIndex < index ? index - 1 : index)
        ) myLi.style.display = "none";
        // Eventos
        myLi.addEventListener("drop", eventDrop);
        myLi.addEventListener("dragover", eventDragOver);
        // Se añade al documento
        target.appendChild(myLi);
        // Se guarda en menuItems para gestionar
        menuItems.push(myLi);
    });
}

/**
 * Devuelve una copia de la lista donde se ha movidoun indice a otra posicion.
 * @param {number} indexFrom
 * @param {number} indexTo
 * @param {Array<any>} list
 * @return {Array<any>}
 */
function arrayMoveIndex(
    indexFrom: number,
    indexTo: number,
    list: Array<any>
): Array<any> {
    // Guarda el valor a mover
    const moveValue = list[indexFrom];
    // Borra de la lista el valor a mover
    const listNotValue = list.filter(
        (currentValue, currentIndex) => currentIndex != indexFrom
    );
    // Concadena todos los fragmentos
    return listNotValue
        .slice(0, indexTo)
        .concat(moveValue, listNotValue.slice(indexTo));
}

/**
 * Añade en un array un valor a una posición concreta
 * @param {number} index
 * @param {any} value
 * @param {Array<any>} list
 * @return {Array<any>}
 */
function arrayAddValuePosition(
    index: number,
    value: any,
    list: Array<any>
): Array<any> {
    // Concat all fragments: start to position + moveValue + rest array
    return list.slice(0, index).concat(value, list.slice(index));
}

/*
 * Eventos Drag and drop
 */

// Drag Start - <li> que se esta arrastrando.
let myDragElement = undefined;

menuList.addEventListener("dragstart", (event) => {
    // Saves which element is moving.
    myDragElement = event.target;
    // Safari fix
    //event.dataTransfer.setData('text/html', myDragElement.innerHTML);
    //event.dataTransfer.setData("text/plain", event.target.textContent);
});

// Drag over - <li> que esta debajo del elemento que se esta arrastrando.
let eventDragOverIndex = -1;
function eventDragOver(event) {
    event.preventDefault();
    // Añade un elemento undefined en el mismo indice donde se esta arrastando con el objetivo de mostrar donde se puede soltar.
    // Guarda el indice
    eventDragOverIndex = event.target.dataset.key;
    // Quita cualquier undefined anteriores
    listElements = listElements.filter((item) => item !== undefined);
    // Añade undefined en la posición donde se encuentra el arrastre
    listElements = arrayAddValuePosition(
        event.target.dataset.key,
        undefined,
        listElements
    );
    // Renderiza
    renderUpdateList(listElements, menuList);
}

// Drop - <li> donde se ha soltado.
function eventDrop(event) {
    // Sustituye el elemento soltado por el elemento que estaba debajo
    const myDropElement = event.target;
    // Se arregla el indice sobrante por el elemento undefined de la zona soltable
    const undefinedIndex = listElements.indexOf(undefined);
    const myDropElementIndex =
        undefinedIndex > myDragElement.dataset.key
            ? myDropElement.dataset.key - 1
            : myDropElement.dataset.key;
    listElements = listElements.filter((item) => item !== undefined);
    listElements = arrayMoveIndex(
        myDragElement.dataset.key,
        myDropElementIndex,
        listElements
    );
    myDragElement = undefined;

    renderUpdateList(listElements, menuList);
}

// Init
renderUpdateList(listElements, menuList);

Ejemplo completo

<html>
	<head>
		<style>
			li {
			  background: #209cee;
			  color: white;
			  padding: 1rem;
			  border: 1px solid black;
			  transition: 0.2s all;
			  cursor: move;
			}
			li.zone {
			  opacity: 0.8;
			  height: 3rem;
			}
		</style>
	</head>
        <body>

			<ul id="list" class="menu-list"></ul>
            <script>
				/*
				 * Variables
				 */
				// Conjunto de datos a orden arrastrando y soltando
				let listElements = [
				    "gato 🐈",
				    "loro 🦜",
				    "elefante 🐘",
				    "serpiente 🐍"
				];
				// Elemento <ul> que será ordenado
				const menuList = document.querySelector("#list");
				// --- Funcionalidad interna -- //
				// Elementos <li> que será creados dentro de la lista desordenada
				let menuItems = [];
				// Clase que será usada para marcar la zona donde se puede soltar el elemento arrastable
				const classZone = "zone";
				/*
				 * Funciones
				 */
				/**
				 * Renderiza los elementos <li> en cada cambio de datos
				 */
				function renderUpdateList(list, target) {
				    // Limpia todos los <li> anteriores
				    target.textContent = "";
				    // Vacia el array donde se guardarán los objetos <li>
				    menuItems = [];
				    // Se itera cada elemento de la lista para crear un <li>
				    list.forEach((value, index) => {
				        const myLi = document.createElement("li");
				        myLi.textContent = value;
				        myLi.setAttribute("draggable", "true");
				        // Se añade una propiedad data-key con la posición de list para manipularla en el futuro
				        myLi.dataset.key = index;
				        // Si existe un elemento undefined, se añade una clase para marcarlo como zona soltable
				        if (value === undefined)
				            myLi.classList.add(classZone);
				        // Si se esta arrastrando el elemento, no se renderiza su <li>
				        if (myDragElement !== undefined &&
				            myDragElement.dataset.key ==
				                (eventDragOverIndex < index ? index - 1 : index))
				            myLi.style.display = "none";
				        // Eventos
				        myLi.addEventListener("drop", eventDrop);
				        myLi.addEventListener("dragover", eventDragOver);
				        // Se añade al documento
				        target.appendChild(myLi);
				        // Se guarda en menuItems para gestionar
				        menuItems.push(myLi);
				    });
				}
				/**
				 * Devuelve una copia de la lista donde se ha movidoun indice a otra posicion.
				 * @param {number} indexFrom
				 * @param {number} indexTo
				 * @param {Array<any>} list
				 * @return {Array<any>}
				 */
				function arrayMoveIndex(indexFrom, indexTo, list) {
				    // Guarda el valor a mover
				    const moveValue = list[indexFrom];
				    // Borra de la lista el valor a mover
				    const listNotValue = list.filter((currentValue, currentIndex) => currentIndex != indexFrom);
				    // Concadena todos los fragmentos
				    return listNotValue
				        .slice(0, indexTo)
				        .concat(moveValue, listNotValue.slice(indexTo));
				}
				/**
				 * Añade en un array un valor a una posición concreta
				 * @param {number} index
				 * @param {any} value
				 * @param {Array<any>} list
				 * @return {Array<any>}
				 */
				function arrayAddValuePosition(index, value, list) {
				    // Concat all fragments: start to position + moveValue + rest array
				    return list.slice(0, index).concat(value, list.slice(index));
				}
				/*
				 * Eventos Drag and drop
				 */
				// Drag Start - <li> que se esta arrastrando.
				let myDragElement = undefined;
				menuList.addEventListener("dragstart", (event) => {
				    // Saves which element is moving.
				    myDragElement = event.target;
				    // Safari fix
				    //event.dataTransfer.setData('text/html', myDragElement.innerHTML);
				    //event.dataTransfer.setData("text/plain", event.target.textContent);
				});
				// Drag over - <li> que esta debajo del elemento que se esta arrastrando.
				let eventDragOverIndex = -1;
				function eventDragOver(event) {
				    event.preventDefault();
				    // Añade un elemento undefined en el mismo indice donde se esta arrastando con el objetivo de mostrar donde se puede soltar.
				    // Guarda el indice
				    eventDragOverIndex = event.target.dataset.key;
				    // Quita cualquier undefined anteriores
				    listElements = listElements.filter((item) => item !== undefined);
				    // Añade undefined en la posición donde se encuentra el arrastre
				    listElements = arrayAddValuePosition(event.target.dataset.key, undefined, listElements);
				    // Renderiza
				    renderUpdateList(listElements, menuList);
				}
				// Drop - <li> donde se ha soltado.
				function eventDrop(event) {
				    // Sustituye el elemento soltado por el elemento que estaba debajo
				    const myDropElement = event.target;
				    // Se arregla el indice sobrante por el elemento undefined de la zona soltable
				    const undefinedIndex = listElements.indexOf(undefined);
				    const myDropElementIndex = undefinedIndex > myDragElement.dataset.key
				        ? myDropElement.dataset.key - 1
				        : myDropElement.dataset.key;
				    listElements = listElements.filter((item) => item !== undefined);
				    listElements = arrayMoveIndex(myDragElement.dataset.key, myDropElementIndex, listElements);
				    myDragElement = undefined;
				    renderUpdateList(listElements, menuList);
				}
				// Init
				renderUpdateList(listElements, menuList);


            </script>
        </body>
</html>

Por último quiero añadir que no funciona con dispositivos móviles por ciertas limitaciones en los eventos.

Espero que os sea útil.

Esta obra está bajo una Licencia Creative Commons Atribución-NoComercial-SinDerivadas 4.0 Internacional.

Atribución/Reconocimiento-NoComercial-SinDerivados 4.0 Internacional

Donación con recompensa

  • 1 café: Respondo a tu duda en los comentarios.
  • 2 cafés: Respondo en menos de 24h a tu comentario.
  • 3 cafés: Todo lo anterior y además te doy las gracias en mis redes.
Comprame un café

Comentarios

{{ comments.length }} comentarios

Nuevo comentario

Nueva replica  {{ formatEllipsisAuthor(replyComment.author) }}

Acepto la política de Protección de Datos.

Escribe el primer comentario

Tal vez también te interese...