feat: added more sections and page customization
This commit is contained in:
@@ -44,9 +44,17 @@
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@page { margin: 0; }
|
||||||
|
|
||||||
#panel-output,
|
#panel-output,
|
||||||
#panel-output * {
|
#panel-output * {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
/* Los navegadores no imprimen colores de fondo por defecto (dependen de
|
||||||
|
la opción "Gráficos de fondo" del diálogo de impresión, que el
|
||||||
|
usuario puede tener desmarcada); esto fuerza a respetar los colores
|
||||||
|
elegidos en una sección de "Nota" aunque esa opción esté desactivada. */
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
}
|
}
|
||||||
|
|
||||||
#panel-output .no-print {
|
#panel-output .no-print {
|
||||||
@@ -60,4 +68,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#preview-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-1
@@ -8,6 +8,10 @@
|
|||||||
<link rel="stylesheet" href="css/main.css">
|
<link rel="stylesheet" href="css/main.css">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||||
|
<!-- Rellenada por js/dnd.js con el @page (tamaño/orientación) elegido en
|
||||||
|
"Personalización de página": un <style> aparte porque @page no se
|
||||||
|
puede cambiar como una propiedad normal desde JS. -->
|
||||||
|
<style id="page-size-style"></style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -33,6 +37,59 @@
|
|||||||
|
|
||||||
<div class="p-4 pt-0">
|
<div class="p-4 pt-0">
|
||||||
|
|
||||||
|
<!-- Configuración única para todo el documento (no es una sección más:
|
||||||
|
no se repite, no se reordena ni se duplica), por eso vive fuera
|
||||||
|
de #panel-top-sections. <details> nativo para no necesitar JS
|
||||||
|
solo para plegarla/desplegarla. -->
|
||||||
|
<details class="border border-base-300 rounded-box p-4 mb-4 no-print">
|
||||||
|
<summary class="text-lg font-semibold cursor-pointer select-none">Personalización de página</summary>
|
||||||
|
<div class="flex flex-col gap-4 mt-4">
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4">
|
||||||
|
<label class="flex flex-col gap-1 flex-1">
|
||||||
|
<span class="text-sm">Título del patrón</span>
|
||||||
|
<input id="page-title-input" type="text" class="input input-bordered" placeholder="Título del patrón...">
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1 flex-1">
|
||||||
|
<span class="text-sm">Autor</span>
|
||||||
|
<input id="page-author-input" type="text" class="input input-bordered" placeholder="Autor...">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-end gap-4">
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="text-sm">Color de fondo</span>
|
||||||
|
<input id="page-bg-color" type="color" value="#ffffff">
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="text-sm">Tipografía</span>
|
||||||
|
<select id="page-font-select" class="select select-bordered select-sm">
|
||||||
|
<option value="sans-serif" selected>Sans-serif</option>
|
||||||
|
<option value="serif">Serif</option>
|
||||||
|
<option value="Georgia, serif">Georgia</option>
|
||||||
|
<option value="'Trebuchet MS', sans-serif">Trebuchet MS</option>
|
||||||
|
<option value="'Courier New', monospace">Monoespaciada</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="text-sm">Tamaño de página</span>
|
||||||
|
<select id="page-size-select" class="select select-bordered select-sm">
|
||||||
|
<option value="A4" selected>A4</option>
|
||||||
|
<option value="A3">A3</option>
|
||||||
|
<option value="A5">A5</option>
|
||||||
|
<option value="letter">Carta (Letter)</option>
|
||||||
|
<option value="legal">Legal</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="text-sm">Orientación</span>
|
||||||
|
<select id="page-orientation-select" class="select select-bordered select-sm">
|
||||||
|
<option value="portrait" selected>Vertical</option>
|
||||||
|
<option value="landscape">Horizontal</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<div class="tabs tabs-boxed w-fit mb-4 md:hidden no-print">
|
<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-sections" type="button" class="tab tab-active">Secciones</button>
|
||||||
<button id="tab-output" type="button" class="tab">Vista previa</button>
|
<button id="tab-output" type="button" class="tab">Vista previa</button>
|
||||||
@@ -53,6 +110,11 @@
|
|||||||
data-type-label="Subtítulo" data-placeholder="Subtítulo...">+ Subtítulo</button>
|
data-type-label="Subtítulo" data-placeholder="Subtítulo...">+ Subtítulo</button>
|
||||||
<button id="add-text-section" type="button" class="btn btn-sm"
|
<button id="add-text-section" type="button" class="btn btn-sm"
|
||||||
data-type-label="Texto" data-placeholder="Escribe aquí...">+ Texto</button>
|
data-type-label="Texto" data-placeholder="Escribe aquí...">+ Texto</button>
|
||||||
|
<button id="add-note-section" type="button" class="btn btn-sm"
|
||||||
|
data-type-label="Nota" data-placeholder="Escribe una nota o consejo..."
|
||||||
|
data-text-color-label="Color del texto" data-bg-color-label="Color de fondo">+ Nota</button>
|
||||||
|
<button id="add-materials-section" type="button" class="btn btn-sm"
|
||||||
|
data-type-label="Materiales" data-placeholder="Añadir material..." data-add-line-label="+ Añadir línea">+ Materiales</button>
|
||||||
<button id="add-image-section" type="button" class="btn btn-sm"
|
<button id="add-image-section" type="button" class="btn btn-sm"
|
||||||
data-type-label="Imagen">+ Imagen</button>
|
data-type-label="Imagen">+ Imagen</button>
|
||||||
<button id="add-dnd-section" type="button" class="btn btn-sm"
|
<button id="add-dnd-section" type="button" class="btn btn-sm"
|
||||||
@@ -68,7 +130,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="panel-output" class="w-full md:w-1/2 border border-base-300 rounded-box p-4 panel-hidden-mobile">
|
<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>
|
<h2 id="preview-title" class="text-lg font-semibold mb-2">Vista previa</h2>
|
||||||
<div id="panel-output-text" class="text-sm"
|
<div id="panel-output-text" class="text-sm"
|
||||||
data-empty-message="Añade una sección para ver aquí el resultado."></div>
|
data-empty-message="Añade una sección para ver aquí el resultado."></div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -5,6 +5,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const sectionsContainer = document.getElementById('panel-top-sections'); // Contiene las secciones que el usuario va añadiendo.
|
const sectionsContainer = document.getElementById('panel-top-sections'); // Contiene las secciones que el usuario va añadiendo.
|
||||||
const draggableItemsSource = document.getElementById('draggable-items-source'); // Lista oculta con los elementos disponibles para arrastrar: fuente de datos única, se clona (visible) en cada sección de tipo "arrastrar y soltar".
|
const draggableItemsSource = document.getElementById('draggable-items-source'); // Lista oculta con los elementos disponibles para arrastrar: fuente de datos única, se clona (visible) en cada sección de tipo "arrastrar y soltar".
|
||||||
const languageSelect = document.getElementById('language-select'); // Idioma del contenido (no de la interfaz): controla qué versión de los textos de título/subtítulo/texto se muestra y edita.
|
const languageSelect = document.getElementById('language-select'); // Idioma del contenido (no de la interfaz): controla qué versión de los textos de título/subtítulo/texto se muestra y edita.
|
||||||
|
const panelOutput = document.getElementById('panel-output');
|
||||||
|
|
||||||
|
// Controles de "Personalización de página": configuración única para todo
|
||||||
|
// el documento (no una sección más), ver el bloque más abajo.
|
||||||
|
const pageTitleInput = document.getElementById('page-title-input');
|
||||||
|
const pageAuthorInput = document.getElementById('page-author-input');
|
||||||
|
const pageBgColorInput = document.getElementById('page-bg-color');
|
||||||
|
const pageFontSelect = document.getElementById('page-font-select');
|
||||||
|
const pageSizeSelect = document.getElementById('page-size-select');
|
||||||
|
const pageOrientationSelect = document.getElementById('page-orientation-select');
|
||||||
|
const pageSizeStyle = document.getElementById('page-size-style');
|
||||||
|
|
||||||
// Idioma actualmente seleccionado para el contenido. La estructura de
|
// Idioma actualmente seleccionado para el contenido. La estructura de
|
||||||
// secciones (cuáles hay, su orden, su tipo) es la misma para todos los
|
// secciones (cuáles hay, su orden, su tipo) es la misma para todos los
|
||||||
@@ -395,9 +406,48 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const textarea = section.querySelector('textarea');
|
const textInput = section.querySelector('.text-input');
|
||||||
if (textarea) {
|
if (textInput) {
|
||||||
appendOutputLine(target, 'p', textarea.value.trim(), '');
|
appendOutputLine(target, 'p', textInput.value.trim(), '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nota/consejo: se destaca con un "alert" de daisyUI (para la forma:
|
||||||
|
// padding, bordes redondeados...) pero con los colores de texto y fondo
|
||||||
|
// que haya elegido el usuario, en vez de los del tema, para que se
|
||||||
|
// distinga a simple vista de un texto libre.
|
||||||
|
const noteInput = section.querySelector('.note-input');
|
||||||
|
if (noteInput) {
|
||||||
|
const text = noteInput.value.trim();
|
||||||
|
if (text) {
|
||||||
|
const note = document.createElement('div');
|
||||||
|
note.className = 'alert text-sm my-2';
|
||||||
|
const textColorInput = section.querySelector('.note-text-color');
|
||||||
|
const bgColorInput = section.querySelector('.note-bg-color');
|
||||||
|
if (textColorInput) note.style.color = textColorInput.value;
|
||||||
|
if (bgColorInput) note.style.backgroundColor = bgColorInput.value;
|
||||||
|
note.textContent = text;
|
||||||
|
target.appendChild(note);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Materiales: una línea por elemento de la lista, en vez de un único párrafo.
|
||||||
|
const materialsList = section.querySelector('.materials-list');
|
||||||
|
if (materialsList) {
|
||||||
|
const items = Array.from(materialsList.querySelectorAll('.material-input'))
|
||||||
|
.map(input => input.value.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (items.length) {
|
||||||
|
const ul = document.createElement('ul');
|
||||||
|
ul.className = 'list-disc list-inside my-1';
|
||||||
|
items.forEach(text => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = text;
|
||||||
|
ul.appendChild(li);
|
||||||
|
});
|
||||||
|
target.appendChild(ul);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,6 +475,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
function renderOutput() {
|
function renderOutput() {
|
||||||
outputText.innerHTML = '';
|
outputText.innerHTML = '';
|
||||||
|
|
||||||
|
// Título y autor del patrón (ver "Personalización de página"): son
|
||||||
|
// configuración de todo el documento, no una sección más, así que se
|
||||||
|
// pintan siempre delante, haya o no secciones añadidas.
|
||||||
|
appendOutputLine(outputText, 'h1', pageTitleInput.value.trim(), 'text-2xl font-bold mb-1');
|
||||||
|
appendOutputLine(outputText, 'p', pageAuthorInput.value.trim(), 'text-sm text-base-content/70 mb-2');
|
||||||
|
|
||||||
// Si aún no se ha añadido ninguna sección, avisar en vez de dejar el
|
// Si aún no se ha añadido ninguna sección, avisar en vez de dejar el
|
||||||
// resultado vacío.
|
// resultado vacío.
|
||||||
if (sectionsContainer.children.length === 0) {
|
if (sectionsContainer.children.length === 0) {
|
||||||
@@ -444,8 +500,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// tecla también se guarda en su mapa de traducciones para el idioma
|
// tecla también se guarda en su mapa de traducciones para el idioma
|
||||||
// actual, así no hace falta un paso aparte de "guardar" al cambiar de
|
// actual, así no hace falta un paso aparte de "guardar" al cambiar de
|
||||||
// idioma o al serializar la sección.
|
// idioma o al serializar la sección.
|
||||||
|
const TRANSLATABLE_FIELDS_SELECTOR = '.title-input, .subtitle-input, .material-input, textarea'; // textarea cubre tanto "Texto" (.text-input) como "Nota" (.note-input).
|
||||||
|
|
||||||
sectionsContainer.addEventListener('input', (e) => {
|
sectionsContainer.addEventListener('input', (e) => {
|
||||||
if (e.target.matches('.title-input, .subtitle-input, textarea')) {
|
if (e.target.matches(TRANSLATABLE_FIELDS_SELECTOR)) {
|
||||||
setTranslation(e.target, e.target.value);
|
setTranslation(e.target, e.target.value);
|
||||||
}
|
}
|
||||||
renderOutput();
|
renderOutput();
|
||||||
@@ -459,7 +517,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// nada en ese idioma), y se repinta la vista previa.
|
// nada en ese idioma), y se repinta la vista previa.
|
||||||
languageSelect.addEventListener('change', () => {
|
languageSelect.addEventListener('change', () => {
|
||||||
currentLanguage = languageSelect.value;
|
currentLanguage = languageSelect.value;
|
||||||
sectionsContainer.querySelectorAll('.title-input, .subtitle-input, textarea').forEach(el => {
|
sectionsContainer.querySelectorAll(TRANSLATABLE_FIELDS_SELECTOR).forEach(el => {
|
||||||
el.value = getTranslations(el)[currentLanguage] || '';
|
el.value = getTranslations(el)[currentLanguage] || '';
|
||||||
});
|
});
|
||||||
renderOutput();
|
renderOutput();
|
||||||
@@ -633,12 +691,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
container.appendChild(section);
|
container.appendChild(section);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sección de texto libre.
|
// Sección de texto libre. Se marca con la clase 'text-input' (además de
|
||||||
|
// 'textarea') para poder distinguirla de la de "Nota" en
|
||||||
|
// serializeSection()/renderOutputSection(): ambas son un <textarea>, así
|
||||||
|
// que buscar por la etiqueta genérica sería ambiguo.
|
||||||
function addTextSection(data, translations = {}, container = sectionsContainer) {
|
function addTextSection(data, translations = {}, container = sectionsContainer) {
|
||||||
const { section, body } = createSectionShell(data.typeLabel);
|
const { section, body } = createSectionShell(data.typeLabel);
|
||||||
|
|
||||||
const textarea = document.createElement('textarea');
|
const textarea = document.createElement('textarea');
|
||||||
textarea.className = 'textarea textarea-bordered w-full';
|
textarea.className = 'text-input textarea textarea-bordered w-full';
|
||||||
textarea.placeholder = data.placeholder;
|
textarea.placeholder = data.placeholder;
|
||||||
textarea.dataset.translations = JSON.stringify(translations);
|
textarea.dataset.translations = JSON.stringify(translations);
|
||||||
textarea.value = translations[currentLanguage] || '';
|
textarea.value = translations[currentLanguage] || '';
|
||||||
@@ -647,6 +708,102 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
container.appendChild(section);
|
container.appendChild(section);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Colores por defecto de una nota nueva (no traducibles: son los mismos
|
||||||
|
// para todos los idiomas, a diferencia del propio texto de la nota).
|
||||||
|
const NOTE_DEFAULT_TEXT_COLOR = '#075985';
|
||||||
|
const NOTE_DEFAULT_BG_COLOR = '#e0f2fe';
|
||||||
|
|
||||||
|
// Sección de nota/consejo: igual que la de texto libre, pero se muestra
|
||||||
|
// en el resultado como un aviso destacado con color de texto y de fondo
|
||||||
|
// elegidos por el usuario, para diferenciarla visualmente de un párrafo
|
||||||
|
// normal. `colors` ({ text, bg }), si se pasa, precarga los colores ya
|
||||||
|
// elegidos (al restaurar un patrón guardado o al duplicar la sección).
|
||||||
|
function addNoteSection(data, translations = {}, container = sectionsContainer, colors = {}) {
|
||||||
|
const { section, body } = createSectionShell(data.typeLabel);
|
||||||
|
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.className = 'note-input textarea textarea-bordered w-full';
|
||||||
|
textarea.placeholder = data.placeholder;
|
||||||
|
textarea.dataset.translations = JSON.stringify(translations);
|
||||||
|
textarea.value = translations[currentLanguage] || '';
|
||||||
|
|
||||||
|
const colorsRow = document.createElement('div');
|
||||||
|
colorsRow.className = 'flex items-center gap-4 mt-2';
|
||||||
|
|
||||||
|
const textColorLabel = document.createElement('label');
|
||||||
|
textColorLabel.className = 'flex items-center gap-1 text-sm';
|
||||||
|
textColorLabel.append(data.textColorLabel);
|
||||||
|
const textColorInput = document.createElement('input');
|
||||||
|
textColorInput.type = 'color';
|
||||||
|
textColorInput.className = 'note-text-color';
|
||||||
|
textColorInput.value = colors.text || NOTE_DEFAULT_TEXT_COLOR;
|
||||||
|
textColorLabel.appendChild(textColorInput);
|
||||||
|
|
||||||
|
const bgColorLabel = document.createElement('label');
|
||||||
|
bgColorLabel.className = 'flex items-center gap-1 text-sm';
|
||||||
|
bgColorLabel.append(data.bgColorLabel);
|
||||||
|
const bgColorInput = document.createElement('input');
|
||||||
|
bgColorInput.type = 'color';
|
||||||
|
bgColorInput.className = 'note-bg-color';
|
||||||
|
bgColorInput.value = colors.bg || NOTE_DEFAULT_BG_COLOR;
|
||||||
|
bgColorLabel.appendChild(bgColorInput);
|
||||||
|
|
||||||
|
colorsRow.appendChild(textColorLabel);
|
||||||
|
colorsRow.appendChild(bgColorLabel);
|
||||||
|
|
||||||
|
body.appendChild(textarea);
|
||||||
|
body.appendChild(colorsRow);
|
||||||
|
container.appendChild(section);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sección de materiales: una lista de líneas de texto libre (hilo, tipo
|
||||||
|
// de aguja, calibre...) en vez de un único campo, para poder añadir o
|
||||||
|
// quitar líneas según haga falta sin imponer un esquema rígido de campos.
|
||||||
|
// Cada línea es traducible por separado, igual que título/subtítulo/texto.
|
||||||
|
// `materials`, si se pasa, es un array de mapas { idioma: texto }, uno
|
||||||
|
// por línea (se usa al restaurar un patrón guardado o al duplicar).
|
||||||
|
function addMaterialsSection(data, materials = [], container = sectionsContainer) {
|
||||||
|
const { section, body } = createSectionShell(data.typeLabel);
|
||||||
|
|
||||||
|
const list = document.createElement('div');
|
||||||
|
list.className = 'materials-list flex flex-col gap-2';
|
||||||
|
|
||||||
|
function addMaterialRow(translations = {}) {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'flex items-center gap-2';
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.className = 'material-input input input-bordered input-sm w-full';
|
||||||
|
input.placeholder = data.placeholder;
|
||||||
|
input.dataset.translations = JSON.stringify(translations);
|
||||||
|
input.value = translations[currentLanguage] || '';
|
||||||
|
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.type = 'button';
|
||||||
|
removeBtn.className = 'btn btn-xs btn-circle';
|
||||||
|
removeBtn.textContent = '✕';
|
||||||
|
removeBtn.addEventListener('click', () => row.remove());
|
||||||
|
|
||||||
|
row.appendChild(input);
|
||||||
|
row.appendChild(removeBtn);
|
||||||
|
list.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
materials.forEach(addMaterialRow);
|
||||||
|
if (materials.length === 0) addMaterialRow(); // Al menos una línea para empezar.
|
||||||
|
|
||||||
|
const addLineBtn = document.createElement('button');
|
||||||
|
addLineBtn.type = 'button';
|
||||||
|
addLineBtn.className = 'btn btn-xs btn-outline w-fit';
|
||||||
|
addLineBtn.textContent = data.addLineLabel;
|
||||||
|
addLineBtn.addEventListener('click', () => addMaterialRow());
|
||||||
|
|
||||||
|
body.appendChild(list);
|
||||||
|
body.appendChild(addLineBtn);
|
||||||
|
container.appendChild(section);
|
||||||
|
}
|
||||||
|
|
||||||
// Sección de imagen: selector de archivo + vista previa (tamaño limitado
|
// Sección de imagen: selector de archivo + vista previa (tamaño limitado
|
||||||
// para que una foto de alta resolución no ocupe toda la pantalla).
|
// para que una foto de alta resolución no ocupe toda la pantalla).
|
||||||
// `dataUrl`, si se pasa, precarga la imagen (al restaurar un patrón guardado).
|
// `dataUrl`, si se pasa, precarga la imagen (al restaurar un patrón guardado).
|
||||||
@@ -745,6 +902,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
[addTitleBtn, addTitleSection],
|
[addTitleBtn, addTitleSection],
|
||||||
[addSubtitleBtn, addSubtitleSection],
|
[addSubtitleBtn, addSubtitleSection],
|
||||||
[addTextBtn, addTextSection],
|
[addTextBtn, addTextSection],
|
||||||
|
[addNoteBtn, addNoteSection],
|
||||||
|
[addMaterialsBtn, addMaterialsSection],
|
||||||
[addImageBtn, addImageSection],
|
[addImageBtn, addImageSection],
|
||||||
[addDndBtn, addDndSection],
|
[addDndBtn, addDndSection],
|
||||||
[addGroupBtn, addGroupSection],
|
[addGroupBtn, addGroupSection],
|
||||||
@@ -770,6 +929,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const addTitleBtn = document.getElementById('add-title-section');
|
const addTitleBtn = document.getElementById('add-title-section');
|
||||||
const addSubtitleBtn = document.getElementById('add-subtitle-section');
|
const addSubtitleBtn = document.getElementById('add-subtitle-section');
|
||||||
const addTextBtn = document.getElementById('add-text-section');
|
const addTextBtn = document.getElementById('add-text-section');
|
||||||
|
const addNoteBtn = document.getElementById('add-note-section');
|
||||||
|
const addMaterialsBtn = document.getElementById('add-materials-section');
|
||||||
const addImageBtn = document.getElementById('add-image-section');
|
const addImageBtn = document.getElementById('add-image-section');
|
||||||
const addDndBtn = document.getElementById('add-dnd-section');
|
const addDndBtn = document.getElementById('add-dnd-section');
|
||||||
const addGroupBtn = document.getElementById('add-group-section');
|
const addGroupBtn = document.getElementById('add-group-section');
|
||||||
@@ -777,6 +938,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
addTitleBtn.addEventListener('click', () => addTitleSection(addTitleBtn.dataset));
|
addTitleBtn.addEventListener('click', () => addTitleSection(addTitleBtn.dataset));
|
||||||
addSubtitleBtn.addEventListener('click', () => addSubtitleSection(addSubtitleBtn.dataset));
|
addSubtitleBtn.addEventListener('click', () => addSubtitleSection(addSubtitleBtn.dataset));
|
||||||
addTextBtn.addEventListener('click', () => addTextSection(addTextBtn.dataset));
|
addTextBtn.addEventListener('click', () => addTextSection(addTextBtn.dataset));
|
||||||
|
addNoteBtn.addEventListener('click', () => addNoteSection(addNoteBtn.dataset));
|
||||||
|
addMaterialsBtn.addEventListener('click', () => addMaterialsSection(addMaterialsBtn.dataset));
|
||||||
addImageBtn.addEventListener('click', () => addImageSection(addImageBtn.dataset));
|
addImageBtn.addEventListener('click', () => addImageSection(addImageBtn.dataset));
|
||||||
addDndBtn.addEventListener('click', () => addDndSection(addDndBtn.dataset));
|
addDndBtn.addEventListener('click', () => addDndSection(addDndBtn.dataset));
|
||||||
addGroupBtn.addEventListener('click', () => addGroupSection(addGroupBtn.dataset));
|
addGroupBtn.addEventListener('click', () => addGroupSection(addGroupBtn.dataset));
|
||||||
@@ -817,6 +980,57 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
: toggleCollapseAllBtn.dataset.collapseAllLabel;
|
: toggleCollapseAllBtn.dataset.collapseAllLabel;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Personalización de página: color de fondo, tipografía y tamaño/
|
||||||
|
// orientación del PDF. A diferencia de las secciones, es única para todo
|
||||||
|
// el documento (no se repite, reordena ni duplica), así que no pasa por
|
||||||
|
// serializeSections()/buildSectionsFrom(): se guarda aparte (ver más
|
||||||
|
// abajo, en el guardado/carga) y se aplica directamente sobre
|
||||||
|
// #panel-output.
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Aplica a #panel-output el color de fondo y la tipografía elegidos, el
|
||||||
|
// tamaño de página al diálogo de impresión (a través de un <style> propio,
|
||||||
|
// ya que @page no se puede tocar como una propiedad normal desde JS), y
|
||||||
|
// el título del patrón como título de la pestaña del navegador (que de
|
||||||
|
// paso el propio navegador suele usar como nombre sugerido al exportar a PDF).
|
||||||
|
function renderPageMeta() {
|
||||||
|
panelOutput.style.backgroundColor = pageBgColorInput.value;
|
||||||
|
panelOutput.style.fontFamily = pageFontSelect.value;
|
||||||
|
pageSizeStyle.textContent = `@page { size: ${pageSizeSelect.value} ${pageOrientationSelect.value}; }`;
|
||||||
|
document.title = pageTitleInput.value.trim() || 'Crochet';
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializePageSettings() {
|
||||||
|
return {
|
||||||
|
title: pageTitleInput.value,
|
||||||
|
author: pageAuthorInput.value,
|
||||||
|
bgColor: pageBgColorInput.value,
|
||||||
|
font: pageFontSelect.value,
|
||||||
|
pageSize: pageSizeSelect.value,
|
||||||
|
orientation: pageOrientationSelect.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPageSettings(settings = {}) {
|
||||||
|
pageTitleInput.value = settings.title || '';
|
||||||
|
pageAuthorInput.value = settings.author || '';
|
||||||
|
pageBgColorInput.value = settings.bgColor || '#ffffff';
|
||||||
|
pageFontSelect.value = settings.font || 'sans-serif';
|
||||||
|
pageSizeSelect.value = settings.pageSize || 'A4';
|
||||||
|
pageOrientationSelect.value = settings.orientation || 'portrait';
|
||||||
|
renderPageMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
[pageTitleInput, pageAuthorInput].forEach(el => el.addEventListener('input', () => {
|
||||||
|
renderPageMeta();
|
||||||
|
renderOutput();
|
||||||
|
}));
|
||||||
|
[pageBgColorInput, pageFontSelect, pageSizeSelect, pageOrientationSelect].forEach(el => {
|
||||||
|
el.addEventListener('input', renderPageMeta);
|
||||||
|
el.addEventListener('change', renderPageMeta);
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// Pestañas (solo en móvil, ver css/main.css): alternar entre el panel de
|
// 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
|
// "Secciones" y el de "Vista previa". A partir del breakpoint md la clase
|
||||||
@@ -826,7 +1040,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const tabSections = document.getElementById('tab-sections');
|
const tabSections = document.getElementById('tab-sections');
|
||||||
const tabOutput = document.getElementById('tab-output');
|
const tabOutput = document.getElementById('tab-output');
|
||||||
const panelOutput = document.getElementById('panel-output');
|
|
||||||
|
|
||||||
function showTab(tab) {
|
function showTab(tab) {
|
||||||
const showSections = tab === 'sections';
|
const showSections = tab === 'sections';
|
||||||
@@ -877,8 +1090,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const subtitleInput = section.querySelector('.subtitle-input');
|
const subtitleInput = section.querySelector('.subtitle-input');
|
||||||
if (subtitleInput) return { type: 'subtitle', text: getTranslations(subtitleInput) };
|
if (subtitleInput) return { type: 'subtitle', text: getTranslations(subtitleInput) };
|
||||||
|
|
||||||
const textarea = section.querySelector('textarea');
|
const textInput = section.querySelector('.text-input');
|
||||||
if (textarea) return { type: 'text', text: getTranslations(textarea) };
|
if (textInput) return { type: 'text', text: getTranslations(textInput) };
|
||||||
|
|
||||||
|
const noteInput = section.querySelector('.note-input');
|
||||||
|
if (noteInput) {
|
||||||
|
const textColorInput = section.querySelector('.note-text-color');
|
||||||
|
const bgColorInput = section.querySelector('.note-bg-color');
|
||||||
|
return {
|
||||||
|
type: 'note',
|
||||||
|
text: getTranslations(noteInput),
|
||||||
|
textColor: textColorInput ? textColorInput.value : undefined,
|
||||||
|
bgColor: bgColorInput ? bgColorInput.value : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const materialsList = section.querySelector('.materials-list');
|
||||||
|
if (materialsList) {
|
||||||
|
const materials = Array.from(materialsList.querySelectorAll('.material-input')).map(getTranslations);
|
||||||
|
return { type: 'materials', materials };
|
||||||
|
}
|
||||||
|
|
||||||
const dndCanvas = section.querySelector('.dnd-canvas');
|
const dndCanvas = section.querySelector('.dnd-canvas');
|
||||||
if (dndCanvas) {
|
if (dndCanvas) {
|
||||||
@@ -916,6 +1147,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (sectionData.type === 'title') addTitleSection(addTitleBtn.dataset, sectionData.text, container);
|
if (sectionData.type === 'title') addTitleSection(addTitleBtn.dataset, sectionData.text, container);
|
||||||
if (sectionData.type === 'subtitle') addSubtitleSection(addSubtitleBtn.dataset, sectionData.text, container);
|
if (sectionData.type === 'subtitle') addSubtitleSection(addSubtitleBtn.dataset, sectionData.text, container);
|
||||||
if (sectionData.type === 'text') addTextSection(addTextBtn.dataset, sectionData.text, container);
|
if (sectionData.type === 'text') addTextSection(addTextBtn.dataset, sectionData.text, container);
|
||||||
|
if (sectionData.type === 'note') {
|
||||||
|
addNoteSection(addNoteBtn.dataset, sectionData.text, container,
|
||||||
|
{ text: sectionData.textColor, bg: sectionData.bgColor });
|
||||||
|
}
|
||||||
|
if (sectionData.type === 'materials') addMaterialsSection(addMaterialsBtn.dataset, sectionData.materials, container);
|
||||||
if (sectionData.type === 'image') addImageSection(addImageBtn.dataset, sectionData.dataUrl, container);
|
if (sectionData.type === 'image') addImageSection(addImageBtn.dataset, sectionData.dataUrl, container);
|
||||||
if (sectionData.type === 'dnd') addDndSection(addDndBtn.dataset, sectionData.items, container);
|
if (sectionData.type === 'dnd') addDndSection(addDndBtn.dataset, sectionData.items, container);
|
||||||
if (sectionData.type === 'group') addGroupSection(addGroupBtn.dataset, sectionData.children, container);
|
if (sectionData.type === 'group') addGroupSection(addGroupBtn.dataset, sectionData.children, container);
|
||||||
@@ -952,14 +1188,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const SECTIONS_STORAGE_KEY = 'sections-data';
|
const SECTIONS_STORAGE_KEY = 'sections-data';
|
||||||
|
|
||||||
document.getElementById('save-pattern').addEventListener('click', () => {
|
document.getElementById('save-pattern').addEventListener('click', () => {
|
||||||
localStorage.setItem(SECTIONS_STORAGE_KEY, JSON.stringify(serializeSections()));
|
const data = { sections: serializeSections(), pageSettings: serializePageSettings() };
|
||||||
|
localStorage.setItem(SECTIONS_STORAGE_KEY, JSON.stringify(data));
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('load-pattern').addEventListener('click', () => {
|
document.getElementById('load-pattern').addEventListener('click', () => {
|
||||||
const raw = localStorage.getItem(SECTIONS_STORAGE_KEY);
|
const raw = localStorage.getItem(SECTIONS_STORAGE_KEY);
|
||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
buildSectionsFrom(JSON.parse(raw));
|
const data = JSON.parse(raw);
|
||||||
|
buildSectionsFrom(data.sections || []);
|
||||||
|
applyPageSettings(data.pageSettings);
|
||||||
|
renderOutput();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
renderPageMeta();
|
||||||
renderOutput(); // Pinta el mensaje de "sin secciones" nada más cargar la página.
|
renderOutput(); // Pinta el mensaje de "sin secciones" nada más cargar la página.
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user