// Drag & drop de elementos arrastrables (los
  • 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
  • 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(', '); }