From acdb47c20a6872b8d16c97306a794d08cd324ae6 Mon Sep 17 00:00:00 2001 From: Vincent Vanwaelscappel Date: Tue, 14 Dec 2021 17:37:47 +0100 Subject: [PATCH] wip #4907 @1 --- index.html | 1 + js/libs/scorm/apiwrapper.js | 1089 ++++++++++++++++++++++------------- js/libs/scorm/facade.js | 44 ++ js/scormpackage.js | 260 ++++++++- 4 files changed, 971 insertions(+), 423 deletions(-) create mode 100644 js/libs/scorm/facade.js diff --git a/index.html b/index.html index b81cbd2..0a6b685 100644 --- a/index.html +++ b/index.html @@ -173,6 +173,7 @@ + diff --git a/js/libs/scorm/apiwrapper.js b/js/libs/scorm/apiwrapper.js index 8adfc01..16f7aeb 100644 --- a/js/libs/scorm/apiwrapper.js +++ b/js/libs/scorm/apiwrapper.js @@ -1,444 +1,739 @@ -/******************************************************************************* - ** - ** This software is provided "AS IS," without a warranty of any kind. ALL - ** EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY - ** IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR NON- - ** INFRINGEMENT, ARE HEREBY EXCLUDED. PAUL BECKWITH AND HIS LICENSORS SHALL NOT BE LIABLE - ** FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR - ** DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL PAUL BECKWITH OR ITS - ** LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, - ** INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER - ** CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF - ** OR INABILITY TO USE SOFTWARE, EVEN IF CTC HAS BEEN ADVISED OF THE POSSIBILITY - ** OF SUCH DAMAGES. - ** javascript: - ** var result = doLMSInitialize(); - ** if (result != true) - ** { -** // handle error -** } - ** - *******************************************************************************/ - - -var _Debug = false; // set this to false to turn debugging off - // and get rid of those annoying alert boxes. - -// Define exception/error codes -var _NoError = 0; -var _GeneralException = 101; -var _ServerBusy = 102; -var _InvalidArgumentError = 201; -var _ElementCannotHaveChildren = 202; -var _ElementIsNotAnArray = 203; -var _NotInitialized = 301; -var _NotImplementedError = 401; -var _InvalidSetValue = 402; -var _ElementIsReadOnly = 403; -var _ElementIsWriteOnly = 404; -var _IncorrectDataType = 405; - - -// local variable definitions -var apiHandle = null; -var API = null; -var findAPITries = 0; - - -/******************************************************************************* - ** - ** Function: doLMSInitialize() - ** Inputs: None - ** Return: CMIBoolean true if the initialization was successful, or - ** CMIBoolean false if the initialization failed. - ** - ** Description: - ** Initialize communication with LMS by calling the LMSInitialize - ** function which will be implemented by the LMS. - ** - *******************************************************************************/ -function doLMSInitialize() { - startTimer(); - - var api = getAPIHandle(); - if (api == null) { - console.error("Unable to locate the LMS's API Implementation.\nLMSInitialize was not successful."); - return false; +/*global console*/ + +/* =========================================================== + +pipwerks SCORM Wrapper for JavaScript +v1.1.20160322 + +Created by Philip Hutchison, January 2008-2016 +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 + +=============================================================== */ + +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 = 12, + 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; } - var result = api.LMSInitialize(""); + //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 (result.toString() != "true") { - var err = ErrorHandler(); + 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 result.toString(); -} - -/******************************************************************************* - ** - ** Function doLMSFinish() - ** Inputs: None - ** Return: CMIBoolean true if successful - ** CMIBoolean false if failed. - ** - ** Description: - ** Close communication with LMS by calling the LMSFinish - ** function which will be implemented by the LMS - ** - *******************************************************************************/ -function doLMSFinish() { - - setSessionTime(); - - var api = getAPIHandle(); - if (api == null) { - console.error("Unable to locate the LMS's API Implementation.\nLMSFinish was not successful."); - return "false"; + 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); } - else { - // call the LMSFinish function that should be implemented by the API - var result = api.LMSFinish(""); - if (result.toString() != "true") { - var err = ErrorHandler(); - } + 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); } - return result.toString(); -} - -/******************************************************************************* - ** - ** Function doLMSGetValue(name) - ** Inputs: name - string representing the cmi data model defined category or - ** element (e.g. cmi.core.student_id) - ** Return: The value presently assigned by the LMS to the cmi data model - ** element defined by the element or category identified by the name - ** input value. - ** - ** Description: - ** Wraps the call to the LMS LMSGetValue method - ** - *******************************************************************************/ -function doLMSGetValue(name) { - var api = getAPIHandle(); - if (api == null) { - console.error("Unable to locate the LMS's API Implementation.\nLMSGetValue was not successful."); - return ""; + if (API) { + scorm.API.isFound = true; + } else { + trace("API.get failed: Can't find the API!"); } - else { - var value = api.LMSGetValue(name); - var errCode = api.LMSGetLastError().toString(); - if (errCode != _NoError) { - // an error was encountered so display the error description - var errDescription = api.LMSGetErrorString(errCode); - console.error("LMSGetValue(" + name + ") failed. \n" + errDescription); - return ""; - } - else { - return value.toString(); + 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) { + if (API.Initialized !== true) { + switch (scorm.version) { + case "1.2" : + success = makeBoolean(API.LMSInitialize("")); + break; + case "2004": + success = makeBoolean(API.Initialize("")); + break; + } + } else { + success = true; + } + + if (success) { + //Double-check that connection is active and working before returning 'true' boolean + errorCode = debug.getCode(); + if (API.Initialized===true || (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"); + scorm.set('cmi.success_status', 'unknown'); + break; + //SCORM 2004 only + case "unknown" : + scorm.status("set", "incomplete"); + scorm.set('cmi.success_status', 'unknown'); + 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 + default : + scorm.status('set', completionStatus); + scorm.set('cmi.success_status', scorm.get('cmi.success_status')); + break; + } + //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."); + } -} - -/******************************************************************************* - ** - ** Function doLMSSetValue(name, value) - ** Inputs: name -string representing the data model defined category or element - ** value -the value that the named element or category will be assigned - ** Return: CMIBoolean true if successful - ** CMIBoolean false if failed. - ** - ** Description: - ** Wraps the call to the LMS LMSSetValue function - ** - *******************************************************************************/ -function doLMSSetValue(name, value) { - var api = getAPIHandle(); - if (api == null) { - console.error("Unable to locate the LMS's API Implementation.\nLMSSetValue was not successful."); - return; + 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", "suspend"); + break; + case "2004": + success = scorm.set("cmi.exit", "suspend"); + break; + } + } + } + + //Ensure we persist the data + success = scorm.save(); + + 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."); } - else { - var result = api.LMSSetValue(name, value); - if (result.toString() != "true") { - var err = ErrorHandler(); + 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; + } - return; -} - -/******************************************************************************* - ** - ** Function doLMSCommit() - ** Inputs: None - ** Return: None - ** - ** Description: - ** Call the LMSCommit function - ** - *******************************************************************************/ -function doLMSCommit() { - var api = getAPIHandle(); - if (api == null) { - console.error("Unable to locate the LMS's API Implementation.\nLMSCommit was not successful."); - return "false"; + if (success) { + if (parameter === "cmi.core.lesson_status" || parameter === "cmi.completion_status") { + scorm.data.completionStatus = value; + } else if (parameter == 'cmi.core.exit' || parameter == 'cmi.exit') { + scorm.data.exitStatus = 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."); } - else { - var result = api.LMSCommit(""); - if (result != "true") { - var err = ErrorHandler(); + 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 result.toString(); -} - -/******************************************************************************* - ** - ** Function doLMSGetLastError() - ** Inputs: None - ** Return: The error code that was set by the last LMS function call - ** - ** Description: - ** Call the LMSGetLastError function - ** - *******************************************************************************/ -function doLMSGetLastError() { - var api = getAPIHandle(); - if (api == null) { - console.error("Unable to locate the LMS's API Implementation.\nLMSGetLastError was not successful."); - //since we can't get the error code from the LMS, return a general error - return _GeneralError; - } + return success; - return api.LMSGetLastError().toString(); -} - -/******************************************************************************* - ** - ** Function doLMSGetErrorString(errorCode) - ** Inputs: errorCode - Error Code - ** Return: The textual description that corresponds to the input error code - ** - ** Description: - ** Call the LMSGetErrorString function - ** - ********************************************************************************/ -function doLMSGetErrorString(errorCode) { - var api = getAPIHandle(); - if (api == null) { - console.error("Unable to locate the LMS's API Implementation.\nLMSGetErrorString was not successful."); - } +}; - return api.LMSGetErrorString(errorCode).toString(); -} - -/******************************************************************************* - ** - ** Function doLMSGetDiagnostic(errorCode) - ** Inputs: errorCode - Error Code(integer format), or null - ** Return: The vendor specific textual description that corresponds to the - ** input error code - ** - ** Description: - ** Call the LMSGetDiagnostic function - ** - *******************************************************************************/ -function doLMSGetDiagnostic(errorCode) { - var api = getAPIHandle(); - if (api == null) { - console.error("Unable to locate the LMS's API Implementation.\nLMSGetDiagnostic was not successful."); - } - return api.LMSGetDiagnostic(errorCode).toString(); -} - -/******************************************************************************* - ** - ** Function LMSIsInitialized() - ** Inputs: none - ** Return: true if the LMS API is currently initialized, otherwise false - ** - ** Description: - ** Determines if the LMS API is currently initialized or not. - ** - *******************************************************************************/ -function LMSIsInitialized() { - // there is no direct method for determining if the LMS API is initialized - // for example an LMSIsInitialized function defined on the API so we'll try - // a simple LMSGetValue and trap for the LMS Not Initialized Error - - var api = getAPIHandle(); - if (api == null) { - console.error("Unable to locate the LMS's API Implementation.\nLMSIsInitialized() failed."); - return false; - } - else { - var value = api.LMSGetValue("cmi.core.student_name"); - var errCode = api.LMSGetLastError().toString(); - if (errCode == _NotInitialized) { - return false; +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; } - else { - return true; + + 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."); } - } -} - -/******************************************************************************* - ** - ** Function ErrorHandler() - ** Inputs: None - ** Return: The current value of the LMS Error Code - ** - ** Description: - ** Determines if an error was encountered by the previous API call - ** and if so, displays a message to the user. If the error code - ** has associated text it is also displayed. - ** - *******************************************************************************/ -function ErrorHandler() { - var api = getAPIHandle(); - if (api == null) { - console.error("Unable to locate the LMS's API Implementation.\nCannot determine LMS error code."); - return; + } else { + trace(traceMsgPrefix + ": action was not specified."); } - // check for errors caused by or from the LMS - var errCode = api.LMSGetLastError().toString(); - if (errCode != _NoError) { - // an error was encountered so display the error description - var errDescription = api.LMSGetErrorString(errCode); - - if (_Debug == true) { - errDescription += "\n"; - errDescription += api.LMSGetDiagnostic(null); - // by passing null to LMSGetDiagnostic, we get any available diagnostics - // on the previous error. - } + return success; - console.error(errDescription); - } +}; - return errCode; -} - -/****************************************************************************** - ** - ** Function getAPIHandle() - ** Inputs: None - ** Return: value contained by APIHandle - ** - ** Description: - ** Returns the handle to API object if it was previously set, - ** otherwise it returns null - ** - *******************************************************************************/ -function getAPIHandle() { - if (apiHandle == null) { - apiHandle = getAPI(); - } - return apiHandle; -} - - -/******************************************************************************* - ** - ** Function findAPI(win) - ** Inputs: win - a Window Object - ** Return: If an API object is found, it's returned, otherwise null is returned - ** - ** Description: - ** This function looks for an object named API in parent and opener windows - ** - *******************************************************************************/ -function findAPI(win) { - try { - while ((win.API == null) && (win.parent != null) && (win.parent != win)) { - findAPITries++; - // Note: 7 is an arbitrary number, but should be more than sufficient - if (findAPITries > 7) { - console.error("Error finding API -- too deeply nested."); - return null; - } +// ------------------------------------------------------------------------- // +// --- 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 () { - win = win.parent; + 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; } - return win.API; - } catch (err) { - return null; + } else { + trace("SCORM.debug.getCode failed: API is null."); } -} - - -/******************************************************************************* - ** - ** Function getAPI() - ** Inputs: none - ** Return: If an API object is found, it's returned, otherwise null is returned - ** - ** Description: - ** This function 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). - ** - *******************************************************************************/ -function getAPI() { - var theAPI = findAPI(window); - if ((theAPI == null) && (window.opener != null) && (typeof(window.opener) != "undefined")) { - theAPI = findAPI(window.opener); - } - if (theAPI == null) { - console.error("Unable to find an API adapter"); + + 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 theAPI -} -var startTime; - -function startTimer() { - startTime = new Date().getTime(); -} - - -// call function right before LMSFinish -function setSessionTime() { - var currentTime = new Date(); - var endTime = currentTime.getTime() - var calculatedTime = endTime - startTime; - var totalHours = Math.floor(calculatedTime / 1000 / 60 / 60); - calculatedTime = calculatedTime - totalHours * 1000 * 60 * 60 - - if (totalHours < 1000 && totalHours > 99) { - totalHours = "0" + totalHours; - } else if (totalHours < 100 && totalHours > 9) { - totalHours = "00" + totalHours; - } else if (totalHours < 10) { - totalHours = "000" + totalHours; + 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."); } - var totalMinutes = Math.floor(calculatedTime / 1000 / 60); - calculatedTime = calculatedTime - totalMinutes * 1000 * 60; + return String(result); +}; - if (totalMinutes < 10) { - totalMinutes = "0" + totalMinutes; - } - var totalSeconds = Math.floor(calculatedTime / 1000); +// ------------------------------------------------------------------------- // +// --- 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; + - if (totalSeconds < 10) { - totalSeconds = "0" + totalSeconds; +// ------------------------------------------------------------------------- // +// --- 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; } +}; - var sessionTime = totalHours + ":" + totalMinutes + ":" + totalSeconds; - doLMSSetValue("cmi.core.session_time", sessionTime); -} +/* ------------------------------------------------------------------------- + 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); + } + } +}; diff --git a/js/libs/scorm/facade.js b/js/libs/scorm/facade.js new file mode 100644 index 0000000..fd27768 --- /dev/null +++ b/js/libs/scorm/facade.js @@ -0,0 +1,44 @@ +function SCORMFacade() { +} + +SCORMFacade.prototype = { + Initialize: function () { + this._log('Init'); + return true; + }, + Terminate: function () { + this._log('Terminate'); + return finishScorm(); + }, + GetValue: function (key) { + this._log('Get value ' + key); + if(key==='cmi.completion_status'){ + + } + return getScormValue(key); + }, + SetValue: function (key, value) { + this._log('Set value ' + key + ' :: ' + value); + if(key===''){ + + } + + // TODO save data in module + }, + Commit: function (d) { + this._log('Commit'); + return true; + }, + GetLastError: function () { + return 0; + }, + GetErrorString: function () { + return ''; + }, + GetDiagnostic: function () { + return ''; + }, + _log: function (log) { + console.log('SCORM facade : ' + log); + }, +}; \ No newline at end of file diff --git a/js/scormpackage.js b/js/scormpackage.js index 0e93cec..ce38a76 100644 --- a/js/scormpackage.js +++ b/js/scormpackage.js @@ -1,16 +1,145 @@ window.savedState = {}; -console.log(window.savedState); +SCORM_INITED = false; +SCORM_START_TIME = null; +SCORM_INTERACTION_TIMESTAMPS = []; +SCORM_CORRECT_ANSWERS = []; +SCORM_ID_TO_N = {}; +SCORM_WEIGHTING = 0; +SCORM_QUESTIONS = []; +SCORM_SUCCESS_STATUS = 'unknown'; +SCORM_SUCCESS_SCORE = 0; +SCORM_EVENTS_INITED = false; +SCORM_INTERACTIONS_INITED = false; +SCORM_LOCATION_INITED = false; +SCORM_OK = false; + +var _CMI12 = { + 'location': 'cmi.core.lesson_location', + 'status': "cmi.core.lesson_status", + 'session_time': 'cmi.core.session_time', + 'success_status': '', + 'exit': 'cmi.core.exit', + 'cmi.score.raw': 'cmi.core.score.raw', + 'cmi.score.min': 'cmi.core.score.min', + 'cmi.score.max': 'cmi.core.score.max', + 'cmi.score.scaled': '', +}; + +var _CMI2004 = { + 'location': 'cmi.location', + 'status': 'cmi.completion_status', + 'session_time': 'cmi.session_time', + 'success_status': 'cmi.success_status', + 'exit': 'cmi.exit', +}; + + +$(function () { + $("header #logo").html(getSpriteIcon('logo')); + $("header #tile").html(getSpriteIcon('tile')); + initScorm(); +}); + +function _cmi(key) { + var res = null; + switch (pipwerks.SCORM.version) { + case "1.2" : + res = _CMI12[key]; + break; + case '2004': + res = _CMI2004[key]; + break; + } + if (res == undefined || res == null) { + res = key; + } + return res; +} -(function (global) { - $(function () { - $("header #logo").html(getSpriteIcon('logo')); - $("header #tile").html(getSpriteIcon('tile')); - initScorm(); - }); +function initScorm() { + if (SCORM_INITED) { + return; + } -})(typeof window === 'undefined' ? this : window); + SCORM_INITED = true; + try { + if (pipwerks.SCORM.init()) { + SCORM_OK = true; + } + } catch (e) { + + } + + try { + if (FORCE_SCORM) { + SCORM_OK = true; + } + } catch (e) { + + } + + setContents(); + initEvents(); + if (SCORM_OK) { + scormExit(); + startScormTimer(); + initScormEvents(); + } + window.API = null; + window.API_1484_11 = new SCORMFacade(); + return SCORM_OK; +} + +function scormMarkAsComplete() { + if (!SCORM_OK) { + return; + } + scormExit(); +} + + +function finishScorm() { + if (!SCORM_OK) { + return; + } + setSessionTime(); + pipwerks.SCORM.save(); + if (fluidbook.settings.multilang === '') { + pipwerks.SCORM.quit(); + } +} + +function scormExit() { + if (!SCORM_OK) { + return; + } + var v = 'suspend'; + if (fluidbook.settings.scorm_force_attempts) { + if (pipwerks.SCORM.version === '1.2') { + v = 'logout'; + } else { + v = 'normal'; + } + } + + setScormValue('exit', v); +} +function startScormTimer() { + SCORM_START_TIME = new Date(); +} + +function scormCompleteAndClose() { + scormComplete(); + scormClose(); +} + +function scormClose() { + parent.close(); + top.close(); + window.close(); +} function initEvents() { $(document).on('click', '[data-id]', function () { @@ -84,7 +213,6 @@ function setContents() { $('tbody').html(''); $.each(DATA.modules, function (k, v) { var s = (status[v.id] === undefined) ? {complete_status: 'incomplete', success_status: 'unknown', score: 0, location: {}} : status[v.id]; - console.log(s); var tr = $('' + getSpriteIcon('icon-' + v.type) + ''); $('tbody').append(tr); tr.append('' + v.title + ''); @@ -114,24 +242,30 @@ function setContents() { // Using `this` for web workers & supports Browserify / Webpack. })(typeof window === 'undefined' ? this : window); -function initScorm() { - SCORM = doLMSInitialize(); + +function initScormEvents() { + if (!SCORM_OK || SCORM_EVENTS_INITED) { + return; + } + SCORM_EVENTS_INITED = true; $(window).on('unload', function () { - doLMSFinish(); + finishScorm(); }); - try { - var l = getScormValue('cmi.location'); - if (l === null || l === 'null') { - window.savedState = {}; - } else { - window.savedState = JSON.parse(l); - } - } catch (e) { + if (SCORM_LOCATION_INITED) { + return; + } + SCORM_LOCATION_INITED = true; + var currentLocation = getScormValue('location'); + if (currentLocation !== 'null' && currentLocation !== '') { + window.savedState = {}; + } else { + window.savedState = JSON.parse(currentLocation); } + if (window.savedState === undefined || window.savedState === null || window.savedState === 'null') { window.savedState = {}; } @@ -139,7 +273,10 @@ function initScorm() { initEvents(); setContents(); - return true; + + setInterval(function () { + pipwerks.SCORM.save(); + }, 5000); } function setSCORMLocation(location) { @@ -158,19 +295,29 @@ function setSCORMScore(score, max, min) { function getScormValue(elementName) { - if (!SCORM) { + if (!SCORM_OK) { return null; } - var result = String(doLMSGetValue(elementName)); + var cmi = _cmi(elementName); + if (cmi == '') { + return null; + } + var result = pipwerks.SCORM.get(cmi); return result; } function setScormValue(elementName, value) { - if (!SCORM) { + if (!SCORM_OK) { return; } - var result = doLMSSetValue(elementName, value); - doLMSCommit(); + var cmi = _cmi(elementName); + if (cmi == '') { + return false; + } + var result = pipwerks.SCORM.set(cmi, value); + if (fluidbook.settings.scorm_immediate_commit) { + pipwerks.SCORM.save(); + } return result; } @@ -212,4 +359,65 @@ function getSpriteIcon(icon, attrs, dimensions) { a.push(k + '="' + v + '"'); }); return ''; +} + + +function setSessionTime() { + if (!SCORM_OK) { + return; + } + var currentTime = new Date(); + var sessionTime; + + var endTime = currentTime.getTime() + var calculatedTime = endTime - SCORM_START_TIME; + + if (pipwerks.SCORM.version == '1.2') { + var totalHours = Math.floor(calculatedTime / 1000 / 60 / 60); + calculatedTime = calculatedTime - totalHours * 1000 * 60 * 60 + if (totalHours < 1000 && totalHours > 99) { + totalHours = "0" + totalHours; + } else if (totalHours < 100 && totalHours > 9) { + totalHours = "00" + totalHours; + } else if (totalHours < 10) { + totalHours = "000" + totalHours; + } + + var totalMinutes = Math.floor(calculatedTime / 1000 / 60); + calculatedTime = calculatedTime - totalMinutes * 1000 * 60; + if (totalMinutes < 10) { + totalMinutes = "0" + totalMinutes; + } + + var totalSeconds = Math.floor(calculatedTime / 1000); + if (totalSeconds < 10) { + totalSeconds = "0" + totalSeconds; + } + sessionTime = totalHours + ":" + totalMinutes + ":" + totalSeconds; + setScormValue('session_time', sessionTime); + } else { + setScormValue('session_time', scormSecondsToTimeInterval(calculatedTime / 1000)); + } + +} + +function dateToScormTime(date) { + var res = date.toISOString(); + var e = res.split('.'); + e.pop(); + return e.join('.'); +} + +function getScormTimeInterval(start, end) { + var diff = Math.round((end.getTime() - start.getTime()) / 1000); + return scormSecondsToTimeInterval(diff); +} + +function scormSecondsToTimeInterval(diff) { + var diff = Math.round(diff); + var h = Math.floor(diff / 3600); + diff = diff % 3600; + var m = Math.floor(diff / 60); + var s = diff % 60; + return 'PT' + h + 'H' + m + 'M' + s + 'S'; } \ No newline at end of file -- 2.39.5