205 lines
8.1 KiB
JavaScript
205 lines
8.1 KiB
JavaScript
// Drag & drop de elementos arrastrables (los <li> de "Elementos") hacia una
|
|
// zona de destino (el "dnd-canvas" de una sección de "Patrón").
|
|
|
|
import { draggableItemsSource } from './dom-refs.js';
|
|
import { getTouchPoint, moveIfNeeded, getItemAfterPoint, itemDrag } from './drag-reorder.js';
|
|
|
|
// Hace arrastrable un <li> de la lista de "Elementos" y mete su HTML en el
|
|
// dataTransfer para poder clonarlo al soltarlo. addDndItem() se usa aquí
|
|
// dentro del listener aunque se declare más abajo: en JS los `function` se
|
|
// izan (hoisting), así que el orden dentro del módulo no importa.
|
|
export function makeDraggable(item) {
|
|
item.setAttribute('draggable', 'true');
|
|
item.addEventListener('dragstart', (e) => {
|
|
e.dataTransfer.effectAllowed = 'copy';
|
|
e.dataTransfer.setData('text/html', item.outerHTML);
|
|
});
|
|
|
|
// Equivalente táctil: crea un clon flotante que sigue al dedo y, al
|
|
// soltar, mira qué elemento hay debajo del punto final para saber si
|
|
// cayó dentro de una zona de drop.
|
|
item.addEventListener('touchstart', (e) => {
|
|
e.preventDefault();
|
|
const start = getTouchPoint(e);
|
|
|
|
const ghost = item.cloneNode(true);
|
|
ghost.style.position = 'fixed';
|
|
ghost.style.left = `${start.x}px`;
|
|
ghost.style.top = `${start.y}px`;
|
|
ghost.style.pointerEvents = 'none';
|
|
ghost.style.opacity = '0.9';
|
|
ghost.style.zIndex = '9999';
|
|
// Se agranda y se desplaza por encima del punto de contacto: el dedo
|
|
// tapa el elemento original, así que a tamaño normal el usuario no
|
|
// vería qué está arrastrando.
|
|
ghost.style.transform = 'translate(-50%, -300%) scale(1.8)';
|
|
ghost.style.transformOrigin = 'center';
|
|
document.body.appendChild(ghost);
|
|
|
|
const onTouchMove = (moveEvent) => {
|
|
moveEvent.preventDefault();
|
|
const point = getTouchPoint(moveEvent);
|
|
ghost.style.left = `${point.x}px`;
|
|
ghost.style.top = `${point.y}px`;
|
|
};
|
|
|
|
const onTouchEnd = (endEvent) => {
|
|
document.removeEventListener('touchmove', onTouchMove);
|
|
document.removeEventListener('touchend', onTouchEnd);
|
|
ghost.remove();
|
|
|
|
const point = getTouchPoint(endEvent);
|
|
const target = document.elementFromPoint(point.x, point.y);
|
|
const dropCanvas = target && target.closest('.dnd-canvas');
|
|
if (dropCanvas) addDndItem(dropCanvas, item.textContent.trim(), 1);
|
|
};
|
|
|
|
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
document.addEventListener('touchend', onTouchEnd);
|
|
}, { passive: false });
|
|
}
|
|
|
|
// Añade a una zona de drop un grupo: cantidad (input numérico), el elemento
|
|
// (buscado en draggableItemsSource por su texto) y un botón para
|
|
// eliminarlo. Se usa tanto al soltar un elemento como al restaurar un
|
|
// patrón guardado previamente.
|
|
export function addDndItem(dropCanvas, label, count) {
|
|
const sourceItem = Array.from(draggableItemsSource.children)
|
|
.find(li => li.textContent.trim() === label);
|
|
if (!sourceItem) return;
|
|
|
|
const clone = sourceItem.cloneNode(true);
|
|
clone.removeAttribute('id');
|
|
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.
|
|
|
|
// Resalta la misma zona ampliada que usa setupDropZone() (la sección
|
|
// completa), no solo dropCanvas, para que el resaltado visual coincida
|
|
// con dónde realmente se puede soltar.
|
|
const highlightTarget = dropCanvas.closest('.top-section') || dropCanvas;
|
|
|
|
const countInput = document.createElement('input');
|
|
countInput.type = 'number';
|
|
countInput.min = '1';
|
|
countInput.value = count;
|
|
countInput.className = 'input input-bordered input-xs w-16';
|
|
|
|
const group = document.createElement('div');
|
|
group.className = 'inline-flex items-center gap-1';
|
|
|
|
// Botón para poder quitar el elemento si el usuario se equivoca.
|
|
const removeItemBtn = document.createElement('button');
|
|
removeItemBtn.type = 'button';
|
|
removeItemBtn.className = 'btn btn-xs btn-circle';
|
|
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) => {
|
|
itemDrag.item = group;
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
e.dataTransfer.setData('text/plain', 'item');
|
|
group.classList.add('dragging');
|
|
highlightTarget.classList.add('drop-target-active');
|
|
});
|
|
|
|
clone.addEventListener('dragend', () => {
|
|
group.classList.remove('dragging');
|
|
highlightTarget.classList.remove('drop-target-active');
|
|
itemDrag.item = 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();
|
|
itemDrag.item = group;
|
|
group.classList.add('dragging');
|
|
highlightTarget.classList.add('drop-target-active');
|
|
|
|
const onTouchMove = (moveEvent) => {
|
|
moveEvent.preventDefault();
|
|
const point = getTouchPoint(moveEvent);
|
|
const afterElement = getItemAfterPoint(dropCanvas, point.x, point.y);
|
|
moveIfNeeded(dropCanvas, itemDrag.item, afterElement);
|
|
};
|
|
|
|
const onTouchEnd = () => {
|
|
document.removeEventListener('touchmove', onTouchMove);
|
|
document.removeEventListener('touchend', onTouchEnd);
|
|
group.classList.remove('dragging');
|
|
highlightTarget.classList.remove('drop-target-active');
|
|
itemDrag.item = null;
|
|
};
|
|
|
|
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
document.addEventListener('touchend', onTouchEnd);
|
|
}, { passive: false });
|
|
|
|
group.appendChild(countInput);
|
|
group.appendChild(clone);
|
|
group.appendChild(removeItemBtn);
|
|
|
|
dropCanvas.appendChild(group);
|
|
}
|
|
|
|
// Convierte cualquier contenedor en una zona donde soltar elementos
|
|
// arrastrables. `onChange`, si se pasa, se ejecuta cuando cambia el
|
|
// contenido del canvas (al soltar un elemento o cambiar la cantidad).
|
|
// `hitArea` (por defecto el propio dropCanvas) es donde se escuchan
|
|
// dragover/drop. dropCanvas encoge al tamaño exacto de sus elementos, así
|
|
// que soltar justo por encima del primero o por debajo del último se
|
|
// quedaba fuera de su zona; se pasa la sección completa (con su padding)
|
|
// como hitArea para tener el mismo margen que ya se amplió al reordenar
|
|
// secciones.
|
|
export function setupDropZone(dropCanvas, onChange, hitArea = dropCanvas) {
|
|
hitArea.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
|
|
if (itemDrag.item) {
|
|
// 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);
|
|
moveIfNeeded(dropCanvas, itemDrag.item, afterElement);
|
|
return;
|
|
}
|
|
|
|
e.dataTransfer.dropEffect = 'copy';
|
|
});
|
|
|
|
hitArea.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
if (itemDrag.item) 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).
|
|
|
|
const wrapper = document.createElement('div');
|
|
wrapper.innerHTML = html;
|
|
const clone = wrapper.firstElementChild;
|
|
if (!clone) return;
|
|
|
|
addDndItem(dropCanvas, clone.textContent.trim(), 1);
|
|
});
|
|
|
|
if (onChange) {
|
|
dropCanvas.addEventListener('input', onChange); // Cambios en la cantidad.
|
|
new MutationObserver(onChange).observe(dropCanvas, { childList: true }); // Añadir/quitar elementos.
|
|
}
|
|
}
|
|
|
|
// Convierte el contenido de una zona de drop en texto tipo
|
|
// "3 Elemento A, 1 Elemento B".
|
|
export function getDndCanvasText(dndCanvas) {
|
|
const parts = Array.from(dndCanvas.children).map(group => {
|
|
const input = group.querySelector('input');
|
|
const label = group.querySelector('li');
|
|
if (!input || !label) return '';
|
|
return `${input.value} ${label.textContent}`;
|
|
});
|
|
return parts.filter(Boolean).join(', ');
|
|
}
|