scorm-again 1.7.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc +18 -7
- package/.github/dependabot.yml +5 -0
- package/.github/workflows/main.yml +79 -0
- package/.github/workflows/stale.yml +14 -0
- package/.jsdoc.json +4 -5
- package/.mocharc.json +8 -0
- package/.run/{Mocha Unit Tests.run.xml → Mocha Unit Tests (watch).run.xml } +6 -3
- package/.run/Template Mocha.run.xml +17 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +183 -71
- package/dist/aicc.js +3822 -7030
- package/dist/aicc.js.map +1 -1
- package/dist/aicc.min.js +2 -40
- package/dist/aicc.min.js.map +1 -0
- package/dist/scorm-again.js +5965 -10498
- package/dist/scorm-again.js.map +1 -1
- package/dist/scorm-again.min.js +2 -52
- package/dist/scorm-again.min.js.map +1 -0
- package/dist/scorm12.js +3028 -5373
- package/dist/scorm12.js.map +1 -1
- package/dist/scorm12.min.js +2 -34
- package/dist/scorm12.min.js.map +1 -0
- package/dist/scorm2004.js +4054 -6693
- package/dist/scorm2004.js.map +1 -1
- package/dist/scorm2004.min.js +2 -40
- package/dist/scorm2004.min.js.map +1 -0
- package/eslint.config.js +21 -0
- package/package.json +76 -34
- package/results.json +34254 -0
- package/src/AICC.ts +72 -0
- package/src/BaseAPI.ts +1300 -0
- package/src/Scorm12API.ts +387 -0
- package/src/Scorm2004API.ts +688 -0
- package/src/cmi/aicc/attempts.ts +94 -0
- package/src/cmi/aicc/cmi.ts +100 -0
- package/src/cmi/aicc/core.ts +360 -0
- package/src/cmi/aicc/evaluation.ts +157 -0
- package/src/cmi/aicc/paths.ts +180 -0
- package/src/cmi/aicc/student_data.ts +86 -0
- package/src/cmi/aicc/student_demographics.ts +367 -0
- package/src/cmi/aicc/student_preferences.ts +176 -0
- package/src/cmi/aicc/tries.ts +116 -0
- package/src/cmi/aicc/validation.ts +25 -0
- package/src/cmi/common/array.ts +77 -0
- package/src/cmi/common/base_cmi.ts +46 -0
- package/src/cmi/common/score.ts +203 -0
- package/src/cmi/common/validation.ts +60 -0
- package/src/cmi/scorm12/cmi.ts +224 -0
- package/src/cmi/scorm12/interactions.ts +368 -0
- package/src/cmi/scorm12/nav.ts +54 -0
- package/src/cmi/scorm12/objectives.ts +112 -0
- package/src/cmi/scorm12/student_data.ts +130 -0
- package/src/cmi/scorm12/student_preference.ts +158 -0
- package/src/cmi/scorm12/validation.ts +48 -0
- package/src/cmi/scorm2004/adl.ts +272 -0
- package/src/cmi/scorm2004/cmi.ts +599 -0
- package/src/cmi/scorm2004/comments.ts +163 -0
- package/src/cmi/scorm2004/interactions.ts +466 -0
- package/src/cmi/scorm2004/learner_preference.ts +152 -0
- package/src/cmi/scorm2004/objectives.ts +212 -0
- package/src/cmi/scorm2004/score.ts +78 -0
- package/src/cmi/scorm2004/validation.ts +42 -0
- package/src/constants/api_constants.ts +318 -0
- package/src/constants/default_settings.ts +81 -0
- package/src/constants/enums.ts +5 -0
- package/src/constants/error_codes.ts +88 -0
- package/src/constants/language_constants.ts +394 -0
- package/src/constants/regex.ts +97 -0
- package/src/constants/{response_constants.js → response_constants.ts} +69 -62
- package/src/exceptions.ts +154 -0
- package/src/exports/aicc.js +1 -1
- package/src/exports/scorm-again.js +3 -3
- package/src/exports/scorm12.js +1 -1
- package/src/exports/scorm2004.js +1 -1
- package/src/helpers/scheduled_commit.ts +42 -0
- package/src/interfaces/IBaseAPI.ts +35 -0
- package/src/types/api_types.ts +32 -0
- package/src/utilities/debounce.ts +31 -0
- package/src/utilities.ts +338 -0
- package/tea.yaml +6 -0
- package/test/{AICC.spec.js → AICC.spec.ts} +79 -71
- package/test/Scorm12API.spec.ts +833 -0
- package/test/Scorm2004API.spec.ts +1298 -0
- package/test/api_helpers.ts +176 -0
- package/test/cmi/aicc_cmi.spec.ts +845 -0
- package/test/cmi/{scorm12_cmi.spec.js → scorm12_cmi.spec.ts} +253 -271
- package/test/cmi/scorm2004_cmi.spec.ts +1031 -0
- package/test/cmi_helpers.ts +207 -0
- package/test/exceptions.spec.ts +79 -0
- package/test/field_values.ts +202 -0
- package/test/types/api_types.spec.ts +126 -0
- package/test/utilities/debounce.spec.ts +56 -0
- package/test/utilities.spec.ts +322 -0
- package/tsconfig.json +18 -0
- package/webpack.config.js +65 -0
- package/.circleci/config.yml +0 -99
- package/.codeclimate.yml +0 -7
- package/.eslintrc.js +0 -36
- package/src/.flowconfig +0 -11
- package/src/AICC.js +0 -68
- package/src/BaseAPI.js +0 -1275
- package/src/Scorm12API.js +0 -308
- package/src/Scorm2004API.js +0 -572
- package/src/cmi/aicc_cmi.js +0 -1141
- package/src/cmi/common.js +0 -328
- package/src/cmi/scorm12_cmi.js +0 -1312
- package/src/cmi/scorm2004_cmi.js +0 -1692
- package/src/constants/api_constants.js +0 -218
- package/src/constants/error_codes.js +0 -87
- package/src/constants/language_constants.js +0 -76
- package/src/constants/regex.js +0 -84
- package/src/exceptions.js +0 -104
- package/src/utilities.js +0 -242
- package/test/Scorm12API.spec.js +0 -528
- package/test/Scorm2004API.spec.js +0 -775
- package/test/abstract_classes.spec.js +0 -17
- package/test/api_helpers.js +0 -128
- package/test/cmi/aicc_cmi.spec.js +0 -684
- package/test/cmi/scorm2004_cmi.spec.js +0 -1066
- package/test/cmi_helpers.js +0 -161
- package/test/exceptions.spec.js +0 -71
- package/test/field_values.js +0 -353
- package/test/utilities.spec.js +0 -339
- package/webpack.js +0 -78
package/src/BaseAPI.ts
ADDED
|
@@ -0,0 +1,1300 @@
|
|
|
1
|
+
import { CMIArray } from "./cmi/common/array";
|
|
2
|
+
import { ValidationError } from "./exceptions";
|
|
3
|
+
import ErrorCodes, { ErrorCode } from "./constants/error_codes";
|
|
4
|
+
import APIConstants from "./constants/api_constants";
|
|
5
|
+
import { formatMessage, stringMatches, unflatten } from "./utilities";
|
|
6
|
+
import { BaseCMI } from "./cmi/common/base_cmi";
|
|
7
|
+
import { debounce } from "./utilities/debounce";
|
|
8
|
+
import { RefObject, ResultObject, Settings } from "./types/api_types";
|
|
9
|
+
import { DefaultSettings } from "./constants/default_settings";
|
|
10
|
+
import { IBaseAPI } from "./interfaces/IBaseAPI";
|
|
11
|
+
import { ScheduledCommit } from "./helpers/scheduled_commit";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Base API class for AICC, SCORM 1.2, and SCORM 2004. Should be considered
|
|
15
|
+
* abstract, and never initialized on its own.
|
|
16
|
+
*/
|
|
17
|
+
export default abstract class BaseAPI implements IBaseAPI {
|
|
18
|
+
private _timeout?: ScheduledCommit;
|
|
19
|
+
private readonly _error_codes: ErrorCode;
|
|
20
|
+
private _settings: Settings = DefaultSettings;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Constructor for Base API class. Sets some shared API fields, as well as
|
|
24
|
+
* sets up options for the API.
|
|
25
|
+
* @param {ErrorCode} error_codes
|
|
26
|
+
* @param {Settings} settings
|
|
27
|
+
*/
|
|
28
|
+
protected constructor(error_codes: ErrorCode, settings?: Settings) {
|
|
29
|
+
if (new.target === BaseAPI) {
|
|
30
|
+
throw new TypeError("Cannot construct BaseAPI instances directly");
|
|
31
|
+
}
|
|
32
|
+
this.currentState = APIConstants.global.STATE_NOT_INITIALIZED;
|
|
33
|
+
this.lastErrorCode = "0";
|
|
34
|
+
this.listenerArray = [];
|
|
35
|
+
|
|
36
|
+
this._error_codes = error_codes;
|
|
37
|
+
|
|
38
|
+
if (settings) {
|
|
39
|
+
this.settings = settings;
|
|
40
|
+
}
|
|
41
|
+
this.apiLogLevel = this.settings.logLevel;
|
|
42
|
+
this.selfReportSessionTime = this.settings.selfReportSessionTime;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public abstract cmi: BaseCMI;
|
|
46
|
+
public startingData?: RefObject;
|
|
47
|
+
|
|
48
|
+
public currentState: number;
|
|
49
|
+
public lastErrorCode: string;
|
|
50
|
+
public listenerArray: any[];
|
|
51
|
+
public apiLogLevel: number;
|
|
52
|
+
public selfReportSessionTime: boolean;
|
|
53
|
+
|
|
54
|
+
abstract reset(settings?: Settings): void;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Common reset method for all APIs. New settings are merged with the existing settings.
|
|
58
|
+
* @param {Settings} settings
|
|
59
|
+
* @protected
|
|
60
|
+
*/
|
|
61
|
+
commonReset(settings?: Settings): void {
|
|
62
|
+
this.settings = { ...this.settings, ...settings };
|
|
63
|
+
|
|
64
|
+
this.currentState = APIConstants.global.STATE_NOT_INITIALIZED;
|
|
65
|
+
this.lastErrorCode = "0";
|
|
66
|
+
this.listenerArray = [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Initialize the API
|
|
71
|
+
* @param {string} callbackName
|
|
72
|
+
* @param {string} initializeMessage
|
|
73
|
+
* @param {string} terminationMessage
|
|
74
|
+
* @return {string}
|
|
75
|
+
*/
|
|
76
|
+
initialize(
|
|
77
|
+
callbackName: string,
|
|
78
|
+
initializeMessage?: string,
|
|
79
|
+
terminationMessage?: string,
|
|
80
|
+
): string {
|
|
81
|
+
let returnValue = APIConstants.global.SCORM_FALSE;
|
|
82
|
+
|
|
83
|
+
if (this.isInitialized()) {
|
|
84
|
+
this.throwSCORMError(this._error_codes.INITIALIZED, initializeMessage);
|
|
85
|
+
} else if (this.isTerminated()) {
|
|
86
|
+
this.throwSCORMError(this._error_codes.TERMINATED, terminationMessage);
|
|
87
|
+
} else {
|
|
88
|
+
if (this.selfReportSessionTime) {
|
|
89
|
+
this.cmi.setStartTime();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.currentState = APIConstants.global.STATE_INITIALIZED;
|
|
93
|
+
this.lastErrorCode = "0";
|
|
94
|
+
returnValue = APIConstants.global.SCORM_TRUE;
|
|
95
|
+
this.processListeners(callbackName);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.apiLog(
|
|
99
|
+
callbackName,
|
|
100
|
+
"returned: " + returnValue,
|
|
101
|
+
APIConstants.global.LOG_LEVEL_INFO,
|
|
102
|
+
);
|
|
103
|
+
this.clearSCORMError(returnValue);
|
|
104
|
+
|
|
105
|
+
return returnValue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
abstract lmsInitialize(): string;
|
|
109
|
+
|
|
110
|
+
abstract lmsFinish(): string;
|
|
111
|
+
|
|
112
|
+
abstract lmsGetValue(CMIElement: string): string;
|
|
113
|
+
|
|
114
|
+
abstract lmsSetValue(CMIElement: string, value: any): string;
|
|
115
|
+
|
|
116
|
+
abstract lmsCommit(): string;
|
|
117
|
+
|
|
118
|
+
abstract lmsGetLastError(): string;
|
|
119
|
+
|
|
120
|
+
abstract lmsGetErrorString(CMIErrorCode: string | number): string;
|
|
121
|
+
|
|
122
|
+
abstract lmsGetDiagnostic(CMIErrorCode: string | number): string;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Abstract method for validating that a response is correct.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} _CMIElement
|
|
128
|
+
* @param {any} _value
|
|
129
|
+
*/
|
|
130
|
+
abstract validateCorrectResponse(_CMIElement: string, _value: any): void;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Gets or builds a new child element to add to the array.
|
|
134
|
+
* APIs that inherit BaseAPI should override this method.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} _CMIElement - unused
|
|
137
|
+
* @param {*} _value - unused
|
|
138
|
+
* @param {boolean} _foundFirstIndex - unused
|
|
139
|
+
* @return {BaseCMI|null}
|
|
140
|
+
* @abstract
|
|
141
|
+
*/
|
|
142
|
+
abstract getChildElement(
|
|
143
|
+
_CMIElement: string,
|
|
144
|
+
_value: any,
|
|
145
|
+
_foundFirstIndex: boolean,
|
|
146
|
+
): BaseCMI | null;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Attempts to store the data to the LMS, logs data if no LMS configured
|
|
150
|
+
* APIs that inherit BaseAPI should override this function
|
|
151
|
+
*
|
|
152
|
+
* @param {boolean} _calculateTotalTime
|
|
153
|
+
* @return {ResultObject}
|
|
154
|
+
* @abstract
|
|
155
|
+
*/
|
|
156
|
+
abstract storeData(_calculateTotalTime: boolean): Promise<ResultObject>;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Render the cmi object to the proper format for LMS commit
|
|
160
|
+
* APIs that inherit BaseAPI should override this function
|
|
161
|
+
*
|
|
162
|
+
* @param {boolean} _terminateCommit
|
|
163
|
+
* @return {RefObject|Array}
|
|
164
|
+
* @abstract
|
|
165
|
+
*/
|
|
166
|
+
abstract renderCommitCMI(_terminateCommit: boolean): RefObject | Array<any>;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Logging for all SCORM actions
|
|
170
|
+
*
|
|
171
|
+
* @param {string} functionName
|
|
172
|
+
* @param {string} logMessage
|
|
173
|
+
* @param {number} messageLevel
|
|
174
|
+
* @param {string} CMIElement
|
|
175
|
+
*/
|
|
176
|
+
apiLog(
|
|
177
|
+
functionName: string,
|
|
178
|
+
logMessage: string,
|
|
179
|
+
messageLevel: number,
|
|
180
|
+
CMIElement?: string,
|
|
181
|
+
) {
|
|
182
|
+
logMessage = formatMessage(functionName, logMessage, CMIElement);
|
|
183
|
+
|
|
184
|
+
if (messageLevel >= this.apiLogLevel) {
|
|
185
|
+
this.settings.onLogMessage(messageLevel, logMessage);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Getter for _error_codes
|
|
191
|
+
* @return {ErrorCode}
|
|
192
|
+
*/
|
|
193
|
+
get error_codes(): ErrorCode {
|
|
194
|
+
return this._error_codes;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Getter for _settings
|
|
199
|
+
* @return {Settings}
|
|
200
|
+
*/
|
|
201
|
+
get settings(): Settings {
|
|
202
|
+
return this._settings;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Setter for _settings
|
|
207
|
+
* @param {Settings} settings
|
|
208
|
+
*/
|
|
209
|
+
set settings(settings: Settings) {
|
|
210
|
+
this._settings = { ...this._settings, ...settings };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Terminates the current run of the API
|
|
215
|
+
* @param {string} callbackName
|
|
216
|
+
* @param {boolean} checkTerminated
|
|
217
|
+
* @return {string}
|
|
218
|
+
*/
|
|
219
|
+
async terminate(
|
|
220
|
+
callbackName: string,
|
|
221
|
+
checkTerminated: boolean,
|
|
222
|
+
): Promise<string> {
|
|
223
|
+
let returnValue = APIConstants.global.SCORM_FALSE;
|
|
224
|
+
|
|
225
|
+
if (
|
|
226
|
+
this.checkState(
|
|
227
|
+
checkTerminated,
|
|
228
|
+
this._error_codes.TERMINATION_BEFORE_INIT,
|
|
229
|
+
this._error_codes.MULTIPLE_TERMINATION,
|
|
230
|
+
)
|
|
231
|
+
) {
|
|
232
|
+
this.currentState = APIConstants.global.STATE_TERMINATED;
|
|
233
|
+
|
|
234
|
+
const result: ResultObject = await this.storeData(true);
|
|
235
|
+
if (typeof result.errorCode !== "undefined" && result.errorCode > 0) {
|
|
236
|
+
this.throwSCORMError(result.errorCode);
|
|
237
|
+
}
|
|
238
|
+
returnValue =
|
|
239
|
+
typeof result !== "undefined" && result.result
|
|
240
|
+
? result.result
|
|
241
|
+
: APIConstants.global.SCORM_FALSE;
|
|
242
|
+
|
|
243
|
+
if (checkTerminated) this.lastErrorCode = "0";
|
|
244
|
+
|
|
245
|
+
returnValue = APIConstants.global.SCORM_TRUE;
|
|
246
|
+
this.processListeners(callbackName);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.apiLog(
|
|
250
|
+
callbackName,
|
|
251
|
+
"returned: " + returnValue,
|
|
252
|
+
APIConstants.global.LOG_LEVEL_INFO,
|
|
253
|
+
);
|
|
254
|
+
this.clearSCORMError(returnValue);
|
|
255
|
+
|
|
256
|
+
return returnValue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get the value of the CMIElement.
|
|
261
|
+
*
|
|
262
|
+
* @param {string} callbackName
|
|
263
|
+
* @param {boolean} checkTerminated
|
|
264
|
+
* @param {string} CMIElement
|
|
265
|
+
* @return {string}
|
|
266
|
+
*/
|
|
267
|
+
getValue(
|
|
268
|
+
callbackName: string,
|
|
269
|
+
checkTerminated: boolean,
|
|
270
|
+
CMIElement: string,
|
|
271
|
+
): string {
|
|
272
|
+
let returnValue: string = "";
|
|
273
|
+
|
|
274
|
+
if (
|
|
275
|
+
this.checkState(
|
|
276
|
+
checkTerminated,
|
|
277
|
+
this._error_codes.RETRIEVE_BEFORE_INIT,
|
|
278
|
+
this._error_codes.RETRIEVE_AFTER_TERM,
|
|
279
|
+
)
|
|
280
|
+
) {
|
|
281
|
+
if (checkTerminated) this.lastErrorCode = "0";
|
|
282
|
+
try {
|
|
283
|
+
returnValue = this.getCMIValue(CMIElement);
|
|
284
|
+
} catch (e) {
|
|
285
|
+
returnValue = this.handleValueAccessException(e, returnValue);
|
|
286
|
+
}
|
|
287
|
+
this.processListeners(callbackName, CMIElement);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
this.apiLog(
|
|
291
|
+
callbackName,
|
|
292
|
+
": returned: " + returnValue,
|
|
293
|
+
APIConstants.global.LOG_LEVEL_INFO,
|
|
294
|
+
CMIElement,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
if (returnValue === undefined) {
|
|
298
|
+
return "";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
this.clearSCORMError(returnValue);
|
|
302
|
+
|
|
303
|
+
return returnValue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Sets the value of the CMIElement.
|
|
308
|
+
*
|
|
309
|
+
* @param {string} callbackName
|
|
310
|
+
* @param {string} commitCallback
|
|
311
|
+
* @param {boolean} checkTerminated
|
|
312
|
+
* @param {string} CMIElement
|
|
313
|
+
* @param {*} value
|
|
314
|
+
* @return {string}
|
|
315
|
+
*/
|
|
316
|
+
setValue(
|
|
317
|
+
callbackName: string,
|
|
318
|
+
commitCallback: string,
|
|
319
|
+
checkTerminated: boolean,
|
|
320
|
+
CMIElement: string,
|
|
321
|
+
value: any,
|
|
322
|
+
): string {
|
|
323
|
+
if (value !== undefined) {
|
|
324
|
+
value = String(value);
|
|
325
|
+
}
|
|
326
|
+
let returnValue: string = APIConstants.global.SCORM_FALSE;
|
|
327
|
+
|
|
328
|
+
if (
|
|
329
|
+
this.checkState(
|
|
330
|
+
checkTerminated,
|
|
331
|
+
this._error_codes.STORE_BEFORE_INIT,
|
|
332
|
+
this._error_codes.STORE_AFTER_TERM,
|
|
333
|
+
)
|
|
334
|
+
) {
|
|
335
|
+
if (checkTerminated) this.lastErrorCode = "0";
|
|
336
|
+
try {
|
|
337
|
+
returnValue = this.setCMIValue(CMIElement, value);
|
|
338
|
+
} catch (e) {
|
|
339
|
+
this.handleValueAccessException(e, returnValue);
|
|
340
|
+
}
|
|
341
|
+
this.processListeners(callbackName, CMIElement, value);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (returnValue === undefined) {
|
|
345
|
+
returnValue = APIConstants.global.SCORM_FALSE;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// If we didn't have any errors while setting the data, go ahead and
|
|
349
|
+
// schedule a commit, if autocommit is turned on
|
|
350
|
+
if (String(this.lastErrorCode) === "0") {
|
|
351
|
+
if (this.settings.autocommit && !this._timeout) {
|
|
352
|
+
this.scheduleCommit(
|
|
353
|
+
this.settings.autocommitSeconds * 1000,
|
|
354
|
+
commitCallback,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
this.apiLog(
|
|
360
|
+
callbackName,
|
|
361
|
+
": " + value + ": result: " + returnValue,
|
|
362
|
+
APIConstants.global.LOG_LEVEL_INFO,
|
|
363
|
+
CMIElement,
|
|
364
|
+
);
|
|
365
|
+
this.clearSCORMError(returnValue);
|
|
366
|
+
|
|
367
|
+
return returnValue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Orders LMS to store all content parameters
|
|
372
|
+
* @param {string} callbackName
|
|
373
|
+
* @param {boolean} checkTerminated
|
|
374
|
+
* @return {string}
|
|
375
|
+
*/
|
|
376
|
+
async commit(
|
|
377
|
+
callbackName: string,
|
|
378
|
+
checkTerminated: boolean = false,
|
|
379
|
+
): Promise<string> {
|
|
380
|
+
this.clearScheduledCommit();
|
|
381
|
+
|
|
382
|
+
let returnValue = APIConstants.global.SCORM_FALSE;
|
|
383
|
+
|
|
384
|
+
if (
|
|
385
|
+
this.checkState(
|
|
386
|
+
checkTerminated,
|
|
387
|
+
this._error_codes.COMMIT_BEFORE_INIT,
|
|
388
|
+
this._error_codes.COMMIT_AFTER_TERM,
|
|
389
|
+
)
|
|
390
|
+
) {
|
|
391
|
+
const result = await this.storeData(false);
|
|
392
|
+
if (result.errorCode && result.errorCode > 0) {
|
|
393
|
+
this.throwSCORMError(result.errorCode);
|
|
394
|
+
}
|
|
395
|
+
returnValue =
|
|
396
|
+
typeof result !== "undefined" && result.result
|
|
397
|
+
? result.result
|
|
398
|
+
: APIConstants.global.SCORM_FALSE;
|
|
399
|
+
|
|
400
|
+
this.apiLog(
|
|
401
|
+
callbackName,
|
|
402
|
+
" Result: " + returnValue,
|
|
403
|
+
APIConstants.global.LOG_LEVEL_DEBUG,
|
|
404
|
+
"HttpRequest",
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
if (checkTerminated) this.lastErrorCode = "0";
|
|
408
|
+
|
|
409
|
+
this.processListeners(callbackName);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
this.apiLog(
|
|
413
|
+
callbackName,
|
|
414
|
+
"returned: " + returnValue,
|
|
415
|
+
APIConstants.global.LOG_LEVEL_INFO,
|
|
416
|
+
);
|
|
417
|
+
this.clearSCORMError(returnValue);
|
|
418
|
+
|
|
419
|
+
return returnValue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Returns last error code
|
|
424
|
+
* @param {string} callbackName
|
|
425
|
+
* @return {string}
|
|
426
|
+
*/
|
|
427
|
+
getLastError(callbackName: string): string {
|
|
428
|
+
const returnValue = String(this.lastErrorCode);
|
|
429
|
+
|
|
430
|
+
this.processListeners(callbackName);
|
|
431
|
+
|
|
432
|
+
this.apiLog(
|
|
433
|
+
callbackName,
|
|
434
|
+
"returned: " + returnValue,
|
|
435
|
+
APIConstants.global.LOG_LEVEL_INFO,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
return returnValue;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Returns the errorNumber error description
|
|
443
|
+
*
|
|
444
|
+
* @param {string} callbackName
|
|
445
|
+
* @param {(string|number)} CMIErrorCode
|
|
446
|
+
* @return {string}
|
|
447
|
+
*/
|
|
448
|
+
getErrorString(callbackName: string, CMIErrorCode: string | number): string {
|
|
449
|
+
let returnValue = "";
|
|
450
|
+
|
|
451
|
+
if (CMIErrorCode !== null && CMIErrorCode !== "") {
|
|
452
|
+
returnValue = this.getLmsErrorMessageDetails(CMIErrorCode);
|
|
453
|
+
this.processListeners(callbackName);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
this.apiLog(
|
|
457
|
+
callbackName,
|
|
458
|
+
"returned: " + returnValue,
|
|
459
|
+
APIConstants.global.LOG_LEVEL_INFO,
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
return returnValue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Returns a comprehensive description of the errorNumber error.
|
|
467
|
+
*
|
|
468
|
+
* @param {string} callbackName
|
|
469
|
+
* @param {(string|number)} CMIErrorCode
|
|
470
|
+
* @return {string}
|
|
471
|
+
*/
|
|
472
|
+
getDiagnostic(callbackName: string, CMIErrorCode: string | number): string {
|
|
473
|
+
let returnValue = "";
|
|
474
|
+
|
|
475
|
+
if (CMIErrorCode !== null && CMIErrorCode !== "") {
|
|
476
|
+
returnValue = this.getLmsErrorMessageDetails(CMIErrorCode, true);
|
|
477
|
+
this.processListeners(callbackName);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
this.apiLog(
|
|
481
|
+
callbackName,
|
|
482
|
+
"returned: " + returnValue,
|
|
483
|
+
APIConstants.global.LOG_LEVEL_INFO,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
return returnValue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Checks the LMS state and ensures it has been initialized.
|
|
491
|
+
*
|
|
492
|
+
* @param {boolean} checkTerminated
|
|
493
|
+
* @param {number} beforeInitError
|
|
494
|
+
* @param {number} afterTermError
|
|
495
|
+
* @return {boolean}
|
|
496
|
+
*/
|
|
497
|
+
checkState(
|
|
498
|
+
checkTerminated: boolean,
|
|
499
|
+
beforeInitError: number,
|
|
500
|
+
afterTermError: number,
|
|
501
|
+
): boolean {
|
|
502
|
+
if (this.isNotInitialized()) {
|
|
503
|
+
this.throwSCORMError(beforeInitError);
|
|
504
|
+
return false;
|
|
505
|
+
} else if (checkTerminated && this.isTerminated()) {
|
|
506
|
+
this.throwSCORMError(afterTermError);
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Returns the message that corresponds to errorNumber
|
|
515
|
+
* APIs that inherit BaseAPI should override this function
|
|
516
|
+
*
|
|
517
|
+
* @param {(string|number)} _errorNumber
|
|
518
|
+
* @param {boolean} _detail
|
|
519
|
+
* @return {string}
|
|
520
|
+
* @abstract
|
|
521
|
+
*/
|
|
522
|
+
getLmsErrorMessageDetails(
|
|
523
|
+
_errorNumber: string | number,
|
|
524
|
+
_detail: boolean = false,
|
|
525
|
+
): string {
|
|
526
|
+
throw new Error(
|
|
527
|
+
"The getLmsErrorMessageDetails method has not been implemented",
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Gets the value for the specific element.
|
|
533
|
+
* APIs that inherit BaseAPI should override this function
|
|
534
|
+
*
|
|
535
|
+
* @param {string} _CMIElement
|
|
536
|
+
* @return {string}
|
|
537
|
+
* @abstract
|
|
538
|
+
*/
|
|
539
|
+
getCMIValue(_CMIElement: string): string {
|
|
540
|
+
throw new Error("The getCMIValue method has not been implemented");
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Sets the value for the specific element.
|
|
545
|
+
* APIs that inherit BaseAPI should override this function
|
|
546
|
+
*
|
|
547
|
+
* @param {string} _CMIElement
|
|
548
|
+
* @param {any} _value
|
|
549
|
+
* @return {string}
|
|
550
|
+
* @abstract
|
|
551
|
+
*/
|
|
552
|
+
setCMIValue(_CMIElement: string, _value: any): string {
|
|
553
|
+
throw new Error("The setCMIValue method has not been implemented");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Shared API method to set a valid for a given element.
|
|
558
|
+
*
|
|
559
|
+
* @param {string} methodName
|
|
560
|
+
* @param {boolean} scorm2004
|
|
561
|
+
* @param {string} CMIElement
|
|
562
|
+
* @param {any} value
|
|
563
|
+
* @return {string}
|
|
564
|
+
*/
|
|
565
|
+
_commonSetCMIValue(
|
|
566
|
+
methodName: string,
|
|
567
|
+
scorm2004: boolean,
|
|
568
|
+
CMIElement: string,
|
|
569
|
+
value: any,
|
|
570
|
+
): string {
|
|
571
|
+
if (!CMIElement || CMIElement === "") {
|
|
572
|
+
return APIConstants.global.SCORM_FALSE;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const structure = CMIElement.split(".");
|
|
576
|
+
let refObject: RefObject = this;
|
|
577
|
+
let returnValue = APIConstants.global.SCORM_FALSE;
|
|
578
|
+
let foundFirstIndex = false;
|
|
579
|
+
|
|
580
|
+
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
|
|
581
|
+
const invalidErrorCode = scorm2004
|
|
582
|
+
? this._error_codes.UNDEFINED_DATA_MODEL
|
|
583
|
+
: this._error_codes.GENERAL;
|
|
584
|
+
|
|
585
|
+
for (let idx = 0; idx < structure.length; idx++) {
|
|
586
|
+
const attribute = structure[idx];
|
|
587
|
+
|
|
588
|
+
if (idx === structure.length - 1) {
|
|
589
|
+
if (scorm2004 && attribute.substring(0, 8) === "{target=") {
|
|
590
|
+
if (this.isInitialized()) {
|
|
591
|
+
this.throwSCORMError(this._error_codes.READ_ONLY_ELEMENT);
|
|
592
|
+
} else {
|
|
593
|
+
refObject = {
|
|
594
|
+
...refObject,
|
|
595
|
+
attribute: value,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
} else if (!this._checkObjectHasProperty(refObject, attribute)) {
|
|
599
|
+
this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
|
|
600
|
+
} else {
|
|
601
|
+
if (
|
|
602
|
+
stringMatches(CMIElement, "\\.correct_responses\\.\\d+") &&
|
|
603
|
+
this.isInitialized()
|
|
604
|
+
) {
|
|
605
|
+
this.validateCorrectResponse(CMIElement, value);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (!scorm2004 || this.lastErrorCode === "0") {
|
|
609
|
+
refObject[attribute] = value;
|
|
610
|
+
returnValue = APIConstants.global.SCORM_TRUE;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
} else {
|
|
614
|
+
refObject = refObject[attribute];
|
|
615
|
+
if (!refObject) {
|
|
616
|
+
this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (refObject instanceof CMIArray) {
|
|
621
|
+
const index = parseInt(structure[idx + 1], 10);
|
|
622
|
+
|
|
623
|
+
// SCO is trying to set an item on an array
|
|
624
|
+
if (!isNaN(index)) {
|
|
625
|
+
const item = refObject.childArray[index];
|
|
626
|
+
|
|
627
|
+
if (item) {
|
|
628
|
+
refObject = item;
|
|
629
|
+
foundFirstIndex = true;
|
|
630
|
+
} else {
|
|
631
|
+
const newChild = this.getChildElement(
|
|
632
|
+
CMIElement,
|
|
633
|
+
value,
|
|
634
|
+
foundFirstIndex,
|
|
635
|
+
);
|
|
636
|
+
foundFirstIndex = true;
|
|
637
|
+
|
|
638
|
+
if (!newChild) {
|
|
639
|
+
this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
|
|
640
|
+
} else {
|
|
641
|
+
if (refObject.initialized) newChild.initialize();
|
|
642
|
+
|
|
643
|
+
refObject.childArray.push(newChild);
|
|
644
|
+
refObject = newChild;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Have to update idx value to skip the array position
|
|
649
|
+
idx++;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (returnValue === APIConstants.global.SCORM_FALSE) {
|
|
656
|
+
this.apiLog(
|
|
657
|
+
methodName,
|
|
658
|
+
`There was an error setting the value for: ${CMIElement}, value of: ${value}`,
|
|
659
|
+
APIConstants.global.LOG_LEVEL_WARNING,
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return returnValue;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Gets a value from the CMI Object
|
|
668
|
+
*
|
|
669
|
+
* @param {string} methodName
|
|
670
|
+
* @param {boolean} scorm2004
|
|
671
|
+
* @param {string} CMIElement
|
|
672
|
+
* @return {any}
|
|
673
|
+
*/
|
|
674
|
+
_commonGetCMIValue(
|
|
675
|
+
methodName: string,
|
|
676
|
+
scorm2004: boolean,
|
|
677
|
+
CMIElement: string,
|
|
678
|
+
): any {
|
|
679
|
+
if (!CMIElement || CMIElement === "") {
|
|
680
|
+
return "";
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const structure = CMIElement.split(".");
|
|
684
|
+
let refObject: RefObject = this;
|
|
685
|
+
let attribute = null;
|
|
686
|
+
|
|
687
|
+
const uninitializedErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) has not been initialized.`;
|
|
688
|
+
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
|
|
689
|
+
const invalidErrorCode = scorm2004
|
|
690
|
+
? this._error_codes.UNDEFINED_DATA_MODEL
|
|
691
|
+
: this._error_codes.GENERAL;
|
|
692
|
+
|
|
693
|
+
for (let idx = 0; idx < structure.length; idx++) {
|
|
694
|
+
attribute = structure[idx];
|
|
695
|
+
|
|
696
|
+
if (!scorm2004) {
|
|
697
|
+
if (idx === structure.length - 1) {
|
|
698
|
+
if (!this._checkObjectHasProperty(refObject, attribute)) {
|
|
699
|
+
this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
if (
|
|
705
|
+
String(attribute).substring(0, 8) === "{target=" &&
|
|
706
|
+
typeof refObject._isTargetValid == "function"
|
|
707
|
+
) {
|
|
708
|
+
const target = String(attribute).substring(
|
|
709
|
+
8,
|
|
710
|
+
String(attribute).length - 9,
|
|
711
|
+
);
|
|
712
|
+
return refObject._isTargetValid(target);
|
|
713
|
+
} else if (!this._checkObjectHasProperty(refObject, attribute)) {
|
|
714
|
+
this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
refObject = refObject[attribute];
|
|
720
|
+
if (refObject === undefined) {
|
|
721
|
+
this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (refObject instanceof CMIArray) {
|
|
726
|
+
const index = parseInt(structure[idx + 1], 10);
|
|
727
|
+
|
|
728
|
+
// SCO is trying to set an item on an array
|
|
729
|
+
if (!isNaN(index)) {
|
|
730
|
+
const item = refObject.childArray[index];
|
|
731
|
+
|
|
732
|
+
if (item) {
|
|
733
|
+
refObject = item;
|
|
734
|
+
} else {
|
|
735
|
+
this.throwSCORMError(
|
|
736
|
+
this._error_codes.VALUE_NOT_INITIALIZED,
|
|
737
|
+
uninitializedErrorMessage,
|
|
738
|
+
);
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Have to update idx value to skip the array position
|
|
743
|
+
idx++;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (refObject === null || refObject === undefined) {
|
|
749
|
+
if (!scorm2004) {
|
|
750
|
+
if (attribute === "_children") {
|
|
751
|
+
this.throwSCORMError(ErrorCodes.scorm12.CHILDREN_ERROR);
|
|
752
|
+
} else if (attribute === "_count") {
|
|
753
|
+
this.throwSCORMError(ErrorCodes.scorm12.COUNT_ERROR);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
} else {
|
|
757
|
+
return refObject;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Returns true if the API's current state is STATE_INITIALIZED
|
|
763
|
+
*
|
|
764
|
+
* @return {boolean}
|
|
765
|
+
*/
|
|
766
|
+
isInitialized(): boolean {
|
|
767
|
+
return this.currentState === APIConstants.global.STATE_INITIALIZED;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Returns true if the API's current state is STATE_NOT_INITIALIZED
|
|
772
|
+
*
|
|
773
|
+
* @return {boolean}
|
|
774
|
+
*/
|
|
775
|
+
isNotInitialized(): boolean {
|
|
776
|
+
return this.currentState === APIConstants.global.STATE_NOT_INITIALIZED;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Returns true if the API's current state is STATE_TERMINATED
|
|
781
|
+
*
|
|
782
|
+
* @return {boolean}
|
|
783
|
+
*/
|
|
784
|
+
isTerminated(): boolean {
|
|
785
|
+
return this.currentState === APIConstants.global.STATE_TERMINATED;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Provides a mechanism for attaching to a specific SCORM event
|
|
790
|
+
*
|
|
791
|
+
* @param {string} listenerName
|
|
792
|
+
* @param {function} callback
|
|
793
|
+
*/
|
|
794
|
+
on(listenerName: string, callback: Function) {
|
|
795
|
+
if (!callback) return;
|
|
796
|
+
|
|
797
|
+
const listenerFunctions = listenerName.split(" ");
|
|
798
|
+
for (let i = 0; i < listenerFunctions.length; i++) {
|
|
799
|
+
const listenerSplit = listenerFunctions[i].split(".");
|
|
800
|
+
if (listenerSplit.length === 0) return;
|
|
801
|
+
|
|
802
|
+
const functionName = listenerSplit[0];
|
|
803
|
+
|
|
804
|
+
let CMIElement = null;
|
|
805
|
+
if (listenerSplit.length > 1) {
|
|
806
|
+
CMIElement = listenerName.replace(functionName + ".", "");
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
this.listenerArray.push({
|
|
810
|
+
functionName: functionName,
|
|
811
|
+
CMIElement: CMIElement,
|
|
812
|
+
callback: callback,
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
this.apiLog(
|
|
816
|
+
"on",
|
|
817
|
+
`Added event listener: ${this.listenerArray.length}`,
|
|
818
|
+
APIConstants.global.LOG_LEVEL_INFO,
|
|
819
|
+
functionName,
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Provides a mechanism for detaching a specific SCORM event listener
|
|
826
|
+
*
|
|
827
|
+
* @param {string} listenerName
|
|
828
|
+
* @param {function} callback
|
|
829
|
+
*/
|
|
830
|
+
off(listenerName: string, callback: Function) {
|
|
831
|
+
if (!callback) return;
|
|
832
|
+
|
|
833
|
+
const listenerFunctions = listenerName.split(" ");
|
|
834
|
+
for (let i = 0; i < listenerFunctions.length; i++) {
|
|
835
|
+
const listenerSplit = listenerFunctions[i].split(".");
|
|
836
|
+
if (listenerSplit.length === 0) return;
|
|
837
|
+
|
|
838
|
+
const functionName = listenerSplit[0];
|
|
839
|
+
|
|
840
|
+
let CMIElement = null;
|
|
841
|
+
if (listenerSplit.length > 1) {
|
|
842
|
+
CMIElement = listenerName.replace(functionName + ".", "");
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const removeIndex = this.listenerArray.findIndex(
|
|
846
|
+
(obj) =>
|
|
847
|
+
obj.functionName === functionName &&
|
|
848
|
+
obj.CMIElement === CMIElement &&
|
|
849
|
+
obj.callback === callback,
|
|
850
|
+
);
|
|
851
|
+
if (removeIndex !== -1) {
|
|
852
|
+
this.listenerArray.splice(removeIndex, 1);
|
|
853
|
+
this.apiLog(
|
|
854
|
+
"off",
|
|
855
|
+
`Removed event listener: ${this.listenerArray.length}`,
|
|
856
|
+
APIConstants.global.LOG_LEVEL_INFO,
|
|
857
|
+
functionName,
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Provides a mechanism for clearing all listeners from a specific SCORM event
|
|
865
|
+
*
|
|
866
|
+
* @param {string} listenerName
|
|
867
|
+
*/
|
|
868
|
+
clear(listenerName: string) {
|
|
869
|
+
const listenerFunctions = listenerName.split(" ");
|
|
870
|
+
for (let i = 0; i < listenerFunctions.length; i++) {
|
|
871
|
+
const listenerSplit = listenerFunctions[i].split(".");
|
|
872
|
+
if (listenerSplit.length === 0) return;
|
|
873
|
+
|
|
874
|
+
const functionName = listenerSplit[0];
|
|
875
|
+
|
|
876
|
+
let CMIElement = null;
|
|
877
|
+
if (listenerSplit.length > 1) {
|
|
878
|
+
CMIElement = listenerName.replace(functionName + ".", "");
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
this.listenerArray = this.listenerArray.filter(
|
|
882
|
+
(obj) =>
|
|
883
|
+
obj.functionName !== functionName && obj.CMIElement !== CMIElement,
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Processes any 'on' listeners that have been created
|
|
890
|
+
*
|
|
891
|
+
* @param {string} functionName
|
|
892
|
+
* @param {string} CMIElement
|
|
893
|
+
* @param {any} value
|
|
894
|
+
*/
|
|
895
|
+
processListeners(functionName: string, CMIElement?: string, value?: any) {
|
|
896
|
+
this.apiLog(
|
|
897
|
+
functionName,
|
|
898
|
+
value,
|
|
899
|
+
APIConstants.global.LOG_LEVEL_INFO,
|
|
900
|
+
CMIElement,
|
|
901
|
+
);
|
|
902
|
+
for (let i = 0; i < this.listenerArray.length; i++) {
|
|
903
|
+
const listener = this.listenerArray[i];
|
|
904
|
+
const functionsMatch = listener.functionName === functionName;
|
|
905
|
+
const listenerHasCMIElement = !!listener.CMIElement;
|
|
906
|
+
let CMIElementsMatch = false;
|
|
907
|
+
if (
|
|
908
|
+
CMIElement &&
|
|
909
|
+
listener.CMIElement &&
|
|
910
|
+
listener.CMIElement.substring(listener.CMIElement.length - 1) === "*"
|
|
911
|
+
) {
|
|
912
|
+
CMIElementsMatch =
|
|
913
|
+
CMIElement.indexOf(
|
|
914
|
+
listener.CMIElement.substring(0, listener.CMIElement.length - 1),
|
|
915
|
+
) === 0;
|
|
916
|
+
} else {
|
|
917
|
+
CMIElementsMatch = listener.CMIElement === CMIElement;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (functionsMatch && (!listenerHasCMIElement || CMIElementsMatch)) {
|
|
921
|
+
this.apiLog(
|
|
922
|
+
"processListeners",
|
|
923
|
+
`Processing listener: ${listener.functionName}`,
|
|
924
|
+
APIConstants.global.LOG_LEVEL_INFO,
|
|
925
|
+
CMIElement,
|
|
926
|
+
);
|
|
927
|
+
listener.callback(CMIElement, value);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Throws a SCORM error
|
|
934
|
+
*
|
|
935
|
+
* @param {number} errorNumber
|
|
936
|
+
* @param {string} message
|
|
937
|
+
*/
|
|
938
|
+
throwSCORMError(errorNumber: number, message?: string) {
|
|
939
|
+
if (!message) {
|
|
940
|
+
message = this.getLmsErrorMessageDetails(errorNumber);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
this.apiLog(
|
|
944
|
+
"throwSCORMError",
|
|
945
|
+
errorNumber + ": " + message,
|
|
946
|
+
APIConstants.global.LOG_LEVEL_ERROR,
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
this.lastErrorCode = String(errorNumber);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Clears the last SCORM error code on success.
|
|
954
|
+
*
|
|
955
|
+
* @param {string} success
|
|
956
|
+
*/
|
|
957
|
+
clearSCORMError(success: string) {
|
|
958
|
+
if (success !== undefined && success !== APIConstants.global.SCORM_FALSE) {
|
|
959
|
+
this.lastErrorCode = "0";
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Load the CMI from a flattened JSON object
|
|
965
|
+
* @param {RefObject} json
|
|
966
|
+
* @param {string} CMIElement
|
|
967
|
+
*/
|
|
968
|
+
loadFromFlattenedJSON(json: RefObject, CMIElement?: string) {
|
|
969
|
+
if (!CMIElement) {
|
|
970
|
+
// by default, we start from a blank string because we're expecting each element to start with `cmi`
|
|
971
|
+
CMIElement = "";
|
|
972
|
+
}
|
|
973
|
+
if (!this.isNotInitialized()) {
|
|
974
|
+
console.error(
|
|
975
|
+
"loadFromFlattenedJSON can only be called before the call to lmsInitialize.",
|
|
976
|
+
);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Tests two strings against a given regular expression pattern and determines a numeric or null result based on the matching criterion.
|
|
982
|
+
*
|
|
983
|
+
* @param {string} a - The first string to be tested against the pattern.
|
|
984
|
+
* @param {string} c - The second string to be tested against the pattern.
|
|
985
|
+
* @param {RegExp} a_pattern - The regular expression pattern to test the strings against.
|
|
986
|
+
* @return {number | null} A numeric result based on the matching criterion, or null if the strings do not match the pattern.
|
|
987
|
+
*/
|
|
988
|
+
function testPattern(
|
|
989
|
+
a: string,
|
|
990
|
+
c: string,
|
|
991
|
+
a_pattern: RegExp,
|
|
992
|
+
): number | null {
|
|
993
|
+
const a_match = a.match(a_pattern);
|
|
994
|
+
|
|
995
|
+
let c_match;
|
|
996
|
+
if (a_match !== null && (c_match = c.match(a_pattern)) !== null) {
|
|
997
|
+
const a_num = Number(a_match[2]);
|
|
998
|
+
const c_num = Number(c_match[2]);
|
|
999
|
+
if (a_num === c_num) {
|
|
1000
|
+
if (a_match[3] === "id") {
|
|
1001
|
+
return -1;
|
|
1002
|
+
} else if (a_match[3] === "type") {
|
|
1003
|
+
if (c_match[3] === "id") {
|
|
1004
|
+
return 1;
|
|
1005
|
+
} else {
|
|
1006
|
+
return -1;
|
|
1007
|
+
}
|
|
1008
|
+
} else {
|
|
1009
|
+
return 1;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return a_num - c_num;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const int_pattern = /^(cmi\.interactions\.)(\d+)\.(.*)$/;
|
|
1019
|
+
const obj_pattern = /^(cmi\.objectives\.)(\d+)\.(.*)$/;
|
|
1020
|
+
|
|
1021
|
+
const result = Object.keys(json).map(function (key) {
|
|
1022
|
+
return [String(key), json[key]];
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// CMI interactions need to have id and type loaded before any other fields
|
|
1026
|
+
result.sort(function ([a, _b], [c, _d]) {
|
|
1027
|
+
let test;
|
|
1028
|
+
if ((test = testPattern(a, c, int_pattern)) !== null) {
|
|
1029
|
+
return test;
|
|
1030
|
+
}
|
|
1031
|
+
if ((test = testPattern(a, c, obj_pattern)) !== null) {
|
|
1032
|
+
return test;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (a < c) {
|
|
1036
|
+
return -1;
|
|
1037
|
+
}
|
|
1038
|
+
if (a > c) {
|
|
1039
|
+
return 1;
|
|
1040
|
+
}
|
|
1041
|
+
return 0;
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
let obj: RefObject;
|
|
1045
|
+
result.forEach((element) => {
|
|
1046
|
+
obj = {};
|
|
1047
|
+
obj[element[0]] = element[1];
|
|
1048
|
+
this.loadFromJSON(unflatten(obj), CMIElement);
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Loads CMI data from a JSON object.
|
|
1054
|
+
*
|
|
1055
|
+
* @param {RefObject} json
|
|
1056
|
+
* @param {string} CMIElement
|
|
1057
|
+
*/
|
|
1058
|
+
loadFromJSON(json: RefObject, CMIElement: string) {
|
|
1059
|
+
if (!this.isNotInitialized()) {
|
|
1060
|
+
console.error(
|
|
1061
|
+
"loadFromJSON can only be called before the call to lmsInitialize.",
|
|
1062
|
+
);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
CMIElement = CMIElement !== undefined ? CMIElement : "cmi";
|
|
1067
|
+
|
|
1068
|
+
this.startingData = json;
|
|
1069
|
+
|
|
1070
|
+
// could this be refactored down to flatten(json) then setCMIValue on each?
|
|
1071
|
+
for (const key in json) {
|
|
1072
|
+
if ({}.hasOwnProperty.call(json, key) && json[key]) {
|
|
1073
|
+
const currentCMIElement = (CMIElement ? CMIElement + "." : "") + key;
|
|
1074
|
+
const value = json[key];
|
|
1075
|
+
|
|
1076
|
+
if (value["childArray"]) {
|
|
1077
|
+
for (let i = 0; i < value["childArray"].length; i++) {
|
|
1078
|
+
this.loadFromJSON(
|
|
1079
|
+
value["childArray"][i],
|
|
1080
|
+
currentCMIElement + "." + i,
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
} else if (value.constructor === Object) {
|
|
1084
|
+
this.loadFromJSON(value, currentCMIElement);
|
|
1085
|
+
} else {
|
|
1086
|
+
this.setCMIValue(currentCMIElement, value);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Render the CMI object to JSON for sending to an LMS.
|
|
1094
|
+
*
|
|
1095
|
+
* @return {string}
|
|
1096
|
+
*/
|
|
1097
|
+
renderCMIToJSONString(): string {
|
|
1098
|
+
const cmi = this.cmi;
|
|
1099
|
+
// Do we want/need to return fields that have no set value?
|
|
1100
|
+
if (this.settings.sendFullCommit) {
|
|
1101
|
+
return JSON.stringify({ cmi });
|
|
1102
|
+
}
|
|
1103
|
+
return JSON.stringify({ cmi }, (k, v) => (v === undefined ? null : v), 2);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Returns a JS object representing the current cmi
|
|
1108
|
+
* @return {object}
|
|
1109
|
+
*/
|
|
1110
|
+
renderCMIToJSONObject(): object {
|
|
1111
|
+
return JSON.parse(this.renderCMIToJSONString());
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* Send the request to the LMS
|
|
1116
|
+
* @param {string} url
|
|
1117
|
+
* @param {RefObject|Array} params
|
|
1118
|
+
* @param {boolean} immediate
|
|
1119
|
+
* @return {ResultObject}
|
|
1120
|
+
*/
|
|
1121
|
+
async processHttpRequest(
|
|
1122
|
+
url: string,
|
|
1123
|
+
params: RefObject | Array<any>,
|
|
1124
|
+
immediate: boolean = false,
|
|
1125
|
+
): Promise<ResultObject> {
|
|
1126
|
+
const api = this;
|
|
1127
|
+
const genericError: ResultObject = {
|
|
1128
|
+
result: APIConstants.global.SCORM_FALSE,
|
|
1129
|
+
errorCode: this.error_codes.GENERAL,
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
// if we are terminating the module or closing the browser window/tab, we need to make this fetch ASAP.
|
|
1133
|
+
// Some browsers, especially Chrome, do not like synchronous requests to be made when the window is closing.
|
|
1134
|
+
if (immediate) {
|
|
1135
|
+
this.performFetch(url, params).then(async (response) => {
|
|
1136
|
+
await this.transformResponse(response);
|
|
1137
|
+
});
|
|
1138
|
+
return {
|
|
1139
|
+
result: APIConstants.global.SCORM_TRUE,
|
|
1140
|
+
errorCode: 0,
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const process = async (
|
|
1145
|
+
url: string,
|
|
1146
|
+
params: RefObject | Array<any>,
|
|
1147
|
+
settings: Settings,
|
|
1148
|
+
): Promise<ResultObject> => {
|
|
1149
|
+
try {
|
|
1150
|
+
params = settings.requestHandler(params);
|
|
1151
|
+
const response = await this.performFetch(url, params);
|
|
1152
|
+
|
|
1153
|
+
return this.transformResponse(response);
|
|
1154
|
+
} catch (e) {
|
|
1155
|
+
this.apiLog(
|
|
1156
|
+
"processHttpRequest",
|
|
1157
|
+
e,
|
|
1158
|
+
APIConstants.global.LOG_LEVEL_ERROR,
|
|
1159
|
+
);
|
|
1160
|
+
api.processListeners("CommitError");
|
|
1161
|
+
return genericError;
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
if (this.settings.asyncCommit) {
|
|
1166
|
+
const debouncedProcess = debounce(process, 500, immediate);
|
|
1167
|
+
debouncedProcess(url, params, this.settings);
|
|
1168
|
+
|
|
1169
|
+
return {
|
|
1170
|
+
result: APIConstants.global.SCORM_TRUE,
|
|
1171
|
+
errorCode: 0,
|
|
1172
|
+
};
|
|
1173
|
+
} else {
|
|
1174
|
+
return await process(url, params, this.settings);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Throws a SCORM error
|
|
1180
|
+
*
|
|
1181
|
+
* @param {number} when - the number of milliseconds to wait before committing
|
|
1182
|
+
* @param {string} callback - the name of the commit event callback
|
|
1183
|
+
*/
|
|
1184
|
+
scheduleCommit(when: number, callback: string) {
|
|
1185
|
+
this._timeout = new ScheduledCommit(this, when, callback);
|
|
1186
|
+
this.apiLog(
|
|
1187
|
+
"scheduleCommit",
|
|
1188
|
+
"scheduled",
|
|
1189
|
+
APIConstants.global.LOG_LEVEL_DEBUG,
|
|
1190
|
+
"",
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Clears and cancels any currently scheduled commits
|
|
1196
|
+
*/
|
|
1197
|
+
clearScheduledCommit() {
|
|
1198
|
+
if (this._timeout) {
|
|
1199
|
+
this._timeout.cancel();
|
|
1200
|
+
this._timeout = undefined;
|
|
1201
|
+
this.apiLog(
|
|
1202
|
+
"clearScheduledCommit",
|
|
1203
|
+
"cleared",
|
|
1204
|
+
APIConstants.global.LOG_LEVEL_DEBUG,
|
|
1205
|
+
"",
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Check to see if the specific object has the given property
|
|
1212
|
+
* @param {RefObject} refObject
|
|
1213
|
+
* @param {string} attribute
|
|
1214
|
+
* @return {boolean}
|
|
1215
|
+
* @private
|
|
1216
|
+
*/
|
|
1217
|
+
private _checkObjectHasProperty(
|
|
1218
|
+
refObject: RefObject,
|
|
1219
|
+
attribute: string,
|
|
1220
|
+
): boolean {
|
|
1221
|
+
return (
|
|
1222
|
+
Object.hasOwnProperty.call(refObject, attribute) ||
|
|
1223
|
+
Object.getOwnPropertyDescriptor(
|
|
1224
|
+
Object.getPrototypeOf(refObject),
|
|
1225
|
+
attribute,
|
|
1226
|
+
) != null ||
|
|
1227
|
+
attribute in refObject
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Handles the error that occurs when trying to access a value
|
|
1233
|
+
* @param {any} e
|
|
1234
|
+
* @param {string} returnValue
|
|
1235
|
+
* @return {string}
|
|
1236
|
+
* @private
|
|
1237
|
+
*/
|
|
1238
|
+
private handleValueAccessException(e: any, returnValue: string): string {
|
|
1239
|
+
if (e instanceof ValidationError) {
|
|
1240
|
+
this.lastErrorCode = String(e.errorCode);
|
|
1241
|
+
returnValue = APIConstants.global.SCORM_FALSE;
|
|
1242
|
+
} else {
|
|
1243
|
+
if (e instanceof Error && e.message) {
|
|
1244
|
+
console.error(e.message);
|
|
1245
|
+
} else {
|
|
1246
|
+
console.error(e);
|
|
1247
|
+
}
|
|
1248
|
+
this.throwSCORMError(this._error_codes.GENERAL);
|
|
1249
|
+
}
|
|
1250
|
+
return returnValue;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* Perform the fetch request to the LMS
|
|
1255
|
+
* @param {string} url
|
|
1256
|
+
* @param {RefObject|Array} params
|
|
1257
|
+
* @return {Promise<Response>}
|
|
1258
|
+
* @private
|
|
1259
|
+
*/
|
|
1260
|
+
private async performFetch(
|
|
1261
|
+
url: string,
|
|
1262
|
+
params: RefObject | Array<any>,
|
|
1263
|
+
): Promise<Response> {
|
|
1264
|
+
return fetch(url, {
|
|
1265
|
+
method: "POST",
|
|
1266
|
+
body: params instanceof Array ? params.join("&") : JSON.stringify(params),
|
|
1267
|
+
headers: {
|
|
1268
|
+
...this.settings.xhrHeaders,
|
|
1269
|
+
"Content-Type": this.settings.commitRequestDataType,
|
|
1270
|
+
},
|
|
1271
|
+
credentials: this.settings.xhrWithCredentials ? "include" : undefined,
|
|
1272
|
+
keepalive: true,
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Transforms the response from the LMS to a ResultObject
|
|
1278
|
+
* @param {Response} response
|
|
1279
|
+
* @return {Promise<ResultObject>}
|
|
1280
|
+
* @private
|
|
1281
|
+
*/
|
|
1282
|
+
private async transformResponse(response: Response): Promise<ResultObject> {
|
|
1283
|
+
const result =
|
|
1284
|
+
typeof this.settings.responseHandler === "function"
|
|
1285
|
+
? await this.settings.responseHandler(response)
|
|
1286
|
+
: await response.json();
|
|
1287
|
+
|
|
1288
|
+
if (
|
|
1289
|
+
response.status >= 200 &&
|
|
1290
|
+
response.status <= 299 &&
|
|
1291
|
+
(result.result === true ||
|
|
1292
|
+
result.result === APIConstants.global.SCORM_TRUE)
|
|
1293
|
+
) {
|
|
1294
|
+
this.processListeners("CommitSuccess");
|
|
1295
|
+
} else {
|
|
1296
|
+
this.processListeners("CommitError");
|
|
1297
|
+
}
|
|
1298
|
+
return result;
|
|
1299
|
+
}
|
|
1300
|
+
}
|