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 = `
No hay contenido.
`; setupNavigation(); } function showStartOptions() { if (typeof Swal === "undefined") { currentIndex = confirm("¿Retomar tu progreso?") ? courseData.maximumAdvance : 0; initializeCourse(); } else { Swal.fire({ title: "¿Dónde quieres empezar?", text: "Retomar o comenzar de nuevo", icon: "question", showCancelButton: true, confirmButtonText: "Retomar", cancelButtonText: "Comenzar", target: MAIN_CONTENT, customClass: { confirmButton: "btn btn-primary", cancelButton: "btn btn-secondary", }, }).then((res) => { currentIndex = res.isConfirmed ? courseData.maximumAdvance : 0; initializeCourse(); }); } } /* ========================================================================== * === 6. Construcción y manejo del menú ==================================== * ========================================================================== */ function buildMenu() { COURSE_MENU.innerHTML = ""; (courseStructure.modules || []).forEach((module) => { const ul = document.createElement("ul"); ul.classList.add("course-menu"); ul.appendChild(createMenuItem(module)); COURSE_MENU.appendChild(ul); }); hideDuplicateLinks(); } function createMenuItem(item) { const li = document.createElement("li"); li.classList.add("menu-item"); const wdiv = document.createElement("div"); wdiv.classList.add("witem"); li.appendChild(wdiv); const link = document.createElement("a"); link.classList.add("coursenav-link"); link.textContent = item.title; const idx = courseData.contentArray.findIndex((c) => c.content === item.content && c.title === item.title); link.dataset.coursenavindex = idx; link.dataset.coursenavvisited = true; wdiv.appendChild(link); link.addEventListener("click", () => { CLICK_SOUND.play(); const index = parseInt(link.dataset.coursenavindex, 10); if (index >= 0) { // Permitir navegación libre desde el menú currentIndex = index; closeSidebar(); loadContent(); } else { const toggle = wdiv.querySelector(".toggle-icon"); toggle && toggle.click(); } }); if (item.topics?.length) { const toggle = document.createElement("span"); toggle.classList.add("toggle-icon"); toggle.innerHTML = ''; wdiv.appendChild(toggle); const subUl = document.createElement("ul"); subUl.classList.add("sub-ul", "open"); item.topics.forEach((sub) => subUl.appendChild(createMenuItem(sub))); li.appendChild(subUl); toggle.addEventListener("click", () => { CLICK_SOUND.play(); const isOpen = subUl.classList.toggle("open"); const icon = toggle.querySelector("i"); icon.classList.toggle("fa-square-chevron-down", isOpen); icon.classList.toggle("fa-square-chevron-right", !isOpen); }); } return li; } function hideDuplicateLinks() { function processUl(ul) { const seen = new Set(); Array.from(ul.children) .filter((el) => el.tagName === "LI") .forEach((li) => { const link = li.querySelector(":scope > .witem > .coursenav-link"); if (link) { const text = link.textContent.trim(); if (seen.has(text)) li.style.display = "none"; else seen.add(text); } li.querySelectorAll(":scope > ul").forEach(processUl); }); } document.querySelectorAll("#coursenav-main-menu > ul.course-menu").forEach(processUl); } function closeSidebar() { const offEl = document.getElementById("coursenav-offcanvas"); const bsOff = bootstrap.Offcanvas.getInstance(offEl) || new bootstrap.Offcanvas(offEl); bsOff.hide(); } function showLockedContentWarning() { if (typeof Swal === "undefined") { alert("Debes completar el contenido actual antes de avanzar."); } else { Swal.fire({ text: "Debes completar el contenido actual antes de avanzar.", icon: "warning", target: MAIN_CONTENT, customClass: { confirmButton: "btn btn-primary", cancelButton: "btn btn-warning", }, }); } } /* ========================================================================== * === 7. Carga de contenido y actualización de la interfaz ================ * ========================================================================== */ function loadContent() { MAIN_CONTENT.innerHTML = ""; WRAP_COURSE_CONTENT.className = WRAP_COURSE_CONTENT.getAttribute("data-original-class"); window.scrollTo(0, 0); LOADER_ELEMENT.style.display = "block"; const item = courseData.contentArray[currentIndex]; if (!item?.content) return console.warn("Ítem inválido:", item); audioController.stopAudio(); Howler._howls?.forEach((h) => h.stop()); if (Swal.isVisible()) { Swal.close(); } // Al cerrar, limpiamos clases y atributos de html/body document.documentElement.classList.remove("swal2-shown", "swal2-height-auto"); document.body.classList.remove("swal2-shown", "swal2-height-auto"); document.documentElement.removeAttribute("aria-hidden"); document.body.removeAttribute("aria-hidden"); // Y volvemos a quitar aria-hidden de los scripts document.querySelectorAll("script[aria-hidden]").forEach((el) => el.removeAttribute("aria-hidden")); fetch(item.content, { cache: "no-store" }) .then((r) => { if (!r.ok) throw new Error(r.statusText); return r.text(); }) .then((html) => { //MAIN_CONTENT.innerHTML = html; //executeInjectedScripts(MAIN_CONTENT); $(MAIN_CONTENT).html(html); }) .catch((err) => { console.error("Error cargando contenido:", err); 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} Un objeto con * { "Título de módulo": porcentaje (0–100), … } */ function getProgressByModule() { // Recolectamos totales y visitados por módulo const stats = {}; courseData.contentArray.forEach((slide) => { const mod = slide.moduleTitle || "Sin módulo"; if (!stats[mod]) stats[mod] = { total: 0, visited: 0 }; stats[mod].total++; if (slide.visited) stats[mod].visited++; }); // Calculamos porcentajes const result = {}; Object.entries(stats).forEach(([mod, { total, visited }]) => { result[mod] = parseFloat(((visited / total) * 100).toFixed(2)); }); return result; } function setProgress(p) { setSuspendData(p); } function getProgress() { return getSuspendData() || { contentArray: [], maximumAdvance: 0 }; } function finishScorm() { if (pipwerks.SCORM.connection.isActive && !scormAPIUnloaded) { const elapsed = (Date.now() - sessionStartTime) / 1000; const hh = String(Math.floor(elapsed / 3600)).padStart(2, "0"); const mm = String(Math.floor((elapsed % 3600) / 60)).padStart(2, "0"); const ss = String(Math.floor(elapsed % 60)).padStart(2, "0"); pipwerks.SCORM.set("cmi.core.session_time", `${hh}:${mm}:${ss}`); pipwerks.SCORM.save(); pipwerks.SCORM.quit(); scormAPIUnloaded = true; } } function getScormData(key) { return pipwerks.SCORM.connection.isActive ? pipwerks.SCORM.get(key) ?? "" : sessionStorage.getItem(key) ?? ""; } function setScormData(key, value) { if (pipwerks.SCORM.connection.isActive) { const ok = pipwerks.SCORM.set(key, value); if (ok) pipwerks.SCORM.save(); return ok; } sessionStorage.setItem(key, value); return true; } /* ========================================================================== * === 11. Arranque DOM y offcanvas ========================================= * ========================================================================== */ document.addEventListener("DOMContentLoaded", () => { initializeScorm(() => loadConfig()); window.addEventListener("beforeunload", finishScorm); // Tooltips var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); tooltipTriggerList.map((el) => new bootstrap.Tooltip(el)); // Offcanvas backdrop dentro de custom container const offcanvasEl = document.getElementById("coursenav-offcanvas"); const customContainer = document.getElementById("wrap-course-content"); const bsOffcanvas = bootstrap.Offcanvas.getOrCreateInstance(offcanvasEl); offcanvasEl.addEventListener("shown.bs.offcanvas", () => { if (customContainer.querySelector(".offcanvas-backdrop")) return; const backdrop = document.createElement("div"); backdrop.className = "offcanvas-backdrop fade"; customContainer.appendChild(backdrop); backdrop.getBoundingClientRect(); backdrop.classList.add("show"); backdrop.addEventListener("click", () => bsOffcanvas.hide()); }); offcanvasEl.addEventListener("hidden.bs.offcanvas", () => { customContainer.querySelector(".offcanvas-backdrop")?.remove(); }); }); /* ========================================================================== * === 12. API pública ======================================================= * ========================================================================== */ return { /* Audio */ audioController, createSound, soundClick, /* Debug */ isDebug, /* SCORM Básico */ getStudentName: () => getScormData("cmi.core.student_name"), getLessonLocation, setLessonLocation, getLessonStatus, setLessonStatus, getScore, setScore, getSuspendData, setSuspendData, getScormData, setScormData, /* Navegación */ nextSlide: () => navigate(1), prevSlide: () => navigate(-1), gotoSlide, isVisited: () => courseData.contentArray[currentIndex]?.visited || false, isCompletedSlideIndex: (idx) => (idx >= 0 && idx < courseData.contentArray.length ? courseData.contentArray[idx].visited : undefined), /* Estado del curso */ getCurrentSlide: () => courseData.contentArray[currentIndex], getCurrentIndex: () => currentIndex, getCourseData: () => courseData, getCourseStructure: () => courseStructure, getCourseConfig: () => COURSE_CONFIG, getCourseTitle: () => courseStructure?.title || "", getCourseModules: () => courseStructure?.modules || [], getCourseContentArray: () => courseData.contentArray, /* Curso actual */ resetCourse, markSlidesAsVisited, setSlideVisited, completeLesson: () => setLessonStatus("completed"), updateProgressBar, getProgressPercent, getProgressByModule, getCurrentModuleSlides: () => { const module = courseData.contentArray[currentIndex]?.moduleTitle; return courseData.contentArray.filter((s) => s.moduleTitle === module); }, getCurrentModuleTitle: () => courseData.contentArray[currentIndex]?.moduleTitle || "", getCurrentCourseTitle: () => courseData.contentArray[currentIndex]?.courseTitle || "", /* SCORM avanzado */ save: () => (pipwerks.SCORM.connection.isActive ? pipwerks.SCORM.save() : setProgress(courseData)), reload: loadContent, loadModule: (moduleTitle) => { const idx = courseData.contentArray.findIndex((s) => s.moduleTitle === moduleTitle); if (idx >= 0) { currentIndex = idx; loadContent(); } }, }; })(COURSE_CONFIG); window.CourseNav = CourseNav;