// Motor genérico de reordenado por arrastre, usado tanto para las secciones // (arrastrando su cabecera) como para los elementos ya soltados dentro de // una sección de "Patrón". 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, así que todo esto también se // reimplementa con eventos touch usando getTouchPoint() como coordenadas // comunes. // Sección que se está arrastrando actualmente para reordenar, junto con el // contenedor al que pertenece (el nivel superior, o el de un grupo) y la // zona ampliada que se resalta mientras dura el arrastre. Se exporta como // un objeto mutable (en vez de variables sueltas con getters/setters) // porque quien arrastra (sections.js) y quien reordena // (setupSectionsReorder, aquí mismo) necesitan compartir el mismo estado // en todo momento. export const sectionDrag = { section: null, container: null, hitArea: null }; // Igual que sectionDrag, pero para reordenar un elemento ya soltado (un // "grupo": cantidad + punto + botón eliminar) dentro de su zona de drop. export const itemDrag = { item: null }; export function getTouchPoint(e) { const touch = e.touches[0] || e.changedTouches[0]; return { x: touch.clientX, y: touch.clientY }; } // Mueve `node` a la posición indicada por `afterElement` (null = al final) // solo si no está ya ahí. dragover/touchmove se disparan decenas de veces // por segundo mientras se arrastra, y sin esta comprobación se repetía un // insertBefore/appendChild en cada uno aunque la posición no cambiase: eso // fuerza un reflow y una mutación del DOM de más en cada evento, lo que // además retrasa el propio cálculo de posiciones del siguiente evento (los // rects de los elementos vecinos se recalculan sin necesidad) y podía hacer // que el reordenado se notase bloqueado o dando tirones cerca del límite // entre dos elementos. export function moveIfNeeded(container, node, afterElement) { if (afterElement == null) { if (container.lastElementChild !== node) container.appendChild(node); } else if (afterElement !== node && node.nextElementSibling !== afterElement) { container.insertBefore(node, afterElement); } } // Dado un contenedor de secciones y un punto Y del cursor, devuelve la // sección justo debajo de la que hay que insertar la sección arrastrada (o // null si va al final). Sirve tanto para #panel-top-sections como para el // contenedor interno de cualquier sección de tipo "Grupo". export function getSectionAfterY(container, y) { const sections = Array.from(container.querySelectorAll(':scope > .top-section:not(.dragging)')); return sections.reduce((closest, child) => { const box = child.getBoundingClientRect(); const offset = y - box.top - box.height / 2; if (offset < 0 && offset > closest.offset) { return { offset, element: child }; } return closest; }, { offset: -Infinity, element: null }).element; } // Da de alta el reordenado por arrastre para un contenedor de secciones (el // de nivel superior, o el de un grupo). `hitArea` es donde se escuchan // dragover/drop: el contenedor encoge al tamaño exacto de sus secciones, // así que soltar justo por encima de la primera o por debajo de la última // quedaría fuera de su zona; por eso se usa como hitArea su envoltorio con // padding (#panel-top, o la sección "Grupo" completa). export function setupSectionsReorder(container, hitArea) { hitArea.addEventListener('dragover', (e) => { if (sectionDrag.container !== container) return; // El arrastre no pertenece a este contenedor (o no hay ninguno). e.preventDefault(); e.dataTransfer.dropEffect = 'move'; // Mueve en vivo la sección arrastrada a su nueva posición según el cursor. const afterElement = getSectionAfterY(container, e.clientY); moveIfNeeded(container, sectionDrag.section, afterElement); }); hitArea.addEventListener('drop', (e) => { if (sectionDrag.container !== container) return; 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. export 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; }