scorm-again 1.7.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc +18 -7
- package/.github/dependabot.yml +5 -0
- package/.github/workflows/main.yml +79 -0
- package/.github/workflows/stale.yml +14 -0
- package/.jsdoc.json +4 -5
- package/.mocharc.json +8 -0
- package/.run/{Mocha Unit Tests.run.xml → Mocha Unit Tests (watch).run.xml } +6 -3
- package/.run/Template Mocha.run.xml +17 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +183 -71
- package/dist/aicc.js +3822 -7030
- package/dist/aicc.js.map +1 -1
- package/dist/aicc.min.js +2 -40
- package/dist/aicc.min.js.map +1 -0
- package/dist/scorm-again.js +5965 -10498
- package/dist/scorm-again.js.map +1 -1
- package/dist/scorm-again.min.js +2 -52
- package/dist/scorm-again.min.js.map +1 -0
- package/dist/scorm12.js +3028 -5373
- package/dist/scorm12.js.map +1 -1
- package/dist/scorm12.min.js +2 -34
- package/dist/scorm12.min.js.map +1 -0
- package/dist/scorm2004.js +4054 -6693
- package/dist/scorm2004.js.map +1 -1
- package/dist/scorm2004.min.js +2 -40
- package/dist/scorm2004.min.js.map +1 -0
- package/eslint.config.js +21 -0
- package/package.json +76 -34
- package/results.json +34254 -0
- package/src/AICC.ts +72 -0
- package/src/BaseAPI.ts +1300 -0
- package/src/Scorm12API.ts +387 -0
- package/src/Scorm2004API.ts +688 -0
- package/src/cmi/aicc/attempts.ts +94 -0
- package/src/cmi/aicc/cmi.ts +100 -0
- package/src/cmi/aicc/core.ts +360 -0
- package/src/cmi/aicc/evaluation.ts +157 -0
- package/src/cmi/aicc/paths.ts +180 -0
- package/src/cmi/aicc/student_data.ts +86 -0
- package/src/cmi/aicc/student_demographics.ts +367 -0
- package/src/cmi/aicc/student_preferences.ts +176 -0
- package/src/cmi/aicc/tries.ts +116 -0
- package/src/cmi/aicc/validation.ts +25 -0
- package/src/cmi/common/array.ts +77 -0
- package/src/cmi/common/base_cmi.ts +46 -0
- package/src/cmi/common/score.ts +203 -0
- package/src/cmi/common/validation.ts +60 -0
- package/src/cmi/scorm12/cmi.ts +224 -0
- package/src/cmi/scorm12/interactions.ts +368 -0
- package/src/cmi/scorm12/nav.ts +54 -0
- package/src/cmi/scorm12/objectives.ts +112 -0
- package/src/cmi/scorm12/student_data.ts +130 -0
- package/src/cmi/scorm12/student_preference.ts +158 -0
- package/src/cmi/scorm12/validation.ts +48 -0
- package/src/cmi/scorm2004/adl.ts +272 -0
- package/src/cmi/scorm2004/cmi.ts +599 -0
- package/src/cmi/scorm2004/comments.ts +163 -0
- package/src/cmi/scorm2004/interactions.ts +466 -0
- package/src/cmi/scorm2004/learner_preference.ts +152 -0
- package/src/cmi/scorm2004/objectives.ts +212 -0
- package/src/cmi/scorm2004/score.ts +78 -0
- package/src/cmi/scorm2004/validation.ts +42 -0
- package/src/constants/api_constants.ts +318 -0
- package/src/constants/default_settings.ts +81 -0
- package/src/constants/enums.ts +5 -0
- package/src/constants/error_codes.ts +88 -0
- package/src/constants/language_constants.ts +394 -0
- package/src/constants/regex.ts +97 -0
- package/src/constants/{response_constants.js → response_constants.ts} +69 -62
- package/src/exceptions.ts +154 -0
- package/src/exports/aicc.js +1 -1
- package/src/exports/scorm-again.js +3 -3
- package/src/exports/scorm12.js +1 -1
- package/src/exports/scorm2004.js +1 -1
- package/src/helpers/scheduled_commit.ts +42 -0
- package/src/interfaces/IBaseAPI.ts +35 -0
- package/src/types/api_types.ts +32 -0
- package/src/utilities/debounce.ts +31 -0
- package/src/utilities.ts +338 -0
- package/tea.yaml +6 -0
- package/test/{AICC.spec.js → AICC.spec.ts} +79 -71
- package/test/Scorm12API.spec.ts +833 -0
- package/test/Scorm2004API.spec.ts +1298 -0
- package/test/api_helpers.ts +176 -0
- package/test/cmi/aicc_cmi.spec.ts +845 -0
- package/test/cmi/{scorm12_cmi.spec.js → scorm12_cmi.spec.ts} +253 -271
- package/test/cmi/scorm2004_cmi.spec.ts +1031 -0
- package/test/cmi_helpers.ts +207 -0
- package/test/exceptions.spec.ts +79 -0
- package/test/field_values.ts +202 -0
- package/test/types/api_types.spec.ts +126 -0
- package/test/utilities/debounce.spec.ts +56 -0
- package/test/utilities.spec.ts +322 -0
- package/tsconfig.json +18 -0
- package/webpack.config.js +65 -0
- package/.circleci/config.yml +0 -99
- package/.codeclimate.yml +0 -7
- package/.eslintrc.js +0 -36
- package/src/.flowconfig +0 -11
- package/src/AICC.js +0 -68
- package/src/BaseAPI.js +0 -1275
- package/src/Scorm12API.js +0 -308
- package/src/Scorm2004API.js +0 -572
- package/src/cmi/aicc_cmi.js +0 -1141
- package/src/cmi/common.js +0 -328
- package/src/cmi/scorm12_cmi.js +0 -1312
- package/src/cmi/scorm2004_cmi.js +0 -1692
- package/src/constants/api_constants.js +0 -218
- package/src/constants/error_codes.js +0 -87
- package/src/constants/language_constants.js +0 -76
- package/src/constants/regex.js +0 -84
- package/src/exceptions.js +0 -104
- package/src/utilities.js +0 -242
- package/test/Scorm12API.spec.js +0 -528
- package/test/Scorm2004API.spec.js +0 -775
- package/test/abstract_classes.spec.js +0 -17
- package/test/api_helpers.js +0 -128
- package/test/cmi/aicc_cmi.spec.js +0 -684
- package/test/cmi/scorm2004_cmi.spec.js +0 -1066
- package/test/cmi_helpers.js +0 -161
- package/test/exceptions.spec.js +0 -71
- package/test/field_values.js +0 -353
- package/test/utilities.spec.js +0 -339
- package/webpack.js +0 -78
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import BaseAPI from "./BaseAPI";
|
|
2
|
+
import { CMI } from "./cmi/scorm2004/cmi";
|
|
3
|
+
import * as Utilities from "./utilities";
|
|
4
|
+
import { stringMatches } from "./utilities";
|
|
5
|
+
import APIConstants from "./constants/api_constants";
|
|
6
|
+
import ErrorCodes from "./constants/error_codes";
|
|
7
|
+
import { CorrectResponses, ResponseType } from "./constants/response_constants";
|
|
8
|
+
import ValidLanguages from "./constants/language_constants";
|
|
9
|
+
import Regex from "./constants/regex";
|
|
10
|
+
import regex from "./constants/regex";
|
|
11
|
+
import { CMIArray } from "./cmi/common/array";
|
|
12
|
+
import { BaseCMI } from "./cmi/common/base_cmi";
|
|
13
|
+
import {
|
|
14
|
+
CMIInteractionsCorrectResponsesObject,
|
|
15
|
+
CMIInteractionsObject,
|
|
16
|
+
CMIInteractionsObjectivesObject,
|
|
17
|
+
} from "./cmi/scorm2004/interactions";
|
|
18
|
+
import { CMICommentsObject } from "./cmi/scorm2004/comments";
|
|
19
|
+
import { CMIObjectivesObject } from "./cmi/scorm2004/objectives";
|
|
20
|
+
import { ADL } from "./cmi/scorm2004/adl";
|
|
21
|
+
import { RefObject, ResultObject, Settings } from "./types/api_types";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* API class for SCORM 2004
|
|
25
|
+
*/
|
|
26
|
+
export default class Scorm2004API extends BaseAPI {
|
|
27
|
+
private _version: string = "1.0";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Constructor for SCORM 2004 API
|
|
31
|
+
* @param {Settings} settings
|
|
32
|
+
*/
|
|
33
|
+
constructor(settings?: Settings) {
|
|
34
|
+
if (settings) {
|
|
35
|
+
if (settings.mastery_override === undefined) {
|
|
36
|
+
settings.mastery_override = false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
super(ErrorCodes.scorm2004, settings);
|
|
41
|
+
|
|
42
|
+
this.cmi = new CMI();
|
|
43
|
+
this.adl = new ADL();
|
|
44
|
+
|
|
45
|
+
// Rename functions to match 2004 Spec and expose to modules
|
|
46
|
+
this.Initialize = this.lmsInitialize;
|
|
47
|
+
this.Terminate = this.lmsFinish;
|
|
48
|
+
this.GetValue = this.lmsGetValue;
|
|
49
|
+
this.SetValue = this.lmsSetValue;
|
|
50
|
+
this.Commit = this.lmsCommit;
|
|
51
|
+
this.GetLastError = this.lmsGetLastError;
|
|
52
|
+
this.GetErrorString = this.lmsGetErrorString;
|
|
53
|
+
this.GetDiagnostic = this.lmsGetDiagnostic;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public cmi: CMI;
|
|
57
|
+
public adl: ADL;
|
|
58
|
+
|
|
59
|
+
public Initialize: () => string;
|
|
60
|
+
public Terminate: () => string;
|
|
61
|
+
public GetValue: (CMIElement: string) => string;
|
|
62
|
+
public SetValue: (CMIElement: string, value: any) => string;
|
|
63
|
+
public Commit: () => string;
|
|
64
|
+
public GetLastError: () => string;
|
|
65
|
+
public GetErrorString: (CMIErrorCode: string | number) => string;
|
|
66
|
+
public GetDiagnostic: (CMIErrorCode: string | number) => string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Called when the API needs to be reset
|
|
70
|
+
*/
|
|
71
|
+
reset(settings?: Settings) {
|
|
72
|
+
this.commonReset(settings);
|
|
73
|
+
|
|
74
|
+
this.cmi = new CMI();
|
|
75
|
+
this.adl = new ADL();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Getter for _version
|
|
80
|
+
* @return {string}
|
|
81
|
+
*/
|
|
82
|
+
get version(): string {
|
|
83
|
+
return this._version;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @return {string} bool
|
|
88
|
+
*/
|
|
89
|
+
lmsInitialize(): string {
|
|
90
|
+
this.cmi.initialize();
|
|
91
|
+
return this.initialize("Initialize");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @return {string} bool
|
|
96
|
+
*/
|
|
97
|
+
lmsFinish(): string {
|
|
98
|
+
(async () => {
|
|
99
|
+
await this.internalFinish();
|
|
100
|
+
})();
|
|
101
|
+
return APIConstants.global.SCORM_TRUE;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async internalFinish(): Promise<string> {
|
|
105
|
+
const result = await this.terminate("Terminate", true);
|
|
106
|
+
|
|
107
|
+
if (result === APIConstants.global.SCORM_TRUE) {
|
|
108
|
+
if (this.adl.nav.request !== "_none_") {
|
|
109
|
+
const navActions: { [key: string]: string } = {
|
|
110
|
+
continue: "SequenceNext",
|
|
111
|
+
previous: "SequencePrevious",
|
|
112
|
+
choice: "SequenceChoice",
|
|
113
|
+
jump: "SequenceJump",
|
|
114
|
+
exit: "SequenceExit",
|
|
115
|
+
exitAll: "SequenceExitAll",
|
|
116
|
+
abandon: "SequenceAbandon",
|
|
117
|
+
abandonAll: "SequenceAbandonAll",
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
let request = this.adl.nav.request;
|
|
121
|
+
const choiceJumpRegex = new RegExp(regex.scorm2004.NAVEvent);
|
|
122
|
+
const matches = request.match(choiceJumpRegex);
|
|
123
|
+
let target = "";
|
|
124
|
+
if (matches && matches.length > 2) {
|
|
125
|
+
target = matches[2];
|
|
126
|
+
request = matches[1].replace(target, "");
|
|
127
|
+
}
|
|
128
|
+
const action = navActions[request];
|
|
129
|
+
if (action) {
|
|
130
|
+
this.processListeners(action, "adl.nav.request", target);
|
|
131
|
+
}
|
|
132
|
+
} else if (this.settings.autoProgress) {
|
|
133
|
+
this.processListeners("SequenceNext");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} CMIElement
|
|
142
|
+
* @return {string}
|
|
143
|
+
*/
|
|
144
|
+
lmsGetValue(CMIElement: string): string {
|
|
145
|
+
const adlNavRequestRegex =
|
|
146
|
+
"^adl\\.nav\\.request_valid\\.(choice|jump)\\.{target=\\S{0,}([a-zA-Z0-9-_]+)}$";
|
|
147
|
+
if (stringMatches(CMIElement, adlNavRequestRegex)) {
|
|
148
|
+
const matches = CMIElement.match(adlNavRequestRegex);
|
|
149
|
+
const request = matches[1];
|
|
150
|
+
const target = matches[2].replace("{target=", "").replace("}", "");
|
|
151
|
+
if (request === "choice" || request === "jump") {
|
|
152
|
+
if (this.settings.scoItemIdValidator) {
|
|
153
|
+
return String(this.settings.scoItemIdValidator(target));
|
|
154
|
+
}
|
|
155
|
+
return String(this.settings.scoItemIds.includes(target));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return this.getValue("GetValue", true, CMIElement);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {string} CMIElement
|
|
163
|
+
* @param {any} value
|
|
164
|
+
* @return {string}
|
|
165
|
+
*/
|
|
166
|
+
lmsSetValue(CMIElement: string, value: any): string {
|
|
167
|
+
return this.setValue("SetValue", "Commit", true, CMIElement, value);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Orders LMS to store all content parameters
|
|
172
|
+
*
|
|
173
|
+
* @return {string} bool
|
|
174
|
+
*/
|
|
175
|
+
lmsCommit(): string {
|
|
176
|
+
(async () => {
|
|
177
|
+
await this.commit("Commit");
|
|
178
|
+
})();
|
|
179
|
+
return APIConstants.global.SCORM_TRUE;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Returns last error code
|
|
184
|
+
*
|
|
185
|
+
* @return {string}
|
|
186
|
+
*/
|
|
187
|
+
lmsGetLastError(): string {
|
|
188
|
+
return this.getLastError("GetLastError");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Returns the errorNumber error description
|
|
193
|
+
*
|
|
194
|
+
* @param {(string|number)} CMIErrorCode
|
|
195
|
+
* @return {string}
|
|
196
|
+
*/
|
|
197
|
+
lmsGetErrorString(CMIErrorCode: string | number): string {
|
|
198
|
+
return this.getErrorString("GetErrorString", CMIErrorCode);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Returns a comprehensive description of the errorNumber error.
|
|
203
|
+
*
|
|
204
|
+
* @param {(string|number)} CMIErrorCode
|
|
205
|
+
* @return {string}
|
|
206
|
+
*/
|
|
207
|
+
lmsGetDiagnostic(CMIErrorCode: string | number): string {
|
|
208
|
+
return this.getDiagnostic("GetDiagnostic", CMIErrorCode);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Sets a value on the CMI Object
|
|
213
|
+
*
|
|
214
|
+
* @param {string} CMIElement
|
|
215
|
+
* @param {any} value
|
|
216
|
+
* @return {string}
|
|
217
|
+
*/
|
|
218
|
+
setCMIValue(CMIElement: string, value: any): string {
|
|
219
|
+
return this._commonSetCMIValue("SetValue", true, CMIElement, value);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Gets or builds a new child element to add to the array.
|
|
224
|
+
*
|
|
225
|
+
* @param {string} CMIElement
|
|
226
|
+
* @param {any} value
|
|
227
|
+
* @param {boolean} foundFirstIndex
|
|
228
|
+
* @return {BaseCMI|null}
|
|
229
|
+
*/
|
|
230
|
+
getChildElement(
|
|
231
|
+
CMIElement: string,
|
|
232
|
+
value: any,
|
|
233
|
+
foundFirstIndex: boolean,
|
|
234
|
+
): BaseCMI | null {
|
|
235
|
+
if (stringMatches(CMIElement, "cmi\\.objectives\\.\\d+")) {
|
|
236
|
+
return new CMIObjectivesObject();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (foundFirstIndex) {
|
|
240
|
+
if (
|
|
241
|
+
stringMatches(
|
|
242
|
+
CMIElement,
|
|
243
|
+
"cmi\\.interactions\\.\\d+\\.correct_responses\\.\\d+",
|
|
244
|
+
)
|
|
245
|
+
) {
|
|
246
|
+
return this.createCorrectResponsesObject(CMIElement, value);
|
|
247
|
+
} else if (
|
|
248
|
+
stringMatches(
|
|
249
|
+
CMIElement,
|
|
250
|
+
"cmi\\.interactions\\.\\d+\\.objectives\\.\\d+",
|
|
251
|
+
)
|
|
252
|
+
) {
|
|
253
|
+
return new CMIInteractionsObjectivesObject();
|
|
254
|
+
}
|
|
255
|
+
} else if (stringMatches(CMIElement, "cmi\\.interactions\\.\\d+")) {
|
|
256
|
+
return new CMIInteractionsObject();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (stringMatches(CMIElement, "cmi\\.comments_from_learner\\.\\d+")) {
|
|
260
|
+
return new CMICommentsObject();
|
|
261
|
+
} else if (stringMatches(CMIElement, "cmi\\.comments_from_lms\\.\\d+")) {
|
|
262
|
+
return new CMICommentsObject(true);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private createCorrectResponsesObject(
|
|
269
|
+
CMIElement: string,
|
|
270
|
+
value: any,
|
|
271
|
+
): BaseCMI | null {
|
|
272
|
+
const parts = CMIElement.split(".");
|
|
273
|
+
const index = Number(parts[2]);
|
|
274
|
+
const interaction = this.cmi.interactions.childArray[index];
|
|
275
|
+
|
|
276
|
+
if (this.isInitialized()) {
|
|
277
|
+
if (!interaction.type) {
|
|
278
|
+
this.throwSCORMError(ErrorCodes.scorm2004.DEPENDENCY_NOT_ESTABLISHED);
|
|
279
|
+
} else {
|
|
280
|
+
this.checkDuplicateChoiceResponse(interaction, value);
|
|
281
|
+
const response_type = CorrectResponses[interaction.type];
|
|
282
|
+
if (response_type) {
|
|
283
|
+
this.checkValidResponseType(response_type, value, interaction.type);
|
|
284
|
+
} else {
|
|
285
|
+
this.throwSCORMError(
|
|
286
|
+
ErrorCodes.scorm2004.GENERAL_SET_FAILURE,
|
|
287
|
+
"Incorrect Response Type: " + interaction.type,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (this.lastErrorCode === "0") {
|
|
294
|
+
return new CMIInteractionsCorrectResponsesObject();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Checks for valid response types
|
|
302
|
+
* @param {object} response_type
|
|
303
|
+
* @param {any} value
|
|
304
|
+
* @param {string} interaction_type
|
|
305
|
+
*/
|
|
306
|
+
checkValidResponseType(
|
|
307
|
+
response_type: ResponseType,
|
|
308
|
+
value: any,
|
|
309
|
+
interaction_type: string,
|
|
310
|
+
) {
|
|
311
|
+
let nodes = [];
|
|
312
|
+
if (response_type?.delimiter) {
|
|
313
|
+
nodes = String(value).split(response_type.delimiter);
|
|
314
|
+
} else {
|
|
315
|
+
nodes[0] = value;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (nodes.length > 0 && nodes.length <= response_type.max) {
|
|
319
|
+
this.checkCorrectResponseValue(interaction_type, nodes, value);
|
|
320
|
+
} else if (nodes.length > response_type.max) {
|
|
321
|
+
this.throwSCORMError(
|
|
322
|
+
ErrorCodes.scorm2004.GENERAL_SET_FAILURE,
|
|
323
|
+
"Data Model Element Pattern Too Long",
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Checks for duplicate 'choice' responses.
|
|
330
|
+
* @param {CMIInteractionsObject} interaction
|
|
331
|
+
* @param {any} value
|
|
332
|
+
*/
|
|
333
|
+
checkDuplicateChoiceResponse(interaction: CMIInteractionsObject, value: any) {
|
|
334
|
+
const interaction_count = interaction.correct_responses._count;
|
|
335
|
+
if (interaction.type === "choice") {
|
|
336
|
+
for (
|
|
337
|
+
let i = 0;
|
|
338
|
+
i < interaction_count && this.lastErrorCode === "0";
|
|
339
|
+
i++
|
|
340
|
+
) {
|
|
341
|
+
const response = interaction.correct_responses.childArray[i];
|
|
342
|
+
if (response.pattern === value) {
|
|
343
|
+
this.throwSCORMError(ErrorCodes.scorm2004.GENERAL_SET_FAILURE);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Validate correct response.
|
|
351
|
+
* @param {string} CMIElement
|
|
352
|
+
* @param {*} value
|
|
353
|
+
*/
|
|
354
|
+
validateCorrectResponse(CMIElement: string, value: any) {
|
|
355
|
+
const parts = CMIElement.split(".");
|
|
356
|
+
const index = Number(parts[2]);
|
|
357
|
+
const pattern_index = Number(parts[4]);
|
|
358
|
+
const interaction = this.cmi.interactions.childArray[index];
|
|
359
|
+
|
|
360
|
+
const interaction_count = interaction.correct_responses._count;
|
|
361
|
+
this.checkDuplicateChoiceResponse(interaction, value);
|
|
362
|
+
|
|
363
|
+
const response_type = CorrectResponses[interaction.type];
|
|
364
|
+
if (
|
|
365
|
+
typeof response_type.limit === "undefined" ||
|
|
366
|
+
interaction_count <= response_type.limit
|
|
367
|
+
) {
|
|
368
|
+
this.checkValidResponseType(response_type, value, interaction.type);
|
|
369
|
+
|
|
370
|
+
if (
|
|
371
|
+
(this.lastErrorCode === "0" &&
|
|
372
|
+
(!response_type.duplicate ||
|
|
373
|
+
!this.checkDuplicatedPattern(
|
|
374
|
+
interaction.correct_responses,
|
|
375
|
+
pattern_index,
|
|
376
|
+
value,
|
|
377
|
+
))) ||
|
|
378
|
+
(this.lastErrorCode === "0" && value === "")
|
|
379
|
+
) {
|
|
380
|
+
// do nothing, we want the inverse
|
|
381
|
+
} else {
|
|
382
|
+
if (this.lastErrorCode === "0") {
|
|
383
|
+
this.throwSCORMError(
|
|
384
|
+
ErrorCodes.scorm2004.GENERAL_SET_FAILURE,
|
|
385
|
+
"Data Model Element Pattern Already Exists",
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
this.throwSCORMError(
|
|
391
|
+
ErrorCodes.scorm2004.GENERAL_SET_FAILURE,
|
|
392
|
+
"Data Model Element Collection Limit Reached",
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Gets a value from the CMI Object
|
|
399
|
+
*
|
|
400
|
+
* @param {string} CMIElement
|
|
401
|
+
* @return {*}
|
|
402
|
+
*/
|
|
403
|
+
getCMIValue(CMIElement: string): any {
|
|
404
|
+
return this._commonGetCMIValue("GetValue", true, CMIElement);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Returns the message that corresponds to errorNumber.
|
|
409
|
+
*
|
|
410
|
+
* @param {(string|number)} errorNumber
|
|
411
|
+
* @param {boolean} detail
|
|
412
|
+
* @return {string}
|
|
413
|
+
*/
|
|
414
|
+
getLmsErrorMessageDetails(
|
|
415
|
+
errorNumber: string | number,
|
|
416
|
+
detail: boolean,
|
|
417
|
+
): string {
|
|
418
|
+
let basicMessage = "";
|
|
419
|
+
let detailMessage = "";
|
|
420
|
+
|
|
421
|
+
// Set error number to string since inconsistent from modules if string or number
|
|
422
|
+
errorNumber = String(errorNumber);
|
|
423
|
+
if (APIConstants.scorm2004.error_descriptions[errorNumber]) {
|
|
424
|
+
basicMessage =
|
|
425
|
+
APIConstants.scorm2004.error_descriptions[errorNumber].basicMessage;
|
|
426
|
+
detailMessage =
|
|
427
|
+
APIConstants.scorm2004.error_descriptions[errorNumber].detailMessage;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return detail ? detailMessage : basicMessage;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Check to see if a correct_response value has been duplicated
|
|
435
|
+
* @param {CMIArray} correct_response
|
|
436
|
+
* @param {number} current_index
|
|
437
|
+
* @param {*} value
|
|
438
|
+
* @return {boolean}
|
|
439
|
+
*/
|
|
440
|
+
checkDuplicatedPattern(
|
|
441
|
+
correct_response: CMIArray,
|
|
442
|
+
current_index: number,
|
|
443
|
+
value: any,
|
|
444
|
+
): boolean {
|
|
445
|
+
let found = false;
|
|
446
|
+
const count = correct_response._count;
|
|
447
|
+
for (let i = 0; i < count && !found; i++) {
|
|
448
|
+
if (i !== current_index && correct_response.childArray[i] === value) {
|
|
449
|
+
found = true;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return found;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Checks for a valid correct_response value
|
|
457
|
+
* @param {string} interaction_type
|
|
458
|
+
* @param {Array} nodes
|
|
459
|
+
* @param {*} value
|
|
460
|
+
*/
|
|
461
|
+
checkCorrectResponseValue(
|
|
462
|
+
interaction_type: string,
|
|
463
|
+
nodes: Array<any>,
|
|
464
|
+
value: any,
|
|
465
|
+
) {
|
|
466
|
+
const response = CorrectResponses[interaction_type];
|
|
467
|
+
const formatRegex = new RegExp(response.format);
|
|
468
|
+
for (let i = 0; i < nodes.length && this.lastErrorCode === "0"; i++) {
|
|
469
|
+
if (
|
|
470
|
+
interaction_type.match(
|
|
471
|
+
"^(fill-in|long-fill-in|matching|performance|sequencing)$",
|
|
472
|
+
)
|
|
473
|
+
) {
|
|
474
|
+
nodes[i] = this.removeCorrectResponsePrefixes(nodes[i]);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (response?.delimiter2) {
|
|
478
|
+
const values = nodes[i].split(response.delimiter2);
|
|
479
|
+
if (values.length === 2) {
|
|
480
|
+
const matches = values[0].match(formatRegex);
|
|
481
|
+
if (!matches) {
|
|
482
|
+
this.throwSCORMError(ErrorCodes.scorm2004.TYPE_MISMATCH);
|
|
483
|
+
} else {
|
|
484
|
+
if (
|
|
485
|
+
!response.format2 ||
|
|
486
|
+
!values[1].match(new RegExp(response.format2))
|
|
487
|
+
) {
|
|
488
|
+
this.throwSCORMError(ErrorCodes.scorm2004.TYPE_MISMATCH);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
this.throwSCORMError(ErrorCodes.scorm2004.TYPE_MISMATCH);
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
const matches = nodes[i].match(formatRegex);
|
|
496
|
+
if (
|
|
497
|
+
(!matches && value !== "") ||
|
|
498
|
+
(!matches && interaction_type === "true-false")
|
|
499
|
+
) {
|
|
500
|
+
this.throwSCORMError(ErrorCodes.scorm2004.TYPE_MISMATCH);
|
|
501
|
+
} else {
|
|
502
|
+
if (interaction_type === "numeric" && nodes.length > 1) {
|
|
503
|
+
if (Number(nodes[0]) > Number(nodes[1])) {
|
|
504
|
+
this.throwSCORMError(ErrorCodes.scorm2004.TYPE_MISMATCH);
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
if (nodes[i] !== "" && response.unique) {
|
|
508
|
+
for (let j = 0; j < i && this.lastErrorCode === "0"; j++) {
|
|
509
|
+
if (nodes[i] === nodes[j]) {
|
|
510
|
+
this.throwSCORMError(ErrorCodes.scorm2004.TYPE_MISMATCH);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Remove prefixes from correct_response
|
|
522
|
+
* @param {string} node
|
|
523
|
+
* @return {*}
|
|
524
|
+
*/
|
|
525
|
+
removeCorrectResponsePrefixes(node: string): any {
|
|
526
|
+
let seenOrder = false;
|
|
527
|
+
let seenCase = false;
|
|
528
|
+
let seenLang = false;
|
|
529
|
+
|
|
530
|
+
const prefixRegex = new RegExp(
|
|
531
|
+
"^({(lang|case_matters|order_matters)=([^}]+)})",
|
|
532
|
+
);
|
|
533
|
+
let matches = node.match(prefixRegex);
|
|
534
|
+
let langMatches = null;
|
|
535
|
+
while (matches) {
|
|
536
|
+
switch (matches[2]) {
|
|
537
|
+
case "lang":
|
|
538
|
+
langMatches = node.match(Regex.scorm2004.CMILangcr);
|
|
539
|
+
if (langMatches) {
|
|
540
|
+
const lang = langMatches[3];
|
|
541
|
+
if (lang !== undefined && lang.length > 0) {
|
|
542
|
+
if (!ValidLanguages.includes(lang.toLowerCase())) {
|
|
543
|
+
this.throwSCORMError(ErrorCodes.scorm2004.TYPE_MISMATCH);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
seenLang = true;
|
|
548
|
+
break;
|
|
549
|
+
case "case_matters":
|
|
550
|
+
if (!seenLang && !seenOrder && !seenCase) {
|
|
551
|
+
if (matches[3] !== "true" && matches[3] !== "false") {
|
|
552
|
+
this.throwSCORMError(ErrorCodes.scorm2004.TYPE_MISMATCH);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
seenCase = true;
|
|
557
|
+
break;
|
|
558
|
+
case "order_matters":
|
|
559
|
+
if (!seenCase && !seenLang && !seenOrder) {
|
|
560
|
+
if (matches[3] !== "true" && matches[3] !== "false") {
|
|
561
|
+
this.throwSCORMError(ErrorCodes.scorm2004.TYPE_MISMATCH);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
seenOrder = true;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
node = node.substring(matches[1].length);
|
|
569
|
+
matches = node.match(prefixRegex);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return node;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Replace the whole API with another
|
|
577
|
+
* @param {Scorm2004API} newAPI
|
|
578
|
+
*/
|
|
579
|
+
replaceWithAnotherScormAPI(newAPI: Scorm2004API) {
|
|
580
|
+
// Data Model
|
|
581
|
+
this.cmi = newAPI.cmi;
|
|
582
|
+
this.adl = newAPI.adl;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Render the cmi object to the proper format for LMS commit
|
|
587
|
+
*
|
|
588
|
+
* @param {boolean} terminateCommit
|
|
589
|
+
* @return {object|Array}
|
|
590
|
+
*/
|
|
591
|
+
renderCommitCMI(terminateCommit: boolean): object | Array<any> {
|
|
592
|
+
const cmiExport: RefObject = this.renderCMIToJSONObject();
|
|
593
|
+
|
|
594
|
+
if (terminateCommit) {
|
|
595
|
+
cmiExport.cmi.total_time = this.cmi.getCurrentTotalTime();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const result = [];
|
|
599
|
+
const flattened: RefObject = Utilities.flatten(cmiExport);
|
|
600
|
+
switch (this.settings.dataCommitFormat) {
|
|
601
|
+
case "flattened":
|
|
602
|
+
return Utilities.flatten(cmiExport);
|
|
603
|
+
case "params":
|
|
604
|
+
for (const item in flattened) {
|
|
605
|
+
if ({}.hasOwnProperty.call(flattened, item)) {
|
|
606
|
+
result.push(`${item}=${flattened[item]}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return result;
|
|
610
|
+
case "json":
|
|
611
|
+
default:
|
|
612
|
+
return cmiExport;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Attempts to store the data to the LMS
|
|
618
|
+
*
|
|
619
|
+
* @param {boolean} terminateCommit
|
|
620
|
+
* @return {ResultObject}
|
|
621
|
+
*/
|
|
622
|
+
async storeData(terminateCommit: boolean): Promise<ResultObject> {
|
|
623
|
+
if (terminateCommit) {
|
|
624
|
+
if (this.cmi.mode === "normal") {
|
|
625
|
+
if (this.cmi.credit === "credit") {
|
|
626
|
+
if (this.cmi.completion_threshold && this.cmi.progress_measure) {
|
|
627
|
+
if (this.cmi.progress_measure >= this.cmi.completion_threshold) {
|
|
628
|
+
this.cmi.completion_status = "completed";
|
|
629
|
+
} else {
|
|
630
|
+
this.cmi.completion_status = "incomplete";
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (this.cmi.scaled_passing_score && this.cmi.score.scaled) {
|
|
634
|
+
if (this.cmi.score.scaled >= this.cmi.scaled_passing_score) {
|
|
635
|
+
this.cmi.success_status = "passed";
|
|
636
|
+
} else {
|
|
637
|
+
this.cmi.success_status = "failed";
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
let navRequest = false;
|
|
645
|
+
if (
|
|
646
|
+
this.adl.nav.request !== this.startingData?.adl?.nav?.request &&
|
|
647
|
+
this.adl.nav.request !== "_none_"
|
|
648
|
+
) {
|
|
649
|
+
this.adl.nav.request = encodeURIComponent(this.adl.nav.request);
|
|
650
|
+
navRequest = true;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const commitObject = this.renderCommitCMI(
|
|
654
|
+
terminateCommit || this.settings.alwaysSendTotalTime,
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
if (this.apiLogLevel === APIConstants.global.LOG_LEVEL_DEBUG) {
|
|
658
|
+
console.debug(
|
|
659
|
+
"Commit (terminated: " + (terminateCommit ? "yes" : "no") + "): ",
|
|
660
|
+
);
|
|
661
|
+
console.debug(commitObject);
|
|
662
|
+
}
|
|
663
|
+
if (typeof this.settings.lmsCommitUrl === "string") {
|
|
664
|
+
const result = await this.processHttpRequest(
|
|
665
|
+
this.settings.lmsCommitUrl,
|
|
666
|
+
commitObject,
|
|
667
|
+
terminateCommit,
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
// check if this is a sequencing call, and then call the necessary JS
|
|
671
|
+
{
|
|
672
|
+
if (
|
|
673
|
+
navRequest &&
|
|
674
|
+
result.navRequest !== undefined &&
|
|
675
|
+
result.navRequest !== ""
|
|
676
|
+
) {
|
|
677
|
+
Function(`"use strict";(() => { ${result.navRequest} })()`)();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return result;
|
|
681
|
+
} else {
|
|
682
|
+
return {
|
|
683
|
+
result: APIConstants.global.SCORM_TRUE,
|
|
684
|
+
errorCode: 0,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|