feat: improved user feedback on dnd and reordering element list

This commit is contained in:
2026-07-09 08:30:18 +02:00
parent ebb1fc2906
commit a923559d1a
2 changed files with 124 additions and 6 deletions
+107 -6
View File
@@ -13,6 +13,11 @@ document.addEventListener('DOMContentLoaded', () => {
// no interfiere con el drag & drop de los elementos arrastrables.
let draggedSection = null;
// Igual que draggedSection, pero para reordenar un elemento ya soltado
// (un "grupo": cantidad + punto + botón eliminar) dentro de su zona de
// drop. Ver getItemAfterPoint() y setupDropZone() más abajo.
let draggedItem = null;
// La API nativa de Drag and Drop (HTML5) no está implementada en los
// navegadores móviles: no se disparan dragstart/dragover/drop con el
// dedo. Por eso todo el drag & drop de la página (reordenar secciones y
@@ -56,6 +61,34 @@ document.addEventListener('DOMContentLoaded', () => {
e.preventDefault(); // El reordenado ya se hizo en el dragover; aquí solo evitamos el comportamiento por defecto del navegador.
});
// Igual que getSectionAfterY(), pero para una zona de drop con
// flex-wrap: los elementos pueden estar en distintas filas, así que no
// basta con comparar la coordenada Y. Se busca el elemento más cercano al
// punto (por distancia recta a su centro) y se decide si el punto cae
// antes o después según su posición horizontal respecto a ese centro.
function getItemAfterPoint(container, x, y) {
const items = Array.from(container.children).filter(child => !child.classList.contains('dragging'));
if (items.length === 0) return null;
let nearestChild = null;
let nearestCenterX = 0;
let nearestDistance = Infinity;
items.forEach(child => {
const box = child.getBoundingClientRect();
const centerX = box.left + box.width / 2;
const centerY = box.top + box.height / 2;
const distance = Math.hypot(x - centerX, y - centerY);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestChild = child;
nearestCenterX = centerX;
}
});
return x < nearestCenterX ? nearestChild : nearestChild.nextElementSibling;
}
// ---------------------------------------------------------------------
// Drag & drop de elementos arrastrables hacia una zona de destino
// ---------------------------------------------------------------------
@@ -125,9 +158,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (!sourceItem) return;
const clone = sourceItem.cloneNode(true);
clone.removeAttribute('draggable');
clone.removeAttribute('id');
clone.classList.add('inline-block');
clone.classList.add('inline-block', 'cursor-move');
clone.setAttribute('draggable', 'true'); // Arrastrable, pero para reordenar dentro del canvas (ver más abajo), no para volver a soltarlo como si fuera nuevo.
const countInput = document.createElement('input');
countInput.type = 'number';
@@ -145,6 +178,54 @@ document.addEventListener('DOMContentLoaded', () => {
removeItemBtn.textContent = '✕';
removeItemBtn.addEventListener('click', () => group.remove());
// Reordenar el elemento dentro de su zona de drop: se distingue de
// "soltar un elemento nuevo" porque ese arrastre parte de la lista de
// "Elementos", no de un punto ya colocado en el canvas.
clone.addEventListener('dragstart', (e) => {
draggedItem = group;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', 'item');
group.classList.add('dragging');
dropCanvas.classList.add('drop-target-active');
});
clone.addEventListener('dragend', () => {
group.classList.remove('dragging');
dropCanvas.classList.remove('drop-target-active');
draggedItem = null;
});
// Equivalente táctil: mientras el dedo se mueve por encima del canvas,
// reutiliza getItemAfterPoint() para ir desplazando el grupo en vivo.
clone.addEventListener('touchstart', (e) => {
e.preventDefault();
draggedItem = group;
group.classList.add('dragging');
dropCanvas.classList.add('drop-target-active');
const onTouchMove = (moveEvent) => {
moveEvent.preventDefault();
const point = getTouchPoint(moveEvent);
const afterElement = getItemAfterPoint(dropCanvas, point.x, point.y);
if (afterElement == null) {
dropCanvas.appendChild(draggedItem);
} else if (afterElement !== draggedItem) {
dropCanvas.insertBefore(draggedItem, afterElement);
}
};
const onTouchEnd = () => {
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
group.classList.remove('dragging');
dropCanvas.classList.remove('drop-target-active');
draggedItem = null;
};
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('touchend', onTouchEnd);
}, { passive: false });
group.appendChild(countInput);
group.appendChild(clone);
group.appendChild(removeItemBtn);
@@ -158,11 +239,27 @@ document.addEventListener('DOMContentLoaded', () => {
function setupDropZone(dropCanvas, onChange) {
dropCanvas.addEventListener('dragover', (e) => {
e.preventDefault();
if (draggedItem) {
// No se está soltando un elemento nuevo, se está reordenando uno
// que ya estaba en este canvas.
e.dataTransfer.dropEffect = 'move';
const afterElement = getItemAfterPoint(dropCanvas, e.clientX, e.clientY);
if (afterElement == null) {
dropCanvas.appendChild(draggedItem);
} else if (afterElement !== draggedItem) {
dropCanvas.insertBefore(draggedItem, afterElement);
}
return;
}
e.dataTransfer.dropEffect = 'copy';
});
dropCanvas.addEventListener('drop', (e) => {
e.preventDefault();
if (draggedItem) return; // El reordenado ya se hizo en el dragover.
const html = e.dataTransfer.getData('text/html');
if (!html) return; // No es un elemento arrastrable (p.ej. es una sección siendo reordenada).
@@ -290,11 +387,13 @@ document.addEventListener('DOMContentLoaded', () => {
draggedSection = section;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', 'section'); // Necesario para que el arrastre se inicie en todos los navegadores.
section.classList.add('dragging', 'opacity-50');
section.classList.add('dragging');
sectionsContainer.classList.add('drop-target-active');
});
dragHandle.addEventListener('dragend', () => {
section.classList.remove('dragging', 'opacity-50');
section.classList.remove('dragging');
sectionsContainer.classList.remove('drop-target-active');
draggedSection = null;
});
@@ -304,7 +403,8 @@ document.addEventListener('DOMContentLoaded', () => {
dragHandle.addEventListener('touchstart', (e) => {
e.preventDefault();
draggedSection = section;
section.classList.add('dragging', 'opacity-50');
section.classList.add('dragging');
sectionsContainer.classList.add('drop-target-active');
const onTouchMove = (moveEvent) => {
moveEvent.preventDefault();
@@ -320,7 +420,8 @@ document.addEventListener('DOMContentLoaded', () => {
const onTouchEnd = () => {
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
section.classList.remove('dragging', 'opacity-50');
section.classList.remove('dragging');
sectionsContainer.classList.remove('drop-target-active');
draggedSection = null;
};