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.
{{ comments.length }} comentarios