tessera-learn 0.0.11 → 0.0.13
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 +1 -0
- package/dist/audit-BBJpQGqb.js +204 -0
- package/dist/audit-BBJpQGqb.js.map +1 -0
- package/dist/plugin/a11y-cli.d.ts +1 -0
- package/dist/plugin/a11y-cli.js +36 -0
- package/dist/plugin/a11y-cli.js.map +1 -0
- package/dist/plugin/cli.js +2 -1
- package/dist/plugin/cli.js.map +1 -1
- package/dist/plugin/index.d.ts +16 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +85 -10
- package/dist/plugin/index.js.map +1 -1
- package/dist/{validation-D9DXlqNP.js → validation-B-xTvM9B.js} +342 -18
- package/dist/validation-B-xTvM9B.js.map +1 -0
- package/package.json +17 -2
- package/src/components/Accordion.svelte +3 -1
- package/src/components/AccordionItem.svelte +1 -5
- package/src/components/Audio.svelte +17 -3
- package/src/components/Callout.svelte +5 -1
- package/src/components/Carousel.svelte +24 -8
- package/src/components/DefaultLayout.svelte +41 -12
- package/src/components/FillInTheBlank.svelte +16 -6
- package/src/components/Image.svelte +12 -3
- package/src/components/LockedBanner.svelte +2 -1
- package/src/components/Matching.svelte +48 -19
- package/src/components/MediaTracks.svelte +21 -0
- package/src/components/MultipleChoice.svelte +33 -13
- package/src/components/Quiz.svelte +61 -20
- package/src/components/ResultIcon.svelte +20 -4
- package/src/components/RevealModal.svelte +25 -22
- package/src/components/Sorting.svelte +61 -26
- package/src/components/Transcript.svelte +37 -0
- package/src/components/Video.svelte +21 -18
- package/src/components/util.ts +3 -1
- package/src/components/video-embed.ts +25 -0
- package/src/index.ts +2 -7
- package/src/plugin/a11y/audit.ts +299 -0
- package/src/plugin/a11y/contrast.ts +67 -0
- package/src/plugin/a11y-cli.ts +35 -0
- package/src/plugin/cli.ts +4 -1
- package/src/plugin/export.ts +42 -14
- package/src/plugin/index.ts +216 -44
- package/src/plugin/manifest.ts +62 -22
- package/src/plugin/validation.ts +736 -122
- package/src/runtime/App.svelte +119 -48
- package/src/runtime/LoadingBar.svelte +12 -3
- package/src/runtime/Sidebar.svelte +24 -8
- package/src/runtime/access.ts +15 -3
- package/src/runtime/adapters/cmi5.ts +55 -33
- package/src/runtime/adapters/index.ts +22 -10
- package/src/runtime/adapters/retry.ts +25 -20
- package/src/runtime/adapters/scorm-base.ts +19 -15
- package/src/runtime/adapters/scorm12.ts +7 -8
- package/src/runtime/adapters/scorm2004.ts +11 -14
- package/src/runtime/adapters/web.ts +1 -1
- package/src/runtime/hooks.svelte.ts +152 -326
- package/src/runtime/interaction-format.ts +30 -12
- package/src/runtime/interaction.ts +44 -11
- package/src/runtime/navigation.svelte.ts +27 -11
- package/src/runtime/persistence.ts +2 -2
- package/src/runtime/progress.svelte.ts +13 -9
- package/src/runtime/quiz-engine.svelte.ts +361 -0
- package/src/runtime/quiz-policy.ts +9 -3
- package/src/runtime/types.ts +24 -2
- package/src/runtime/xapi/agent-rules.ts +4 -1
- package/src/runtime/xapi/client.ts +5 -5
- package/src/runtime/xapi/derive-actor.ts +2 -2
- package/src/runtime/xapi/publisher.ts +32 -29
- package/src/runtime/xapi/setup.ts +18 -15
- package/src/runtime/xapi/validation.ts +15 -6
- package/src/virtual.d.ts +4 -1
- package/styles/base.css +32 -11
- package/styles/layout.css +39 -18
- package/styles/theme.css +15 -3
- package/dist/validation-D9DXlqNP.js.map +0 -1
|
@@ -20,7 +20,7 @@ export class LMSAdapterError extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function missingApiError(
|
|
23
|
-
standard: 'scorm12' | 'scorm2004' | 'cmi5'
|
|
23
|
+
standard: 'scorm12' | 'scorm2004' | 'cmi5',
|
|
24
24
|
): LMSAdapterError {
|
|
25
25
|
const label =
|
|
26
26
|
standard === 'scorm12'
|
|
@@ -36,7 +36,7 @@ function missingApiError(
|
|
|
36
36
|
standard,
|
|
37
37
|
`Tessera: this course is configured for ${label} but ${detail} ` +
|
|
38
38
|
`The course must be launched from an LMS that provides the ${label} runtime. ` +
|
|
39
|
-
`If you are testing locally, run \`npm run dev\` instead, or set export.standard to "web"
|
|
39
|
+
`If you are testing locally, run \`npm run dev\` instead, or set export.standard to "web".`,
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -63,13 +63,22 @@ export interface CreateAdapterOptions {
|
|
|
63
63
|
type LMSStandard = 'scorm12' | 'scorm2004' | 'cmi5';
|
|
64
64
|
|
|
65
65
|
/** Per-standard LMS detection. `detect` returns an adapter when the LMS runtime is reachable, else null. */
|
|
66
|
-
const LMS_ADAPTERS: Record<
|
|
66
|
+
const LMS_ADAPTERS: Record<
|
|
67
|
+
LMSStandard,
|
|
68
|
+
{ detect: () => PersistenceAdapter | null; label: string }
|
|
69
|
+
> = {
|
|
67
70
|
scorm12: {
|
|
68
|
-
detect: () => {
|
|
71
|
+
detect: () => {
|
|
72
|
+
const api = findSCORM12API();
|
|
73
|
+
return api ? new SCORM12Adapter(api) : null;
|
|
74
|
+
},
|
|
69
75
|
label: 'SCORM 1.2 API',
|
|
70
76
|
},
|
|
71
77
|
scorm2004: {
|
|
72
|
-
detect: () => {
|
|
78
|
+
detect: () => {
|
|
79
|
+
const api = findSCORM2004API();
|
|
80
|
+
return api ? new SCORM2004Adapter(api) : null;
|
|
81
|
+
},
|
|
73
82
|
label: 'SCORM 2004 API',
|
|
74
83
|
},
|
|
75
84
|
cmi5: {
|
|
@@ -80,18 +89,21 @@ const LMS_ADAPTERS: Record<LMSStandard, { detect: () => PersistenceAdapter | nul
|
|
|
80
89
|
|
|
81
90
|
export function createAdapter(
|
|
82
91
|
config: CourseConfig,
|
|
83
|
-
options: CreateAdapterOptions = {}
|
|
92
|
+
options: CreateAdapterOptions = {},
|
|
84
93
|
): PersistenceAdapter {
|
|
85
|
-
const allowFallback =
|
|
86
|
-
options.allowFallback ?? import.meta.env?.DEV === true;
|
|
94
|
+
const allowFallback = options.allowFallback ?? import.meta.env?.DEV === true;
|
|
87
95
|
const standard = config.export?.standard;
|
|
88
|
-
if (
|
|
96
|
+
if (
|
|
97
|
+
standard === 'scorm12' ||
|
|
98
|
+
standard === 'scorm2004' ||
|
|
99
|
+
standard === 'cmi5'
|
|
100
|
+
) {
|
|
89
101
|
const entry = LMS_ADAPTERS[standard];
|
|
90
102
|
const adapter = entry.detect();
|
|
91
103
|
if (adapter) return adapter;
|
|
92
104
|
if (!allowFallback) throw missingApiError(standard);
|
|
93
105
|
console.warn(
|
|
94
|
-
`Tessera (dev): ${entry.label} not found — falling back to localStorage
|
|
106
|
+
`Tessera (dev): ${entry.label} not found — falling back to localStorage`,
|
|
95
107
|
);
|
|
96
108
|
}
|
|
97
109
|
return new WebAdapter(config);
|
|
@@ -24,29 +24,37 @@ function lmsCallSucceeded(result: unknown): boolean {
|
|
|
24
24
|
|
|
25
25
|
function readLastErrorCode(reporter: LMSErrorReporter | undefined): string {
|
|
26
26
|
if (!reporter) return '';
|
|
27
|
-
try {
|
|
27
|
+
try {
|
|
28
|
+
return reporter.code();
|
|
29
|
+
} catch {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
function logRetryGiveUp(
|
|
31
35
|
errorReporter: LMSErrorReporter | undefined,
|
|
32
36
|
lastErrCode: string,
|
|
33
|
-
context: string | undefined
|
|
37
|
+
context: string | undefined,
|
|
34
38
|
): void {
|
|
35
39
|
const ctx = context ? ` [${context}]` : '';
|
|
36
40
|
console.warn(
|
|
37
|
-
`Tessera: LMS call failed after retries${ctx}${formatLMSErrorDetail(errorReporter, lastErrCode)}, continuing without persistence
|
|
41
|
+
`Tessera: LMS call failed after retries${ctx}${formatLMSErrorDetail(errorReporter, lastErrCode)}, continuing without persistence`,
|
|
38
42
|
);
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export function formatLMSErrorDetail(
|
|
42
46
|
errorReporter: LMSErrorReporter | undefined,
|
|
43
|
-
code: string
|
|
47
|
+
code: string,
|
|
44
48
|
): string {
|
|
45
49
|
if (!errorReporter || !code || code === '0') return '';
|
|
46
50
|
let msg = '';
|
|
47
51
|
let diag = '';
|
|
48
|
-
try {
|
|
49
|
-
|
|
52
|
+
try {
|
|
53
|
+
msg = errorReporter.message(code);
|
|
54
|
+
} catch {}
|
|
55
|
+
try {
|
|
56
|
+
diag = errorReporter.diagnostic?.(code) ?? '';
|
|
57
|
+
} catch {}
|
|
50
58
|
let detail = ` (LMS error ${code}`;
|
|
51
59
|
if (msg) detail += `: ${msg}`;
|
|
52
60
|
if (diag && diag !== msg) detail += ` — ${diag}`;
|
|
@@ -56,24 +64,21 @@ export function formatLMSErrorDetail(
|
|
|
56
64
|
|
|
57
65
|
/** Sync call that warns with the LMS error code on failure (terminate-path). */
|
|
58
66
|
export function callSyncOrWarn(
|
|
59
|
-
fn: () =>
|
|
67
|
+
fn: () => unknown,
|
|
60
68
|
context: string,
|
|
61
|
-
errorReporter?: LMSErrorReporter
|
|
69
|
+
errorReporter?: LMSErrorReporter,
|
|
62
70
|
): boolean {
|
|
63
|
-
let ok
|
|
71
|
+
let ok: boolean;
|
|
64
72
|
try {
|
|
65
73
|
ok = lmsCallSucceeded(fn());
|
|
66
74
|
} catch (err) {
|
|
67
|
-
console.warn(
|
|
68
|
-
`Tessera: LMS call threw [${context}] during terminate`,
|
|
69
|
-
err
|
|
70
|
-
);
|
|
75
|
+
console.warn(`Tessera: LMS call threw [${context}] during terminate`, err);
|
|
71
76
|
return false;
|
|
72
77
|
}
|
|
73
78
|
if (!ok) {
|
|
74
79
|
const code = readLastErrorCode(errorReporter);
|
|
75
80
|
console.warn(
|
|
76
|
-
`Tessera: LMS call failed [${context}] during terminate${formatLMSErrorDetail(errorReporter, code)}
|
|
81
|
+
`Tessera: LMS call failed [${context}] during terminate${formatLMSErrorDetail(errorReporter, code)}`,
|
|
77
82
|
);
|
|
78
83
|
}
|
|
79
84
|
return ok;
|
|
@@ -95,10 +100,10 @@ export function callSyncOrWarn(
|
|
|
95
100
|
* for SCORM adapters where the underlying API calls are synchronous.
|
|
96
101
|
*/
|
|
97
102
|
export async function withRetry(
|
|
98
|
-
fn: () =>
|
|
103
|
+
fn: () => unknown,
|
|
99
104
|
maxRetries = RETRY_ATTEMPTS,
|
|
100
105
|
errorReporter?: LMSErrorReporter,
|
|
101
|
-
context?: string
|
|
106
|
+
context?: string,
|
|
102
107
|
): Promise<boolean> {
|
|
103
108
|
let lastErrCode = '';
|
|
104
109
|
let threw = false;
|
|
@@ -128,7 +133,7 @@ export async function withRetry(
|
|
|
128
133
|
* Synchronous single-attempt LMS call. Used during page unload
|
|
129
134
|
* where async retries cannot run.
|
|
130
135
|
*/
|
|
131
|
-
export function callSync(fn: () =>
|
|
136
|
+
export function callSync(fn: () => unknown): boolean {
|
|
132
137
|
try {
|
|
133
138
|
return lmsCallSucceeded(fn());
|
|
134
139
|
} catch {
|
|
@@ -143,7 +148,7 @@ export function callSync(fn: () => any): boolean {
|
|
|
143
148
|
* the failed operation on the next flush trigger.
|
|
144
149
|
*/
|
|
145
150
|
interface QueueEntry {
|
|
146
|
-
fn: () =>
|
|
151
|
+
fn: () => unknown;
|
|
147
152
|
context?: string;
|
|
148
153
|
}
|
|
149
154
|
|
|
@@ -164,10 +169,10 @@ export class WriteQueue {
|
|
|
164
169
|
/**
|
|
165
170
|
* Enqueue an operation and trigger a flush.
|
|
166
171
|
*/
|
|
167
|
-
enqueue(fn: () =>
|
|
172
|
+
enqueue(fn: () => unknown, context?: string): void {
|
|
168
173
|
this.#queue.push({ fn, context });
|
|
169
174
|
if (!this.#flushing) {
|
|
170
|
-
this.#flush();
|
|
175
|
+
void this.#flush();
|
|
171
176
|
}
|
|
172
177
|
}
|
|
173
178
|
|
|
@@ -71,11 +71,11 @@ export abstract class BaseScormAdapter<TApi> implements PersistenceAdapter {
|
|
|
71
71
|
() => this.dialect.initialize(this.api),
|
|
72
72
|
undefined,
|
|
73
73
|
this.errorReporter,
|
|
74
|
-
'Initialize'
|
|
74
|
+
'Initialize',
|
|
75
75
|
);
|
|
76
76
|
if (!initialized) {
|
|
77
77
|
console.warn(
|
|
78
|
-
'Tessera: LMS Initialize failed — all subsequent persistence calls will fail with error 301 (Not Initialized). Reload the launch from the LMS.'
|
|
78
|
+
'Tessera: LMS Initialize failed — all subsequent persistence calls will fail with error 301 (Not Initialized). Reload the launch from the LMS.',
|
|
79
79
|
);
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
@@ -86,7 +86,7 @@ export abstract class BaseScormAdapter<TApi> implements PersistenceAdapter {
|
|
|
86
86
|
} catch (err) {
|
|
87
87
|
console.warn(
|
|
88
88
|
'Tessera: LMS threw on GetValue(cmi.suspend_data); resume disabled for this launch',
|
|
89
|
-
err
|
|
89
|
+
err,
|
|
90
90
|
);
|
|
91
91
|
}
|
|
92
92
|
if (raw && raw.trim()) {
|
|
@@ -95,7 +95,7 @@ export abstract class BaseScormAdapter<TApi> implements PersistenceAdapter {
|
|
|
95
95
|
} catch (err) {
|
|
96
96
|
console.warn(
|
|
97
97
|
'Tessera: cmi.suspend_data is not valid JSON; resume disabled for this launch (the LMS may have truncated a prior write)',
|
|
98
|
-
err
|
|
98
|
+
err,
|
|
99
99
|
);
|
|
100
100
|
this.#state = null;
|
|
101
101
|
}
|
|
@@ -103,13 +103,13 @@ export abstract class BaseScormAdapter<TApi> implements PersistenceAdapter {
|
|
|
103
103
|
|
|
104
104
|
// n indexing must continue from _count — restarting at 0 would overwrite
|
|
105
105
|
// the prior session's records (the LMS uses n as the array key).
|
|
106
|
-
let countRaw
|
|
106
|
+
let countRaw: string;
|
|
107
107
|
try {
|
|
108
108
|
countRaw = this.dialect.getValue(this.api, 'cmi.interactions._count');
|
|
109
109
|
} catch (err) {
|
|
110
110
|
console.warn(
|
|
111
111
|
'Tessera: LMS threw on GetValue(cmi.interactions._count); new interactions will be written from index 0 and may overwrite prior session records',
|
|
112
|
-
err
|
|
112
|
+
err,
|
|
113
113
|
);
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
@@ -119,7 +119,7 @@ export abstract class BaseScormAdapter<TApi> implements PersistenceAdapter {
|
|
|
119
119
|
this.interactionCount = n;
|
|
120
120
|
} else {
|
|
121
121
|
console.warn(
|
|
122
|
-
`Tessera: LMS returned non-numeric cmi.interactions._count="${countRaw}"; new interactions will be written from index 0 and may overwrite prior session records
|
|
122
|
+
`Tessera: LMS returned non-numeric cmi.interactions._count="${countRaw}"; new interactions will be written from index 0 and may overwrite prior session records`,
|
|
123
123
|
);
|
|
124
124
|
}
|
|
125
125
|
}
|
|
@@ -141,12 +141,12 @@ export abstract class BaseScormAdapter<TApi> implements PersistenceAdapter {
|
|
|
141
141
|
`${this.dialect.suspendDataLimitLabel} limit. The LMS will likely ` +
|
|
142
142
|
`truncate it and the next resume will lose state. Reduce ` +
|
|
143
143
|
`usePersistence() payloads or switch export.standard to a ` +
|
|
144
|
-
`larger-limit standard (scorm2004/cmi5)
|
|
144
|
+
`larger-limit standard (scorm2004/cmi5).`,
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
147
|
this.queue.enqueue(
|
|
148
148
|
() => this.dialect.setValue(this.api, 'cmi.suspend_data', json),
|
|
149
|
-
'cmi.suspend_data'
|
|
149
|
+
'cmi.suspend_data',
|
|
150
150
|
);
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -155,14 +155,14 @@ export abstract class BaseScormAdapter<TApi> implements PersistenceAdapter {
|
|
|
155
155
|
this.queue.enqueue(
|
|
156
156
|
() =>
|
|
157
157
|
this.dialect.setValue(this.api, this.dialect.sessionTimeKey, formatted),
|
|
158
|
-
this.dialect.sessionTimeKey
|
|
158
|
+
this.dialect.sessionTimeKey,
|
|
159
159
|
);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
reportInteraction(
|
|
163
163
|
questionId: string,
|
|
164
164
|
interaction: Interaction,
|
|
165
|
-
correct: boolean | null
|
|
165
|
+
correct: boolean | null,
|
|
166
166
|
): void {
|
|
167
167
|
const n = this.interactionCount++;
|
|
168
168
|
const fields = buildScormInteractionFields(
|
|
@@ -177,12 +177,12 @@ export abstract class BaseScormAdapter<TApi> implements PersistenceAdapter {
|
|
|
177
177
|
typeValue: this.dialect.interactionFields.typeValue(interaction.type),
|
|
178
178
|
resultLabels: this.dialect.interactionFields.resultLabels,
|
|
179
179
|
format: this.dialect.interactionFields.format,
|
|
180
|
-
}
|
|
180
|
+
},
|
|
181
181
|
);
|
|
182
182
|
for (const [key, value] of fields) {
|
|
183
183
|
this.queue.enqueue(
|
|
184
184
|
() => this.dialect.setValue(this.api, key, value),
|
|
185
|
-
key
|
|
185
|
+
key,
|
|
186
186
|
);
|
|
187
187
|
}
|
|
188
188
|
}
|
|
@@ -196,11 +196,15 @@ export abstract class BaseScormAdapter<TApi> implements PersistenceAdapter {
|
|
|
196
196
|
this.#terminated = true;
|
|
197
197
|
// Async retries can't run during page unload — drain + commit + finish synchronously.
|
|
198
198
|
this.queue.drainSync();
|
|
199
|
-
callSyncOrWarn(
|
|
199
|
+
callSyncOrWarn(
|
|
200
|
+
() => this.dialect.commit(this.api),
|
|
201
|
+
'Commit',
|
|
202
|
+
this.errorReporter,
|
|
203
|
+
);
|
|
200
204
|
callSyncOrWarn(
|
|
201
205
|
() => this.dialect.terminate(this.api),
|
|
202
206
|
'Terminate',
|
|
203
|
-
this.errorReporter
|
|
207
|
+
this.errorReporter,
|
|
204
208
|
);
|
|
205
209
|
}
|
|
206
210
|
|
|
@@ -67,24 +67,23 @@ export class SCORM12Adapter extends BaseScormAdapter<SCORM12API> {
|
|
|
67
67
|
super.saveState(state);
|
|
68
68
|
// §3.4.5.3 — bookmark for LMS "Resume from page N" affordances.
|
|
69
69
|
this.queue.enqueue(
|
|
70
|
-
() =>
|
|
71
|
-
|
|
72
|
-
'cmi.core.lesson_location'
|
|
70
|
+
() => this.api.LMSSetValue('cmi.core.lesson_location', String(state.b)),
|
|
71
|
+
'cmi.core.lesson_location',
|
|
73
72
|
);
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
setScore(score: number): void {
|
|
77
76
|
this.queue.enqueue(
|
|
78
77
|
() => this.api.LMSSetValue('cmi.core.score.raw', formatReal107(score)),
|
|
79
|
-
'cmi.core.score.raw'
|
|
78
|
+
'cmi.core.score.raw',
|
|
80
79
|
);
|
|
81
80
|
this.queue.enqueue(
|
|
82
81
|
() => this.api.LMSSetValue('cmi.core.score.min', '0'),
|
|
83
|
-
'cmi.core.score.min'
|
|
82
|
+
'cmi.core.score.min',
|
|
84
83
|
);
|
|
85
84
|
this.queue.enqueue(
|
|
86
85
|
() => this.api.LMSSetValue('cmi.core.score.max', '100'),
|
|
87
|
-
'cmi.core.score.max'
|
|
86
|
+
'cmi.core.score.max',
|
|
88
87
|
);
|
|
89
88
|
}
|
|
90
89
|
|
|
@@ -104,7 +103,7 @@ export class SCORM12Adapter extends BaseScormAdapter<SCORM12API> {
|
|
|
104
103
|
const value = this.#successStatus ?? this.#completionStatus;
|
|
105
104
|
this.queue.enqueue(
|
|
106
105
|
() => this.api.LMSSetValue('cmi.core.lesson_status', value),
|
|
107
|
-
'cmi.core.lesson_status'
|
|
106
|
+
'cmi.core.lesson_status',
|
|
108
107
|
);
|
|
109
108
|
}
|
|
110
109
|
|
|
@@ -113,7 +112,7 @@ export class SCORM12Adapter extends BaseScormAdapter<SCORM12API> {
|
|
|
113
112
|
const value = mode === 'suspend' ? 'suspend' : '';
|
|
114
113
|
this.queue.enqueue(
|
|
115
114
|
() => this.api.LMSSetValue('cmi.core.exit', value),
|
|
116
|
-
'cmi.core.exit'
|
|
115
|
+
'cmi.core.exit',
|
|
117
116
|
);
|
|
118
117
|
}
|
|
119
118
|
}
|
|
@@ -86,7 +86,7 @@ export class SCORM2004Adapter extends BaseScormAdapter<SCORM2004API> {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
#readScaledThreshold(key: string): number | null {
|
|
89
|
-
let raw
|
|
89
|
+
let raw: string;
|
|
90
90
|
try {
|
|
91
91
|
raw = this.api.GetValue(key);
|
|
92
92
|
} catch {
|
|
@@ -104,7 +104,7 @@ export class SCORM2004Adapter extends BaseScormAdapter<SCORM2004API> {
|
|
|
104
104
|
// §4.2.1.4 — bookmark for LMS "Resume from page N" affordances.
|
|
105
105
|
this.queue.enqueue(
|
|
106
106
|
() => this.api.SetValue('cmi.location', String(state.b)),
|
|
107
|
-
'cmi.location'
|
|
107
|
+
'cmi.location',
|
|
108
108
|
);
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -116,7 +116,7 @@ export class SCORM2004Adapter extends BaseScormAdapter<SCORM2004API> {
|
|
|
116
116
|
reportInteraction(
|
|
117
117
|
questionId: string,
|
|
118
118
|
interaction: import('../interaction.js').Interaction,
|
|
119
|
-
correct: boolean | null
|
|
119
|
+
correct: boolean | null,
|
|
120
120
|
): void {
|
|
121
121
|
if (!this.#canWrite) return;
|
|
122
122
|
super.reportInteraction(questionId, interaction, correct);
|
|
@@ -129,19 +129,19 @@ export class SCORM2004Adapter extends BaseScormAdapter<SCORM2004API> {
|
|
|
129
129
|
const scaled = formatReal107(Math.max(0, Math.min(1, score / 100)));
|
|
130
130
|
this.queue.enqueue(
|
|
131
131
|
() => this.api.SetValue('cmi.score.raw', raw),
|
|
132
|
-
'cmi.score.raw'
|
|
132
|
+
'cmi.score.raw',
|
|
133
133
|
);
|
|
134
134
|
this.queue.enqueue(
|
|
135
135
|
() => this.api.SetValue('cmi.score.min', '0'),
|
|
136
|
-
'cmi.score.min'
|
|
136
|
+
'cmi.score.min',
|
|
137
137
|
);
|
|
138
138
|
this.queue.enqueue(
|
|
139
139
|
() => this.api.SetValue('cmi.score.max', '100'),
|
|
140
|
-
'cmi.score.max'
|
|
140
|
+
'cmi.score.max',
|
|
141
141
|
);
|
|
142
142
|
this.queue.enqueue(
|
|
143
143
|
() => this.api.SetValue('cmi.score.scaled', scaled),
|
|
144
|
-
'cmi.score.scaled'
|
|
144
|
+
'cmi.score.scaled',
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -150,13 +150,13 @@ export class SCORM2004Adapter extends BaseScormAdapter<SCORM2004API> {
|
|
|
150
150
|
const value = status === 'complete' ? 'completed' : 'incomplete';
|
|
151
151
|
this.queue.enqueue(
|
|
152
152
|
() => this.api.SetValue('cmi.completion_status', value),
|
|
153
|
-
'cmi.completion_status'
|
|
153
|
+
'cmi.completion_status',
|
|
154
154
|
);
|
|
155
155
|
// §4.2.4.2 — writing 1.0 surfaces a "100%" reading on LMS dashboards.
|
|
156
156
|
if (status === 'complete') {
|
|
157
157
|
this.queue.enqueue(
|
|
158
158
|
() => this.api.SetValue('cmi.progress_measure', '1'),
|
|
159
|
-
'cmi.progress_measure'
|
|
159
|
+
'cmi.progress_measure',
|
|
160
160
|
);
|
|
161
161
|
}
|
|
162
162
|
}
|
|
@@ -167,15 +167,12 @@ export class SCORM2004Adapter extends BaseScormAdapter<SCORM2004API> {
|
|
|
167
167
|
// a null status to "passed".
|
|
168
168
|
this.queue.enqueue(
|
|
169
169
|
() => this.api.SetValue('cmi.success_status', status),
|
|
170
|
-
'cmi.success_status'
|
|
170
|
+
'cmi.success_status',
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
setExit(mode: 'suspend' | 'normal'): void {
|
|
175
175
|
if (!this.#canWrite) return;
|
|
176
|
-
this.queue.enqueue(
|
|
177
|
-
() => this.api.SetValue('cmi.exit', mode),
|
|
178
|
-
'cmi.exit'
|
|
179
|
-
);
|
|
176
|
+
this.queue.enqueue(() => this.api.SetValue('cmi.exit', mode), 'cmi.exit');
|
|
180
177
|
}
|
|
181
178
|
}
|
|
@@ -51,7 +51,7 @@ export class WebAdapter implements PersistenceAdapter {
|
|
|
51
51
|
reportInteraction(
|
|
52
52
|
_questionId: string,
|
|
53
53
|
_interaction: Interaction,
|
|
54
|
-
_correct: boolean | null
|
|
54
|
+
_correct: boolean | null,
|
|
55
55
|
): void {
|
|
56
56
|
// Web adapter has no external LMS; learner interaction data lives only
|
|
57
57
|
// in memory. Authors who want to persist per-question state can use
|