feat: splitted js in different modules
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user