fix: added mobile support

This commit is contained in:
2026-07-08 14:45:01 +02:00
parent a5b5783556
commit ebb1fc2906
3 changed files with 170 additions and 37 deletions
+116 -3
View File
@@ -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 <li> 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();
});