Manual_Vantive/js/coursenav.js
2025-10-08 11:06:36 -06:00

871 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = 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 = '<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 = 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<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;