JavaScript reordenando lista arrastrando y soltando

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.

Este trabajo está bajo una licencia Attribution-NonCommercial-NoDerivatives 4.0 International.

¿Me invitas a un café?

Puedes usar el terminal.

ssh customer@andros.dev -p 5555

Escrito por Andros Fenollosa

febrero 19, 2022

8 min de lectura

Sigue leyendo