var CourseNav = (function (COURSE_CONFIG) { "use strict"; /* ========================================================================== * === 1. Inyección y ejecución de scripts de contenido dinámico ============ * ========================================================================== */ const loadedScriptSrcs = new Set(); function executeInjectedScripts(container) { // 1) Scripts externos (src) container.querySelectorAll("script[src]").forEach((old) => { const src = old.src; if (!loadedScriptSrcs.has(src)) { loadedScriptSrcs.add(src); const s = document.createElement("script"); s.src = src; s.async = false; document.body.appendChild(s); s.addEventListener("load", () => s.remove()); } old.remove(); }); // 2) Scripts inline container.querySelectorAll("script:not([src])").forEach((old) => { try { (0, eval)(old.textContent); } catch (e) { console.error("Error al ejecutar script inline:", e); } old.remove(); }); } /* ========================================================================== * === 2. Extensión de Howl para eventos de tiempo =========================== * ========================================================================== */ class ExtendedHowl extends Howl { constructor(options) { super(options); this._timeupdateListeners = []; this._interval = null; this._startTimeUpdate(); } _startTimeUpdate() { this._interval = setInterval(() => { if (this.playing()) this._emitTimeUpdate(this.seek()); }, 250); } _emitTimeUpdate(currentTime) { this._timeupdateListeners.forEach((cb) => cb(currentTime)); } onTimeUpdate(cb) { this._timeupdateListeners.push(cb); } offTimeUpdate(cb) { this._timeupdateListeners = this._timeupdateListeners.filter((l) => l !== cb); } play(id) { const result = super.play(id); if (!this._interval) this._startTimeUpdate(); return result; } pause(id) { const result = super.pause(id); clearInterval(this._interval); this._interval = null; return result; } stop(id) { super.stop(id); clearInterval(this._interval); this._interval = null; } } function createSound(audioUrl) { return audioUrl ? new ExtendedHowl({ src: [audioUrl] }) : null; } /* ========================================================================== * === 3. Controlador de audio =============================================== * ========================================================================== */ class AudioController { constructor() { this.audioElement = null; this.audioControlButton = document.getElementById("coursenav-audio-control"); this.audioIcon = document.getElementById("coursenav-audio-icon"); this.progressCircle = document.getElementById("coursenav-progress-circle"); this.isMuted = false; if (this.progressCircle) this.progressCircle.style.display = "none"; if (this.audioControlButton) { this.audioControlButton.addEventListener("click", this.toggleAudio.bind(this)); } } stopAllSoundsAndPlay(howl) { Howler._howls?.forEach((sound) => sound.stop()); this.setAudio(howl); this.playAudio(); } loadAudio(url) { if (this.audioElement) this.audioElement.stop(); if (!url) return; this.audioElement = createSound(url); this._bindAudioEvents(); } playAudio() { this.audioElement?.play(); } pauseAudio() { this.audioElement?.pause(); } stopAudio() { this.audioElement?.stop(); } toggleAudio() { if (!this.audioElement) return; this.audioElement.playing() ? this.pauseAudio() : this.playAudio(); } toggleMute() { this.isMuted = !this.isMuted; Howler.mute(this.isMuted); this.updateIcon(); document.querySelectorAll("video").forEach((v) => (v.muted = this.isMuted)); } onPlay() { if (this.progressCircle) this.progressCircle.style.display = "block"; const t = this.audioElement.seek(); this.updateProgressCircle(t); this.updateIcon(); } onEnd() { if (this.progressCircle) this.progressCircle.style.display = "none"; this.updateIcon(); } updateIcon() { if (!this.audioIcon) return; const playing = this.audioElement?.playing(); this.audioIcon.className = playing ? "fa-duotone fa-solid fa-pause" : "fa-duotone fa-solid fa-play"; } updateProgressCircle(currentTime) { if (!this.progressCircle || !this.audioElement) return; const r = parseFloat(this.progressCircle.getAttribute("r")); const circ = 2 * Math.PI * r; const offset = circ - (currentTime / this.audioElement.duration()) * circ; this.progressCircle.setAttribute("stroke-dashoffset", offset); } setAudioUrl(url) { this.loadAudio(url); } setAudio(howl) { if (!(howl instanceof ExtendedHowl)) return; this.audioElement?.stop(); this.audioElement = howl; this._bindAudioEvents(); } _bindAudioEvents() { this.audioElement.on("play", this.onPlay.bind(this)); this.audioElement.on("pause", this.updateIcon.bind(this)); this.audioElement.on("stop", this.updateIcon.bind(this)); this.audioElement.on("end", this.onEnd.bind(this)); this.audioElement.onTimeUpdate(this.updateProgressCircle.bind(this)); } } const audioController = new AudioController(); /* ========================================================================== * === 4. Configuración global y constantes ================================= * ========================================================================== */ const COURSE_CONFIG_URL = COURSE_CONFIG.COURSE_CONFIG_URL || "config.json"; const DEBUG = COURSE_CONFIG.DEBUG || false; const KEY_APP = COURSE_CONFIG.KEY || Infinity; const MAIN_CONTENT = document.getElementById("coursenav-main-content"); const WRAP_COURSE_CONTENT = document.getElementById("wrap-course-content"); WRAP_COURSE_CONTENT.setAttribute("data-original-class", WRAP_COURSE_CONTENT.className); const LOADER_ELEMENT = document.getElementById("coursenav-loader-course"); const CLICK_SOUND = new Audio("audio/click.mp3"); const PREV_BTN = document.getElementById("coursenav-prev-btn"); const NEXT_BTN = document.getElementById("coursenav-next-btn"); const COURSE_PROGRESS_BAR = document.getElementById("coursenav-progress-bar"); const COURSE_MENU = document.getElementById("coursenav-main-menu"); pipwerks.SCORM.version = "1.2"; pipwerks.debug.isActive = DEBUG; pipwerks.SCORM.handleExitMode = false; let sessionStartTime; let scormAPIUnloaded = false; let courseStructure = null; let courseData = { contentArray: [], maximumAdvance: 0 }; let currentIndex = 0; /* ========================================================================== * === 5. SCORM: Inicialización y helpers de alto nivel ===================== * ========================================================================== */ function initializeScorm(callback) { const scorm = pipwerks.SCORM; const connected = scorm.init(); if (connected) { const status = scorm.get("cmi.core.lesson_status"); if (status === "not attempted") { scorm.set("cmi.core.lesson_status", "incomplete"); scorm.save(); } sessionStartTime = Date.now(); } else { // SCORM API no encontrada. Usando sessionStorage para web. } callback(connected); } function buildContentArray(mods, courseTitle = "", moduleTitle = "", parentTitle = null) { mods.forEach((m) => { const isModuleLevel = !parentTitle && !moduleTitle; const currentModuleTitle = isModuleLevel ? m.title : moduleTitle; if (m.content) { courseData.contentArray.push({ title: m.title, content: m.content, audio: m.audio, visited: false, courseTitle, moduleTitle: currentModuleTitle, parentTitle, }); } if (m.topics) { buildContentArray(m.topics, courseTitle, currentModuleTitle, m.title); } }); } function verifyContentArray(saved, curr) { if (!saved || !curr || saved.length !== curr.length) return false; return saved.every((s, i) => s.title === curr[i].title && s.content === curr[i].content); } function loadConfig() { const xhr = new XMLHttpRequest(); xhr.open("GET", `${COURSE_CONFIG_URL}?_=${Date.now()}`, true); xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.withCredentials = true; xhr.responseType = "json"; xhr.onload = function () { if (xhr.status >= 200 && xhr.status < 300) { courseStructure = xhr.response; courseData = { contentArray: [], maximumAdvance: 0 }; if (courseStructure.title) document.title = courseStructure.title; buildContentArray(courseStructure.modules, courseStructure.title || ""); const saved = getProgress(); if (saved.contentArray && verifyContentArray(saved.contentArray, courseData.contentArray)) { courseData = saved; } if (courseData.maximumAdvance > 0) showStartOptions(); else initializeCourse(); setProgress(courseData); } else { MAIN_CONTENT?.remove(); console.error("Error cargando config:", xhr.status, xhr.statusText); } }; xhr.onerror = function () { MAIN_CONTENT?.remove(); console.error("Error de red al cargar config"); }; xhr.send(); } function initializeCourse() { if (COURSE_MENU) { buildMenu(); hideDuplicateLinks(); } if (courseData.contentArray.length > 0) loadContent(); else MAIN_CONTENT.innerHTML = `
${err.message}`;
})
.finally(() => {
courseData.maximumAdvance = Math.max(courseData.maximumAdvance, currentIndex);
LOADER_ELEMENT.style.display = "none";
updateUITemplate();
triggerSlideChange(currentIndex, courseData.contentArray);
});
}
function updateUITemplate() {
setProgress(courseData);
updateNavigationButtons();
updateProgressBar();
updateCourseNavLinks();
}
function updateCourseNavLinks() {
document.querySelectorAll(".coursenav-link").forEach((link) => {
const idx = parseInt(link.dataset.coursenavindex, 10);
const item = courseData.contentArray[idx];
if (item) {
link.dataset.coursenavvisited = true;
link.classList.add("visited");
}
});
}
function setupNavigation() {
PREV_BTN?.addEventListener("click", () => {
CLICK_SOUND.play();
navigate(-1);
});
NEXT_BTN?.addEventListener("click", () => {
CLICK_SOUND.play();
navigate(1);
});
updateNavigationButtons();
}
function navigate(dir) {
triggerBeforeSlideChange(currentIndex, courseData.contentArray);
const newIndex = currentIndex + dir;
if (newIndex < 0 || newIndex >= courseData.contentArray.length) return;
if (dir === -1 || courseData.contentArray[currentIndex].visited || DEBUG) {
currentIndex = newIndex;
loadContent();
} else {
showLockedContentWarning();
}
updateNavigationButtons();
}
function updateNavigationButtons() {
if (!courseData.contentArray.length || !courseData.contentArray[currentIndex]) {
PREV_BTN.disabled = NEXT_BTN.disabled = true;
return;
}
PREV_BTN.disabled = currentIndex === 0;
NEXT_BTN.disabled = currentIndex >= courseData.contentArray.length - 1 || (!courseData.contentArray[currentIndex].visited && !DEBUG);
}
function updateProgressBar() {
const visited = courseData.contentArray.filter((i) => i.visited).length;
const pct = (visited / courseData.contentArray.length) * 100;
COURSE_PROGRESS_BAR.style.width = pct + "%";
COURSE_PROGRESS_BAR.setAttribute("aria-valuenow", pct.toFixed(2));
COURSE_PROGRESS_BAR.textContent = `${pct.toFixed(0)}%`;
}
/* ==========================================================================
* === 8. Eventos custom =====================================================
* ========================================================================== */
function triggerSlideChange(currentIndex, contentArray) {
document.body.dispatchEvent(
new CustomEvent("slideChange", {
detail: { message: "Slide changed!", slideIndex: currentIndex, contentArray },
})
);
}
function triggerSlideCompleted(index, total, slideObj) {
document.body.dispatchEvent(
new CustomEvent("slideCompleted", {
detail: { message: "Slide completed!", slideIndex: index, totalSlides: total, slide: slideObj },
})
);
}
function triggerBeforeSlideChange(currentIndex, contentArray) {
document.body.dispatchEvent(
new CustomEvent("beforeSlideChange", {
detail: { message: "Before slide change!", currentIndex, contentArray },
})
);
}
/* ==========================================================================
* === 9. API públicas y utilitarios =========================================
* ========================================================================== */
function setSlideVisited(state = true) {
courseData.contentArray[currentIndex].visited = state;
updateUITemplate();
triggerSlideCompleted(currentIndex, courseData.contentArray.length, courseData.contentArray[currentIndex]);
}
function markSlidesAsVisited(indices) {
indices
.sort((a, b) => b - a)
.forEach((i) => {
currentIndex = i;
setSlideVisited(true);
});
}
function resetCourse() {
courseData.contentArray.forEach((i) => (i.visited = false));
courseData.maximumAdvance = 0;
currentIndex = 0;
updateUITemplate();
loadContent();
}
function soundClick() {
CLICK_SOUND.play();
}
function isDebug() {
return DEBUG;
}
function gotoSlide(index) {
const i = Math.floor(index);
if (!isNaN(i) && i >= 0 && i < courseData.contentArray.length) {
currentIndex = i;
loadContent();
} else {
console.error("gotoSlide: índice inválido", index);
}
}
/* ==========================================================================
* === 10. SCORM: helpers de bajo nivel y progreso ============================
* ========================================================================== */
function getLessonLocation() {
if (pipwerks.SCORM.connection.isActive) {
const val = pipwerks.SCORM.get("cmi.core.lesson_location");
return val ?? "";
}
return sessionStorage.getItem("cmi.core.lesson_location") ?? "";
}
function setLessonLocation(loc) {
if (pipwerks.SCORM.connection.isActive) {
const ok = pipwerks.SCORM.set("cmi.core.lesson_location", loc);
if (ok) pipwerks.SCORM.save();
return ok;
}
sessionStorage.setItem("cmi.core.lesson_location", loc);
return true;
}
function getLessonStatus() {
if (pipwerks.SCORM.connection.isActive) {
return pipwerks.SCORM.get("cmi.core.lesson_status") ?? "";
}
return sessionStorage.getItem("cmi.core.lesson_status") ?? "";
}
function setLessonStatus(st) {
if (pipwerks.SCORM.connection.isActive) {
const ok = pipwerks.SCORM.set("cmi.core.lesson_status", st);
if (ok) pipwerks.SCORM.save();
return ok;
}
sessionStorage.setItem("cmi.core.lesson_status", st);
return true;
}
function getScore() {
let val = pipwerks.SCORM.connection.isActive ? pipwerks.SCORM.get("cmi.core.score.raw") : sessionStorage.getItem("cmi.core.score.raw");
return val != null && val !== "" ? Number(val) : null;
}
function setScore(sc) {
if (pipwerks.SCORM.connection.isActive) {
const ok = pipwerks.SCORM.set("cmi.core.score.raw", sc);
if (ok) pipwerks.SCORM.save();
return ok;
}
sessionStorage.setItem("cmi.core.score.raw", sc);
return true;
}
function getSuspendData() {
let val = pipwerks.SCORM.connection.isActive ? pipwerks.SCORM.get("cmi.suspend_data") : sessionStorage.getItem("cmi.suspend_data");
if (val) {
try {
return JSON.parse(val);
} catch {
return val;
}
}
return "";
}
function setSuspendData(data) {
const json = JSON.stringify(data);
if (pipwerks.SCORM.connection.isActive) {
const ok = pipwerks.SCORM.set("cmi.suspend_data", json);
if (ok) pipwerks.SCORM.save();
return ok;
}
sessionStorage.setItem("cmi.suspend_data", json);
return true;
}
function getProgressPercent(byModule = false) {
if (!byModule) {
const visited = courseData.contentArray.filter((i) => i.visited).length;
return parseFloat(((visited / courseData.contentArray.length) * 100).toFixed(2));
}
const currentSlide = courseData.contentArray[currentIndex];
const moduleSlides = courseData.contentArray.filter((s) => s.moduleTitle === currentSlide.moduleTitle);
const visited = moduleSlides.filter((i) => i.visited).length;
return moduleSlides.length ? parseFloat(((visited / moduleSlides.length) * 100).toFixed(2)) : 0;
}
/**
* Obtiene el porcentaje de avance de cada módulo.
* @returns {Object