update template y una actividad
This commit is contained in:
@@ -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;
|
||||
|
||||
}));
|
||||
Vendored
+7
File diff suppressed because one or more lines are too long
+365
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* Configuración global del curso.
|
||||
* @namespace
|
||||
* @property {string} COURSE_CONFIG_URL - Ruta al archivo de configuración JSON del curso.
|
||||
* @property {boolean} DEBUG - Habilita/deshabilita el modo de depuración.
|
||||
*/
|
||||
window.COURSE_CONFIG = {
|
||||
COURSE_CONFIG_URL: "config.json",
|
||||
DEBUG: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renderiza la paginación mostrando la posición actual dentro del módulo.
|
||||
* @function renderPagination
|
||||
* @param {number} currentIndex - Índice del slide actual en el array completo.
|
||||
* @param {Array<Object>} contentArray - Array completo de slides del curso.
|
||||
* @example
|
||||
* // Ejemplo de uso:
|
||||
* renderPagination(2, courseData.contentArray);
|
||||
*/
|
||||
function renderPagination(currentIndex, contentArray) {
|
||||
const pageNumber = document.getElementById("coursenav-page-number");
|
||||
const totalPages = document.getElementById("coursenav-total-pages");
|
||||
|
||||
if (!Array.isArray(contentArray)) return;
|
||||
// Gestionar visibilidad de menús primero
|
||||
manageMenuVisibility(currentIndex, contentArray);
|
||||
// Obtener el módulo actual basado en el slide actual
|
||||
const currentSlide = contentArray[currentIndex];
|
||||
if (!currentSlide) return;
|
||||
|
||||
// Filtrar todos los slides del mismo módulo
|
||||
const moduleSlides = contentArray.filter((slide) => slide.moduleTitle === currentSlide.moduleTitle);
|
||||
|
||||
// Encontrar la posición del slide actual dentro del módulo
|
||||
const moduleSlideIndex = moduleSlides.findIndex((slide) => slide.content === currentSlide.content);
|
||||
|
||||
if (pageNumber) pageNumber.textContent = moduleSlideIndex + 1;
|
||||
if (totalPages) totalPages.textContent = " / " + moduleSlides.length;
|
||||
//Navegación personalizada
|
||||
updateNavButtons(moduleSlideIndex, moduleSlides);
|
||||
}
|
||||
/**
|
||||
* Actualiza el estado (habilitado/deshabilitado) de los botones de navegación
|
||||
* del módulo (anterior/siguiente).
|
||||
*
|
||||
* Esta función deshabilita el botón "siguiente" si el usuario está en la última
|
||||
* diapositiva del módulo y el botón "anterior" si está en la primera.
|
||||
* Sin embargo, si el curso está en modo de depuración (`CourseNav.isDebug()` es true),
|
||||
* los botones permanecerán habilitados, permitiendo la navegación libre.
|
||||
*
|
||||
* @function updateNavButtons
|
||||
* @param {number} moduleSlideIndex - El índice de base cero de la diapositiva actual dentro de su módulo.
|
||||
* @param {Array<Object>} moduleSlides - Un array de los objetos de diapositiva que pertenecen al módulo actual.
|
||||
*/
|
||||
function updateNavButtons(moduleSlideIndex, moduleSlides) {
|
||||
const nextBtn = document.getElementById("coursenav-next-btn");
|
||||
const prevBtn = document.getElementById("coursenav-prev-btn");
|
||||
|
||||
if (!nextBtn || !prevBtn) return;
|
||||
|
||||
const isLastSlide = moduleSlideIndex + 1 === moduleSlides.length;
|
||||
const isFirstSlide = moduleSlideIndex === 0;
|
||||
const isDebugMode = CourseNav.isDebug();
|
||||
|
||||
nextBtn.classList.toggle("disabled", isLastSlide && !isDebugMode);
|
||||
prevBtn.classList.toggle("disabled", isFirstSlide && !isDebugMode);
|
||||
}
|
||||
/**
|
||||
* Gestiona la visibilidad de los menús del curso en la barra de navegación.
|
||||
*
|
||||
* Esta función muestra u oculta dinámicamente los menús secundarios
|
||||
* (`ul.course-menu`) basándose en el módulo del slide actual. Se asume que
|
||||
* el primer menú es el principal y siempre debe estar visible, mientras que
|
||||
* los menús subsecuentes son específicos de cada módulo y solo se muestran
|
||||
* cuando el usuario está navegando en dicho módulo.
|
||||
*
|
||||
* @function manageMenuVisibility
|
||||
* @param {number} currentIndex - El índice del slide actual en el array global de contenido.
|
||||
* @param {Array<Object>} contentArray - El array completo de objetos de slide del curso.
|
||||
*/
|
||||
function manageMenuVisibility(currentIndex, contentArray) {
|
||||
const courseMenus = document.querySelectorAll("#coursenav-main-menu > ul.course-menu");
|
||||
if (!courseMenus.length) return;
|
||||
|
||||
// El primer menú siempre está visible.
|
||||
courseMenus[0].style.display = "block";
|
||||
|
||||
// Si no hay contenido, ocultar los demás menús.
|
||||
if (!Array.isArray(contentArray) || !contentArray.length) {
|
||||
for (let i = 1; i < courseMenus.length; i++) {
|
||||
courseMenus[i].style.display = "none";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSlide = contentArray[currentIndex];
|
||||
// Si el slide actual no existe o no tiene un título de módulo, ocultar los demás.
|
||||
if (!currentSlide?.moduleTitle) {
|
||||
for (let i = 1; i < courseMenus.length; i++) {
|
||||
courseMenus[i].style.display = "none";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const currentModuleTitle = currentSlide.moduleTitle;
|
||||
|
||||
for (let i = 1; i < courseMenus.length; i++) {
|
||||
const menuItems = Array.from(courseMenus[i].querySelectorAll(".coursenav-link"));
|
||||
const shouldShow = menuItems.some((item) => {
|
||||
const itemIndex = parseInt(item.dataset.coursenavindex);
|
||||
return itemIndex >= 0 && contentArray[itemIndex]?.moduleTitle === currentModuleTitle;
|
||||
});
|
||||
courseMenus[i].style.display = shouldShow ? "block" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajusta el contenido del curso para ocupar el máximo de pantalla.
|
||||
* @function scaleWrapCourseContent
|
||||
*/
|
||||
function scaleWrapCourseContent() {
|
||||
const content = document.querySelector(".wrap-course-content");
|
||||
if (!content) return;
|
||||
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
const minWidth = 768;
|
||||
|
||||
if (vw < minWidth) {
|
||||
// Escalar en móviles
|
||||
const scale = vw / minWidth;
|
||||
content.style.transform = `scale(${scale})`;
|
||||
content.style.transformOrigin = "top left";
|
||||
content.style.width = minWidth + "px";
|
||||
content.style.height = (vh / scale) + "px";
|
||||
content.style.position = "fixed";
|
||||
content.style.left = "0";
|
||||
content.style.top = "0";
|
||||
content.style.overflow = "hidden";
|
||||
} else {
|
||||
// Pantalla completa en desktop
|
||||
content.style.transform = "";
|
||||
content.style.transformOrigin = "";
|
||||
content.style.width = "100vw";
|
||||
content.style.height = "100vh";
|
||||
content.style.position = "fixed";
|
||||
content.style.left = "0";
|
||||
content.style.top = "0";
|
||||
content.style.overflow = "auto";
|
||||
}
|
||||
content.style.zIndex = "1";
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderiza un stepper visual que muestra el progreso del curso.
|
||||
* @function renderStepper
|
||||
* @param {HTMLElement|string} stepperEl - Elemento o selector del contenedor del stepper.
|
||||
* @param {number} progressPercent - Porcentaje de progreso (0-100).
|
||||
* @param {HTMLElement|string} mobileEl - Elemento o selector del indicador móvil.
|
||||
* @param {number} [stepsCount=5] - Número de pasos visibles en el stepper.
|
||||
* @throws {Error} Si los elementos no son válidos.
|
||||
* @example
|
||||
* // Ejemplo de uso:
|
||||
* renderStepper('#stepper', 75, '#step-movil', 4);
|
||||
*/
|
||||
function renderStepper(stepperEl, progressPercent, mobileEl, stepsCount = 5) {
|
||||
// Validación y obtención de elementos
|
||||
if (typeof stepperEl === "string") stepperEl = document.querySelector(stepperEl);
|
||||
if (typeof mobileEl === "string") mobileEl = document.querySelector(mobileEl);
|
||||
if (!(stepperEl instanceof HTMLElement) || !(mobileEl instanceof HTMLElement)) {
|
||||
throw new Error("renderStepper: elementos inválidos.");
|
||||
}
|
||||
|
||||
// Limpiar contenido previo
|
||||
stepperEl.querySelectorAll(".step").forEach((el) => el.remove());
|
||||
|
||||
// Calcular posiciones de los pasos
|
||||
const stepPercents = Array.from({ length: stepsCount }, (_, i) => (i / (stepsCount - 1)) * 100);
|
||||
|
||||
// Determinar paso actual
|
||||
const currentIndex = stepPercents
|
||||
.map((p, i) => ({ p, i }))
|
||||
.filter(({ p }) => p <= progressPercent)
|
||||
.pop().i;
|
||||
|
||||
// Crear elementos de los pasos
|
||||
stepPercents.forEach((pct, i) => {
|
||||
const step = document.createElement("div");
|
||||
step.classList.add("step");
|
||||
if (i < currentIndex) step.classList.add("completed");
|
||||
if (i === currentIndex) {
|
||||
step.classList.add("completed");
|
||||
step.setAttribute("data-label", Math.round(pct) + "%");
|
||||
}
|
||||
stepperEl.appendChild(step);
|
||||
});
|
||||
|
||||
// Actualizar estilos y posición
|
||||
stepperEl.style.setProperty("--pct", progressPercent + "%");
|
||||
|
||||
const halfMobile = mobileEl.offsetWidth;
|
||||
mobileEl.style.left = `calc(${progressPercent}% - ${halfMobile}px)`;
|
||||
mobileEl.setAttribute("data-label", progressPercent + "%");
|
||||
}
|
||||
|
||||
/**
|
||||
* Navega al primer slide que tenga el título "Menús de la herramienta".
|
||||
*
|
||||
* Esta función busca en todo el contenido del curso y se desplaza a la primera
|
||||
* diapositiva que coincida con ese título específico y modulo actual de donde es invocada la funcion. Es útil para crear
|
||||
* accesos directos a secciones clave.
|
||||
* @function gotoFirstMenuToolSlide
|
||||
* @example
|
||||
* // Se puede vincular a un botón:
|
||||
* // document.getElementById('mi-boton').addEventListener('click', gotoFirstMenuToolSlide);
|
||||
*/
|
||||
function gotoFirstMenuToolSlide() {
|
||||
const contentArray = CourseNav.getCourseContentArray();
|
||||
const targetTitle = "Menús de la herramienta";
|
||||
const currentSlide = CourseNav.getCurrentSlide();
|
||||
// Filtrar todos los slides del mismo módulo
|
||||
const targetIndex = contentArray.findIndex((slide) => slide.title === targetTitle && slide.moduleTitle === currentSlide.moduleTitle);
|
||||
|
||||
if (targetIndex !== -1) {
|
||||
CourseNav.gotoSlide(targetIndex);
|
||||
} else {
|
||||
console.warn(`No se encontró ningún slide con el título '${targetTitle}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configura los event listeners cuando el DOM está completamente cargado.
|
||||
* @event DOMContentLoaded
|
||||
*/
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
/**
|
||||
* 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)) {
|
||||
renderPagination(e.detail.slideIndex, e.detail.contentArray);
|
||||
console.log(e.detail.contentArray[e.detail.slideIndex].content);
|
||||
const contentArray = e.detail.contentArray;
|
||||
const targetTitle = "Menús de la herramienta";
|
||||
const currentSlide = CourseNav.getCurrentSlide();
|
||||
// Filtrar todos los slides del mismo módulo
|
||||
const targetIndex = contentArray.findIndex((slide) => slide.title === targetTitle && slide.moduleTitle === currentSlide.moduleTitle);
|
||||
const btn = document.getElementById("coursenav-other-btn");
|
||||
if (!btn) return;
|
||||
if (targetIndex !== -1) {
|
||||
btn.classList.remove("disabled");
|
||||
} else {
|
||||
btn.classList.add("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
const titleSlide = document.getElementById("coursenav-course-title");
|
||||
if (titleSlide) {
|
||||
const slide = e.detail.contentArray[e.detail.slideIndex];
|
||||
const moduleTitle = slide.moduleTitle ? slide.moduleTitle + " | " : "";
|
||||
const title = slide.title || "Sin título";
|
||||
titleSlide.textContent = moduleTitle + title;
|
||||
}
|
||||
|
||||
const stepper = document.getElementById("stepper");
|
||||
const movil = document.getElementById("step-movil");
|
||||
const progreso = CourseNav.getProgressPercent(true);
|
||||
renderStepper(stepper, progreso, movil);
|
||||
});
|
||||
|
||||
/**
|
||||
* Evento al completar un slide.
|
||||
* @event slideCompleted
|
||||
* @property {Object} detail - Detalles del evento.
|
||||
*/
|
||||
document.body.addEventListener("slideCompleted", (e) => {
|
||||
console.log("Slide completado:", e.detail);
|
||||
const stepper = document.getElementById("stepper");
|
||||
const movil = document.getElementById("step-movil");
|
||||
const progreso = CourseNav.getProgressPercent(true);
|
||||
renderStepper(stepper, progreso, movil);
|
||||
renderPagination(e.detail.slideIndex, CourseNav.getCourseContentArray());
|
||||
});
|
||||
|
||||
// Event listener para botón personalizado
|
||||
const customButtonEvent = document.getElementById("coursenav-other-btn");
|
||||
if (customButtonEvent) {
|
||||
customButtonEvent.addEventListener("click", () => {
|
||||
console.log("Botón personalizado clickeado");
|
||||
gotoFirstMenuToolSlide();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Escalar contenido al cargar y redimensionar
|
||||
window.addEventListener("DOMContentLoaded", () => scaleWrapCourseContent());
|
||||
window.addEventListener("resize", () => scaleWrapCourseContent());
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
function renderPagination(e,t){const n=document.getElementById("coursenav-page-number"),o=document.getElementById("coursenav-total-pages");if(!Array.isArray(t))return;manageMenuVisibility(e,t);const r=t[e];if(!r)return;const s=t.filter((e=>e.moduleTitle===r.moduleTitle)),l=s.findIndex((e=>e.content===r.content));n&&(n.textContent=l+1),o&&(o.textContent=" / "+s.length),updateNavButtons(l,s)}function updateNavButtons(e,t){const n=document.getElementById("coursenav-next-btn"),o=document.getElementById("coursenav-prev-btn");if(!n||!o)return;const r=e+1===t.length,s=0===e,l=CourseNav.isDebug();n.classList.toggle("disabled",r&&!l),o.classList.toggle("disabled",s&&!l)}function manageMenuVisibility(e,t){const n=document.querySelectorAll("#coursenav-main-menu > ul.course-menu");if(!n.length)return;if(n[0].style.display="block",!Array.isArray(t)||!t.length){for(let e=1;e<n.length;e++)n[e].style.display="none";return}const o=t[e];if(!o?.moduleTitle){for(let e=1;e<n.length;e++)n[e].style.display="none";return}const r=o.moduleTitle;for(let e=1;e<n.length;e++){const o=Array.from(n[e].querySelectorAll(".coursenav-link")).some((e=>{const n=parseInt(e.dataset.coursenavindex);return n>=0&&t[n]?.moduleTitle===r}));n[e].style.display=o?"block":"none"}}function scrollToElementTop(e,t={}){const n=Object.assign({behavior:"smooth",block:"start",inline:"nearest"},t),o=document.querySelector(e);o&&o.scrollIntoView(n)}function animateOnScroll(e,t,n={}){const{threshold:o=.1,animateOnce:r=!0,prefix:s="animate__animated"}=n,l=new IntersectionObserver(((e,n)=>{e.forEach((e=>{e.isIntersecting?(e.target.classList.add(s,t),r&&n.unobserve(e.target)):r||e.target.classList.remove(s,t)}))}),{threshold:o});return document.querySelectorAll(e).forEach((e=>l.observe(e))),l}function scaleWrapCourseContent(){const e=document.querySelector(".wrap-course-content");if(!e)return;const t=window.innerWidth,n=window.innerHeight;if(t<768){const o=t/768;e.style.transform=`scale(${o})`,e.style.transformOrigin="top left",e.style.width="768px",e.style.height=n/o+"px",e.style.position="fixed",e.style.left="0",e.style.top="0",e.style.overflow="hidden"}else e.style.transform="",e.style.transformOrigin="",e.style.width="100vw",e.style.height="100vh",e.style.position="fixed",e.style.left="0",e.style.top="0",e.style.overflow="auto";e.style.zIndex="1"}function renderStepper(e,t,n,o=5){if("string"==typeof e&&(e=document.querySelector(e)),"string"==typeof n&&(n=document.querySelector(n)),!(e instanceof HTMLElement&&n instanceof HTMLElement))throw new Error("renderStepper: elementos inválidos.");e.querySelectorAll(".step").forEach((e=>e.remove()));const r=Array.from({length:o},((e,t)=>t/(o-1)*100)),s=r.map(((e,t)=>({p:e,i:t}))).filter((({p:e})=>e<=t)).pop().i;r.forEach(((t,n)=>{const o=document.createElement("div");o.classList.add("step"),n<s&&o.classList.add("completed"),n===s&&(o.classList.add("completed"),o.setAttribute("data-label",Math.round(t)+"%")),e.appendChild(o)})),e.style.setProperty("--pct",t+"%");const l=n.offsetWidth;n.style.left=`calc(${t}% - ${l}px)`,n.setAttribute("data-label",t+"%")}function gotoFirstMenuToolSlide(){const e=CourseNav.getCourseContentArray(),t=CourseNav.getCurrentSlide(),n=e.findIndex((e=>"Menús de la herramienta"===e.title&&e.moduleTitle===t.moduleTitle));-1!==n&&CourseNav.gotoSlide(n)}window.COURSE_CONFIG={COURSE_CONFIG_URL:"config.json",DEBUG:!1},document.addEventListener("DOMContentLoaded",(()=>{document.body.addEventListener("beforeSlideChange",(e=>{})),document.body.addEventListener("slideChange",(e=>{if(e.detail&&"number"==typeof e.detail.slideIndex&&Array.isArray(e.detail.contentArray)){renderPagination(e.detail.slideIndex,e.detail.contentArray);const t=e.detail.contentArray,n="Menús de la herramienta",o=CourseNav.getCurrentSlide(),r=t.findIndex((e=>e.title===n&&e.moduleTitle===o.moduleTitle)),s=document.getElementById("coursenav-other-btn");if(!s)return;-1!==r?s.classList.remove("disabled"):s.classList.add("disabled")}const t=document.getElementById("coursenav-course-title");if(t){const n=e.detail.contentArray[e.detail.slideIndex],o=n.moduleTitle?n.moduleTitle+" | ":"",r=n.title||"Sin título";t.textContent=o+r}const n=document.getElementById("stepper"),o=document.getElementById("step-movil");renderStepper(n,CourseNav.getProgressPercent(!0),o)})),document.body.addEventListener("slideCompleted",(e=>{const t=document.getElementById("stepper"),n=document.getElementById("step-movil");renderStepper(t,CourseNav.getProgressPercent(!0),n),renderPagination(e.detail.slideIndex,CourseNav.getCourseContentArray())}));const e=document.getElementById("coursenav-other-btn");e&&e.addEventListener("click",(()=>{gotoFirstMenuToolSlide()}))})),window.addEventListener("DOMContentLoaded",(()=>scaleWrapCourseContent())),window.addEventListener("resize",(()=>scaleWrapCourseContent()));
|
||||
+874
@@ -0,0 +1,874 @@
|
||||
var CourseNav = (function (COURSE_CONFIG) {
|
||||
"use strict";
|
||||
|
||||
/* ==========================================================================
|
||||
* === 1. Inyección y ejecución de scripts de contenido dinámico ============
|
||||
* ========================================================================== */
|
||||
const loadedScriptSrcs = new Set();
|
||||
|
||||
function executeInjectedScripts(container) {
|
||||
// 1) Scripts externos (src)
|
||||
container.querySelectorAll("script[src]").forEach((old) => {
|
||||
const src = old.src;
|
||||
if (!loadedScriptSrcs.has(src)) {
|
||||
loadedScriptSrcs.add(src);
|
||||
const s = document.createElement("script");
|
||||
s.src = src;
|
||||
s.async = false;
|
||||
document.body.appendChild(s);
|
||||
s.addEventListener("load", () => s.remove());
|
||||
}
|
||||
old.remove();
|
||||
});
|
||||
|
||||
// 2) Scripts inline
|
||||
container.querySelectorAll("script:not([src])").forEach((old) => {
|
||||
try {
|
||||
(0, eval)(old.textContent);
|
||||
} catch (e) {
|
||||
console.error("Error al ejecutar script inline:", e);
|
||||
}
|
||||
old.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* === 2. Extensión de Howl para eventos de tiempo ===========================
|
||||
* ========================================================================== */
|
||||
class ExtendedHowl extends Howl {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this._timeupdateListeners = [];
|
||||
this._interval = null;
|
||||
this._startTimeUpdate();
|
||||
}
|
||||
|
||||
_startTimeUpdate() {
|
||||
this._interval = setInterval(() => {
|
||||
if (this.playing()) this._emitTimeUpdate(this.seek());
|
||||
}, 250);
|
||||
}
|
||||
|
||||
_emitTimeUpdate(currentTime) {
|
||||
this._timeupdateListeners.forEach((cb) => cb(currentTime));
|
||||
}
|
||||
|
||||
onTimeUpdate(cb) {
|
||||
this._timeupdateListeners.push(cb);
|
||||
}
|
||||
|
||||
offTimeUpdate(cb) {
|
||||
this._timeupdateListeners = this._timeupdateListeners.filter((l) => l !== cb);
|
||||
}
|
||||
|
||||
play(id) {
|
||||
const result = super.play(id);
|
||||
if (!this._interval) this._startTimeUpdate();
|
||||
return result;
|
||||
}
|
||||
|
||||
pause(id) {
|
||||
const result = super.pause(id);
|
||||
clearInterval(this._interval);
|
||||
this._interval = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
stop(id) {
|
||||
super.stop(id);
|
||||
clearInterval(this._interval);
|
||||
this._interval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function createSound(audioUrl) {
|
||||
return audioUrl ? new ExtendedHowl({ src: [audioUrl] }) : null;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* === 3. Controlador de audio ===============================================
|
||||
* ========================================================================== */
|
||||
class AudioController {
|
||||
constructor() {
|
||||
this.audioElement = null;
|
||||
this.audioControlButton = document.getElementById("coursenav-audio-control");
|
||||
this.audioIcon = document.getElementById("coursenav-audio-icon");
|
||||
this.progressCircle = document.getElementById("coursenav-progress-circle");
|
||||
this.isMuted = false;
|
||||
|
||||
if (this.progressCircle) this.progressCircle.style.display = "none";
|
||||
if (this.audioControlButton) {
|
||||
this.audioControlButton.addEventListener("click", this.toggleAudio.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
stopAllSoundsAndPlay(howl) {
|
||||
Howler._howls?.forEach((sound) => sound.stop());
|
||||
this.setAudio(howl);
|
||||
this.playAudio();
|
||||
}
|
||||
|
||||
loadAudio(url) {
|
||||
if (this.audioElement) this.audioElement.stop();
|
||||
if (!url) return;
|
||||
this.audioElement = createSound(url);
|
||||
this._bindAudioEvents();
|
||||
}
|
||||
|
||||
playAudio() {
|
||||
this.audioElement?.play();
|
||||
}
|
||||
|
||||
pauseAudio() {
|
||||
this.audioElement?.pause();
|
||||
}
|
||||
|
||||
stopAudio() {
|
||||
this.audioElement?.stop();
|
||||
}
|
||||
|
||||
toggleAudio() {
|
||||
if (!this.audioElement) return;
|
||||
this.audioElement.playing() ? this.pauseAudio() : this.playAudio();
|
||||
}
|
||||
|
||||
toggleMute() {
|
||||
this.isMuted = !this.isMuted;
|
||||
Howler.mute(this.isMuted);
|
||||
this.updateIcon();
|
||||
document.querySelectorAll("video").forEach((v) => (v.muted = this.isMuted));
|
||||
}
|
||||
|
||||
onPlay() {
|
||||
if (this.progressCircle) this.progressCircle.style.display = "block";
|
||||
const t = this.audioElement.seek();
|
||||
this.updateProgressCircle(t);
|
||||
this.updateIcon();
|
||||
}
|
||||
|
||||
onEnd() {
|
||||
if (this.progressCircle) this.progressCircle.style.display = "none";
|
||||
this.updateIcon();
|
||||
}
|
||||
|
||||
updateIcon() {
|
||||
if (!this.audioIcon) return;
|
||||
const playing = this.audioElement?.playing();
|
||||
this.audioIcon.className = playing ? "fa-duotone fa-solid fa-pause" : "fa-duotone fa-solid fa-play";
|
||||
}
|
||||
|
||||
updateProgressCircle(currentTime) {
|
||||
if (!this.progressCircle || !this.audioElement) return;
|
||||
const r = parseFloat(this.progressCircle.getAttribute("r"));
|
||||
const circ = 2 * Math.PI * r;
|
||||
const offset = circ - (currentTime / this.audioElement.duration()) * circ;
|
||||
this.progressCircle.setAttribute("stroke-dashoffset", offset);
|
||||
}
|
||||
|
||||
setAudioUrl(url) {
|
||||
this.loadAudio(url);
|
||||
}
|
||||
|
||||
setAudio(howl) {
|
||||
if (!(howl instanceof ExtendedHowl)) return;
|
||||
this.audioElement?.stop();
|
||||
this.audioElement = howl;
|
||||
this._bindAudioEvents();
|
||||
}
|
||||
|
||||
_bindAudioEvents() {
|
||||
this.audioElement.on("play", this.onPlay.bind(this));
|
||||
this.audioElement.on("pause", this.updateIcon.bind(this));
|
||||
this.audioElement.on("stop", this.updateIcon.bind(this));
|
||||
this.audioElement.on("end", this.onEnd.bind(this));
|
||||
this.audioElement.onTimeUpdate(this.updateProgressCircle.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
const audioController = new AudioController();
|
||||
|
||||
/* ==========================================================================
|
||||
* === 4. Configuración global y constantes =================================
|
||||
* ========================================================================== */
|
||||
const COURSE_CONFIG_URL = COURSE_CONFIG.COURSE_CONFIG_URL || "config.json";
|
||||
const DEBUG = COURSE_CONFIG.DEBUG || false;
|
||||
const KEY_APP = COURSE_CONFIG.KEY || Infinity;
|
||||
|
||||
const MAIN_CONTENT = document.getElementById("coursenav-main-content");
|
||||
const WRAP_COURSE_CONTENT = document.getElementById("wrap-course-content");
|
||||
WRAP_COURSE_CONTENT.setAttribute("data-original-class", WRAP_COURSE_CONTENT.className);
|
||||
|
||||
const LOADER_ELEMENT = document.getElementById("coursenav-loader-course");
|
||||
const CLICK_SOUND = new Audio("audio/click.mp3");
|
||||
const PREV_BTN = document.getElementById("coursenav-prev-btn");
|
||||
const NEXT_BTN = document.getElementById("coursenav-next-btn");
|
||||
const COURSE_PROGRESS_BAR = document.getElementById("coursenav-progress-bar");
|
||||
const COURSE_MENU = document.getElementById("coursenav-main-menu");
|
||||
|
||||
pipwerks.SCORM.version = "1.2";
|
||||
pipwerks.debug.isActive = DEBUG;
|
||||
pipwerks.SCORM.handleExitMode = false;
|
||||
|
||||
let sessionStartTime;
|
||||
let scormAPIUnloaded = false;
|
||||
let courseStructure = null;
|
||||
let courseData = { contentArray: [], maximumAdvance: 0 };
|
||||
let currentIndex = 0;
|
||||
|
||||
/* ==========================================================================
|
||||
* === 5. SCORM: Inicialización y helpers de alto nivel =====================
|
||||
* ========================================================================== */
|
||||
function initializeScorm(callback) {
|
||||
const scorm = pipwerks.SCORM;
|
||||
const connected = scorm.init();
|
||||
if (connected) {
|
||||
const status = scorm.get("cmi.core.lesson_status");
|
||||
if (status === "not attempted") {
|
||||
scorm.set("cmi.core.lesson_status", "incomplete");
|
||||
scorm.save();
|
||||
}
|
||||
sessionStartTime = Date.now();
|
||||
} else {
|
||||
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,
|
||||
audio: m.audio,
|
||||
visited: false,
|
||||
courseTitle,
|
||||
moduleTitle: currentModuleTitle,
|
||||
parentTitle,
|
||||
});
|
||||
}
|
||||
if (m.topics) {
|
||||
buildContentArray(m.topics, courseTitle, currentModuleTitle, m.title);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function verifyContentArray(saved, curr) {
|
||||
if (!saved || !curr || saved.length !== curr.length) return false;
|
||||
return saved.every((s, i) => s.title === curr[i].title && s.content === curr[i].content);
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", `${COURSE_CONFIG_URL}?_=${Date.now()}`, true);
|
||||
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
||||
xhr.withCredentials = true;
|
||||
xhr.responseType = "json";
|
||||
|
||||
xhr.onload = function () {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
courseStructure = xhr.response;
|
||||
courseData = { contentArray: [], maximumAdvance: 0 };
|
||||
if (courseStructure.title) document.title = courseStructure.title;
|
||||
|
||||
buildContentArray(courseStructure.modules, courseStructure.title || "");
|
||||
|
||||
const saved = getProgress();
|
||||
if (saved.contentArray && verifyContentArray(saved.contentArray, courseData.contentArray)) {
|
||||
courseData = saved;
|
||||
}
|
||||
|
||||
if (courseData.maximumAdvance > 0) showStartOptions();
|
||||
else initializeCourse();
|
||||
|
||||
setProgress(courseData);
|
||||
} else {
|
||||
MAIN_CONTENT?.remove();
|
||||
console.error("Error cargando config:", xhr.status, xhr.statusText);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function () {
|
||||
MAIN_CONTENT?.remove();
|
||||
console.error("Error de red al cargar config");
|
||||
};
|
||||
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function initializeCourse() {
|
||||
if (COURSE_MENU) {
|
||||
buildMenu();
|
||||
hideDuplicateLinks();
|
||||
}
|
||||
if (courseData.contentArray.length > 0) loadContent();
|
||||
else MAIN_CONTENT.innerHTML = `<div class='alert alert-warning'>No hay contenido.</div>`;
|
||||
setupNavigation();
|
||||
}
|
||||
|
||||
function showStartOptions() {
|
||||
if (typeof Swal === "undefined") {
|
||||
currentIndex = confirm("¿Retomar tu progreso?") ? courseData.maximumAdvance : 0;
|
||||
initializeCourse();
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "¿Dónde quieres empezar?",
|
||||
text: "Retomar o comenzar de nuevo",
|
||||
icon: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Retomar",
|
||||
cancelButtonText: "Comenzar",
|
||||
target: MAIN_CONTENT,
|
||||
customClass: {
|
||||
confirmButton: "btn btn-primary",
|
||||
cancelButton: "btn btn-secondary",
|
||||
},
|
||||
}).then((res) => {
|
||||
currentIndex = res.isConfirmed ? courseData.maximumAdvance : 0;
|
||||
initializeCourse();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* === 6. Construcción y manejo del menú ====================================
|
||||
* ========================================================================== */
|
||||
function buildMenu() {
|
||||
COURSE_MENU.innerHTML = "";
|
||||
(courseStructure.modules || []).forEach((module) => {
|
||||
const ul = document.createElement("ul");
|
||||
ul.classList.add("course-menu");
|
||||
ul.appendChild(createMenuItem(module));
|
||||
COURSE_MENU.appendChild(ul);
|
||||
});
|
||||
hideDuplicateLinks();
|
||||
}
|
||||
|
||||
function createMenuItem(item) {
|
||||
const li = document.createElement("li");
|
||||
li.classList.add("menu-item");
|
||||
|
||||
const wdiv = document.createElement("div");
|
||||
wdiv.classList.add("witem");
|
||||
li.appendChild(wdiv);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.classList.add("coursenav-link");
|
||||
link.textContent = item.title;
|
||||
|
||||
const idx = courseData.contentArray.findIndex((c) => c.content === item.content && c.title === item.title);
|
||||
link.dataset.coursenavindex = idx;
|
||||
link.dataset.coursenavvisited = idx >= 0 && courseData.contentArray[idx].visited;
|
||||
wdiv.appendChild(link);
|
||||
|
||||
link.addEventListener("click", () => {
|
||||
CLICK_SOUND.play();
|
||||
const index = parseInt(link.dataset.coursenavindex, 10);
|
||||
if (index >= 0) {
|
||||
if (DEBUG || courseData.contentArray[index].visited) {
|
||||
currentIndex = index;
|
||||
closeSidebar();
|
||||
loadContent();
|
||||
} else {
|
||||
closeSidebar();
|
||||
showLockedContentWarning();
|
||||
}
|
||||
} else {
|
||||
const toggle = wdiv.querySelector(".toggle-icon");
|
||||
toggle && toggle.click();
|
||||
}
|
||||
});
|
||||
|
||||
if (item.topics?.length) {
|
||||
const toggle = document.createElement("span");
|
||||
toggle.classList.add("toggle-icon");
|
||||
toggle.innerHTML = '<i class="fa-duotone fa-solid fa-square-chevron-down"></i>';
|
||||
wdiv.appendChild(toggle);
|
||||
|
||||
const subUl = document.createElement("ul");
|
||||
subUl.classList.add("sub-ul", "open");
|
||||
item.topics.forEach((sub) => subUl.appendChild(createMenuItem(sub)));
|
||||
li.appendChild(subUl);
|
||||
|
||||
toggle.addEventListener("click", () => {
|
||||
CLICK_SOUND.play();
|
||||
const isOpen = subUl.classList.toggle("open");
|
||||
const icon = toggle.querySelector("i");
|
||||
icon.classList.toggle("fa-square-chevron-down", isOpen);
|
||||
icon.classList.toggle("fa-square-chevron-right", !isOpen);
|
||||
});
|
||||
}
|
||||
|
||||
return li;
|
||||
}
|
||||
|
||||
function hideDuplicateLinks() {
|
||||
function processUl(ul) {
|
||||
const seen = new Set();
|
||||
Array.from(ul.children)
|
||||
.filter((el) => el.tagName === "LI")
|
||||
.forEach((li) => {
|
||||
const link = li.querySelector(":scope > .witem > .coursenav-link");
|
||||
if (link) {
|
||||
const text = link.textContent.trim();
|
||||
if (seen.has(text)) li.style.display = "none";
|
||||
else seen.add(text);
|
||||
}
|
||||
li.querySelectorAll(":scope > ul").forEach(processUl);
|
||||
});
|
||||
}
|
||||
document.querySelectorAll("#coursenav-main-menu > ul.course-menu").forEach(processUl);
|
||||
}
|
||||
|
||||
function closeSidebar() {
|
||||
const offEl = document.getElementById("coursenav-offcanvas");
|
||||
const bsOff = bootstrap.Offcanvas.getInstance(offEl) || new bootstrap.Offcanvas(offEl);
|
||||
bsOff.hide();
|
||||
}
|
||||
|
||||
function showLockedContentWarning() {
|
||||
if (typeof Swal === "undefined") {
|
||||
alert("Debes completar el contenido actual antes de avanzar.");
|
||||
} else {
|
||||
Swal.fire({
|
||||
text: "Debes completar el contenido actual antes de avanzar.",
|
||||
icon: "warning",
|
||||
target: MAIN_CONTENT,
|
||||
customClass: {
|
||||
confirmButton: "btn btn-primary",
|
||||
cancelButton: "btn btn-warning",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* === 7. Carga de contenido y actualización de la interfaz ================
|
||||
* ========================================================================== */
|
||||
function loadContent() {
|
||||
MAIN_CONTENT.innerHTML = "";
|
||||
WRAP_COURSE_CONTENT.className = WRAP_COURSE_CONTENT.getAttribute("data-original-class");
|
||||
window.scrollTo(0, 0);
|
||||
LOADER_ELEMENT.style.display = "block";
|
||||
|
||||
const item = courseData.contentArray[currentIndex];
|
||||
if (!item?.content) return console.warn("Ítem inválido:", item);
|
||||
|
||||
audioController.stopAudio();
|
||||
Howler._howls?.forEach((h) => h.stop());
|
||||
|
||||
if (Swal.isVisible()) {
|
||||
Swal.close();
|
||||
}
|
||||
// Al cerrar, limpiamos clases y atributos de html/body
|
||||
document.documentElement.classList.remove("swal2-shown", "swal2-height-auto");
|
||||
document.body.classList.remove("swal2-shown", "swal2-height-auto");
|
||||
document.documentElement.removeAttribute("aria-hidden");
|
||||
document.body.removeAttribute("aria-hidden");
|
||||
// Y volvemos a quitar aria-hidden de los scripts
|
||||
document.querySelectorAll("script[aria-hidden]").forEach((el) => el.removeAttribute("aria-hidden"));
|
||||
fetch(item.content, { cache: "no-store" })
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error(r.statusText);
|
||||
return r.text();
|
||||
})
|
||||
.then((html) => {
|
||||
//MAIN_CONTENT.innerHTML = html;
|
||||
//executeInjectedScripts(MAIN_CONTENT);
|
||||
$(MAIN_CONTENT).html(html);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error cargando contenido:", err);
|
||||
MAIN_CONTENT.innerHTML = `<pre>${err.message}</pre>`;
|
||||
})
|
||||
.finally(() => {
|
||||
courseData.maximumAdvance = Math.max(courseData.maximumAdvance, currentIndex);
|
||||
LOADER_ELEMENT.style.display = "none";
|
||||
updateUITemplate();
|
||||
triggerSlideChange(currentIndex, courseData.contentArray);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUITemplate() {
|
||||
setProgress(courseData);
|
||||
updateNavigationButtons();
|
||||
updateProgressBar();
|
||||
updateCourseNavLinks();
|
||||
}
|
||||
|
||||
function updateCourseNavLinks() {
|
||||
document.querySelectorAll(".coursenav-link").forEach((link) => {
|
||||
const idx = parseInt(link.dataset.coursenavindex, 10);
|
||||
const item = courseData.contentArray[idx];
|
||||
if (item) {
|
||||
link.dataset.coursenavvisited = item.visited;
|
||||
item.visited ? link.classList.add("visited") : link.classList.remove("visited");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupNavigation() {
|
||||
PREV_BTN?.addEventListener("click", () => {
|
||||
CLICK_SOUND.play();
|
||||
navigate(-1);
|
||||
});
|
||||
NEXT_BTN?.addEventListener("click", () => {
|
||||
CLICK_SOUND.play();
|
||||
navigate(1);
|
||||
});
|
||||
updateNavigationButtons();
|
||||
}
|
||||
|
||||
function navigate(dir) {
|
||||
triggerBeforeSlideChange(currentIndex, courseData.contentArray);
|
||||
const newIndex = currentIndex + dir;
|
||||
if (newIndex < 0 || newIndex >= courseData.contentArray.length) return;
|
||||
|
||||
if (dir === -1 || courseData.contentArray[currentIndex].visited || DEBUG) {
|
||||
currentIndex = newIndex;
|
||||
loadContent();
|
||||
} else {
|
||||
showLockedContentWarning();
|
||||
}
|
||||
updateNavigationButtons();
|
||||
}
|
||||
|
||||
function updateNavigationButtons() {
|
||||
if (!courseData.contentArray.length || !courseData.contentArray[currentIndex]) {
|
||||
PREV_BTN.disabled = NEXT_BTN.disabled = true;
|
||||
return;
|
||||
}
|
||||
PREV_BTN.disabled = currentIndex === 0;
|
||||
NEXT_BTN.disabled = currentIndex >= courseData.contentArray.length - 1 || (!courseData.contentArray[currentIndex].visited && !DEBUG);
|
||||
}
|
||||
|
||||
function updateProgressBar() {
|
||||
const visited = courseData.contentArray.filter((i) => i.visited).length;
|
||||
const pct = (visited / courseData.contentArray.length) * 100;
|
||||
COURSE_PROGRESS_BAR.style.width = pct + "%";
|
||||
COURSE_PROGRESS_BAR.setAttribute("aria-valuenow", pct.toFixed(2));
|
||||
COURSE_PROGRESS_BAR.textContent = `${pct.toFixed(0)}%`;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* === 8. Eventos custom =====================================================
|
||||
* ========================================================================== */
|
||||
function triggerSlideChange(currentIndex, contentArray) {
|
||||
document.body.dispatchEvent(
|
||||
new CustomEvent("slideChange", {
|
||||
detail: { message: "Slide changed!", slideIndex: currentIndex, contentArray },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function triggerSlideCompleted(index, total, slideObj) {
|
||||
document.body.dispatchEvent(
|
||||
new CustomEvent("slideCompleted", {
|
||||
detail: { message: "Slide completed!", slideIndex: index, totalSlides: total, slide: slideObj },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function triggerBeforeSlideChange(currentIndex, contentArray) {
|
||||
document.body.dispatchEvent(
|
||||
new CustomEvent("beforeSlideChange", {
|
||||
detail: { message: "Before slide change!", currentIndex, contentArray },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* === 9. API públicas y utilitarios =========================================
|
||||
* ========================================================================== */
|
||||
function setSlideVisited(state = true) {
|
||||
courseData.contentArray[currentIndex].visited = state;
|
||||
updateUITemplate();
|
||||
triggerSlideCompleted(currentIndex, courseData.contentArray.length, courseData.contentArray[currentIndex]);
|
||||
}
|
||||
|
||||
function markSlidesAsVisited(indices) {
|
||||
indices
|
||||
.sort((a, b) => b - a)
|
||||
.forEach((i) => {
|
||||
currentIndex = i;
|
||||
setSlideVisited(true);
|
||||
});
|
||||
}
|
||||
|
||||
function resetCourse() {
|
||||
courseData.contentArray.forEach((i) => (i.visited = false));
|
||||
courseData.maximumAdvance = 0;
|
||||
currentIndex = 0;
|
||||
updateUITemplate();
|
||||
loadContent();
|
||||
}
|
||||
|
||||
function soundClick() {
|
||||
CLICK_SOUND.play();
|
||||
}
|
||||
|
||||
function isDebug() {
|
||||
return DEBUG;
|
||||
}
|
||||
|
||||
function gotoSlide(index) {
|
||||
const i = Math.floor(index);
|
||||
if (!isNaN(i) && i >= 0 && i < courseData.contentArray.length) {
|
||||
currentIndex = i;
|
||||
loadContent();
|
||||
} else {
|
||||
console.error("gotoSlide: índice inválido", index);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* === 10. SCORM: helpers de bajo nivel y progreso ============================
|
||||
* ========================================================================== */
|
||||
function getLessonLocation() {
|
||||
if (pipwerks.SCORM.connection.isActive) {
|
||||
const val = pipwerks.SCORM.get("cmi.core.lesson_location");
|
||||
return val ?? "";
|
||||
}
|
||||
return sessionStorage.getItem("cmi.core.lesson_location") ?? "";
|
||||
}
|
||||
|
||||
function setLessonLocation(loc) {
|
||||
if (pipwerks.SCORM.connection.isActive) {
|
||||
const ok = pipwerks.SCORM.set("cmi.core.lesson_location", loc);
|
||||
if (ok) pipwerks.SCORM.save();
|
||||
return ok;
|
||||
}
|
||||
sessionStorage.setItem("cmi.core.lesson_location", loc);
|
||||
return true;
|
||||
}
|
||||
|
||||
function getLessonStatus() {
|
||||
if (pipwerks.SCORM.connection.isActive) {
|
||||
return pipwerks.SCORM.get("cmi.core.lesson_status") ?? "";
|
||||
}
|
||||
return sessionStorage.getItem("cmi.core.lesson_status") ?? "";
|
||||
}
|
||||
|
||||
function setLessonStatus(st) {
|
||||
if (pipwerks.SCORM.connection.isActive) {
|
||||
const ok = pipwerks.SCORM.set("cmi.core.lesson_status", st);
|
||||
if (ok) pipwerks.SCORM.save();
|
||||
return ok;
|
||||
}
|
||||
sessionStorage.setItem("cmi.core.lesson_status", st);
|
||||
return true;
|
||||
}
|
||||
|
||||
function getScore() {
|
||||
let val = pipwerks.SCORM.connection.isActive ? pipwerks.SCORM.get("cmi.core.score.raw") : sessionStorage.getItem("cmi.core.score.raw");
|
||||
return val != null && val !== "" ? Number(val) : null;
|
||||
}
|
||||
|
||||
function setScore(sc) {
|
||||
if (pipwerks.SCORM.connection.isActive) {
|
||||
const ok = pipwerks.SCORM.set("cmi.core.score.raw", sc);
|
||||
if (ok) pipwerks.SCORM.save();
|
||||
return ok;
|
||||
}
|
||||
sessionStorage.setItem("cmi.core.score.raw", sc);
|
||||
return true;
|
||||
}
|
||||
|
||||
function getSuspendData() {
|
||||
let val = pipwerks.SCORM.connection.isActive ? pipwerks.SCORM.get("cmi.suspend_data") : sessionStorage.getItem("cmi.suspend_data");
|
||||
if (val) {
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function setSuspendData(data) {
|
||||
const json = JSON.stringify(data);
|
||||
if (pipwerks.SCORM.connection.isActive) {
|
||||
const ok = pipwerks.SCORM.set("cmi.suspend_data", json);
|
||||
if (ok) pipwerks.SCORM.save();
|
||||
return ok;
|
||||
}
|
||||
sessionStorage.setItem("cmi.suspend_data", json);
|
||||
return true;
|
||||
}
|
||||
|
||||
function getProgressPercent(byModule = false) {
|
||||
if (!byModule) {
|
||||
const visited = courseData.contentArray.filter((i) => i.visited).length;
|
||||
return parseFloat(((visited / courseData.contentArray.length) * 100).toFixed(2));
|
||||
}
|
||||
const currentSlide = courseData.contentArray[currentIndex];
|
||||
const moduleSlides = courseData.contentArray.filter((s) => s.moduleTitle === currentSlide.moduleTitle);
|
||||
const visited = moduleSlides.filter((i) => i.visited).length;
|
||||
return moduleSlides.length ? parseFloat(((visited / moduleSlides.length) * 100).toFixed(2)) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el porcentaje de avance de cada módulo.
|
||||
* @returns {Object<string, number>} Un objeto con
|
||||
* { "Título de módulo": porcentaje (0–100), … }
|
||||
*/
|
||||
function getProgressByModule() {
|
||||
// Recolectamos totales y visitados por módulo
|
||||
const stats = {};
|
||||
courseData.contentArray.forEach((slide) => {
|
||||
const mod = slide.moduleTitle || "Sin módulo";
|
||||
if (!stats[mod]) stats[mod] = { total: 0, visited: 0 };
|
||||
stats[mod].total++;
|
||||
if (slide.visited) stats[mod].visited++;
|
||||
});
|
||||
|
||||
// Calculamos porcentajes
|
||||
const result = {};
|
||||
Object.entries(stats).forEach(([mod, { total, visited }]) => {
|
||||
result[mod] = parseFloat(((visited / total) * 100).toFixed(2));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function setProgress(p) {
|
||||
setSuspendData(p);
|
||||
}
|
||||
|
||||
function getProgress() {
|
||||
return getSuspendData() || { contentArray: [], maximumAdvance: 0 };
|
||||
}
|
||||
|
||||
function finishScorm() {
|
||||
if (pipwerks.SCORM.connection.isActive && !scormAPIUnloaded) {
|
||||
const elapsed = (Date.now() - sessionStartTime) / 1000;
|
||||
const hh = String(Math.floor(elapsed / 3600)).padStart(2, "0");
|
||||
const mm = String(Math.floor((elapsed % 3600) / 60)).padStart(2, "0");
|
||||
const ss = String(Math.floor(elapsed % 60)).padStart(2, "0");
|
||||
pipwerks.SCORM.set("cmi.core.session_time", `${hh}:${mm}:${ss}`);
|
||||
pipwerks.SCORM.save();
|
||||
pipwerks.SCORM.quit();
|
||||
scormAPIUnloaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function getScormData(key) {
|
||||
return pipwerks.SCORM.connection.isActive ? pipwerks.SCORM.get(key) ?? "" : sessionStorage.getItem(key) ?? "";
|
||||
}
|
||||
|
||||
function setScormData(key, value) {
|
||||
if (pipwerks.SCORM.connection.isActive) {
|
||||
const ok = pipwerks.SCORM.set(key, value);
|
||||
if (ok) pipwerks.SCORM.save();
|
||||
return ok;
|
||||
}
|
||||
sessionStorage.setItem(key, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* === 11. Arranque DOM y offcanvas =========================================
|
||||
* ========================================================================== */
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initializeScorm(() => loadConfig());
|
||||
window.addEventListener("beforeunload", finishScorm);
|
||||
|
||||
// Tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map((el) => new bootstrap.Tooltip(el));
|
||||
|
||||
// Offcanvas backdrop dentro de custom container
|
||||
const offcanvasEl = document.getElementById("coursenav-offcanvas");
|
||||
const customContainer = document.getElementById("wrap-course-content");
|
||||
const bsOffcanvas = bootstrap.Offcanvas.getOrCreateInstance(offcanvasEl);
|
||||
|
||||
offcanvasEl.addEventListener("shown.bs.offcanvas", () => {
|
||||
if (customContainer.querySelector(".offcanvas-backdrop")) return;
|
||||
const backdrop = document.createElement("div");
|
||||
backdrop.className = "offcanvas-backdrop fade";
|
||||
customContainer.appendChild(backdrop);
|
||||
backdrop.getBoundingClientRect();
|
||||
backdrop.classList.add("show");
|
||||
backdrop.addEventListener("click", () => bsOffcanvas.hide());
|
||||
});
|
||||
|
||||
offcanvasEl.addEventListener("hidden.bs.offcanvas", () => {
|
||||
customContainer.querySelector(".offcanvas-backdrop")?.remove();
|
||||
});
|
||||
});
|
||||
|
||||
/* ==========================================================================
|
||||
* === 12. API pública =======================================================
|
||||
* ========================================================================== */
|
||||
return {
|
||||
/* Audio */
|
||||
audioController,
|
||||
createSound,
|
||||
soundClick,
|
||||
|
||||
/* Debug */
|
||||
isDebug,
|
||||
|
||||
/* SCORM Básico */
|
||||
getStudentName: () => getScormData("cmi.core.student_name"),
|
||||
getLessonLocation,
|
||||
setLessonLocation,
|
||||
getLessonStatus,
|
||||
setLessonStatus,
|
||||
getScore,
|
||||
setScore,
|
||||
getSuspendData,
|
||||
setSuspendData,
|
||||
getScormData,
|
||||
setScormData,
|
||||
|
||||
/* Navegación */
|
||||
nextSlide: () => navigate(1),
|
||||
prevSlide: () => navigate(-1),
|
||||
gotoSlide,
|
||||
isVisited: () => courseData.contentArray[currentIndex]?.visited || false,
|
||||
isCompletedSlideIndex: (idx) => (idx >= 0 && idx < courseData.contentArray.length ? courseData.contentArray[idx].visited : undefined),
|
||||
|
||||
/* Estado del curso */
|
||||
getCurrentSlide: () => courseData.contentArray[currentIndex],
|
||||
getCurrentIndex: () => currentIndex,
|
||||
getCourseData: () => courseData,
|
||||
getCourseStructure: () => courseStructure,
|
||||
getCourseConfig: () => COURSE_CONFIG,
|
||||
getCourseTitle: () => courseStructure?.title || "",
|
||||
getCourseModules: () => courseStructure?.modules || [],
|
||||
getCourseContentArray: () => courseData.contentArray,
|
||||
|
||||
/* Curso actual */
|
||||
resetCourse,
|
||||
markSlidesAsVisited,
|
||||
setSlideVisited,
|
||||
completeLesson: () => setLessonStatus("completed"),
|
||||
updateProgressBar,
|
||||
getProgressPercent,
|
||||
getProgressByModule,
|
||||
getCurrentModuleSlides: () => {
|
||||
const module = courseData.contentArray[currentIndex]?.moduleTitle;
|
||||
return courseData.contentArray.filter((s) => s.moduleTitle === module);
|
||||
},
|
||||
getCurrentModuleTitle: () => courseData.contentArray[currentIndex]?.moduleTitle || "",
|
||||
getCurrentCourseTitle: () => courseData.contentArray[currentIndex]?.courseTitle || "",
|
||||
|
||||
/* SCORM avanzado */
|
||||
save: () => (pipwerks.SCORM.connection.isActive ? pipwerks.SCORM.save() : setProgress(courseData)),
|
||||
reload: loadContent,
|
||||
loadModule: (moduleTitle) => {
|
||||
const idx = courseData.contentArray.findIndex((s) => s.moduleTitle === moduleTitle);
|
||||
if (idx >= 0) {
|
||||
currentIndex = idx;
|
||||
loadContent();
|
||||
}
|
||||
},
|
||||
};
|
||||
})(COURSE_CONFIG);
|
||||
|
||||
window.CourseNav = CourseNav;
|
||||
Vendored
+1
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
+2
File diff suppressed because one or more lines are too long
Vendored
+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
+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