diff --git a/css/main.css b/css/main.css index cafbc8f..ccd4b99 100644 --- a/css/main.css +++ b/css/main.css @@ -9,6 +9,17 @@ width: fit-content; } +/* En pantallas pequeñas, "Secciones" y "Vista previa" se comportan como + pestañas: solo se muestra una a la vez (ver .tabs en index.html). A + partir del breakpoint md ambos paneles se muestran siempre, lado a lado. + Se restringe a "screen" para que nunca afecte a la exportación a PDF + (@media print más abajo), sea cual sea el ancho de pantalla del usuario. */ +@media screen and (max-width: 767px) { + .panel-hidden-mobile { + display: none; + } +} + /* Al exportar/imprimir, solo se muestra #panel-output (y no sus propios controles, marcados con .no-print), ocultando el resto de la página. */ @media print { diff --git a/index.html b/index.html index 3495cae..1dbca9b 100644 --- a/index.html +++ b/index.html @@ -13,42 +13,51 @@
-
+
+

Crochet

+
+ + + +
+
-
-
-

Secciones

-
- - +
+ +
+ + +
+ +
+ +
+

Secciones

+
+ + + + +
-
-
- - - - - -
-
-
+
+ -
-
-

Vista previa

- -
-
-
+
+

Vista previa

+
+
+ +
@@ -60,6 +69,6 @@
- + diff --git a/js/main.js b/js/dnd.js similarity index 79% rename from js/main.js rename to js/dnd.js index 0ad946a..3b6e551 100644 --- a/js/main.js +++ b/js/dnd.js @@ -13,6 +13,16 @@ document.addEventListener('DOMContentLoaded', () => { // no interfiere con el drag & drop de los elementos arrastrables. let draggedSection = null; + // 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. Por eso todo el drag & drop de la página (reordenar secciones y + // soltar elementos) también se reimplementa con eventos touch, usando + // este punto como coordenadas comunes. + function getTouchPoint(e) { + const touch = e.touches[0] || e.changedTouches[0]; + return { x: touch.clientX, y: touch.clientY }; + } + // Dado 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). function getSectionAfterY(y) { @@ -51,13 +61,58 @@ document.addEventListener('DOMContentLoaded', () => { // --------------------------------------------------------------------- // Hace arrastrable un
  • de la lista de "Elementos" y mete su HTML en - // el dataTransfer para poder clonarlo al soltarlo. + // el dataTransfer para poder clonarlo al soltarlo. addDndItem() se define + // más abajo; se referencia aquí dentro del listener, no al declarar la + // función, así que el orden no importa. 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 @@ -243,6 +298,36 @@ document.addEventListener('DOMContentLoaded', () => { draggedSection = null; }); + // Equivalente táctil del reordenado por dragover/drop de más arriba: + // mientras el dedo se mueve, se reutiliza getSectionAfterY() para ir + // desplazando la sección en vivo dentro de #panel-top-sections. + dragHandle.addEventListener('touchstart', (e) => { + e.preventDefault(); + draggedSection = section; + section.classList.add('dragging', 'opacity-50'); + + const onTouchMove = (moveEvent) => { + moveEvent.preventDefault(); + const { y } = getTouchPoint(moveEvent); + const afterElement = getSectionAfterY(y); + if (afterElement == null) { + sectionsContainer.appendChild(draggedSection); + } else if (afterElement !== draggedSection) { + sectionsContainer.insertBefore(draggedSection, afterElement); + } + }; + + const onTouchEnd = () => { + document.removeEventListener('touchmove', onTouchMove); + document.removeEventListener('touchend', onTouchEnd); + section.classList.remove('dragging', 'opacity-50'); + draggedSection = 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'); @@ -375,13 +460,13 @@ document.addEventListener('DOMContentLoaded', () => { const { section, body } = createSectionShell(data.typeLabel); const row = document.createElement('div'); - row.className = 'flex gap-4'; + row.className = 'flex flex-col sm:flex-row gap-4'; const dndCanvas = document.createElement('div'); dndCanvas.className = 'dnd-canvas flex-1 flex flex-wrap items-center gap-2 min-h-24'; const elementsPanel = document.createElement('aside'); - elementsPanel.className = 'w-48 shrink-0 border border-base-300 rounded-box p-4'; + elementsPanel.className = 'w-full sm:w-48 sm:shrink-0 border border-base-300 rounded-box p-4'; const elementsTitle = document.createElement('h3'); elementsTitle.className = 'font-semibold mb-2'; @@ -419,12 +504,40 @@ document.addEventListener('DOMContentLoaded', () => { addImageBtn.addEventListener('click', () => addImageSection(addImageBtn.dataset)); addDndBtn.addEventListener('click', () => addDndSection(addDndBtn.dataset)); + // --------------------------------------------------------------------- + // Pestañas (solo en móvil, ver css/main.css): alternar entre el panel de + // "Secciones" y el de "Vista previa". A partir del breakpoint md la clase + // `panel-hidden-mobile` no tiene efecto y ambos paneles quedan visibles + // permanentemente, lado a lado. + // --------------------------------------------------------------------- + + const tabSections = document.getElementById('tab-sections'); + const tabOutput = document.getElementById('tab-output'); + const panelTop = document.getElementById('panel-top'); + const panelOutput = document.getElementById('panel-output'); + + function showTab(tab) { + const showSections = tab === 'sections'; + panelTop.classList.toggle('panel-hidden-mobile', !showSections); + panelOutput.classList.toggle('panel-hidden-mobile', showSections); + tabSections.classList.toggle('tab-active', showSections); + tabOutput.classList.toggle('tab-active', !showSections); + } + + tabSections.addEventListener('click', () => showTab('sections')); + tabOutput.addEventListener('click', () => showTab('output')); + // Exportar #panel-output a PDF: se usa el diálogo de impresión del propio // navegador (con "Guardar como PDF") en vez de una librería externa, ya // que las librerías tipo html2canvas no renderizan bien los colores oklch // que usa Tailwind v4. La hoja de estilos de impresión (css/main.css) // aísla #panel-output para que solo se exporte el resultado. document.getElementById('export-pdf').addEventListener('click', () => { + // `panel-hidden-mobile` está restringida a "screen" en el CSS, así que + // nunca debería afectar a la impresión, pero por si algún navegador + // calcula el ancho de la vista de impresión igual al de pantalla en + // móvil, se cambia de pestaña antes de imprimir para curarse en salud. + showTab('output'); window.print(); });