393 lines
14 KiB
JavaScript
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);
|
||
|
|
}
|