Drossagyn curso completo
This commit is contained in:
Vendored
+7
File diff suppressed because one or more lines are too long
@@ -0,0 +1,448 @@
|
||||
(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
|
||||
* @author 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",
|
||||
correctOption: "opcion_c",
|
||||
correctAnswers: "correctas",
|
||||
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]) {
|
||||
console.error(`Missing mandatory field: '${fields.question}'`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!row[fields.correctAnswers] && !row[fields.correctOption]) {
|
||||
console.error(`Missing correct answers field: need either '${fields.correctAnswers}' or '${fields.correctOption}'`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let options = Object.keys(row)
|
||||
.filter((key) => key.startsWith(config.optionPrefix))
|
||||
.map((key, index) => {
|
||||
let isCorrect = false;
|
||||
|
||||
if (row[fields.correctAnswers]) {
|
||||
const correctIndices = row[fields.correctAnswers]
|
||||
.toString()
|
||||
.split(',')
|
||||
.map(num => parseInt(num.trim()) - 1);
|
||||
isCorrect = correctIndices.includes(index);
|
||||
} else if (row[fields.correctOption]) {
|
||||
isCorrect = key === fields.correctOption;
|
||||
}
|
||||
|
||||
return {
|
||||
id: key,
|
||||
text: row[key]?.trim(),
|
||||
isCorrect: isCorrect,
|
||||
};
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
if (config.randomizeOptions) {
|
||||
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) => {
|
||||
// Convertir a cadena antes de aplicar .trim() y manejar valores nulos/indefinidos
|
||||
const value = row[key] !== null && row[key] !== undefined ? String(row[key]).trim() : "";
|
||||
info[key] = value;
|
||||
return info;
|
||||
}, {});
|
||||
|
||||
const weight = parseFloat(row["ponderacion"]) || config.defaultWeight;
|
||||
|
||||
return {
|
||||
question: row[fields.question].trim(),
|
||||
options,
|
||||
correctFeedback: row[fields.correctFeedback]?.trim() || "",
|
||||
incorrectFeedback: row[fields.incorrectFeedback]?.trim() || "",
|
||||
additionalInfo,
|
||||
weight,
|
||||
};
|
||||
})
|
||||
.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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra la respuesta del usuario a la pregunta por su indice.
|
||||
* @param {boolean} isCorrect - Indica si la respuesta fue correcta.
|
||||
*/
|
||||
answerCurrentQuestionById(index, isCorrect) {
|
||||
this.answerQuestionById(index, isCorrect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si hay más preguntas disponibles.
|
||||
* @returns {boolean} `true` si hay más preguntas, `false` de lo contrario.
|
||||
*/
|
||||
hasMoreQuestions() {
|
||||
// Contamos cuántas preguntas han sido respondidas
|
||||
const answeredQuestions = this.questions.filter((q) => q.answered).length;
|
||||
|
||||
// Si el número de preguntas respondidas es menor que el total, hay más preguntas
|
||||
return answeredQuestions < this.questions.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera un resumen del estado de un conjunto de preguntas.
|
||||
* @param {Set} questionSet - Conjunto de preguntas del cuestionario.
|
||||
* @returns {Object} Resumen del cuestionario.
|
||||
*/
|
||||
generateSummaryFromSet(questionSet) {
|
||||
// Convertir el Set a un Array para procesarlo
|
||||
const questions = Array.from(questionSet);
|
||||
// Variables para calcular el resumen
|
||||
const totalQuestions = questions.length;
|
||||
const maxScore = questions.reduce((sum, q) => sum + (q.weight || 1), 0);
|
||||
const correctAnswersCount = questions.filter((q) => q.isAnswerCorrect).length;
|
||||
const incorrectAnswersCount = totalQuestions - correctAnswersCount;
|
||||
const totalScore = questions.reduce((sum, q) => (q.isAnswerCorrect ? sum + (q.weight || 1) : sum), 0);
|
||||
const scorePercentage = maxScore > 0 ? Math.round((totalScore * 100) / maxScore) : 0;
|
||||
const passed = scorePercentage >= this.config.passingScore;
|
||||
// Generar el resumen
|
||||
return {
|
||||
correct: correctAnswersCount,
|
||||
incorrect: 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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
getQuestionById(id) {
|
||||
if (id < 0 || id >= this.questions.length) {
|
||||
console.error(`Invalid question ID: ${id}`);
|
||||
return null;
|
||||
}
|
||||
return this.questions[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
Vendored
+25
File diff suppressed because one or more lines are too long
@@ -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
Vendored
+7
File diff suppressed because one or more lines are too long
Vendored
+7
@@ -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("W").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}));
|
||||
@@ -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: true,
|
||||
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: 600,
|
||||
easing: 'ease-out',
|
||||
distance: '50px',
|
||||
opacity: 0,
|
||||
scale: 0.95,
|
||||
});
|
||||
}, 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: 600,
|
||||
easing: 'ease-out',
|
||||
distance: '50px',
|
||||
opacity: 0,
|
||||
scale: 0.95,
|
||||
});
|
||||
}, 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);
|
||||
}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -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 (0–100), … }
|
||||
*/
|
||||
function getProgressByModule() {
|
||||
// Recolectamos totales y visitados por módulo
|
||||
const stats = {};
|
||||
courseData.contentArray.forEach((slide) => {
|
||||
const mod = slide.moduleTitle || "Sin módulo";
|
||||
if (!stats[mod]) stats[mod] = { total: 0, visited: 0 };
|
||||
stats[mod].total++;
|
||||
if (slide.visited) stats[mod].visited++;
|
||||
});
|
||||
|
||||
// Calculamos porcentajes
|
||||
const result = {};
|
||||
Object.entries(stats).forEach(([mod, { total, visited }]) => {
|
||||
result[mod] = parseFloat(((visited / total) * 100).toFixed(2));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function setProgress(p) {
|
||||
setSuspendData(p);
|
||||
}
|
||||
|
||||
function getProgress() {
|
||||
return getSuspendData() || { contentArray: [], maximumAdvance: 0 };
|
||||
}
|
||||
|
||||
function finishScorm() {
|
||||
if (pipwerks.SCORM.connection.isActive && !scormAPIUnloaded) {
|
||||
const elapsed = (Date.now() - sessionStartTime) / 1000;
|
||||
const hh = String(Math.floor(elapsed / 3600)).padStart(2, "0");
|
||||
const mm = String(Math.floor((elapsed % 3600) / 60)).padStart(2, "0");
|
||||
const ss = String(Math.floor(elapsed % 60)).padStart(2, "0");
|
||||
pipwerks.SCORM.set("cmi.core.session_time", `${hh}:${mm}:${ss}`);
|
||||
pipwerks.SCORM.save();
|
||||
pipwerks.SCORM.quit();
|
||||
scormAPIUnloaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function getScormData(key) {
|
||||
return pipwerks.SCORM.connection.isActive ? pipwerks.SCORM.get(key) ?? "" : sessionStorage.getItem(key) ?? "";
|
||||
}
|
||||
|
||||
function setScormData(key, value) {
|
||||
if (pipwerks.SCORM.connection.isActive) {
|
||||
const ok = pipwerks.SCORM.set(key, value);
|
||||
if (ok) pipwerks.SCORM.save();
|
||||
return ok;
|
||||
}
|
||||
sessionStorage.setItem(key, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* === 11. Arranque DOM y offcanvas =========================================
|
||||
* ========================================================================== */
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initializeScorm(() => loadConfig());
|
||||
window.addEventListener("beforeunload", finishScorm);
|
||||
// Tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map((el) => new bootstrap.Tooltip(el));
|
||||
});
|
||||
|
||||
/* ==========================================================================
|
||||
* === 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;
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+11
File diff suppressed because one or more lines are too long
Vendored
+4
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
+6
File diff suppressed because one or more lines are too long
Vendored
+6
@@ -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
|
||||
Vendored
+2
File diff suppressed because one or more lines are too long
+11
@@ -0,0 +1,11 @@
|
||||
/*!
|
||||
* jQuery UI Touch Punch 0.2.3
|
||||
*
|
||||
* Copyright 2011–2014, 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);
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -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
|
||||
Vendored
+13
File diff suppressed because one or more lines are too long
Vendored
+5
File diff suppressed because one or more lines are too long
Vendored
+14
File diff suppressed because one or more lines are too long
Vendored
+22
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user