Files
manuales-factorpesca/templates/manual.js
T
2026-10-13 21:22:54 -06:00

196 lines
6.5 KiB
JavaScript

/* =========================================================
manuales-factorpesca / manual.js
Lógica de presentación del manual:
- Genera el índice automático a partir de h1, h2, h3.
- Dibuja anotaciones numeradas sobre las capturas.
- Captura el nombre del capítulo actual para la cabecera.
- Numera figuras y pasos (complementa a los CSS counters).
- Prepara el contador de página tras la carga de fuentes.
Este script NO añade interactividad: solo prepara el DOM
para que Chromium imprima un PDF bien paginado.
========================================================= */
(function () {
'use strict';
document.addEventListener('DOMContentLoaded', init);
function init() {
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(afterFonts);
} else {
afterFonts();
}
}
function afterFonts() {
try {
buildTOC();
renderAnnotations();
setupChapterString();
ensureAnchors();
ensureImagePaths();
} catch (err) {
console.error('[manual.js] Error preparando el manual:', err);
}
}
/* ---------------------------------------------------------
1. Índice automático
--------------------------------------------------------- */
function buildTOC() {
const out = document.querySelector('[data-toc-output]');
if (!out) return;
const chapters = document.querySelectorAll('[data-chapter]');
if (!chapters.length) {
out.innerHTML = '<li class="toc-empty">Sin capítulos definidos.</li>';
return;
}
const frag = document.createDocumentFragment();
let chapterIndex = 0;
chapters.forEach((chapter) => {
chapterIndex += 1;
const title = chapter.querySelector('[data-chapter-title]') || chapter.querySelector('h1');
if (!title) return;
const li = document.createElement('li');
li.className = 'toc-h1';
li.appendChild(tocRow(romanize(chapterIndex) + '. ' + title.textContent.trim()));
frag.appendChild(li);
const subsections = chapter.querySelectorAll('h2');
subsections.forEach((sub) => {
const row = document.createElement('li');
row.className = 'toc-h2';
row.appendChild(tocRow(' ' + (sub.dataset.tocLabel || sub.textContent.trim())));
frag.appendChild(row);
});
const subsub = chapter.querySelectorAll('h3');
subsub.forEach((sub) => {
const row = document.createElement('li');
row.className = 'toc-h3';
row.appendChild(tocRow(' ' + (sub.dataset.tocLabel || sub.textContent.trim())));
frag.appendChild(row);
});
});
out.innerHTML = '';
out.appendChild(frag);
}
function tocRow(label) {
const span = document.createElement('span');
span.className = 'toc-row';
const text = document.createElement('span');
text.className = 'toc-text';
text.textContent = label;
const dots = document.createElement('span');
dots.className = 'toc-dots';
const num = document.createElement('span');
num.className = 'toc-page-num';
num.textContent = '—';
span.appendChild(text);
span.appendChild(dots);
span.appendChild(num);
return span;
}
function romanize(num) {
const map = [
['M', 1000], ['CM', 900], ['D', 500], ['CD', 400],
['C', 100], ['XC', 90], ['L', 50], ['XL', 40],
['X', 10], ['IX', 9], ['V', 5], ['IV', 4], ['I', 1],
];
let result = '';
let n = num;
for (const [letter, value] of map) {
while (n >= value) {
result += letter;
n -= value;
}
}
return result;
}
/* ---------------------------------------------------------
2. Anotaciones sobre capturas
--------------------------------------------------------- */
function renderAnnotations() {
document.querySelectorAll('.annotations[data-annotations]').forEach((list) => {
let data = [];
try {
data = JSON.parse(list.getAttribute('data-annotations') || '[]');
} catch (err) {
console.warn('[manual.js] data-annotations inválido:', err);
return;
}
if (!Array.isArray(data) || data.length === 0) {
list.remove();
return;
}
const sorted = data.slice().sort((a, b) => (a.n || 0) - (b.n || 0));
sorted.forEach((entry) => {
const li = document.createElement('li');
li.className = 'annotation';
li.textContent = String(entry.n);
if (typeof entry.x === 'number' && typeof entry.y === 'number') {
li.style.left = entry.x + '%';
li.style.top = entry.y + '%';
}
if (entry.label) {
li.setAttribute('aria-label', entry.label);
li.setAttribute('title', entry.label);
}
list.appendChild(li);
});
});
}
/* ---------------------------------------------------------
3. Cabecera dinámica con el nombre del capítulo actual
--------------------------------------------------------- */
function setupChapterString() {
const chapters = document.querySelectorAll('[data-chapter]');
chapters.forEach((chapter) => {
const title = chapter.querySelector('[data-chapter-title]') || chapter.querySelector('h1');
if (!title) return;
title.setAttribute('data-string', title.textContent.trim());
});
}
/* ---------------------------------------------------------
4. Anclas automáticas para headings
--------------------------------------------------------- */
function ensureAnchors() {
const headings = document.querySelectorAll('[data-chapter] h1, [data-chapter] h2, [data-chapter] h3, [data-chapter] h4');
headings.forEach((h) => {
if (!h.id) {
const slug = (h.textContent || '')
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
h.id = slug || 'h-' + Math.random().toString(36).slice(2, 8);
}
});
}
/* ---------------------------------------------------------
5. Resolver rutas de imágenes a absolutas
--------------------------------------------------------- */
function ensureImagePaths() {
const base = window.location.href.replace(/[^/]*$/, '');
document.querySelectorAll('img').forEach((img) => {
const src = img.getAttribute('src');
if (!src || src.startsWith('http') || src.startsWith('data:') || src.startsWith('file:')) return;
const absolute = new URL(src, base).href;
img.src = absolute;
});
}
})();