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