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/scorm12.js
CHANGED
|
@@ -161,6 +161,15 @@ function memoize(fn, keyFn) {
|
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
const appendQueryParam = (url, name, value) => {
|
|
165
|
+
const fragmentIndex = url.indexOf("#");
|
|
166
|
+
const baseUrl = fragmentIndex === -1 ? url : url.slice(0, fragmentIndex);
|
|
167
|
+
const fragment = fragmentIndex === -1 ? "" : url.slice(fragmentIndex);
|
|
168
|
+
const separator = baseUrl.includes("?") ? baseUrl.endsWith("?") || baseUrl.endsWith("&") ? "" : "&" : "?";
|
|
169
|
+
const queryParam = `${encodeURIComponent(name)}=${encodeURIComponent(String(value))}`;
|
|
170
|
+
return `${baseUrl}${separator}${queryParam}${fragment}`;
|
|
171
|
+
};
|
|
172
|
+
|
|
164
173
|
class BaseCMI {
|
|
165
174
|
/**
|
|
166
175
|
* Flag used during JSON serialization to allow getter access without initialization checks.
|
|
@@ -408,7 +417,10 @@ const scorm12_errors = {
|
|
|
408
417
|
READ_ONLY_ELEMENT: 403,
|
|
409
418
|
WRITE_ONLY_ELEMENT: 404,
|
|
410
419
|
TYPE_MISMATCH: 405,
|
|
411
|
-
|
|
420
|
+
// SCORM 1.2 has no out-of-range error code; an out-of-range value is reported
|
|
421
|
+
// as 405 (incorrect data type), per the SCORM 1.2 RTE error table and the ADL
|
|
422
|
+
// 1.2 CTS (DataModelValidator.checkScoreDecimal failure -> CMICategory 405).
|
|
423
|
+
VALUE_OUT_OF_RANGE: 405,
|
|
412
424
|
DEPENDENCY_NOT_ESTABLISHED: 408
|
|
413
425
|
};
|
|
414
426
|
|
|
@@ -523,6 +535,7 @@ const DefaultSettings = {
|
|
|
523
535
|
xhrWithCredentials: false,
|
|
524
536
|
fetchMode: "cors",
|
|
525
537
|
asyncModeBeaconBehavior: "never",
|
|
538
|
+
includeCommitSequence: false,
|
|
526
539
|
responseHandler: async function(response) {
|
|
527
540
|
if (typeof response !== "undefined") {
|
|
528
541
|
let httpResult = null;
|
|
@@ -907,13 +920,14 @@ class ScheduledCommit {
|
|
|
907
920
|
wrapper() {
|
|
908
921
|
if (!this._cancelled) {
|
|
909
922
|
if (this._API.isInitialized()) {
|
|
910
|
-
(async () => await this._API.commit(this._callback))();
|
|
923
|
+
(async () => await this._API.commit(this._callback, false, "autocommit"))();
|
|
911
924
|
}
|
|
912
925
|
}
|
|
913
926
|
}
|
|
914
927
|
}
|
|
915
928
|
|
|
916
929
|
class AsynchronousHttpService {
|
|
930
|
+
reportsRequestCompletion = true;
|
|
917
931
|
settings;
|
|
918
932
|
error_codes;
|
|
919
933
|
/**
|
|
@@ -938,10 +952,20 @@ class AsynchronousHttpService {
|
|
|
938
952
|
* @param {boolean} immediate - Whether to send the request immediately without waiting
|
|
939
953
|
* @param {Function} apiLog - Function to log API messages with appropriate levels
|
|
940
954
|
* @param {Function} processListeners - Function to trigger event listeners for commit events
|
|
955
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
956
|
+
* @param {Function} onRequestComplete - Callback invoked after the background request settles
|
|
941
957
|
* @return {ResultObject} - Immediate optimistic success result
|
|
942
958
|
*/
|
|
943
|
-
processHttpRequest(url, params, immediate = false, apiLog, processListeners) {
|
|
944
|
-
this._performAsyncRequest(
|
|
959
|
+
processHttpRequest(url, params, immediate = false, apiLog, processListeners, metadata, onRequestComplete) {
|
|
960
|
+
this._performAsyncRequest(
|
|
961
|
+
url,
|
|
962
|
+
params,
|
|
963
|
+
immediate,
|
|
964
|
+
apiLog,
|
|
965
|
+
processListeners,
|
|
966
|
+
metadata,
|
|
967
|
+
onRequestComplete
|
|
968
|
+
);
|
|
945
969
|
return {
|
|
946
970
|
result: global_constants.SCORM_TRUE,
|
|
947
971
|
errorCode: 0
|
|
@@ -954,11 +978,14 @@ class AsynchronousHttpService {
|
|
|
954
978
|
* @param {boolean} immediate - Whether this is an immediate request
|
|
955
979
|
* @param apiLog - Function to log API messages
|
|
956
980
|
* @param {Function} processListeners - Function to process event listeners
|
|
981
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
982
|
+
* @param {Function} onRequestComplete - Callback invoked after the request settles
|
|
957
983
|
* @private
|
|
958
984
|
*/
|
|
959
|
-
async _performAsyncRequest(url, params, immediate, apiLog, processListeners) {
|
|
985
|
+
async _performAsyncRequest(url, params, immediate, apiLog, processListeners, metadata, onRequestComplete) {
|
|
960
986
|
try {
|
|
961
|
-
const
|
|
987
|
+
const handledParams = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
|
|
988
|
+
const processedParams = handledParams;
|
|
962
989
|
let response;
|
|
963
990
|
if (immediate && this.settings.asyncModeBeaconBehavior !== "never") {
|
|
964
991
|
response = await this.performBeacon(url, processedParams);
|
|
@@ -974,7 +1001,9 @@ class AsynchronousHttpService {
|
|
|
974
1001
|
} catch (e) {
|
|
975
1002
|
const message = e instanceof Error ? e.message : String(e);
|
|
976
1003
|
apiLog("processHttpRequest", `Async request failed: ${message}`, LogLevelEnum.ERROR);
|
|
977
|
-
processListeners("CommitError");
|
|
1004
|
+
processListeners("CommitError", void 0, this.error_codes.GENERAL_COMMIT_FAILURE || 391);
|
|
1005
|
+
} finally {
|
|
1006
|
+
onRequestComplete?.();
|
|
978
1007
|
}
|
|
979
1008
|
}
|
|
980
1009
|
/**
|
|
@@ -1117,10 +1146,13 @@ class CMIValueAccessService {
|
|
|
1117
1146
|
}
|
|
1118
1147
|
/**
|
|
1119
1148
|
* Gets the appropriate error code for undefined data model elements.
|
|
1120
|
-
* SCORM 2004
|
|
1149
|
+
* Both SCORM 2004 and SCORM 1.2 use UNDEFINED_DATA_MODEL (401): an
|
|
1150
|
+
* unrecognized element is "Not implemented", not a general exception. SCORM
|
|
1151
|
+
* 1.2 previously returned GENERAL (101) here, which is non-conformant — the
|
|
1152
|
+
* ADL 1.2 CTS and the SCORM 1.2 RTE spec expect 401 for an unknown element.
|
|
1121
1153
|
*/
|
|
1122
|
-
getUndefinedDataModelErrorCode(
|
|
1123
|
-
return
|
|
1154
|
+
getUndefinedDataModelErrorCode() {
|
|
1155
|
+
return getErrorCode(this.context.errorCodes, "UNDEFINED_DATA_MODEL");
|
|
1124
1156
|
}
|
|
1125
1157
|
/**
|
|
1126
1158
|
* Sets a value on a CMI element path
|
|
@@ -1148,7 +1180,7 @@ class CMIValueAccessService {
|
|
|
1148
1180
|
let returnValue = global_constants.SCORM_FALSE;
|
|
1149
1181
|
let foundFirstIndex = false;
|
|
1150
1182
|
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
|
|
1151
|
-
const invalidErrorCode = this.getUndefinedDataModelErrorCode(
|
|
1183
|
+
const invalidErrorCode = this.getUndefinedDataModelErrorCode();
|
|
1152
1184
|
for (let idx = 0; idx < structure.length; idx++) {
|
|
1153
1185
|
const attribute = structure[idx];
|
|
1154
1186
|
if (idx === structure.length - 1) {
|
|
@@ -1218,12 +1250,13 @@ class CMIValueAccessService {
|
|
|
1218
1250
|
);
|
|
1219
1251
|
return "";
|
|
1220
1252
|
}
|
|
1253
|
+
this.context.setLastErrorCode("0");
|
|
1221
1254
|
const structure = CMIElement.split(".");
|
|
1222
1255
|
let refObject = this.context.getDataModel();
|
|
1223
1256
|
let attribute = null;
|
|
1224
1257
|
const uninitializedErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) has not been initialized.`;
|
|
1225
1258
|
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
|
|
1226
|
-
const invalidErrorCode = this.getUndefinedDataModelErrorCode(
|
|
1259
|
+
const invalidErrorCode = this.getUndefinedDataModelErrorCode();
|
|
1227
1260
|
for (let idx = 0; idx < structure.length; idx++) {
|
|
1228
1261
|
attribute = structure[idx];
|
|
1229
1262
|
const validationResult = this.validateGetAttribute(
|
|
@@ -1418,7 +1451,19 @@ class CMIValueAccessService {
|
|
|
1418
1451
|
if (!scorm2004) {
|
|
1419
1452
|
if (isFinalAttribute) {
|
|
1420
1453
|
if (typeof attribute === "undefined" || !this.context.checkObjectHasProperty(refObject, attribute)) {
|
|
1421
|
-
|
|
1454
|
+
if (attribute === "_children") {
|
|
1455
|
+
this.context.throwSCORMError(
|
|
1456
|
+
CMIElement,
|
|
1457
|
+
getErrorCode(this.context.errorCodes, "CHILDREN_ERROR")
|
|
1458
|
+
);
|
|
1459
|
+
} else if (attribute === "_count") {
|
|
1460
|
+
this.context.throwSCORMError(
|
|
1461
|
+
CMIElement,
|
|
1462
|
+
getErrorCode(this.context.errorCodes, "COUNT_ERROR")
|
|
1463
|
+
);
|
|
1464
|
+
} else {
|
|
1465
|
+
this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
|
|
1466
|
+
}
|
|
1422
1467
|
return { error: true };
|
|
1423
1468
|
}
|
|
1424
1469
|
}
|
|
@@ -1923,8 +1968,9 @@ class EventService {
|
|
|
1923
1968
|
* @param {string} functionName - The name of the function that triggered the event
|
|
1924
1969
|
* @param {string} CMIElement - The CMI element that was affected
|
|
1925
1970
|
* @param {any} value - The value that was set
|
|
1971
|
+
* @param {CommitEventContext} context - Optional context for commit lifecycle events
|
|
1926
1972
|
*/
|
|
1927
|
-
processListeners(functionName, CMIElement, value) {
|
|
1973
|
+
processListeners(functionName, CMIElement, value, context) {
|
|
1928
1974
|
this.apiLog(functionName, value, LogLevelEnum.INFO, CMIElement);
|
|
1929
1975
|
const listeners = this.listenerMap.get(functionName);
|
|
1930
1976
|
if (!listeners) return;
|
|
@@ -1949,9 +1995,17 @@ class EventService {
|
|
|
1949
1995
|
if (functionName.startsWith("Sequence")) {
|
|
1950
1996
|
listener.callback(value);
|
|
1951
1997
|
} else if (functionName === "CommitError") {
|
|
1952
|
-
|
|
1998
|
+
if (context !== void 0) {
|
|
1999
|
+
listener.callback(value, context);
|
|
2000
|
+
} else {
|
|
2001
|
+
listener.callback(value);
|
|
2002
|
+
}
|
|
1953
2003
|
} else if (functionName === "CommitSuccess") {
|
|
1954
|
-
|
|
2004
|
+
if (context !== void 0) {
|
|
2005
|
+
listener.callback(context);
|
|
2006
|
+
} else {
|
|
2007
|
+
listener.callback();
|
|
2008
|
+
}
|
|
1955
2009
|
} else {
|
|
1956
2010
|
listener.callback(CMIElement, value);
|
|
1957
2011
|
}
|
|
@@ -2085,16 +2139,19 @@ class OfflineStorageService {
|
|
|
2085
2139
|
* Store commit data offline
|
|
2086
2140
|
* @param {string} courseId - Identifier for the course
|
|
2087
2141
|
* @param {CommitObject} commitData - The data to store offline
|
|
2142
|
+
* @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
|
|
2088
2143
|
* @returns {ResultObject} - Result of the storage operation
|
|
2089
2144
|
*/
|
|
2090
|
-
storeOffline(courseId, commitData) {
|
|
2145
|
+
storeOffline(courseId, commitData, metadata) {
|
|
2091
2146
|
try {
|
|
2092
2147
|
const queueItem = {
|
|
2093
2148
|
id: `${courseId}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
|
2094
2149
|
courseId,
|
|
2095
2150
|
timestamp: Date.now(),
|
|
2096
2151
|
data: commitData,
|
|
2097
|
-
syncAttempts: 0
|
|
2152
|
+
syncAttempts: 0,
|
|
2153
|
+
...metadata?.isTerminateCommit !== void 0 ? { isTerminateCommit: metadata.isTerminateCommit } : {},
|
|
2154
|
+
...metadata?.sequence !== void 0 ? { sequence: metadata.sequence } : {}
|
|
2098
2155
|
};
|
|
2099
2156
|
const currentQueue = this.getFromStorage(this.syncQueue) || [];
|
|
2100
2157
|
currentQueue.push(queueItem);
|
|
@@ -2173,7 +2230,10 @@ class OfflineStorageService {
|
|
|
2173
2230
|
continue;
|
|
2174
2231
|
}
|
|
2175
2232
|
try {
|
|
2176
|
-
const syncResult = await this.sendDataToLMS(item.data
|
|
2233
|
+
const syncResult = await this.sendDataToLMS(item.data, {
|
|
2234
|
+
...item.isTerminateCommit !== void 0 ? { isTerminateCommit: item.isTerminateCommit } : {},
|
|
2235
|
+
...item.sequence !== void 0 ? { sequence: item.sequence } : {}
|
|
2236
|
+
});
|
|
2177
2237
|
if (syncResult.result === true || syncResult.result === global_constants.SCORM_TRUE) {
|
|
2178
2238
|
this.apiLog(
|
|
2179
2239
|
"OfflineStorageService",
|
|
@@ -2220,17 +2280,25 @@ class OfflineStorageService {
|
|
|
2220
2280
|
/**
|
|
2221
2281
|
* Send data to the LMS when online
|
|
2222
2282
|
* @param {CommitObject} data - The data to send to the LMS
|
|
2283
|
+
* @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
|
|
2223
2284
|
* @returns {Promise<ResultObject>} - Result of the sync operation
|
|
2224
2285
|
*/
|
|
2225
|
-
async sendDataToLMS(data) {
|
|
2226
|
-
|
|
2286
|
+
async sendDataToLMS(data, metadata) {
|
|
2287
|
+
const configuredCommitUrl = this.settings.lmsCommitUrl;
|
|
2288
|
+
if (!configuredCommitUrl) {
|
|
2227
2289
|
return {
|
|
2228
2290
|
result: global_constants.SCORM_FALSE,
|
|
2229
2291
|
errorCode: this.error_codes.GENERAL || 101
|
|
2230
2292
|
};
|
|
2231
2293
|
}
|
|
2232
2294
|
try {
|
|
2233
|
-
const
|
|
2295
|
+
const lmsCommitUrl = String(configuredCommitUrl);
|
|
2296
|
+
const processedData = this.settings.requestHandler(data, {
|
|
2297
|
+
isTerminateCommit: metadata?.isTerminateCommit ?? false,
|
|
2298
|
+
trigger: "offline-replay",
|
|
2299
|
+
...metadata?.sequence !== void 0 ? { sequence: metadata.sequence } : {}
|
|
2300
|
+
});
|
|
2301
|
+
const requestUrl = metadata?.isTerminateCommit && this.settings.terminateCommitParam ? appendQueryParam(lmsCommitUrl, this.settings.terminateCommitParam, "true") : lmsCommitUrl;
|
|
2234
2302
|
const init = {
|
|
2235
2303
|
method: "POST",
|
|
2236
2304
|
mode: this.settings.fetchMode,
|
|
@@ -2243,7 +2311,7 @@ class OfflineStorageService {
|
|
|
2243
2311
|
if (this.settings.xhrWithCredentials) {
|
|
2244
2312
|
init.credentials = "include";
|
|
2245
2313
|
}
|
|
2246
|
-
const response = await fetch(
|
|
2314
|
+
const response = await fetch(requestUrl, init);
|
|
2247
2315
|
const result = typeof this.settings.responseHandler === "function" ? await this.settings.responseHandler(response) : await response.json();
|
|
2248
2316
|
if (response.status >= 200 && response.status <= 299 && (result.result === true || result.result === global_constants.SCORM_TRUE)) {
|
|
2249
2317
|
if (!Object.hasOwnProperty.call(result, "errorCode")) {
|
|
@@ -2640,6 +2708,8 @@ class SynchronousHttpService {
|
|
|
2640
2708
|
* @param {boolean} immediate - Whether this is a termination commit (use sendBeacon)
|
|
2641
2709
|
* @param {Function} _apiLog - Function to log API messages (unused in synchronous mode - errors returned directly)
|
|
2642
2710
|
* @param {Function} _processListeners - Function to trigger event listeners (unused in synchronous mode - no async events)
|
|
2711
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
2712
|
+
* @param {Function} _onRequestComplete - Completion callback (unused because requests settle before return)
|
|
2643
2713
|
* @return {ResultObject} - The result of the request (synchronous)
|
|
2644
2714
|
*
|
|
2645
2715
|
* @remarks
|
|
@@ -2649,21 +2719,23 @@ class SynchronousHttpService {
|
|
|
2649
2719
|
* - No async events need to be triggered (CommitSuccess/CommitError) since results are synchronous
|
|
2650
2720
|
* - AsynchronousHttpService uses these parameters to handle background request results
|
|
2651
2721
|
*/
|
|
2652
|
-
processHttpRequest(url, params, immediate = false, _apiLog, _processListeners) {
|
|
2722
|
+
processHttpRequest(url, params, immediate = false, _apiLog, _processListeners, metadata, _onRequestComplete) {
|
|
2653
2723
|
if (immediate) {
|
|
2654
|
-
return this._handleImmediateRequest(url, params);
|
|
2724
|
+
return this._handleImmediateRequest(url, params, metadata);
|
|
2655
2725
|
}
|
|
2656
|
-
return this._performSyncXHR(url, params);
|
|
2726
|
+
return this._performSyncXHR(url, params, metadata);
|
|
2657
2727
|
}
|
|
2658
2728
|
/**
|
|
2659
2729
|
* Handles an immediate request using sendBeacon
|
|
2660
2730
|
* @param {string} url - The URL to send the request to
|
|
2661
2731
|
* @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
|
|
2732
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
2662
2733
|
* @return {ResultObject} - The result based on beacon success
|
|
2663
2734
|
* @private
|
|
2664
2735
|
*/
|
|
2665
|
-
_handleImmediateRequest(url, params) {
|
|
2666
|
-
const
|
|
2736
|
+
_handleImmediateRequest(url, params, metadata) {
|
|
2737
|
+
const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
|
|
2738
|
+
const requestPayload = handledPayload ?? params;
|
|
2667
2739
|
const { body } = this._prepareRequestBody(requestPayload);
|
|
2668
2740
|
const beaconSuccess = navigator.sendBeacon(
|
|
2669
2741
|
url,
|
|
@@ -2678,11 +2750,13 @@ class SynchronousHttpService {
|
|
|
2678
2750
|
* Performs a synchronous XMLHttpRequest
|
|
2679
2751
|
* @param {string} url - The URL to send the request to
|
|
2680
2752
|
* @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
|
|
2753
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
2681
2754
|
* @return {ResultObject} - The result of the request
|
|
2682
2755
|
* @private
|
|
2683
2756
|
*/
|
|
2684
|
-
_performSyncXHR(url, params) {
|
|
2685
|
-
const
|
|
2757
|
+
_performSyncXHR(url, params, metadata) {
|
|
2758
|
+
const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
|
|
2759
|
+
const requestPayload = handledPayload ?? params;
|
|
2686
2760
|
const { body, contentType } = this._prepareRequestBody(requestPayload);
|
|
2687
2761
|
const xhr = new XMLHttpRequest();
|
|
2688
2762
|
xhr.open("POST", url, false);
|
|
@@ -2761,9 +2835,15 @@ class ValidationService {
|
|
|
2761
2835
|
* @param {number} invalidTypeCode - The error code for invalid type
|
|
2762
2836
|
* @param {number} invalidRangeCode - The error code for invalid range
|
|
2763
2837
|
* @param {typeof BaseScormValidationError} errorClass - The error class to use for validation errors
|
|
2838
|
+
* @param {boolean} allowEmptyString - When true, an empty string is accepted (clears the value).
|
|
2839
|
+
* SCORM 1.2 score elements may be blank per the ADL 1.2 CTS
|
|
2840
|
+
* (DataModelValidator.checkScoreDecimal treats blank as valid).
|
|
2764
2841
|
* @return {boolean} - True if validation passes, throws an error otherwise
|
|
2765
2842
|
*/
|
|
2766
|
-
validateScore(CMIElement, value, decimalRegex, scoreRange, invalidTypeCode, invalidRangeCode, errorClass) {
|
|
2843
|
+
validateScore(CMIElement, value, decimalRegex, scoreRange, invalidTypeCode, invalidRangeCode, errorClass, allowEmptyString = false) {
|
|
2844
|
+
if (allowEmptyString && value === "") {
|
|
2845
|
+
return true;
|
|
2846
|
+
}
|
|
2767
2847
|
return checkValidFormat(CMIElement, value, decimalRegex, invalidTypeCode, errorClass) && (!scoreRange || checkValidRange(CMIElement, value, scoreRange, invalidRangeCode, errorClass));
|
|
2768
2848
|
}
|
|
2769
2849
|
/**
|
|
@@ -2837,6 +2917,21 @@ class BaseAPI {
|
|
|
2837
2917
|
_offlineStorageService;
|
|
2838
2918
|
_cmiValueAccessService;
|
|
2839
2919
|
_courseId = "";
|
|
2920
|
+
_pendingCommitCount = 0;
|
|
2921
|
+
/**
|
|
2922
|
+
* Monotonic sequence for commits captured by this API instance. It is
|
|
2923
|
+
* intentionally not reset by reset().
|
|
2924
|
+
*/
|
|
2925
|
+
_commitSequence = 0;
|
|
2926
|
+
_commitSettleWaiters = [];
|
|
2927
|
+
/**
|
|
2928
|
+
* Canonical paths of every CMI element that has been explicitly assigned a
|
|
2929
|
+
* value via SetValue / loadFromJSON (both funnel through _commonSetCMIValue).
|
|
2930
|
+
* Used by standards that must tell "implemented but never set" apart from a
|
|
2931
|
+
* legitimately empty value when answering GetValue (SCORM 2004 error 403).
|
|
2932
|
+
* Cleared on reset so a fresh SCO attempt starts with nothing "set".
|
|
2933
|
+
*/
|
|
2934
|
+
_setCMIElements = /* @__PURE__ */ new Set();
|
|
2840
2935
|
/**
|
|
2841
2936
|
* Constructor for Base API class. Sets some shared API fields, as well as
|
|
2842
2937
|
* sets up options for the API.
|
|
@@ -3022,6 +3117,7 @@ class BaseAPI {
|
|
|
3022
3117
|
this.lastErrorCode = "0";
|
|
3023
3118
|
this._eventService.reset();
|
|
3024
3119
|
this.startingData = {};
|
|
3120
|
+
this._setCMIElements.clear();
|
|
3025
3121
|
if (this._offlineStorageService) {
|
|
3026
3122
|
this._offlineStorageService.updateSettings(this.settings);
|
|
3027
3123
|
if (settings?.courseId) {
|
|
@@ -3106,6 +3202,52 @@ class BaseAPI {
|
|
|
3106
3202
|
this._loggingService?.setLogHandler(settings.onLogMessage);
|
|
3107
3203
|
}
|
|
3108
3204
|
}
|
|
3205
|
+
/**
|
|
3206
|
+
* Gets the number of captured commit requests that have not yet settled.
|
|
3207
|
+
*
|
|
3208
|
+
* @return {number} The number of in-flight commits
|
|
3209
|
+
*/
|
|
3210
|
+
get pendingCommitCount() {
|
|
3211
|
+
return this._pendingCommitCount;
|
|
3212
|
+
}
|
|
3213
|
+
/**
|
|
3214
|
+
* Resolves when all currently in-flight commits have settled. A timeout is
|
|
3215
|
+
* best-effort: the promise resolves when it elapses even if commits remain,
|
|
3216
|
+
* and callers can inspect pendingCommitCount afterward to detect that case.
|
|
3217
|
+
*
|
|
3218
|
+
* @param {Object} [options] - Settle options
|
|
3219
|
+
* @param {number} [options.timeoutMs] - Maximum time to wait in milliseconds
|
|
3220
|
+
* @return {Promise<void>} A promise that resolves after the drain or timeout
|
|
3221
|
+
*/
|
|
3222
|
+
whenCommitsSettled(options) {
|
|
3223
|
+
if (this._pendingCommitCount === 0) {
|
|
3224
|
+
return Promise.resolve();
|
|
3225
|
+
}
|
|
3226
|
+
return new Promise((resolve) => {
|
|
3227
|
+
const waiter = { resolve };
|
|
3228
|
+
if (options?.timeoutMs !== void 0) {
|
|
3229
|
+
waiter.timeoutId = setTimeout(() => {
|
|
3230
|
+
const waiterIndex = this._commitSettleWaiters.indexOf(waiter);
|
|
3231
|
+
if (waiterIndex === -1) {
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
this._commitSettleWaiters.splice(waiterIndex, 1);
|
|
3235
|
+
resolve();
|
|
3236
|
+
}, options.timeoutMs);
|
|
3237
|
+
}
|
|
3238
|
+
this._commitSettleWaiters.push(waiter);
|
|
3239
|
+
});
|
|
3240
|
+
}
|
|
3241
|
+
/** Resolve and clear every waiter after the pending count reaches zero. */
|
|
3242
|
+
_flushCommitSettleWaiters() {
|
|
3243
|
+
const waiters = this._commitSettleWaiters.splice(0);
|
|
3244
|
+
for (const waiter of waiters) {
|
|
3245
|
+
if (waiter.timeoutId !== void 0) {
|
|
3246
|
+
clearTimeout(waiter.timeoutId);
|
|
3247
|
+
}
|
|
3248
|
+
waiter.resolve();
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3109
3251
|
/**
|
|
3110
3252
|
* Terminates the current run of the API
|
|
3111
3253
|
* @param {string} callbackName
|
|
@@ -3126,7 +3268,7 @@ class BaseAPI {
|
|
|
3126
3268
|
} else {
|
|
3127
3269
|
stateCheckPassed = true;
|
|
3128
3270
|
this.processListeners("BeforeTerminate");
|
|
3129
|
-
const result = this.storeData(true);
|
|
3271
|
+
const result = this.storeData(true, "terminate");
|
|
3130
3272
|
if ((result.errorCode ?? 0) > 0) {
|
|
3131
3273
|
if (result.errorMessage) {
|
|
3132
3274
|
this.apiLog(
|
|
@@ -3178,6 +3320,9 @@ class BaseAPI {
|
|
|
3178
3320
|
} catch (e) {
|
|
3179
3321
|
returnValue = this.handleValueAccessException(CMIElement, e, returnValue);
|
|
3180
3322
|
}
|
|
3323
|
+
if (this.lastErrorCode === "0") {
|
|
3324
|
+
this.checkUninitializedGet(CMIElement, returnValue);
|
|
3325
|
+
}
|
|
3181
3326
|
this.processListeners(callbackName, CMIElement);
|
|
3182
3327
|
}
|
|
3183
3328
|
this.apiLog(callbackName, ": returned: " + returnValue, LogLevelEnum.INFO, CMIElement);
|
|
@@ -3187,6 +3332,10 @@ class BaseAPI {
|
|
|
3187
3332
|
if (this.lastErrorCode === "0") {
|
|
3188
3333
|
this.clearSCORMError(returnValue);
|
|
3189
3334
|
}
|
|
3335
|
+
const rawReturn = returnValue;
|
|
3336
|
+
if (typeof rawReturn === "number" || typeof rawReturn === "boolean") {
|
|
3337
|
+
return String(rawReturn);
|
|
3338
|
+
}
|
|
3190
3339
|
return returnValue;
|
|
3191
3340
|
}
|
|
3192
3341
|
/**
|
|
@@ -3239,9 +3388,10 @@ class BaseAPI {
|
|
|
3239
3388
|
* Orders LMS to store all content parameters
|
|
3240
3389
|
* @param {string} callbackName
|
|
3241
3390
|
* @param {boolean} checkTerminated
|
|
3391
|
+
* @param {CommitTrigger} trigger - What initiated the commit
|
|
3242
3392
|
* @return {string}
|
|
3243
3393
|
*/
|
|
3244
|
-
commit(callbackName, checkTerminated = false) {
|
|
3394
|
+
commit(callbackName, checkTerminated = false, trigger = "manual") {
|
|
3245
3395
|
this.clearScheduledCommit();
|
|
3246
3396
|
let returnValue = global_constants.SCORM_TRUE;
|
|
3247
3397
|
if (this.isNotInitialized()) {
|
|
@@ -3253,8 +3403,9 @@ class BaseAPI {
|
|
|
3253
3403
|
this.throwSCORMError("api", errorCode);
|
|
3254
3404
|
if (errorCode === 143) returnValue = global_constants.SCORM_FALSE;
|
|
3255
3405
|
} else {
|
|
3256
|
-
const result = this.storeData(false);
|
|
3257
|
-
|
|
3406
|
+
const result = this.storeData(false, trigger);
|
|
3407
|
+
const errorCode = result.errorCode ?? 0;
|
|
3408
|
+
if (errorCode > 0) {
|
|
3258
3409
|
if (result.errorMessage) {
|
|
3259
3410
|
this.apiLog(
|
|
3260
3411
|
"commit",
|
|
@@ -3269,12 +3420,12 @@ class BaseAPI {
|
|
|
3269
3420
|
LogLevelEnum.DEBUG
|
|
3270
3421
|
);
|
|
3271
3422
|
}
|
|
3272
|
-
this.throwSCORMError("api",
|
|
3423
|
+
this.throwSCORMError("api", errorCode);
|
|
3273
3424
|
}
|
|
3274
3425
|
const resultValue = result?.result ?? global_constants.SCORM_FALSE;
|
|
3275
3426
|
returnValue = typeof resultValue === "boolean" ? String(resultValue) : resultValue;
|
|
3276
3427
|
this.apiLog(callbackName, " Result: " + returnValue, LogLevelEnum.DEBUG, "HttpRequest");
|
|
3277
|
-
if (checkTerminated) this.lastErrorCode = "0";
|
|
3428
|
+
if (checkTerminated && errorCode === 0) this.lastErrorCode = "0";
|
|
3278
3429
|
this.processListeners(callbackName);
|
|
3279
3430
|
if (this.settings.enableOfflineSupport && this._offlineStorageService && this._offlineStorageService.isDeviceOnline() && this._courseId) {
|
|
3280
3431
|
this._offlineStorageService.hasPendingOfflineData(this._courseId).then((hasPendingData) => {
|
|
@@ -3483,7 +3634,16 @@ class BaseAPI {
|
|
|
3483
3634
|
* @return {string}
|
|
3484
3635
|
*/
|
|
3485
3636
|
_commonSetCMIValue(methodName, scorm2004, CMIElement, value) {
|
|
3486
|
-
|
|
3637
|
+
const result = this._cmiValueAccessService.setCMIValue(
|
|
3638
|
+
methodName,
|
|
3639
|
+
scorm2004,
|
|
3640
|
+
CMIElement,
|
|
3641
|
+
value
|
|
3642
|
+
);
|
|
3643
|
+
if (result === global_constants.SCORM_TRUE) {
|
|
3644
|
+
this._setCMIElements.add(CMIElement);
|
|
3645
|
+
}
|
|
3646
|
+
return result;
|
|
3487
3647
|
}
|
|
3488
3648
|
/**
|
|
3489
3649
|
* Gets a value from the CMI Object.
|
|
@@ -3497,6 +3657,18 @@ class BaseAPI {
|
|
|
3497
3657
|
_commonGetCMIValue(methodName, scorm2004, CMIElement) {
|
|
3498
3658
|
return this._cmiValueAccessService.getCMIValue(methodName, scorm2004, CMIElement);
|
|
3499
3659
|
}
|
|
3660
|
+
/**
|
|
3661
|
+
* Hook invoked by getValue after a successful resolution. Standards that must
|
|
3662
|
+
* distinguish "implemented but never set, no default value" from a
|
|
3663
|
+
* legitimately empty value override this to raise VALUE_NOT_INITIALIZED.
|
|
3664
|
+
* Default is a no-op, so SCORM 1.2 / AICC keep returning "" with code 0.
|
|
3665
|
+
*
|
|
3666
|
+
* @param {string} _CMIElement - the element that was read
|
|
3667
|
+
* @param {any} _returnValue - the value getCMIValue resolved
|
|
3668
|
+
* @protected
|
|
3669
|
+
*/
|
|
3670
|
+
checkUninitializedGet(_CMIElement, _returnValue) {
|
|
3671
|
+
}
|
|
3500
3672
|
/**
|
|
3501
3673
|
* Returns true if the API's current state is STATE_INITIALIZED
|
|
3502
3674
|
*
|
|
@@ -3579,9 +3751,14 @@ class BaseAPI {
|
|
|
3579
3751
|
* @param {string} functionName - The name of the function/event that occurred
|
|
3580
3752
|
* @param {string} CMIElement - Optional CMI element involved in the event
|
|
3581
3753
|
* @param {any} value - Optional value associated with the event
|
|
3754
|
+
* @param {CommitEventContext} context - Optional context for commit lifecycle events
|
|
3582
3755
|
*/
|
|
3583
|
-
processListeners(functionName, CMIElement, value) {
|
|
3584
|
-
|
|
3756
|
+
processListeners(functionName, CMIElement, value, context) {
|
|
3757
|
+
if (context !== void 0) {
|
|
3758
|
+
this._eventService.processListeners(functionName, CMIElement, value, context);
|
|
3759
|
+
} else {
|
|
3760
|
+
this._eventService.processListeners(functionName, CMIElement, value);
|
|
3761
|
+
}
|
|
3585
3762
|
}
|
|
3586
3763
|
/**
|
|
3587
3764
|
* Throws a SCORM error with the specified error number and optional message.
|
|
@@ -3714,36 +3891,106 @@ class BaseAPI {
|
|
|
3714
3891
|
* @param {string} url - The URL to send the request to
|
|
3715
3892
|
* @param {CommitObject | StringKeyMap | Array<any>} params - The parameters to send
|
|
3716
3893
|
* @param {boolean} immediate - Whether to send the request immediately without waiting
|
|
3894
|
+
* @param {CommitTrigger} [trigger] - What initiated the commit
|
|
3717
3895
|
* @returns {ResultObject} - The result of the request
|
|
3718
3896
|
*/
|
|
3719
|
-
processHttpRequest(url, params, immediate = false) {
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
)
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3897
|
+
processHttpRequest(url, params, immediate = false, trigger) {
|
|
3898
|
+
const sequence = ++this._commitSequence;
|
|
3899
|
+
this._pendingCommitCount += 1;
|
|
3900
|
+
let settled = false;
|
|
3901
|
+
let completionDeferred = false;
|
|
3902
|
+
const settle = () => {
|
|
3903
|
+
if (settled) {
|
|
3904
|
+
return;
|
|
3905
|
+
}
|
|
3906
|
+
settled = true;
|
|
3907
|
+
this._pendingCommitCount -= 1;
|
|
3908
|
+
if (this._pendingCommitCount === 0) {
|
|
3909
|
+
this._flushCommitSettleWaiters();
|
|
3910
|
+
}
|
|
3911
|
+
};
|
|
3912
|
+
try {
|
|
3913
|
+
const resolvedTrigger = trigger ?? (immediate ? "terminate" : "manual");
|
|
3914
|
+
let finalParams = params;
|
|
3915
|
+
if (immediate && this.settings.terminateCommitPayloadField) {
|
|
3916
|
+
const field = this.settings.terminateCommitPayloadField;
|
|
3917
|
+
if (Array.isArray(finalParams)) {
|
|
3918
|
+
finalParams = [...finalParams, `${encodeURIComponent(field)}=true`];
|
|
3919
|
+
} else if (finalParams && typeof finalParams === "object") {
|
|
3920
|
+
finalParams = { ...finalParams, [field]: true };
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
if (this.settings.includeCommitSequence === true) {
|
|
3924
|
+
if (Array.isArray(finalParams)) {
|
|
3925
|
+
finalParams = [...finalParams, `commitSequence=${sequence}`];
|
|
3926
|
+
} else if (finalParams && typeof finalParams === "object") {
|
|
3927
|
+
finalParams = { ...finalParams, commitSequence: sequence };
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
const finalUrl = immediate && this.settings.terminateCommitParam ? appendQueryParam(url, this.settings.terminateCommitParam, "true") : url;
|
|
3931
|
+
const metadata = {
|
|
3932
|
+
isTerminateCommit: immediate,
|
|
3933
|
+
trigger: resolvedTrigger,
|
|
3934
|
+
sequence
|
|
3935
|
+
};
|
|
3936
|
+
const context = {
|
|
3937
|
+
url: finalUrl,
|
|
3938
|
+
trigger: resolvedTrigger,
|
|
3939
|
+
isTerminateCommit: immediate,
|
|
3940
|
+
sequence
|
|
3941
|
+
};
|
|
3942
|
+
if (this.settings.enableOfflineSupport && this._offlineStorageService && !this._offlineStorageService.isDeviceOnline() && this._courseId) {
|
|
3729
3943
|
this.apiLog(
|
|
3730
3944
|
"processHttpRequest",
|
|
3731
|
-
"
|
|
3732
|
-
LogLevelEnum.
|
|
3945
|
+
"Device is offline, storing data locally",
|
|
3946
|
+
LogLevelEnum.INFO
|
|
3733
3947
|
);
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3948
|
+
if (finalParams && typeof finalParams === "object" && "cmi" in finalParams) {
|
|
3949
|
+
return this._offlineStorageService.storeOffline(
|
|
3950
|
+
this._courseId,
|
|
3951
|
+
finalParams,
|
|
3952
|
+
{ isTerminateCommit: immediate, sequence }
|
|
3953
|
+
);
|
|
3954
|
+
} else {
|
|
3955
|
+
this.apiLog(
|
|
3956
|
+
"processHttpRequest",
|
|
3957
|
+
"Invalid commit data format for offline storage",
|
|
3958
|
+
LogLevelEnum.ERROR
|
|
3959
|
+
);
|
|
3960
|
+
return {
|
|
3961
|
+
result: global_constants.SCORM_FALSE,
|
|
3962
|
+
errorCode: this._error_codes.GENERAL ?? 101
|
|
3963
|
+
};
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
const apiLog = (functionName, message, level, element) => this.apiLog(functionName, message, level, element);
|
|
3967
|
+
const processListeners = (functionName, CMIElement, value) => {
|
|
3968
|
+
if (functionName === "CommitSuccess" || functionName === "CommitError") {
|
|
3969
|
+
if (functionName === "CommitError" && typeof value === "number") {
|
|
3970
|
+
context.errorCode = value;
|
|
3971
|
+
}
|
|
3972
|
+
settle();
|
|
3973
|
+
this.processListeners(functionName, CMIElement, value, context);
|
|
3974
|
+
} else {
|
|
3975
|
+
this.processListeners(functionName, CMIElement, value);
|
|
3976
|
+
}
|
|
3977
|
+
};
|
|
3978
|
+
const result = this._httpService.processHttpRequest(
|
|
3979
|
+
finalUrl,
|
|
3980
|
+
finalParams,
|
|
3981
|
+
immediate,
|
|
3982
|
+
apiLog,
|
|
3983
|
+
processListeners,
|
|
3984
|
+
metadata,
|
|
3985
|
+
settle
|
|
3986
|
+
);
|
|
3987
|
+
completionDeferred = this._httpService.reportsRequestCompletion === true;
|
|
3988
|
+
return result;
|
|
3989
|
+
} finally {
|
|
3990
|
+
if (!completionDeferred) {
|
|
3991
|
+
settle();
|
|
3738
3992
|
}
|
|
3739
3993
|
}
|
|
3740
|
-
return this._httpService.processHttpRequest(
|
|
3741
|
-
url,
|
|
3742
|
-
params,
|
|
3743
|
-
immediate,
|
|
3744
|
-
(functionName, message, level, element) => this.apiLog(functionName, message, level, element),
|
|
3745
|
-
(functionName, CMIElement, value) => this.processListeners(functionName, CMIElement, value)
|
|
3746
|
-
);
|
|
3747
3994
|
}
|
|
3748
3995
|
/**
|
|
3749
3996
|
* Schedules a commit operation to occur after a specified delay.
|
|
@@ -3903,6 +4150,11 @@ class CMIScore extends BaseCMI {
|
|
|
3903
4150
|
__invalid_range_code;
|
|
3904
4151
|
__decimal_regex;
|
|
3905
4152
|
__error_class;
|
|
4153
|
+
/**
|
|
4154
|
+
* When true, an empty string is a valid value (clears the element). SCORM 1.2
|
|
4155
|
+
* score elements may be blank per the ADL 1.2 CTS; SCORM 2004 leaves this off.
|
|
4156
|
+
*/
|
|
4157
|
+
__allow_empty_string;
|
|
3906
4158
|
_raw = "";
|
|
3907
4159
|
_min = "";
|
|
3908
4160
|
_max;
|
|
@@ -3937,6 +4189,7 @@ class CMIScore extends BaseCMI {
|
|
|
3937
4189
|
this.__invalid_range_code = params.invalidRangeCode || scorm12_errors.VALUE_OUT_OF_RANGE;
|
|
3938
4190
|
this.__decimal_regex = params.decimalRegex || scorm12_regex.CMIDecimal;
|
|
3939
4191
|
this.__error_class = params.errorClass;
|
|
4192
|
+
this.__allow_empty_string = params.allowEmptyString ?? false;
|
|
3940
4193
|
}
|
|
3941
4194
|
/**
|
|
3942
4195
|
* Called when the API has been reset
|
|
@@ -3983,7 +4236,8 @@ class CMIScore extends BaseCMI {
|
|
|
3983
4236
|
this.__score_range,
|
|
3984
4237
|
this.__invalid_type_code,
|
|
3985
4238
|
this.__invalid_range_code,
|
|
3986
|
-
this.__error_class
|
|
4239
|
+
this.__error_class,
|
|
4240
|
+
this.__allow_empty_string
|
|
3987
4241
|
)) {
|
|
3988
4242
|
this._raw = raw;
|
|
3989
4243
|
}
|
|
@@ -4007,7 +4261,8 @@ class CMIScore extends BaseCMI {
|
|
|
4007
4261
|
this.__score_range,
|
|
4008
4262
|
this.__invalid_type_code,
|
|
4009
4263
|
this.__invalid_range_code,
|
|
4010
|
-
this.__error_class
|
|
4264
|
+
this.__error_class,
|
|
4265
|
+
this.__allow_empty_string
|
|
4011
4266
|
)) {
|
|
4012
4267
|
this._min = min;
|
|
4013
4268
|
}
|
|
@@ -4031,7 +4286,8 @@ class CMIScore extends BaseCMI {
|
|
|
4031
4286
|
this.__score_range,
|
|
4032
4287
|
this.__invalid_type_code,
|
|
4033
4288
|
this.__invalid_range_code,
|
|
4034
|
-
this.__error_class
|
|
4289
|
+
this.__error_class,
|
|
4290
|
+
this.__allow_empty_string
|
|
4035
4291
|
)) {
|
|
4036
4292
|
this._max = max;
|
|
4037
4293
|
}
|
|
@@ -4088,7 +4344,9 @@ class CMICore extends BaseCMI {
|
|
|
4088
4344
|
invalidErrorCode: scorm12_errors.INVALID_SET_VALUE,
|
|
4089
4345
|
invalidTypeCode: scorm12_errors.TYPE_MISMATCH,
|
|
4090
4346
|
invalidRangeCode: scorm12_errors.VALUE_OUT_OF_RANGE,
|
|
4091
|
-
errorClass: Scorm12ValidationError
|
|
4347
|
+
errorClass: Scorm12ValidationError,
|
|
4348
|
+
// SCORM 1.2 score elements may be set blank (clears the value), per ADL 1.2 CTS.
|
|
4349
|
+
allowEmptyString: true
|
|
4092
4350
|
});
|
|
4093
4351
|
}
|
|
4094
4352
|
score;
|
|
@@ -4518,7 +4776,9 @@ class CMIObjectivesObject extends BaseCMI {
|
|
|
4518
4776
|
invalidErrorCode: scorm12_errors.INVALID_SET_VALUE,
|
|
4519
4777
|
invalidTypeCode: scorm12_errors.TYPE_MISMATCH,
|
|
4520
4778
|
invalidRangeCode: scorm12_errors.VALUE_OUT_OF_RANGE,
|
|
4521
|
-
errorClass: Scorm12ValidationError
|
|
4779
|
+
errorClass: Scorm12ValidationError,
|
|
4780
|
+
// SCORM 1.2 score elements may be set blank (clears the value), per ADL 1.2 CTS.
|
|
4781
|
+
allowEmptyString: true
|
|
4522
4782
|
});
|
|
4523
4783
|
}
|
|
4524
4784
|
score;
|
|
@@ -5988,16 +6248,17 @@ class Scorm12API extends BaseAPI {
|
|
|
5988
6248
|
* Attempts to store the data to the LMS
|
|
5989
6249
|
*
|
|
5990
6250
|
* @param {boolean} terminateCommit
|
|
6251
|
+
* @param {CommitTrigger} [trigger] - What initiated the commit
|
|
5991
6252
|
* @return {ResultObject}
|
|
5992
6253
|
*/
|
|
5993
|
-
storeData(terminateCommit) {
|
|
6254
|
+
storeData(terminateCommit, trigger) {
|
|
5994
6255
|
if (terminateCommit) {
|
|
5995
6256
|
const originalStatus = this.cmi.core.lesson_status;
|
|
5996
6257
|
if (this.cmi.core.lesson_mode === "browse") {
|
|
5997
6258
|
const startingStatus = this.startingData?.cmi?.core?.lesson_status || "";
|
|
5998
6259
|
if (startingStatus === "" && originalStatus === "not attempted") {
|
|
5999
6260
|
this.cmi.core.lesson_status = "browsed";
|
|
6000
|
-
return this.processCommitData(terminateCommit);
|
|
6261
|
+
return this.processCommitData(terminateCommit, trigger);
|
|
6001
6262
|
}
|
|
6002
6263
|
}
|
|
6003
6264
|
if (!this.cmi.core.lesson_status || !this.statusSetByModule && this.cmi.core.lesson_status === "not attempted") {
|
|
@@ -6026,12 +6287,17 @@ class Scorm12API extends BaseAPI {
|
|
|
6026
6287
|
}
|
|
6027
6288
|
}
|
|
6028
6289
|
}
|
|
6029
|
-
return this.processCommitData(terminateCommit);
|
|
6290
|
+
return this.processCommitData(terminateCommit, trigger);
|
|
6030
6291
|
}
|
|
6031
|
-
processCommitData(terminateCommit) {
|
|
6292
|
+
processCommitData(terminateCommit, trigger) {
|
|
6032
6293
|
const commitObject = this.getCommitObject(terminateCommit);
|
|
6033
6294
|
if (typeof this.settings.lmsCommitUrl === "string") {
|
|
6034
|
-
return this.processHttpRequest(
|
|
6295
|
+
return this.processHttpRequest(
|
|
6296
|
+
this.settings.lmsCommitUrl,
|
|
6297
|
+
commitObject,
|
|
6298
|
+
terminateCommit,
|
|
6299
|
+
trigger
|
|
6300
|
+
);
|
|
6035
6301
|
} else {
|
|
6036
6302
|
return {
|
|
6037
6303
|
result: global_constants.SCORM_TRUE,
|