// Creación de secciones (título, subtítulo, texto, nota, materiales, imagen // y patrón): el "armazón" común (createSectionShell) y una función // addXxxSection por tipo. La sección de "grupo" no vive aquí sino en // section-types.js, porque para reconstruir sus hijas necesita el registro // completo de tipos (y este módulo no lo conoce, para que no haya un // import circular entre ambos). import { panelTop, sectionsContainer, draggableItemsSource } from './dom-refs.js'; import { getCurrentLanguage } from './i18n.js'; import { getTouchPoint, moveIfNeeded, getSectionAfterY, sectionDrag } from './drag-reorder.js'; import { makeDraggable, addDndItem, setupDropZone } from './dnd-items.js'; // Crea el "armazón" común a toda sección: el recuadro, la cabecera (asa // para reordenar, tipo de sección, botón de colapsar, duplicar y de // eliminar) y el cuerpo donde cada función addXxxSection() mete su // contenido específico. Devuelve { section, body }: quien llama decide en // qué contenedor meter `section` (el nivel superior, o el de un grupo), y // `body` es donde hay que appendear el contenido propio de cada tipo. export function createSectionShell(typeLabel) { const section = document.createElement('div'); section.className = 'top-section border border-base-300 rounded-box p-4 flex flex-col gap-2'; // Toda la cabecera es la zona de arrastre para reordenar (no solo el // icono "⠿"), así que draggable/dragstart/etc. van en `header`. const header = document.createElement('div'); header.className = 'flex items-center gap-2 cursor-move'; header.title = sectionsContainer.dataset.dragTitle; header.setAttribute('draggable', 'true'); const dragHandle = document.createElement('span'); dragHandle.className = 'select-none text-base-content/50 px-1'; dragHandle.textContent = '⠿'; header.addEventListener('dragstart', (e) => { sectionDrag.section = section; sectionDrag.container = section.parentElement; sectionDrag.hitArea = sectionDrag.container.closest('.top-section') || panelTop; 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'); sectionDrag.hitArea.classList.add('drop-target-active'); }); header.addEventListener('dragend', () => { section.classList.remove('dragging'); sectionDrag.hitArea.classList.remove('drop-target-active'); sectionDrag.section = null; sectionDrag.container = null; sectionDrag.hitArea = null; }); // Equivalente táctil del reordenado por dragover/drop de // setupSectionsReorder(): mientras el dedo se mueve, se reutiliza // getSectionAfterY() para ir desplazando la sección en vivo dentro de su // contenedor actual. header.addEventListener('touchstart', (e) => { if (e.target.closest('button')) return; // No interferir con "colapsar", "duplicar" ni "eliminar". e.preventDefault(); sectionDrag.section = section; sectionDrag.container = section.parentElement; sectionDrag.hitArea = sectionDrag.container.closest('.top-section') || panelTop; section.classList.add('dragging'); sectionDrag.hitArea.classList.add('drop-target-active'); const onTouchMove = (moveEvent) => { moveEvent.preventDefault(); const { y } = getTouchPoint(moveEvent); const afterElement = getSectionAfterY(sectionDrag.container, y); moveIfNeeded(sectionDrag.container, sectionDrag.section, afterElement); }; const onTouchEnd = () => { document.removeEventListener('touchmove', onTouchMove); document.removeEventListener('touchend', onTouchEnd); section.classList.remove('dragging'); sectionDrag.hitArea.classList.remove('drop-target-active'); sectionDrag.section = null; sectionDrag.container = null; sectionDrag.hitArea = null; }; document.addEventListener('touchmove', onTouchMove, { passive: false }); document.addEventListener('touchend', onTouchEnd); }, { passive: false }); // Botón para colapsar/expandir el cuerpo de la sección, útil cuando hay // muchas secciones o su contenido (p.ej. una imagen) ocupa mucho espacio. const collapseBtn = document.createElement('button'); collapseBtn.type = 'button'; collapseBtn.className = 'collapse-btn btn btn-xs btn-ghost'; collapseBtn.textContent = '▾'; collapseBtn.title = sectionsContainer.dataset.collapseTitle; // Etiqueta que indica de qué tipo es la sección (Título, Texto, Imagen...). const badge = document.createElement('span'); badge.className = 'badge badge-sm badge-outline select-none'; badge.textContent = typeLabel; const spacer = document.createElement('div'); spacer.className = 'flex-1'; // Botón para duplicar la sección: se dispara con un evento en vez de // llamar directamente a duplicateSection() (que vive en // section-types.js), para que este módulo no necesite importarlo: es // section-types.js quien depende de sections.js (para crear cada tipo), // no al revés. const duplicateBtn = document.createElement('button'); duplicateBtn.type = 'button'; duplicateBtn.className = 'btn btn-xs btn-circle'; duplicateBtn.textContent = '⧉'; duplicateBtn.title = sectionsContainer.dataset.duplicateTitle; duplicateBtn.addEventListener('click', () => { section.dispatchEvent(new CustomEvent('section:duplicate', { bubbles: true })); }); const removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = 'btn btn-xs btn-circle'; removeBtn.textContent = '✕'; removeBtn.title = sectionsContainer.dataset.removeTitle; removeBtn.addEventListener('click', () => section.remove()); header.appendChild(dragHandle); header.appendChild(collapseBtn); header.appendChild(badge); header.appendChild(spacer); header.appendChild(duplicateBtn); header.appendChild(removeBtn); const body = document.createElement('div'); body.className = 'section-body flex flex-col gap-2'; collapseBtn.addEventListener('click', () => { body.classList.toggle('hidden'); collapseBtn.textContent = body.classList.contains('hidden') ? '▸' : '▾'; }); section.appendChild(header); section.appendChild(body); return { section, body }; } // Sección de título: un input de una línea que se muestra como

en el // resultado. `data` es el dataset del botón "+ Título" (data-type-label, // data-placeholder). `translations`, si se pasa, es el mapa { idioma: // texto } completo (se usa al restaurar un patrón guardado o al duplicar // una sección); se muestra el que corresponda al idioma actual. `container` // es dónde se añade la sección: #panel-top-sections por defecto, o el de // un grupo. export function addTitleSection(data, translations = {}, container = sectionsContainer) { const { section, body } = createSectionShell(data.typeLabel); const input = document.createElement('input'); input.type = 'text'; input.className = 'title-input input input-bordered w-full'; input.placeholder = data.placeholder; input.dataset.translations = JSON.stringify(translations); input.value = translations[getCurrentLanguage()] || ''; body.appendChild(input); container.appendChild(section); } // Sección de subtítulo: igual que la de título pero se muestra como

// (más pequeño) en el resultado, para marcar un encabezado secundario. export function addSubtitleSection(data, translations = {}, container = sectionsContainer) { const { section, body } = createSectionShell(data.typeLabel); const input = document.createElement('input'); input.type = 'text'; input.className = 'subtitle-input input input-bordered w-full'; input.placeholder = data.placeholder; input.dataset.translations = JSON.stringify(translations); input.value = translations[getCurrentLanguage()] || ''; body.appendChild(input); container.appendChild(section); } // Sección de texto libre. Se marca con la clase 'text-input' (además de // 'textarea') para poder distinguirla de la de "Nota" en section-types.js: // ambas son un