scorm-again 3.0.4 → 3.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/README.md +96 -5
- package/dist/cross-frame-api.js.map +1 -1
- package/dist/esm/cross-frame-api.js.map +1 -1
- package/dist/esm/scorm-again.js +503 -138
- package/dist/esm/scorm-again.js.map +1 -1
- package/dist/esm/scorm-again.min.js +1 -1
- package/dist/esm/scorm-again.min.js.map +1 -1
- package/dist/esm/scorm12.js +339 -73
- package/dist/esm/scorm12.js.map +1 -1
- package/dist/esm/scorm12.min.js +1 -1
- package/dist/esm/scorm12.min.js.map +1 -1
- package/dist/esm/scorm2004.js +486 -131
- package/dist/esm/scorm2004.js.map +1 -1
- package/dist/esm/scorm2004.min.js +1 -1
- package/dist/esm/scorm2004.min.js.map +1 -1
- package/dist/scorm-again.js +443 -119
- package/dist/scorm-again.js.map +1 -1
- package/dist/scorm-again.min.js +1 -1
- package/dist/scorm-again.min.js.map +1 -1
- package/dist/scorm12.js +316 -57
- package/dist/scorm12.js.map +1 -1
- package/dist/scorm12.min.js +1 -1
- package/dist/scorm12.min.js.map +1 -1
- package/dist/scorm2004.js +431 -112
- package/dist/scorm2004.js.map +1 -1
- package/dist/scorm2004.min.js +1 -1
- package/dist/scorm2004.min.js.map +1 -1
- package/dist/types/BaseAPI.d.ts +15 -5
- package/dist/types/Scorm12API.d.ts +2 -2
- package/dist/types/Scorm2004API.d.ts +3 -2
- package/dist/types/cmi/common/score.d.ts +2 -0
- package/dist/types/cmi/scorm2004/interaction_delimiters.d.ts +4 -0
- package/dist/types/interfaces/services.d.ts +8 -4
- package/dist/types/services/AsynchronousHttpService.d.ts +3 -2
- package/dist/types/services/EventService.d.ts +2 -2
- package/dist/types/services/OfflineStorageService.d.ts +6 -1
- package/dist/types/services/SynchronousHttpService.d.ts +2 -2
- package/dist/types/services/ValidationService.d.ts +1 -1
- package/dist/types/types/api_types.d.ts +22 -2
- package/dist/types/utilities/index.d.ts +1 -0
- package/dist/types/utilities/url.d.ts +2 -0
- package/package.json +46 -25
package/dist/esm/scorm-again.js
CHANGED
|
@@ -268,6 +268,15 @@ function parseNavigationRequest(navRequest) {
|
|
|
268
268
|
};
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
const appendQueryParam = (url, name, value) => {
|
|
272
|
+
const fragmentIndex = url.indexOf("#");
|
|
273
|
+
const baseUrl = fragmentIndex === -1 ? url : url.slice(0, fragmentIndex);
|
|
274
|
+
const fragment = fragmentIndex === -1 ? "" : url.slice(fragmentIndex);
|
|
275
|
+
const separator = baseUrl.includes("?") ? baseUrl.endsWith("?") || baseUrl.endsWith("&") ? "" : "&" : "?";
|
|
276
|
+
const queryParam = `${encodeURIComponent(name)}=${encodeURIComponent(String(value))}`;
|
|
277
|
+
return `${baseUrl}${separator}${queryParam}${fragment}`;
|
|
278
|
+
};
|
|
279
|
+
|
|
271
280
|
class BaseCMI {
|
|
272
281
|
/**
|
|
273
282
|
* Flag used during JSON serialization to allow getter access without initialization checks.
|
|
@@ -659,7 +668,10 @@ const scorm12_errors = {
|
|
|
659
668
|
READ_ONLY_ELEMENT: 403,
|
|
660
669
|
WRITE_ONLY_ELEMENT: 404,
|
|
661
670
|
TYPE_MISMATCH: 405,
|
|
662
|
-
|
|
671
|
+
// SCORM 1.2 has no out-of-range error code; an out-of-range value is reported
|
|
672
|
+
// as 405 (incorrect data type), per the SCORM 1.2 RTE error table and the ADL
|
|
673
|
+
// 1.2 CTS (DataModelValidator.checkScoreDecimal failure -> CMICategory 405).
|
|
674
|
+
VALUE_OUT_OF_RANGE: 405,
|
|
663
675
|
DEPENDENCY_NOT_ESTABLISHED: 408
|
|
664
676
|
};
|
|
665
677
|
const scorm2004_errors = {
|
|
@@ -807,6 +819,7 @@ const DefaultSettings = {
|
|
|
807
819
|
xhrWithCredentials: false,
|
|
808
820
|
fetchMode: "cors",
|
|
809
821
|
asyncModeBeaconBehavior: "never",
|
|
822
|
+
includeCommitSequence: false,
|
|
810
823
|
responseHandler: async function(response) {
|
|
811
824
|
if (typeof response !== "undefined") {
|
|
812
825
|
let httpResult = null;
|
|
@@ -1160,6 +1173,11 @@ const scorm2004_regex = {
|
|
|
1160
1173
|
progress_range: "0#1"
|
|
1161
1174
|
};
|
|
1162
1175
|
|
|
1176
|
+
const PERFORMANCE_STEP_NAME = "^$|" + scorm2004_regex.CMIShortIdentifier;
|
|
1177
|
+
const PERFORMANCE_CHARACTERSTRING = "(?![\\s\\S]*(?:\\[,\\]|\\[\\.\\]|\\[:\\]))[\\s\\S]{1,250}";
|
|
1178
|
+
const PERFORMANCE_NUMERIC_RANGE = "(?:-?\\d+(?:\\.\\d+)?)?\\[:\\](?:-?\\d+(?:\\.\\d+)?)?";
|
|
1179
|
+
const CR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_NUMERIC_RANGE + "|" + PERFORMANCE_CHARACTERSTRING + ")$";
|
|
1180
|
+
const LR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_CHARACTERSTRING + ")$";
|
|
1163
1181
|
const LearnerResponses = {
|
|
1164
1182
|
"true-false": {
|
|
1165
1183
|
format: "^true$|^false$",
|
|
@@ -1194,8 +1212,8 @@ const LearnerResponses = {
|
|
|
1194
1212
|
unique: false
|
|
1195
1213
|
},
|
|
1196
1214
|
performance: {
|
|
1197
|
-
format:
|
|
1198
|
-
format2:
|
|
1215
|
+
format: PERFORMANCE_STEP_NAME,
|
|
1216
|
+
format2: LR_PERFORMANCE_STEP_ANSWER,
|
|
1199
1217
|
max: 250,
|
|
1200
1218
|
delimiter: "[,]",
|
|
1201
1219
|
delimiter2: "[.]",
|
|
@@ -1271,10 +1289,10 @@ const CorrectResponses = {
|
|
|
1271
1289
|
delimiter2: "[.]",
|
|
1272
1290
|
unique: false,
|
|
1273
1291
|
duplicate: false,
|
|
1274
|
-
// step_name
|
|
1275
|
-
format:
|
|
1276
|
-
// step_answer
|
|
1277
|
-
format2:
|
|
1292
|
+
// step_name: optional short_identifier_type
|
|
1293
|
+
format: PERFORMANCE_STEP_NAME,
|
|
1294
|
+
// step_answer: optional characterstring (spaces allowed) or numeric range
|
|
1295
|
+
format2: CR_PERFORMANCE_STEP_ANSWER
|
|
1278
1296
|
},
|
|
1279
1297
|
sequencing: {
|
|
1280
1298
|
max: 36,
|
|
@@ -1734,7 +1752,7 @@ class ScheduledCommit {
|
|
|
1734
1752
|
wrapper() {
|
|
1735
1753
|
if (!this._cancelled) {
|
|
1736
1754
|
if (this._API.isInitialized()) {
|
|
1737
|
-
(async () => await this._API.commit(this._callback))();
|
|
1755
|
+
(async () => await this._API.commit(this._callback, false, "autocommit"))();
|
|
1738
1756
|
}
|
|
1739
1757
|
}
|
|
1740
1758
|
}
|
|
@@ -5079,6 +5097,7 @@ class ActivityDeliveryService {
|
|
|
5079
5097
|
}
|
|
5080
5098
|
|
|
5081
5099
|
class AsynchronousHttpService {
|
|
5100
|
+
reportsRequestCompletion = true;
|
|
5082
5101
|
settings;
|
|
5083
5102
|
error_codes;
|
|
5084
5103
|
/**
|
|
@@ -5103,10 +5122,20 @@ class AsynchronousHttpService {
|
|
|
5103
5122
|
* @param {boolean} immediate - Whether to send the request immediately without waiting
|
|
5104
5123
|
* @param {Function} apiLog - Function to log API messages with appropriate levels
|
|
5105
5124
|
* @param {Function} processListeners - Function to trigger event listeners for commit events
|
|
5125
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
5126
|
+
* @param {Function} onRequestComplete - Callback invoked after the background request settles
|
|
5106
5127
|
* @return {ResultObject} - Immediate optimistic success result
|
|
5107
5128
|
*/
|
|
5108
|
-
processHttpRequest(url, params, immediate = false, apiLog, processListeners) {
|
|
5109
|
-
this._performAsyncRequest(
|
|
5129
|
+
processHttpRequest(url, params, immediate = false, apiLog, processListeners, metadata, onRequestComplete) {
|
|
5130
|
+
this._performAsyncRequest(
|
|
5131
|
+
url,
|
|
5132
|
+
params,
|
|
5133
|
+
immediate,
|
|
5134
|
+
apiLog,
|
|
5135
|
+
processListeners,
|
|
5136
|
+
metadata,
|
|
5137
|
+
onRequestComplete
|
|
5138
|
+
);
|
|
5110
5139
|
return {
|
|
5111
5140
|
result: global_constants.SCORM_TRUE,
|
|
5112
5141
|
errorCode: 0
|
|
@@ -5119,11 +5148,14 @@ class AsynchronousHttpService {
|
|
|
5119
5148
|
* @param {boolean} immediate - Whether this is an immediate request
|
|
5120
5149
|
* @param apiLog - Function to log API messages
|
|
5121
5150
|
* @param {Function} processListeners - Function to process event listeners
|
|
5151
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
5152
|
+
* @param {Function} onRequestComplete - Callback invoked after the request settles
|
|
5122
5153
|
* @private
|
|
5123
5154
|
*/
|
|
5124
|
-
async _performAsyncRequest(url, params, immediate, apiLog, processListeners) {
|
|
5155
|
+
async _performAsyncRequest(url, params, immediate, apiLog, processListeners, metadata, onRequestComplete) {
|
|
5125
5156
|
try {
|
|
5126
|
-
const
|
|
5157
|
+
const handledParams = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
|
|
5158
|
+
const processedParams = handledParams;
|
|
5127
5159
|
let response;
|
|
5128
5160
|
if (immediate && this.settings.asyncModeBeaconBehavior !== "never") {
|
|
5129
5161
|
response = await this.performBeacon(url, processedParams);
|
|
@@ -5139,7 +5171,9 @@ class AsynchronousHttpService {
|
|
|
5139
5171
|
} catch (e) {
|
|
5140
5172
|
const message = e instanceof Error ? e.message : String(e);
|
|
5141
5173
|
apiLog("processHttpRequest", `Async request failed: ${message}`, LogLevelEnum.ERROR);
|
|
5142
|
-
processListeners("CommitError");
|
|
5174
|
+
processListeners("CommitError", void 0, this.error_codes.GENERAL_COMMIT_FAILURE || 391);
|
|
5175
|
+
} finally {
|
|
5176
|
+
onRequestComplete?.();
|
|
5143
5177
|
}
|
|
5144
5178
|
}
|
|
5145
5179
|
/**
|
|
@@ -5282,10 +5316,13 @@ class CMIValueAccessService {
|
|
|
5282
5316
|
}
|
|
5283
5317
|
/**
|
|
5284
5318
|
* Gets the appropriate error code for undefined data model elements.
|
|
5285
|
-
* SCORM 2004
|
|
5319
|
+
* Both SCORM 2004 and SCORM 1.2 use UNDEFINED_DATA_MODEL (401): an
|
|
5320
|
+
* unrecognized element is "Not implemented", not a general exception. SCORM
|
|
5321
|
+
* 1.2 previously returned GENERAL (101) here, which is non-conformant — the
|
|
5322
|
+
* ADL 1.2 CTS and the SCORM 1.2 RTE spec expect 401 for an unknown element.
|
|
5286
5323
|
*/
|
|
5287
|
-
getUndefinedDataModelErrorCode(
|
|
5288
|
-
return
|
|
5324
|
+
getUndefinedDataModelErrorCode() {
|
|
5325
|
+
return getErrorCode(this.context.errorCodes, "UNDEFINED_DATA_MODEL");
|
|
5289
5326
|
}
|
|
5290
5327
|
/**
|
|
5291
5328
|
* Sets a value on a CMI element path
|
|
@@ -5313,7 +5350,7 @@ class CMIValueAccessService {
|
|
|
5313
5350
|
let returnValue = global_constants.SCORM_FALSE;
|
|
5314
5351
|
let foundFirstIndex = false;
|
|
5315
5352
|
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
|
|
5316
|
-
const invalidErrorCode = this.getUndefinedDataModelErrorCode(
|
|
5353
|
+
const invalidErrorCode = this.getUndefinedDataModelErrorCode();
|
|
5317
5354
|
for (let idx = 0; idx < structure.length; idx++) {
|
|
5318
5355
|
const attribute = structure[idx];
|
|
5319
5356
|
if (idx === structure.length - 1) {
|
|
@@ -5383,12 +5420,13 @@ class CMIValueAccessService {
|
|
|
5383
5420
|
);
|
|
5384
5421
|
return "";
|
|
5385
5422
|
}
|
|
5423
|
+
this.context.setLastErrorCode("0");
|
|
5386
5424
|
const structure = CMIElement.split(".");
|
|
5387
5425
|
let refObject = this.context.getDataModel();
|
|
5388
5426
|
let attribute = null;
|
|
5389
5427
|
const uninitializedErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) has not been initialized.`;
|
|
5390
5428
|
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
|
|
5391
|
-
const invalidErrorCode = this.getUndefinedDataModelErrorCode(
|
|
5429
|
+
const invalidErrorCode = this.getUndefinedDataModelErrorCode();
|
|
5392
5430
|
for (let idx = 0; idx < structure.length; idx++) {
|
|
5393
5431
|
attribute = structure[idx];
|
|
5394
5432
|
const validationResult = this.validateGetAttribute(
|
|
@@ -5583,7 +5621,19 @@ class CMIValueAccessService {
|
|
|
5583
5621
|
if (!scorm2004) {
|
|
5584
5622
|
if (isFinalAttribute) {
|
|
5585
5623
|
if (typeof attribute === "undefined" || !this.context.checkObjectHasProperty(refObject, attribute)) {
|
|
5586
|
-
|
|
5624
|
+
if (attribute === "_children") {
|
|
5625
|
+
this.context.throwSCORMError(
|
|
5626
|
+
CMIElement,
|
|
5627
|
+
getErrorCode(this.context.errorCodes, "CHILDREN_ERROR")
|
|
5628
|
+
);
|
|
5629
|
+
} else if (attribute === "_count") {
|
|
5630
|
+
this.context.throwSCORMError(
|
|
5631
|
+
CMIElement,
|
|
5632
|
+
getErrorCode(this.context.errorCodes, "COUNT_ERROR")
|
|
5633
|
+
);
|
|
5634
|
+
} else {
|
|
5635
|
+
this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
|
|
5636
|
+
}
|
|
5587
5637
|
return { error: true };
|
|
5588
5638
|
}
|
|
5589
5639
|
}
|
|
@@ -6088,8 +6138,9 @@ class EventService {
|
|
|
6088
6138
|
* @param {string} functionName - The name of the function that triggered the event
|
|
6089
6139
|
* @param {string} CMIElement - The CMI element that was affected
|
|
6090
6140
|
* @param {any} value - The value that was set
|
|
6141
|
+
* @param {CommitEventContext} context - Optional context for commit lifecycle events
|
|
6091
6142
|
*/
|
|
6092
|
-
processListeners(functionName, CMIElement, value) {
|
|
6143
|
+
processListeners(functionName, CMIElement, value, context) {
|
|
6093
6144
|
this.apiLog(functionName, value, LogLevelEnum.INFO, CMIElement);
|
|
6094
6145
|
const listeners = this.listenerMap.get(functionName);
|
|
6095
6146
|
if (!listeners) return;
|
|
@@ -6114,9 +6165,17 @@ class EventService {
|
|
|
6114
6165
|
if (functionName.startsWith("Sequence")) {
|
|
6115
6166
|
listener.callback(value);
|
|
6116
6167
|
} else if (functionName === "CommitError") {
|
|
6117
|
-
|
|
6168
|
+
if (context !== void 0) {
|
|
6169
|
+
listener.callback(value, context);
|
|
6170
|
+
} else {
|
|
6171
|
+
listener.callback(value);
|
|
6172
|
+
}
|
|
6118
6173
|
} else if (functionName === "CommitSuccess") {
|
|
6119
|
-
|
|
6174
|
+
if (context !== void 0) {
|
|
6175
|
+
listener.callback(context);
|
|
6176
|
+
} else {
|
|
6177
|
+
listener.callback();
|
|
6178
|
+
}
|
|
6120
6179
|
} else {
|
|
6121
6180
|
listener.callback(CMIElement, value);
|
|
6122
6181
|
}
|
|
@@ -6250,16 +6309,19 @@ class OfflineStorageService {
|
|
|
6250
6309
|
* Store commit data offline
|
|
6251
6310
|
* @param {string} courseId - Identifier for the course
|
|
6252
6311
|
* @param {CommitObject} commitData - The data to store offline
|
|
6312
|
+
* @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
|
|
6253
6313
|
* @returns {ResultObject} - Result of the storage operation
|
|
6254
6314
|
*/
|
|
6255
|
-
storeOffline(courseId, commitData) {
|
|
6315
|
+
storeOffline(courseId, commitData, metadata) {
|
|
6256
6316
|
try {
|
|
6257
6317
|
const queueItem = {
|
|
6258
6318
|
id: `${courseId}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
|
6259
6319
|
courseId,
|
|
6260
6320
|
timestamp: Date.now(),
|
|
6261
6321
|
data: commitData,
|
|
6262
|
-
syncAttempts: 0
|
|
6322
|
+
syncAttempts: 0,
|
|
6323
|
+
...metadata?.isTerminateCommit !== void 0 ? { isTerminateCommit: metadata.isTerminateCommit } : {},
|
|
6324
|
+
...metadata?.sequence !== void 0 ? { sequence: metadata.sequence } : {}
|
|
6263
6325
|
};
|
|
6264
6326
|
const currentQueue = this.getFromStorage(this.syncQueue) || [];
|
|
6265
6327
|
currentQueue.push(queueItem);
|
|
@@ -6338,7 +6400,10 @@ class OfflineStorageService {
|
|
|
6338
6400
|
continue;
|
|
6339
6401
|
}
|
|
6340
6402
|
try {
|
|
6341
|
-
const syncResult = await this.sendDataToLMS(item.data
|
|
6403
|
+
const syncResult = await this.sendDataToLMS(item.data, {
|
|
6404
|
+
...item.isTerminateCommit !== void 0 ? { isTerminateCommit: item.isTerminateCommit } : {},
|
|
6405
|
+
...item.sequence !== void 0 ? { sequence: item.sequence } : {}
|
|
6406
|
+
});
|
|
6342
6407
|
if (syncResult.result === true || syncResult.result === global_constants.SCORM_TRUE) {
|
|
6343
6408
|
this.apiLog(
|
|
6344
6409
|
"OfflineStorageService",
|
|
@@ -6385,17 +6450,25 @@ class OfflineStorageService {
|
|
|
6385
6450
|
/**
|
|
6386
6451
|
* Send data to the LMS when online
|
|
6387
6452
|
* @param {CommitObject} data - The data to send to the LMS
|
|
6453
|
+
* @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
|
|
6388
6454
|
* @returns {Promise<ResultObject>} - Result of the sync operation
|
|
6389
6455
|
*/
|
|
6390
|
-
async sendDataToLMS(data) {
|
|
6391
|
-
|
|
6456
|
+
async sendDataToLMS(data, metadata) {
|
|
6457
|
+
const configuredCommitUrl = this.settings.lmsCommitUrl;
|
|
6458
|
+
if (!configuredCommitUrl) {
|
|
6392
6459
|
return {
|
|
6393
6460
|
result: global_constants.SCORM_FALSE,
|
|
6394
6461
|
errorCode: this.error_codes.GENERAL || 101
|
|
6395
6462
|
};
|
|
6396
6463
|
}
|
|
6397
6464
|
try {
|
|
6398
|
-
const
|
|
6465
|
+
const lmsCommitUrl = String(configuredCommitUrl);
|
|
6466
|
+
const processedData = this.settings.requestHandler(data, {
|
|
6467
|
+
isTerminateCommit: metadata?.isTerminateCommit ?? false,
|
|
6468
|
+
trigger: "offline-replay",
|
|
6469
|
+
...metadata?.sequence !== void 0 ? { sequence: metadata.sequence } : {}
|
|
6470
|
+
});
|
|
6471
|
+
const requestUrl = metadata?.isTerminateCommit && this.settings.terminateCommitParam ? appendQueryParam(lmsCommitUrl, this.settings.terminateCommitParam, "true") : lmsCommitUrl;
|
|
6399
6472
|
const init = {
|
|
6400
6473
|
method: "POST",
|
|
6401
6474
|
mode: this.settings.fetchMode,
|
|
@@ -6408,7 +6481,7 @@ class OfflineStorageService {
|
|
|
6408
6481
|
if (this.settings.xhrWithCredentials) {
|
|
6409
6482
|
init.credentials = "include";
|
|
6410
6483
|
}
|
|
6411
|
-
const response = await fetch(
|
|
6484
|
+
const response = await fetch(requestUrl, init);
|
|
6412
6485
|
const result = typeof this.settings.responseHandler === "function" ? await this.settings.responseHandler(response) : await response.json();
|
|
6413
6486
|
if (response.status >= 200 && response.status <= 299 && (result.result === true || result.result === global_constants.SCORM_TRUE)) {
|
|
6414
6487
|
if (!Object.hasOwnProperty.call(result, "errorCode")) {
|
|
@@ -15373,6 +15446,8 @@ class SynchronousHttpService {
|
|
|
15373
15446
|
* @param {boolean} immediate - Whether this is a termination commit (use sendBeacon)
|
|
15374
15447
|
* @param {Function} _apiLog - Function to log API messages (unused in synchronous mode - errors returned directly)
|
|
15375
15448
|
* @param {Function} _processListeners - Function to trigger event listeners (unused in synchronous mode - no async events)
|
|
15449
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
15450
|
+
* @param {Function} _onRequestComplete - Completion callback (unused because requests settle before return)
|
|
15376
15451
|
* @return {ResultObject} - The result of the request (synchronous)
|
|
15377
15452
|
*
|
|
15378
15453
|
* @remarks
|
|
@@ -15382,21 +15457,23 @@ class SynchronousHttpService {
|
|
|
15382
15457
|
* - No async events need to be triggered (CommitSuccess/CommitError) since results are synchronous
|
|
15383
15458
|
* - AsynchronousHttpService uses these parameters to handle background request results
|
|
15384
15459
|
*/
|
|
15385
|
-
processHttpRequest(url, params, immediate = false, _apiLog, _processListeners) {
|
|
15460
|
+
processHttpRequest(url, params, immediate = false, _apiLog, _processListeners, metadata, _onRequestComplete) {
|
|
15386
15461
|
if (immediate) {
|
|
15387
|
-
return this._handleImmediateRequest(url, params);
|
|
15462
|
+
return this._handleImmediateRequest(url, params, metadata);
|
|
15388
15463
|
}
|
|
15389
|
-
return this._performSyncXHR(url, params);
|
|
15464
|
+
return this._performSyncXHR(url, params, metadata);
|
|
15390
15465
|
}
|
|
15391
15466
|
/**
|
|
15392
15467
|
* Handles an immediate request using sendBeacon
|
|
15393
15468
|
* @param {string} url - The URL to send the request to
|
|
15394
15469
|
* @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
|
|
15470
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
15395
15471
|
* @return {ResultObject} - The result based on beacon success
|
|
15396
15472
|
* @private
|
|
15397
15473
|
*/
|
|
15398
|
-
_handleImmediateRequest(url, params) {
|
|
15399
|
-
const
|
|
15474
|
+
_handleImmediateRequest(url, params, metadata) {
|
|
15475
|
+
const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
|
|
15476
|
+
const requestPayload = handledPayload ?? params;
|
|
15400
15477
|
const { body } = this._prepareRequestBody(requestPayload);
|
|
15401
15478
|
const beaconSuccess = navigator.sendBeacon(
|
|
15402
15479
|
url,
|
|
@@ -15411,11 +15488,13 @@ class SynchronousHttpService {
|
|
|
15411
15488
|
* Performs a synchronous XMLHttpRequest
|
|
15412
15489
|
* @param {string} url - The URL to send the request to
|
|
15413
15490
|
* @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
|
|
15491
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
15414
15492
|
* @return {ResultObject} - The result of the request
|
|
15415
15493
|
* @private
|
|
15416
15494
|
*/
|
|
15417
|
-
_performSyncXHR(url, params) {
|
|
15418
|
-
const
|
|
15495
|
+
_performSyncXHR(url, params, metadata) {
|
|
15496
|
+
const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
|
|
15497
|
+
const requestPayload = handledPayload ?? params;
|
|
15419
15498
|
const { body, contentType } = this._prepareRequestBody(requestPayload);
|
|
15420
15499
|
const xhr = new XMLHttpRequest();
|
|
15421
15500
|
xhr.open("POST", url, false);
|
|
@@ -15494,9 +15573,15 @@ class ValidationService {
|
|
|
15494
15573
|
* @param {number} invalidTypeCode - The error code for invalid type
|
|
15495
15574
|
* @param {number} invalidRangeCode - The error code for invalid range
|
|
15496
15575
|
* @param {typeof BaseScormValidationError} errorClass - The error class to use for validation errors
|
|
15576
|
+
* @param {boolean} allowEmptyString - When true, an empty string is accepted (clears the value).
|
|
15577
|
+
* SCORM 1.2 score elements may be blank per the ADL 1.2 CTS
|
|
15578
|
+
* (DataModelValidator.checkScoreDecimal treats blank as valid).
|
|
15497
15579
|
* @return {boolean} - True if validation passes, throws an error otherwise
|
|
15498
15580
|
*/
|
|
15499
|
-
validateScore(CMIElement, value, decimalRegex, scoreRange, invalidTypeCode, invalidRangeCode, errorClass) {
|
|
15581
|
+
validateScore(CMIElement, value, decimalRegex, scoreRange, invalidTypeCode, invalidRangeCode, errorClass, allowEmptyString = false) {
|
|
15582
|
+
if (allowEmptyString && value === "") {
|
|
15583
|
+
return true;
|
|
15584
|
+
}
|
|
15500
15585
|
return checkValidFormat(CMIElement, value, decimalRegex, invalidTypeCode, errorClass) && (!scoreRange || checkValidRange(CMIElement, value, scoreRange, invalidRangeCode, errorClass));
|
|
15501
15586
|
}
|
|
15502
15587
|
/**
|
|
@@ -15570,6 +15655,21 @@ class BaseAPI {
|
|
|
15570
15655
|
_offlineStorageService;
|
|
15571
15656
|
_cmiValueAccessService;
|
|
15572
15657
|
_courseId = "";
|
|
15658
|
+
_pendingCommitCount = 0;
|
|
15659
|
+
/**
|
|
15660
|
+
* Monotonic sequence for commits captured by this API instance. It is
|
|
15661
|
+
* intentionally not reset by reset().
|
|
15662
|
+
*/
|
|
15663
|
+
_commitSequence = 0;
|
|
15664
|
+
_commitSettleWaiters = [];
|
|
15665
|
+
/**
|
|
15666
|
+
* Canonical paths of every CMI element that has been explicitly assigned a
|
|
15667
|
+
* value via SetValue / loadFromJSON (both funnel through _commonSetCMIValue).
|
|
15668
|
+
* Used by standards that must tell "implemented but never set" apart from a
|
|
15669
|
+
* legitimately empty value when answering GetValue (SCORM 2004 error 403).
|
|
15670
|
+
* Cleared on reset so a fresh SCO attempt starts with nothing "set".
|
|
15671
|
+
*/
|
|
15672
|
+
_setCMIElements = /* @__PURE__ */ new Set();
|
|
15573
15673
|
/**
|
|
15574
15674
|
* Constructor for Base API class. Sets some shared API fields, as well as
|
|
15575
15675
|
* sets up options for the API.
|
|
@@ -15755,6 +15855,7 @@ class BaseAPI {
|
|
|
15755
15855
|
this.lastErrorCode = "0";
|
|
15756
15856
|
this._eventService.reset();
|
|
15757
15857
|
this.startingData = {};
|
|
15858
|
+
this._setCMIElements.clear();
|
|
15758
15859
|
if (this._offlineStorageService) {
|
|
15759
15860
|
this._offlineStorageService.updateSettings(this.settings);
|
|
15760
15861
|
if (settings?.courseId) {
|
|
@@ -15839,6 +15940,52 @@ class BaseAPI {
|
|
|
15839
15940
|
this._loggingService?.setLogHandler(settings.onLogMessage);
|
|
15840
15941
|
}
|
|
15841
15942
|
}
|
|
15943
|
+
/**
|
|
15944
|
+
* Gets the number of captured commit requests that have not yet settled.
|
|
15945
|
+
*
|
|
15946
|
+
* @return {number} The number of in-flight commits
|
|
15947
|
+
*/
|
|
15948
|
+
get pendingCommitCount() {
|
|
15949
|
+
return this._pendingCommitCount;
|
|
15950
|
+
}
|
|
15951
|
+
/**
|
|
15952
|
+
* Resolves when all currently in-flight commits have settled. A timeout is
|
|
15953
|
+
* best-effort: the promise resolves when it elapses even if commits remain,
|
|
15954
|
+
* and callers can inspect pendingCommitCount afterward to detect that case.
|
|
15955
|
+
*
|
|
15956
|
+
* @param {Object} [options] - Settle options
|
|
15957
|
+
* @param {number} [options.timeoutMs] - Maximum time to wait in milliseconds
|
|
15958
|
+
* @return {Promise<void>} A promise that resolves after the drain or timeout
|
|
15959
|
+
*/
|
|
15960
|
+
whenCommitsSettled(options) {
|
|
15961
|
+
if (this._pendingCommitCount === 0) {
|
|
15962
|
+
return Promise.resolve();
|
|
15963
|
+
}
|
|
15964
|
+
return new Promise((resolve) => {
|
|
15965
|
+
const waiter = { resolve };
|
|
15966
|
+
if (options?.timeoutMs !== void 0) {
|
|
15967
|
+
waiter.timeoutId = setTimeout(() => {
|
|
15968
|
+
const waiterIndex = this._commitSettleWaiters.indexOf(waiter);
|
|
15969
|
+
if (waiterIndex === -1) {
|
|
15970
|
+
return;
|
|
15971
|
+
}
|
|
15972
|
+
this._commitSettleWaiters.splice(waiterIndex, 1);
|
|
15973
|
+
resolve();
|
|
15974
|
+
}, options.timeoutMs);
|
|
15975
|
+
}
|
|
15976
|
+
this._commitSettleWaiters.push(waiter);
|
|
15977
|
+
});
|
|
15978
|
+
}
|
|
15979
|
+
/** Resolve and clear every waiter after the pending count reaches zero. */
|
|
15980
|
+
_flushCommitSettleWaiters() {
|
|
15981
|
+
const waiters = this._commitSettleWaiters.splice(0);
|
|
15982
|
+
for (const waiter of waiters) {
|
|
15983
|
+
if (waiter.timeoutId !== void 0) {
|
|
15984
|
+
clearTimeout(waiter.timeoutId);
|
|
15985
|
+
}
|
|
15986
|
+
waiter.resolve();
|
|
15987
|
+
}
|
|
15988
|
+
}
|
|
15842
15989
|
/**
|
|
15843
15990
|
* Terminates the current run of the API
|
|
15844
15991
|
* @param {string} callbackName
|
|
@@ -15859,7 +16006,7 @@ class BaseAPI {
|
|
|
15859
16006
|
} else {
|
|
15860
16007
|
stateCheckPassed = true;
|
|
15861
16008
|
this.processListeners("BeforeTerminate");
|
|
15862
|
-
const result = this.storeData(true);
|
|
16009
|
+
const result = this.storeData(true, "terminate");
|
|
15863
16010
|
if ((result.errorCode ?? 0) > 0) {
|
|
15864
16011
|
if (result.errorMessage) {
|
|
15865
16012
|
this.apiLog(
|
|
@@ -15911,6 +16058,9 @@ class BaseAPI {
|
|
|
15911
16058
|
} catch (e) {
|
|
15912
16059
|
returnValue = this.handleValueAccessException(CMIElement, e, returnValue);
|
|
15913
16060
|
}
|
|
16061
|
+
if (this.lastErrorCode === "0") {
|
|
16062
|
+
this.checkUninitializedGet(CMIElement, returnValue);
|
|
16063
|
+
}
|
|
15914
16064
|
this.processListeners(callbackName, CMIElement);
|
|
15915
16065
|
}
|
|
15916
16066
|
this.apiLog(callbackName, ": returned: " + returnValue, LogLevelEnum.INFO, CMIElement);
|
|
@@ -15920,6 +16070,10 @@ class BaseAPI {
|
|
|
15920
16070
|
if (this.lastErrorCode === "0") {
|
|
15921
16071
|
this.clearSCORMError(returnValue);
|
|
15922
16072
|
}
|
|
16073
|
+
const rawReturn = returnValue;
|
|
16074
|
+
if (typeof rawReturn === "number" || typeof rawReturn === "boolean") {
|
|
16075
|
+
return String(rawReturn);
|
|
16076
|
+
}
|
|
15923
16077
|
return returnValue;
|
|
15924
16078
|
}
|
|
15925
16079
|
/**
|
|
@@ -15972,9 +16126,10 @@ class BaseAPI {
|
|
|
15972
16126
|
* Orders LMS to store all content parameters
|
|
15973
16127
|
* @param {string} callbackName
|
|
15974
16128
|
* @param {boolean} checkTerminated
|
|
16129
|
+
* @param {CommitTrigger} trigger - What initiated the commit
|
|
15975
16130
|
* @return {string}
|
|
15976
16131
|
*/
|
|
15977
|
-
commit(callbackName, checkTerminated = false) {
|
|
16132
|
+
commit(callbackName, checkTerminated = false, trigger = "manual") {
|
|
15978
16133
|
this.clearScheduledCommit();
|
|
15979
16134
|
let returnValue = global_constants.SCORM_TRUE;
|
|
15980
16135
|
if (this.isNotInitialized()) {
|
|
@@ -15986,8 +16141,9 @@ class BaseAPI {
|
|
|
15986
16141
|
this.throwSCORMError("api", errorCode);
|
|
15987
16142
|
if (errorCode === 143) returnValue = global_constants.SCORM_FALSE;
|
|
15988
16143
|
} else {
|
|
15989
|
-
const result = this.storeData(false);
|
|
15990
|
-
|
|
16144
|
+
const result = this.storeData(false, trigger);
|
|
16145
|
+
const errorCode = result.errorCode ?? 0;
|
|
16146
|
+
if (errorCode > 0) {
|
|
15991
16147
|
if (result.errorMessage) {
|
|
15992
16148
|
this.apiLog(
|
|
15993
16149
|
"commit",
|
|
@@ -16002,12 +16158,12 @@ class BaseAPI {
|
|
|
16002
16158
|
LogLevelEnum.DEBUG
|
|
16003
16159
|
);
|
|
16004
16160
|
}
|
|
16005
|
-
this.throwSCORMError("api",
|
|
16161
|
+
this.throwSCORMError("api", errorCode);
|
|
16006
16162
|
}
|
|
16007
16163
|
const resultValue = result?.result ?? global_constants.SCORM_FALSE;
|
|
16008
16164
|
returnValue = typeof resultValue === "boolean" ? String(resultValue) : resultValue;
|
|
16009
16165
|
this.apiLog(callbackName, " Result: " + returnValue, LogLevelEnum.DEBUG, "HttpRequest");
|
|
16010
|
-
if (checkTerminated) this.lastErrorCode = "0";
|
|
16166
|
+
if (checkTerminated && errorCode === 0) this.lastErrorCode = "0";
|
|
16011
16167
|
this.processListeners(callbackName);
|
|
16012
16168
|
if (this.settings.enableOfflineSupport && this._offlineStorageService && this._offlineStorageService.isDeviceOnline() && this._courseId) {
|
|
16013
16169
|
this._offlineStorageService.hasPendingOfflineData(this._courseId).then((hasPendingData) => {
|
|
@@ -16216,7 +16372,16 @@ class BaseAPI {
|
|
|
16216
16372
|
* @return {string}
|
|
16217
16373
|
*/
|
|
16218
16374
|
_commonSetCMIValue(methodName, scorm2004, CMIElement, value) {
|
|
16219
|
-
|
|
16375
|
+
const result = this._cmiValueAccessService.setCMIValue(
|
|
16376
|
+
methodName,
|
|
16377
|
+
scorm2004,
|
|
16378
|
+
CMIElement,
|
|
16379
|
+
value
|
|
16380
|
+
);
|
|
16381
|
+
if (result === global_constants.SCORM_TRUE) {
|
|
16382
|
+
this._setCMIElements.add(CMIElement);
|
|
16383
|
+
}
|
|
16384
|
+
return result;
|
|
16220
16385
|
}
|
|
16221
16386
|
/**
|
|
16222
16387
|
* Gets a value from the CMI Object.
|
|
@@ -16230,6 +16395,18 @@ class BaseAPI {
|
|
|
16230
16395
|
_commonGetCMIValue(methodName, scorm2004, CMIElement) {
|
|
16231
16396
|
return this._cmiValueAccessService.getCMIValue(methodName, scorm2004, CMIElement);
|
|
16232
16397
|
}
|
|
16398
|
+
/**
|
|
16399
|
+
* Hook invoked by getValue after a successful resolution. Standards that must
|
|
16400
|
+
* distinguish "implemented but never set, no default value" from a
|
|
16401
|
+
* legitimately empty value override this to raise VALUE_NOT_INITIALIZED.
|
|
16402
|
+
* Default is a no-op, so SCORM 1.2 / AICC keep returning "" with code 0.
|
|
16403
|
+
*
|
|
16404
|
+
* @param {string} _CMIElement - the element that was read
|
|
16405
|
+
* @param {any} _returnValue - the value getCMIValue resolved
|
|
16406
|
+
* @protected
|
|
16407
|
+
*/
|
|
16408
|
+
checkUninitializedGet(_CMIElement, _returnValue) {
|
|
16409
|
+
}
|
|
16233
16410
|
/**
|
|
16234
16411
|
* Returns true if the API's current state is STATE_INITIALIZED
|
|
16235
16412
|
*
|
|
@@ -16312,9 +16489,14 @@ class BaseAPI {
|
|
|
16312
16489
|
* @param {string} functionName - The name of the function/event that occurred
|
|
16313
16490
|
* @param {string} CMIElement - Optional CMI element involved in the event
|
|
16314
16491
|
* @param {any} value - Optional value associated with the event
|
|
16492
|
+
* @param {CommitEventContext} context - Optional context for commit lifecycle events
|
|
16315
16493
|
*/
|
|
16316
|
-
processListeners(functionName, CMIElement, value) {
|
|
16317
|
-
|
|
16494
|
+
processListeners(functionName, CMIElement, value, context) {
|
|
16495
|
+
if (context !== void 0) {
|
|
16496
|
+
this._eventService.processListeners(functionName, CMIElement, value, context);
|
|
16497
|
+
} else {
|
|
16498
|
+
this._eventService.processListeners(functionName, CMIElement, value);
|
|
16499
|
+
}
|
|
16318
16500
|
}
|
|
16319
16501
|
/**
|
|
16320
16502
|
* Throws a SCORM error with the specified error number and optional message.
|
|
@@ -16447,36 +16629,106 @@ class BaseAPI {
|
|
|
16447
16629
|
* @param {string} url - The URL to send the request to
|
|
16448
16630
|
* @param {CommitObject | StringKeyMap | Array<any>} params - The parameters to send
|
|
16449
16631
|
* @param {boolean} immediate - Whether to send the request immediately without waiting
|
|
16632
|
+
* @param {CommitTrigger} [trigger] - What initiated the commit
|
|
16450
16633
|
* @returns {ResultObject} - The result of the request
|
|
16451
16634
|
*/
|
|
16452
|
-
processHttpRequest(url, params, immediate = false) {
|
|
16453
|
-
|
|
16454
|
-
|
|
16455
|
-
|
|
16456
|
-
|
|
16457
|
-
|
|
16458
|
-
)
|
|
16459
|
-
|
|
16460
|
-
|
|
16461
|
-
|
|
16635
|
+
processHttpRequest(url, params, immediate = false, trigger) {
|
|
16636
|
+
const sequence = ++this._commitSequence;
|
|
16637
|
+
this._pendingCommitCount += 1;
|
|
16638
|
+
let settled = false;
|
|
16639
|
+
let completionDeferred = false;
|
|
16640
|
+
const settle = () => {
|
|
16641
|
+
if (settled) {
|
|
16642
|
+
return;
|
|
16643
|
+
}
|
|
16644
|
+
settled = true;
|
|
16645
|
+
this._pendingCommitCount -= 1;
|
|
16646
|
+
if (this._pendingCommitCount === 0) {
|
|
16647
|
+
this._flushCommitSettleWaiters();
|
|
16648
|
+
}
|
|
16649
|
+
};
|
|
16650
|
+
try {
|
|
16651
|
+
const resolvedTrigger = trigger ?? (immediate ? "terminate" : "manual");
|
|
16652
|
+
let finalParams = params;
|
|
16653
|
+
if (immediate && this.settings.terminateCommitPayloadField) {
|
|
16654
|
+
const field = this.settings.terminateCommitPayloadField;
|
|
16655
|
+
if (Array.isArray(finalParams)) {
|
|
16656
|
+
finalParams = [...finalParams, `${encodeURIComponent(field)}=true`];
|
|
16657
|
+
} else if (finalParams && typeof finalParams === "object") {
|
|
16658
|
+
finalParams = { ...finalParams, [field]: true };
|
|
16659
|
+
}
|
|
16660
|
+
}
|
|
16661
|
+
if (this.settings.includeCommitSequence === true) {
|
|
16662
|
+
if (Array.isArray(finalParams)) {
|
|
16663
|
+
finalParams = [...finalParams, `commitSequence=${sequence}`];
|
|
16664
|
+
} else if (finalParams && typeof finalParams === "object") {
|
|
16665
|
+
finalParams = { ...finalParams, commitSequence: sequence };
|
|
16666
|
+
}
|
|
16667
|
+
}
|
|
16668
|
+
const finalUrl = immediate && this.settings.terminateCommitParam ? appendQueryParam(url, this.settings.terminateCommitParam, "true") : url;
|
|
16669
|
+
const metadata = {
|
|
16670
|
+
isTerminateCommit: immediate,
|
|
16671
|
+
trigger: resolvedTrigger,
|
|
16672
|
+
sequence
|
|
16673
|
+
};
|
|
16674
|
+
const context = {
|
|
16675
|
+
url: finalUrl,
|
|
16676
|
+
trigger: resolvedTrigger,
|
|
16677
|
+
isTerminateCommit: immediate,
|
|
16678
|
+
sequence
|
|
16679
|
+
};
|
|
16680
|
+
if (this.settings.enableOfflineSupport && this._offlineStorageService && !this._offlineStorageService.isDeviceOnline() && this._courseId) {
|
|
16462
16681
|
this.apiLog(
|
|
16463
16682
|
"processHttpRequest",
|
|
16464
|
-
"
|
|
16465
|
-
LogLevelEnum.
|
|
16683
|
+
"Device is offline, storing data locally",
|
|
16684
|
+
LogLevelEnum.INFO
|
|
16466
16685
|
);
|
|
16467
|
-
|
|
16468
|
-
|
|
16469
|
-
|
|
16470
|
-
|
|
16686
|
+
if (finalParams && typeof finalParams === "object" && "cmi" in finalParams) {
|
|
16687
|
+
return this._offlineStorageService.storeOffline(
|
|
16688
|
+
this._courseId,
|
|
16689
|
+
finalParams,
|
|
16690
|
+
{ isTerminateCommit: immediate, sequence }
|
|
16691
|
+
);
|
|
16692
|
+
} else {
|
|
16693
|
+
this.apiLog(
|
|
16694
|
+
"processHttpRequest",
|
|
16695
|
+
"Invalid commit data format for offline storage",
|
|
16696
|
+
LogLevelEnum.ERROR
|
|
16697
|
+
);
|
|
16698
|
+
return {
|
|
16699
|
+
result: global_constants.SCORM_FALSE,
|
|
16700
|
+
errorCode: this._error_codes.GENERAL ?? 101
|
|
16701
|
+
};
|
|
16702
|
+
}
|
|
16703
|
+
}
|
|
16704
|
+
const apiLog = (functionName, message, level, element) => this.apiLog(functionName, message, level, element);
|
|
16705
|
+
const processListeners = (functionName, CMIElement, value) => {
|
|
16706
|
+
if (functionName === "CommitSuccess" || functionName === "CommitError") {
|
|
16707
|
+
if (functionName === "CommitError" && typeof value === "number") {
|
|
16708
|
+
context.errorCode = value;
|
|
16709
|
+
}
|
|
16710
|
+
settle();
|
|
16711
|
+
this.processListeners(functionName, CMIElement, value, context);
|
|
16712
|
+
} else {
|
|
16713
|
+
this.processListeners(functionName, CMIElement, value);
|
|
16714
|
+
}
|
|
16715
|
+
};
|
|
16716
|
+
const result = this._httpService.processHttpRequest(
|
|
16717
|
+
finalUrl,
|
|
16718
|
+
finalParams,
|
|
16719
|
+
immediate,
|
|
16720
|
+
apiLog,
|
|
16721
|
+
processListeners,
|
|
16722
|
+
metadata,
|
|
16723
|
+
settle
|
|
16724
|
+
);
|
|
16725
|
+
completionDeferred = this._httpService.reportsRequestCompletion === true;
|
|
16726
|
+
return result;
|
|
16727
|
+
} finally {
|
|
16728
|
+
if (!completionDeferred) {
|
|
16729
|
+
settle();
|
|
16471
16730
|
}
|
|
16472
16731
|
}
|
|
16473
|
-
return this._httpService.processHttpRequest(
|
|
16474
|
-
url,
|
|
16475
|
-
params,
|
|
16476
|
-
immediate,
|
|
16477
|
-
(functionName, message, level, element) => this.apiLog(functionName, message, level, element),
|
|
16478
|
-
(functionName, CMIElement, value) => this.processListeners(functionName, CMIElement, value)
|
|
16479
|
-
);
|
|
16480
16732
|
}
|
|
16481
16733
|
/**
|
|
16482
16734
|
* Schedules a commit operation to occur after a specified delay.
|
|
@@ -16636,6 +16888,11 @@ class CMIScore extends BaseCMI {
|
|
|
16636
16888
|
__invalid_range_code;
|
|
16637
16889
|
__decimal_regex;
|
|
16638
16890
|
__error_class;
|
|
16891
|
+
/**
|
|
16892
|
+
* When true, an empty string is a valid value (clears the element). SCORM 1.2
|
|
16893
|
+
* score elements may be blank per the ADL 1.2 CTS; SCORM 2004 leaves this off.
|
|
16894
|
+
*/
|
|
16895
|
+
__allow_empty_string;
|
|
16639
16896
|
_raw = "";
|
|
16640
16897
|
_min = "";
|
|
16641
16898
|
_max;
|
|
@@ -16670,6 +16927,7 @@ class CMIScore extends BaseCMI {
|
|
|
16670
16927
|
this.__invalid_range_code = params.invalidRangeCode || scorm12_errors.VALUE_OUT_OF_RANGE;
|
|
16671
16928
|
this.__decimal_regex = params.decimalRegex || scorm12_regex.CMIDecimal;
|
|
16672
16929
|
this.__error_class = params.errorClass;
|
|
16930
|
+
this.__allow_empty_string = params.allowEmptyString ?? false;
|
|
16673
16931
|
}
|
|
16674
16932
|
/**
|
|
16675
16933
|
* Called when the API has been reset
|
|
@@ -16716,7 +16974,8 @@ class CMIScore extends BaseCMI {
|
|
|
16716
16974
|
this.__score_range,
|
|
16717
16975
|
this.__invalid_type_code,
|
|
16718
16976
|
this.__invalid_range_code,
|
|
16719
|
-
this.__error_class
|
|
16977
|
+
this.__error_class,
|
|
16978
|
+
this.__allow_empty_string
|
|
16720
16979
|
)) {
|
|
16721
16980
|
this._raw = raw;
|
|
16722
16981
|
}
|
|
@@ -16740,7 +16999,8 @@ class CMIScore extends BaseCMI {
|
|
|
16740
16999
|
this.__score_range,
|
|
16741
17000
|
this.__invalid_type_code,
|
|
16742
17001
|
this.__invalid_range_code,
|
|
16743
|
-
this.__error_class
|
|
17002
|
+
this.__error_class,
|
|
17003
|
+
this.__allow_empty_string
|
|
16744
17004
|
)) {
|
|
16745
17005
|
this._min = min;
|
|
16746
17006
|
}
|
|
@@ -16764,7 +17024,8 @@ class CMIScore extends BaseCMI {
|
|
|
16764
17024
|
this.__score_range,
|
|
16765
17025
|
this.__invalid_type_code,
|
|
16766
17026
|
this.__invalid_range_code,
|
|
16767
|
-
this.__error_class
|
|
17027
|
+
this.__error_class,
|
|
17028
|
+
this.__allow_empty_string
|
|
16768
17029
|
)) {
|
|
16769
17030
|
this._max = max;
|
|
16770
17031
|
}
|
|
@@ -16821,7 +17082,9 @@ class CMICore extends BaseCMI {
|
|
|
16821
17082
|
invalidErrorCode: scorm12_errors.INVALID_SET_VALUE,
|
|
16822
17083
|
invalidTypeCode: scorm12_errors.TYPE_MISMATCH,
|
|
16823
17084
|
invalidRangeCode: scorm12_errors.VALUE_OUT_OF_RANGE,
|
|
16824
|
-
errorClass: Scorm12ValidationError
|
|
17085
|
+
errorClass: Scorm12ValidationError,
|
|
17086
|
+
// SCORM 1.2 score elements may be set blank (clears the value), per ADL 1.2 CTS.
|
|
17087
|
+
allowEmptyString: true
|
|
16825
17088
|
});
|
|
16826
17089
|
}
|
|
16827
17090
|
score;
|
|
@@ -17251,7 +17514,9 @@ let CMIObjectivesObject$1 = class CMIObjectivesObject extends BaseCMI {
|
|
|
17251
17514
|
invalidErrorCode: scorm12_errors.INVALID_SET_VALUE,
|
|
17252
17515
|
invalidTypeCode: scorm12_errors.TYPE_MISMATCH,
|
|
17253
17516
|
invalidRangeCode: scorm12_errors.VALUE_OUT_OF_RANGE,
|
|
17254
|
-
errorClass: Scorm12ValidationError
|
|
17517
|
+
errorClass: Scorm12ValidationError,
|
|
17518
|
+
// SCORM 1.2 score elements may be set blank (clears the value), per ADL 1.2 CTS.
|
|
17519
|
+
allowEmptyString: true
|
|
17255
17520
|
});
|
|
17256
17521
|
}
|
|
17257
17522
|
score;
|
|
@@ -18721,16 +18986,17 @@ class Scorm12API extends BaseAPI {
|
|
|
18721
18986
|
* Attempts to store the data to the LMS
|
|
18722
18987
|
*
|
|
18723
18988
|
* @param {boolean} terminateCommit
|
|
18989
|
+
* @param {CommitTrigger} [trigger] - What initiated the commit
|
|
18724
18990
|
* @return {ResultObject}
|
|
18725
18991
|
*/
|
|
18726
|
-
storeData(terminateCommit) {
|
|
18992
|
+
storeData(terminateCommit, trigger) {
|
|
18727
18993
|
if (terminateCommit) {
|
|
18728
18994
|
const originalStatus = this.cmi.core.lesson_status;
|
|
18729
18995
|
if (this.cmi.core.lesson_mode === "browse") {
|
|
18730
18996
|
const startingStatus = this.startingData?.cmi?.core?.lesson_status || "";
|
|
18731
18997
|
if (startingStatus === "" && originalStatus === "not attempted") {
|
|
18732
18998
|
this.cmi.core.lesson_status = "browsed";
|
|
18733
|
-
return this.processCommitData(terminateCommit);
|
|
18999
|
+
return this.processCommitData(terminateCommit, trigger);
|
|
18734
19000
|
}
|
|
18735
19001
|
}
|
|
18736
19002
|
if (!this.cmi.core.lesson_status || !this.statusSetByModule && this.cmi.core.lesson_status === "not attempted") {
|
|
@@ -18759,12 +19025,17 @@ class Scorm12API extends BaseAPI {
|
|
|
18759
19025
|
}
|
|
18760
19026
|
}
|
|
18761
19027
|
}
|
|
18762
|
-
return this.processCommitData(terminateCommit);
|
|
19028
|
+
return this.processCommitData(terminateCommit, trigger);
|
|
18763
19029
|
}
|
|
18764
|
-
processCommitData(terminateCommit) {
|
|
19030
|
+
processCommitData(terminateCommit, trigger) {
|
|
18765
19031
|
const commitObject = this.getCommitObject(terminateCommit);
|
|
18766
19032
|
if (typeof this.settings.lmsCommitUrl === "string") {
|
|
18767
|
-
return this.processHttpRequest(
|
|
19033
|
+
return this.processHttpRequest(
|
|
19034
|
+
this.settings.lmsCommitUrl,
|
|
19035
|
+
commitObject,
|
|
19036
|
+
terminateCommit,
|
|
19037
|
+
trigger
|
|
19038
|
+
);
|
|
18768
19039
|
} else {
|
|
18769
19040
|
return {
|
|
18770
19041
|
result: global_constants.SCORM_TRUE,
|
|
@@ -18930,6 +19201,49 @@ class CMILearnerPreference extends BaseCMI {
|
|
|
18930
19201
|
}
|
|
18931
19202
|
}
|
|
18932
19203
|
|
|
19204
|
+
function stripBrackets(delim) {
|
|
19205
|
+
return delim.replace(/[[\]]/g, "");
|
|
19206
|
+
}
|
|
19207
|
+
function escapeRegex(s) {
|
|
19208
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
19209
|
+
}
|
|
19210
|
+
function splitDelimited(value, bracketed) {
|
|
19211
|
+
if (!bracketed) {
|
|
19212
|
+
return [value];
|
|
19213
|
+
}
|
|
19214
|
+
if (value.includes(bracketed)) {
|
|
19215
|
+
return value.split(bracketed);
|
|
19216
|
+
}
|
|
19217
|
+
const bare = stripBrackets(bracketed);
|
|
19218
|
+
if (!bare) {
|
|
19219
|
+
return [value];
|
|
19220
|
+
}
|
|
19221
|
+
const splitRe = new RegExp(`(?<!\\\\)${escapeRegex(bare)}`, "g");
|
|
19222
|
+
const unescapeRe = new RegExp(`\\\\${escapeRegex(bare)}`, "g");
|
|
19223
|
+
return value.split(splitRe).map((part) => part.replace(unescapeRe, bare));
|
|
19224
|
+
}
|
|
19225
|
+
function splitFirstDelimited(value, bracketed) {
|
|
19226
|
+
if (!bracketed) {
|
|
19227
|
+
return [value];
|
|
19228
|
+
}
|
|
19229
|
+
if (value.includes(bracketed)) {
|
|
19230
|
+
const idx = value.indexOf(bracketed);
|
|
19231
|
+
return [value.slice(0, idx), value.slice(idx + bracketed.length)];
|
|
19232
|
+
}
|
|
19233
|
+
const bare = stripBrackets(bracketed);
|
|
19234
|
+
if (!bare) {
|
|
19235
|
+
return [value];
|
|
19236
|
+
}
|
|
19237
|
+
const splitRe = new RegExp(`(?<!\\\\)${escapeRegex(bare)}`);
|
|
19238
|
+
const unescapeRe = new RegExp(`\\\\${escapeRegex(bare)}`, "g");
|
|
19239
|
+
const parts = value.split(splitRe);
|
|
19240
|
+
const first = (parts[0] ?? "").replace(unescapeRe, bare);
|
|
19241
|
+
if (parts.length === 1) {
|
|
19242
|
+
return [first];
|
|
19243
|
+
}
|
|
19244
|
+
return [first, parts.slice(1).join(bare).replace(unescapeRe, bare)];
|
|
19245
|
+
}
|
|
19246
|
+
|
|
18933
19247
|
class CMIInteractions extends CMIArray {
|
|
18934
19248
|
/**
|
|
18935
19249
|
* Constructor for `cmi.interactions` Array
|
|
@@ -19139,8 +19453,7 @@ class CMIInteractionsObject extends BaseCMI {
|
|
|
19139
19453
|
const response_type = LearnerResponses[this.type];
|
|
19140
19454
|
if (response_type) {
|
|
19141
19455
|
if (response_type?.delimiter) {
|
|
19142
|
-
|
|
19143
|
-
nodes = learner_response.split(delimiter);
|
|
19456
|
+
nodes = splitDelimited(learner_response, response_type.delimiter);
|
|
19144
19457
|
} else {
|
|
19145
19458
|
nodes[0] = learner_response;
|
|
19146
19459
|
}
|
|
@@ -19148,10 +19461,10 @@ class CMIInteractionsObject extends BaseCMI {
|
|
|
19148
19461
|
const formatRegex = new RegExp(response_type.format);
|
|
19149
19462
|
for (let i = 0; i < nodes.length; i++) {
|
|
19150
19463
|
if (response_type?.delimiter2) {
|
|
19151
|
-
const
|
|
19152
|
-
const values =
|
|
19464
|
+
const node = nodes[i] ?? "";
|
|
19465
|
+
const values = this.type === "performance" ? splitFirstDelimited(node, response_type.delimiter2) : splitDelimited(node, response_type.delimiter2);
|
|
19153
19466
|
if (values?.length === 2) {
|
|
19154
|
-
if (this.type === "performance" &&
|
|
19467
|
+
if (this.type === "performance" && values[0] === "" && values[1] === "") {
|
|
19155
19468
|
throw new Scorm2004ValidationError(
|
|
19156
19469
|
this._cmi_element + ".learner_response",
|
|
19157
19470
|
scorm2004_errors.TYPE_MISMATCH
|
|
@@ -19370,30 +19683,13 @@ class CMIInteractionsObjectivesObject extends BaseCMI {
|
|
|
19370
19683
|
return result;
|
|
19371
19684
|
}
|
|
19372
19685
|
}
|
|
19373
|
-
|
|
19374
|
-
|
|
19375
|
-
|
|
19376
|
-
|
|
19377
|
-
|
|
19378
|
-
}
|
|
19379
|
-
function splitUnescaped(text, delim) {
|
|
19380
|
-
const reDelim = escapeRegex(delim);
|
|
19381
|
-
const splitRe = new RegExp(`(?<!\\\\)${reDelim}`, "g");
|
|
19382
|
-
const unescapeRe = new RegExp(`\\\\${reDelim}`, "g");
|
|
19383
|
-
return text.split(splitRe).map((part) => part.replace(unescapeRe, delim));
|
|
19384
|
-
}
|
|
19385
|
-
function splitFirstUnescaped(text, delim) {
|
|
19386
|
-
const reDelim = escapeRegex(delim);
|
|
19387
|
-
const splitRe = new RegExp(`(?<!\\\\)${reDelim}`);
|
|
19388
|
-
const unescapeRe = new RegExp(`\\\\${reDelim}`, "g");
|
|
19389
|
-
const parts = text.split(splitRe);
|
|
19390
|
-
const firstPart = parts[0] ?? "";
|
|
19391
|
-
if (parts.length === 1) {
|
|
19392
|
-
return [firstPart.replace(unescapeRe, delim)];
|
|
19686
|
+
const RESPONSE_PREFIX_RE = /^\{(?:lang|case_matters|order_matters)=[^}]+\}/;
|
|
19687
|
+
function stripResponsePrefixes(node) {
|
|
19688
|
+
let result = node;
|
|
19689
|
+
while (RESPONSE_PREFIX_RE.test(result)) {
|
|
19690
|
+
result = result.replace(RESPONSE_PREFIX_RE, "");
|
|
19393
19691
|
}
|
|
19394
|
-
|
|
19395
|
-
const part2 = parts.slice(1).join(delim).replace(unescapeRe, delim);
|
|
19396
|
-
return [part1, part2];
|
|
19692
|
+
return result;
|
|
19397
19693
|
}
|
|
19398
19694
|
function validatePattern(type, pattern, responseDef) {
|
|
19399
19695
|
if (pattern.trim() !== pattern) {
|
|
@@ -19402,8 +19698,7 @@ function validatePattern(type, pattern, responseDef) {
|
|
|
19402
19698
|
scorm2004_errors.TYPE_MISMATCH
|
|
19403
19699
|
);
|
|
19404
19700
|
}
|
|
19405
|
-
const
|
|
19406
|
-
const rawNodes = subDelim1 ? splitUnescaped(pattern, subDelim1) : [pattern];
|
|
19701
|
+
const rawNodes = responseDef.delimiter ? splitDelimited(pattern, responseDef.delimiter) : [pattern];
|
|
19407
19702
|
for (const raw of rawNodes) {
|
|
19408
19703
|
if (raw.trim() !== raw) {
|
|
19409
19704
|
throw new Scorm2004ValidationError(
|
|
@@ -19415,20 +19710,14 @@ function validatePattern(type, pattern, responseDef) {
|
|
|
19415
19710
|
if (type === "fill-in" && pattern === "") {
|
|
19416
19711
|
return;
|
|
19417
19712
|
}
|
|
19418
|
-
const
|
|
19419
|
-
let nodes;
|
|
19420
|
-
if (delim1) {
|
|
19421
|
-
nodes = splitUnescaped(pattern, delim1);
|
|
19422
|
-
} else {
|
|
19423
|
-
nodes = [pattern];
|
|
19424
|
-
}
|
|
19713
|
+
const nodes = responseDef.delimiter ? splitDelimited(pattern, responseDef.delimiter) : [pattern];
|
|
19425
19714
|
if (!responseDef.delimiter && pattern.includes(",")) {
|
|
19426
19715
|
throw new Scorm2004ValidationError(
|
|
19427
19716
|
"cmi.interactions.n.correct_responses.n.pattern",
|
|
19428
19717
|
scorm2004_errors.TYPE_MISMATCH
|
|
19429
19718
|
);
|
|
19430
19719
|
}
|
|
19431
|
-
if (responseDef.unique || responseDef.duplicate === false) {
|
|
19720
|
+
if (type !== "numeric" && (responseDef.unique || responseDef.duplicate === false)) {
|
|
19432
19721
|
const seen = new Set(nodes);
|
|
19433
19722
|
if (seen.size !== nodes.length) {
|
|
19434
19723
|
throw new Scorm2004ValidationError(
|
|
@@ -19460,8 +19749,7 @@ function validatePattern(type, pattern, responseDef) {
|
|
|
19460
19749
|
scorm2004_errors.TYPE_MISMATCH
|
|
19461
19750
|
);
|
|
19462
19751
|
}
|
|
19463
|
-
const
|
|
19464
|
-
const parts = value.split(new RegExp(`(?<!\\\\)${escapeRegex(delim)}`, "g")).map((n) => n.replace(new RegExp(`\\\\${escapeRegex(delim)}`, "g"), delim));
|
|
19752
|
+
const parts = splitDelimited(value, delimBracketed);
|
|
19465
19753
|
if (parts.length !== 2 || parts[0] === "" || parts[1] === "") {
|
|
19466
19754
|
throw new Scorm2004ValidationError(
|
|
19467
19755
|
"cmi.interactions.n.correct_responses.n.pattern",
|
|
@@ -19478,15 +19766,17 @@ function validatePattern(type, pattern, responseDef) {
|
|
|
19478
19766
|
for (const node of nodes) {
|
|
19479
19767
|
switch (type) {
|
|
19480
19768
|
case "numeric": {
|
|
19481
|
-
|
|
19482
|
-
|
|
19483
|
-
|
|
19484
|
-
|
|
19485
|
-
|
|
19486
|
-
|
|
19487
|
-
|
|
19769
|
+
if (node === "") {
|
|
19770
|
+
const bracketedRange = nodes.length >= 2 && !!responseDef.delimiter && pattern.includes(responseDef.delimiter);
|
|
19771
|
+
if (!bracketedRange) {
|
|
19772
|
+
throw new Scorm2004ValidationError(
|
|
19773
|
+
"cmi.interactions.n.correct_responses.n.pattern",
|
|
19774
|
+
scorm2004_errors.TYPE_MISMATCH
|
|
19775
|
+
);
|
|
19776
|
+
}
|
|
19777
|
+
break;
|
|
19488
19778
|
}
|
|
19489
|
-
|
|
19779
|
+
checkSingle(node);
|
|
19490
19780
|
break;
|
|
19491
19781
|
}
|
|
19492
19782
|
case "performance": {
|
|
@@ -19497,8 +19787,8 @@ function validatePattern(type, pattern, responseDef) {
|
|
|
19497
19787
|
scorm2004_errors.TYPE_MISMATCH
|
|
19498
19788
|
);
|
|
19499
19789
|
}
|
|
19500
|
-
const
|
|
19501
|
-
const parts =
|
|
19790
|
+
const record = stripResponsePrefixes(node);
|
|
19791
|
+
const parts = splitFirstDelimited(record, delimBracketed);
|
|
19502
19792
|
if (parts.length !== 2) {
|
|
19503
19793
|
throw new Scorm2004ValidationError(
|
|
19504
19794
|
"cmi.interactions.n.correct_responses.n.pattern",
|
|
@@ -19506,7 +19796,7 @@ function validatePattern(type, pattern, responseDef) {
|
|
|
19506
19796
|
);
|
|
19507
19797
|
}
|
|
19508
19798
|
const [part1, part2] = parts;
|
|
19509
|
-
if (part1 === ""
|
|
19799
|
+
if (part1 === "" && part2 === "") {
|
|
19510
19800
|
throw new Scorm2004ValidationError(
|
|
19511
19801
|
"cmi.interactions.n.correct_responses.n.pattern",
|
|
19512
19802
|
scorm2004_errors.TYPE_MISMATCH
|
|
@@ -22385,7 +22675,7 @@ class Scorm2004ResponseValidator {
|
|
|
22385
22675
|
checkValidResponseType(CMIElement, response_type, value, interaction_type) {
|
|
22386
22676
|
let nodes = [];
|
|
22387
22677
|
if (response_type?.delimiter) {
|
|
22388
|
-
nodes = String(value)
|
|
22678
|
+
nodes = splitDelimited(String(value), response_type.delimiter);
|
|
22389
22679
|
} else {
|
|
22390
22680
|
nodes[0] = value;
|
|
22391
22681
|
}
|
|
@@ -22507,22 +22797,30 @@ class Scorm2004ResponseValidator {
|
|
|
22507
22797
|
nodes[i] = this.removeCorrectResponsePrefixes(CMIElement, nodes[i]);
|
|
22508
22798
|
}
|
|
22509
22799
|
if (response?.delimiter2) {
|
|
22510
|
-
const values = nodes[i].
|
|
22800
|
+
const values = interaction_type === "performance" ? splitFirstDelimited(nodes[i], response.delimiter2) : splitDelimited(nodes[i], response.delimiter2);
|
|
22511
22801
|
if (values.length === 2) {
|
|
22512
|
-
|
|
22513
|
-
if (!matches) {
|
|
22802
|
+
if (interaction_type === "performance" && values[0] === "" && values[1] === "") {
|
|
22514
22803
|
this.context.throwSCORMError(
|
|
22515
22804
|
CMIElement,
|
|
22516
22805
|
scorm2004_errors.TYPE_MISMATCH,
|
|
22517
22806
|
`${interaction_type}: ${value}`
|
|
22518
22807
|
);
|
|
22519
22808
|
} else {
|
|
22520
|
-
|
|
22809
|
+
const matches = values[0]?.match(formatRegex);
|
|
22810
|
+
if (!matches) {
|
|
22521
22811
|
this.context.throwSCORMError(
|
|
22522
22812
|
CMIElement,
|
|
22523
22813
|
scorm2004_errors.TYPE_MISMATCH,
|
|
22524
22814
|
`${interaction_type}: ${value}`
|
|
22525
22815
|
);
|
|
22816
|
+
} else {
|
|
22817
|
+
if (!response.format2 || !values[1]?.match(new RegExp(response.format2))) {
|
|
22818
|
+
this.context.throwSCORMError(
|
|
22819
|
+
CMIElement,
|
|
22820
|
+
scorm2004_errors.TYPE_MISMATCH,
|
|
22821
|
+
`${interaction_type}: ${value}`
|
|
22822
|
+
);
|
|
22823
|
+
}
|
|
22526
22824
|
}
|
|
22527
22825
|
}
|
|
22528
22826
|
} else {
|
|
@@ -22533,6 +22831,9 @@ class Scorm2004ResponseValidator {
|
|
|
22533
22831
|
);
|
|
22534
22832
|
}
|
|
22535
22833
|
} else {
|
|
22834
|
+
if (interaction_type === "numeric" && nodes.length > 1 && nodes[i] === "" && !!response.delimiter && String(value).includes(response.delimiter)) {
|
|
22835
|
+
continue;
|
|
22836
|
+
}
|
|
22536
22837
|
const matches = nodes[i].match(formatRegex);
|
|
22537
22838
|
if (!matches && value !== "" || !matches && interaction_type === "true-false") {
|
|
22538
22839
|
this.context.throwSCORMError(
|
|
@@ -22542,7 +22843,7 @@ class Scorm2004ResponseValidator {
|
|
|
22542
22843
|
);
|
|
22543
22844
|
} else {
|
|
22544
22845
|
if (interaction_type === "numeric" && nodes.length > 1) {
|
|
22545
|
-
if (Number(nodes[0]) > Number(nodes[1])) {
|
|
22846
|
+
if (nodes[0] !== "" && nodes[1] !== "" && Number(nodes[0]) > Number(nodes[1])) {
|
|
22546
22847
|
this.context.throwSCORMError(
|
|
22547
22848
|
CMIElement,
|
|
22548
22849
|
scorm2004_errors.TYPE_MISMATCH,
|
|
@@ -24318,6 +24619,35 @@ class Scorm2004DataSerializer {
|
|
|
24318
24619
|
}
|
|
24319
24620
|
}
|
|
24320
24621
|
|
|
24622
|
+
const NO_DEFAULT_2004_ELEMENTS = /* @__PURE__ */ new Set([
|
|
24623
|
+
"cmi.suspend_data",
|
|
24624
|
+
"cmi.location",
|
|
24625
|
+
"cmi.scaled_passing_score",
|
|
24626
|
+
"cmi.max_time_allowed",
|
|
24627
|
+
"cmi.completion_threshold",
|
|
24628
|
+
"cmi.progress_measure",
|
|
24629
|
+
"cmi.score.scaled",
|
|
24630
|
+
"cmi.score.raw",
|
|
24631
|
+
"cmi.score.min",
|
|
24632
|
+
"cmi.score.max",
|
|
24633
|
+
"cmi.objectives.N.score.scaled",
|
|
24634
|
+
"cmi.objectives.N.score.raw",
|
|
24635
|
+
"cmi.objectives.N.score.min",
|
|
24636
|
+
"cmi.objectives.N.score.max",
|
|
24637
|
+
"cmi.objectives.N.progress_measure",
|
|
24638
|
+
"cmi.objectives.N.description",
|
|
24639
|
+
"cmi.interactions.N.weighting",
|
|
24640
|
+
"cmi.interactions.N.type",
|
|
24641
|
+
"cmi.interactions.N.timestamp",
|
|
24642
|
+
"cmi.interactions.N.result",
|
|
24643
|
+
"cmi.interactions.N.latency",
|
|
24644
|
+
"cmi.interactions.N.learner_response",
|
|
24645
|
+
"cmi.interactions.N.description",
|
|
24646
|
+
"cmi.comments_from_learner.N.timestamp"
|
|
24647
|
+
]);
|
|
24648
|
+
function normalizeCMIIndices(CMIElement) {
|
|
24649
|
+
return CMIElement.replace(/\.\d+(?=\.|$)/g, ".N");
|
|
24650
|
+
}
|
|
24321
24651
|
class Scorm2004API extends BaseAPI {
|
|
24322
24652
|
_version = "1.0";
|
|
24323
24653
|
_sequencing;
|
|
@@ -24843,6 +25173,39 @@ class Scorm2004API extends BaseAPI {
|
|
|
24843
25173
|
getCMIValue(CMIElement) {
|
|
24844
25174
|
return this._commonGetCMIValue("GetValue", true, CMIElement);
|
|
24845
25175
|
}
|
|
25176
|
+
/**
|
|
25177
|
+
* Raises 403 (VALUE_NOT_INITIALIZED) when an implemented, no-default element is
|
|
25178
|
+
* read before it has ever been set, per IEEE 1484.11.2 / SCORM 2004 RTE.
|
|
25179
|
+
*
|
|
25180
|
+
* Invoked by BaseAPI.getValue after an otherwise-successful resolution, so it
|
|
25181
|
+
* only sees values the data model already returned cleanly. The decision is
|
|
25182
|
+
* deliberately gated to this public boundary (never the getters) to keep
|
|
25183
|
+
* serialization/commit/rollup — which read the getters directly — unaffected.
|
|
25184
|
+
*
|
|
25185
|
+
* A 403 is raised only when all hold:
|
|
25186
|
+
* - the resolved value is "" — any non-empty value is by definition set,
|
|
25187
|
+
* including values written by internal sequencing/activity-tree paths that
|
|
25188
|
+
* bypass _commonSetCMIValue (those only ever write non-empty values);
|
|
25189
|
+
* - the element was never set — _setCMIElements records every successful
|
|
25190
|
+
* SetValue, loadFromJSON, and global-objective restore (all route through
|
|
25191
|
+
* _commonSetCMIValue), so an explicit SetValue(x, "") still reads back as
|
|
25192
|
+
* "" with code 0; and
|
|
25193
|
+
* - the element has no spec-defined default ({@link NO_DEFAULT_2004_ELEMENTS}).
|
|
25194
|
+
*
|
|
25195
|
+
* @param {string} CMIElement
|
|
25196
|
+
* @param {*} returnValue - the value getCMIValue resolved
|
|
25197
|
+
* @protected
|
|
25198
|
+
*/
|
|
25199
|
+
checkUninitializedGet(CMIElement, returnValue) {
|
|
25200
|
+
if (returnValue !== "") return;
|
|
25201
|
+
if (this._setCMIElements.has(CMIElement)) return;
|
|
25202
|
+
if (!NO_DEFAULT_2004_ELEMENTS.has(normalizeCMIIndices(CMIElement))) return;
|
|
25203
|
+
this.throwSCORMError(
|
|
25204
|
+
CMIElement,
|
|
25205
|
+
this._error_codes.VALUE_NOT_INITIALIZED ?? 403,
|
|
25206
|
+
`The data model element passed to GetValue (${CMIElement}) has not been initialized.`
|
|
25207
|
+
);
|
|
25208
|
+
}
|
|
24846
25209
|
/**
|
|
24847
25210
|
* Returns the message that corresponds to errorNumber.
|
|
24848
25211
|
* @param {(string|number)} errorNumber
|
|
@@ -24889,9 +25252,10 @@ class Scorm2004API extends BaseAPI {
|
|
|
24889
25252
|
/**
|
|
24890
25253
|
* Attempts to store the data to the LMS
|
|
24891
25254
|
* @param {boolean} terminateCommit
|
|
25255
|
+
* @param {CommitTrigger} [trigger] - What initiated the commit
|
|
24892
25256
|
* @return {ResultObject}
|
|
24893
25257
|
*/
|
|
24894
|
-
storeData(terminateCommit) {
|
|
25258
|
+
storeData(terminateCommit, trigger) {
|
|
24895
25259
|
if (terminateCommit) {
|
|
24896
25260
|
if (this.cmi.mode === "normal") {
|
|
24897
25261
|
if (this.cmi.credit === "credit") {
|
|
@@ -24939,7 +25303,8 @@ class Scorm2004API extends BaseAPI {
|
|
|
24939
25303
|
const result = this.processHttpRequest(
|
|
24940
25304
|
this.settings.lmsCommitUrl,
|
|
24941
25305
|
commitObject,
|
|
24942
|
-
terminateCommit
|
|
25306
|
+
terminateCommit,
|
|
25307
|
+
trigger
|
|
24943
25308
|
);
|
|
24944
25309
|
if (navRequest && result.navRequest !== void 0 && result.navRequest !== "" && typeof result.navRequest === "string") {
|
|
24945
25310
|
const parsed = parseNavigationRequest(result.navRequest);
|