437 lines
13 KiB
JavaScript
437 lines
13 KiB
JavaScript
// @flow
|
|
import BaseAPI from './BaseAPI';
|
|
import {
|
|
ADL,
|
|
CMI,
|
|
CMICommentsObject,
|
|
CMIInteractionsCorrectResponsesObject,
|
|
CMIInteractionsObject,
|
|
CMIInteractionsObjectivesObject,
|
|
CMIObjectivesObject,
|
|
} from './cmi/scorm2004_cmi';
|
|
import * as Util from './utilities';
|
|
import {scorm2004_constants} from './constants/api_constants';
|
|
import {scorm2004_error_codes} from './constants/error_codes';
|
|
import {correct_responses} from './constants/response_constants';
|
|
import {valid_languages} from './constants/language_constants';
|
|
import {scorm2004_regex} from './constants/regex';
|
|
|
|
const constants = scorm2004_constants;
|
|
|
|
/**
|
|
* API class for SCORM 2004
|
|
*/
|
|
export default class Scorm2004API extends BaseAPI {
|
|
#version: '1.0';
|
|
|
|
/**
|
|
* Constructor for SCORM 2004 API
|
|
*/
|
|
constructor() {
|
|
super(scorm2004_error_codes);
|
|
|
|
this.cmi = new CMI(this);
|
|
this.adl = new ADL(this);
|
|
|
|
// Rename functions to match 2004 Spec and expose to modules
|
|
this.Initialize = this.lmsInitialize;
|
|
this.Terminate = this.lmsTerminate;
|
|
this.GetValue = this.lmsGetValue;
|
|
this.SetValue = this.lmsSetValue;
|
|
this.Commit = this.lmsCommit;
|
|
this.GetLastError = this.lmsGetLastError;
|
|
this.GetErrorString = this.lmsGetErrorString;
|
|
this.GetDiagnostic = this.lmsGetDiagnostic;
|
|
}
|
|
|
|
/**
|
|
* Getter for #version
|
|
* @return {string}
|
|
*/
|
|
get version() {
|
|
return this.#version;
|
|
}
|
|
|
|
/**
|
|
* @return {string} bool
|
|
*/
|
|
lmsInitialize() {
|
|
this.cmi.initialize();
|
|
return this.initialize('Initialize');
|
|
}
|
|
|
|
/**
|
|
* @return {string} bool
|
|
*/
|
|
lmsTerminate() {
|
|
return this.terminate('Terminate', true);
|
|
}
|
|
|
|
/**
|
|
* @param {string} CMIElement
|
|
* @return {string}
|
|
*/
|
|
lmsGetValue(CMIElement) {
|
|
return this.getValue('GetValue', true, CMIElement);
|
|
}
|
|
|
|
/**
|
|
* @param {string} CMIElement
|
|
* @param {any} value
|
|
* @return {string}
|
|
*/
|
|
lmsSetValue(CMIElement, value) {
|
|
return this.setValue('SetValue', true, CMIElement, value);
|
|
}
|
|
|
|
/**
|
|
* Orders LMS to store all content parameters
|
|
*
|
|
* @return {string} bool
|
|
*/
|
|
lmsCommit() {
|
|
return this.commit('Commit');
|
|
}
|
|
|
|
/**
|
|
* Returns last error code
|
|
*
|
|
* @return {string}
|
|
*/
|
|
lmsGetLastError() {
|
|
return this.getLastError('GetLastError');
|
|
}
|
|
|
|
/**
|
|
* Returns the errorNumber error description
|
|
*
|
|
* @param {(string|number)} CMIErrorCode
|
|
* @return {string}
|
|
*/
|
|
lmsGetErrorString(CMIErrorCode) {
|
|
return this.getErrorString('GetErrorString', CMIErrorCode);
|
|
}
|
|
|
|
/**
|
|
* Returns a comprehensive description of the errorNumber error.
|
|
*
|
|
* @param {(string|number)} CMIErrorCode
|
|
* @return {string}
|
|
*/
|
|
lmsGetDiagnostic(CMIErrorCode) {
|
|
return this.getDiagnostic('GetDiagnostic', CMIErrorCode);
|
|
}
|
|
|
|
/**
|
|
* Sets a value on the CMI Object
|
|
*
|
|
* @param {string} CMIElement
|
|
* @param {any} value
|
|
* @return {string}
|
|
*/
|
|
setCMIValue(CMIElement, value) {
|
|
return this._commonSetCMIValue('SetValue', true, CMIElement, value);
|
|
}
|
|
|
|
/**
|
|
* Gets or builds a new child element to add to the array.
|
|
*
|
|
* @param {string} CMIElement
|
|
* @param {any} value
|
|
* @param {boolean} foundFirstIndex
|
|
* @return {any}
|
|
*/
|
|
getChildElement(CMIElement, value, foundFirstIndex) {
|
|
let newChild;
|
|
|
|
if (this.stringMatches(CMIElement, 'cmi\\.objectives\\.\\d')) {
|
|
newChild = new CMIObjectivesObject();
|
|
} else if (foundFirstIndex && this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d\\.correct_responses\\.\\d')) {
|
|
const parts = CMIElement.split('.');
|
|
const index = Number(parts[2]);
|
|
const interaction = this.cmi.interactions.childArray[index];
|
|
if (typeof interaction.type === 'undefined') {
|
|
this.throwSCORMError(scorm2004_error_codes.DEPENDENCY_NOT_ESTABLISHED);
|
|
} else {
|
|
const interaction_type = interaction.type;
|
|
const interaction_count = interaction.correct_responses._count;
|
|
if (interaction_type === 'choice') {
|
|
for (let i = 0; i < interaction_count && this.lastErrorCode ===
|
|
0; i++) {
|
|
const response = interaction.correct_responses.childArray[i];
|
|
if (response.pattern === value) {
|
|
this.throwSCORMError(scorm2004_error_codes.GENERAL_SET_FAILURE);
|
|
}
|
|
}
|
|
}
|
|
|
|
const response_type = correct_responses[interaction_type];
|
|
let nodes = [];
|
|
if (response_type.delimiter !== '') {
|
|
nodes = String(value).split(response_type.delimiter);
|
|
} else {
|
|
nodes[0] = value;
|
|
}
|
|
|
|
if (nodes.length > 0 && nodes.length <= response_type.max) {
|
|
this.checkCorrectResponseValue(interaction_type, nodes, value);
|
|
} else if (nodes.length > response_type.max) {
|
|
this.throwSCORMError(scorm2004_error_codes.GENERAL_SET_FAILURE,
|
|
'Data Model Element Pattern Too Long');
|
|
}
|
|
}
|
|
if (this.lastErrorCode === 0) {
|
|
newChild = new CMIInteractionsCorrectResponsesObject();
|
|
}
|
|
} else if (foundFirstIndex && this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d\\.objectives\\.\\d')) {
|
|
newChild = new CMIInteractionsObjectivesObject();
|
|
} else if (this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d')) {
|
|
newChild = new CMIInteractionsObject();
|
|
} else if (this.stringMatches(CMIElement, 'cmi\\.comments_from_learner\\.\\d')) {
|
|
newChild = new CMICommentsObject();
|
|
} else if (this.stringMatches(CMIElement, 'cmi\\.comments_from_lms\\.\\d')) {
|
|
newChild = new CMICommentsObject(true);
|
|
}
|
|
|
|
return newChild;
|
|
}
|
|
|
|
/**
|
|
* Validate correct response.
|
|
* @param {string} CMIElement
|
|
* @param {*} value
|
|
*/
|
|
validateCorrectResponse(CMIElement, value) {
|
|
const parts = CMIElement.split('.');
|
|
const index = Number(parts[2]);
|
|
const pattern_index = Number(parts[4]);
|
|
const interaction = this.cmi.interactions.childArray[index];
|
|
|
|
const interaction_type = interaction.type;
|
|
const interaction_count = interaction.correct_responses._count;
|
|
if (interaction_type === 'choice') {
|
|
for (let i = 0; i < interaction_count && this.lastErrorCode === 0; i++) {
|
|
const response = interaction.correct_responses.childArray[i];
|
|
if (response.pattern === value) {
|
|
this.throwSCORMError(scorm2004_error_codes.GENERAL_SET_FAILURE);
|
|
}
|
|
}
|
|
}
|
|
|
|
const response_type = scorm2004_constants.correct_responses[interaction_type];
|
|
if (typeof response_type.limit !== 'undefined' || interaction_count <
|
|
response_type.limit) {
|
|
let nodes = [];
|
|
if (response_type.delimiter !== '') {
|
|
nodes = String(value).split(response_type.delimiter);
|
|
} else {
|
|
nodes[0] = value;
|
|
}
|
|
|
|
if (nodes.length > 0 && nodes.length <= response_type.max) {
|
|
this.checkCorrectResponseValue(interaction_type, nodes, value);
|
|
} else if (nodes.length > response_type.max) {
|
|
this.throwSCORMError(scorm2004_error_codes.GENERAL_SET_FAILURE,
|
|
'Data Model Element Pattern Too Long');
|
|
}
|
|
|
|
if (this.lastErrorCode === 0 &&
|
|
(!response_type.duplicate ||
|
|
!this.checkDuplicatedPattern(interaction.correct_responses,
|
|
pattern_index, value)) ||
|
|
(this.lastErrorCode === 0 && value === '')) {
|
|
// do nothing, we want the inverse
|
|
} else {
|
|
if (this.lastErrorCode === 0) {
|
|
this.throwSCORMError(scorm2004_error_codes.GENERAL_SET_FAILURE,
|
|
'Data Model Element Pattern Already Exists');
|
|
}
|
|
}
|
|
} else {
|
|
this.throwSCORMError(scorm2004_error_codes.GENERAL_SET_FAILURE,
|
|
'Data Model Element Collection Limit Reached');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a value from the CMI Object
|
|
*
|
|
* @param {string} CMIElement
|
|
* @return {*}
|
|
*/
|
|
getCMIValue(CMIElement) {
|
|
return this._commonGetCMIValue('GetValue', true, CMIElement);
|
|
}
|
|
|
|
/**
|
|
* Returns the message that corresponds to errorNumber.
|
|
*
|
|
* @param {(string|number)} errorNumber
|
|
* @param {boolean} detail
|
|
* @return {string}
|
|
*/
|
|
getLmsErrorMessageDetails(errorNumber, detail) {
|
|
let basicMessage = '';
|
|
let detailMessage = '';
|
|
|
|
// Set error number to string since inconsistent from modules if string or number
|
|
errorNumber = String(errorNumber);
|
|
if (constants.error_descriptions[errorNumber]) {
|
|
basicMessage = constants.error_descriptions[errorNumber].basicMessage;
|
|
detailMessage = constants.error_descriptions[errorNumber].detailMessage;
|
|
}
|
|
|
|
return detail ? detailMessage : basicMessage;
|
|
}
|
|
|
|
/**
|
|
* Check to see if a correct_response value has been duplicated
|
|
* @param {CMIArray} correct_response
|
|
* @param {number} current_index
|
|
* @param {*} value
|
|
* @return {boolean}
|
|
*/
|
|
checkDuplicatedPattern = (correct_response, current_index, value) => {
|
|
let found = false;
|
|
const count = correct_response._count;
|
|
for (let i = 0; i < count && !found; i++) {
|
|
if (i !== current_index && correct_response.childArray[i] === value) {
|
|
found = true;
|
|
}
|
|
}
|
|
return found;
|
|
};
|
|
|
|
/**
|
|
* Checks for a valid correct_response value
|
|
* @param {string} interaction_type
|
|
* @param {Array} nodes
|
|
* @param {*} value
|
|
*/
|
|
checkCorrectResponseValue(interaction_type, nodes, value) {
|
|
const response = correct_responses[interaction_type];
|
|
const formatRegex = new RegExp(response.format);
|
|
for (let i = 0; i < nodes.length && this.lastErrorCode === 0; i++) {
|
|
if (interaction_type.match(
|
|
'^(fill-in|long-fill-in|matching|performance|sequencing)$')) {
|
|
nodes[i] = this.removeCorrectResponsePrefixes(nodes[i]);
|
|
}
|
|
|
|
if (response.delimiter2 !== undefined) {
|
|
const values = nodes[i].split(response.delimiter2);
|
|
if (values.length === 2) {
|
|
const matches = values[0].match(formatRegex);
|
|
if (!matches) {
|
|
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
|
|
} else {
|
|
if (!values[1].match(new RegExp(response.format2))) {
|
|
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
|
|
}
|
|
}
|
|
} else {
|
|
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
|
|
}
|
|
} else {
|
|
const matches = nodes[i].match(formatRegex);
|
|
if ((!matches && value !== '') ||
|
|
(!matches && interaction_type === 'true-false')) {
|
|
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
|
|
} else {
|
|
if (interaction_type === 'numeric' && nodes.length > 1) {
|
|
if (Number(nodes[0]) > Number(nodes[1])) {
|
|
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
|
|
}
|
|
} else {
|
|
if (nodes[i] !== '' && response.unique) {
|
|
for (let j = 0; j < i && this.lastErrorCode === 0; j++) {
|
|
if (nodes[i] === nodes[j]) {
|
|
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove prefixes from correct_response
|
|
* @param {string} node
|
|
* @return {*}
|
|
*/
|
|
removeCorrectResponsePrefixes(node) {
|
|
let seenOrder = false;
|
|
let seenCase = false;
|
|
let seenLang = false;
|
|
|
|
const prefixRegex = new RegExp(
|
|
'^({(lang|case_matters|order_matters)=([^}]+)})');
|
|
let matches = node.match(prefixRegex);
|
|
let langMatches = null;
|
|
while (matches) {
|
|
switch (matches[2]) {
|
|
case 'lang':
|
|
langMatches = node.match(scorm2004_regex.CMILangcr);
|
|
if (langMatches) {
|
|
const lang = langMatches[3];
|
|
if (lang !== undefined && lang.length > 0) {
|
|
if (valid_languages[lang.toLowerCase()] === undefined) {
|
|
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
|
|
}
|
|
}
|
|
}
|
|
seenLang = true;
|
|
break;
|
|
case 'case_matters':
|
|
if (!seenLang && !seenOrder && !seenCase) {
|
|
if (matches[3] !== 'true' && matches[3] !== 'false') {
|
|
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
|
|
}
|
|
}
|
|
|
|
seenCase = true;
|
|
break;
|
|
case 'order_matters':
|
|
if (!seenCase && !seenLang && !seenOrder) {
|
|
if (matches[3] !== 'true' && matches[3] !== 'false') {
|
|
this.throwSCORMError(scorm2004_error_codes.TYPE_MISMATCH);
|
|
}
|
|
}
|
|
|
|
seenOrder = true;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
node = node.substr(matches[1].length);
|
|
matches = node.match(prefixRegex);
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Replace the whole API with another
|
|
* @param {Scorm2004API} newAPI
|
|
*/
|
|
replaceWithAnotherScormAPI(newAPI) {
|
|
// Data Model
|
|
this.cmi = newAPI.cmi;
|
|
this.adl = newAPI.adl;
|
|
}
|
|
|
|
/**
|
|
* Adds the current session time to the existing total time.
|
|
*
|
|
* @return {string} ISO8601 Duration
|
|
*/
|
|
getCurrentTotalTime() {
|
|
const totalTime = this.cmi.total_time;
|
|
const sessionTime = this.cmi.session_time;
|
|
|
|
return Util.addTwoDurations(totalTime, sessionTime,
|
|
scorm2004_regex.CMITimespan);
|
|
}
|
|
}
|