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,154 @@
|
|
|
1
|
+
import APIConstants from "./constants/api_constants";
|
|
2
|
+
|
|
3
|
+
const scorm12_errors = APIConstants.scorm12.error_descriptions;
|
|
4
|
+
const aicc_errors = APIConstants.aicc.error_descriptions;
|
|
5
|
+
const scorm2004_errors = APIConstants.scorm2004.error_descriptions;
|
|
6
|
+
|
|
7
|
+
type APIError = {
|
|
8
|
+
errorCode: number;
|
|
9
|
+
errorMessage: string;
|
|
10
|
+
detailedMessage: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export class BaseScormValidationError extends Error {
|
|
14
|
+
constructor(errorCode: number) {
|
|
15
|
+
super(errorCode.toString());
|
|
16
|
+
this._errorCode = errorCode;
|
|
17
|
+
this.name = "ScormValidationError";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private readonly _errorCode: number;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Getter for _errorCode
|
|
24
|
+
* @return {number}
|
|
25
|
+
*/
|
|
26
|
+
get errorCode(): number {
|
|
27
|
+
return this._errorCode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setMessage(message: string) {
|
|
31
|
+
this.message = message;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Base Validation Exception
|
|
37
|
+
*/
|
|
38
|
+
export class ValidationError
|
|
39
|
+
extends BaseScormValidationError
|
|
40
|
+
implements APIError
|
|
41
|
+
{
|
|
42
|
+
/**
|
|
43
|
+
* Constructor to take in an error message and code
|
|
44
|
+
* @param {number} errorCode
|
|
45
|
+
* @param {string} errorMessage
|
|
46
|
+
* @param {string} detailedMessage
|
|
47
|
+
*/
|
|
48
|
+
constructor(
|
|
49
|
+
errorCode: number,
|
|
50
|
+
errorMessage: string,
|
|
51
|
+
detailedMessage?: string,
|
|
52
|
+
) {
|
|
53
|
+
super(errorCode);
|
|
54
|
+
this.setMessage(errorMessage);
|
|
55
|
+
this._errorMessage = errorMessage;
|
|
56
|
+
if (detailedMessage) {
|
|
57
|
+
this._detailedMessage = detailedMessage;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private readonly _errorMessage: string;
|
|
62
|
+
private readonly _detailedMessage: string = "";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Getter for _errorMessage
|
|
66
|
+
* @return {string}
|
|
67
|
+
*/
|
|
68
|
+
get errorMessage(): string {
|
|
69
|
+
return this._errorMessage;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Getter for _detailedMessage
|
|
74
|
+
* @return {string}
|
|
75
|
+
*/
|
|
76
|
+
get detailedMessage(): string {
|
|
77
|
+
return this._detailedMessage;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* SCORM 1.2 Validation Error
|
|
83
|
+
*/
|
|
84
|
+
export class Scorm12ValidationError extends ValidationError {
|
|
85
|
+
/**
|
|
86
|
+
* Constructor to take in an error code
|
|
87
|
+
* @param {number} errorCode
|
|
88
|
+
*/
|
|
89
|
+
constructor(errorCode: number) {
|
|
90
|
+
if ({}.hasOwnProperty.call(scorm12_errors, String(errorCode))) {
|
|
91
|
+
super(
|
|
92
|
+
errorCode,
|
|
93
|
+
scorm12_errors[String(errorCode)].basicMessage,
|
|
94
|
+
scorm12_errors[String(errorCode)].detailMessage,
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
super(
|
|
98
|
+
101,
|
|
99
|
+
scorm12_errors["101"].basicMessage,
|
|
100
|
+
scorm12_errors["101"].detailMessage,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* AICC Validation Error
|
|
108
|
+
*/
|
|
109
|
+
export class AICCValidationError extends ValidationError {
|
|
110
|
+
/**
|
|
111
|
+
* Constructor to take in an error code
|
|
112
|
+
* @param {number} errorCode
|
|
113
|
+
*/
|
|
114
|
+
constructor(errorCode: number) {
|
|
115
|
+
if ({}.hasOwnProperty.call(aicc_errors, String(errorCode))) {
|
|
116
|
+
super(
|
|
117
|
+
errorCode,
|
|
118
|
+
aicc_errors[String(errorCode)].basicMessage,
|
|
119
|
+
aicc_errors[String(errorCode)].detailMessage,
|
|
120
|
+
);
|
|
121
|
+
} else {
|
|
122
|
+
super(
|
|
123
|
+
101,
|
|
124
|
+
aicc_errors["101"].basicMessage,
|
|
125
|
+
aicc_errors["101"].detailMessage,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* SCORM 2004 Validation Error
|
|
133
|
+
*/
|
|
134
|
+
export class Scorm2004ValidationError extends ValidationError {
|
|
135
|
+
/**
|
|
136
|
+
* Constructor to take in an error code
|
|
137
|
+
* @param {number} errorCode
|
|
138
|
+
*/
|
|
139
|
+
constructor(errorCode: number) {
|
|
140
|
+
if ({}.hasOwnProperty.call(scorm2004_errors, String(errorCode))) {
|
|
141
|
+
super(
|
|
142
|
+
errorCode,
|
|
143
|
+
scorm2004_errors[String(errorCode)].basicMessage,
|
|
144
|
+
scorm2004_errors[String(errorCode)].detailMessage,
|
|
145
|
+
);
|
|
146
|
+
} else {
|
|
147
|
+
super(
|
|
148
|
+
101,
|
|
149
|
+
scorm2004_errors["101"].basicMessage,
|
|
150
|
+
scorm2004_errors["101"].detailMessage,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
package/src/exports/aicc.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import Scorm2004API from
|
|
2
|
-
import Scorm12API from
|
|
3
|
-
import AICC from
|
|
1
|
+
import Scorm2004API from "../Scorm2004API.ts";
|
|
2
|
+
import Scorm12API from "../Scorm12API.ts";
|
|
3
|
+
import AICC from "../AICC.ts";
|
|
4
4
|
|
|
5
5
|
window.Scorm12API = Scorm12API;
|
|
6
6
|
window.Scorm2004API = Scorm2004API;
|
package/src/exports/scorm12.js
CHANGED
package/src/exports/scorm2004.js
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import BaseAPI from "../BaseAPI";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Private class that wraps a timeout call to the commit() function
|
|
5
|
+
*/
|
|
6
|
+
export class ScheduledCommit {
|
|
7
|
+
private _API;
|
|
8
|
+
private _cancelled = false;
|
|
9
|
+
private readonly _timeout;
|
|
10
|
+
private readonly _callback;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Constructor for ScheduledCommit
|
|
14
|
+
* @param {BaseAPI} API
|
|
15
|
+
* @param {number} when
|
|
16
|
+
* @param {string} callback
|
|
17
|
+
*/
|
|
18
|
+
constructor(API: BaseAPI, when: number, callback: string) {
|
|
19
|
+
this._API = API;
|
|
20
|
+
this._timeout = setTimeout(this.wrapper.bind(this), when);
|
|
21
|
+
this._callback = callback;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cancel any currently scheduled commit
|
|
26
|
+
*/
|
|
27
|
+
cancel() {
|
|
28
|
+
this._cancelled = true;
|
|
29
|
+
if (this._timeout) {
|
|
30
|
+
clearTimeout(this._timeout);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Wrap the API commit call to check if the call has already been cancelled
|
|
36
|
+
*/
|
|
37
|
+
wrapper() {
|
|
38
|
+
if (!this._cancelled) {
|
|
39
|
+
(async () => await this._API.commit(this._callback))();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { RefObject, ResultObject } from "../types/api_types";
|
|
2
|
+
import { BaseCMI } from "../cmi/common/base_cmi";
|
|
3
|
+
|
|
4
|
+
export interface IBaseAPI {
|
|
5
|
+
cmi: BaseCMI;
|
|
6
|
+
startingData?: RefObject;
|
|
7
|
+
|
|
8
|
+
initialize(
|
|
9
|
+
callbackName: string,
|
|
10
|
+
initializeMessage?: string,
|
|
11
|
+
terminationMessage?: string,
|
|
12
|
+
): string;
|
|
13
|
+
lmsInitialize(): string;
|
|
14
|
+
lmsFinish(): string;
|
|
15
|
+
lmsGetValue(CMIElement: string): string;
|
|
16
|
+
lmsSetValue(CMIElement: string, value: any): string;
|
|
17
|
+
lmsCommit(): string;
|
|
18
|
+
lmsGetLastError(): string;
|
|
19
|
+
lmsGetErrorString(CMIErrorCode: string | number): string;
|
|
20
|
+
lmsGetDiagnostic(CMIErrorCode: string | number): string;
|
|
21
|
+
storeData(_calculateTotalTime: boolean): Promise<ResultObject>;
|
|
22
|
+
renderCommitCMI(_terminateCommit: boolean): RefObject | Array<any>;
|
|
23
|
+
getLmsErrorMessageDetails(
|
|
24
|
+
_errorNumber: string | number,
|
|
25
|
+
_detail?: boolean,
|
|
26
|
+
): string;
|
|
27
|
+
getCMIValue(_CMIElement: string): string;
|
|
28
|
+
setCMIValue(_CMIElement: string, _value: any): string;
|
|
29
|
+
validateCorrectResponse(_CMIElement: string, _value: any): void;
|
|
30
|
+
getChildElement(
|
|
31
|
+
_CMIElement: string,
|
|
32
|
+
_value: any,
|
|
33
|
+
_foundFirstIndex: boolean,
|
|
34
|
+
): BaseCMI | null;
|
|
35
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type Settings = {
|
|
2
|
+
autocommit?: boolean;
|
|
3
|
+
autocommitSeconds?: number;
|
|
4
|
+
asyncCommit?: boolean;
|
|
5
|
+
sendFullCommit?: boolean;
|
|
6
|
+
lmsCommitUrl?: boolean | string;
|
|
7
|
+
dataCommitFormat?: string;
|
|
8
|
+
commitRequestDataType?: string;
|
|
9
|
+
autoProgress?: boolean;
|
|
10
|
+
logLevel?: number;
|
|
11
|
+
selfReportSessionTime?: boolean;
|
|
12
|
+
alwaysSendTotalTime?: boolean;
|
|
13
|
+
strict_errors?: boolean;
|
|
14
|
+
xhrHeaders?: RefObject;
|
|
15
|
+
xhrWithCredentials?: boolean;
|
|
16
|
+
responseHandler?: (response: Response) => Promise<ResultObject>;
|
|
17
|
+
requestHandler?: (commitObject: any) => any;
|
|
18
|
+
onLogMessage?: (messageLevel: number, logMessage: string) => void;
|
|
19
|
+
scoItemIds?: string[];
|
|
20
|
+
scoItemIdValidator?: false | ((scoItemId: string) => boolean);
|
|
21
|
+
mastery_override?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type RefObject = {
|
|
25
|
+
[key: string]: any;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ResultObject = {
|
|
29
|
+
result: string;
|
|
30
|
+
errorCode: number;
|
|
31
|
+
navRequest?: string;
|
|
32
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debounce function to delay the execution of a given function.
|
|
3
|
+
*
|
|
4
|
+
* @param func - The function to debounce.
|
|
5
|
+
* @param wait - The number of milliseconds to delay.
|
|
6
|
+
* @param immediate - If `true`, the function will be triggered on the leading edge instead of the trailing.
|
|
7
|
+
* @returns A debounced version of the provided function.
|
|
8
|
+
*/
|
|
9
|
+
export function debounce<T extends (...args: any[]) => void>(
|
|
10
|
+
func: T,
|
|
11
|
+
wait: number,
|
|
12
|
+
immediate = false,
|
|
13
|
+
): (...args: Parameters<T>) => void {
|
|
14
|
+
let timeout: ReturnType<typeof setTimeout> | null;
|
|
15
|
+
|
|
16
|
+
return function (this: any, ...args: Parameters<T>) {
|
|
17
|
+
const context = this;
|
|
18
|
+
|
|
19
|
+
const later = () => {
|
|
20
|
+
timeout = null;
|
|
21
|
+
if (!immediate) func.apply(context, args);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const callNow = immediate && !timeout;
|
|
25
|
+
|
|
26
|
+
if (timeout) clearTimeout(timeout);
|
|
27
|
+
timeout = setTimeout(later, wait);
|
|
28
|
+
|
|
29
|
+
if (callNow) func.apply(context, args);
|
|
30
|
+
};
|
|
31
|
+
}
|
package/src/utilities.ts
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
export const SECONDS_PER_SECOND = 1.0;
|
|
2
|
+
export const SECONDS_PER_MINUTE = 60;
|
|
3
|
+
export const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
|
|
4
|
+
export const SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
|
|
5
|
+
|
|
6
|
+
type Designation = {
|
|
7
|
+
[key: string]: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type StringKeyMap = {
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const designations: Designation = {
|
|
15
|
+
D: SECONDS_PER_DAY,
|
|
16
|
+
H: SECONDS_PER_HOUR,
|
|
17
|
+
M: SECONDS_PER_MINUTE,
|
|
18
|
+
S: SECONDS_PER_SECOND,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Converts a Number to a String of HH:MM:SS
|
|
23
|
+
*
|
|
24
|
+
* @param {number} totalSeconds
|
|
25
|
+
* @return {string}
|
|
26
|
+
*/
|
|
27
|
+
export function getSecondsAsHHMMSS(totalSeconds: number | null): string {
|
|
28
|
+
// SCORM spec does not deal with negative durations, give zero back
|
|
29
|
+
if (!totalSeconds || totalSeconds <= 0) {
|
|
30
|
+
return "00:00:00";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const hours = Math.floor(totalSeconds / SECONDS_PER_HOUR);
|
|
34
|
+
const dateObj = new Date(totalSeconds * 1000);
|
|
35
|
+
const minutes = dateObj.getUTCMinutes();
|
|
36
|
+
// make sure we add any possible decimal value
|
|
37
|
+
const seconds = dateObj.getSeconds();
|
|
38
|
+
const ms = totalSeconds % 1.0;
|
|
39
|
+
let msStr = "";
|
|
40
|
+
|
|
41
|
+
if (countDecimals(ms) > 0) {
|
|
42
|
+
if (countDecimals(ms) > 2) {
|
|
43
|
+
msStr = ms.toFixed(2);
|
|
44
|
+
} else {
|
|
45
|
+
msStr = String(ms);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
msStr = "." + msStr.split(".")[1];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
(hours + ":" + minutes + ":" + seconds).replace(/\b\d\b/g, "0$&") + msStr
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Calculate the number of seconds from ISO 8601 Duration
|
|
58
|
+
*
|
|
59
|
+
* @param {number} seconds
|
|
60
|
+
* @return {string}
|
|
61
|
+
*/
|
|
62
|
+
export function getSecondsAsISODuration(seconds: number | null): string {
|
|
63
|
+
// SCORM spec does not deal with negative durations, give zero back
|
|
64
|
+
if (!seconds || seconds <= 0) {
|
|
65
|
+
return "PT0S";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let duration = "P";
|
|
69
|
+
let remainder = seconds;
|
|
70
|
+
for (const designationsKey in designations) {
|
|
71
|
+
const current_seconds = designations[designationsKey];
|
|
72
|
+
let value = Math.floor(remainder / current_seconds);
|
|
73
|
+
remainder = remainder % current_seconds;
|
|
74
|
+
|
|
75
|
+
if (countDecimals(remainder) > 2) {
|
|
76
|
+
remainder = Number(Number(remainder).toFixed(2));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// If we have anything left in the remainder, and we're currently adding
|
|
80
|
+
// seconds to the duration, go ahead and add the decimal to the seconds
|
|
81
|
+
if (designationsKey === "S" && remainder > 0) {
|
|
82
|
+
value += remainder;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (value) {
|
|
86
|
+
if (
|
|
87
|
+
(duration.indexOf("D") > 0 ||
|
|
88
|
+
designationsKey === "H" ||
|
|
89
|
+
designationsKey === "M" ||
|
|
90
|
+
designationsKey === "S") &&
|
|
91
|
+
duration.indexOf("T") === -1
|
|
92
|
+
) {
|
|
93
|
+
duration += "T";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
duration += `${value}${designationsKey}`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return duration;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Calculate the number of seconds from HH:MM:SS.DDDDDD
|
|
104
|
+
*
|
|
105
|
+
* @param {string} timeString
|
|
106
|
+
* @param {RegExp} timeRegex
|
|
107
|
+
* @return {number}
|
|
108
|
+
*/
|
|
109
|
+
export function getTimeAsSeconds(
|
|
110
|
+
timeString: string | number | boolean | null,
|
|
111
|
+
timeRegex: RegExp | string,
|
|
112
|
+
): number {
|
|
113
|
+
if (typeof timeString === "number" || typeof timeString === "boolean") {
|
|
114
|
+
timeString = String(timeString);
|
|
115
|
+
}
|
|
116
|
+
if (typeof timeRegex === "string") {
|
|
117
|
+
timeRegex = new RegExp(timeRegex);
|
|
118
|
+
}
|
|
119
|
+
if (!timeString || !timeString.match(timeRegex)) {
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const parts = timeString.split(":");
|
|
124
|
+
const hours = Number(parts[0]);
|
|
125
|
+
const minutes = Number(parts[1]);
|
|
126
|
+
const seconds = Number(parts[2]);
|
|
127
|
+
return hours * 3600 + minutes * 60 + seconds;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Calculate the number of seconds from ISO 8601 Duration
|
|
132
|
+
*
|
|
133
|
+
* @param {string} duration
|
|
134
|
+
* @param {RegExp} durationRegex
|
|
135
|
+
* @return {number}
|
|
136
|
+
*/
|
|
137
|
+
export function getDurationAsSeconds(
|
|
138
|
+
duration: string | null,
|
|
139
|
+
durationRegex: RegExp | string,
|
|
140
|
+
): number {
|
|
141
|
+
if (typeof durationRegex === "string") {
|
|
142
|
+
durationRegex = new RegExp(durationRegex);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!duration || !duration.match(durationRegex)) {
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const [, years, _, , days, hours, minutes, seconds] =
|
|
150
|
+
new RegExp(durationRegex).exec(duration) || [];
|
|
151
|
+
let result = 0.0;
|
|
152
|
+
result += Number(seconds) || 0.0;
|
|
153
|
+
result += Number(minutes) * 60.0 || 0.0;
|
|
154
|
+
result += Number(hours) * 3600.0 || 0.0;
|
|
155
|
+
result += Number(days) * (60 * 60 * 24.0) || 0.0;
|
|
156
|
+
result += Number(years) * (60 * 60 * 24 * 365.0) || 0.0;
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Adds together two ISO8601 Duration strings
|
|
162
|
+
*
|
|
163
|
+
* @param {string} first
|
|
164
|
+
* @param {string} second
|
|
165
|
+
* @param {RegExp|string} durationRegex
|
|
166
|
+
* @return {string}
|
|
167
|
+
*/
|
|
168
|
+
export function addTwoDurations(
|
|
169
|
+
first: string,
|
|
170
|
+
second: string,
|
|
171
|
+
durationRegex: RegExp | string,
|
|
172
|
+
): string {
|
|
173
|
+
const regex: RegExp =
|
|
174
|
+
typeof durationRegex === "string"
|
|
175
|
+
? new RegExp(durationRegex)
|
|
176
|
+
: durationRegex;
|
|
177
|
+
return getSecondsAsISODuration(
|
|
178
|
+
getDurationAsSeconds(first, regex) + getDurationAsSeconds(second, regex),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Add together two HH:MM:SS.DD strings
|
|
184
|
+
*
|
|
185
|
+
* @param {string} first
|
|
186
|
+
* @param {string} second
|
|
187
|
+
* @param {RegExp} timeRegex
|
|
188
|
+
* @return {string}
|
|
189
|
+
*/
|
|
190
|
+
export function addHHMMSSTimeStrings(
|
|
191
|
+
first: string,
|
|
192
|
+
second: string,
|
|
193
|
+
timeRegex: RegExp | string,
|
|
194
|
+
): string {
|
|
195
|
+
if (typeof timeRegex === "string") {
|
|
196
|
+
timeRegex = new RegExp(timeRegex);
|
|
197
|
+
}
|
|
198
|
+
return getSecondsAsHHMMSS(
|
|
199
|
+
getTimeAsSeconds(first, timeRegex) + getTimeAsSeconds(second, timeRegex),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Flatten a JSON object down to string paths for each values
|
|
205
|
+
* @param {object} data
|
|
206
|
+
* @return {object}
|
|
207
|
+
*/
|
|
208
|
+
export function flatten(data: StringKeyMap): object {
|
|
209
|
+
const result: StringKeyMap = {};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Recurse through the object
|
|
213
|
+
* @param {*} cur
|
|
214
|
+
* @param {*} prop
|
|
215
|
+
*/
|
|
216
|
+
function recurse(cur: any, prop: any) {
|
|
217
|
+
if (Object(cur) !== cur) {
|
|
218
|
+
result[prop] = cur;
|
|
219
|
+
} else if (Array.isArray(cur)) {
|
|
220
|
+
for (let i = 0, l = cur.length; i < l; i++) {
|
|
221
|
+
recurse(cur[i], prop + "[" + i + "]");
|
|
222
|
+
if (l === 0) result[prop] = [];
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
let isEmpty = true;
|
|
226
|
+
|
|
227
|
+
for (const p in cur) {
|
|
228
|
+
if ({}.hasOwnProperty.call(cur, p)) {
|
|
229
|
+
isEmpty = false;
|
|
230
|
+
recurse(cur[p], prop ? prop + "." + p : p);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (isEmpty && prop) result[prop] = {};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
recurse(data, "");
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Un-flatten a flat JSON object
|
|
244
|
+
* @param {object} data
|
|
245
|
+
* @return {object}
|
|
246
|
+
*/
|
|
247
|
+
export function unflatten(data: StringKeyMap): object {
|
|
248
|
+
"use strict";
|
|
249
|
+
|
|
250
|
+
if (Object(data) !== data || Array.isArray(data)) return data;
|
|
251
|
+
const regex = /\.?([^.[\]]+)|\[(\d+)]/g;
|
|
252
|
+
const result: StringKeyMap = {};
|
|
253
|
+
|
|
254
|
+
for (const p in data) {
|
|
255
|
+
if ({}.hasOwnProperty.call(data, p)) {
|
|
256
|
+
let cur = result;
|
|
257
|
+
let prop = "";
|
|
258
|
+
let m = regex.exec(p);
|
|
259
|
+
|
|
260
|
+
while (m) {
|
|
261
|
+
cur = cur[prop] || (cur[prop] = m[2] ? [] : {});
|
|
262
|
+
prop = m[2] || m[1];
|
|
263
|
+
m = regex.exec(p);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
cur[prop] = data[p];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return result[""] || result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Counts the number of decimal places
|
|
275
|
+
* @param {number} num
|
|
276
|
+
* @return {number}
|
|
277
|
+
*/
|
|
278
|
+
export function countDecimals(num: number): number {
|
|
279
|
+
if (Math.floor(num) === num || String(num).indexOf(".") < 0) return 0;
|
|
280
|
+
const parts = num.toString().split(".")[1];
|
|
281
|
+
return parts.length || 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Formats the SCORM messages for easy reading
|
|
286
|
+
*
|
|
287
|
+
* @param {string} functionName
|
|
288
|
+
* @param {string} message
|
|
289
|
+
* @param {string} CMIElement
|
|
290
|
+
* @return {string}
|
|
291
|
+
*/
|
|
292
|
+
export function formatMessage(
|
|
293
|
+
functionName: string,
|
|
294
|
+
message: string,
|
|
295
|
+
CMIElement?: string,
|
|
296
|
+
): string {
|
|
297
|
+
const baseLength = 20;
|
|
298
|
+
let messageString = "";
|
|
299
|
+
|
|
300
|
+
messageString += functionName;
|
|
301
|
+
|
|
302
|
+
let fillChars = baseLength - messageString.length;
|
|
303
|
+
|
|
304
|
+
for (let i = 0; i < fillChars; i++) {
|
|
305
|
+
messageString += " ";
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
messageString += ": ";
|
|
309
|
+
|
|
310
|
+
if (CMIElement) {
|
|
311
|
+
const CMIElementBaseLength = 70;
|
|
312
|
+
|
|
313
|
+
messageString += CMIElement;
|
|
314
|
+
|
|
315
|
+
fillChars = CMIElementBaseLength - messageString.length;
|
|
316
|
+
|
|
317
|
+
for (let j = 0; j < fillChars; j++) {
|
|
318
|
+
messageString += " ";
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (message) {
|
|
323
|
+
messageString += message;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return messageString;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Checks to see if {str} contains {tester}
|
|
331
|
+
*
|
|
332
|
+
* @param {string} str String to check against
|
|
333
|
+
* @param {string} tester String to check for
|
|
334
|
+
* @return {boolean}
|
|
335
|
+
*/
|
|
336
|
+
export function stringMatches(str: string, tester: string): boolean {
|
|
337
|
+
return str?.match(tester) !== null;
|
|
338
|
+
}
|