/**
* File : report/SCORM.js
* Created : 18/07/2016
* By : Francesc Busquets <francesc@gmail.com>
*
* JClic.js
* An HTML5 player of JClic activities
* https://projectestac.github.io/jclic.js
*
* @source https://github.com/projectestac/jclic.js
*
* @license EUPL-1.2
* @licstart
* (c) 2000-2020 Educational Telematic Network of Catalonia (XTEC)
*
* Licensed under the EUPL, Version 1.1 or -as soon they will be approved by
* the European Commission- subsequent versions of the EUPL (the "Licence");
* You may not use this work except in compliance with the Licence.
*
* You may obtain a copy of the Licence at:
* https://joinup.ec.europa.eu/software/page/eupl
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the Licence is distributed on an "AS IS" basis, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* Licence for the specific language governing permissions and limitations
* under the Licence.
* @licend
* @module
*/
/* global window */
import $ from 'jquery';
import { log } from '../Utils.js';
/**
* This class detects if JClic.js is running in an SCORM environment and, if true,
* exposes the methods needed to notify the results of activities.
* Both SCORM 1.2 and 2004 are supported.
*/
export class SCORM {
/**
* SCORM constructor
* @param {object} API - The global SCORM API object
* @param {module:report/Reporter.Reporter} reporter - The {@link module:Reporter.Reporter Reporter} associated to this SCORM object
*/
constructor(API, reporter) {
this.API = API;
// Check if 'API' has a function named 'Initialized'
if (typeof API.Initialize === 'function')
this.is2004 = true;
else {
// SCORM 1.2
this.prefix = 'LMS';
this.core = 'cmi.core.';
}
this.reporter = reporter;
}
/**
* Recursive function used to find the SCORM "API" object
* @param {object} win - The 'window' object to scan for global SCORM API objects
* @param {number} tries - Recursive attempts currently achieved
* @returns {object} - The global SCORM API object, or `null` if not found
*/
static scanForAPI(win, tries) {
if (win.API_1484_11 && win.API_1184_11.Initialize && win.API_1184_11.SetValue && win.API_1184_11.Commit)
return win.API_1184_11;
else if (win.API && win.API.LMSInitialize && win.API.LMSSetValue && win.API.LMSCommit)
return win.API;
else if (win.parent && win.parent !== win && tries++ < SCORM.DISCOVER_MAX_TRIES)
return SCORM.scanForAPI(win.parent, tries);
else
return null;
}
/**
* Checks for the presence of a SCORM API on the current browser session.
* @param {module:report/Reporter.Reporter} reporter - The {@link module:Reporter.Reporter Reporter} linked to the requested SCORM object
* @returns {module:report/SCORM.SCORM} - A valid SCORM object, or `null` if no SCORM API was found.
*/
static getSCORM(reporter) {
let result = null;
try {
let api = SCORM.scanForAPI(window, 0);
if (api === null && window.opener)
api = SCORM.scanForAPI(window.opener, 0);
if (api) {
result = new SCORM(api, reporter);
if (!result.initialize())
result = null;
}
} catch (ex) {
result = null;
log('warn', 'Unable to use SCORM: %s', ex.toString());
}
return result;
}
/**
* Initializes communication with the SCORM API
* @returns {boolean}
*/
initialize() {
let result = false;
try {
result = this.API[this.prefix + 'Initialize']('');
if (result) {
this.studentId = this.getValue(this.core + (this.is2004 ? 'learner_id' : 'student_id'));
this.studentName = this.getValue(this.core + (this.is2004 ? 'learner_name' : 'student_name'));
this.setValue(this.core + 'score.min', 0);
this.setValue(this.core + 'score.max', 100);
$(window).on('unload', () => {
this.commitInfo();
this.terminate();
this.API = null;
});
}
log('debug', 'SCORM initialized');
} catch (ex) {
log('error', `Error initializing SCORM API: ${ex.message}`);
}
return result;
}
/**
* Terminates communication with the SCORM API
* @returns {boolean}
*/
terminate() {
let result = false;
try {
result = this.API[this.is2004 ? 'Terminate' : 'LMSFinish']('');
} catch (ex) {
log('error', `Error terminating SCORM API: ${ex.message}`);
}
return result;
}
/**
* Commits the current information to the SCORM API
*/
commitInfo() {
const
info = this.reporter.getInfo(),
score = Math.round(info.globalScore * 100),
time = this.getTimeExpression(info.tTime);
this.setValue(this.core + 'score.raw', score);
this.setValue(this.core + 'session_time', time);
this.commit();
log('debug', `SCORM results reported: ${score} (${time})`);
}
/**
* Commits current pending data to the SCORM API
* @returns {boolean}
*/
commit() {
let result = false;
try {
result = this.API[this.prefix + 'Commit']('');
} catch (ex) {
log('error', `Error commiting data to the SCORM API: ${ex.message}`);
}
return result;
}
/**
* Sends a specific value to the SCORM API
* @param {string} key - A SCORM valid key
* @param {string|number} value - The value associated with this key
* @returns {string}
*/
setValue(key, value) {
let result = false;
try {
result = this.API[this.prefix + 'SetValue'](key, value);
} catch (ex) {
log('error', `Error setting value "${value}" to "${key}" in SCORM API: ${ex.message}`);
}
return result;
}
/**
* Gets a specific value from the SCORM API
* @param {string} key - A SCORM valid key
* @returns {string} - The value associated with the provided key, or `null` if not found
*/
getValue(key) {
let result = false;
try {
result = this.API[this.prefix + 'GetValue'](key);
} catch (ex) {
log('error', `Error retrieving "${key}" from SCORM API: ${ex.message}`);
}
return result;
}
/**
* Gets a string expression of the given time (in milliseconds) suitable for a SCORM transaction.
* @see {@link http://www.ostyn.com/standards/scorm/samples/ISOTimeForSCORM.htm}
* @param {number} millis - The amount of time, in milliseconds
* @returns {string} - An ISO8601 valid expression
*/
getTimeExpression(millis) {
const
d = new Date(millis),
h = d.getUTCHours(),
m = d.getUTCMinutes(),
s = d.getUTCSeconds();
return this.is2004 ?
`PT${h}H${m}M${s}S` :
`${('0000' + h).slice(-4)}:${('00' + m).slice(-2)}:${('00' + s).slice(-2)}`;
}
/**
* Gets the SCORM type of this SCORM object
* @returns {string}
*/
getScormType() {
return `SCORM ${this.is2004 ? '2004' : '1.2'}`;
}
}
Object.assign(SCORM.prototype, {
/**
* True when the API is of type SCORM 2004, false for SCORM 1.2
* @name module:report/SCORM.SCORM#is2004
* @type {boolean} */
is2004: false,
/**
* The Reporter associated to this SCORM object
* @name module:report/SCORM.SCORM#reporter
* @type {module:report/Reporter.Reporter} */
reporter: null,
/**
* Prefix to be used in SCORM function names. Should be 'LMS' for SCORM 1.2
* @name module:report/SCORM.SCORM#prefix
* @type {string} */
prefix: '',
/**
* Prefix used in core SCORM keys. Should be 'cmi.core.' for 1.2 and 'cmi.' for 2004
* @name module:report/SCORM.SCORM#core
* @type {string} */
core: 'cmi.',
/**
* SCORM API object used to communicate with the LMS
* @name module:report/SCORM.SCORM#API
* @type {object} */
API: null,
/**
* The student ID retrieved from the SCORM API
* @name module:report/SCORM.SCORM#studentId
* @type {string} */
studentId: '',
/**
* The student name retrieved from the SCORM API
* @name module:report/SCORM.SCORM#studentName
* @type {string} */
studentName: '',
});
/**
* Maximum recursive attempts allowed to find the global SCORM API object
* @type {number} */
SCORM.DISCOVER_MAX_TRIES = 50;
export default SCORM;