2025-12-09 14:10:02 -06:00

393 lines
14 KiB
JavaScript

/**
* Configuración global del curso.
* @namespace
* @property {string} COURSE_CONFIG_URL - Ruta al archivo de configuración JSON del curso.
* @property {boolean} DEBUG - Habilita/deshabilita el modo de depuración.
*/
window.COURSE_CONFIG = {
COURSE_CONFIG_URL: 'config.json',
DEBUG: false,
SHOW_PAGINATION: false, // Bandera para mostrar/ocultar paginación
SHOW_TITLE: false, // Bandera para mostrar/ocultar título
SHOW_GLOSSARY: false, // Bandera para mostrar/ocultar glosario
};
/**
* Maneja la lógica del glosario para un botón específico
* @param {HTMLElement} button - El botón del glosario
*/
async function handleGlossaryButton(button) {
const offcanvasEl = document.getElementById('offcanvasGlossary');
if (!offcanvasEl) {
console.error('Elemento offcanvasGlossary no encontrado');
return;
}
const offcanvas = new bootstrap.Offcanvas(offcanvasEl);
try {
const res = await fetch('manual_pld_ft.html');
if (!res.ok) throw new Error('Error al cargar el glosario');
const text = await res.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const main = doc.querySelector('main');
offcanvasEl.querySelector('.offcanvas-body').innerHTML = main
? main.innerHTML
: '<p class="text-danger">No se encontró el contenido.</p>';
const item = CourseNav.getCurrentSlide();
const tituloActual = item.title;
offcanvasEl.querySelectorAll('section[data-title]').forEach((seccion) => {
seccion.classList.toggle('d-none', seccion.dataset.title !== tituloActual);
});
offcanvas.show();
} catch (err) {
console.error('Error en glosario:', err);
offcanvasEl.querySelector('.offcanvas-body').innerHTML =
'<p class="text-danger">Error al cargar el glosario. Recargue la página e intente nuevamente.</p>';
offcanvas.show();
}
}
/**
* Configura los event listeners para el glosario
*/
function setupGlossaryListeners() {
// Event delegation para manejar clicks en cualquier botón con la clase 'btn-glossary'
document.body.addEventListener('click', function (e) {
const glossaryBtn = e.target.closest('.btn-glossary');
if (glossaryBtn) {
e.preventDefault();
handleGlossaryButton(glossaryBtn);
}
});
}
/**
* Navega a una sección específica, la muestra y hace scroll hacia ella.
* @function gotoSection
* @param {string|number} sectionId - ID de la sección (con o sin #) o número de sección.
* @param {Object} [options={}] - Opciones de scroll.
* @param {string} [options.behavior="smooth"] - Comportamiento del scroll ('auto' o 'smooth').
* @param {string} [options.block="start"] - Alineación vertical ('start', 'center', 'end' o 'nearest').
* @example
* // Ejemplos de uso:
* gotoSection('sec1'); // Va a #sec1
* gotoSection('#sec2'); // Va a #sec2
* gotoSection(3); // Va a #sec3
*/
function gotoSection(sectionId, options = {}) {
const defaults = { behavior: 'smooth', block: 'start' };
const opts = Object.assign(defaults, options);
// Normalizar el ID de la sección
let targetId = sectionId;
if (typeof sectionId === 'number') {
targetId = `sec${sectionId}`;
} else if (typeof sectionId === 'string' && !sectionId.startsWith('#')) {
targetId = sectionId.startsWith('sec') ? sectionId : `sec${sectionId}`;
} else if (sectionId.startsWith('#')) {
targetId = sectionId.substring(1);
}
const targetElement = document.getElementById(targetId);
if (targetElement) {
// Mostrar la sección si está oculta
if (targetElement.style.display === 'none') {
targetElement.style.display = '';
}
// Hacer scroll hacia la sección
targetElement.scrollIntoView(opts);
return true;
}
console.warn(`Sección ${targetId} no encontrada`);
return false;
}
/**
* Desplaza la ventana hasta la parte superior de un elemento.
* @function scrollToElementTop
* @param {string} selector - Selector CSS del elemento objetivo.
* @param {Object} [options={}] - Opciones de scroll.
* @param {string} [options.behavior="smooth"] - Comportamiento del scroll ('auto' o 'smooth').
* @param {string} [options.block="start"] - Alineación vertical ('start', 'center', 'end' o 'nearest').
* @param {string} [options.inline="nearest"] - Alineación horizontal ('start', 'center', 'end' o 'nearest').
* @example
* // Ejemplo de uso:
* scrollToElementTop('#main-content', { behavior: 'smooth', block: 'start' });
*/
function scrollToElementTop(selector, options = {}) {
const defaults = { behavior: 'smooth', block: 'start', inline: 'nearest' };
const opts = Object.assign(defaults, options);
const el = document.querySelector(selector);
if (el) el.scrollIntoView(opts);
}
/**
* Observador de intersección para animar elementos cuando son visibles en el viewport.
* @function animateOnScroll
* @param {string} selector - Selector de los elementos a observar.
* @param {string} animationClass - Clase de animación de Animate.css (ej. 'animate__fadeInUp').
* @param {Object} [options={}] - Opciones de configuración.
* @param {number} [options.threshold=0.1] - Umbral de visibilidad (0-1).
* @param {boolean} [options.animateOnce=true] - Si es true, la animación solo se ejecuta una vez.
* @param {string} [options.prefix='animate__animated'] - Prefijo para clases de animación.
* @returns {IntersectionObserver} Instancia del observador.
* @example
* // Ejemplo de uso:
* animateOnScroll('.animar', 'animate__fadeIn', { threshold: 0.2 });
*/
function animateOnScroll(selector, animationClass, options = {}) {
const { threshold = 0.1, animateOnce = true, prefix = 'animate__animated' } = options;
const cb = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add(prefix, animationClass);
if (animateOnce) observer.unobserve(entry.target);
} else if (!animateOnce) {
entry.target.classList.remove(prefix, animationClass);
}
});
};
const observer = new IntersectionObserver(cb, { threshold });
document.querySelectorAll(selector).forEach((el) => observer.observe(el));
return observer;
}
/**
* Configura los event listeners cuando el DOM está completamente cargado.
* @event DOMContentLoaded
*/
document.addEventListener('DOMContentLoaded', () => {
setupGlossaryListeners();
// Inicializar sal.js
if (typeof sal !== 'undefined' && document.querySelectorAll('[data-sal]').length > 0) {
setTimeout(() => {
document.querySelectorAll('[data-sal]').forEach((el) => (el.style.visibility = 'visible'));
sal({
once: false,
threshold: 0.3,
duration: 10000,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
distance: '100px',
opacity: 0.2,
scale: 0.85,
});
}, 200);
}
/**
* Evento antes de cambiar de slide.
* @event beforeSlideChange
* @property {Object} detail - Detalles del evento.
* @property {number} detail.currentIndex - Índice del slide actual.
* @property {Array} detail.contentArray - Array completo de contenido.
*/
document.body.addEventListener('beforeSlideChange', (e) => {
// console.log("Antes de cambiar de slide:", e.detail);
});
/**
* Evento al cambiar de slide.
* @event slideChange
* @property {Object} detail - Detalles del evento.
* @property {number} detail.slideIndex - Índice del nuevo slide.
* @property {Array} detail.contentArray - Array completo de contenido.
*/
document.body.addEventListener('slideChange', (e) => {
if (e.detail && typeof e.detail.slideIndex === 'number' && Array.isArray(e.detail.contentArray)) {
console.log(e.detail.contentArray[e.detail.slideIndex].content);
// Inicializar sal.js si hay elementos con data-sal
if (typeof sal !== 'undefined' && document.querySelectorAll('[data-sal]').length > 0) {
setTimeout(() => {
document.querySelectorAll('[data-sal]').forEach((el) => (el.style.visibility = 'visible'));
sal({
once: false,
threshold: 0.3,
duration: 10000,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
distance: '100px',
opacity: 0.2,
scale: 0.85,
});
}, 200);
}
// Paginación
const paginationEl = document.getElementById('pagination');
const paginacionScoEl = document.querySelector('.paginacion_sco');
if (window.COURSE_CONFIG.SHOW_PAGINATION) {
if (paginationEl) {
paginationEl.innerHTML = e.detail.slideIndex + 1 + ' / ' + e.detail.contentArray.length;
}
if (paginacionScoEl) {
paginacionScoEl.classList.remove('isFalse');
}
} else {
if (paginacionScoEl) {
paginacionScoEl.classList.add('isFalse');
}
}
// Título de la pantalla actual
const titleScoEl = document.getElementById('titleSco');
if (titleScoEl) {
if (window.COURSE_CONFIG.SHOW_TITLE) {
titleScoEl.classList.remove('d-none');
if (e.detail.contentArray[e.detail.slideIndex]) {
titleScoEl.innerHTML = e.detail.contentArray[e.detail.slideIndex].title || '';
}
} else {
titleScoEl.classList.add('d-none');
}
}
// Glosario
const btnGlossary = document.getElementById('btn-glossary');
if (btnGlossary) {
if (window.COURSE_CONFIG.SHOW_GLOSSARY) {
const noShowBtnGlossaryIn = [0, 1, e.detail.contentArray.length - 1];
btnGlossary.style.display = noShowBtnGlossaryIn.includes(e.detail.slideIndex) ? 'none' : 'block';
} else {
btnGlossary.style.display = 'none';
}
}
}
});
/**
* Evento al completar un slide.
* @event slideCompleted
* @property {Object} detail - Detalles del evento.
*/
document.body.addEventListener('slideCompleted', (e) => {
// console.log("Slide completado:", e.detail);
});
// Event listener para botón personalizado
const customButtonEvent = document.getElementById('btn-glossary');
if (customButtonEvent) {
customButtonEvent.addEventListener('click', async () => {
const offcanvasEl = document.getElementById('offcanvasGlossary');
const offcanvas = new bootstrap.Offcanvas(offcanvasEl);
try {
// Carga el HTML completo
const res = await fetch('manual_pld_ft.html');
const text = await res.text();
// Parsea el documento y extrae solo el <main>
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const main = doc.querySelector('main');
// Inserta el contenido dentro del offcanvas-body
offcanvasEl.querySelector('.offcanvas-body').innerHTML = main
? main.innerHTML
: '<p class="text-danger">No se encontró el contenido.</p>';
// **Ocultar todas las secciones del glosario y mostrar sólo la correspondiente**
const item = CourseNav.getCurrentSlide();
const tituloActual = item.title;
console.log('Slide actual:', tituloActual);
// console.warn(tituloActual);
// Selecciona únicamente las secciones dentro del offcanvas
offcanvasEl.querySelectorAll('section[data-title]').forEach((seccion) => {
if (seccion.dataset.title === tituloActual) {
seccion.classList.remove('d-none');
} else {
seccion.classList.add('d-none');
}
});
// Finalmente, muestra el offcanvas
offcanvas.show();
} catch (err) {
console.error(err);
offcanvasEl.querySelector('.offcanvas-body').innerHTML =
'<p class="text-danger">Error al cargar el glosario.</p>';
offcanvas.show();
}
});
}
});
/**
* Inicializa un Swiper con opciones personalizadas, reproducción opcional de audio por slide y detección de visita a todos los slides.
*
* @param {HTMLElement|string} swiperContainer - Selector CSS o elemento contenedor del Swiper.
* @param {Function} [callback] - Función que se ejecuta al visitar todos los slides.
* @param {Object} [options={}] - Opciones personalizadas de inicialización para Swiper (navigation, pagination, etc.).
* @param {Array} [audios=[]] - Arreglo opcional de objetos Howler (o similar) con audios asignados por slide.
* @returns {Swiper} - Instancia inicializada de Swiper.
* @example
* initializeSwiper('#mySwiper', () => {
* console.log('Todos los slides fueron visitados');
* }, {
* effect: 'flip',
* nextSelector: '.custom-next',
* prevSelector: '.custom-prev',
* paginationSelector: '.custom-pagination',
* }, [
* new Howl({ src: 'audio1.mp3' }),
* null, // Slide sin audio
* new Howl({ src: 'audio3.mp3' }),
* ]);
*
*/
function initializeSwiper(swiperContainer, callback, options = {}, audios = [], autoPlaySound = true) {
const visitedSlides = new Set();
const container = typeof swiperContainer === 'string' ? document.querySelector(swiperContainer) : swiperContainer;
const parent = container.parentElement;
if (options.navigation && options.navigation.nextEl && options.navigation.prevEl) {
options.navigation.nextEl = container.querySelector(options.navigation.nextEl)
? container.querySelector(options.navigation.nextEl)
: parent.querySelector(options.navigation.nextEl);
options.navigation.prevEl = container.querySelector(options.navigation.prevEl)
? container.querySelector(options.navigation.prevEl)
: parent.querySelector(options.navigation.prevEl);
}
const defaultParams = {
effect: 'slide',
loop: false,
autoHeight: true,
};
// Combina opciones personalizadas con las por defecto
const params = {
...defaultParams,
...options,
on: {
init: function () {
visitedSlides.add(this.activeIndex);
if (audios.length > 0 && audios[this.activeIndex] && autoPlaySound) {
CourseNav.audioController.stopAllSoundsAndPlay(audios[this.activeIndex]);
}
},
slideChange: function () {
const index = this.activeIndex;
visitedSlides.add(index);
if (audios.length > 0) {
CourseNav.audioController.stopAudio();
}
if (audios.length > 0 && audios[index]) {
CourseNav.audioController.stopAllSoundsAndPlay(audios[index]);
}
if (visitedSlides.size === this.slides.length && typeof callback === 'function') {
callback();
}
},
},
};
return new Swiper(container, params);
}