feat: improved user feedback on dnd and reordering element list
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user