Update: Base para Andractiv

This commit is contained in:
2025-12-11 09:09:57 -06:00
parent 701ffc78c6
commit cb7ef24da8
296 changed files with 47388 additions and 0 deletions
File diff suppressed because one or more lines are too long
+413
View File
@@ -0,0 +1,413 @@
(function (global) {
"use strict";
/**
* QuizManager: Clase principal para gestionar cuestionarios basados en un archivo Excel.
*/
class QuizManager {
/**
* @constructor
* @param {Object} config - Configuración del cuestionario.
* @param {string} config.excelFileUrl - URL del archivo Excel que contiene las preguntas.
* @param {Object} [config.mandatoryFields] - Campos obligatorios en el archivo Excel para el cuestionario.
* @param {string} [config.mandatoryFields.question="pregunta"] - Nombre de la columna para el texto de la pregunta.
* @param {string} [config.mandatoryFields.correctOption="opcion_c"] - Nombre de la columna que identifica la respuesta correcta.
* @param {string} [config.mandatoryFields.correctFeedback="retroalimentacion_correcta"] - Nombre de la columna para la retroalimentación en respuestas correctas.
* @param {string} [config.mandatoryFields.incorrectFeedback="retroalimentacion_incorrecta"] - Nombre de la columna para la retroalimentación en respuestas incorrectas.
* @param {string} [config.optionPrefix="opcion"] - Prefijo utilizado para identificar las columnas de las opciones de respuesta.
* @param {boolean} [config.randomizeQuestions=false] - Indica si las preguntas deben presentarse en orden aleatorio.
* @param {boolean} [config.randomizeOptions=false] - Indica si las opciones dentro de una pregunta deben ordenarse aleatoriamente.
* @param {number} [config.passingScore=80] - Puntaje mínimo necesario para aprobar el cuestionario, expresado en porcentaje.
* @param {number} [config.defaultWeight=1] - Ponderación por defecto asignada a cada pregunta, si no se especifica en el archivo Excel.
* @param {number} [config.maxQuestions=Infinity] - Número máximo de preguntas a incluir en el cuestionario. Si no se especifica, se incluyen todas las preguntas.
* @param {number} [config.maxAttempts=Infinity] - Número máximo de intentos permitidos para completar el cuestionario. Por defecto, es infinito.
* @license Licencia Comercial Propietaria
* @version 1.0.0
* @autor Salvador Martínez
* @copyright Este software está protegido por derechos de autor y es propiedad de E360 Digital Solutions S.A. de C.V.
* El uso está restringido a los términos y condiciones establecidos en el contrato de licencia.
* Para obtener una licencia y acceder a las características completas y soporte, contáctenos en licencias@espacio360.com.mx
*/
constructor(config) {
this.config = {
excelFileUrl: config.excelFileUrl,
mandatoryFields: config.mandatoryFields || {
question: "pregunta",
correctFeedback: "retroalimentacion_correcta",
incorrectFeedback: "retroalimentacion_incorrecta",
},
optionPrefix: config.optionPrefix || "opcion",
randomizeQuestions: config.randomizeQuestions || false,
randomizeOptions: config.randomizeOptions || false,
passingScore: config.passingScore || 80,
defaultWeight: config.defaultWeight || 1,
maxAttempts: config.maxAttempts || Infinity,
maxQuestions: config.maxQuestions || Infinity,
};
this.questions = []; // Contendrá las preguntas seleccionadas para el cuestionario.
this.currentQuestionIndex = 0; // Índice de la pregunta actual en el cuestionario.
this.correctAnswersCount = 0; // Contador de respuestas correctas.
this.incorrectAnswersCount = 0; // Contador de respuestas incorrectas.
this.totalScore = 0; // Puntaje acumulado del cuestionario.
this.attempts = 0; // Contador de intentos realizados.
}
/**
* Carga las preguntas desde el archivo Excel especificado en la configuración.
* @async
* @returns {Promise<void>}
*/
async loadQuestionsFromExcel() {
const rawData = await QuizManager.readExcelFile(this.config.excelFileUrl);
const sheetName = Object.keys(rawData)[0];
const sheetData = rawData[sheetName];
if (!Array.isArray(sheetData)) {
console.error("The selected sheet does not contain an array of data.");
return;
}
let questions = QuizManager.processQuestions(sheetData, this.config);
if (this.config.randomizeQuestions) {
questions = QuizManager.shuffle(questions);
}
// Validar maxQuestions solo si fue definido explícitamente
const totalQuestions = questions.length;
if (this.config.maxQuestions !== Infinity && this.config.maxQuestions > totalQuestions) {
throw new Error(
`El número máximo de preguntas especificado (${this.config.maxQuestions}) excede el total disponible (${totalQuestions}).`
);
}
// Seleccionar las preguntas según maxQuestions
this.questions = this.config.maxQuestions === Infinity
? questions
: questions.slice(0, this.config.maxQuestions);
}
/**
* Lee un archivo Excel y lo convierte a un objeto JSON.
* @static
* @async
* @param {string} url - URL del archivo Excel.
* @returns {Promise<Object>} Objeto donde las hojas del Excel son claves y sus datos son arrays de objetos.
*/
static async readExcelFile(url) {
try {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const data = new Uint8Array(arrayBuffer);
const workbook = XLSX.read(data, { type: "array" });
const result = {};
workbook.SheetNames.forEach((sheetName) => {
const sheet = workbook.Sheets[sheetName];
result[sheetName] = XLSX.utils.sheet_to_json(sheet);
});
return result;
} catch (error) {
console.error("Error reading Excel file:", error);
throw error;
}
}
/**
* Procesa los datos de una hoja del Excel para generar las preguntas.
* @static
* @param {Array} rawData - Datos de una hoja en formato JSON.
* @param {Object} config - Configuración para procesar preguntas.
* @returns {Array} Array de preguntas procesadas.
*/
static processQuestions(rawData, config) {
return rawData
.map((row) => {
const fields = config.mandatoryFields;
if (!row[fields.question] || !row["respuesta"]) {
console.error(`Missing mandatory fields: '${fields.question}' or 'respuesta'`);
return null;
}
// Asegurarse de que row["respuesta"] sea una cadena
const respuesta = String(row["respuesta"]);
const correctAnswers = respuesta.split(",").map((num) => `${config.optionPrefix}_${num.trim()}`);
const isMultiple = correctAnswers.length > 1;
let options = Object.keys(row)
.filter((key) => key.startsWith(config.optionPrefix))
.map((key) => ({
id: key,
text: row[key] ? String(row[key]).trim() : "", // Asegurarse de que row[key] sea una cadena antes de aplicar trim
isCorrect: correctAnswers.includes(key),
multiple: isMultiple, // Agregar campo multiple a cada opción
}));
const specialOptionsTexts = config.specialOptions || [];
let specialOptions = [];
let regularOptions = [];
options.forEach((option) => {
if (specialOptionsTexts.some((text) => option.text.toLowerCase().includes(text.toLowerCase()))) {
specialOptions.push(option);
} else {
regularOptions.push(option);
}
});
// Verificar si la columna "aleatoria" es 1 para barajar las opciones
if (row["aleatoria"] === 1) {
regularOptions = QuizManager.shuffle(regularOptions);
}
options = [...regularOptions, ...specialOptions];
const additionalInfo = Object.keys(row)
.filter((key) => !Object.values(fields).includes(key) && !key.startsWith(config.optionPrefix))
.reduce((info, key) => {
info[key] = row[key] ? String(row[key]).trim() : ""; // Asegurarse de que row[key] sea una cadena antes de aplicar trim
return info;
}, {});
const weight = parseFloat(row["ponderacion"]) || config.defaultWeight;
return {
question: row[fields.question] ? String(row[fields.question]).trim() : "", // Asegurarse de que row[fields.question] sea una cadena antes de aplicar trim
options,
correctFeedback: row[fields.correctFeedback] ? String(row[fields.correctFeedback]).trim() : "",
incorrectFeedback: row[fields.incorrectFeedback] ? String(row[fields.incorrectFeedback]).trim() : "",
additionalInfo,
weight,
multiple: isMultiple,
};
})
.filter(Boolean);
}
/**
* Baraja un array usando el algoritmo Fisher-Yates.
* @static
* @param {Array} array - Array a barajar.
* @returns {Array} Array barajado.
*/
static shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
/**
* Obtiene la pregunta actual.
* @returns {Object} La pregunta actual.
*/
getCurrentQuestion() {
return this.questions[this.currentQuestionIndex];
}
/**
* Avanza a la siguiente pregunta y la retorna.
* @returns {Object|null} La siguiente pregunta o `null` si no hay más.
*/
getNextQuestion() {
if (this.currentQuestionIndex < this.questions.length - 1) {
this.currentQuestionIndex++;
return this.questions[this.currentQuestionIndex];
}
return null;
}
/**
* Registra la respuesta del usuario a una pregunta específica por su ID.
* @param {number} questionId - El ID de la pregunta a responder.
* @param {boolean} isCorrect - Indica si la respuesta fue correcta.
*/
answerQuestionById(questionId, isCorrect) {
if (questionId < 0 || questionId >= this.questions.length) {
console.error(`Invalid question ID: ${questionId}`);
return;
}
const question = this.questions[questionId];
if (!question.answered) {
question.answered = true; // Marcar la pregunta como respondida
question.isAnswerCorrect = isCorrect; //Guarda el resultado de la respuesta
if (isCorrect) {
this.correctAnswersCount++;
this.totalScore += question.weight;
} else {
this.incorrectAnswersCount++;
}
} else {
console.warn(`Question ID ${questionId} has already been answered.`);
}
}
/**
* Registra la respuesta del usuario a la pregunta actual.
* @param {boolean} isCorrect - Indica si la respuesta fue correcta.
*/
answerCurrentQuestion(isCorrect) {
this.answerQuestionById(this.currentQuestionIndex, isCorrect);
}
/**
* Verifica si hay más preguntas disponibles.
* @returns {boolean} `true` si hay más preguntas, `false` de lo contrario.
*/
hasMoreQuestions() {
return this.currentQuestionIndex < this.questions.length - 1;
}
/**
* Genera un resumen del estado actual del cuestionario.
* @returns {Object} Resumen del cuestionario.
*/
getSummary() {
const totalQuestions = this.questions.length;
const maxScore = this.questions.reduce((sum, q) => sum + q.weight, 0);
const scorePercentage = Math.round((this.totalScore * 100) / maxScore);
const passed = scorePercentage >= this.config.passingScore;
return {
correct: this.correctAnswersCount,
incorrect: this.incorrectAnswersCount,
total: totalQuestions,
score: scorePercentage,
maxScore,
passed,
attempts: this.attempts,
maxAttempts: this.config.maxAttempts,
};
}
/**
* Devuelve los datos necesarios para renderizar la pregunta actual.
* @returns {Object|null} Datos de la pregunta actual o `null` si no existe.
*/
getRenderData() {
const currentQuestion = this.getCurrentQuestion();
if (!currentQuestion) return null;
return {
text: currentQuestion.question,
options: currentQuestion.options.map((option) => ({
id: option.id,
text: option.text,
isCorrect: option.isCorrect,
})),
correctFeedback: currentQuestion.correctFeedback,
incorrectFeedback: currentQuestion.incorrectFeedback,
additionalInfo: currentQuestion.additionalInfo,
multiple: currentQuestion.multiple,
};
}
/**
* Obtiene una pregunta específica por ID.
* @param {number} id - Índice de la pregunta.
* @returns {Object|null} Pregunta correspondiente o `null` si el índice no es válido.
*/
getRenderDataById(id) {
const question = this.getQuestionById(id);
if (!question) return null;
return {
text: question.question,
options: question.options.map((option) => ({
id: option.id,
text: option.text,
isCorrect: option.isCorrect,
})),
correctFeedback: question.correctFeedback,
incorrectFeedback: question.incorrectFeedback,
additionalInfo: question.additionalInfo,
multiple: question.multiple,
};
}
/**
* Genera los datos necesarios para renderizar una pregunta específica por ID.
* @param {number} id - Índice de la pregunta.
* @returns {Object|null} Datos de renderizado o `null` si el ID no es válido.
*/
getRenderDataById(id) {
const question = this.getQuestionById(id);
if (!question) return null;
return {
text: question.question,
options: question.options.map((option) => ({
id: option.id,
text: option.text,
isCorrect: option.isCorrect,
})),
correctFeedback: question.correctFeedback,
incorrectFeedback: question.incorrectFeedback,
additionalInfo: question.additionalInfo,
};
}
/**
* Devuelve los datos necesarios para renderizar todas las preguntas.
* @returns {Array} Array de objetos con los datos de todas las preguntas.
*/
getAllRenderData() {
return this.questions.map((question) => ({
text: question.question,
options: question.options.map((option) => ({
id: option.id,
text: option.text,
isCorrect: option.isCorrect,
})),
correctFeedback: question.correctFeedback,
incorrectFeedback: question.incorrectFeedback,
additionalInfo: question.additionalInfo,
multiple: question.multiple,
}));
}
/**
* Incrementa el número de intentos del cuestionario.
*/
incrementAttempts() {
this.attempts++;
}
/**
* Obtiene el número de intentos actuales.
* @returns {number} Número de intentos.
*/
getAttempts() {
return this.attempts;
}
/**
* Establece el número de intentos desde una fuente externa (LMS, etc.).
* @param {number} attempts - Número inicial de intentos.
*/
setAttempts(attempts) {
if (isNaN(attempts) || attempts < 0) {
console.error("El valor de intentos no es válido.");
return;
}
this.attempts = attempts;
}
/**
* Verifica si el número máximo de intentos se ha alcanzado.
* @returns {boolean} `true` si se alcanzó el máximo de intentos, `false` de lo contrario.
*/
hasReachedMaxAttempts() {
return this.attempts >= this.config.maxAttempts;
}
}
// Exponer la clase al ámbito global
global.QuizManager = QuizManager;
})(window);
File diff suppressed because one or more lines are too long
+924
View File
@@ -0,0 +1,924 @@
/*global define, module */
/* ===========================================================
pipwerks SCORM Wrapper for JavaScript
v1.1.20180906
Created by Philip Hutchison, January 2008-2018
https://github.com/pipwerks/scorm-api-wrapper
Copyright (c) Philip Hutchison
MIT-style license: http://pipwerks.mit-license.org/
This wrapper works with both SCORM 1.2 and SCORM 2004.
Inspired by APIWrapper.js, created by the ADL and
Concurrent Technologies Corporation, distributed by
the ADL (http://www.adlnet.gov/scorm).
SCORM.API.find() and SCORM.API.get() functions based
on ADL code, modified by Mike Rustici
(http://www.scorm.com/resources/apifinder/SCORMAPIFinder.htm),
further modified by Philip Hutchison
=============================================================== */
(function(root, factory) {
"use strict";
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], factory);
} else if (typeof module === 'object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals (root is window)
root.pipwerks = factory();
}
}(this, function() {
"use strict";
var pipwerks = {}; //pipwerks 'namespace' helps ensure no conflicts with possible other "SCORM" variables
pipwerks.UTILS = {}; //For holding UTILS functions
pipwerks.debug = { isActive: true }; //Enable (true) or disable (false) for debug mode
pipwerks.SCORM = { //Define the SCORM object
version: null, //Store SCORM version.
handleCompletionStatus: true, //Whether or not the wrapper should automatically handle the initial completion status
handleExitMode: true, //Whether or not the wrapper should automatically handle the exit mode
API: {
handle: null,
isFound: false
}, //Create API child object
connection: { isActive: false }, //Create connection child object
data: {
completionStatus: null,
exitStatus: null
}, //Create data child object
debug: {} //Create debug child object
};
/* --------------------------------------------------------------------------------
pipwerks.SCORM.isAvailable
A simple function to allow Flash ExternalInterface to confirm
presence of JS wrapper before attempting any LMS communication.
Parameters: none
Returns: Boolean (true)
----------------------------------------------------------------------------------- */
pipwerks.SCORM.isAvailable = function() {
return true;
};
// ------------------------------------------------------------------------- //
// --- SCORM.API functions ------------------------------------------------- //
// ------------------------------------------------------------------------- //
/* -------------------------------------------------------------------------
pipwerks.SCORM.API.find(window)
Looks for an object named API in parent and opener windows
Parameters: window (the browser window object).
Returns: Object if API is found, null if no API found
---------------------------------------------------------------------------- */
pipwerks.SCORM.API.find = function(win) {
var API = null,
findAttempts = 0,
findAttemptLimit = 500,
traceMsgPrefix = "SCORM.API.find",
trace = pipwerks.UTILS.trace,
scorm = pipwerks.SCORM;
while ((!win.API && !win.API_1484_11) &&
(win.parent) &&
(win.parent != win) &&
(findAttempts <= findAttemptLimit)) {
findAttempts++;
win = win.parent;
}
//If SCORM version is specified by user, look for specific API
if (scorm.version) {
switch (scorm.version) {
case "2004":
if (win.API_1484_11) {
API = win.API_1484_11;
} else {
trace(traceMsgPrefix + ": SCORM version 2004 was specified by user, but API_1484_11 cannot be found.");
}
break;
case "1.2":
if (win.API) {
API = win.API;
} else {
trace(traceMsgPrefix + ": SCORM version 1.2 was specified by user, but API cannot be found.");
}
break;
}
} else { //If SCORM version not specified by user, look for APIs
if (win.API_1484_11) { //SCORM 2004-specific API.
scorm.version = "2004"; //Set version
API = win.API_1484_11;
} else if (win.API) { //SCORM 1.2-specific API
scorm.version = "1.2"; //Set version
API = win.API;
}
}
if (API) {
trace(traceMsgPrefix + ": API found. Version: " + scorm.version);
trace("API: " + API);
} else {
trace(traceMsgPrefix + ": Error finding API. \nFind attempts: " + findAttempts + ". \nFind attempt limit: " + findAttemptLimit);
}
return API;
};
/* -------------------------------------------------------------------------
pipwerks.SCORM.API.get()
Looks for an object named API, first in the current window's frame
hierarchy and then, if necessary, in the current window's opener window
hierarchy (if there is an opener window).
Parameters: None.
Returns: Object if API found, null if no API found
---------------------------------------------------------------------------- */
pipwerks.SCORM.API.get = function() {
var API = null,
win = window,
scorm = pipwerks.SCORM,
find = scorm.API.find,
trace = pipwerks.UTILS.trace;
API = find(win);
if (!API && win.parent && win.parent != win) {
API = find(win.parent);
}
if (!API && win.top && win.top.opener) {
API = find(win.top.opener);
}
//Special handling for Plateau
//Thanks to Joseph Venditti for the patch
if (!API && win.top && win.top.opener && win.top.opener.document) {
API = find(win.top.opener.document);
}
if (API) {
scorm.API.isFound = true;
} else {
trace("API.get failed: Can't find the API!");
}
return API;
};
/* -------------------------------------------------------------------------
pipwerks.SCORM.API.getHandle()
Returns the handle to API object if it was previously set
Parameters: None.
Returns: Object (the pipwerks.SCORM.API.handle variable).
---------------------------------------------------------------------------- */
pipwerks.SCORM.API.getHandle = function() {
var API = pipwerks.SCORM.API;
if (!API.handle && !API.isFound) {
API.handle = API.get();
}
return API.handle;
};
// ------------------------------------------------------------------------- //
// --- pipwerks.SCORM.connection functions --------------------------------- //
// ------------------------------------------------------------------------- //
/* -------------------------------------------------------------------------
pipwerks.SCORM.connection.initialize()
Tells the LMS to initiate the communication session.
Parameters: None
Returns: Boolean
---------------------------------------------------------------------------- */
pipwerks.SCORM.connection.initialize = function() {
var success = false,
scorm = pipwerks.SCORM,
completionStatus = scorm.data.completionStatus,
trace = pipwerks.UTILS.trace,
makeBoolean = pipwerks.UTILS.StringToBoolean,
debug = scorm.debug,
traceMsgPrefix = "SCORM.connection.initialize ";
trace("connection.initialize called.");
if (!scorm.connection.isActive) {
var API = scorm.API.getHandle(),
errorCode = 0;
if (API) {
switch (scorm.version) {
case "1.2":
success = makeBoolean(API.LMSInitialize(""));
break;
case "2004":
success = makeBoolean(API.Initialize(""));
break;
}
if (success) {
//Double-check that connection is active and working before returning 'true' boolean
errorCode = debug.getCode();
if (errorCode !== null && errorCode === 0) {
scorm.connection.isActive = true;
if (scorm.handleCompletionStatus) {
//Automatically set new launches to incomplete
completionStatus = scorm.status("get");
if (completionStatus) {
switch (completionStatus) {
//Both SCORM 1.2 and 2004
case "not attempted":
scorm.status("set", "incomplete");
break;
//SCORM 2004 only
case "unknown":
scorm.status("set", "incomplete");
break;
//Additional options, presented here in case you'd like to use them
//case "completed" : break;
//case "incomplete" : break;
//case "passed" : break; //SCORM 1.2 only
//case "failed" : break; //SCORM 1.2 only
//case "browsed" : break; //SCORM 1.2 only
}
//Commit changes
scorm.save();
}
}
} else {
success = false;
trace(traceMsgPrefix + "failed. \nError code: " + errorCode + " \nError info: " + debug.getInfo(errorCode));
}
} else {
errorCode = debug.getCode();
if (errorCode !== null && errorCode !== 0) {
trace(traceMsgPrefix + "failed. \nError code: " + errorCode + " \nError info: " + debug.getInfo(errorCode));
} else {
trace(traceMsgPrefix + "failed: No response from server.");
}
}
} else {
trace(traceMsgPrefix + "failed: API is null.");
}
} else {
trace(traceMsgPrefix + "aborted: Connection already active.");
}
return success;
};
/* -------------------------------------------------------------------------
pipwerks.SCORM.connection.terminate()
Tells the LMS to terminate the communication session
Parameters: None
Returns: Boolean
---------------------------------------------------------------------------- */
pipwerks.SCORM.connection.terminate = function() {
var success = false,
scorm = pipwerks.SCORM,
exitStatus = scorm.data.exitStatus,
completionStatus = scorm.data.completionStatus,
trace = pipwerks.UTILS.trace,
makeBoolean = pipwerks.UTILS.StringToBoolean,
debug = scorm.debug,
traceMsgPrefix = "SCORM.connection.terminate ";
if (scorm.connection.isActive) {
var API = scorm.API.getHandle(),
errorCode = 0;
if (API) {
if (scorm.handleExitMode && !exitStatus) {
if (completionStatus !== "completed" && completionStatus !== "passed") {
switch (scorm.version) {
case "1.2":
success = scorm.set("cmi.core.exit", "suspend");
break;
case "2004":
success = scorm.set("cmi.exit", "suspend");
break;
}
} else {
switch (scorm.version) {
case "1.2":
success = scorm.set("cmi.core.exit", "logout");
break;
case "2004":
success = scorm.set("cmi.exit", "normal");
break;
}
}
}
//Ensure we persist the data for 1.2 - not required for 2004 where an implicit commit is applied during the Terminate
success = (scorm.version === "1.2") ? scorm.save() : true;
if (success) {
switch (scorm.version) {
case "1.2":
success = makeBoolean(API.LMSFinish(""));
break;
case "2004":
success = makeBoolean(API.Terminate(""));
break;
}
if (success) {
scorm.connection.isActive = false;
} else {
errorCode = debug.getCode();
trace(traceMsgPrefix + "failed. \nError code: " + errorCode + " \nError info: " + debug.getInfo(errorCode));
}
}
} else {
trace(traceMsgPrefix + "failed: API is null.");
}
} else {
trace(traceMsgPrefix + "aborted: Connection already terminated.");
}
return success;
};
// ------------------------------------------------------------------------- //
// --- pipwerks.SCORM.data functions --------------------------------------- //
// ------------------------------------------------------------------------- //
/* -------------------------------------------------------------------------
pipwerks.SCORM.data.get(parameter)
Requests information from the LMS.
Parameter: parameter (string, name of the SCORM data model element)
Returns: string (the value of the specified data model element)
---------------------------------------------------------------------------- */
pipwerks.SCORM.data.get = function(parameter) {
var value = null,
scorm = pipwerks.SCORM,
trace = pipwerks.UTILS.trace,
debug = scorm.debug,
traceMsgPrefix = "SCORM.data.get('" + parameter + "') ";
if (scorm.connection.isActive) {
var API = scorm.API.getHandle(),
errorCode = 0;
if (API) {
switch (scorm.version) {
case "1.2":
value = API.LMSGetValue(parameter);
break;
case "2004":
value = API.GetValue(parameter);
break;
}
errorCode = debug.getCode();
//GetValue returns an empty string on errors
//If value is an empty string, check errorCode to make sure there are no errors
if (value !== "" || errorCode === 0) {
//GetValue is successful.
//If parameter is lesson_status/completion_status or exit status, let's
//grab the value and cache it so we can check it during connection.terminate()
switch (parameter) {
case "cmi.core.lesson_status":
case "cmi.completion_status":
scorm.data.completionStatus = value;
break;
case "cmi.core.exit":
case "cmi.exit":
scorm.data.exitStatus = value;
break;
}
} else {
trace(traceMsgPrefix + "failed. \nError code: " + errorCode + "\nError info: " + debug.getInfo(errorCode));
}
} else {
trace(traceMsgPrefix + "failed: API is null.");
}
} else {
trace(traceMsgPrefix + "failed: API connection is inactive.");
}
trace(traceMsgPrefix + " value: " + value);
return String(value);
};
/* -------------------------------------------------------------------------
pipwerks.SCORM.data.set()
Tells the LMS to assign the value to the named data model element.
Also stores the SCO's completion status in a variable named
pipwerks.SCORM.data.completionStatus. This variable is checked whenever
pipwerks.SCORM.connection.terminate() is invoked.
Parameters: parameter (string). The data model element
value (string). The value for the data model element
Returns: Boolean
---------------------------------------------------------------------------- */
pipwerks.SCORM.data.set = function(parameter, value) {
var success = false,
scorm = pipwerks.SCORM,
trace = pipwerks.UTILS.trace,
makeBoolean = pipwerks.UTILS.StringToBoolean,
debug = scorm.debug,
traceMsgPrefix = "SCORM.data.set('" + parameter + "') ";
if (scorm.connection.isActive) {
var API = scorm.API.getHandle(),
errorCode = 0;
if (API) {
switch (scorm.version) {
case "1.2":
success = makeBoolean(API.LMSSetValue(parameter, value));
break;
case "2004":
success = makeBoolean(API.SetValue(parameter, value));
break;
}
if (success) {
if (parameter === "cmi.core.lesson_status" || parameter === "cmi.completion_status") {
scorm.data.completionStatus = value;
}
} else {
errorCode = debug.getCode();
trace(traceMsgPrefix + "failed. \nError code: " + errorCode + ". \nError info: " + debug.getInfo(errorCode));
}
} else {
trace(traceMsgPrefix + "failed: API is null.");
}
} else {
trace(traceMsgPrefix + "failed: API connection is inactive.");
}
trace(traceMsgPrefix + " value: " + value);
return success;
};
/* -------------------------------------------------------------------------
pipwerks.SCORM.data.save()
Instructs the LMS to persist all data to this point in the session
Parameters: None
Returns: Boolean
---------------------------------------------------------------------------- */
pipwerks.SCORM.data.save = function() {
var success = false,
scorm = pipwerks.SCORM,
trace = pipwerks.UTILS.trace,
makeBoolean = pipwerks.UTILS.StringToBoolean,
traceMsgPrefix = "SCORM.data.save failed";
if (scorm.connection.isActive) {
var API = scorm.API.getHandle();
if (API) {
switch (scorm.version) {
case "1.2":
success = makeBoolean(API.LMSCommit(""));
break;
case "2004":
success = makeBoolean(API.Commit(""));
break;
}
} else {
trace(traceMsgPrefix + ": API is null.");
}
} else {
trace(traceMsgPrefix + ": API connection is inactive.");
}
return success;
};
pipwerks.SCORM.status = function(action, status) {
var success = false,
scorm = pipwerks.SCORM,
trace = pipwerks.UTILS.trace,
traceMsgPrefix = "SCORM.getStatus failed",
cmi = "";
if (action !== null) {
switch (scorm.version) {
case "1.2":
cmi = "cmi.core.lesson_status";
break;
case "2004":
cmi = "cmi.completion_status";
break;
}
switch (action) {
case "get":
success = scorm.data.get(cmi);
break;
case "set":
if (status !== null) {
success = scorm.data.set(cmi, status);
} else {
success = false;
trace(traceMsgPrefix + ": status was not specified.");
}
break;
default:
success = false;
trace(traceMsgPrefix + ": no valid action was specified.");
}
} else {
trace(traceMsgPrefix + ": action was not specified.");
}
return success;
};
// ------------------------------------------------------------------------- //
// --- pipwerks.SCORM.debug functions -------------------------------------- //
// ------------------------------------------------------------------------- //
/* -------------------------------------------------------------------------
pipwerks.SCORM.debug.getCode
Requests the error code for the current error state from the LMS
Parameters: None
Returns: Integer (the last error code).
---------------------------------------------------------------------------- */
pipwerks.SCORM.debug.getCode = function() {
var scorm = pipwerks.SCORM,
API = scorm.API.getHandle(),
trace = pipwerks.UTILS.trace,
code = 0;
if (API) {
switch (scorm.version) {
case "1.2":
code = parseInt(API.LMSGetLastError(), 10);
break;
case "2004":
code = parseInt(API.GetLastError(), 10);
break;
}
} else {
trace("SCORM.debug.getCode failed: API is null.");
}
return code;
};
/* -------------------------------------------------------------------------
pipwerks.SCORM.debug.getInfo()
"Used by a SCO to request the textual description for the error code
specified by the value of [errorCode]."
Parameters: errorCode (integer).
Returns: String.
----------------------------------------------------------------------------- */
pipwerks.SCORM.debug.getInfo = function(errorCode) {
var scorm = pipwerks.SCORM,
API = scorm.API.getHandle(),
trace = pipwerks.UTILS.trace,
result = "";
if (API) {
switch (scorm.version) {
case "1.2":
result = API.LMSGetErrorString(errorCode.toString());
break;
case "2004":
result = API.GetErrorString(errorCode.toString());
break;
}
} else {
trace("SCORM.debug.getInfo failed: API is null.");
}
return String(result);
};
/* -------------------------------------------------------------------------
pipwerks.SCORM.debug.getDiagnosticInfo
"Exists for LMS specific use. It allows the LMS to define additional
diagnostic information through the API Instance."
Parameters: errorCode (integer).
Returns: String (Additional diagnostic information about the given error code).
---------------------------------------------------------------------------- */
pipwerks.SCORM.debug.getDiagnosticInfo = function(errorCode) {
var scorm = pipwerks.SCORM,
API = scorm.API.getHandle(),
trace = pipwerks.UTILS.trace,
result = "";
if (API) {
switch (scorm.version) {
case "1.2":
result = API.LMSGetDiagnostic(errorCode);
break;
case "2004":
result = API.GetDiagnostic(errorCode);
break;
}
} else {
trace("SCORM.debug.getDiagnosticInfo failed: API is null.");
}
return String(result);
};
// ------------------------------------------------------------------------- //
// --- Shortcuts! ---------------------------------------------------------- //
// ------------------------------------------------------------------------- //
// Because nobody likes typing verbose code.
pipwerks.SCORM.init = pipwerks.SCORM.connection.initialize;
pipwerks.SCORM.get = pipwerks.SCORM.data.get;
pipwerks.SCORM.set = pipwerks.SCORM.data.set;
pipwerks.SCORM.save = pipwerks.SCORM.data.save;
pipwerks.SCORM.quit = pipwerks.SCORM.connection.terminate;
// ------------------------------------------------------------------------- //
// --- pipwerks.UTILS functions -------------------------------------------- //
// ------------------------------------------------------------------------- //
/* -------------------------------------------------------------------------
pipwerks.UTILS.StringToBoolean()
Converts 'boolean strings' into actual valid booleans.
(Most values returned from the API are the strings "true" and "false".)
Parameters: String
Returns: Boolean
---------------------------------------------------------------------------- */
pipwerks.UTILS.StringToBoolean = function(value) {
var t = typeof value;
switch (t) {
//typeof new String("true") === "object", so handle objects as string via fall-through.
//See https://github.com/pipwerks/scorm-api-wrapper/issues/3
case "object":
case "string":
return (/(true|1)/i).test(value);
case "number":
return !!value;
case "boolean":
return value;
case "undefined":
return null;
default:
return false;
}
};
/* -------------------------------------------------------------------------
pipwerks.UTILS.trace()
Displays error messages when in debug mode.
Parameters: msg (string)
Return: None
---------------------------------------------------------------------------- */
pipwerks.UTILS.trace = function(msg) {
if (pipwerks.debug.isActive) {
if (window.console && window.console.log) {
window.console.log(msg);
} else {
//alert(msg);
}
}
};
return pipwerks;
}));
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
/*!
* chartjs-gauge.js v0.3.0
* https://github.com/haiiaaa/chartjs-gauge/
* (c) 2021 chartjs-gauge.js Contributors
* Released under the MIT License
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("chart.js")):"function"==typeof define&&define.amd?define(["chart.js"],e):(t=t||self).Gauge=e(t.Chart)}(this,(function(t){"use strict";function e(t,e,r){return e in t?Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}):t[e]=r,t}function r(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(t);e&&(a=a.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),r.push.apply(r,a)}return r}(t=t&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t).defaults._set("gauge",{needle:{radiusPercentage:2,widthPercentage:3.2,lengthPercentage:80,color:"rgba(0, 0, 0, 1)"},valueLabel:{display:!0,formatter:null,color:"rgba(255, 255, 255, 1)",backgroundColor:"rgba(0, 0, 0, 1)",borderRadius:5,padding:{top:5,right:5,bottom:5,left:5},bottomMarginPercentage:5},animation:{duration:1e3,animateRotate:!0,animateScale:!1},cutoutPercentage:50,rotation:-Math.PI,circumference:Math.PI,legend:{display:!1},tooltips:{enabled:!1}});var a=t.controllers.doughnut.extend({getValuePercent:function(t,e){var r=t.minValue,a=t.data,n=r||0;return(e-n)/((a[a.length-1]||1)-n)},getWidth:function(t){return t.chartArea.right-t.chartArea.left},getTranslation:function(t){var e=t.chartArea,r=t.offsetX,a=t.offsetY;return{dx:(e.left+e.right)/2+r,dy:(e.top+e.bottom)/2+a}},getAngle:function(t){var e=t.chart,r=t.valuePercent,a=e.options;return a.rotation+a.circumference*r},drawNeedle:function(t){this.chart.animating||(t=1);var e=this.chart,r=e.ctx,a=e.config,n=e.innerRadius,i=e.outerRadius,o=a.data.datasets[this.index],l=this.getMeta().previous,c=a.options.needle,s=c.radiusPercentage,u=c.widthPercentage,h=c.lengthPercentage,d=c.color,g=this.getWidth(this.chart),f=s/100*g,p=u/100*g,b=h/100*(i-n)+n,P=this.getTranslation(this.chart),v=P.dx,m=P.dy,y=this.getAngle({chart:this.chart,valuePercent:l.valuePercent}),x=y+(this.getAngle({chart:this.chart,valuePercent:this.getValuePercent(o,o.value)})-y)*t;r.save(),r.translate(v,m),r.rotate(x),r.fillStyle=d,r.beginPath(),r.ellipse(0,0,f,f,0,0,2*Math.PI),r.fill(),r.beginPath(),r.moveTo(0,p/2),r.lineTo(b,0),r.lineTo(0,-p/2),r.fill(),r.restore()},drawValueLabel:function(e){if(this.chart.config.options.valueLabel.display){var r=this.chart,a=r.ctx,n=r.config,i=n.options.defaultFontFamily,o=n.data.datasets[this.index],l=n.options.valueLabel,c=l.formatter,s=l.fontSize,u=l.color,h=l.backgroundColor,d=l.borderRadius,g=l.padding,f=l.bottomMarginPercentage/100*this.getWidth(this.chart),p=(c||function(t){return t})(o.value).toString();a.textBaseline="middle",a.textAlign="center",s&&(a.font="".concat(s,"px ").concat(i));var b=a.measureText(p).width,P=Math.max(a.measureText("m").width,a.measureText("").width),v=-(g.left+b/2),m=-(g.top+P/2),y=g.left+b+g.right,x=g.top+P+g.bottom,w=this.getTranslation(this.chart),O=w.dx,j=w.dy,M=this.chart.options.rotation%(2*Math.PI);O+=f*Math.cos(M+Math.PI/2),j+=f*Math.sin(M+Math.PI/2),a.save(),a.translate(O,j),a.beginPath(),t.helpers.canvas.roundedRect(a,v,m,y,x,d),a.fillStyle=h,a.fill(),a.fillStyle=u||n.options.defaultFontColor;a.fillText(p,0,.075*P),a.restore()}},update:function(e){var r=this.chart.config.data.datasets[this.index];r.minValue=r.minValue||0;var a=this.getMeta(),n={valuePercent:0};e?(a.previous=null,a.current=n):(r.data.sort((function(t,e){return t-e})),a.previous=a.current||n,a.current={valuePercent:this.getValuePercent(r,r.value)}),t.controllers.doughnut.prototype.update.call(this,e)},updateElement:function(a,n,i){t.controllers.doughnut.prototype.updateElement.call(this,a,n,i);var o=this.getDataset(),l=o.data,c=0===n?o.minValue:l[n-1],s=l[n],u=this.getAngle({chart:this.chart,valuePercent:this.getValuePercent(o,c)}),h=this.getAngle({chart:this.chart,valuePercent:this.getValuePercent(o,s)}),d=h-u;a._model=function(t){for(var a=1;a<arguments.length;a++){var n=null!=arguments[a]?arguments[a]:{};a%2?r(Object(n),!0).forEach((function(r){e(t,r,n[r])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):r(Object(n)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(n,e))}))}return t}({},a._model,{startAngle:u,endAngle:h,circumference:d})},draw:function(e){t.controllers.doughnut.prototype.draw.call(this,e),this.drawNeedle(e),this.drawValueLabel(e)}});return void 0===CanvasRenderingContext2D.prototype.ellipse&&(CanvasRenderingContext2D.prototype.ellipse=function(t,e,r,a,n,i,o,l){this.save(),this.translate(t,e),this.rotate(n),this.scale(r,a),this.arc(0,0,1,i,o,l),this.restore()}),t.controllers.gauge=a,t.Gauge=function(e,r){return r.type="gauge",new t(e,r)},t.Gauge}));
+392
View File
@@ -0,0 +1,392 @@
/**
* Configuración global del curso.
* @namespace
* @property {string} COURSE_CONFIG_URL - Ruta al archivo de configuración JSON del curso.
* @property {boolean} DEBUG - Habilita/deshabilita el modo de depuración.
*/
window.COURSE_CONFIG = {
COURSE_CONFIG_URL: 'config.json',
DEBUG: false,
SHOW_PAGINATION: false, // Bandera para mostrar/ocultar paginación
SHOW_TITLE: false, // Bandera para mostrar/ocultar título
SHOW_GLOSSARY: false, // Bandera para mostrar/ocultar glosario
};
/**
* Maneja la lógica del glosario para un botón específico
* @param {HTMLElement} button - El botón del glosario
*/
async function handleGlossaryButton(button) {
const offcanvasEl = document.getElementById('offcanvasGlossary');
if (!offcanvasEl) {
console.error('Elemento offcanvasGlossary no encontrado');
return;
}
const offcanvas = new bootstrap.Offcanvas(offcanvasEl);
try {
const res = await fetch('manual_pld_ft.html');
if (!res.ok) throw new Error('Error al cargar el glosario');
const text = await res.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const main = doc.querySelector('main');
offcanvasEl.querySelector('.offcanvas-body').innerHTML = main
? main.innerHTML
: '<p class="text-danger">No se encontró el contenido.</p>';
const item = CourseNav.getCurrentSlide();
const tituloActual = item.title;
offcanvasEl.querySelectorAll('section[data-title]').forEach((seccion) => {
seccion.classList.toggle('d-none', seccion.dataset.title !== tituloActual);
});
offcanvas.show();
} catch (err) {
console.error('Error en glosario:', err);
offcanvasEl.querySelector('.offcanvas-body').innerHTML =
'<p class="text-danger">Error al cargar el glosario. Recargue la página e intente nuevamente.</p>';
offcanvas.show();
}
}
/**
* Configura los event listeners para el glosario
*/
function setupGlossaryListeners() {
// Event delegation para manejar clicks en cualquier botón con la clase 'btn-glossary'
document.body.addEventListener('click', function (e) {
const glossaryBtn = e.target.closest('.btn-glossary');
if (glossaryBtn) {
e.preventDefault();
handleGlossaryButton(glossaryBtn);
}
});
}
/**
* Navega a una sección específica, la muestra y hace scroll hacia ella.
* @function gotoSection
* @param {string|number} sectionId - ID de la sección (con o sin #) o número de sección.
* @param {Object} [options={}] - Opciones de scroll.
* @param {string} [options.behavior="smooth"] - Comportamiento del scroll ('auto' o 'smooth').
* @param {string} [options.block="start"] - Alineación vertical ('start', 'center', 'end' o 'nearest').
* @example
* // Ejemplos de uso:
* gotoSection('sec1'); // Va a #sec1
* gotoSection('#sec2'); // Va a #sec2
* gotoSection(3); // Va a #sec3
*/
function gotoSection(sectionId, options = {}) {
const defaults = { behavior: 'smooth', block: 'start' };
const opts = Object.assign(defaults, options);
// Normalizar el ID de la sección
let targetId = sectionId;
if (typeof sectionId === 'number') {
targetId = `sec${sectionId}`;
} else if (typeof sectionId === 'string' && !sectionId.startsWith('#')) {
targetId = sectionId.startsWith('sec') ? sectionId : `sec${sectionId}`;
} else if (sectionId.startsWith('#')) {
targetId = sectionId.substring(1);
}
const targetElement = document.getElementById(targetId);
if (targetElement) {
// Mostrar la sección si está oculta
if (targetElement.style.display === 'none') {
targetElement.style.display = '';
}
// Hacer scroll hacia la sección
targetElement.scrollIntoView(opts);
return true;
}
console.warn(`Sección ${targetId} no encontrada`);
return false;
}
/**
* Desplaza la ventana hasta la parte superior de un elemento.
* @function scrollToElementTop
* @param {string} selector - Selector CSS del elemento objetivo.
* @param {Object} [options={}] - Opciones de scroll.
* @param {string} [options.behavior="smooth"] - Comportamiento del scroll ('auto' o 'smooth').
* @param {string} [options.block="start"] - Alineación vertical ('start', 'center', 'end' o 'nearest').
* @param {string} [options.inline="nearest"] - Alineación horizontal ('start', 'center', 'end' o 'nearest').
* @example
* // Ejemplo de uso:
* scrollToElementTop('#main-content', { behavior: 'smooth', block: 'start' });
*/
function scrollToElementTop(selector, options = {}) {
const defaults = { behavior: 'smooth', block: 'start', inline: 'nearest' };
const opts = Object.assign(defaults, options);
const el = document.querySelector(selector);
if (el) el.scrollIntoView(opts);
}
/**
* Observador de intersección para animar elementos cuando son visibles en el viewport.
* @function animateOnScroll
* @param {string} selector - Selector de los elementos a observar.
* @param {string} animationClass - Clase de animación de Animate.css (ej. 'animate__fadeInUp').
* @param {Object} [options={}] - Opciones de configuración.
* @param {number} [options.threshold=0.1] - Umbral de visibilidad (0-1).
* @param {boolean} [options.animateOnce=true] - Si es true, la animación solo se ejecuta una vez.
* @param {string} [options.prefix='animate__animated'] - Prefijo para clases de animación.
* @returns {IntersectionObserver} Instancia del observador.
* @example
* // Ejemplo de uso:
* animateOnScroll('.animar', 'animate__fadeIn', { threshold: 0.2 });
*/
function animateOnScroll(selector, animationClass, options = {}) {
const { threshold = 0.1, animateOnce = true, prefix = 'animate__animated' } = options;
const cb = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add(prefix, animationClass);
if (animateOnce) observer.unobserve(entry.target);
} else if (!animateOnce) {
entry.target.classList.remove(prefix, animationClass);
}
});
};
const observer = new IntersectionObserver(cb, { threshold });
document.querySelectorAll(selector).forEach((el) => observer.observe(el));
return observer;
}
/**
* Configura los event listeners cuando el DOM está completamente cargado.
* @event DOMContentLoaded
*/
document.addEventListener('DOMContentLoaded', () => {
setupGlossaryListeners();
// Inicializar sal.js
if (typeof sal !== 'undefined' && document.querySelectorAll('[data-sal]').length > 0) {
setTimeout(() => {
document.querySelectorAll('[data-sal]').forEach((el) => (el.style.visibility = 'visible'));
sal({
once: false,
threshold: 0.3,
duration: 10000,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
distance: '100px',
opacity: 0.2,
scale: 0.85,
});
}, 200);
}
/**
* Evento antes de cambiar de slide.
* @event beforeSlideChange
* @property {Object} detail - Detalles del evento.
* @property {number} detail.currentIndex - Índice del slide actual.
* @property {Array} detail.contentArray - Array completo de contenido.
*/
document.body.addEventListener('beforeSlideChange', (e) => {
// console.log("Antes de cambiar de slide:", e.detail);
});
/**
* Evento al cambiar de slide.
* @event slideChange
* @property {Object} detail - Detalles del evento.
* @property {number} detail.slideIndex - Índice del nuevo slide.
* @property {Array} detail.contentArray - Array completo de contenido.
*/
document.body.addEventListener('slideChange', (e) => {
if (e.detail && typeof e.detail.slideIndex === 'number' && Array.isArray(e.detail.contentArray)) {
console.log(e.detail.contentArray[e.detail.slideIndex].content);
// Inicializar sal.js si hay elementos con data-sal
if (typeof sal !== 'undefined' && document.querySelectorAll('[data-sal]').length > 0) {
setTimeout(() => {
document.querySelectorAll('[data-sal]').forEach((el) => (el.style.visibility = 'visible'));
sal({
once: false,
threshold: 0.3,
duration: 10000,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
distance: '100px',
opacity: 0.2,
scale: 0.85,
});
}, 200);
}
// Paginación
const paginationEl = document.getElementById('pagination');
const paginacionScoEl = document.querySelector('.paginacion_sco');
if (window.COURSE_CONFIG.SHOW_PAGINATION) {
if (paginationEl) {
paginationEl.innerHTML = e.detail.slideIndex + 1 + ' / ' + e.detail.contentArray.length;
}
if (paginacionScoEl) {
paginacionScoEl.classList.remove('isFalse');
}
} else {
if (paginacionScoEl) {
paginacionScoEl.classList.add('isFalse');
}
}
// Título de la pantalla actual
const titleScoEl = document.getElementById('titleSco');
if (titleScoEl) {
if (window.COURSE_CONFIG.SHOW_TITLE) {
titleScoEl.classList.remove('d-none');
if (e.detail.contentArray[e.detail.slideIndex]) {
titleScoEl.innerHTML = e.detail.contentArray[e.detail.slideIndex].title || '';
}
} else {
titleScoEl.classList.add('d-none');
}
}
// Glosario
const btnGlossary = document.getElementById('btn-glossary');
if (btnGlossary) {
if (window.COURSE_CONFIG.SHOW_GLOSSARY) {
const noShowBtnGlossaryIn = [0, 1, e.detail.contentArray.length - 1];
btnGlossary.style.display = noShowBtnGlossaryIn.includes(e.detail.slideIndex) ? 'none' : 'block';
} else {
btnGlossary.style.display = 'none';
}
}
}
});
/**
* Evento al completar un slide.
* @event slideCompleted
* @property {Object} detail - Detalles del evento.
*/
document.body.addEventListener('slideCompleted', (e) => {
// console.log("Slide completado:", e.detail);
});
// Event listener para botón personalizado
const customButtonEvent = document.getElementById('btn-glossary');
if (customButtonEvent) {
customButtonEvent.addEventListener('click', async () => {
const offcanvasEl = document.getElementById('offcanvasGlossary');
const offcanvas = new bootstrap.Offcanvas(offcanvasEl);
try {
// Carga el HTML completo
const res = await fetch('manual_pld_ft.html');
const text = await res.text();
// Parsea el documento y extrae solo el <main>
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const main = doc.querySelector('main');
// Inserta el contenido dentro del offcanvas-body
offcanvasEl.querySelector('.offcanvas-body').innerHTML = main
? main.innerHTML
: '<p class="text-danger">No se encontró el contenido.</p>';
// **Ocultar todas las secciones del glosario y mostrar sólo la correspondiente**
const item = CourseNav.getCurrentSlide();
const tituloActual = item.title;
console.log('Slide actual:', tituloActual);
// console.warn(tituloActual);
// Selecciona únicamente las secciones dentro del offcanvas
offcanvasEl.querySelectorAll('section[data-title]').forEach((seccion) => {
if (seccion.dataset.title === tituloActual) {
seccion.classList.remove('d-none');
} else {
seccion.classList.add('d-none');
}
});
// Finalmente, muestra el offcanvas
offcanvas.show();
} catch (err) {
console.error(err);
offcanvasEl.querySelector('.offcanvas-body').innerHTML =
'<p class="text-danger">Error al cargar el glosario.</p>';
offcanvas.show();
}
});
}
});
/**
* Inicializa un Swiper con opciones personalizadas, reproducción opcional de audio por slide y detección de visita a todos los slides.
*
* @param {HTMLElement|string} swiperContainer - Selector CSS o elemento contenedor del Swiper.
* @param {Function} [callback] - Función que se ejecuta al visitar todos los slides.
* @param {Object} [options={}] - Opciones personalizadas de inicialización para Swiper (navigation, pagination, etc.).
* @param {Array} [audios=[]] - Arreglo opcional de objetos Howler (o similar) con audios asignados por slide.
* @returns {Swiper} - Instancia inicializada de Swiper.
* @example
* initializeSwiper('#mySwiper', () => {
* console.log('Todos los slides fueron visitados');
* }, {
* effect: 'flip',
* nextSelector: '.custom-next',
* prevSelector: '.custom-prev',
* paginationSelector: '.custom-pagination',
* }, [
* new Howl({ src: 'audio1.mp3' }),
* null, // Slide sin audio
* new Howl({ src: 'audio3.mp3' }),
* ]);
*
*/
function initializeSwiper(swiperContainer, callback, options = {}, audios = [], autoPlaySound = true) {
const visitedSlides = new Set();
const container = typeof swiperContainer === 'string' ? document.querySelector(swiperContainer) : swiperContainer;
const parent = container.parentElement;
if (options.navigation && options.navigation.nextEl && options.navigation.prevEl) {
options.navigation.nextEl = container.querySelector(options.navigation.nextEl)
? container.querySelector(options.navigation.nextEl)
: parent.querySelector(options.navigation.nextEl);
options.navigation.prevEl = container.querySelector(options.navigation.prevEl)
? container.querySelector(options.navigation.prevEl)
: parent.querySelector(options.navigation.prevEl);
}
const defaultParams = {
effect: 'slide',
loop: false,
autoHeight: true,
};
// Combina opciones personalizadas con las por defecto
const params = {
...defaultParams,
...options,
on: {
init: function () {
visitedSlides.add(this.activeIndex);
if (audios.length > 0 && audios[this.activeIndex] && autoPlaySound) {
CourseNav.audioController.stopAllSoundsAndPlay(audios[this.activeIndex]);
}
},
slideChange: function () {
const index = this.activeIndex;
visitedSlides.add(index);
if (audios.length > 0) {
CourseNav.audioController.stopAudio();
}
if (audios.length > 0 && audios[index]) {
CourseNav.audioController.stopAllSoundsAndPlay(audios[index]);
}
if (visitedSlides.size === this.slides.length && typeof callback === 'function') {
callback();
}
},
},
};
return new Swiper(container, params);
}
File diff suppressed because one or more lines are too long
+941
View File
@@ -0,0 +1,941 @@
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);
}
off(event, fn, id) {
// Cuando se llama a off() sin argumentos, limpia todos los listeners.
// Lo extendemos para que también limpie nuestros listeners de timeupdate.
if (arguments.length === 0) {
this._timeupdateListeners = [];
}
// Llama al método off() del padre
return super.off(event, fn, id);
}
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) => {
// Detener y remover eventos de todos los sonidos EXCEPTO el que se va a reproducir.
if (sound !== howl) {
sound.stop();
//sound.off();
}
});
this.updateIcon();
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();
if (this.progressCircle) this.progressCircle.style.display = "none";
this.updateIconVolume();
this.audioElement = null;
}
toggleAudio() {
if (!this.audioElement) return;
this.audioElement.playing() ? this.toggleMute() : this.playAudio();
}
toggleMute() {
this.isMuted = !this.isMuted;
Howler.mute(this.isMuted);
this.updateIconVolume();
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.updateIconVolume();
}
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";
}
updateIconVolume() {
if (!this.audioIcon) return;
this.audioIcon.className = this.isMuted ? "fa-duotone fa-solid fa-volume-slash" : "fa-duotone fa-solid fa-volume";
}
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 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");
const BODY_CLASSES = [...document.body.classList];
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 {
console.warn("SCORM API no encontrada. Usando sessionStorage.");
}
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,
visited: false,
showAnimation: true,
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 text-white",
cancelButton: "btn btn-secondary text-white",
},
}).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.innerHTML = 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();
}
/**
* Abre el offcanvas de navegación (o cualquier sidebar) usando la API de Bootstrap.
*/
function openSidebar() {
const offEl = document.getElementById("coursenav-offcanvas");
if (offEl) {
// Obtén la instancia de Bootstrap Offcanvas o créala si no existe
const bsOff = bootstrap.Offcanvas.getInstance(offEl) || new bootstrap.Offcanvas(offEl);
bsOff.show();
}
}
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 text-white",
cancelButton: "btn btn-warning text-white",
},
});
}
}
/* ==========================================================================
* === 7. Carga de contenido y actualización de la interfaz ================
* ========================================================================== */
function loadContent() {
document.body.className = ""; // Limpiar primero
BODY_CLASSES.forEach((clase) => document.body.classList.add(clase));
MAIN_CONTENT.innerHTML = "";
window.scrollTo(0, 0);
LOADER_ELEMENT.style.display = "block";
if (NEXT_BTN) NEXT_BTN.classList.remove('look_at_me');
const item = courseData.contentArray[currentIndex];
if (!item?.content) return console.warn("Ítem inválido:", item);
audioController.stopAudio();
audioController.audioElement?.off();
Howler._howls?.forEach((h) => h.stop());
// Detener todas las animaciones de GSAP para evitar que se ejecuten en segundo plano
if (window.gsap) {
// gsap.killAll() es para miembros del Club GreenSock.
// Si no está disponible, usamos un método alternativo para la versión gratuita.
if (typeof gsap.killAll === "function") {
gsap.killAll();
} else {
gsap.killTweensOf(gsap.utils.toArray("*"));
}
}
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"));
document.querySelectorAll("[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;
$(MAIN_CONTENT).html(html);
//executeInjectedScripts(MAIN_CONTENT);
})
.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);
if (NEXT_BTN) NEXT_BTN.classList.remove('look_at_me');
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);
if (NEXT_BTN) {
const isLastSlide = currentIndex >= courseData.contentArray.length - 1;
const currentSlide = courseData.contentArray[currentIndex];
// Mostrar animación solo si showAnimation es true y no es el último slide
if (!isLastSlide && currentSlide.showAnimation && currentSlide.visited) {
NEXT_BTN.classList.add('look_at_me');
currentSlide.showAnimation = false; // Marcar para no mostrar nuevamente
setProgress(courseData); // Guardar el estado
} else {
NEXT_BTN.classList.remove('look_at_me');
}
}
}
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]);
// Agregar o remover la clase look_at_me al botón next
const nextBtn = document.getElementById('coursenav-next-btn');
}
function markSlidesAsVisited(indices) {
indices
.sort((a, b) => b - a)
.forEach((i) => {
currentIndex = i;
setSlideVisited(true);
});
}
function resetCourse() {
// 1. Resetear el estado local
courseData.contentArray.forEach(i => {
i.visited = false;
i.showAnimation = true;
});
courseData.maximumAdvance = 0;
currentIndex = 0;
// 2. Borrar todo el estado persistente
if (pipwerks.SCORM.connection.isActive) {
pipwerks.SCORM.set("cmi.core.lesson_status", "incomplete");
pipwerks.SCORM.set("cmi.core.lesson_location", "0"); // <- Esto es lo clave
pipwerks.SCORM.set("cmi.core.score.raw", "");
pipwerks.SCORM.set("cmi.suspend_data", "");
pipwerks.SCORM.save();
} else {
sessionStorage.removeItem("cmi.core.lesson_status");
sessionStorage.setItem("cmi.core.lesson_location", "0"); // <- Esto es lo clave
sessionStorage.removeItem("cmi.core.score.raw");
sessionStorage.removeItem("cmi.suspend_data");
}
// 3. Forzar recarga limpia
setProgress(courseData);
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));
});
/* ==========================================================================
* === 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,
openSidebar,
closeSidebar,
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;
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
/*! flip - v1.0.11 - 2015-07-13
* https://github.com/nnattawat/flip
* Copyright (c) 2015 Nattawat Nonsung; Licensed MIT */
!function(a){var b=function(a){a.data("flipped",!0);var b="rotate"+a.data("axis");a.find(a.data("front")).css({transform:b+(a.data("reverse")?"(-180deg)":"(180deg)"),"z-index":"0"}),a.find(a.data("back")).css({transform:b+"(0deg)","z-index":"1"})},c=function(a){a.data("flipped",!1);var b="rotate"+a.data("axis");a.find(a.data("front")).css({transform:b+"(0deg)","z-index":"1"}),a.find(a.data("back")).css({transform:b+(a.data("reverse")?"(180deg)":"(-180deg)"),"z-index":"0"})},d=function(){var a,b=document.createElement("fakeelement"),c={transition:"transitionend",OTransition:"oTransitionEnd",MozTransition:"transitionend",WebkitTransition:"webkitTransitionEnd"};for(a in c)if(void 0!==b.style[a])return c[a]};a.fn.flip=function(f,g){return"function"==typeof f&&(g=f),this.each(function(){var h=a(this);if(void 0===f||"boolean"!=typeof f&&"string"!=typeof f)if(h.data("initiated"))void 0!==f.axis&&e.call(this,f.axis,g);else{h.data("initiated",!0);var i=a.extend({axis:"y",reverse:!1,trigger:"click",speed:500,forceHeight:!1,forceWidth:!1,autoSize:!0,front:"auto",back:"auto"},f);"auto"==i.front?i.front=h.find(".front").length>0?".front":"div:first-child":"autostrict"==i.front&&(i.front="div:first-child"),"auto"==i.back?i.back=h.find(".back").length>0?".back":"div:first-child + div":"autostrict"==i.back&&(i.back="div:first-child + div"),h.data("reverse",i.reverse),h.data("axis",i.axis),h.data("front",i.front),h.data("back",i.back);var j="rotate"+("x"==i.axis.toLowerCase()?"x":"y"),k=2*h["outer"+("rotatex"==j?"Height":"Width")]();h.find(h.data("back")).css({transform:j+"("+(i.reverse?"180deg":"-180deg")+")"}),h.css({perspective:k,position:"relative"});var l=i.speed/1e3||.5,m=h.find(i.front).add(i.back,h);if(i.forceHeight?m.outerHeight(h.height()):i.autoSize&&m.css({height:"100%"}),i.forceWidth?m.outerWidth(h.width()):i.autoSize&&m.css({width:"100%"}),m.css({"backface-visibility":"hidden","transform-style":"preserve-3d",position:"absolute","z-index":"1"}),h.find(h.data("back")).css({transform:j+"("+(i.reverse?"180deg":"-180deg")+")","z-index":"0"}),setTimeout(function(){m.css({transition:"all "+l+"s ease-out"}),void 0!==g&&g.call(this)},20),"click"==i.trigger.toLowerCase())h.on(a.fn.tap?"tap":"click",function(){h.find(a(event.target).closest('button, a, input[type="submit"]')).length||(h.data("flipped")?c(h):b(h))});else if("hover"==i.trigger.toLowerCase()){var n=function(){h.unbind("mouseleave",o),b(h),setTimeout(function(){h.bind("mouseleave",o),h.is(":hover")||c(h)},i.speed+150)},o=function(){c(h)};h.mouseenter(n),h.mouseleave(o)}}else"toggle"==f&&(f=!h.data("flipped")),f?b(h):c(h),void 0!==g&&a(this).one(d(),function(){g.call(this)})}),this};var e=function(b,c){if(a(this).data("axis")!=b.toLowerCase()){var d=a(this).find(a(this).data("front")).add(a(this).data("back"),a(this)),e=d.css("transition");d.css({transition:"none"}),b=b.toLowerCase(),a(this).data("axis",b);var f="rotate"+b;a(this).data("flipped")?a(this).find(a(this).data("front")).css({transform:f+(a(this).data("reverse")?"(-180deg)":"(180deg)"),"z-index":"0"}):a(this).find(a(this).data("back")).css({transform:f+"("+(a(this).data("reverse")?"180deg":"-180deg")+")","z-index":"0"}),setTimeout(function(){d.css({transition:e}),void 0!==c&&c.call(this)},0)}else void 0!==c&&setTimeout(c.bind(this),0)}}(jQuery);
//# sourceMappingURL=jquery.flip.min.js.map
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
/*!
* jQuery UI Touch Punch 0.2.3
*
* Copyright 20112014, Dave Furfero
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Depends:
* jquery.ui.widget.js
* jquery.ui.mouse.js
*/
!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.sal=t():e.sal=t()}(this,(function(){return(()=>{"use strict";var e={d:(t,n)=>{for(var r in n)e.o(n,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:n[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};function n(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function r(e){for(var t=1;t<arguments.length;t++){var r=null!=arguments[t]?arguments[t]:{};t%2?n(Object(r),!0).forEach((function(t){o(e,t,r[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):n(Object(r)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(r,t))}))}return e}function o(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}e.d(t,{default:()=>j});var a="Sal was not initialised! Probably it is used in SSR.",s="Your browser does not support IntersectionObserver!\nGet a polyfill from here:\nhttps://github.com/w3c/IntersectionObserver/tree/master/polyfill",i={root:null,rootMargin:"0% 50%",threshold:.5,animateClassName:"sal-animate",disabledClassName:"sal-disabled",enterEventName:"sal:in",exitEventName:"sal:out",selector:"[data-sal]",once:!0,disabled:!1},l=[],c=null,u=function(e){e&&e!==i&&(i=r(r({},i),e))},d=function(e){e.classList.remove(i.animateClassName)},f=function(e,t){var n=new CustomEvent(e,{bubbles:!0,detail:t});t.target.dispatchEvent(n)},b=function(){document.body.classList.add(i.disabledClassName)},p=function(){c.disconnect(),c=null},m=function(){return i.disabled||"function"==typeof i.disabled&&i.disabled()},v=function(e,t){e.forEach((function(e){var n=e.target,r=void 0!==n.dataset.salRepeat,o=void 0!==n.dataset.salOnce,a=r||!(o||i.once);e.intersectionRatio>=i.threshold?(function(e){e.target.classList.add(i.animateClassName),f(i.enterEventName,e)}(e),a||t.unobserve(n)):a&&function(e){d(e.target),f(i.exitEventName,e)}(e)}))},y=function(){var e=[].filter.call(document.querySelectorAll(i.selector),(function(e){return!function(e){return e.classList.contains(i.animateClassName)}(e,i.animateClassName)}));return e.forEach((function(e){return c.observe(e)})),e},O=function(){b(),p()},h=function(){document.body.classList.remove(i.disabledClassName),c=new IntersectionObserver(v,{root:i.root,rootMargin:i.rootMargin,threshold:i.threshold}),l=y()},g=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};p(),Array.from(document.querySelectorAll(i.selector)).forEach(d),u(e),h()},w=function(){var e=y();l.push(e)};const j=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:i;if(u(e),"undefined"==typeof window)return console.warn(a),{elements:l,disable:O,enable:h,reset:g,update:w};if(!window.IntersectionObserver)throw b(),Error(s);return m()?b():h(),{elements:l,disable:O,enable:h,reset:g,update:w}};return t.default})()}));
//# sourceMappingURL=sal.js.map
+13
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long