fix: added mobile support
This commit is contained in:
+116
-3
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user