Manual_Vantive/js/coursenav.js

875 lines
29 KiB
JavaScript
Raw Normal View History

2025-09-25 11:08:16 -06:00
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 = `<div class='alert alert-warning'>No hay contenido.</div>`;
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 = idx >= 0 && courseData.contentArray[idx].visited;
wdiv.appendChild(link);
link.addEventListener("click", () => {
CLICK_SOUND.play();
const index = parseInt(link.dataset.coursenavindex, 10);
if (index >= 0) {
if (DEBUG || courseData.contentArray[index].visited) {
currentIndex = index;
closeSidebar();
loadContent();
} else {
closeSidebar();
showLockedContentWarning();
}
} 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 = '<i class="fa-duotone fa-solid fa-square-chevron-down"></i>';
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 = `<pre>${err.message}</pre>`;
})
.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 = item.visited;
item.visited ? link.classList.add("visited") : link.classList.remove("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<string, number>} Un objeto con
* { "Título de módulo": porcentaje (0100), }
*/
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;