fix: added mobile support
This commit is contained in:
@@ -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 {
|
||||
|
||||
+21
-12
@@ -13,17 +13,27 @@
|
||||
<body>
|
||||
<main>
|
||||
|
||||
<div class="flex gap-4 p-4 items-start">
|
||||
|
||||
<section id="panel-top" class="w-1/2 border border-base-300 rounded-box p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h2 class="text-lg font-semibold">Secciones</h2>
|
||||
<header class="flex flex-wrap items-center justify-between gap-2 p-4 no-print">
|
||||
<h1 class="text-xl font-semibold">Crochet</h1>
|
||||
<div class="flex gap-2">
|
||||
<button id="save-pattern" type="button" class="btn btn-sm btn-outline">Guardar</button>
|
||||
<button id="load-pattern" type="button" class="btn btn-sm btn-outline">Cargar</button>
|
||||
<button id="export-pdf" type="button" class="btn btn-sm">Exportar a PDF</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="p-4 pt-0">
|
||||
|
||||
<div class="tabs tabs-boxed w-fit mb-4 md:hidden no-print">
|
||||
<button id="tab-sections" type="button" class="tab tab-active">Secciones</button>
|
||||
<button id="tab-output" type="button" class="tab">Vista previa</button>
|
||||
</div>
|
||||
<div class="flex gap-2 mb-4">
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
|
||||
<section id="panel-top" class="w-full md:w-1/2 border border-base-300 rounded-box p-4">
|
||||
<h2 class="text-lg font-semibold mb-2">Secciones</h2>
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<button id="add-title-section" type="button" class="btn btn-sm"
|
||||
data-type-label="Título" data-placeholder="Título...">+ Título</button>
|
||||
<button id="add-subtitle-section" type="button" class="btn btn-sm"
|
||||
@@ -41,17 +51,16 @@
|
||||
data-remove-title="Eliminar sección"></div>
|
||||
</section>
|
||||
|
||||
<section id="panel-output" class="w-1/2 border border-base-300 rounded-box p-4">
|
||||
<div class="flex items-center justify-between mb-2 no-print">
|
||||
<h2 class="text-lg font-semibold">Vista previa</h2>
|
||||
<button id="export-pdf" type="button" class="btn btn-sm">Exportar a PDF</button>
|
||||
</div>
|
||||
<section id="panel-output" class="w-full md:w-1/2 border border-base-300 rounded-box p-4 panel-hidden-mobile">
|
||||
<h2 class="text-lg font-semibold mb-2">Vista previa</h2>
|
||||
<div id="panel-output-text" class="text-sm"
|
||||
data-empty-message="Añade una sección para ver aquí el resultado."></div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Fuente de datos de los elementos arrastrables: se mantiene oculta y
|
||||
se clona (haciéndola visible) dentro de cada sección de arrastrar y soltar. -->
|
||||
<ul id="draggable-items-source" class="menu bg-base-100 w-full hidden">
|
||||
@@ -60,6 +69,6 @@
|
||||
</ul>
|
||||
</main>
|
||||
</body>
|
||||
<script src="js/main.js"></script>
|
||||
<script src="js/dnd.js"></script>
|
||||
|
||||
</html>
|
||||
|
||||
+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