@lessonkit/react 1.3.0 → 1.4.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/block-catalog.v3.json +1005 -107
- package/dist/AssessmentLessonGuard-D2Plzybb.d.cts +21 -0
- package/dist/AssessmentLessonGuard-D2Plzybb.d.ts +21 -0
- package/dist/blocks-entry.cjs +4563 -0
- package/dist/blocks-entry.d.cts +411 -0
- package/dist/blocks-entry.d.ts +411 -0
- package/dist/blocks-entry.js +69 -0
- package/dist/chunk-4LQ4TTEE.js +4018 -0
- package/dist/chunk-TDM3ARE7.js +1775 -0
- package/dist/chunk-UUTXECVW.js +252 -0
- package/dist/index.cjs +2555 -313
- package/dist/index.d.cts +36 -282
- package/dist/index.d.ts +36 -282
- package/dist/index.js +433 -4065
- package/dist/testing.cjs +540 -0
- package/dist/testing.d.cts +16 -0
- package/dist/testing.d.ts +16 -0
- package/dist/testing.js +18 -0
- package/package.json +33 -16
package/dist/index.js
CHANGED
|
@@ -1,4074 +1,198 @@
|
|
|
1
|
-
// src/components.tsx
|
|
2
|
-
import { useEffect as useEffect4, useId as useId2, useMemo as useMemo6, useRef as useRef5, useState as useState4 } from "react";
|
|
3
|
-
import { visuallyHiddenStyle as visuallyHiddenStyle2 } from "@lessonkit/accessibility";
|
|
4
|
-
|
|
5
|
-
// src/context.tsx
|
|
6
|
-
import { createContext } from "react";
|
|
7
|
-
|
|
8
|
-
// src/provider/useLessonkitProviderRuntime.ts
|
|
9
|
-
import {
|
|
10
|
-
useCallback,
|
|
11
|
-
useEffect,
|
|
12
|
-
useLayoutEffect,
|
|
13
|
-
useMemo,
|
|
14
|
-
useRef,
|
|
15
|
-
useState
|
|
16
|
-
} from "react";
|
|
17
|
-
import { createLessonkitRuntime, createTrackingClient as createTrackingClient2, assertValidId } from "@lessonkit/core";
|
|
18
|
-
|
|
19
|
-
// src/runtime/observability.ts
|
|
20
|
-
import { createInMemoryXAPIQueue } from "@lessonkit/xapi";
|
|
21
|
-
function createXapiQueueFromObservability(observability) {
|
|
22
|
-
const opts = {};
|
|
23
|
-
if (observability?.onXapiQueueDepth) {
|
|
24
|
-
opts.onDepth = observability.onXapiQueueDepth;
|
|
25
|
-
}
|
|
26
|
-
if (observability?.onXapiQueueCap) {
|
|
27
|
-
opts.onCap = observability.onXapiQueueCap;
|
|
28
|
-
}
|
|
29
|
-
return createInMemoryXAPIQueue(opts);
|
|
30
|
-
}
|
|
31
|
-
function wrapTrackingSink(sink, observability) {
|
|
32
|
-
if (!sink || !observability?.onTelemetrySinkError) return sink;
|
|
33
|
-
const onError = observability.onTelemetrySinkError;
|
|
34
|
-
return ((event) => {
|
|
35
|
-
try {
|
|
36
|
-
const result = sink(event);
|
|
37
|
-
if (result != null && typeof result.catch === "function") {
|
|
38
|
-
return result.catch((err) => {
|
|
39
|
-
onError(err, { sinkId: "tracking" });
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
return result;
|
|
43
|
-
} catch (err) {
|
|
44
|
-
onError(err, { sinkId: "tracking" });
|
|
45
|
-
return void 0;
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// src/provider/useLessonkitProviderRuntime.ts
|
|
51
|
-
import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement3 } from "@lessonkit/xapi";
|
|
52
|
-
|
|
53
|
-
// src/runtime/emitTelemetry.ts
|
|
54
|
-
import { buildTelemetryEvent, tryBuildTelemetryEvent } from "@lessonkit/core";
|
|
55
|
-
|
|
56
|
-
// src/runtime/telemetryPipeline.ts
|
|
57
|
-
import {
|
|
58
|
-
createTelemetryPipeline,
|
|
59
|
-
createTrackingPipelineSink
|
|
60
|
-
} from "@lessonkit/core";
|
|
61
|
-
import { telemetryEventToXAPIStatement } from "@lessonkit/xapi";
|
|
62
|
-
|
|
63
|
-
// src/runtime/lxpackBridge.ts
|
|
64
1
|
import {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
} catch (err) {
|
|
99
|
-
if (isDevEnvironment()) {
|
|
100
|
-
console.warn(
|
|
101
|
-
"[lessonkit] xAPI mapping skipped:",
|
|
102
|
-
err instanceof Error ? err.message : err
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
id: "lxpack-bridge",
|
|
110
|
-
emit(event) {
|
|
111
|
-
forwardTelemetryToLxpack(event, opts.lxpackBridge, {
|
|
112
|
-
onBridgeMiss: opts.onLxpackBridgeMiss
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
...extraSinks
|
|
117
|
-
]);
|
|
118
|
-
}
|
|
119
|
-
function emitThroughPipeline(event, opts, extraSinks) {
|
|
120
|
-
createLegacyPipeline(opts, extraSinks).emit(event);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// src/runtime/emitTelemetry.ts
|
|
124
|
-
var warnedMissingCourseId = false;
|
|
125
|
-
function isDevEnvironment2() {
|
|
126
|
-
const g = globalThis;
|
|
127
|
-
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
128
|
-
}
|
|
129
|
-
function emitTelemetry(tracking, xapi, event, opts) {
|
|
130
|
-
if (!event.courseId) {
|
|
131
|
-
if (isDevEnvironment2() && !warnedMissingCourseId) {
|
|
132
|
-
warnedMissingCourseId = true;
|
|
133
|
-
console.warn("[lessonkit] telemetry event missing courseId");
|
|
134
|
-
}
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
const legacy = {
|
|
138
|
-
tracking,
|
|
139
|
-
xapi,
|
|
140
|
-
lxpackBridge: opts?.lxpackBridge ?? "auto",
|
|
141
|
-
onLxpackBridgeMiss: opts?.onLxpackBridgeMiss
|
|
142
|
-
};
|
|
143
|
-
emitThroughPipeline(event, legacy, opts?.extraSinks);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// src/runtime/ports.ts
|
|
2
|
+
Accordion,
|
|
3
|
+
ArithmeticQuiz,
|
|
4
|
+
AssessmentSequence,
|
|
5
|
+
DialogCards,
|
|
6
|
+
DragAndDrop,
|
|
7
|
+
DragTheWords,
|
|
8
|
+
Essay,
|
|
9
|
+
FillInTheBlanks,
|
|
10
|
+
FindHotspot,
|
|
11
|
+
FindMultipleHotspots,
|
|
12
|
+
Flashcards,
|
|
13
|
+
Heading,
|
|
14
|
+
Image,
|
|
15
|
+
ImageHotspots,
|
|
16
|
+
ImagePairing,
|
|
17
|
+
ImageSequencing,
|
|
18
|
+
ImageSlider,
|
|
19
|
+
InformationWall,
|
|
20
|
+
InteractiveBook,
|
|
21
|
+
InteractiveVideo,
|
|
22
|
+
MarkTheWords,
|
|
23
|
+
MemoryGame,
|
|
24
|
+
Page,
|
|
25
|
+
ParallaxSlideshow,
|
|
26
|
+
Questionnaire,
|
|
27
|
+
Slide,
|
|
28
|
+
SlideDeck,
|
|
29
|
+
Summary,
|
|
30
|
+
Text,
|
|
31
|
+
TimedCue,
|
|
32
|
+
TrueFalse,
|
|
33
|
+
Video
|
|
34
|
+
} from "./chunk-4LQ4TTEE.js";
|
|
147
35
|
import {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
} from "
|
|
154
|
-
|
|
155
|
-
// src/provider/useLessonkitProviderRuntime.ts
|
|
156
|
-
import { resetSharedVolatileSessionIdForTests } from "@lessonkit/core";
|
|
157
|
-
|
|
158
|
-
// src/runtime/progress.ts
|
|
159
|
-
import { createProgressController } from "@lessonkit/core";
|
|
160
|
-
|
|
161
|
-
// src/runtime/xapi.ts
|
|
162
|
-
import { createXAPIClient } from "@lessonkit/xapi";
|
|
163
|
-
function createXapiClientFromConfig(config, queue) {
|
|
164
|
-
if (config.xapi?.enabled === false) return null;
|
|
165
|
-
if (config.xapi?.client) return config.xapi.client;
|
|
166
|
-
if (!config.courseId) return null;
|
|
167
|
-
const hasTransport = typeof config.xapi?.transport === "function";
|
|
168
|
-
if (!hasTransport && config.xapi?.enabled !== true) return null;
|
|
169
|
-
return createXAPIClient({
|
|
170
|
-
courseId: config.courseId,
|
|
171
|
-
transport: config.xapi?.transport,
|
|
172
|
-
queue
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// src/runtime/session.ts
|
|
36
|
+
KnowledgeCheck,
|
|
37
|
+
Quiz,
|
|
38
|
+
getLessonMountCount,
|
|
39
|
+
registerLessonMount,
|
|
40
|
+
resetQuizWarningsForTests
|
|
41
|
+
} from "./chunk-UUTXECVW.js";
|
|
177
42
|
import {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement2 } from "@lessonkit/xapi";
|
|
192
|
-
function isDevEnvironment3() {
|
|
193
|
-
const g = globalThis;
|
|
194
|
-
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
195
|
-
}
|
|
196
|
-
function warnExtraSinkFailure(sinkId, err) {
|
|
197
|
-
if (isDevEnvironment3()) {
|
|
198
|
-
console.warn(
|
|
199
|
-
`[lessonkit] course_started extra sink "${sinkId}" failed:`,
|
|
200
|
-
err instanceof Error ? err.message : err
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
async function emitExtraSinks(sinks, event, emitCtx) {
|
|
205
|
-
await Promise.all(
|
|
206
|
-
sinks.map(async (sink) => {
|
|
207
|
-
let result;
|
|
208
|
-
try {
|
|
209
|
-
result = sink.emit(event, emitCtx);
|
|
210
|
-
} catch (err) {
|
|
211
|
-
warnExtraSinkFailure(sink.id, err);
|
|
212
|
-
throw err;
|
|
213
|
-
}
|
|
214
|
-
if (result != null && typeof result.then === "function") {
|
|
215
|
-
try {
|
|
216
|
-
await result;
|
|
217
|
-
} catch (err) {
|
|
218
|
-
warnExtraSinkFailure(sink.id, err);
|
|
219
|
-
throw err;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
})
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
async function emitCourseStartedNonTrackingPipeline(opts) {
|
|
226
|
-
let xapiStatementSent = false;
|
|
227
|
-
if (!opts.skipXapi && opts.xapi) {
|
|
228
|
-
const statement = telemetryEventToXAPIStatement2(opts.event);
|
|
229
|
-
if (statement) {
|
|
230
|
-
opts.xapi.send(statement);
|
|
231
|
-
xapiStatementSent = true;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
forwardTelemetryToLxpack(opts.event, opts.lxpackBridge, {
|
|
235
|
-
onBridgeMiss: opts.onLxpackBridgeMiss
|
|
236
|
-
});
|
|
237
|
-
const emitCtx = {
|
|
238
|
-
courseId: opts.event.courseId,
|
|
239
|
-
sessionId: opts.event.sessionId,
|
|
240
|
-
attemptId: opts.event.attemptId
|
|
241
|
-
};
|
|
242
|
-
await emitExtraSinks(opts.extraSinks ?? [], opts.event, emitCtx);
|
|
243
|
-
return { xapiStatementSent };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// src/runtime/plugins.ts
|
|
247
|
-
import { buildPluginContext as buildPluginContextFromCore, createPluginRegistry } from "@lessonkit/core";
|
|
248
|
-
function createReactPluginHost(plugins) {
|
|
249
|
-
if (!plugins?.length) return null;
|
|
250
|
-
return createPluginRegistry(plugins);
|
|
251
|
-
}
|
|
252
|
-
function buildPluginContext(opts) {
|
|
253
|
-
return buildPluginContextFromCore(opts);
|
|
254
|
-
}
|
|
255
|
-
function emitTelemetryWithPlugins(opts) {
|
|
256
|
-
const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
|
|
257
|
-
if (next === null) return;
|
|
258
|
-
emitTelemetry(opts.tracking, opts.xapi, next, {
|
|
259
|
-
lxpackBridge: opts.lxpackBridge ?? "auto",
|
|
260
|
-
extraSinks: opts.extraSinks,
|
|
261
|
-
onLxpackBridgeMiss: opts.onLxpackBridgeMiss
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// src/provider/courseStarted/emit.ts
|
|
266
|
-
var courseStartedTrackingFlightKey = null;
|
|
267
|
-
function isTrackingActive(tracking) {
|
|
268
|
-
return tracking?.enabled !== false;
|
|
269
|
-
}
|
|
270
|
-
function isCourseStartedSinkSettled(result) {
|
|
271
|
-
return result === "emitted";
|
|
272
|
-
}
|
|
273
|
-
function buildCourseStartedEvent(opts) {
|
|
274
|
-
const pluginCtx = buildPluginContext({
|
|
275
|
-
courseId: opts.courseId,
|
|
276
|
-
sessionId: opts.sessionId,
|
|
277
|
-
attemptId: opts.attemptId,
|
|
278
|
-
user: opts.user
|
|
279
|
-
});
|
|
280
|
-
const built = buildTelemetryEvent({
|
|
281
|
-
name: "course_started",
|
|
282
|
-
courseId: opts.courseId,
|
|
283
|
-
sessionId: opts.sessionId,
|
|
284
|
-
attemptId: opts.attemptId,
|
|
285
|
-
user: opts.user
|
|
286
|
-
});
|
|
287
|
-
return opts.pluginHost ? opts.pluginHost.runTelemetry(built, pluginCtx) : built;
|
|
288
|
-
}
|
|
289
|
-
async function emitCourseStartedToTracking(tracking, storage, sessionId, courseId, event, shouldCommit) {
|
|
290
|
-
const flightKey = `${sessionId}:${courseId}`;
|
|
291
|
-
if (hasCourseStartedEmittedToTracking(storage, sessionId, courseId)) {
|
|
292
|
-
return true;
|
|
293
|
-
}
|
|
294
|
-
if (courseStartedTrackingFlightKey === flightKey) {
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
courseStartedTrackingFlightKey = flightKey;
|
|
298
|
-
try {
|
|
299
|
-
if (shouldCommit && !shouldCommit()) return false;
|
|
300
|
-
tracking.track(event);
|
|
301
|
-
markCourseStartedEmittedToTracking(storage, sessionId, courseId);
|
|
302
|
-
const delivered = await tracking.flush?.();
|
|
303
|
-
if (delivered === false) return false;
|
|
304
|
-
if (shouldCommit && !shouldCommit()) return false;
|
|
305
|
-
return true;
|
|
306
|
-
} catch {
|
|
307
|
-
return false;
|
|
308
|
-
} finally {
|
|
309
|
-
if (courseStartedTrackingFlightKey === flightKey) {
|
|
310
|
-
courseStartedTrackingFlightKey = null;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
async function emitCourseStartedPipelineOnly(opts) {
|
|
315
|
-
try {
|
|
316
|
-
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
317
|
-
const { xapiStatementSent } = await emitCourseStartedNonTrackingPipeline({
|
|
318
|
-
event: opts.event,
|
|
319
|
-
xapi: opts.xapi,
|
|
320
|
-
lxpackBridge: opts.lxpackBridge,
|
|
321
|
-
onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
|
|
322
|
-
extraSinks: opts.extraSinks,
|
|
323
|
-
skipXapi: opts.skipXapi
|
|
324
|
-
});
|
|
325
|
-
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
326
|
-
markCourseStarted(opts.storage, opts.sessionId, opts.courseId);
|
|
327
|
-
markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId);
|
|
328
|
-
if (xapiStatementSent) {
|
|
329
|
-
opts.onXapiStatementSent?.();
|
|
330
|
-
}
|
|
331
|
-
return "emitted";
|
|
332
|
-
} catch {
|
|
333
|
-
return "failed";
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
async function emitCourseStarted(opts) {
|
|
337
|
-
const event = buildCourseStartedEvent(opts);
|
|
338
|
-
if (event === null) return "filtered";
|
|
339
|
-
const tracked = await emitCourseStartedToTracking(
|
|
340
|
-
opts.tracking,
|
|
341
|
-
opts.storage,
|
|
342
|
-
opts.sessionId,
|
|
343
|
-
opts.courseId,
|
|
344
|
-
event,
|
|
345
|
-
opts.shouldCommit
|
|
346
|
-
);
|
|
347
|
-
if (!tracked) return "failed";
|
|
348
|
-
return emitCourseStartedPipelineOnly({
|
|
349
|
-
...opts,
|
|
350
|
-
event,
|
|
351
|
-
skipXapi: opts.skipXapi,
|
|
352
|
-
onXapiStatementSent: opts.onXapiStatementSent,
|
|
353
|
-
shouldCommit: opts.shouldCommit
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
async function emitCourseStartedToTrackingOnly(opts) {
|
|
357
|
-
const event = buildCourseStartedEvent(opts);
|
|
358
|
-
if (event === null) return "filtered";
|
|
359
|
-
const tracked = await emitCourseStartedToTracking(
|
|
360
|
-
opts.tracking,
|
|
361
|
-
opts.storage,
|
|
362
|
-
opts.sessionId,
|
|
363
|
-
opts.courseId,
|
|
364
|
-
event,
|
|
365
|
-
opts.shouldCommit
|
|
366
|
-
);
|
|
367
|
-
if (!tracked) return "failed";
|
|
368
|
-
try {
|
|
369
|
-
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
370
|
-
await emitCourseStartedNonTrackingPipeline({
|
|
371
|
-
event,
|
|
372
|
-
xapi: null,
|
|
373
|
-
lxpackBridge: opts.lxpackBridge,
|
|
374
|
-
onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
|
|
375
|
-
extraSinks: opts.extraSinks,
|
|
376
|
-
skipXapi: true
|
|
377
|
-
});
|
|
378
|
-
markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId);
|
|
379
|
-
return "emitted";
|
|
380
|
-
} catch {
|
|
381
|
-
return "failed";
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
async function emitPendingCourseStarted(opts) {
|
|
385
|
-
const trackingEmitted = hasCourseStartedEmittedToTracking(
|
|
386
|
-
opts.storage,
|
|
387
|
-
opts.sessionId,
|
|
388
|
-
opts.courseId
|
|
389
|
-
);
|
|
390
|
-
const sessionStarted = hasCourseStarted(opts.storage, opts.sessionId, opts.courseId);
|
|
391
|
-
if (sessionStarted && !trackingEmitted) {
|
|
392
|
-
return emitCourseStartedToTrackingOnly(opts);
|
|
393
|
-
}
|
|
394
|
-
if (trackingEmitted && !sessionStarted) {
|
|
395
|
-
const event = buildCourseStartedEvent(opts);
|
|
396
|
-
if (event === null) return "filtered";
|
|
397
|
-
return emitCourseStartedPipelineOnly({ ...opts, event });
|
|
398
|
-
}
|
|
399
|
-
if (!trackingEmitted && !sessionStarted) {
|
|
400
|
-
return emitCourseStarted(opts);
|
|
401
|
-
}
|
|
402
|
-
const pipelineDelivered = hasCourseStartedPipelineDelivered(
|
|
403
|
-
opts.storage,
|
|
404
|
-
opts.sessionId,
|
|
405
|
-
opts.courseId
|
|
406
|
-
);
|
|
407
|
-
if (sessionStarted && trackingEmitted && pipelineDelivered) {
|
|
408
|
-
return "emitted";
|
|
409
|
-
}
|
|
410
|
-
if (sessionStarted && trackingEmitted && !pipelineDelivered) {
|
|
411
|
-
const event = buildCourseStartedEvent(opts);
|
|
412
|
-
if (event === null) return "filtered";
|
|
413
|
-
return emitCourseStartedPipelineOnly({
|
|
414
|
-
...opts,
|
|
415
|
-
event,
|
|
416
|
-
skipXapi: opts.skipXapi,
|
|
417
|
-
onXapiStatementSent: opts.onXapiStatementSent
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
return "emitted";
|
|
421
|
-
}
|
|
422
|
-
function assertTrackingSinkConfig(tracking) {
|
|
423
|
-
if (!tracking?.sink || !tracking?.batchSink) return;
|
|
424
|
-
throw new Error(
|
|
425
|
-
"[lessonkit] tracking.sink and tracking.batchSink cannot both be set; use batchSink alone for batched delivery"
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// src/runtime/telemetry.ts
|
|
430
|
-
import { createTrackingClient } from "@lessonkit/core";
|
|
431
|
-
function createTrackingClientFromConfig(config, observability) {
|
|
432
|
-
if (config.tracking?.enabled === false) return createTrackingClient();
|
|
433
|
-
if (config.tracking?.createClient) return config.tracking.createClient();
|
|
434
|
-
return createTrackingClient({
|
|
435
|
-
sink: config.tracking?.sink,
|
|
436
|
-
batchSink: config.tracking?.batchSink,
|
|
437
|
-
batch: config.tracking?.batch,
|
|
438
|
-
onBufferDrop: observability?.onTelemetryBufferDrop
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
async function disposeTrackingClient(client) {
|
|
442
|
-
try {
|
|
443
|
-
await client?.flush?.();
|
|
444
|
-
} catch {
|
|
445
|
-
}
|
|
446
|
-
try {
|
|
447
|
-
await client?.dispose?.();
|
|
448
|
-
} catch {
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// src/provider/useLessonkitProviderRuntime.ts
|
|
453
|
-
var useIsoLayoutEffect = (
|
|
454
|
-
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
455
|
-
typeof window !== "undefined" ? useLayoutEffect : useEffect
|
|
456
|
-
);
|
|
457
|
-
var defaultStorage = createSessionStoragePort();
|
|
458
|
-
function useLessonkitProviderRuntime(config) {
|
|
459
|
-
const normalizedCourseId = useMemo(
|
|
460
|
-
() => assertValidId(config.courseId, "courseId"),
|
|
461
|
-
[config.courseId]
|
|
462
|
-
);
|
|
463
|
-
const normalizedConfig = useMemo(
|
|
464
|
-
() => ({ ...config, courseId: normalizedCourseId }),
|
|
465
|
-
[config, normalizedCourseId]
|
|
466
|
-
);
|
|
467
|
-
const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
|
|
468
|
-
useEffect(() => {
|
|
469
|
-
if (useV2Runtime) return;
|
|
470
|
-
const g = globalThis;
|
|
471
|
-
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
|
|
472
|
-
console.warn(
|
|
473
|
-
'[lessonkit] LessonkitProvider runtimeVersion "v1" is deprecated; omit or use "v2" (default). v1 will be removed in LessonKit 2.0.'
|
|
474
|
-
);
|
|
475
|
-
}, [useV2Runtime]);
|
|
476
|
-
const extraSinksRef = useRef(normalizedConfig.sinks);
|
|
477
|
-
extraSinksRef.current = normalizedConfig.sinks;
|
|
478
|
-
const headlessRef = useRef(null);
|
|
479
|
-
const sessionIdRef = useRef(resolveSessionId(defaultStorage, normalizedConfig.session?.sessionId));
|
|
480
|
-
const prevConfiguredSessionIdRef = useRef(normalizedConfig.session?.sessionId);
|
|
481
|
-
if (normalizedConfig.session?.sessionId) {
|
|
482
|
-
sessionIdRef.current = normalizedConfig.session.sessionId;
|
|
483
|
-
} else if (prevConfiguredSessionIdRef.current) {
|
|
484
|
-
sessionIdRef.current = resolveSessionId(defaultStorage, void 0);
|
|
485
|
-
}
|
|
486
|
-
const attemptIdRef = useRef(normalizedConfig.session?.attemptId);
|
|
487
|
-
const userRef = useRef(normalizedConfig.session?.user);
|
|
488
|
-
attemptIdRef.current = normalizedConfig.session?.attemptId;
|
|
489
|
-
userRef.current = normalizedConfig.session?.user;
|
|
490
|
-
const courseIdRef = useRef(normalizedCourseId);
|
|
491
|
-
courseIdRef.current = normalizedCourseId;
|
|
492
|
-
const lxpackBridgeModeRef = useRef(normalizedConfig.lxpack?.bridge ?? "auto");
|
|
493
|
-
lxpackBridgeModeRef.current = normalizedConfig.lxpack?.bridge ?? "auto";
|
|
494
|
-
const observabilityRef = useRef(normalizedConfig.observability);
|
|
495
|
-
observabilityRef.current = normalizedConfig.observability;
|
|
496
|
-
const onLxpackBridgeMiss = useCallback((event) => {
|
|
497
|
-
observabilityRef.current?.onLxpackBridgeMiss?.(event);
|
|
498
|
-
}, []);
|
|
499
|
-
const pluginsFingerprint = normalizedConfig.plugins?.map((p) => `${p.id}\0${p.version}`).join("|") ?? "";
|
|
500
|
-
const pluginHost = useMemo(
|
|
501
|
-
() => createReactPluginHost(normalizedConfig.plugins),
|
|
502
|
-
[pluginsFingerprint]
|
|
503
|
-
);
|
|
504
|
-
const pluginHostRef = useRef(pluginHost);
|
|
505
|
-
pluginHostRef.current = pluginHost;
|
|
506
|
-
const progressRef = useRef(createProgressController());
|
|
507
|
-
const courseStartedEmittedToSinkRef = useRef(false);
|
|
508
|
-
const courseStartedEmitGenerationRef = useRef(0);
|
|
509
|
-
const prevCourseIdForProgressRef = useRef(normalizedCourseId);
|
|
510
|
-
const pendingCourseIdResetRef = useRef(false);
|
|
511
|
-
const prevUseV2RuntimeRef = useRef(useV2Runtime);
|
|
512
|
-
const xapiCourseStartedSentOnClientRef = useRef(false);
|
|
513
|
-
if (prevUseV2RuntimeRef.current !== useV2Runtime) {
|
|
514
|
-
prevUseV2RuntimeRef.current = useV2Runtime;
|
|
515
|
-
if (useV2Runtime) {
|
|
516
|
-
headlessRef.current = createLessonkitRuntime({
|
|
517
|
-
courseId: normalizedCourseId,
|
|
518
|
-
runtimeVersion: "v2",
|
|
519
|
-
session: normalizedConfig.session,
|
|
520
|
-
plugins: pluginHostRef.current ?? normalizedConfig.plugins,
|
|
521
|
-
deferPluginSetup: true
|
|
522
|
-
});
|
|
523
|
-
progressRef.current = headlessRef.current.progress;
|
|
524
|
-
} else {
|
|
525
|
-
headlessRef.current = null;
|
|
526
|
-
progressRef.current = createProgressController();
|
|
527
|
-
}
|
|
528
|
-
pendingCourseIdResetRef.current = true;
|
|
529
|
-
courseStartedEmittedToSinkRef.current = false;
|
|
530
|
-
courseStartedEmitGenerationRef.current += 1;
|
|
531
|
-
} else if (useV2Runtime && !headlessRef.current) {
|
|
532
|
-
headlessRef.current = createLessonkitRuntime({
|
|
533
|
-
courseId: normalizedCourseId,
|
|
534
|
-
runtimeVersion: "v2",
|
|
535
|
-
session: normalizedConfig.session,
|
|
536
|
-
plugins: pluginHostRef.current ?? normalizedConfig.plugins,
|
|
537
|
-
deferPluginSetup: true
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
|
|
541
|
-
prevCourseIdForProgressRef.current = normalizedCourseId;
|
|
542
|
-
if (useV2Runtime && headlessRef.current) {
|
|
543
|
-
headlessRef.current.resetForCourseChange(normalizedCourseId);
|
|
544
|
-
progressRef.current = headlessRef.current.progress;
|
|
545
|
-
} else {
|
|
546
|
-
progressRef.current = createProgressController();
|
|
547
|
-
}
|
|
548
|
-
pendingCourseIdResetRef.current = true;
|
|
549
|
-
courseStartedEmittedToSinkRef.current = false;
|
|
550
|
-
courseStartedEmitGenerationRef.current += 1;
|
|
551
|
-
}
|
|
552
|
-
if (useV2Runtime && headlessRef.current) {
|
|
553
|
-
progressRef.current = headlessRef.current.progress;
|
|
554
|
-
}
|
|
555
|
-
const [progress, setProgress] = useState(() => progressRef.current.getState());
|
|
556
|
-
const syncProgress = useCallback(() => {
|
|
557
|
-
setProgress(progressRef.current.getState());
|
|
558
|
-
}, []);
|
|
559
|
-
const activeLessonIdRef = useRef(progress.activeLessonId);
|
|
560
|
-
activeLessonIdRef.current = progress.activeLessonId;
|
|
561
|
-
const xapiQueueRef = useRef(createXapiQueueFromObservability(normalizedConfig.observability));
|
|
562
|
-
const xapiRef = useRef(null);
|
|
563
|
-
const [xapi, setXapi] = useState(null);
|
|
564
|
-
const prevXapiCourseIdRef = useRef(normalizedCourseId);
|
|
565
|
-
const xapiEnabled = normalizedConfig.xapi?.enabled;
|
|
566
|
-
const xapiClient = normalizedConfig.xapi?.client;
|
|
567
|
-
const xapiTransport = normalizedConfig.xapi?.transport;
|
|
568
|
-
const courseId = normalizedCourseId;
|
|
569
|
-
const trackingEnabled = normalizedConfig.tracking?.enabled;
|
|
570
|
-
useIsoLayoutEffect(() => {
|
|
571
|
-
const courseChanged = prevXapiCourseIdRef.current !== courseId;
|
|
572
|
-
if (courseChanged) {
|
|
573
|
-
if (normalizedConfig.xapi?.client) {
|
|
574
|
-
const g = globalThis;
|
|
575
|
-
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production") {
|
|
576
|
-
console.warn(
|
|
577
|
-
"[lessonkit] courseId changed while using config.xapi.client; flush the client between courses or use config.xapi.transport so the provider can manage the queue."
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
void xapiRef.current?.flush();
|
|
581
|
-
}
|
|
582
|
-
xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
|
|
583
|
-
prevXapiCourseIdRef.current = courseId;
|
|
584
|
-
xapiCourseStartedSentOnClientRef.current = false;
|
|
585
|
-
}
|
|
586
|
-
const prev = xapiRef.current;
|
|
587
|
-
const next = createXapiClientFromConfig(normalizedConfig, xapiQueueRef.current);
|
|
588
|
-
xapiRef.current = next;
|
|
589
|
-
setXapi(next);
|
|
590
|
-
if (next) {
|
|
591
|
-
const sessionId = sessionIdRef.current;
|
|
592
|
-
const cid = courseIdRef.current;
|
|
593
|
-
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
594
|
-
const alreadyStarted = hasCourseStarted(defaultStorage, sessionId, cid);
|
|
595
|
-
const clientChanged = !prev || prev !== next;
|
|
596
|
-
const skipBootstrap = trackingActive && !alreadyStarted;
|
|
597
|
-
const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!alreadyStarted || clientChanged);
|
|
598
|
-
if (needsBootstrap) {
|
|
599
|
-
try {
|
|
600
|
-
const event = buildCourseStartedEvent({
|
|
601
|
-
pluginHost: pluginHostRef.current,
|
|
602
|
-
courseId: cid,
|
|
603
|
-
sessionId,
|
|
604
|
-
attemptId: attemptIdRef.current,
|
|
605
|
-
user: userRef.current,
|
|
606
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
607
|
-
});
|
|
608
|
-
if (event === null) {
|
|
609
|
-
} else {
|
|
610
|
-
const statement = telemetryEventToXAPIStatement3(event);
|
|
611
|
-
if (statement) {
|
|
612
|
-
next.send(statement);
|
|
613
|
-
if (!alreadyStarted) {
|
|
614
|
-
markCourseStarted(defaultStorage, sessionId, cid);
|
|
615
|
-
}
|
|
616
|
-
xapiCourseStartedSentOnClientRef.current = true;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
} catch {
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
let cancelled = false;
|
|
624
|
-
void (async () => {
|
|
625
|
-
if (prev) {
|
|
626
|
-
try {
|
|
627
|
-
await prev.flush();
|
|
628
|
-
} catch {
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
if (cancelled) return;
|
|
632
|
-
try {
|
|
633
|
-
await next?.flush();
|
|
634
|
-
} catch {
|
|
635
|
-
}
|
|
636
|
-
})();
|
|
637
|
-
return () => {
|
|
638
|
-
cancelled = true;
|
|
639
|
-
void prev?.flush();
|
|
640
|
-
};
|
|
641
|
-
}, [xapiEnabled, xapiClient, xapiTransport, courseId, trackingEnabled]);
|
|
642
|
-
const trackingRef = useRef(createTrackingClient2());
|
|
643
|
-
const trackingClientForUnmountRef = useRef(trackingRef.current);
|
|
644
|
-
const [tracking, setTracking] = useState(() => trackingRef.current);
|
|
645
|
-
const trackingSink = normalizedConfig.tracking?.sink;
|
|
646
|
-
const trackingBatchSink = normalizedConfig.tracking?.batchSink;
|
|
647
|
-
const batchEnabled = normalizedConfig.tracking?.batch?.enabled;
|
|
648
|
-
const batchFlushIntervalMs = normalizedConfig.tracking?.batch?.flushIntervalMs;
|
|
649
|
-
const batchMaxBatchSize = normalizedConfig.tracking?.batch?.maxBatchSize;
|
|
650
|
-
const buildCurrentPluginCtx = useCallback(
|
|
651
|
-
() => buildPluginContext({
|
|
652
|
-
courseId: courseIdRef.current,
|
|
653
|
-
sessionId: sessionIdRef.current,
|
|
654
|
-
attemptId: attemptIdRef.current,
|
|
655
|
-
user: userRef.current
|
|
656
|
-
}),
|
|
657
|
-
[]
|
|
658
|
-
);
|
|
659
|
-
useIsoLayoutEffect(() => {
|
|
660
|
-
const prev = trackingRef.current;
|
|
661
|
-
const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
|
|
662
|
-
const userBatchSink = normalizedConfig.tracking?.batchSink;
|
|
663
|
-
assertTrackingSinkConfig(normalizedConfig.tracking);
|
|
664
|
-
const sink = pluginHostRef.current && baseSink ? (
|
|
665
|
-
/* v8 ignore next -- composeTrackingSink may return null; fall back to base sink */
|
|
666
|
-
pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink
|
|
667
|
-
) : baseSink;
|
|
668
|
-
const batchSink = pluginHostRef.current && userBatchSink ? async (events) => {
|
|
669
|
-
const host = pluginHostRef.current;
|
|
670
|
-
const ctx = buildCurrentPluginCtx();
|
|
671
|
-
const delivered = host.deliverTelemetryBatch(events, ctx);
|
|
672
|
-
const perEventForBatch = [];
|
|
673
|
-
const collector = (event) => {
|
|
674
|
-
perEventForBatch.push(event);
|
|
675
|
-
};
|
|
676
|
-
const composedPerEvent = host.composeTrackingSink(collector, buildCurrentPluginCtx) ?? collector;
|
|
677
|
-
for (const event of delivered) {
|
|
678
|
-
await Promise.resolve(composedPerEvent(event));
|
|
679
|
-
}
|
|
680
|
-
return userBatchSink(perEventForBatch);
|
|
681
|
-
} : userBatchSink;
|
|
682
|
-
const next = createTrackingClientFromConfig(
|
|
683
|
-
{
|
|
684
|
-
tracking: { ...normalizedConfig.tracking, sink, batchSink }
|
|
685
|
-
},
|
|
686
|
-
observabilityRef.current
|
|
687
|
-
);
|
|
688
|
-
trackingRef.current = next;
|
|
689
|
-
trackingClientForUnmountRef.current = next;
|
|
690
|
-
setTracking(next);
|
|
691
|
-
const sessionId = sessionIdRef.current;
|
|
692
|
-
const cid = courseIdRef.current;
|
|
693
|
-
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
694
|
-
const courseStartedFullySettled = hasCourseStartedEmittedToTracking(defaultStorage, sessionId, cid) && hasCourseStarted(defaultStorage, sessionId, cid) && hasCourseStartedPipelineDelivered(defaultStorage, sessionId, cid);
|
|
695
|
-
if (!trackingActive) {
|
|
696
|
-
courseStartedEmittedToSinkRef.current = false;
|
|
697
|
-
} else if (courseStartedFullySettled) {
|
|
698
|
-
courseStartedEmittedToSinkRef.current = true;
|
|
699
|
-
} else if (!courseStartedEmittedToSinkRef.current) {
|
|
700
|
-
const generation = ++courseStartedEmitGenerationRef.current;
|
|
701
|
-
const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
|
|
702
|
-
void (async () => {
|
|
703
|
-
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
704
|
-
const result = await emitPendingCourseStarted({
|
|
705
|
-
pluginHost: pluginHostRef.current,
|
|
706
|
-
tracking: next,
|
|
707
|
-
xapi: xapiRef.current,
|
|
708
|
-
storage: defaultStorage,
|
|
709
|
-
sessionId,
|
|
710
|
-
courseId: cid,
|
|
711
|
-
attemptId: attemptIdRef.current,
|
|
712
|
-
user: userRef.current,
|
|
713
|
-
lxpackBridge: lxpackBridgeModeRef.current,
|
|
714
|
-
onLxpackBridgeMiss,
|
|
715
|
-
extraSinks: extraSinksRef.current,
|
|
716
|
-
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
717
|
-
onXapiStatementSent: () => {
|
|
718
|
-
xapiCourseStartedSentOnClientRef.current = true;
|
|
719
|
-
},
|
|
720
|
-
shouldCommit
|
|
721
|
-
});
|
|
722
|
-
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
723
|
-
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
724
|
-
})();
|
|
725
|
-
}
|
|
726
|
-
return () => {
|
|
727
|
-
courseStartedEmitGenerationRef.current += 1;
|
|
728
|
-
if (prev !== trackingRef.current) {
|
|
729
|
-
void disposeTrackingClient(prev);
|
|
730
|
-
}
|
|
731
|
-
};
|
|
732
|
-
}, [
|
|
733
|
-
trackingEnabled,
|
|
734
|
-
trackingSink,
|
|
735
|
-
trackingBatchSink,
|
|
736
|
-
batchEnabled,
|
|
737
|
-
batchFlushIntervalMs,
|
|
738
|
-
batchMaxBatchSize,
|
|
739
|
-
normalizedConfig.plugins,
|
|
740
|
-
normalizedCourseId,
|
|
741
|
-
buildCurrentPluginCtx
|
|
742
|
-
]);
|
|
743
|
-
const emitWithBridge = useCallback((trackingClient, event) => {
|
|
744
|
-
emitTelemetryWithPlugins({
|
|
745
|
-
pluginHost: pluginHostRef.current,
|
|
746
|
-
tracking: trackingClient,
|
|
747
|
-
xapi: xapiRef.current,
|
|
748
|
-
event,
|
|
749
|
-
pluginCtx: buildPluginContext({
|
|
750
|
-
courseId: courseIdRef.current,
|
|
751
|
-
sessionId: sessionIdRef.current,
|
|
752
|
-
attemptId: attemptIdRef.current,
|
|
753
|
-
user: userRef.current
|
|
754
|
-
}),
|
|
755
|
-
lxpackBridge: lxpackBridgeModeRef.current,
|
|
756
|
-
onLxpackBridgeMiss,
|
|
757
|
-
extraSinks: extraSinksRef.current
|
|
758
|
-
});
|
|
759
|
-
}, [onLxpackBridgeMiss]);
|
|
760
|
-
const emitLifecycleEvent = useCallback(
|
|
761
|
-
(name, data, lessonId) => {
|
|
762
|
-
const event = tryBuildTelemetryEvent({
|
|
763
|
-
name,
|
|
764
|
-
courseId: courseIdRef.current,
|
|
765
|
-
lessonId: lessonId ?? activeLessonIdRef.current,
|
|
766
|
-
sessionId: sessionIdRef.current,
|
|
767
|
-
attemptId: attemptIdRef.current,
|
|
768
|
-
user: userRef.current,
|
|
769
|
-
data
|
|
770
|
-
});
|
|
771
|
-
if (!event) return;
|
|
772
|
-
emitWithBridge(trackingRef.current, event);
|
|
773
|
-
},
|
|
774
|
-
[emitWithBridge]
|
|
775
|
-
);
|
|
776
|
-
const track = useCallback(
|
|
777
|
-
(name, data, opts) => {
|
|
778
|
-
const event = tryBuildTelemetryEvent({
|
|
779
|
-
name,
|
|
780
|
-
courseId: courseIdRef.current,
|
|
781
|
-
lessonId: opts?.lessonId ?? activeLessonIdRef.current,
|
|
782
|
-
sessionId: sessionIdRef.current,
|
|
783
|
-
attemptId: attemptIdRef.current,
|
|
784
|
-
user: userRef.current,
|
|
785
|
-
data
|
|
786
|
-
});
|
|
787
|
-
if (!event) return;
|
|
788
|
-
emitWithBridge(trackingRef.current, event);
|
|
789
|
-
},
|
|
790
|
-
[emitWithBridge]
|
|
791
|
-
);
|
|
792
|
-
useLayoutEffect(() => {
|
|
793
|
-
if (!pendingCourseIdResetRef.current) return;
|
|
794
|
-
pendingCourseIdResetRef.current = false;
|
|
795
|
-
syncProgress();
|
|
796
|
-
if (!isTrackingActive(normalizedConfig.tracking)) return;
|
|
797
|
-
const sessionId = sessionIdRef.current;
|
|
798
|
-
const cid = courseIdRef.current;
|
|
799
|
-
void (async () => {
|
|
800
|
-
try {
|
|
801
|
-
await trackingRef.current?.flush?.();
|
|
802
|
-
} catch {
|
|
803
|
-
}
|
|
804
|
-
if (!courseStartedEmittedToSinkRef.current) {
|
|
805
|
-
const result = await emitPendingCourseStarted({
|
|
806
|
-
pluginHost: pluginHostRef.current,
|
|
807
|
-
tracking: trackingRef.current,
|
|
808
|
-
xapi: xapiRef.current,
|
|
809
|
-
storage: defaultStorage,
|
|
810
|
-
sessionId,
|
|
811
|
-
courseId: cid,
|
|
812
|
-
attemptId: attemptIdRef.current,
|
|
813
|
-
user: userRef.current,
|
|
814
|
-
lxpackBridge: lxpackBridgeModeRef.current,
|
|
815
|
-
onLxpackBridgeMiss,
|
|
816
|
-
extraSinks: extraSinksRef.current
|
|
817
|
-
});
|
|
818
|
-
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
819
|
-
}
|
|
820
|
-
})();
|
|
821
|
-
}, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress, onLxpackBridgeMiss]);
|
|
822
|
-
const emitLessonCompleted = useCallback(
|
|
823
|
-
(lessonId, durationMs) => {
|
|
824
|
-
track("lesson_completed", { lessonId, durationMs }, { lessonId });
|
|
825
|
-
if (durationMs !== void 0) {
|
|
826
|
-
track("lesson_time_on_task", { lessonId, durationMs }, { lessonId });
|
|
827
|
-
}
|
|
828
|
-
},
|
|
829
|
-
[track]
|
|
830
|
-
);
|
|
831
|
-
const completeLesson = useCallback(
|
|
832
|
-
(lessonId, opts) => {
|
|
833
|
-
if (opts?.courseId !== void 0 && opts.courseId !== courseIdRef.current) {
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
if (useV2Runtime && headlessRef.current) {
|
|
837
|
-
headlessRef.current.completeLesson(lessonId, emitLifecycleEvent);
|
|
838
|
-
syncProgress();
|
|
839
|
-
void Promise.resolve(trackingRef.current?.flush?.());
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
const result = progressRef.current.completeLesson(lessonId, Date.now());
|
|
843
|
-
if (!result.didComplete) return;
|
|
844
|
-
syncProgress();
|
|
845
|
-
emitLessonCompleted(lessonId, result.durationMs);
|
|
846
|
-
void Promise.resolve(trackingRef.current?.flush?.());
|
|
847
|
-
},
|
|
848
|
-
[syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
|
|
849
|
-
);
|
|
850
|
-
useEffect(() => {
|
|
851
|
-
return () => {
|
|
852
|
-
const client = trackingClientForUnmountRef.current;
|
|
853
|
-
const xapi2 = xapiRef.current;
|
|
854
|
-
void (async () => {
|
|
855
|
-
try {
|
|
856
|
-
await xapi2?.flush();
|
|
857
|
-
} catch {
|
|
858
|
-
}
|
|
859
|
-
try {
|
|
860
|
-
await client?.flush?.();
|
|
861
|
-
} catch {
|
|
862
|
-
}
|
|
863
|
-
try {
|
|
864
|
-
await client?.dispose?.();
|
|
865
|
-
} catch {
|
|
866
|
-
}
|
|
867
|
-
})();
|
|
868
|
-
};
|
|
869
|
-
}, []);
|
|
870
|
-
useEffect(() => {
|
|
871
|
-
if (typeof document === "undefined") return;
|
|
872
|
-
const flushOnExit = () => {
|
|
873
|
-
void xapiRef.current?.flush();
|
|
874
|
-
void trackingRef.current?.flush?.();
|
|
875
|
-
};
|
|
876
|
-
const onVisibilityChange = () => {
|
|
877
|
-
if (document.visibilityState === "hidden") flushOnExit();
|
|
878
|
-
};
|
|
879
|
-
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
880
|
-
window.addEventListener("pagehide", flushOnExit);
|
|
881
|
-
return () => {
|
|
882
|
-
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
883
|
-
window.removeEventListener("pagehide", flushOnExit);
|
|
884
|
-
};
|
|
885
|
-
}, []);
|
|
886
|
-
const setActiveLesson = useCallback(
|
|
887
|
-
(lessonId) => {
|
|
888
|
-
if (useV2Runtime && headlessRef.current) {
|
|
889
|
-
headlessRef.current.setActiveLesson(lessonId, emitLifecycleEvent);
|
|
890
|
-
syncProgress();
|
|
891
|
-
void Promise.resolve(trackingRef.current?.flush?.());
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
const current = progressRef.current.getState();
|
|
895
|
-
if (current.activeLessonId === lessonId) return;
|
|
896
|
-
if (current.completedLessonIds.has(lessonId)) {
|
|
897
|
-
progressRef.current.setActiveLesson(lessonId, Date.now());
|
|
898
|
-
syncProgress();
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
const previous = current.activeLessonId;
|
|
902
|
-
if (previous && previous !== lessonId) {
|
|
903
|
-
const completed = progressRef.current.completeLesson(previous, Date.now());
|
|
904
|
-
if (completed.didComplete) {
|
|
905
|
-
emitLessonCompleted(previous, completed.durationMs);
|
|
906
|
-
void Promise.resolve(trackingRef.current?.flush?.());
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
progressRef.current.setActiveLesson(lessonId, Date.now());
|
|
910
|
-
syncProgress();
|
|
911
|
-
track("lesson_started", { lessonId }, { lessonId });
|
|
912
|
-
},
|
|
913
|
-
[track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
|
|
914
|
-
);
|
|
915
|
-
const completeCourse = useCallback(() => {
|
|
916
|
-
if (useV2Runtime && headlessRef.current) {
|
|
917
|
-
headlessRef.current.completeCourse(emitLifecycleEvent);
|
|
918
|
-
syncProgress();
|
|
919
|
-
void trackingRef.current?.flush?.();
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
const current = progressRef.current.getState();
|
|
923
|
-
if (current.activeLessonId) {
|
|
924
|
-
const lessonResult = progressRef.current.completeLesson(current.activeLessonId, Date.now());
|
|
925
|
-
if (lessonResult.didComplete) {
|
|
926
|
-
emitLessonCompleted(current.activeLessonId, lessonResult.durationMs);
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
const result = progressRef.current.completeCourse();
|
|
930
|
-
if (!result.didComplete) return;
|
|
931
|
-
syncProgress();
|
|
932
|
-
track("course_completed");
|
|
933
|
-
void trackingRef.current?.flush?.();
|
|
934
|
-
}, [track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]);
|
|
935
|
-
const sessionUser = normalizedConfig.session?.user;
|
|
936
|
-
const sessionUserKey = useMemo(
|
|
937
|
-
() => sessionUser ? JSON.stringify(sessionUser) : "",
|
|
938
|
-
[sessionUser]
|
|
939
|
-
);
|
|
940
|
-
const sessionAttemptId = normalizedConfig.session?.attemptId;
|
|
941
|
-
const sessionConfiguredId = normalizedConfig.session?.sessionId;
|
|
942
|
-
useEffect(() => {
|
|
943
|
-
if (useV2Runtime && headlessRef.current) {
|
|
944
|
-
headlessRef.current.updateConfig({
|
|
945
|
-
courseId: normalizedCourseId,
|
|
946
|
-
session: normalizedConfig.session
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
}, [
|
|
950
|
-
useV2Runtime,
|
|
951
|
-
normalizedCourseId,
|
|
952
|
-
sessionAttemptId,
|
|
953
|
-
sessionConfiguredId,
|
|
954
|
-
sessionUserKey,
|
|
955
|
-
normalizedConfig.session
|
|
956
|
-
]);
|
|
957
|
-
useEffect(() => {
|
|
958
|
-
if (!useV2Runtime || !headlessRef.current) return;
|
|
959
|
-
headlessRef.current.updateConfig({
|
|
960
|
-
plugins: pluginHostRef.current ?? normalizedConfig.plugins
|
|
961
|
-
});
|
|
962
|
-
}, [useV2Runtime, pluginHost]);
|
|
963
|
-
useEffect(() => {
|
|
964
|
-
const host = useV2Runtime ? headlessRef.current?.pluginHost ?? null : pluginHost;
|
|
965
|
-
if (!host) return;
|
|
966
|
-
const ctx = buildPluginContext({
|
|
967
|
-
courseId: courseIdRef.current,
|
|
968
|
-
sessionId: sessionIdRef.current,
|
|
969
|
-
attemptId: attemptIdRef.current,
|
|
970
|
-
user: userRef.current
|
|
971
|
-
});
|
|
972
|
-
host.setupAll(ctx);
|
|
973
|
-
return () => {
|
|
974
|
-
host.disposeAll();
|
|
975
|
-
};
|
|
976
|
-
}, [pluginHost, useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
|
|
977
|
-
useEffect(() => {
|
|
978
|
-
const nextConfigured = normalizedConfig.session?.sessionId;
|
|
979
|
-
const prevConfigured = prevConfiguredSessionIdRef.current;
|
|
980
|
-
if (nextConfigured === prevConfigured) return;
|
|
981
|
-
prevConfiguredSessionIdRef.current = nextConfigured;
|
|
982
|
-
const cid = courseIdRef.current;
|
|
983
|
-
if (nextConfigured) {
|
|
984
|
-
const fromIds = /* @__PURE__ */ new Set();
|
|
985
|
-
if (prevConfigured) fromIds.add(prevConfigured);
|
|
986
|
-
const tabId = getTabSessionId(defaultStorage);
|
|
987
|
-
if (tabId) fromIds.add(tabId);
|
|
988
|
-
for (const fromId of fromIds) {
|
|
989
|
-
if (fromId !== nextConfigured) {
|
|
990
|
-
migrateCourseStartedMark(defaultStorage, fromId, nextConfigured, cid);
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
sessionIdRef.current = nextConfigured;
|
|
994
|
-
} else if (prevConfigured) {
|
|
995
|
-
const nextAuto = resolveSessionId(defaultStorage, void 0);
|
|
996
|
-
migrateCourseStartedMark(defaultStorage, prevConfigured, nextAuto, cid);
|
|
997
|
-
sessionIdRef.current = nextAuto;
|
|
998
|
-
}
|
|
999
|
-
}, [sessionConfiguredId, normalizedCourseId]);
|
|
1000
|
-
const runtime = useMemo(
|
|
1001
|
-
() => ({
|
|
1002
|
-
config: normalizedConfig,
|
|
1003
|
-
tracking,
|
|
1004
|
-
xapi,
|
|
1005
|
-
storage: defaultStorage,
|
|
1006
|
-
session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
|
|
1007
|
-
progress,
|
|
1008
|
-
setActiveLesson,
|
|
1009
|
-
completeLesson,
|
|
1010
|
-
completeCourse,
|
|
1011
|
-
track,
|
|
1012
|
-
plugins: pluginHost
|
|
1013
|
-
}),
|
|
1014
|
-
[
|
|
1015
|
-
normalizedConfig,
|
|
1016
|
-
tracking,
|
|
1017
|
-
xapi,
|
|
1018
|
-
progress,
|
|
1019
|
-
setActiveLesson,
|
|
1020
|
-
completeLesson,
|
|
1021
|
-
completeCourse,
|
|
1022
|
-
track,
|
|
1023
|
-
pluginHost,
|
|
1024
|
-
sessionUser,
|
|
1025
|
-
sessionAttemptId,
|
|
1026
|
-
sessionConfiguredId
|
|
1027
|
-
]
|
|
1028
|
-
);
|
|
1029
|
-
return runtime;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
// src/context.tsx
|
|
1033
|
-
import { jsx } from "react/jsx-runtime";
|
|
1034
|
-
var LessonkitContext = createContext(null);
|
|
1035
|
-
function LessonkitProvider(props) {
|
|
1036
|
-
const runtime = useLessonkitProviderRuntime(props.config);
|
|
1037
|
-
return /* @__PURE__ */ jsx(LessonkitContext.Provider, { value: runtime, children: props.children });
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
// src/hooks.ts
|
|
1041
|
-
import { useContext, useMemo as useMemo3 } from "react";
|
|
1042
|
-
|
|
1043
|
-
// src/assessment/useAssessmentState.ts
|
|
1044
|
-
import { useMemo as useMemo2 } from "react";
|
|
1045
|
-
function useAssessmentState(enclosingLessonId) {
|
|
1046
|
-
const { track } = useLessonkit();
|
|
1047
|
-
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
1048
|
-
return useMemo2(
|
|
1049
|
-
() => ({
|
|
1050
|
-
answer: (data) => {
|
|
1051
|
-
track("assessment_answered", data, trackOpts);
|
|
1052
|
-
},
|
|
1053
|
-
complete: (data) => {
|
|
1054
|
-
track("assessment_completed", data, trackOpts);
|
|
1055
|
-
}
|
|
1056
|
-
}),
|
|
1057
|
-
[track, enclosingLessonId]
|
|
1058
|
-
);
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
// src/hooks.ts
|
|
1062
|
-
function useLessonkit() {
|
|
1063
|
-
const ctx = useContext(LessonkitContext);
|
|
1064
|
-
if (!ctx) throw new Error("LessonKit: missing LessonkitProvider");
|
|
1065
|
-
return ctx;
|
|
1066
|
-
}
|
|
1067
|
-
function useProgress() {
|
|
1068
|
-
const { progress } = useLessonkit();
|
|
1069
|
-
return progress;
|
|
1070
|
-
}
|
|
1071
|
-
function useTracking() {
|
|
1072
|
-
const { track } = useLessonkit();
|
|
1073
|
-
return useMemo3(() => ({ track }), [track]);
|
|
1074
|
-
}
|
|
1075
|
-
function useCompletion() {
|
|
1076
|
-
const { completeLesson, completeCourse } = useLessonkit();
|
|
1077
|
-
return useMemo3(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
|
|
1078
|
-
}
|
|
1079
|
-
function useQuizState(enclosingLessonId) {
|
|
1080
|
-
const { track } = useLessonkit();
|
|
1081
|
-
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
1082
|
-
return useMemo3(
|
|
1083
|
-
() => ({
|
|
1084
|
-
answer: (opts) => {
|
|
1085
|
-
track("quiz_answered", opts, trackOpts);
|
|
1086
|
-
},
|
|
1087
|
-
complete: (opts) => {
|
|
1088
|
-
track("quiz_completed", opts, trackOpts);
|
|
1089
|
-
}
|
|
1090
|
-
}),
|
|
1091
|
-
[track, enclosingLessonId]
|
|
1092
|
-
);
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// src/lessonContext.tsx
|
|
1096
|
-
import { createContext as createContext2, useContext as useContext2 } from "react";
|
|
1097
|
-
var LessonContext = createContext2(void 0);
|
|
1098
|
-
function useEnclosingLessonId() {
|
|
1099
|
-
return useContext2(LessonContext);
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// src/runtime/validateComponentId.ts
|
|
1103
|
-
import { assertValidId as assertValidId2 } from "@lessonkit/core";
|
|
1104
|
-
function isDevEnvironment4() {
|
|
1105
|
-
const g = globalThis;
|
|
1106
|
-
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
1107
|
-
}
|
|
1108
|
-
function normalizeComponentId(id, path) {
|
|
1109
|
-
if (path === "courseId") return assertValidId2(id, "courseId");
|
|
1110
|
-
if (path === "lessonId") return assertValidId2(id, "lessonId");
|
|
1111
|
-
if (path === "checkId") return assertValidId2(id, "checkId");
|
|
1112
|
-
if (path === "blockId") return assertValidId2(id, "blockId");
|
|
1113
|
-
return assertValidId2(id, path);
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
// src/runtime/lessonMountRegistry.ts
|
|
1117
|
-
var mountCounts = /* @__PURE__ */ new Map();
|
|
1118
|
-
var warnedConcurrentLessons = false;
|
|
1119
|
-
function registerLessonMount(lessonId) {
|
|
1120
|
-
if (isDevEnvironment4() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
|
|
1121
|
-
warnedConcurrentLessons = true;
|
|
1122
|
-
console.warn(
|
|
1123
|
-
"[lessonkit] Multiple <Lesson> components are mounted; only one should be active at a time. Set autoCompleteOnUnmount={false} on routed lessons or unmount the previous lesson before showing the next."
|
|
1124
|
-
);
|
|
1125
|
-
}
|
|
1126
|
-
mountCounts.set(lessonId, (mountCounts.get(lessonId) ?? 0) + 1);
|
|
1127
|
-
return () => {
|
|
1128
|
-
const next = (mountCounts.get(lessonId) ?? 1) - 1;
|
|
1129
|
-
if (next <= 0) {
|
|
1130
|
-
mountCounts.delete(lessonId);
|
|
1131
|
-
} else {
|
|
1132
|
-
mountCounts.set(lessonId, next);
|
|
1133
|
-
}
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
function getLessonMountCount(lessonId) {
|
|
1137
|
-
return mountCounts.get(lessonId) ?? 0;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// src/components/Quiz.tsx
|
|
1141
|
-
import { forwardRef, useEffect as useEffect3, useId, useMemo as useMemo5, useRef as useRef4, useState as useState3 } from "react";
|
|
1142
|
-
import { visuallyHiddenStyle } from "@lessonkit/accessibility";
|
|
1143
|
-
|
|
1144
|
-
// src/assessment/AssessmentLessonGuard.tsx
|
|
1145
|
-
import { useEffect as useEffect2 } from "react";
|
|
1146
|
-
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
1147
|
-
var warnedAssessmentOutsideLesson = false;
|
|
1148
|
-
function resetAssessmentWarningsForTests() {
|
|
1149
|
-
warnedAssessmentOutsideLesson = false;
|
|
1150
|
-
}
|
|
1151
|
-
function AssessmentLessonGuard(props) {
|
|
1152
|
-
const enclosingLessonId = useEnclosingLessonId();
|
|
1153
|
-
const missingLesson = enclosingLessonId === void 0;
|
|
1154
|
-
useEffect2(() => {
|
|
1155
|
-
if (!missingLesson || isDevEnvironment4()) return;
|
|
1156
|
-
if (!warnedAssessmentOutsideLesson) {
|
|
1157
|
-
warnedAssessmentOutsideLesson = true;
|
|
1158
|
-
console.error(
|
|
1159
|
-
`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>; assessment telemetry will not be emitted.`
|
|
1160
|
-
);
|
|
1161
|
-
}
|
|
1162
|
-
}, [missingLesson, props.blockLabel]);
|
|
1163
|
-
if (missingLesson && isDevEnvironment4()) {
|
|
1164
|
-
throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
|
|
1165
|
-
}
|
|
1166
|
-
if (missingLesson) {
|
|
1167
|
-
return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label": `${props.blockLabel} configuration error`, "data-lk-check-id": props.checkId, children: /* @__PURE__ */ jsxs("p", { children: [
|
|
1168
|
-
props.blockLabel,
|
|
1169
|
-
" must be placed inside a Lesson."
|
|
1170
|
-
] }) });
|
|
1171
|
-
}
|
|
1172
|
-
return /* @__PURE__ */ jsx2(Fragment, { children: props.children(enclosingLessonId) });
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
// src/assessment/internal/buildAssessmentHandle.ts
|
|
1176
|
-
function buildAssessmentHandle(opts) {
|
|
1177
|
-
return {
|
|
1178
|
-
getScore: opts.getScore,
|
|
1179
|
-
getMaxScore: opts.getMaxScore,
|
|
1180
|
-
getAnswerGiven: opts.getAnswerGiven,
|
|
1181
|
-
resetTask: opts.resetTask,
|
|
1182
|
-
showSolutions: opts.showSolutions,
|
|
1183
|
-
getXAPIData: opts.getXAPIData,
|
|
1184
|
-
...opts.getCurrentState ? { getCurrentState: opts.getCurrentState } : {},
|
|
1185
|
-
...opts.resume ? { resume: opts.resume } : {}
|
|
1186
|
-
};
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
// src/assessment/internal/resumeState.ts
|
|
1190
|
-
function readBooleanField(state, key) {
|
|
1191
|
-
const value = state[key];
|
|
1192
|
-
if (value === true || value === false || value === null) return value;
|
|
1193
|
-
return void 0;
|
|
1194
|
-
}
|
|
1195
|
-
function readStringField(state, key) {
|
|
1196
|
-
const value = state[key];
|
|
1197
|
-
if (typeof value === "string" || value === null) return value;
|
|
1198
|
-
return void 0;
|
|
1199
|
-
}
|
|
1200
|
-
function readNumberField(state, key) {
|
|
1201
|
-
const value = state[key];
|
|
1202
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1203
|
-
if (value === null) return null;
|
|
1204
|
-
return void 0;
|
|
1205
|
-
}
|
|
1206
|
-
function readBooleanStateField(state, key, apply) {
|
|
1207
|
-
const value = state[key];
|
|
1208
|
-
if (typeof value === "boolean") apply(value);
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
// src/assessment/internal/useAssessmentHandleRegistration.ts
|
|
1212
|
-
import { useImperativeHandle as useImperativeHandle2 } from "react";
|
|
1213
|
-
|
|
1214
|
-
// src/compound/CompoundProvider.tsx
|
|
1215
|
-
import React5, { createContext as createContext5, useCallback as useCallback2, useContext as useContext5, useImperativeHandle, useMemo as useMemo4, useRef as useRef3, useState as useState2 } from "react";
|
|
1216
|
-
import { clampCompoundPageIndex, createCompoundResumeState } from "@lessonkit/core";
|
|
1217
|
-
|
|
1218
|
-
// src/compound/aggregateScores.ts
|
|
1219
|
-
function aggregateAssessmentScores(handles, opts) {
|
|
1220
|
-
let score = 0;
|
|
1221
|
-
let maxScore = 0;
|
|
1222
|
-
let allAnswered = true;
|
|
1223
|
-
for (const entry of handles) {
|
|
1224
|
-
const handle = "handle" in entry ? entry.handle : entry;
|
|
1225
|
-
const pageIndex = "handle" in entry ? entry.pageIndex : void 0;
|
|
1226
|
-
score += handle.getScore();
|
|
1227
|
-
maxScore += handle.getMaxScore();
|
|
1228
|
-
const countsForAnswerGiven = opts?.answerPageIndex === void 0 || pageIndex === void 0 || pageIndex === opts.answerPageIndex;
|
|
1229
|
-
if (countsForAnswerGiven && !handle.getAnswerGiven()) allAnswered = false;
|
|
1230
|
-
}
|
|
1231
|
-
return { score, maxScore, allAnswered };
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
// src/compound/CompoundHydrationBridge.tsx
|
|
1235
|
-
import { createContext as createContext3, useContext as useContext3, useRef as useRef2 } from "react";
|
|
1236
|
-
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
1237
|
-
var CompoundHydrationBridgeContext = createContext3(
|
|
1238
|
-
null
|
|
1239
|
-
);
|
|
1240
|
-
function CompoundHydrationBridgeProvider({ children }) {
|
|
1241
|
-
const bridgeRef = useRef2(null);
|
|
1242
|
-
return /* @__PURE__ */ jsx3(CompoundHydrationBridgeContext.Provider, { value: bridgeRef, children });
|
|
1243
|
-
}
|
|
1244
|
-
function useCompoundHydrationBridgeRef() {
|
|
1245
|
-
return useContext3(CompoundHydrationBridgeContext);
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
// src/compound/CompoundPageIndexContext.tsx
|
|
1249
|
-
import { createContext as createContext4, useContext as useContext4 } from "react";
|
|
1250
|
-
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1251
|
-
var CompoundPageIndexContext = createContext4(void 0);
|
|
1252
|
-
function CompoundPageIndexProvider({
|
|
1253
|
-
pageIndex,
|
|
1254
|
-
children
|
|
1255
|
-
}) {
|
|
1256
|
-
return /* @__PURE__ */ jsx4(CompoundPageIndexContext.Provider, { value: pageIndex, children });
|
|
1257
|
-
}
|
|
1258
|
-
function useCompoundPageIndex() {
|
|
1259
|
-
return useContext4(CompoundPageIndexContext);
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
// src/compound/CompoundProvider.tsx
|
|
1263
|
-
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1264
|
-
var CompoundRegistryContext = createContext5(null);
|
|
1265
|
-
var CompoundHandlesVersionContext = createContext5(0);
|
|
1266
|
-
function CompoundProvider({
|
|
1267
|
-
children,
|
|
1268
|
-
activePageIndex: _activePageIndex,
|
|
1269
|
-
onActivePageIndexChange: _onActivePageIndexChange
|
|
1270
|
-
}) {
|
|
1271
|
-
const registryRef = useRef3(/* @__PURE__ */ new Map());
|
|
1272
|
-
const [handlesVersion, setHandlesVersion] = useState2(0);
|
|
1273
|
-
const register = useCallback2((checkId, handle, pageIndex) => {
|
|
1274
|
-
const prev = registryRef.current.get(checkId);
|
|
1275
|
-
if (prev && prev.handle !== handle) {
|
|
1276
|
-
const message = `[lessonkit] duplicate checkId "${checkId}" registered in the same compound container; the previous handle was replaced.`;
|
|
1277
|
-
if (isDevEnvironment4()) {
|
|
1278
|
-
console.error(message);
|
|
1279
|
-
} else {
|
|
1280
|
-
console.warn(message);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
registryRef.current.set(checkId, { handle, pageIndex });
|
|
1284
|
-
if (prev?.handle !== handle || prev?.pageIndex !== pageIndex) {
|
|
1285
|
-
setHandlesVersion((v) => v + 1);
|
|
1286
|
-
}
|
|
1287
|
-
return () => {
|
|
1288
|
-
const current = registryRef.current.get(checkId);
|
|
1289
|
-
if (current?.handle === handle) {
|
|
1290
|
-
registryRef.current.delete(checkId);
|
|
1291
|
-
setHandlesVersion((v) => v + 1);
|
|
1292
|
-
}
|
|
1293
|
-
};
|
|
1294
|
-
}, []);
|
|
1295
|
-
const registryValue = useMemo4(
|
|
1296
|
-
() => ({
|
|
1297
|
-
register,
|
|
1298
|
-
getHandles: () => {
|
|
1299
|
-
const handles = /* @__PURE__ */ new Map();
|
|
1300
|
-
for (const [checkId, entry] of registryRef.current) {
|
|
1301
|
-
handles.set(checkId, entry.handle);
|
|
1302
|
-
}
|
|
1303
|
-
return handles;
|
|
1304
|
-
},
|
|
1305
|
-
getRegisteredHandles: () => registryRef.current
|
|
1306
|
-
}),
|
|
1307
|
-
[register]
|
|
1308
|
-
);
|
|
1309
|
-
return /* @__PURE__ */ jsx5(CompoundHydrationBridgeProvider, { children: /* @__PURE__ */ jsx5(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ jsx5(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) }) });
|
|
1310
|
-
}
|
|
1311
|
-
function useCompoundRegistry() {
|
|
1312
|
-
const registry = useContext5(CompoundRegistryContext);
|
|
1313
|
-
const handlesVersion = useContext5(CompoundHandlesVersionContext);
|
|
1314
|
-
if (!registry) return null;
|
|
1315
|
-
return { ...registry, handlesVersion };
|
|
1316
|
-
}
|
|
1317
|
-
function useCompoundHandlesVersion() {
|
|
1318
|
-
return useContext5(CompoundHandlesVersionContext);
|
|
1319
|
-
}
|
|
1320
|
-
function useRegisterAssessmentHandle(checkId, handle) {
|
|
1321
|
-
const registry = useContext5(CompoundRegistryContext);
|
|
1322
|
-
const pageIndex = useCompoundPageIndex();
|
|
1323
|
-
React5.useLayoutEffect(() => {
|
|
1324
|
-
if (!registry || !handle) return;
|
|
1325
|
-
return registry.register(checkId, handle, pageIndex);
|
|
1326
|
-
}, [registry, checkId, handle, pageIndex]);
|
|
1327
|
-
}
|
|
1328
|
-
function useCompoundHandleRef(ref, opts) {
|
|
1329
|
-
const { activePageIndex, setActivePageIndex, getHandles, getRegisteredHandles, pageCount } = opts;
|
|
1330
|
-
const bridgeRef = useCompoundHydrationBridgeRef();
|
|
1331
|
-
const setIndexClamped = useCallback2(
|
|
1332
|
-
(index) => {
|
|
1333
|
-
const next = pageCount !== void 0 ? clampCompoundPageIndex(index, pageCount) : Math.max(0, Math.floor(index));
|
|
1334
|
-
setActivePageIndex(next);
|
|
1335
|
-
},
|
|
1336
|
-
[pageCount, setActivePageIndex]
|
|
1337
|
-
);
|
|
1338
|
-
useImperativeHandle(
|
|
1339
|
-
ref,
|
|
1340
|
-
() => ({
|
|
1341
|
-
getScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).score,
|
|
1342
|
-
getMaxScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).maxScore,
|
|
1343
|
-
getAnswerGiven: () => aggregateAssessmentScores(getRegisteredHandles().values(), {
|
|
1344
|
-
answerPageIndex: activePageIndex
|
|
1345
|
-
}).allAnswered,
|
|
1346
|
-
resetTask: () => {
|
|
1347
|
-
for (const entry of getRegisteredHandles().values()) entry.handle.resetTask();
|
|
1348
|
-
},
|
|
1349
|
-
showSolutions: () => {
|
|
1350
|
-
if (!opts.enableSolutionsButton) return;
|
|
1351
|
-
for (const entry of getRegisteredHandles().values()) entry.handle.showSolutions();
|
|
1352
|
-
},
|
|
1353
|
-
getCurrentState: () => {
|
|
1354
|
-
const childStates = {};
|
|
1355
|
-
for (const [checkId, entry] of getRegisteredHandles()) {
|
|
1356
|
-
if (entry.handle.getCurrentState) {
|
|
1357
|
-
childStates[checkId] = entry.handle.getCurrentState();
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
return createCompoundResumeState({ activePageIndex, childStates });
|
|
1361
|
-
},
|
|
1362
|
-
resume: (state) => {
|
|
1363
|
-
bridgeRef?.current?.notifyImperativeResume(state);
|
|
1364
|
-
}
|
|
1365
|
-
}),
|
|
1366
|
-
[activePageIndex, setIndexClamped, getHandles, getRegisteredHandles, opts.enableSolutionsButton, bridgeRef]
|
|
1367
|
-
);
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
// src/assessment/internal/useAssessmentHandleRegistration.ts
|
|
1371
|
-
function useAssessmentHandleRegistration(checkId, handle, ref) {
|
|
1372
|
-
useImperativeHandle2(ref, () => handle, [handle]);
|
|
1373
|
-
useRegisterAssessmentHandle(checkId, handle);
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
// src/assessment/internal/usePluginScoring.ts
|
|
1377
|
-
import { useCallback as useCallback3 } from "react";
|
|
1378
|
-
|
|
1379
|
-
// src/assessment/scoring.ts
|
|
1380
|
-
function resolvePassingThreshold(passingScore, maxScore) {
|
|
1381
|
-
return passingScore ?? maxScore;
|
|
1382
|
-
}
|
|
1383
|
-
function meetsPassingThreshold(score, maxScore, passingScore) {
|
|
1384
|
-
const threshold = resolvePassingThreshold(passingScore, maxScore);
|
|
1385
|
-
return score >= threshold;
|
|
1386
|
-
}
|
|
1387
|
-
function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
|
|
1388
|
-
const maxScore = custom?.maxScore ?? fallbackMax;
|
|
1389
|
-
if (custom?.passed !== void 0) {
|
|
1390
|
-
const score2 = custom.passed ? custom.score ?? maxScore : custom.score ?? 0;
|
|
1391
|
-
return { score: score2, maxScore, passed: custom.passed };
|
|
1392
|
-
}
|
|
1393
|
-
if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
1394
|
-
const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
|
|
1395
|
-
return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
|
|
1396
|
-
}
|
|
1397
|
-
const score = fallbackCorrect ? maxScore : 0;
|
|
1398
|
-
const passed = meetsPassingThreshold(score, maxScore, passingScore);
|
|
1399
|
-
return { score, maxScore, passed };
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
// src/assessment/internal/usePluginScoring.ts
|
|
1403
|
-
function usePluginScoring(checkId, lessonId) {
|
|
1404
|
-
const { plugins, config, session } = useLessonkit();
|
|
1405
|
-
const getPluginScore = useCallback3(
|
|
1406
|
-
(response) => {
|
|
1407
|
-
const pluginCtx = buildPluginContext({
|
|
1408
|
-
courseId: config.courseId,
|
|
1409
|
-
sessionId: session.sessionId,
|
|
1410
|
-
attemptId: session.attemptId,
|
|
1411
|
-
user: session.user
|
|
1412
|
-
});
|
|
1413
|
-
return plugins?.scoreAssessment({ checkId, lessonId, response }, pluginCtx) ?? null;
|
|
1414
|
-
},
|
|
1415
|
-
[checkId, config.courseId, lessonId, plugins, session.attemptId, session.sessionId, session.user]
|
|
1416
|
-
);
|
|
1417
|
-
const scoreResponse = useCallback3(
|
|
1418
|
-
(response, defaultCorrect, maxScore = 1, passingScore) => scoreFromCustom(getPluginScore(response), defaultCorrect, maxScore, passingScore),
|
|
1419
|
-
[getPluginScore]
|
|
1420
|
-
);
|
|
1421
|
-
const isChoiceCorrect = useCallback3(
|
|
1422
|
-
(choice, answer, custom, passingScore) => {
|
|
1423
|
-
if (!custom) return choice === answer;
|
|
1424
|
-
if (custom.passed !== void 0) return custom.passed;
|
|
1425
|
-
if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
1426
|
-
return meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
|
|
1427
|
-
}
|
|
1428
|
-
return choice === answer;
|
|
1429
|
-
},
|
|
1430
|
-
[]
|
|
1431
|
-
);
|
|
1432
|
-
return { getPluginScore, scoreResponse, isChoiceCorrect };
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
// src/components/Quiz.tsx
|
|
1436
|
-
import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1437
|
-
function QuizInner(props, ref) {
|
|
1438
|
-
const { enclosingLessonId } = props;
|
|
1439
|
-
const checkId = useMemo5(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1440
|
-
const quiz = useQuizState(enclosingLessonId);
|
|
1441
|
-
const { getPluginScore, isChoiceCorrect } = usePluginScoring(checkId, enclosingLessonId);
|
|
1442
|
-
const [selected, setSelected] = useState3(null);
|
|
1443
|
-
const [selectionCorrect, setSelectionCorrect] = useState3(null);
|
|
1444
|
-
const [quizPassed, setQuizPassed] = useState3(false);
|
|
1445
|
-
const [completedScore, setCompletedScore] = useState3(null);
|
|
1446
|
-
const [completedMaxScore, setCompletedMaxScore] = useState3(null);
|
|
1447
|
-
const completedRef = useRef4(false);
|
|
1448
|
-
const telemetryReplayedRef = useRef4(false);
|
|
1449
|
-
const questionId = useId();
|
|
1450
|
-
const choicesKey = props.choices.join("\0");
|
|
1451
|
-
useEffect3(() => {
|
|
1452
|
-
completedRef.current = false;
|
|
1453
|
-
telemetryReplayedRef.current = false;
|
|
1454
|
-
setQuizPassed(false);
|
|
1455
|
-
setSelected(null);
|
|
1456
|
-
setSelectionCorrect(null);
|
|
1457
|
-
setCompletedScore(null);
|
|
1458
|
-
setCompletedMaxScore(null);
|
|
1459
|
-
}, [checkId, props.answer, props.question, choicesKey]);
|
|
1460
|
-
const passed = quizPassed;
|
|
1461
|
-
const resolveScores = () => {
|
|
1462
|
-
const maxScore = completedMaxScore ?? 1;
|
|
1463
|
-
if (quizPassed) {
|
|
1464
|
-
return { score: completedScore ?? maxScore, maxScore };
|
|
1465
|
-
}
|
|
1466
|
-
if (selected !== null && selectionCorrect) {
|
|
1467
|
-
return { score: completedMaxScore ?? maxScore, maxScore };
|
|
1468
|
-
}
|
|
1469
|
-
return { score: 0, maxScore };
|
|
1470
|
-
};
|
|
1471
|
-
const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
|
|
1472
|
-
if (!nextPassed || telemetryReplayedRef.current) return;
|
|
1473
|
-
telemetryReplayedRef.current = true;
|
|
1474
|
-
if (nextSelected !== null) {
|
|
1475
|
-
quiz.answer({
|
|
1476
|
-
checkId,
|
|
1477
|
-
question: props.question,
|
|
1478
|
-
choice: nextSelected,
|
|
1479
|
-
correct: nextCorrect ?? false
|
|
1480
|
-
});
|
|
1481
|
-
}
|
|
1482
|
-
quiz.complete({
|
|
1483
|
-
checkId,
|
|
1484
|
-
score: nextScore,
|
|
1485
|
-
maxScore: nextMaxScore,
|
|
1486
|
-
passingScore: props.passingScore ?? nextMaxScore
|
|
1487
|
-
});
|
|
1488
|
-
};
|
|
1489
|
-
const handle = useMemo5(
|
|
1490
|
-
() => buildAssessmentHandle({
|
|
1491
|
-
checkId,
|
|
1492
|
-
getScore: () => resolveScores().score,
|
|
1493
|
-
getMaxScore: () => resolveScores().maxScore,
|
|
1494
|
-
getAnswerGiven: () => selected !== null,
|
|
1495
|
-
resetTask: () => {
|
|
1496
|
-
completedRef.current = false;
|
|
1497
|
-
telemetryReplayedRef.current = false;
|
|
1498
|
-
setQuizPassed(false);
|
|
1499
|
-
setSelected(null);
|
|
1500
|
-
setSelectionCorrect(null);
|
|
1501
|
-
setCompletedScore(null);
|
|
1502
|
-
setCompletedMaxScore(null);
|
|
1503
|
-
},
|
|
1504
|
-
showSolutions: () => {
|
|
1505
|
-
},
|
|
1506
|
-
getXAPIData: () => {
|
|
1507
|
-
const { score, maxScore } = resolveScores();
|
|
1508
|
-
return {
|
|
1509
|
-
checkId,
|
|
1510
|
-
interactionType: "mcq",
|
|
1511
|
-
response: selected ?? void 0,
|
|
1512
|
-
correct: selectionCorrect ?? void 0,
|
|
1513
|
-
score,
|
|
1514
|
-
maxScore
|
|
1515
|
-
};
|
|
1516
|
-
},
|
|
1517
|
-
getCurrentState: () => ({
|
|
1518
|
-
selected,
|
|
1519
|
-
selectionCorrect,
|
|
1520
|
-
quizPassed,
|
|
1521
|
-
completedScore,
|
|
1522
|
-
completedMaxScore
|
|
1523
|
-
}),
|
|
1524
|
-
resume: (state) => {
|
|
1525
|
-
const nextSelected = readStringField(state, "selected");
|
|
1526
|
-
if (typeof nextSelected === "string" || nextSelected === null) setSelected(nextSelected);
|
|
1527
|
-
const nextCorrect = readBooleanField(state, "selectionCorrect");
|
|
1528
|
-
if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
|
|
1529
|
-
setSelectionCorrect(nextCorrect);
|
|
1530
|
-
}
|
|
1531
|
-
const nextCompletedScore = readNumberField(state, "completedScore");
|
|
1532
|
-
if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
|
|
1533
|
-
const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
|
|
1534
|
-
if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
|
|
1535
|
-
const nextPassed = readBooleanField(state, "quizPassed");
|
|
1536
|
-
if (nextPassed === true || nextPassed === false) {
|
|
1537
|
-
setQuizPassed(nextPassed);
|
|
1538
|
-
completedRef.current = nextPassed;
|
|
1539
|
-
if (nextPassed) {
|
|
1540
|
-
const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
|
|
1541
|
-
const score = nextCompletedScore ?? completedScore ?? maxScore;
|
|
1542
|
-
replayTelemetry(
|
|
1543
|
-
nextSelected ?? null,
|
|
1544
|
-
nextCorrect ?? null,
|
|
1545
|
-
nextPassed,
|
|
1546
|
-
score,
|
|
1547
|
-
maxScore
|
|
1548
|
-
);
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
}),
|
|
1553
|
-
[
|
|
1554
|
-
checkId,
|
|
1555
|
-
completedMaxScore,
|
|
1556
|
-
completedScore,
|
|
1557
|
-
props.passingScore,
|
|
1558
|
-
props.question,
|
|
1559
|
-
quiz,
|
|
1560
|
-
quizPassed,
|
|
1561
|
-
selected,
|
|
1562
|
-
selectionCorrect
|
|
1563
|
-
]
|
|
1564
|
-
);
|
|
1565
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1566
|
-
return /* @__PURE__ */ jsxs2("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
1567
|
-
/* @__PURE__ */ jsx6("p", { id: questionId, children: props.question }),
|
|
1568
|
-
/* @__PURE__ */ jsxs2("fieldset", { "aria-labelledby": questionId, children: [
|
|
1569
|
-
/* @__PURE__ */ jsx6("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
|
|
1570
|
-
props.choices.map((c, i) => /* @__PURE__ */ jsxs2("label", { style: { display: "block" }, children: [
|
|
1571
|
-
/* @__PURE__ */ jsx6(
|
|
1572
|
-
"input",
|
|
1573
|
-
{
|
|
1574
|
-
type: "radio",
|
|
1575
|
-
name: questionId,
|
|
1576
|
-
value: c,
|
|
1577
|
-
checked: selected === c,
|
|
1578
|
-
disabled: passed,
|
|
1579
|
-
"aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
|
|
1580
|
-
onChange: () => {
|
|
1581
|
-
if (passed) return;
|
|
1582
|
-
setSelected(c);
|
|
1583
|
-
const custom = getPluginScore(c);
|
|
1584
|
-
const correct = isChoiceCorrect(c, props.answer, custom, props.passingScore);
|
|
1585
|
-
setSelectionCorrect(correct);
|
|
1586
|
-
quiz.answer({
|
|
1587
|
-
checkId,
|
|
1588
|
-
question: props.question,
|
|
1589
|
-
choice: c,
|
|
1590
|
-
correct
|
|
1591
|
-
});
|
|
1592
|
-
if (correct && !completedRef.current) {
|
|
1593
|
-
completedRef.current = true;
|
|
1594
|
-
setQuizPassed(true);
|
|
1595
|
-
const maxScore = custom?.maxScore ?? 1;
|
|
1596
|
-
const score = custom?.score ?? maxScore;
|
|
1597
|
-
setCompletedScore(score);
|
|
1598
|
-
setCompletedMaxScore(maxScore);
|
|
1599
|
-
quiz.complete({
|
|
1600
|
-
checkId,
|
|
1601
|
-
score,
|
|
1602
|
-
maxScore,
|
|
1603
|
-
passingScore: props.passingScore ?? maxScore
|
|
1604
|
-
});
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
),
|
|
1609
|
-
c
|
|
1610
|
-
] }, `${questionId}-${i}`))
|
|
1611
|
-
] }),
|
|
1612
|
-
selected && selectionCorrect !== null ? /* @__PURE__ */ jsx6("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
1613
|
-
] });
|
|
1614
|
-
}
|
|
1615
|
-
var QuizInnerForwarded = forwardRef(QuizInner);
|
|
1616
|
-
var Quiz = forwardRef(function Quiz2(props, ref) {
|
|
1617
|
-
return /* @__PURE__ */ jsx6(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx6(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1618
|
-
});
|
|
1619
|
-
function KnowledgeCheck(props) {
|
|
1620
|
-
return /* @__PURE__ */ jsx6(
|
|
1621
|
-
Quiz,
|
|
1622
|
-
{
|
|
1623
|
-
checkId: props.checkId,
|
|
1624
|
-
question: props.question,
|
|
1625
|
-
choices: props.choices,
|
|
1626
|
-
answer: props.answer,
|
|
1627
|
-
passingScore: props.passingScore
|
|
1628
|
-
}
|
|
1629
|
-
);
|
|
1630
|
-
}
|
|
1631
|
-
function resetQuizWarningsForTests() {
|
|
1632
|
-
resetAssessmentWarningsForTests();
|
|
1633
|
-
}
|
|
43
|
+
LessonContext,
|
|
44
|
+
LessonkitProvider,
|
|
45
|
+
assertProductionCourseConfig,
|
|
46
|
+
normalizeComponentId,
|
|
47
|
+
resetAssessmentWarningsForTests,
|
|
48
|
+
shouldEnforceProductionGuard,
|
|
49
|
+
useAssessmentState,
|
|
50
|
+
useCompletion,
|
|
51
|
+
useLessonkit,
|
|
52
|
+
useProgress,
|
|
53
|
+
useQuizState,
|
|
54
|
+
useTracking
|
|
55
|
+
} from "./chunk-TDM3ARE7.js";
|
|
1634
56
|
|
|
1635
57
|
// src/components.tsx
|
|
1636
|
-
import {
|
|
58
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
59
|
+
import { visuallyHiddenStyle } from "@lessonkit/accessibility";
|
|
60
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1637
61
|
function Course(props) {
|
|
1638
|
-
const courseId =
|
|
1639
|
-
const providerConfig =
|
|
62
|
+
const courseId = useMemo(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
|
|
63
|
+
const providerConfig = useMemo(
|
|
1640
64
|
() => ({ ...props.config, courseId }),
|
|
1641
|
-
[props.config, courseId]
|
|
1642
|
-
);
|
|
1643
|
-
return /* @__PURE__ */ jsx7(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs3("section", { "aria-label": props.title, children: [
|
|
1644
|
-
/* @__PURE__ */ jsx7("h1", { children: props.title }),
|
|
1645
|
-
/* @__PURE__ */ jsx7("div", { children: props.children })
|
|
1646
|
-
] }) });
|
|
1647
|
-
}
|
|
1648
|
-
function Lesson(props) {
|
|
1649
|
-
const lessonId = useMemo6(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
1650
|
-
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
1651
|
-
const { setActiveLesson, config } = useLessonkit();
|
|
1652
|
-
const { completeLesson } = useCompletion();
|
|
1653
|
-
const lessonMountGenerationRef = useRef5(0);
|
|
1654
|
-
const liveCourseIdRef = useRef5(config.courseId);
|
|
1655
|
-
liveCourseIdRef.current = config.courseId;
|
|
1656
|
-
useEffect4(() => {
|
|
1657
|
-
const unregister = registerLessonMount(lessonId);
|
|
1658
|
-
const generation = ++lessonMountGenerationRef.current;
|
|
1659
|
-
const mountedCourseId = config.courseId;
|
|
1660
|
-
let effectSurvivedTick = false;
|
|
1661
|
-
queueMicrotask(() => {
|
|
1662
|
-
queueMicrotask(() => {
|
|
1663
|
-
effectSurvivedTick = true;
|
|
1664
|
-
});
|
|
1665
|
-
});
|
|
1666
|
-
setActiveLesson(lessonId);
|
|
1667
|
-
return () => {
|
|
1668
|
-
unregister();
|
|
1669
|
-
if (getLessonMountCount(lessonId) > 0) {
|
|
1670
|
-
return;
|
|
1671
|
-
}
|
|
1672
|
-
if (!autoComplete) return;
|
|
1673
|
-
queueMicrotask(() => {
|
|
1674
|
-
if (!effectSurvivedTick) return;
|
|
1675
|
-
if (lessonMountGenerationRef.current !== generation) return;
|
|
1676
|
-
if (liveCourseIdRef.current !== mountedCourseId) return;
|
|
1677
|
-
completeLesson(lessonId, { courseId: mountedCourseId });
|
|
1678
|
-
});
|
|
1679
|
-
};
|
|
1680
|
-
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
1681
|
-
return /* @__PURE__ */ jsx7(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs3("article", { "aria-label": props.title, children: [
|
|
1682
|
-
/* @__PURE__ */ jsx7("h2", { children: props.title }),
|
|
1683
|
-
/* @__PURE__ */ jsx7("div", { children: props.children })
|
|
1684
|
-
] }) });
|
|
1685
|
-
}
|
|
1686
|
-
function Scenario(props) {
|
|
1687
|
-
const blockId = useMemo6(
|
|
1688
|
-
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1689
|
-
[props.blockId]
|
|
1690
|
-
);
|
|
1691
|
-
return /* @__PURE__ */ jsx7("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
1692
|
-
}
|
|
1693
|
-
function Reflection(props) {
|
|
1694
|
-
const blockId = useMemo6(
|
|
1695
|
-
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1696
|
-
[props.blockId]
|
|
1697
|
-
);
|
|
1698
|
-
const promptId = useId2();
|
|
1699
|
-
const hintId = useId2();
|
|
1700
|
-
const [internalValue, setInternalValue] = useState4("");
|
|
1701
|
-
const isControlled = props.value !== void 0;
|
|
1702
|
-
const value = isControlled ? props.value : internalValue;
|
|
1703
|
-
const handleChange = (event) => {
|
|
1704
|
-
if (!isControlled) setInternalValue(event.target.value);
|
|
1705
|
-
props.onChange?.(event.target.value);
|
|
1706
|
-
};
|
|
1707
|
-
return /* @__PURE__ */ jsxs3("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
|
|
1708
|
-
props.prompt ? /* @__PURE__ */ jsx7("p", { id: promptId, children: props.prompt }) : null,
|
|
1709
|
-
props.hint ? /* @__PURE__ */ jsx7("p", { id: hintId, style: visuallyHiddenStyle2, children: props.hint }) : null,
|
|
1710
|
-
props.children,
|
|
1711
|
-
/* @__PURE__ */ jsx7(
|
|
1712
|
-
"textarea",
|
|
1713
|
-
{
|
|
1714
|
-
value,
|
|
1715
|
-
onChange: handleChange,
|
|
1716
|
-
"aria-labelledby": props.prompt ? promptId : void 0,
|
|
1717
|
-
"aria-describedby": props.hint ? hintId : void 0,
|
|
1718
|
-
"aria-label": props.prompt ? void 0 : "Reflection response"
|
|
1719
|
-
}
|
|
1720
|
-
)
|
|
1721
|
-
] });
|
|
1722
|
-
}
|
|
1723
|
-
function ProgressTracker(props) {
|
|
1724
|
-
const { progress } = useLessonkit();
|
|
1725
|
-
const completed = progress.completedLessonIds.size;
|
|
1726
|
-
if (props.totalLessons != null) {
|
|
1727
|
-
const total = props.totalLessons;
|
|
1728
|
-
const displayed = Math.min(completed, total);
|
|
1729
|
-
return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx7(
|
|
1730
|
-
"div",
|
|
1731
|
-
{
|
|
1732
|
-
role: "progressbar",
|
|
1733
|
-
"aria-valuemin": 0,
|
|
1734
|
-
"aria-valuemax": total,
|
|
1735
|
-
"aria-valuenow": displayed,
|
|
1736
|
-
"aria-label": "Lessons completed",
|
|
1737
|
-
children: /* @__PURE__ */ jsxs3("p", { children: [
|
|
1738
|
-
"Lessons completed: ",
|
|
1739
|
-
displayed,
|
|
1740
|
-
" of ",
|
|
1741
|
-
total
|
|
1742
|
-
] })
|
|
1743
|
-
}
|
|
1744
|
-
) });
|
|
1745
|
-
}
|
|
1746
|
-
return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs3("p", { children: [
|
|
1747
|
-
"Lessons completed: ",
|
|
1748
|
-
completed
|
|
1749
|
-
] }) });
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
// src/blocks/TrueFalse.tsx
|
|
1753
|
-
import React9, { forwardRef as forwardRef2, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef6, useState as useState5 } from "react";
|
|
1754
|
-
import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1755
|
-
var INTERACTION = "trueFalse";
|
|
1756
|
-
function TrueFalseInner(props, ref) {
|
|
1757
|
-
const { enclosingLessonId } = props;
|
|
1758
|
-
const checkId = useMemo7(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1759
|
-
const assessment = useAssessmentState(enclosingLessonId);
|
|
1760
|
-
const { config } = useLessonkit();
|
|
1761
|
-
const { scoreResponse } = usePluginScoring(checkId, enclosingLessonId);
|
|
1762
|
-
const [selected, setSelected] = useState5(null);
|
|
1763
|
-
const [selectionCorrect, setSelectionCorrect] = useState5(null);
|
|
1764
|
-
const [showSolutions, setShowSolutions] = useState5(false);
|
|
1765
|
-
const [passed, setPassed] = useState5(false);
|
|
1766
|
-
const [completedScore, setCompletedScore] = useState5(null);
|
|
1767
|
-
const [completedMaxScore, setCompletedMaxScore] = useState5(null);
|
|
1768
|
-
const completedRef = useRef6(false);
|
|
1769
|
-
const telemetryReplayedRef = useRef6(false);
|
|
1770
|
-
const questionId = React9.useId();
|
|
1771
|
-
const reset = () => {
|
|
1772
|
-
completedRef.current = false;
|
|
1773
|
-
telemetryReplayedRef.current = false;
|
|
1774
|
-
setPassed(false);
|
|
1775
|
-
setSelected(null);
|
|
1776
|
-
setSelectionCorrect(null);
|
|
1777
|
-
setShowSolutions(false);
|
|
1778
|
-
setCompletedScore(null);
|
|
1779
|
-
setCompletedMaxScore(null);
|
|
1780
|
-
};
|
|
1781
|
-
useEffect5(() => {
|
|
1782
|
-
reset();
|
|
1783
|
-
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
|
|
1784
|
-
const resolveScores = () => {
|
|
1785
|
-
const maxScore = completedMaxScore ?? 1;
|
|
1786
|
-
if (passed) {
|
|
1787
|
-
return { score: completedScore ?? maxScore, maxScore };
|
|
1788
|
-
}
|
|
1789
|
-
if (selectionCorrect) {
|
|
1790
|
-
return { score: completedMaxScore ?? maxScore, maxScore };
|
|
1791
|
-
}
|
|
1792
|
-
return { score: 0, maxScore };
|
|
1793
|
-
};
|
|
1794
|
-
const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
|
|
1795
|
-
if (!nextPassed || telemetryReplayedRef.current) return;
|
|
1796
|
-
telemetryReplayedRef.current = true;
|
|
1797
|
-
if (nextSelected !== null) {
|
|
1798
|
-
assessment.answer({
|
|
1799
|
-
checkId,
|
|
1800
|
-
interactionType: INTERACTION,
|
|
1801
|
-
question: props.question,
|
|
1802
|
-
response: nextSelected,
|
|
1803
|
-
correct: nextCorrect ?? false
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
assessment.complete({
|
|
1807
|
-
checkId,
|
|
1808
|
-
interactionType: INTERACTION,
|
|
1809
|
-
score: nextScore,
|
|
1810
|
-
maxScore: nextMaxScore,
|
|
1811
|
-
passingScore: props.passingScore ?? nextMaxScore
|
|
1812
|
-
});
|
|
1813
|
-
};
|
|
1814
|
-
const handle = useMemo7(
|
|
1815
|
-
() => buildAssessmentHandle({
|
|
1816
|
-
checkId,
|
|
1817
|
-
getScore: () => resolveScores().score,
|
|
1818
|
-
getMaxScore: () => resolveScores().maxScore,
|
|
1819
|
-
getAnswerGiven: () => selected !== null,
|
|
1820
|
-
resetTask: reset,
|
|
1821
|
-
showSolutions: () => setShowSolutions(true),
|
|
1822
|
-
getXAPIData: () => {
|
|
1823
|
-
const { score, maxScore } = resolveScores();
|
|
1824
|
-
return {
|
|
1825
|
-
checkId,
|
|
1826
|
-
interactionType: INTERACTION,
|
|
1827
|
-
response: selected ?? void 0,
|
|
1828
|
-
correct: selectionCorrect ?? void 0,
|
|
1829
|
-
score,
|
|
1830
|
-
maxScore
|
|
1831
|
-
};
|
|
1832
|
-
},
|
|
1833
|
-
getCurrentState: () => ({
|
|
1834
|
-
selected,
|
|
1835
|
-
selectionCorrect,
|
|
1836
|
-
passed,
|
|
1837
|
-
showSolutions,
|
|
1838
|
-
completedScore,
|
|
1839
|
-
completedMaxScore
|
|
1840
|
-
}),
|
|
1841
|
-
resume: (state) => {
|
|
1842
|
-
const nextSelected = readBooleanField(state, "selected");
|
|
1843
|
-
if (nextSelected === true || nextSelected === false || nextSelected === null) {
|
|
1844
|
-
setSelected(nextSelected);
|
|
1845
|
-
}
|
|
1846
|
-
const nextCorrect = readBooleanField(state, "selectionCorrect");
|
|
1847
|
-
if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
|
|
1848
|
-
setSelectionCorrect(nextCorrect);
|
|
1849
|
-
}
|
|
1850
|
-
const nextCompletedScore = readNumberField(state, "completedScore");
|
|
1851
|
-
if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
|
|
1852
|
-
const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
|
|
1853
|
-
if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
|
|
1854
|
-
const nextPassed = readBooleanField(state, "passed");
|
|
1855
|
-
if (nextPassed === true || nextPassed === false) {
|
|
1856
|
-
setPassed(nextPassed);
|
|
1857
|
-
completedRef.current = nextPassed;
|
|
1858
|
-
if (nextPassed) {
|
|
1859
|
-
const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
|
|
1860
|
-
const score = nextCompletedScore ?? completedScore ?? maxScore;
|
|
1861
|
-
replayTelemetry(nextSelected ?? null, nextCorrect ?? null, nextPassed, score, maxScore);
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
1865
|
-
}
|
|
1866
|
-
}),
|
|
1867
|
-
[
|
|
1868
|
-
assessment,
|
|
1869
|
-
checkId,
|
|
1870
|
-
completedMaxScore,
|
|
1871
|
-
completedScore,
|
|
1872
|
-
passed,
|
|
1873
|
-
props.passingScore,
|
|
1874
|
-
props.question,
|
|
1875
|
-
selected,
|
|
1876
|
-
selectionCorrect,
|
|
1877
|
-
showSolutions
|
|
1878
|
-
]
|
|
1879
|
-
);
|
|
1880
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1881
|
-
const submit = (value) => {
|
|
1882
|
-
if (passed && !props.enableRetry) return;
|
|
1883
|
-
setSelected(value);
|
|
1884
|
-
const correct = value === props.answer;
|
|
1885
|
-
const scored = scoreResponse(value, correct, 1, props.passingScore);
|
|
1886
|
-
setSelectionCorrect(scored.passed);
|
|
1887
|
-
assessment.answer({
|
|
1888
|
-
checkId,
|
|
1889
|
-
interactionType: INTERACTION,
|
|
1890
|
-
question: props.question,
|
|
1891
|
-
response: value,
|
|
1892
|
-
correct: scored.passed
|
|
1893
|
-
});
|
|
1894
|
-
if (scored.passed && !completedRef.current) {
|
|
1895
|
-
completedRef.current = true;
|
|
1896
|
-
setPassed(true);
|
|
1897
|
-
setCompletedScore(scored.score);
|
|
1898
|
-
setCompletedMaxScore(scored.maxScore);
|
|
1899
|
-
assessment.complete({
|
|
1900
|
-
checkId,
|
|
1901
|
-
interactionType: INTERACTION,
|
|
1902
|
-
score: scored.score,
|
|
1903
|
-
maxScore: scored.maxScore,
|
|
1904
|
-
passingScore: props.passingScore ?? scored.maxScore
|
|
1905
|
-
});
|
|
1906
|
-
}
|
|
1907
|
-
};
|
|
1908
|
-
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
1909
|
-
return /* @__PURE__ */ jsxs4("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
|
|
1910
|
-
/* @__PURE__ */ jsx8("p", { id: questionId, children: props.question }),
|
|
1911
|
-
/* @__PURE__ */ jsxs4("fieldset", { "aria-labelledby": questionId, children: [
|
|
1912
|
-
/* @__PURE__ */ jsx8("legend", { className: "lk-visually-hidden", children: "True or False" }),
|
|
1913
|
-
/* @__PURE__ */ jsxs4("label", { style: { display: "block", marginRight: "1rem" }, children: [
|
|
1914
|
-
/* @__PURE__ */ jsx8(
|
|
1915
|
-
"input",
|
|
1916
|
-
{
|
|
1917
|
-
type: "radio",
|
|
1918
|
-
name: `${questionId}-tf`,
|
|
1919
|
-
checked: selected === true,
|
|
1920
|
-
disabled: passed && !props.enableRetry,
|
|
1921
|
-
onChange: () => submit(true)
|
|
1922
|
-
}
|
|
1923
|
-
),
|
|
1924
|
-
"True"
|
|
1925
|
-
] }),
|
|
1926
|
-
/* @__PURE__ */ jsxs4("label", { style: { display: "block" }, children: [
|
|
1927
|
-
/* @__PURE__ */ jsx8(
|
|
1928
|
-
"input",
|
|
1929
|
-
{
|
|
1930
|
-
type: "radio",
|
|
1931
|
-
name: `${questionId}-tf`,
|
|
1932
|
-
checked: selected === false,
|
|
1933
|
-
disabled: passed && !props.enableRetry,
|
|
1934
|
-
onChange: () => submit(false)
|
|
1935
|
-
}
|
|
1936
|
-
),
|
|
1937
|
-
"False"
|
|
1938
|
-
] })
|
|
1939
|
-
] }),
|
|
1940
|
-
reveal ? /* @__PURE__ */ jsxs4("p", { children: [
|
|
1941
|
-
"Correct answer: ",
|
|
1942
|
-
/* @__PURE__ */ jsx8("strong", { children: props.answer ? "True" : "False" })
|
|
1943
|
-
] }) : null,
|
|
1944
|
-
selected !== null && selectionCorrect !== null ? /* @__PURE__ */ jsx8("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
|
|
1945
|
-
props.enableRetry && passed ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
1946
|
-
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1947
|
-
] });
|
|
1948
|
-
}
|
|
1949
|
-
var TrueFalseInnerForwarded = forwardRef2(TrueFalseInner);
|
|
1950
|
-
var TrueFalse = forwardRef2(function TrueFalse2(props, ref) {
|
|
1951
|
-
return /* @__PURE__ */ jsx8(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx8(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1952
|
-
});
|
|
1953
|
-
|
|
1954
|
-
// src/blocks/MarkTheWords.tsx
|
|
1955
|
-
import React10, { forwardRef as forwardRef3, useEffect as useEffect6, useMemo as useMemo8, useRef as useRef7, useState as useState6 } from "react";
|
|
1956
|
-
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1957
|
-
var INTERACTION2 = "markTheWords";
|
|
1958
|
-
function tokenize(text) {
|
|
1959
|
-
return text.split(/(\s+)/).filter((t) => t.length > 0);
|
|
1960
|
-
}
|
|
1961
|
-
function MarkTheWordsInner(props, ref) {
|
|
1962
|
-
const checkId = useMemo8(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1963
|
-
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1964
|
-
const tokens = useMemo8(() => tokenize(props.text), [props.text]);
|
|
1965
|
-
const correctSet = useMemo8(
|
|
1966
|
-
() => new Set(props.correctWords.map((w) => w.toLowerCase())),
|
|
1967
|
-
[props.correctWords]
|
|
1968
|
-
);
|
|
1969
|
-
const [marked, setMarked] = useState6(() => /* @__PURE__ */ new Set());
|
|
1970
|
-
const [passed, setPassed] = useState6(false);
|
|
1971
|
-
const [showSolutions, setShowSolutions] = useState6(false);
|
|
1972
|
-
const completedRef = useRef7(false);
|
|
1973
|
-
const reset = () => {
|
|
1974
|
-
completedRef.current = false;
|
|
1975
|
-
setPassed(false);
|
|
1976
|
-
setMarked(/* @__PURE__ */ new Set());
|
|
1977
|
-
setShowSolutions(false);
|
|
1978
|
-
};
|
|
1979
|
-
useEffect6(() => {
|
|
1980
|
-
reset();
|
|
1981
|
-
}, [checkId, props.text, props.correctWords.join("\0")]);
|
|
1982
|
-
const selectableIndices = useMemo8(() => {
|
|
1983
|
-
const indices = [];
|
|
1984
|
-
tokens.forEach((t, i) => {
|
|
1985
|
-
if (!/^\s+$/.test(t) && correctSet.has(t.toLowerCase())) indices.push(i);
|
|
1986
|
-
});
|
|
1987
|
-
return indices;
|
|
1988
|
-
}, [tokens, correctSet]);
|
|
1989
|
-
const hasTargets = selectableIndices.length > 0;
|
|
1990
|
-
const allMarked = hasTargets && selectableIndices.every((i) => marked.has(i));
|
|
1991
|
-
const maxScore = selectableIndices.length;
|
|
1992
|
-
const score = allMarked ? maxScore : marked.size;
|
|
1993
|
-
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
1994
|
-
const handle = useMemo8(
|
|
1995
|
-
() => buildAssessmentHandle({
|
|
1996
|
-
checkId,
|
|
1997
|
-
getScore: () => score,
|
|
1998
|
-
getMaxScore: () => maxScore || 1,
|
|
1999
|
-
getAnswerGiven: () => marked.size > 0,
|
|
2000
|
-
resetTask: reset,
|
|
2001
|
-
showSolutions: () => setShowSolutions(true),
|
|
2002
|
-
getXAPIData: () => ({
|
|
2003
|
-
checkId,
|
|
2004
|
-
interactionType: INTERACTION2,
|
|
2005
|
-
response: [...marked].map((i) => tokens[i]),
|
|
2006
|
-
correct: passedThreshold,
|
|
2007
|
-
score,
|
|
2008
|
-
maxScore: maxScore || 1
|
|
2009
|
-
}),
|
|
2010
|
-
getCurrentState: () => ({ marked: [...marked], passed, showSolutions }),
|
|
2011
|
-
resume: (state) => {
|
|
2012
|
-
const raw = state.marked;
|
|
2013
|
-
if (Array.isArray(raw)) setMarked(new Set(raw.filter((i) => typeof i === "number")));
|
|
2014
|
-
readBooleanStateField(state, "passed", (value) => {
|
|
2015
|
-
setPassed(value);
|
|
2016
|
-
completedRef.current = value;
|
|
2017
|
-
});
|
|
2018
|
-
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
2019
|
-
}
|
|
2020
|
-
}),
|
|
2021
|
-
[checkId, marked, maxScore, passed, passedThreshold, score, showSolutions, tokens]
|
|
2022
|
-
);
|
|
2023
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2024
|
-
const toggle = (index) => {
|
|
2025
|
-
if (passed && !props.enableRetry) return;
|
|
2026
|
-
setMarked((prev) => {
|
|
2027
|
-
const next = new Set(prev);
|
|
2028
|
-
if (next.has(index)) next.delete(index);
|
|
2029
|
-
else next.add(index);
|
|
2030
|
-
return next;
|
|
2031
|
-
});
|
|
2032
|
-
};
|
|
2033
|
-
useEffect6(() => {
|
|
2034
|
-
if (!hasTargets) {
|
|
2035
|
-
if (isDevEnvironment4()) {
|
|
2036
|
-
console.warn(
|
|
2037
|
-
"[lessonkit] MarkTheWords: no tokens match correctWords",
|
|
2038
|
-
props.correctWords
|
|
2039
|
-
);
|
|
2040
|
-
}
|
|
2041
|
-
return;
|
|
2042
|
-
}
|
|
2043
|
-
if (!passedThreshold || completedRef.current) return;
|
|
2044
|
-
completedRef.current = true;
|
|
2045
|
-
setPassed(true);
|
|
2046
|
-
assessment.answer({
|
|
2047
|
-
checkId,
|
|
2048
|
-
interactionType: INTERACTION2,
|
|
2049
|
-
question: props.text,
|
|
2050
|
-
response: [...marked].map((i) => tokens[i]),
|
|
2051
|
-
correct: passedThreshold
|
|
2052
|
-
});
|
|
2053
|
-
assessment.complete({
|
|
2054
|
-
checkId,
|
|
2055
|
-
interactionType: INTERACTION2,
|
|
2056
|
-
score,
|
|
2057
|
-
maxScore,
|
|
2058
|
-
passingScore: props.passingScore ?? maxScore
|
|
2059
|
-
});
|
|
2060
|
-
}, [
|
|
2061
|
-
assessment,
|
|
2062
|
-
checkId,
|
|
2063
|
-
hasTargets,
|
|
2064
|
-
marked,
|
|
2065
|
-
maxScore,
|
|
2066
|
-
passedThreshold,
|
|
2067
|
-
props.passingScore,
|
|
2068
|
-
props.correctWords,
|
|
2069
|
-
props.text,
|
|
2070
|
-
score,
|
|
2071
|
-
tokens
|
|
2072
|
-
]);
|
|
2073
|
-
return /* @__PURE__ */ jsxs5("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
|
|
2074
|
-
!hasTargets ? /* @__PURE__ */ jsxs5("p", { role: "alert", children: [
|
|
2075
|
-
"No words in this sentence match ",
|
|
2076
|
-
/* @__PURE__ */ jsx9("code", { children: "correctWords" }),
|
|
2077
|
-
". Check spelling and capitalization in the source text."
|
|
2078
|
-
] }) : null,
|
|
2079
|
-
/* @__PURE__ */ jsx9("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
|
|
2080
|
-
/* @__PURE__ */ jsx9("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
|
|
2081
|
-
const isWord = !/^\s+$/.test(token);
|
|
2082
|
-
const isTarget = isWord && correctSet.has(token.toLowerCase());
|
|
2083
|
-
if (!isTarget) return /* @__PURE__ */ jsx9(React10.Fragment, { children: token }, i);
|
|
2084
|
-
const selected = marked.has(i);
|
|
2085
|
-
const solution = showSolutions || passed && props.enableSolutionsButton;
|
|
2086
|
-
return /* @__PURE__ */ jsx9(
|
|
2087
|
-
"button",
|
|
2088
|
-
{
|
|
2089
|
-
type: "button",
|
|
2090
|
-
"data-testid": `mark-word-${i}`,
|
|
2091
|
-
"aria-pressed": selected,
|
|
2092
|
-
disabled: passed && !props.enableRetry,
|
|
2093
|
-
onClick: () => toggle(i),
|
|
2094
|
-
style: {
|
|
2095
|
-
margin: "0 0.1em",
|
|
2096
|
-
textDecoration: solution ? "underline" : void 0,
|
|
2097
|
-
fontWeight: selected || solution ? "bold" : void 0
|
|
2098
|
-
},
|
|
2099
|
-
children: token
|
|
2100
|
-
},
|
|
2101
|
-
i
|
|
2102
|
-
);
|
|
2103
|
-
}) }),
|
|
2104
|
-
allMarked ? /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
|
|
2105
|
-
props.enableRetry && passed ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2106
|
-
props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
2107
|
-
] });
|
|
2108
|
-
}
|
|
2109
|
-
var MarkTheWordsInnerForwarded = forwardRef3(MarkTheWordsInner);
|
|
2110
|
-
var MarkTheWords = forwardRef3(function MarkTheWords2(props, ref) {
|
|
2111
|
-
return /* @__PURE__ */ jsx9(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx9(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2112
|
-
});
|
|
2113
|
-
|
|
2114
|
-
// src/blocks/FillInTheBlanks.tsx
|
|
2115
|
-
import React11, { forwardRef as forwardRef4, useEffect as useEffect7, useMemo as useMemo9, useRef as useRef8, useState as useState7 } from "react";
|
|
2116
|
-
|
|
2117
|
-
// src/assessment/internal/parseStarDelimitedTemplate.ts
|
|
2118
|
-
function parseStarDelimitedTemplate(template, idPrefix) {
|
|
2119
|
-
const parts = [];
|
|
2120
|
-
const values = [];
|
|
2121
|
-
const re = /\*([^*]+)\*/g;
|
|
2122
|
-
let last = 0;
|
|
2123
|
-
let match;
|
|
2124
|
-
let n = 0;
|
|
2125
|
-
while ((match = re.exec(template)) !== null) {
|
|
2126
|
-
parts.push(template.slice(last, match.index));
|
|
2127
|
-
values.push(match[1].trim());
|
|
2128
|
-
parts.push(`${idPrefix}-${n++}`);
|
|
2129
|
-
last = match.index + match[0].length;
|
|
2130
|
-
}
|
|
2131
|
-
parts.push(template.slice(last));
|
|
2132
|
-
return { parts, values };
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
// src/blocks/FillInTheBlanks.tsx
|
|
2136
|
-
import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2137
|
-
var INTERACTION3 = "fillInBlanks";
|
|
2138
|
-
function parseTemplate(template) {
|
|
2139
|
-
const { parts, values } = parseStarDelimitedTemplate(template, "blank");
|
|
2140
|
-
return {
|
|
2141
|
-
parts,
|
|
2142
|
-
blanks: values.map((answer, i) => ({ id: `blank-${i}`, answer }))
|
|
2143
|
-
};
|
|
2144
|
-
}
|
|
2145
|
-
function FillInTheBlanksInner(props, ref) {
|
|
2146
|
-
const checkId = useMemo9(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
2147
|
-
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2148
|
-
const parsed = useMemo9(() => parseTemplate(props.template), [props.template]);
|
|
2149
|
-
const blanks = props.blanks ?? parsed.blanks;
|
|
2150
|
-
const [values, setValues] = useState7(
|
|
2151
|
-
() => Object.fromEntries(blanks.map((b) => [b.id, ""]))
|
|
2152
|
-
);
|
|
2153
|
-
const [passed, setPassed] = useState7(false);
|
|
2154
|
-
const [showSolutions, setShowSolutions] = useState7(false);
|
|
2155
|
-
const [submitted, setSubmitted] = useState7(false);
|
|
2156
|
-
const completedRef = useRef8(false);
|
|
2157
|
-
const answeredRef = useRef8(false);
|
|
2158
|
-
const reset = () => {
|
|
2159
|
-
completedRef.current = false;
|
|
2160
|
-
answeredRef.current = false;
|
|
2161
|
-
setPassed(false);
|
|
2162
|
-
setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
|
|
2163
|
-
setShowSolutions(false);
|
|
2164
|
-
setSubmitted(false);
|
|
2165
|
-
};
|
|
2166
|
-
useEffect7(() => {
|
|
2167
|
-
reset();
|
|
2168
|
-
}, [checkId, props.template, blanks.map((b) => b.answer).join("\0")]);
|
|
2169
|
-
const hasBlanks = blanks.length > 0;
|
|
2170
|
-
const allFilled = hasBlanks && blanks.every((b) => (values[b.id] ?? "").trim().length > 0);
|
|
2171
|
-
let score = 0;
|
|
2172
|
-
blanks.forEach((b) => {
|
|
2173
|
-
if ((values[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) score += 1;
|
|
2174
|
-
});
|
|
2175
|
-
const maxScore = blanks.length;
|
|
2176
|
-
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
2177
|
-
const handle = useMemo9(
|
|
2178
|
-
() => buildAssessmentHandle({
|
|
2179
|
-
checkId,
|
|
2180
|
-
getScore: () => score,
|
|
2181
|
-
getMaxScore: () => maxScore || 1,
|
|
2182
|
-
getAnswerGiven: () => allFilled,
|
|
2183
|
-
resetTask: reset,
|
|
2184
|
-
showSolutions: () => setShowSolutions(true),
|
|
2185
|
-
getXAPIData: () => ({
|
|
2186
|
-
checkId,
|
|
2187
|
-
interactionType: INTERACTION3,
|
|
2188
|
-
response: values,
|
|
2189
|
-
correct: passedThreshold,
|
|
2190
|
-
score,
|
|
2191
|
-
maxScore: maxScore || 1
|
|
2192
|
-
}),
|
|
2193
|
-
getCurrentState: () => ({ values, passed, showSolutions, submitted }),
|
|
2194
|
-
resume: (state) => {
|
|
2195
|
-
const raw = state.values;
|
|
2196
|
-
if (raw && typeof raw === "object") setValues({ ...raw });
|
|
2197
|
-
readBooleanStateField(state, "passed", (value) => {
|
|
2198
|
-
setPassed(value);
|
|
2199
|
-
completedRef.current = value;
|
|
2200
|
-
answeredRef.current = value;
|
|
2201
|
-
});
|
|
2202
|
-
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
2203
|
-
readBooleanStateField(state, "submitted", (value) => {
|
|
2204
|
-
setSubmitted(value);
|
|
2205
|
-
if (value) answeredRef.current = true;
|
|
2206
|
-
});
|
|
2207
|
-
}
|
|
2208
|
-
}),
|
|
2209
|
-
[allFilled, checkId, maxScore, passed, passedThreshold, score, showSolutions, submitted, values]
|
|
2210
|
-
);
|
|
2211
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2212
|
-
const check = () => {
|
|
2213
|
-
if (!hasBlanks) {
|
|
2214
|
-
if (isDevEnvironment4()) {
|
|
2215
|
-
console.warn("[lessonkit] FillInTheBlanks has no blanks in template");
|
|
2216
|
-
}
|
|
2217
|
-
return;
|
|
2218
|
-
}
|
|
2219
|
-
if (!allFilled) return;
|
|
2220
|
-
if (answeredRef.current || submitted) return;
|
|
2221
|
-
answeredRef.current = true;
|
|
2222
|
-
setSubmitted(true);
|
|
2223
|
-
assessment.answer({
|
|
2224
|
-
checkId,
|
|
2225
|
-
interactionType: INTERACTION3,
|
|
2226
|
-
question: props.template,
|
|
2227
|
-
response: values,
|
|
2228
|
-
correct: passedThreshold
|
|
2229
|
-
});
|
|
2230
|
-
if (passedThreshold && !completedRef.current) {
|
|
2231
|
-
completedRef.current = true;
|
|
2232
|
-
setPassed(true);
|
|
2233
|
-
assessment.complete({
|
|
2234
|
-
checkId,
|
|
2235
|
-
interactionType: INTERACTION3,
|
|
2236
|
-
score,
|
|
2237
|
-
maxScore,
|
|
2238
|
-
passingScore: props.passingScore ?? maxScore
|
|
2239
|
-
});
|
|
2240
|
-
}
|
|
2241
|
-
};
|
|
2242
|
-
useEffect7(() => {
|
|
2243
|
-
if (!allFilled) {
|
|
2244
|
-
answeredRef.current = false;
|
|
2245
|
-
setSubmitted(false);
|
|
2246
|
-
}
|
|
2247
|
-
}, [allFilled]);
|
|
2248
|
-
useEffect7(() => {
|
|
2249
|
-
if (props.autoCheck && allFilled) check();
|
|
2250
|
-
}, [allFilled, props.autoCheck, values, passedThreshold]);
|
|
2251
|
-
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
2252
|
-
return /* @__PURE__ */ jsxs6("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
|
|
2253
|
-
/* @__PURE__ */ jsx10("p", { children: parsed.parts.map((part, i) => {
|
|
2254
|
-
const blank = blanks.find((b) => b.id === part);
|
|
2255
|
-
if (!blank) return /* @__PURE__ */ jsx10(React11.Fragment, { children: part }, i);
|
|
2256
|
-
return /* @__PURE__ */ jsxs6("label", { style: { margin: "0 0.25em" }, children: [
|
|
2257
|
-
/* @__PURE__ */ jsx10("span", { className: "lk-visually-hidden", children: blank.answer }),
|
|
2258
|
-
/* @__PURE__ */ jsx10(
|
|
2259
|
-
"input",
|
|
2260
|
-
{
|
|
2261
|
-
type: "text",
|
|
2262
|
-
"data-testid": `blank-${blank.id}`,
|
|
2263
|
-
"aria-label": `Blank ${blank.id}`,
|
|
2264
|
-
value: reveal ? blank.answer : values[blank.id] ?? "",
|
|
2265
|
-
readOnly: reveal,
|
|
2266
|
-
disabled: passed && !props.enableRetry,
|
|
2267
|
-
onChange: (e) => setValues((v) => ({ ...v, [blank.id]: e.target.value })),
|
|
2268
|
-
onBlur: () => props.autoCheck && check(),
|
|
2269
|
-
size: Math.max(8, blank.answer.length + 2)
|
|
2270
|
-
}
|
|
2271
|
-
)
|
|
2272
|
-
] }, blank.id);
|
|
2273
|
-
}) }),
|
|
2274
|
-
!props.autoCheck ? /* @__PURE__ */ jsx10("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
|
|
2275
|
-
!hasBlanks ? /* @__PURE__ */ jsx10("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
|
|
2276
|
-
submitted ? /* @__PURE__ */ jsx10("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
|
|
2277
|
-
props.enableRetry && passed ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2278
|
-
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
2279
|
-
] });
|
|
2280
|
-
}
|
|
2281
|
-
var FillInTheBlanksInnerForwarded = forwardRef4(FillInTheBlanksInner);
|
|
2282
|
-
var FillInTheBlanks = forwardRef4(
|
|
2283
|
-
function FillInTheBlanks2(props, ref) {
|
|
2284
|
-
return /* @__PURE__ */ jsx10(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx10(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2285
|
-
}
|
|
2286
|
-
);
|
|
2287
|
-
|
|
2288
|
-
// src/blocks/DragTheWords.tsx
|
|
2289
|
-
import React12, { forwardRef as forwardRef5, useEffect as useEffect8, useMemo as useMemo10, useRef as useRef9, useState as useState8 } from "react";
|
|
2290
|
-
import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2291
|
-
var INTERACTION4 = "dragTheWords";
|
|
2292
|
-
function parseZones(template) {
|
|
2293
|
-
const { parts, values } = parseStarDelimitedTemplate(template, "zone");
|
|
2294
|
-
return { parts, answers: values };
|
|
2295
|
-
}
|
|
2296
|
-
function DragTheWordsInner(props, ref) {
|
|
2297
|
-
const checkId = useMemo10(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
2298
|
-
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2299
|
-
const { parts, answers } = useMemo10(() => parseZones(props.template), [props.template]);
|
|
2300
|
-
const [zones, setZones] = useState8(
|
|
2301
|
-
() => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
|
|
2302
|
-
);
|
|
2303
|
-
const [pool, setPool] = useState8(() => [...props.words]);
|
|
2304
|
-
const [keyboardWord, setKeyboardWord] = useState8(null);
|
|
2305
|
-
const [passed, setPassed] = useState8(false);
|
|
2306
|
-
const [submitted, setSubmitted] = useState8(false);
|
|
2307
|
-
const completedRef = useRef9(false);
|
|
2308
|
-
const answeredRef = useRef9(false);
|
|
2309
|
-
const reset = () => {
|
|
2310
|
-
completedRef.current = false;
|
|
2311
|
-
answeredRef.current = false;
|
|
2312
|
-
setPassed(false);
|
|
2313
|
-
setSubmitted(false);
|
|
2314
|
-
setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
|
|
2315
|
-
setPool([...props.words]);
|
|
2316
|
-
setKeyboardWord(null);
|
|
2317
|
-
};
|
|
2318
|
-
useEffect8(() => {
|
|
2319
|
-
reset();
|
|
2320
|
-
}, [checkId, props.template, props.words.join("\0")]);
|
|
2321
|
-
const hasZones = answers.length > 0;
|
|
2322
|
-
const allFilled = hasZones && answers.every((_, i) => (zones[`zone-${i}`] ?? "").length > 0);
|
|
2323
|
-
let score = 0;
|
|
2324
|
-
answers.forEach((ans, i) => {
|
|
2325
|
-
if ((zones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) score += 1;
|
|
2326
|
-
});
|
|
2327
|
-
const maxScore = answers.length;
|
|
2328
|
-
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
2329
|
-
const handle = useMemo10(
|
|
2330
|
-
() => buildAssessmentHandle({
|
|
2331
|
-
checkId,
|
|
2332
|
-
getScore: () => score,
|
|
2333
|
-
getMaxScore: () => maxScore || 1,
|
|
2334
|
-
getAnswerGiven: () => allFilled,
|
|
2335
|
-
resetTask: reset,
|
|
2336
|
-
showSolutions: () => {
|
|
2337
|
-
},
|
|
2338
|
-
getXAPIData: () => ({
|
|
2339
|
-
checkId,
|
|
2340
|
-
interactionType: INTERACTION4,
|
|
2341
|
-
response: zones,
|
|
2342
|
-
correct: passedThreshold,
|
|
2343
|
-
score,
|
|
2344
|
-
maxScore: maxScore || 1
|
|
2345
|
-
}),
|
|
2346
|
-
getCurrentState: () => ({ zones, pool, passed, keyboardWord, submitted }),
|
|
2347
|
-
resume: (state) => {
|
|
2348
|
-
const rawZones = state.zones;
|
|
2349
|
-
if (rawZones && typeof rawZones === "object") setZones({ ...rawZones });
|
|
2350
|
-
if (Array.isArray(state.pool)) setPool([...state.pool]);
|
|
2351
|
-
readBooleanStateField(state, "passed", (value) => {
|
|
2352
|
-
setPassed(value);
|
|
2353
|
-
completedRef.current = value;
|
|
2354
|
-
answeredRef.current = value;
|
|
2355
|
-
});
|
|
2356
|
-
readBooleanStateField(state, "submitted", (value) => {
|
|
2357
|
-
setSubmitted(value);
|
|
2358
|
-
if (value) answeredRef.current = true;
|
|
2359
|
-
});
|
|
2360
|
-
const kw = state.keyboardWord;
|
|
2361
|
-
if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
|
|
2362
|
-
}
|
|
2363
|
-
}),
|
|
2364
|
-
[allFilled, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, score, submitted, zones]
|
|
2365
|
-
);
|
|
2366
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2367
|
-
const placeInZone = (zoneId, word) => {
|
|
2368
|
-
if (passed && !props.enableRetry) return;
|
|
2369
|
-
const prev = zones[zoneId];
|
|
2370
|
-
setZones((z) => ({ ...z, [zoneId]: word }));
|
|
2371
|
-
setPool((p) => {
|
|
2372
|
-
const next = p.filter((w) => w !== word);
|
|
2373
|
-
if (prev) next.push(prev);
|
|
2374
|
-
return next;
|
|
2375
|
-
});
|
|
2376
|
-
setKeyboardWord(null);
|
|
2377
|
-
};
|
|
2378
|
-
const onDragStart = (word) => (e) => {
|
|
2379
|
-
e.dataTransfer.setData("text/plain", word);
|
|
2380
|
-
};
|
|
2381
|
-
const onDrop = (zoneId) => (e) => {
|
|
2382
|
-
e.preventDefault();
|
|
2383
|
-
const word = e.dataTransfer.getData("text/plain");
|
|
2384
|
-
if (word) placeInZone(zoneId, word);
|
|
2385
|
-
};
|
|
2386
|
-
const check = () => {
|
|
2387
|
-
if (!hasZones) {
|
|
2388
|
-
if (isDevEnvironment4()) {
|
|
2389
|
-
console.warn("[lessonkit] DragTheWords has no drop zones in template");
|
|
2390
|
-
}
|
|
2391
|
-
return;
|
|
2392
|
-
}
|
|
2393
|
-
if (!allFilled) return;
|
|
2394
|
-
if (answeredRef.current || submitted) return;
|
|
2395
|
-
answeredRef.current = true;
|
|
2396
|
-
setSubmitted(true);
|
|
2397
|
-
assessment.answer({
|
|
2398
|
-
checkId,
|
|
2399
|
-
interactionType: INTERACTION4,
|
|
2400
|
-
question: props.template,
|
|
2401
|
-
response: zones,
|
|
2402
|
-
correct: passedThreshold
|
|
2403
|
-
});
|
|
2404
|
-
if (passedThreshold && !completedRef.current) {
|
|
2405
|
-
completedRef.current = true;
|
|
2406
|
-
setPassed(true);
|
|
2407
|
-
assessment.complete({
|
|
2408
|
-
checkId,
|
|
2409
|
-
interactionType: INTERACTION4,
|
|
2410
|
-
score,
|
|
2411
|
-
maxScore,
|
|
2412
|
-
passingScore: props.passingScore ?? maxScore
|
|
2413
|
-
});
|
|
2414
|
-
}
|
|
2415
|
-
};
|
|
2416
|
-
useEffect8(() => {
|
|
2417
|
-
if (!allFilled) {
|
|
2418
|
-
answeredRef.current = false;
|
|
2419
|
-
setSubmitted(false);
|
|
2420
|
-
}
|
|
2421
|
-
}, [allFilled]);
|
|
2422
|
-
useEffect8(() => {
|
|
2423
|
-
if (props.autoCheck && allFilled) check();
|
|
2424
|
-
}, [allFilled, props.autoCheck, zones, passedThreshold]);
|
|
2425
|
-
return /* @__PURE__ */ jsxs7("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
|
|
2426
|
-
/* @__PURE__ */ jsx11("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
|
|
2427
|
-
/* @__PURE__ */ jsx11("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ jsx11(
|
|
2428
|
-
"button",
|
|
2429
|
-
{
|
|
2430
|
-
type: "button",
|
|
2431
|
-
draggable: true,
|
|
2432
|
-
"data-testid": `word-${word}`,
|
|
2433
|
-
"aria-pressed": keyboardWord === word,
|
|
2434
|
-
onDragStart: onDragStart(word),
|
|
2435
|
-
onClick: () => setKeyboardWord(keyboardWord === word ? null : word),
|
|
2436
|
-
style: { margin: "0.25rem" },
|
|
2437
|
-
children: word
|
|
2438
|
-
},
|
|
2439
|
-
word
|
|
2440
|
-
)) }),
|
|
2441
|
-
/* @__PURE__ */ jsx11("p", { children: parts.map((part, i) => {
|
|
2442
|
-
if (!part.startsWith("zone-")) return /* @__PURE__ */ jsx11(React12.Fragment, { children: part }, i);
|
|
2443
|
-
return /* @__PURE__ */ jsx11(
|
|
2444
|
-
"span",
|
|
2445
|
-
{
|
|
2446
|
-
role: "button",
|
|
2447
|
-
tabIndex: 0,
|
|
2448
|
-
"data-testid": part,
|
|
2449
|
-
onDragOver: (e) => e.preventDefault(),
|
|
2450
|
-
onDrop: onDrop(part),
|
|
2451
|
-
onClick: () => keyboardWord && placeInZone(part, keyboardWord),
|
|
2452
|
-
onKeyDown: (e) => {
|
|
2453
|
-
if (e.key === "Enter" && keyboardWord) placeInZone(part, keyboardWord);
|
|
2454
|
-
},
|
|
2455
|
-
style: {
|
|
2456
|
-
display: "inline-block",
|
|
2457
|
-
minWidth: "6em",
|
|
2458
|
-
border: "1px dashed currentColor",
|
|
2459
|
-
padding: "0.2em 0.5em",
|
|
2460
|
-
margin: "0 0.2em"
|
|
2461
|
-
},
|
|
2462
|
-
children: zones[part] || "___"
|
|
2463
|
-
},
|
|
2464
|
-
part
|
|
2465
|
-
);
|
|
2466
|
-
}) }),
|
|
2467
|
-
/* @__PURE__ */ jsx11("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
|
|
2468
|
-
!hasZones ? /* @__PURE__ */ jsx11("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
|
|
2469
|
-
submitted ? /* @__PURE__ */ jsx11("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
|
|
2470
|
-
] });
|
|
2471
|
-
}
|
|
2472
|
-
var DragTheWordsInnerForwarded = forwardRef5(DragTheWordsInner);
|
|
2473
|
-
var DragTheWords = forwardRef5(function DragTheWords2(props, ref) {
|
|
2474
|
-
return /* @__PURE__ */ jsx11(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx11(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2475
|
-
});
|
|
2476
|
-
|
|
2477
|
-
// src/blocks/DragAndDrop.tsx
|
|
2478
|
-
import { forwardRef as forwardRef6, useEffect as useEffect9, useMemo as useMemo11, useRef as useRef10, useState as useState9 } from "react";
|
|
2479
|
-
import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2480
|
-
var INTERACTION5 = "dragAndDrop";
|
|
2481
|
-
function DragAndDropInner(props, ref) {
|
|
2482
|
-
const checkId = useMemo11(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
2483
|
-
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2484
|
-
const [assignments, setAssignments] = useState9(
|
|
2485
|
-
() => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
|
|
2486
|
-
);
|
|
2487
|
-
const [pool, setPool] = useState9(() => props.items.map((i) => i.id));
|
|
2488
|
-
const [keyboardItem, setKeyboardItem] = useState9(null);
|
|
2489
|
-
const [passed, setPassed] = useState9(false);
|
|
2490
|
-
const [checked, setChecked] = useState9(false);
|
|
2491
|
-
const completedRef = useRef10(false);
|
|
2492
|
-
const reset = () => {
|
|
2493
|
-
completedRef.current = false;
|
|
2494
|
-
setPassed(false);
|
|
2495
|
-
setChecked(false);
|
|
2496
|
-
setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
|
|
2497
|
-
setPool(props.items.map((i) => i.id));
|
|
2498
|
-
setKeyboardItem(null);
|
|
2499
|
-
};
|
|
2500
|
-
useEffect9(() => {
|
|
2501
|
-
reset();
|
|
2502
|
-
}, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
|
|
2503
|
-
const hasTargets = props.targets.length > 0;
|
|
2504
|
-
const allFilled = hasTargets && props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
|
|
2505
|
-
let score = 0;
|
|
2506
|
-
props.targets.forEach((t) => {
|
|
2507
|
-
if (assignments[t.id] === t.accepts) score += 1;
|
|
2508
|
-
});
|
|
2509
|
-
const maxScore = props.targets.length || 1;
|
|
2510
|
-
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
2511
|
-
const handle = useMemo11(() => {
|
|
2512
|
-
return buildAssessmentHandle({
|
|
2513
|
-
checkId,
|
|
2514
|
-
getScore: () => score,
|
|
2515
|
-
getMaxScore: () => maxScore,
|
|
2516
|
-
getAnswerGiven: () => hasTargets && allFilled,
|
|
2517
|
-
resetTask: reset,
|
|
2518
|
-
showSolutions: () => {
|
|
2519
|
-
},
|
|
2520
|
-
getXAPIData: () => ({
|
|
2521
|
-
checkId,
|
|
2522
|
-
interactionType: INTERACTION5,
|
|
2523
|
-
response: assignments,
|
|
2524
|
-
correct: passedThreshold,
|
|
2525
|
-
score,
|
|
2526
|
-
maxScore
|
|
2527
|
-
}),
|
|
2528
|
-
getCurrentState: () => ({ assignments, pool, passed, checked, keyboardItem }),
|
|
2529
|
-
resume: (state) => {
|
|
2530
|
-
const rawAssignments = state.assignments;
|
|
2531
|
-
if (rawAssignments && typeof rawAssignments === "object") {
|
|
2532
|
-
setAssignments({ ...rawAssignments });
|
|
2533
|
-
}
|
|
2534
|
-
if (Array.isArray(state.pool)) setPool([...state.pool]);
|
|
2535
|
-
readBooleanStateField(state, "passed", (value) => {
|
|
2536
|
-
setPassed(value);
|
|
2537
|
-
completedRef.current = value;
|
|
2538
|
-
});
|
|
2539
|
-
readBooleanStateField(state, "checked", setChecked);
|
|
2540
|
-
const item = state.keyboardItem;
|
|
2541
|
-
if (item === null || typeof item === "string") setKeyboardItem(item ?? null);
|
|
2542
|
-
}
|
|
2543
|
-
});
|
|
2544
|
-
}, [allFilled, assignments, checkId, checked, hasTargets, keyboardItem, maxScore, passed, passedThreshold, pool, props.targets, score]);
|
|
2545
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2546
|
-
const place = (targetId, itemId) => {
|
|
2547
|
-
if (passed && !props.enableRetry) return;
|
|
2548
|
-
setChecked(false);
|
|
2549
|
-
const prev = assignments[targetId];
|
|
2550
|
-
setAssignments((a) => ({ ...a, [targetId]: itemId }));
|
|
2551
|
-
setPool((p) => {
|
|
2552
|
-
const next = p.filter((id) => id !== itemId);
|
|
2553
|
-
if (prev) next.push(prev);
|
|
2554
|
-
return next;
|
|
2555
|
-
});
|
|
2556
|
-
setKeyboardItem(null);
|
|
2557
|
-
};
|
|
2558
|
-
const check = () => {
|
|
2559
|
-
if (!allFilled) return;
|
|
2560
|
-
setChecked(true);
|
|
2561
|
-
assessment.answer({
|
|
2562
|
-
checkId,
|
|
2563
|
-
interactionType: INTERACTION5,
|
|
2564
|
-
response: assignments,
|
|
2565
|
-
correct: passedThreshold
|
|
2566
|
-
});
|
|
2567
|
-
if (passedThreshold && !completedRef.current) {
|
|
2568
|
-
completedRef.current = true;
|
|
2569
|
-
setPassed(true);
|
|
2570
|
-
assessment.complete({
|
|
2571
|
-
checkId,
|
|
2572
|
-
interactionType: INTERACTION5,
|
|
2573
|
-
score,
|
|
2574
|
-
maxScore,
|
|
2575
|
-
passingScore: props.passingScore ?? maxScore
|
|
2576
|
-
});
|
|
2577
|
-
}
|
|
2578
|
-
};
|
|
2579
|
-
return /* @__PURE__ */ jsxs8("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
|
|
2580
|
-
/* @__PURE__ */ jsx12("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
|
|
2581
|
-
/* @__PURE__ */ jsx12("div", { role: "list", "aria-label": "Draggable items", children: pool.flatMap((id) => {
|
|
2582
|
-
const item = props.items.find((i) => i.id === id);
|
|
2583
|
-
if (!item) return [];
|
|
2584
|
-
return /* @__PURE__ */ jsx12(
|
|
2585
|
-
"button",
|
|
2586
|
-
{
|
|
2587
|
-
type: "button",
|
|
2588
|
-
draggable: true,
|
|
2589
|
-
"data-testid": `drag-item-${id}`,
|
|
2590
|
-
"aria-pressed": keyboardItem === id,
|
|
2591
|
-
onDragStart: (e) => e.dataTransfer.setData("text/plain", id),
|
|
2592
|
-
onClick: () => setKeyboardItem(keyboardItem === id ? null : id),
|
|
2593
|
-
style: { margin: "0.25rem" },
|
|
2594
|
-
children: item.label
|
|
2595
|
-
},
|
|
2596
|
-
id
|
|
2597
|
-
);
|
|
2598
|
-
}) }),
|
|
2599
|
-
/* @__PURE__ */ jsx12("ul", { children: props.targets.map((target) => {
|
|
2600
|
-
const assigned = assignments[target.id];
|
|
2601
|
-
const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
|
|
2602
|
-
return /* @__PURE__ */ jsxs8("li", { children: [
|
|
2603
|
-
/* @__PURE__ */ jsx12("strong", { children: target.label }),
|
|
2604
|
-
" ",
|
|
2605
|
-
/* @__PURE__ */ jsx12(
|
|
2606
|
-
"span",
|
|
2607
|
-
{
|
|
2608
|
-
role: "button",
|
|
2609
|
-
tabIndex: 0,
|
|
2610
|
-
"data-testid": `drop-${target.id}`,
|
|
2611
|
-
onDragOver: (e) => e.preventDefault(),
|
|
2612
|
-
onDrop: (e) => {
|
|
2613
|
-
e.preventDefault();
|
|
2614
|
-
const id = e.dataTransfer.getData("text/plain");
|
|
2615
|
-
if (id) place(target.id, id);
|
|
2616
|
-
},
|
|
2617
|
-
onClick: () => keyboardItem && place(target.id, keyboardItem),
|
|
2618
|
-
onKeyDown: (e) => {
|
|
2619
|
-
if (e.key === "Enter" && keyboardItem) place(target.id, keyboardItem);
|
|
2620
|
-
},
|
|
2621
|
-
style: {
|
|
2622
|
-
display: "inline-block",
|
|
2623
|
-
minWidth: "8em",
|
|
2624
|
-
border: "1px dashed currentColor",
|
|
2625
|
-
padding: "0.25em"
|
|
2626
|
-
},
|
|
2627
|
-
children: label
|
|
2628
|
-
}
|
|
2629
|
-
)
|
|
2630
|
-
] }, target.id);
|
|
2631
|
-
}) }),
|
|
2632
|
-
/* @__PURE__ */ jsx12("button", { type: "button", "data-testid": "check-drag-drop", disabled: !hasTargets || !allFilled || passed, onClick: check, children: "Check" }),
|
|
2633
|
-
checked ? /* @__PURE__ */ jsx12("p", { role: "status", "aria-live": "polite", children: passedThreshold ? "Correct" : "Try again" }) : null
|
|
2634
|
-
] });
|
|
2635
|
-
}
|
|
2636
|
-
var DragAndDropInnerForwarded = forwardRef6(DragAndDropInner);
|
|
2637
|
-
var DragAndDrop = forwardRef6(function DragAndDrop2(props, ref) {
|
|
2638
|
-
return /* @__PURE__ */ jsx12(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx12(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2639
|
-
});
|
|
2640
|
-
|
|
2641
|
-
// src/blocks/AssessmentSequence.tsx
|
|
2642
|
-
import React16, { forwardRef as forwardRef7, useCallback as useCallback7, useEffect as useEffect12, useId as useId3, useMemo as useMemo13, useRef as useRef13, useState as useState10 } from "react";
|
|
2643
|
-
import { deriveId } from "@lessonkit/core";
|
|
2644
|
-
|
|
2645
|
-
// src/compound/useCompoundShell.ts
|
|
2646
|
-
import { useMemo as useMemo12 } from "react";
|
|
2647
|
-
import { clampCompoundPageIndex as clampCompoundPageIndex3 } from "@lessonkit/core";
|
|
2648
|
-
|
|
2649
|
-
// src/compound/useCompoundNavigation.ts
|
|
2650
|
-
import { useCallback as useCallback4 } from "react";
|
|
2651
|
-
function useCompoundNavigation(pageCount, index, setIndex) {
|
|
2652
|
-
const goNext = useCallback4(() => {
|
|
2653
|
-
if (pageCount < 1) return;
|
|
2654
|
-
setIndex((i) => Math.min(i + 1, pageCount - 1));
|
|
2655
|
-
}, [pageCount, setIndex]);
|
|
2656
|
-
const goPrev = useCallback4(() => {
|
|
2657
|
-
setIndex((i) => Math.max(i - 1, 0));
|
|
2658
|
-
}, [setIndex]);
|
|
2659
|
-
const clampedIndex = pageCount < 1 ? 0 : Math.min(index, pageCount - 1);
|
|
2660
|
-
return {
|
|
2661
|
-
index: clampedIndex,
|
|
2662
|
-
setIndex,
|
|
2663
|
-
goNext,
|
|
2664
|
-
goPrev,
|
|
2665
|
-
progress: { current: pageCount < 1 ? 0 : clampedIndex + 1, total: pageCount }
|
|
2666
|
-
};
|
|
2667
|
-
}
|
|
2668
|
-
|
|
2669
|
-
// src/compound/useCompoundPersistence.ts
|
|
2670
|
-
import { useCallback as useCallback6, useContext as useContext7, useEffect as useEffect11, useRef as useRef12 } from "react";
|
|
2671
|
-
import {
|
|
2672
|
-
clampCompoundPageIndex as clampCompoundPageIndex2,
|
|
2673
|
-
createCompoundResumeState as createCompoundResumeState2,
|
|
2674
|
-
createSessionStoragePort as createSessionStoragePort3,
|
|
2675
|
-
loadCompoundState as loadCompoundState2
|
|
2676
|
-
} from "@lessonkit/core";
|
|
2677
|
-
|
|
2678
|
-
// src/compound/resumeChildHandles.ts
|
|
2679
|
-
function filterRegisteredChildStates(handles, childStates) {
|
|
2680
|
-
const filtered = {};
|
|
2681
|
-
for (const [key, value] of Object.entries(childStates)) {
|
|
2682
|
-
if (handles.has(key)) {
|
|
2683
|
-
filtered[key] = value;
|
|
2684
|
-
}
|
|
2685
|
-
}
|
|
2686
|
-
return filtered;
|
|
2687
|
-
}
|
|
2688
|
-
function resumeChildHandles(handles, childStates, opts) {
|
|
2689
|
-
const pendingKeys = Object.keys(childStates);
|
|
2690
|
-
const alreadyResumed = opts?.alreadyResumed;
|
|
2691
|
-
if (opts?.waitForHandles && pendingKeys.length > 0) {
|
|
2692
|
-
if (handles.size === 0) return false;
|
|
2693
|
-
const registeredPending = pendingKeys.filter((k) => handles.has(k));
|
|
2694
|
-
if (registeredPending.length === 0) {
|
|
2695
|
-
return false;
|
|
2696
|
-
}
|
|
2697
|
-
if (registeredPending.length < pendingKeys.length) {
|
|
2698
|
-
for (const key of registeredPending) {
|
|
2699
|
-
if (alreadyResumed?.has(key)) continue;
|
|
2700
|
-
const handle = handles.get(key);
|
|
2701
|
-
const child = childStates[key];
|
|
2702
|
-
if (handle?.resume && child) {
|
|
2703
|
-
handle.resume(child);
|
|
2704
|
-
alreadyResumed?.add(key);
|
|
2705
|
-
}
|
|
2706
|
-
}
|
|
2707
|
-
return false;
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
2710
|
-
for (const [checkId, handle] of handles) {
|
|
2711
|
-
if (alreadyResumed?.has(checkId)) continue;
|
|
2712
|
-
const child = childStates[checkId];
|
|
2713
|
-
if (child && handle.resume) {
|
|
2714
|
-
handle.resume(child);
|
|
2715
|
-
alreadyResumed?.add(checkId);
|
|
2716
|
-
}
|
|
2717
|
-
}
|
|
2718
|
-
return true;
|
|
2719
|
-
}
|
|
2720
|
-
|
|
2721
|
-
// src/compound/useCompoundResume.ts
|
|
2722
|
-
import { useCallback as useCallback5, useContext as useContext6, useEffect as useEffect10, useRef as useRef11 } from "react";
|
|
2723
|
-
import { loadCompoundState, saveCompoundState } from "@lessonkit/core";
|
|
2724
|
-
import { createSessionStoragePort as createSessionStoragePort2 } from "@lessonkit/core";
|
|
2725
|
-
var warnedCompoundPersistFailure = false;
|
|
2726
|
-
function warnCompoundPersistFailure() {
|
|
2727
|
-
if (warnedCompoundPersistFailure || !isDevEnvironment4()) return;
|
|
2728
|
-
warnedCompoundPersistFailure = true;
|
|
2729
|
-
console.warn(
|
|
2730
|
-
"[lessonkit] compound resume state could not be saved to sessionStorage (quota or privacy mode); progress may be lost on reload."
|
|
2731
|
-
);
|
|
2732
|
-
}
|
|
2733
|
-
function useCompoundResume(opts) {
|
|
2734
|
-
const lessonkitCtx = useContext6(LessonkitContext);
|
|
2735
|
-
const storageRef = useRef11(opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2());
|
|
2736
|
-
const resumedRef = useRef11(false);
|
|
2737
|
-
const resumeKeyRef = useRef11("");
|
|
2738
|
-
const prevEnabledRef = useRef11(opts.enabled);
|
|
2739
|
-
useEffect10(() => {
|
|
2740
|
-
storageRef.current = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2();
|
|
2741
|
-
}, [opts.storage, lessonkitCtx?.storage]);
|
|
2742
|
-
useEffect10(() => {
|
|
2743
|
-
if (!prevEnabledRef.current && opts.enabled) {
|
|
2744
|
-
resumedRef.current = false;
|
|
2745
|
-
}
|
|
2746
|
-
prevEnabledRef.current = opts.enabled;
|
|
2747
|
-
const key = `${opts.courseId ?? ""}:${opts.compoundId}`;
|
|
2748
|
-
if (resumeKeyRef.current !== key) {
|
|
2749
|
-
resumeKeyRef.current = key;
|
|
2750
|
-
resumedRef.current = false;
|
|
2751
|
-
}
|
|
2752
|
-
if (!opts.enabled || !opts.courseId || resumedRef.current) return;
|
|
2753
|
-
const saved = loadCompoundState(storageRef.current, opts.courseId, opts.compoundId);
|
|
2754
|
-
if (saved) {
|
|
2755
|
-
resumedRef.current = true;
|
|
2756
|
-
opts.onResume?.(saved);
|
|
2757
|
-
}
|
|
2758
|
-
}, [opts.enabled, opts.courseId, opts.compoundId, opts.onResume]);
|
|
2759
|
-
return useCallback5(
|
|
2760
|
-
(state) => {
|
|
2761
|
-
if (!opts.enabled || !opts.courseId) return;
|
|
2762
|
-
const persisted = saveCompoundState(storageRef.current, opts.courseId, opts.compoundId, state);
|
|
2763
|
-
if (!persisted) warnCompoundPersistFailure();
|
|
2764
|
-
},
|
|
2765
|
-
[opts.enabled, opts.courseId, opts.compoundId]
|
|
2766
|
-
);
|
|
2767
|
-
}
|
|
2768
|
-
|
|
2769
|
-
// src/compound/useCompoundPersistence.ts
|
|
2770
|
-
function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, storage = createSessionStoragePort3()) {
|
|
2771
|
-
if (!enabled || !courseId || pageCount < 1) return 0;
|
|
2772
|
-
const saved = loadCompoundState2(storage, courseId, compoundId);
|
|
2773
|
-
if (!saved) return 0;
|
|
2774
|
-
return clampCompoundPageIndex2(saved.activePageIndex, pageCount);
|
|
2775
|
-
}
|
|
2776
|
-
function stripOrphanChildStates(handles, childStates) {
|
|
2777
|
-
return filterRegisteredChildStates(handles, childStates);
|
|
2778
|
-
}
|
|
2779
|
-
function useCompoundPersistence(opts) {
|
|
2780
|
-
const lessonkitCtx = useContext7(LessonkitContext);
|
|
2781
|
-
const storage = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort3();
|
|
2782
|
-
const ctx = useCompoundRegistry();
|
|
2783
|
-
const handlesVersion = useCompoundHandlesVersion();
|
|
2784
|
-
const bridgeRef = useCompoundHydrationBridgeRef();
|
|
2785
|
-
const pendingChildResumeRef = useRef12(null);
|
|
2786
|
-
const resumedChildKeysRef = useRef12(/* @__PURE__ */ new Set());
|
|
2787
|
-
const loadedChildStatesRef = useRef12({});
|
|
2788
|
-
const skipSaveUntilHydratedRef = useRef12(false);
|
|
2789
|
-
const hydrationKeyRef = useRef12("");
|
|
2790
|
-
const hydrationInitRef = useRef12(false);
|
|
2791
|
-
const hydrationKey = `${opts.courseId ?? ""}:${opts.compoundId}`;
|
|
2792
|
-
if (hydrationKeyRef.current !== hydrationKey) {
|
|
2793
|
-
hydrationKeyRef.current = hydrationKey;
|
|
2794
|
-
hydrationInitRef.current = false;
|
|
2795
|
-
loadedChildStatesRef.current = {};
|
|
2796
|
-
skipSaveUntilHydratedRef.current = false;
|
|
2797
|
-
pendingChildResumeRef.current = null;
|
|
2798
|
-
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
2799
|
-
}
|
|
2800
|
-
if (!hydrationInitRef.current && opts.enabled && opts.courseId) {
|
|
2801
|
-
hydrationInitRef.current = true;
|
|
2802
|
-
const saved = loadCompoundState2(storage, opts.courseId, opts.compoundId);
|
|
2803
|
-
if (saved && Object.keys(saved.childStates).length > 0) {
|
|
2804
|
-
loadedChildStatesRef.current = { ...saved.childStates };
|
|
2805
|
-
skipSaveUntilHydratedRef.current = true;
|
|
2806
|
-
pendingChildResumeRef.current = saved;
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2809
|
-
const buildState = useCallback6(() => {
|
|
2810
|
-
const childStates = {
|
|
2811
|
-
...loadedChildStatesRef.current
|
|
2812
|
-
};
|
|
2813
|
-
if (ctx) {
|
|
2814
|
-
for (const [checkId, entry] of ctx.getRegisteredHandles()) {
|
|
2815
|
-
const handle = entry.handle;
|
|
2816
|
-
if (handle.getCurrentState) {
|
|
2817
|
-
childStates[checkId] = handle.getCurrentState();
|
|
2818
|
-
delete loadedChildStatesRef.current[checkId];
|
|
2819
|
-
}
|
|
2820
|
-
}
|
|
2821
|
-
}
|
|
2822
|
-
return createCompoundResumeState2({
|
|
2823
|
-
activePageIndex: clampCompoundPageIndex2(opts.index, opts.pageCount),
|
|
2824
|
-
childStates
|
|
2825
|
-
});
|
|
2826
|
-
}, [ctx, opts.index, opts.pageCount]);
|
|
2827
|
-
const buildStateRef = useRef12(buildState);
|
|
2828
|
-
buildStateRef.current = buildState;
|
|
2829
|
-
const finalizeHydration = useCallback6(
|
|
2830
|
-
(childStates) => {
|
|
2831
|
-
loadedChildStatesRef.current = {
|
|
2832
|
-
...loadedChildStatesRef.current,
|
|
2833
|
-
...childStates
|
|
2834
|
-
};
|
|
2835
|
-
skipSaveUntilHydratedRef.current = false;
|
|
2836
|
-
pendingChildResumeRef.current = null;
|
|
2837
|
-
},
|
|
2838
|
-
[]
|
|
2839
|
-
);
|
|
2840
|
-
const applyPendingChildResume = useCallback6(() => {
|
|
2841
|
-
const pending = pendingChildResumeRef.current;
|
|
2842
|
-
if (!pending || !ctx) return;
|
|
2843
|
-
const handles = ctx.getHandles();
|
|
2844
|
-
const applied = resumeChildHandles(handles, pending.childStates, {
|
|
2845
|
-
waitForHandles: true,
|
|
2846
|
-
alreadyResumed: resumedChildKeysRef.current
|
|
2847
|
-
});
|
|
2848
|
-
if (!applied) {
|
|
2849
|
-
const handlesAtWait = handles.size;
|
|
2850
|
-
queueMicrotask(() => {
|
|
2851
|
-
if (pendingChildResumeRef.current !== pending) return;
|
|
2852
|
-
const handlesNow = ctx.getHandles();
|
|
2853
|
-
if (handlesNow.size !== handlesAtWait) return;
|
|
2854
|
-
const registeredOnly2 = stripOrphanChildStates(handlesNow, pending.childStates);
|
|
2855
|
-
resumeChildHandles(handlesNow, registeredOnly2, {
|
|
2856
|
-
alreadyResumed: resumedChildKeysRef.current
|
|
2857
|
-
});
|
|
2858
|
-
finalizeHydration(registeredOnly2);
|
|
2859
|
-
});
|
|
2860
|
-
return;
|
|
2861
|
-
}
|
|
2862
|
-
const registeredOnly = stripOrphanChildStates(handles, pending.childStates);
|
|
2863
|
-
finalizeHydration(registeredOnly);
|
|
2864
|
-
}, [ctx, finalizeHydration]);
|
|
2865
|
-
const saveResume = useCompoundResume({
|
|
2866
|
-
courseId: opts.courseId,
|
|
2867
|
-
compoundId: opts.compoundId,
|
|
2868
|
-
enabled: opts.enabled,
|
|
2869
|
-
storage,
|
|
2870
|
-
onResume: (state) => {
|
|
2871
|
-
const clamped = clampCompoundPageIndex2(state.activePageIndex, opts.pageCount);
|
|
2872
|
-
loadedChildStatesRef.current = { ...state.childStates };
|
|
2873
|
-
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
2874
|
-
opts.setIndex(clamped);
|
|
2875
|
-
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
2876
|
-
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
2877
|
-
queueMicrotask(() => applyPendingChildResume());
|
|
2878
|
-
}
|
|
2879
|
-
});
|
|
2880
|
-
const persistNow = useCallback6(() => {
|
|
2881
|
-
if (!opts.enabled || !opts.courseId) return;
|
|
2882
|
-
saveResume(buildStateRef.current());
|
|
2883
|
-
}, [opts.enabled, opts.courseId, saveResume]);
|
|
2884
|
-
const notifyImperativeResume = useCallback6(
|
|
2885
|
-
(state) => {
|
|
2886
|
-
const clamped = clampCompoundPageIndex2(state.activePageIndex, opts.pageCount);
|
|
2887
|
-
loadedChildStatesRef.current = { ...state.childStates };
|
|
2888
|
-
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
2889
|
-
opts.setIndex(clamped);
|
|
2890
|
-
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
2891
|
-
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
2892
|
-
queueMicrotask(() => applyPendingChildResume());
|
|
2893
|
-
},
|
|
2894
|
-
[opts.pageCount, opts.setIndex, applyPendingChildResume]
|
|
2895
|
-
);
|
|
2896
|
-
useEffect11(() => {
|
|
2897
|
-
if (!bridgeRef) return;
|
|
2898
|
-
bridgeRef.current = { notifyImperativeResume };
|
|
2899
|
-
return () => {
|
|
2900
|
-
if (bridgeRef.current?.notifyImperativeResume === notifyImperativeResume) {
|
|
2901
|
-
bridgeRef.current = null;
|
|
2902
|
-
}
|
|
2903
|
-
};
|
|
2904
|
-
}, [bridgeRef, notifyImperativeResume]);
|
|
2905
|
-
useEffect11(() => {
|
|
2906
|
-
persistNow();
|
|
2907
|
-
}, [persistNow, opts.index, opts.pageCount, handlesVersion]);
|
|
2908
|
-
useEffect11(() => {
|
|
2909
|
-
applyPendingChildResume();
|
|
2910
|
-
}, [opts.index, handlesVersion, applyPendingChildResume]);
|
|
2911
|
-
useEffect11(() => {
|
|
2912
|
-
if (!opts.enabled || !opts.courseId || typeof document === "undefined") return;
|
|
2913
|
-
const flushOnExit = () => {
|
|
2914
|
-
if (document.visibilityState === "hidden") persistNow();
|
|
2915
|
-
};
|
|
2916
|
-
document.addEventListener("visibilitychange", flushOnExit);
|
|
2917
|
-
window.addEventListener("pagehide", flushOnExit);
|
|
2918
|
-
return () => {
|
|
2919
|
-
document.removeEventListener("visibilitychange", flushOnExit);
|
|
2920
|
-
window.removeEventListener("pagehide", flushOnExit);
|
|
2921
|
-
};
|
|
2922
|
-
}, [opts.enabled, opts.courseId, persistNow]);
|
|
2923
|
-
}
|
|
2924
|
-
|
|
2925
|
-
// src/compound/useCompoundShell.ts
|
|
2926
|
-
function useCompoundShell(opts) {
|
|
2927
|
-
const ctx = useCompoundRegistry();
|
|
2928
|
-
useCompoundPersistence({
|
|
2929
|
-
courseId: opts.courseId,
|
|
2930
|
-
compoundId: opts.compoundId,
|
|
2931
|
-
pageCount: opts.pageCount,
|
|
2932
|
-
index: opts.index,
|
|
2933
|
-
setIndex: opts.setIndex,
|
|
2934
|
-
enabled: opts.persistEnabled,
|
|
2935
|
-
storage: opts.storage
|
|
2936
|
-
});
|
|
2937
|
-
const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
|
|
2938
|
-
const visibleIndex = clampCompoundPageIndex3(opts.index, opts.pageCount);
|
|
2939
|
-
useCompoundHandleRef(opts.ref, {
|
|
2940
|
-
activePageIndex: visibleIndex,
|
|
2941
|
-
setActivePageIndex: opts.setIndex,
|
|
2942
|
-
getHandles: () => ctx?.getHandles() ?? /* @__PURE__ */ new Map(),
|
|
2943
|
-
getRegisteredHandles: () => ctx?.getRegisteredHandles() ?? /* @__PURE__ */ new Map(),
|
|
2944
|
-
pageCount: opts.pageCount,
|
|
2945
|
-
enableSolutionsButton: opts.enableSolutionsButton
|
|
2946
|
-
});
|
|
2947
|
-
return { visibleIndex, goNext, goPrev, progress, ctx };
|
|
2948
|
-
}
|
|
2949
|
-
function useCompoundInitialIndex(opts) {
|
|
2950
|
-
return useMemo12(
|
|
2951
|
-
() => readCompoundInitialIndex(
|
|
2952
|
-
opts.courseId,
|
|
2953
|
-
opts.compoundId,
|
|
2954
|
-
opts.pageCount,
|
|
2955
|
-
opts.persistEnabled,
|
|
2956
|
-
opts.storage
|
|
2957
|
-
),
|
|
2958
|
-
[opts.courseId, opts.compoundId, opts.pageCount, opts.persistEnabled, opts.storage]
|
|
2959
|
-
);
|
|
2960
|
-
}
|
|
2961
|
-
|
|
2962
|
-
// src/compound/validateChildren.ts
|
|
2963
|
-
import React15 from "react";
|
|
2964
|
-
import {
|
|
2965
|
-
ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
2966
|
-
COMPOUND_MAX_NESTING_DEPTH,
|
|
2967
|
-
isChildTypeAllowed
|
|
2968
|
-
} from "@lessonkit/core";
|
|
2969
|
-
|
|
2970
|
-
// src/compound/blockType.ts
|
|
2971
|
-
var LESSONKIT_BLOCK_TYPE = /* @__PURE__ */ Symbol.for("lessonkit.blockType");
|
|
2972
|
-
function setLessonkitBlockType(component, blockType) {
|
|
2973
|
-
component[LESSONKIT_BLOCK_TYPE] = blockType;
|
|
2974
|
-
if (!component.displayName) {
|
|
2975
|
-
component.displayName = blockType;
|
|
2976
|
-
}
|
|
2977
|
-
return component;
|
|
2978
|
-
}
|
|
2979
|
-
function getLessonkitBlockType(component) {
|
|
2980
|
-
if (!component || typeof component !== "object" && typeof component !== "function") {
|
|
2981
|
-
return void 0;
|
|
2982
|
-
}
|
|
2983
|
-
const typed = component;
|
|
2984
|
-
return typed[LESSONKIT_BLOCK_TYPE] ?? typed.displayName;
|
|
2985
|
-
}
|
|
2986
|
-
|
|
2987
|
-
// src/compound/validateChildren.ts
|
|
2988
|
-
var warnedPairs = /* @__PURE__ */ new Set();
|
|
2989
|
-
var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
2990
|
-
"Page",
|
|
2991
|
-
"InteractiveBook",
|
|
2992
|
-
"Slide",
|
|
2993
|
-
"SlideDeck",
|
|
2994
|
-
"AssessmentSequence"
|
|
2995
|
-
]);
|
|
2996
|
-
function warnOrThrow(msg, strict) {
|
|
2997
|
-
if (strict) throw new Error(msg);
|
|
2998
|
-
if (!warnedPairs.has(msg)) {
|
|
2999
|
-
warnedPairs.add(msg);
|
|
3000
|
-
console.warn(msg);
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
function validateNode(parent, node, depth, strict) {
|
|
3004
|
-
React15.Children.forEach(node, (child) => {
|
|
3005
|
-
if (!React15.isValidElement(child)) return;
|
|
3006
|
-
const blockType = getLessonkitBlockType(child.type);
|
|
3007
|
-
if (!blockType) {
|
|
3008
|
-
if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
3009
|
-
validateNode(parent, child.props.children, depth, strict);
|
|
3010
|
-
}
|
|
3011
|
-
return;
|
|
3012
|
-
}
|
|
3013
|
-
if (!isChildTypeAllowed(parent, blockType)) {
|
|
3014
|
-
const key = `${parent}:${blockType}`;
|
|
3015
|
-
if (!warnedPairs.has(key)) {
|
|
3016
|
-
warnedPairs.add(key);
|
|
3017
|
-
const msg = `[lessonkit] Block "${blockType}" is not in the allowlist for "${parent}"`;
|
|
3018
|
-
if (strict) throw new Error(msg);
|
|
3019
|
-
console.warn(msg);
|
|
3020
|
-
}
|
|
3021
|
-
}
|
|
3022
|
-
if (COMPOUND_CONTAINER_TYPES.has(blockType)) {
|
|
3023
|
-
const maxDepth = COMPOUND_MAX_NESTING_DEPTH[parent];
|
|
3024
|
-
if (depth >= maxDepth) {
|
|
3025
|
-
warnOrThrow(
|
|
3026
|
-
`[lessonkit] Block "${blockType}" exceeds max nesting depth (${maxDepth}) for "${parent}"`,
|
|
3027
|
-
strict
|
|
3028
|
-
);
|
|
3029
|
-
}
|
|
3030
|
-
const nestedParent = blockType;
|
|
3031
|
-
validateNode(nestedParent, child.props.children, depth + 1, strict);
|
|
3032
|
-
} else if (blockType === "Accordion") {
|
|
3033
|
-
const sections = child.props.sections;
|
|
3034
|
-
if (sections) validateAccordionSections(sections, strict);
|
|
3035
|
-
} else if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
3036
|
-
validateSubtreeForForbidden(
|
|
3037
|
-
child.props.children,
|
|
3038
|
-
ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
3039
|
-
strict
|
|
3040
|
-
);
|
|
3041
|
-
}
|
|
3042
|
-
});
|
|
3043
|
-
}
|
|
3044
|
-
function validateSubtreeForForbidden(node, forbidden, strict) {
|
|
3045
|
-
React15.Children.forEach(node, (child) => {
|
|
3046
|
-
if (!React15.isValidElement(child)) return;
|
|
3047
|
-
const blockType = getLessonkitBlockType(child.type);
|
|
3048
|
-
if (blockType && forbidden.includes(blockType)) {
|
|
3049
|
-
warnOrThrow(`[lessonkit] Block "${blockType}" must not nest inside Accordion`, strict);
|
|
3050
|
-
}
|
|
3051
|
-
if (blockType === "Accordion") {
|
|
3052
|
-
const sections = child.props.sections;
|
|
3053
|
-
if (sections) validateAccordionSections(sections, strict);
|
|
3054
|
-
return;
|
|
3055
|
-
}
|
|
3056
|
-
if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
3057
|
-
validateSubtreeForForbidden(
|
|
3058
|
-
child.props.children,
|
|
3059
|
-
forbidden,
|
|
3060
|
-
strict
|
|
3061
|
-
);
|
|
3062
|
-
}
|
|
3063
|
-
});
|
|
3064
|
-
}
|
|
3065
|
-
function validateAccordionSections(sections, strict) {
|
|
3066
|
-
if (!isDevEnvironment4() && !strict) return;
|
|
3067
|
-
for (const section of sections) {
|
|
3068
|
-
validateSubtreeForForbidden(section.content, ACCORDION_FORBIDDEN_CHILD_TYPES, strict);
|
|
3069
|
-
}
|
|
3070
|
-
}
|
|
3071
|
-
function validateCompoundChildren(parent, children, strict) {
|
|
3072
|
-
if (!isDevEnvironment4() && !strict) return;
|
|
3073
|
-
validateNode(parent, children, 0, strict);
|
|
3074
|
-
}
|
|
3075
|
-
|
|
3076
|
-
// src/compound/warnPersistence.ts
|
|
3077
|
-
var DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID = "assessment-sequence";
|
|
3078
|
-
function warnSharedCompoundStorageKey(opts) {
|
|
3079
|
-
if (!opts.persistEnabled || opts.hasExplicitBlockId || !isDevEnvironment4()) return;
|
|
3080
|
-
console.warn(
|
|
3081
|
-
`[lessonkit] <${opts.componentName}> without blockId shares one sessionStorage key when persistCompoundState is enabled; set a unique blockId per instance.`
|
|
3082
|
-
);
|
|
3083
|
-
}
|
|
3084
|
-
|
|
3085
|
-
// src/blocks/AssessmentSequence.tsx
|
|
3086
|
-
import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
3087
|
-
var AssessmentSequenceInner = forwardRef7(
|
|
3088
|
-
function AssessmentSequenceInner2(props, ref) {
|
|
3089
|
-
const { compoundId, childArray, index, setIndex, persistEnabled } = props;
|
|
3090
|
-
const sequential = props.sequential !== false;
|
|
3091
|
-
const { config } = useLessonkit();
|
|
3092
|
-
const { visibleIndex, goNext, goPrev, progress } = useCompoundShell({
|
|
3093
|
-
courseId: config.courseId,
|
|
3094
|
-
compoundId,
|
|
3095
|
-
pageCount: childArray.length,
|
|
3096
|
-
index,
|
|
3097
|
-
setIndex,
|
|
3098
|
-
persistEnabled,
|
|
3099
|
-
ref,
|
|
3100
|
-
enableSolutionsButton: props.enableSolutionsButton
|
|
3101
|
-
});
|
|
3102
|
-
validateCompoundChildren("AssessmentSequence", props.children);
|
|
3103
|
-
if (!sequential) {
|
|
3104
|
-
return /* @__PURE__ */ jsx13("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
|
|
3105
|
-
}
|
|
3106
|
-
return /* @__PURE__ */ jsxs9("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
|
|
3107
|
-
/* @__PURE__ */ jsxs9("p", { children: [
|
|
3108
|
-
"Question ",
|
|
3109
|
-
progress.current,
|
|
3110
|
-
" of ",
|
|
3111
|
-
progress.total
|
|
3112
|
-
] }),
|
|
3113
|
-
/* @__PURE__ */ jsx13("div", { "data-testid": "assessment-sequence-step", children: childArray.map((child, i) => /* @__PURE__ */ jsx13("div", { hidden: i !== visibleIndex, children: /* @__PURE__ */ jsx13(CompoundPageIndexProvider, { pageIndex: i, children: child }) }, child.key ?? i)) }),
|
|
3114
|
-
/* @__PURE__ */ jsxs9("nav", { "aria-label": "Sequence navigation", children: [
|
|
3115
|
-
/* @__PURE__ */ jsx13(
|
|
3116
|
-
"button",
|
|
3117
|
-
{
|
|
3118
|
-
type: "button",
|
|
3119
|
-
"data-testid": "sequence-prev",
|
|
3120
|
-
disabled: visibleIndex === 0 || childArray.length === 0,
|
|
3121
|
-
onClick: goPrev,
|
|
3122
|
-
children: "Previous"
|
|
3123
|
-
}
|
|
3124
|
-
),
|
|
3125
|
-
/* @__PURE__ */ jsx13(
|
|
3126
|
-
"button",
|
|
3127
|
-
{
|
|
3128
|
-
type: "button",
|
|
3129
|
-
"data-testid": "sequence-next",
|
|
3130
|
-
disabled: visibleIndex >= childArray.length - 1 || childArray.length === 0,
|
|
3131
|
-
onClick: goNext,
|
|
3132
|
-
children: "Next"
|
|
3133
|
-
}
|
|
3134
|
-
)
|
|
3135
|
-
] })
|
|
3136
|
-
] });
|
|
3137
|
-
}
|
|
3138
|
-
);
|
|
3139
|
-
var AssessmentSequence = forwardRef7(
|
|
3140
|
-
function AssessmentSequence2(props, ref) {
|
|
3141
|
-
const reactInstanceId = useId3();
|
|
3142
|
-
const autoCompoundIdRef = useRef13(null);
|
|
3143
|
-
if (!props.blockId && !autoCompoundIdRef.current) {
|
|
3144
|
-
autoCompoundIdRef.current = deriveId(`assessment-sequence-${reactInstanceId}`);
|
|
3145
|
-
}
|
|
3146
|
-
const compoundId = useMemo13(
|
|
3147
|
-
() => props.blockId ? normalizeComponentId(props.blockId, "blockId") : autoCompoundIdRef.current ?? DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
|
|
3148
|
-
[props.blockId]
|
|
3149
|
-
);
|
|
3150
|
-
const childArray = React16.Children.toArray(props.children).filter(
|
|
3151
|
-
React16.isValidElement
|
|
3152
|
-
);
|
|
3153
|
-
const { config, storage } = useLessonkit();
|
|
3154
|
-
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
3155
|
-
useEffect12(() => {
|
|
3156
|
-
warnSharedCompoundStorageKey({
|
|
3157
|
-
persistEnabled,
|
|
3158
|
-
hasExplicitBlockId: Boolean(props.blockId),
|
|
3159
|
-
componentName: "AssessmentSequence"
|
|
3160
|
-
});
|
|
3161
|
-
}, [persistEnabled, props.blockId]);
|
|
3162
|
-
const initialIndex = useCompoundInitialIndex({
|
|
3163
|
-
courseId: config.courseId,
|
|
3164
|
-
compoundId,
|
|
3165
|
-
pageCount: childArray.length,
|
|
3166
|
-
persistEnabled,
|
|
3167
|
-
storage
|
|
3168
|
-
});
|
|
3169
|
-
const [index, setIndex] = useState10(initialIndex);
|
|
3170
|
-
const setIndexStable = useCallback7((i) => setIndex(i), []);
|
|
3171
|
-
useEffect12(() => {
|
|
3172
|
-
setIndex(initialIndex);
|
|
3173
|
-
}, [config.courseId, compoundId, initialIndex]);
|
|
3174
|
-
return /* @__PURE__ */ jsx13(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx13(
|
|
3175
|
-
AssessmentSequenceInner,
|
|
3176
|
-
{
|
|
3177
|
-
...props,
|
|
3178
|
-
ref,
|
|
3179
|
-
compoundId,
|
|
3180
|
-
childArray,
|
|
3181
|
-
index,
|
|
3182
|
-
setIndex,
|
|
3183
|
-
persistEnabled
|
|
3184
|
-
}
|
|
3185
|
-
) });
|
|
3186
|
-
}
|
|
3187
|
-
);
|
|
3188
|
-
setLessonkitBlockType(AssessmentSequence, "AssessmentSequence");
|
|
3189
|
-
|
|
3190
|
-
// src/blocks/Text.tsx
|
|
3191
|
-
import "react";
|
|
3192
|
-
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
3193
|
-
function Text(props) {
|
|
3194
|
-
return /* @__PURE__ */ jsx14("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
|
|
3195
|
-
}
|
|
3196
|
-
setLessonkitBlockType(Text, "Text");
|
|
3197
|
-
|
|
3198
|
-
// src/blocks/Heading.tsx
|
|
3199
|
-
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
3200
|
-
function Heading(props) {
|
|
3201
|
-
const Tag = `h${props.level}`;
|
|
3202
|
-
return /* @__PURE__ */ jsx15(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
|
|
3203
|
-
}
|
|
3204
|
-
setLessonkitBlockType(Heading, "Heading");
|
|
3205
|
-
|
|
3206
|
-
// src/blocks/Image.tsx
|
|
3207
|
-
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
3208
|
-
function Image(props) {
|
|
3209
|
-
return /* @__PURE__ */ jsx16(
|
|
3210
|
-
"img",
|
|
3211
|
-
{
|
|
3212
|
-
src: props.src,
|
|
3213
|
-
alt: props.alt,
|
|
3214
|
-
"data-lk-block-id": props.blockId,
|
|
3215
|
-
"data-testid": props.blockId ? `image-${props.blockId}` : "image",
|
|
3216
|
-
style: { maxWidth: "100%", height: "auto" }
|
|
3217
|
-
}
|
|
3218
|
-
);
|
|
3219
|
-
}
|
|
3220
|
-
setLessonkitBlockType(Image, "Image");
|
|
3221
|
-
|
|
3222
|
-
// src/blocks/Page.tsx
|
|
3223
|
-
import { useEffect as useEffect13 } from "react";
|
|
3224
|
-
import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3225
|
-
function Page(props) {
|
|
3226
|
-
validateCompoundChildren("Page", props.children);
|
|
3227
|
-
const { track } = useLessonkit();
|
|
3228
|
-
const lessonId = useEnclosingLessonId();
|
|
3229
|
-
useEffect13(() => {
|
|
3230
|
-
if (props.hidden || !lessonId || props.parentType) return;
|
|
3231
|
-
track(
|
|
3232
|
-
"compound_page_viewed",
|
|
3233
|
-
{
|
|
3234
|
-
blockId: props.blockId,
|
|
3235
|
-
pageIndex: props.pageIndex ?? 0,
|
|
3236
|
-
parentType: props.parentType
|
|
3237
|
-
},
|
|
3238
|
-
{ lessonId }
|
|
3239
|
-
);
|
|
3240
|
-
}, [props.hidden, props.pageIndex, props.parentType, props.blockId, lessonId, track]);
|
|
3241
|
-
return /* @__PURE__ */ jsxs10(
|
|
3242
|
-
"section",
|
|
3243
|
-
{
|
|
3244
|
-
"aria-label": props.title ?? "Page",
|
|
3245
|
-
"data-lk-block-id": props.blockId,
|
|
3246
|
-
"data-testid": `page-${props.blockId}`,
|
|
3247
|
-
hidden: props.hidden ? true : void 0,
|
|
3248
|
-
children: [
|
|
3249
|
-
props.title ? /* @__PURE__ */ jsx17("h3", { children: props.title }) : null,
|
|
3250
|
-
/* @__PURE__ */ jsx17(CompoundPageIndexProvider, { pageIndex: props.pageIndex ?? 0, children: /* @__PURE__ */ jsx17("div", { children: props.children }) })
|
|
3251
|
-
]
|
|
3252
|
-
}
|
|
3253
|
-
);
|
|
3254
|
-
}
|
|
3255
|
-
setLessonkitBlockType(Page, "Page");
|
|
3256
|
-
|
|
3257
|
-
// src/blocks/InteractiveBook.tsx
|
|
3258
|
-
import React19, { forwardRef as forwardRef8, useCallback as useCallback8, useEffect as useEffect14, useMemo as useMemo14, useState as useState11 } from "react";
|
|
3259
|
-
import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3260
|
-
var InteractiveBookInner = forwardRef8(
|
|
3261
|
-
function InteractiveBookInner2(props, ref) {
|
|
3262
|
-
const { blockId, pages, index, setIndex, persistEnabled } = props;
|
|
3263
|
-
validateCompoundChildren("InteractiveBook", pages);
|
|
3264
|
-
const { config, track } = useLessonkit();
|
|
3265
|
-
const lessonId = useEnclosingLessonId();
|
|
3266
|
-
const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
|
|
3267
|
-
courseId: config.courseId,
|
|
3268
|
-
compoundId: blockId,
|
|
3269
|
-
pageCount: pages.length,
|
|
3270
|
-
index,
|
|
3271
|
-
setIndex,
|
|
3272
|
-
persistEnabled,
|
|
3273
|
-
ref
|
|
3274
|
-
});
|
|
3275
|
-
const pageTitles = useMemo14(
|
|
3276
|
-
() => pages.map((page) => page.props.title),
|
|
3277
|
-
[pages]
|
|
3278
|
-
);
|
|
3279
|
-
useEffect14(() => {
|
|
3280
|
-
if (!lessonId || pages.length === 0) return;
|
|
3281
|
-
track(
|
|
3282
|
-
"book_page_viewed",
|
|
3283
|
-
{
|
|
3284
|
-
blockId,
|
|
3285
|
-
pageIndex: visibleIndex,
|
|
3286
|
-
pageTitle: pageTitles[visibleIndex]
|
|
3287
|
-
},
|
|
3288
|
-
{ lessonId }
|
|
3289
|
-
);
|
|
3290
|
-
}, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
|
|
3291
|
-
return /* @__PURE__ */ jsxs11("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
|
|
3292
|
-
/* @__PURE__ */ jsx18("h3", { children: props.title }),
|
|
3293
|
-
/* @__PURE__ */ jsxs11("p", { children: [
|
|
3294
|
-
"Page ",
|
|
3295
|
-
progress.current,
|
|
3296
|
-
" of ",
|
|
3297
|
-
progress.total
|
|
3298
|
-
] }),
|
|
3299
|
-
props.showBookScore && ctx ? /* @__PURE__ */ jsxs11("p", { "data-testid": "book-score", children: [
|
|
3300
|
-
"Score: ",
|
|
3301
|
-
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
3302
|
-
" /",
|
|
3303
|
-
" ",
|
|
3304
|
-
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
3305
|
-
] }) : null,
|
|
3306
|
-
/* @__PURE__ */ jsx18("div", { "data-testid": "interactive-book-page", children: pages.map(
|
|
3307
|
-
(page, i) => React19.cloneElement(page, {
|
|
3308
|
-
key: page.key ?? page.props.blockId,
|
|
3309
|
-
hidden: i !== visibleIndex,
|
|
3310
|
-
pageIndex: i,
|
|
3311
|
-
parentType: "InteractiveBook"
|
|
3312
|
-
})
|
|
3313
|
-
) }),
|
|
3314
|
-
/* @__PURE__ */ jsxs11("nav", { "aria-label": "Book navigation", children: [
|
|
3315
|
-
/* @__PURE__ */ jsx18(
|
|
3316
|
-
"button",
|
|
3317
|
-
{
|
|
3318
|
-
type: "button",
|
|
3319
|
-
"data-testid": "book-prev",
|
|
3320
|
-
disabled: visibleIndex === 0 || pages.length === 0,
|
|
3321
|
-
onClick: goPrev,
|
|
3322
|
-
children: "Previous"
|
|
3323
|
-
}
|
|
3324
|
-
),
|
|
3325
|
-
/* @__PURE__ */ jsx18(
|
|
3326
|
-
"button",
|
|
3327
|
-
{
|
|
3328
|
-
type: "button",
|
|
3329
|
-
"data-testid": "book-next",
|
|
3330
|
-
disabled: visibleIndex >= pages.length - 1 || pages.length === 0,
|
|
3331
|
-
onClick: goNext,
|
|
3332
|
-
children: "Next"
|
|
3333
|
-
}
|
|
3334
|
-
)
|
|
3335
|
-
] })
|
|
3336
|
-
] });
|
|
3337
|
-
}
|
|
3338
|
-
);
|
|
3339
|
-
var InteractiveBook = forwardRef8(function InteractiveBook2(props, ref) {
|
|
3340
|
-
const blockId = useMemo14(
|
|
3341
|
-
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3342
|
-
[props.blockId]
|
|
3343
|
-
);
|
|
3344
|
-
const pages = React19.Children.toArray(props.children).filter(
|
|
3345
|
-
React19.isValidElement
|
|
3346
|
-
);
|
|
3347
|
-
const { config, storage } = useLessonkit();
|
|
3348
|
-
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
3349
|
-
const initialIndex = useCompoundInitialIndex({
|
|
3350
|
-
courseId: config.courseId,
|
|
3351
|
-
compoundId: blockId,
|
|
3352
|
-
pageCount: pages.length,
|
|
3353
|
-
persistEnabled,
|
|
3354
|
-
storage
|
|
3355
|
-
});
|
|
3356
|
-
const [index, setIndex] = useState11(initialIndex);
|
|
3357
|
-
const setIndexStable = useCallback8((i) => setIndex(i), []);
|
|
3358
|
-
useEffect14(() => {
|
|
3359
|
-
setIndex(initialIndex);
|
|
3360
|
-
}, [config.courseId, blockId, initialIndex]);
|
|
3361
|
-
return /* @__PURE__ */ jsx18(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx18(
|
|
3362
|
-
InteractiveBookInner,
|
|
3363
|
-
{
|
|
3364
|
-
...props,
|
|
3365
|
-
ref,
|
|
3366
|
-
blockId,
|
|
3367
|
-
pages,
|
|
3368
|
-
index,
|
|
3369
|
-
setIndex,
|
|
3370
|
-
persistEnabled
|
|
3371
|
-
}
|
|
3372
|
-
) });
|
|
3373
|
-
});
|
|
3374
|
-
setLessonkitBlockType(InteractiveBook, "InteractiveBook");
|
|
3375
|
-
|
|
3376
|
-
// src/blocks/Slide.tsx
|
|
3377
|
-
import { useEffect as useEffect15 } from "react";
|
|
3378
|
-
import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3379
|
-
function Slide(props) {
|
|
3380
|
-
validateCompoundChildren("Slide", props.children);
|
|
3381
|
-
const { track } = useLessonkit();
|
|
3382
|
-
const lessonId = useEnclosingLessonId();
|
|
3383
|
-
useEffect15(() => {
|
|
3384
|
-
if (props.hidden || !lessonId || props.parentType) return;
|
|
3385
|
-
track(
|
|
3386
|
-
"compound_page_viewed",
|
|
3387
|
-
{
|
|
3388
|
-
blockId: props.blockId,
|
|
3389
|
-
pageIndex: props.slideIndex ?? 0,
|
|
3390
|
-
parentType: props.parentType
|
|
3391
|
-
},
|
|
3392
|
-
{ lessonId }
|
|
3393
|
-
);
|
|
3394
|
-
}, [props.hidden, props.slideIndex, props.parentType, props.blockId, lessonId, track]);
|
|
3395
|
-
return /* @__PURE__ */ jsxs12(
|
|
3396
|
-
"section",
|
|
3397
|
-
{
|
|
3398
|
-
"aria-label": props.title ?? "Slide",
|
|
3399
|
-
"data-lk-block-id": props.blockId,
|
|
3400
|
-
"data-testid": `slide-${props.blockId}`,
|
|
3401
|
-
hidden: props.hidden ? true : void 0,
|
|
3402
|
-
children: [
|
|
3403
|
-
props.title ? /* @__PURE__ */ jsx19("h3", { children: props.title }) : null,
|
|
3404
|
-
/* @__PURE__ */ jsx19(CompoundPageIndexProvider, { pageIndex: props.slideIndex ?? 0, children: /* @__PURE__ */ jsx19("div", { children: props.children }) })
|
|
3405
|
-
]
|
|
3406
|
-
}
|
|
65
|
+
[props.config, courseId]
|
|
3407
66
|
);
|
|
67
|
+
return /* @__PURE__ */ jsx(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs("section", { "aria-label": props.title, children: [
|
|
68
|
+
/* @__PURE__ */ jsx("h1", { children: props.title }),
|
|
69
|
+
/* @__PURE__ */ jsx("div", { children: props.children })
|
|
70
|
+
] }) });
|
|
3408
71
|
}
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
}
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
const onKeyDown = (event) => {
|
|
3432
|
-
if (!el.contains(document.activeElement) && document.activeElement !== document.body) {
|
|
72
|
+
function Lesson(props) {
|
|
73
|
+
const lessonId = useMemo(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
74
|
+
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
75
|
+
const { setActiveLesson, config } = useLessonkit();
|
|
76
|
+
const { completeLesson } = useCompletion();
|
|
77
|
+
const lessonMountGenerationRef = useRef(0);
|
|
78
|
+
const liveCourseIdRef = useRef(config.courseId);
|
|
79
|
+
liveCourseIdRef.current = config.courseId;
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const unregister = registerLessonMount(lessonId);
|
|
82
|
+
const generation = ++lessonMountGenerationRef.current;
|
|
83
|
+
const mountedCourseId = config.courseId;
|
|
84
|
+
let effectSurvivedTick = false;
|
|
85
|
+
queueMicrotask(() => {
|
|
86
|
+
queueMicrotask(() => {
|
|
87
|
+
effectSurvivedTick = true;
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
setActiveLesson(lessonId);
|
|
91
|
+
return () => {
|
|
92
|
+
unregister();
|
|
93
|
+
if (getLessonMountCount(lessonId) > 0) {
|
|
3433
94
|
return;
|
|
3434
95
|
}
|
|
3435
|
-
if (
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
}
|
|
3443
|
-
break;
|
|
3444
|
-
case "ArrowLeft":
|
|
3445
|
-
case "ArrowUp":
|
|
3446
|
-
if (visibleIndex > 0) {
|
|
3447
|
-
event.preventDefault();
|
|
3448
|
-
goPrev();
|
|
3449
|
-
}
|
|
3450
|
-
break;
|
|
3451
|
-
case "Home":
|
|
3452
|
-
if (visibleIndex !== 0) {
|
|
3453
|
-
event.preventDefault();
|
|
3454
|
-
setIndex(0);
|
|
3455
|
-
}
|
|
3456
|
-
break;
|
|
3457
|
-
case "End":
|
|
3458
|
-
if (visibleIndex !== pageCount - 1) {
|
|
3459
|
-
event.preventDefault();
|
|
3460
|
-
setIndex(pageCount - 1);
|
|
3461
|
-
}
|
|
3462
|
-
break;
|
|
3463
|
-
default:
|
|
3464
|
-
break;
|
|
3465
|
-
}
|
|
96
|
+
if (!autoComplete) return;
|
|
97
|
+
queueMicrotask(() => {
|
|
98
|
+
if (!effectSurvivedTick) return;
|
|
99
|
+
if (lessonMountGenerationRef.current !== generation) return;
|
|
100
|
+
if (liveCourseIdRef.current !== mountedCourseId) return;
|
|
101
|
+
completeLesson(lessonId, { courseId: mountedCourseId });
|
|
102
|
+
});
|
|
3466
103
|
};
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
104
|
+
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
105
|
+
return /* @__PURE__ */ jsx(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs("article", { "aria-label": props.title, children: [
|
|
106
|
+
/* @__PURE__ */ jsx("h2", { children: props.title }),
|
|
107
|
+
/* @__PURE__ */ jsx("div", { children: props.children })
|
|
108
|
+
] }) });
|
|
3470
109
|
}
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
var SlideDeckInner = forwardRef9(function SlideDeckInner2(props, ref) {
|
|
3475
|
-
const { blockId, slides, index, setIndex, persistEnabled } = props;
|
|
3476
|
-
validateCompoundChildren("SlideDeck", slides);
|
|
3477
|
-
const { config, track } = useLessonkit();
|
|
3478
|
-
const lessonId = useEnclosingLessonId();
|
|
3479
|
-
const containerRef = useRef14(null);
|
|
3480
|
-
const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
|
|
3481
|
-
courseId: config.courseId,
|
|
3482
|
-
compoundId: blockId,
|
|
3483
|
-
pageCount: slides.length,
|
|
3484
|
-
index,
|
|
3485
|
-
setIndex,
|
|
3486
|
-
persistEnabled,
|
|
3487
|
-
ref
|
|
3488
|
-
});
|
|
3489
|
-
const setIndexStable = useCallback9((i) => setIndex(i), [setIndex]);
|
|
3490
|
-
useCompoundKeyboardNav({
|
|
3491
|
-
containerRef,
|
|
3492
|
-
visibleIndex,
|
|
3493
|
-
pageCount: slides.length,
|
|
3494
|
-
goNext,
|
|
3495
|
-
goPrev,
|
|
3496
|
-
setIndex: setIndexStable
|
|
3497
|
-
});
|
|
3498
|
-
const slideTitles = useMemo15(
|
|
3499
|
-
() => slides.map((slide) => slide.props.title),
|
|
3500
|
-
[slides]
|
|
3501
|
-
);
|
|
3502
|
-
useEffect17(() => {
|
|
3503
|
-
if (!lessonId || slides.length === 0) return;
|
|
3504
|
-
track(
|
|
3505
|
-
"slide_viewed",
|
|
3506
|
-
{
|
|
3507
|
-
blockId,
|
|
3508
|
-
slideIndex: visibleIndex,
|
|
3509
|
-
slideTitle: slideTitles[visibleIndex]
|
|
3510
|
-
},
|
|
3511
|
-
{ lessonId }
|
|
3512
|
-
);
|
|
3513
|
-
}, [visibleIndex, blockId, lessonId, slides.length, slideTitles, track]);
|
|
3514
|
-
return /* @__PURE__ */ jsxs13(
|
|
3515
|
-
"section",
|
|
3516
|
-
{
|
|
3517
|
-
ref: containerRef,
|
|
3518
|
-
tabIndex: -1,
|
|
3519
|
-
"aria-label": props.title,
|
|
3520
|
-
"data-testid": "slide-deck",
|
|
3521
|
-
"data-lk-block-id": blockId,
|
|
3522
|
-
children: [
|
|
3523
|
-
/* @__PURE__ */ jsx20("h3", { children: props.title }),
|
|
3524
|
-
/* @__PURE__ */ jsxs13("p", { children: [
|
|
3525
|
-
"Slide ",
|
|
3526
|
-
progress.current,
|
|
3527
|
-
" of ",
|
|
3528
|
-
progress.total
|
|
3529
|
-
] }),
|
|
3530
|
-
props.showDeckScore && ctx ? /* @__PURE__ */ jsxs13("p", { "data-testid": "deck-score", children: [
|
|
3531
|
-
"Score: ",
|
|
3532
|
-
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
3533
|
-
" /",
|
|
3534
|
-
" ",
|
|
3535
|
-
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
3536
|
-
] }) : null,
|
|
3537
|
-
/* @__PURE__ */ jsx20("div", { "data-testid": "slide-deck-slide", children: slides.map(
|
|
3538
|
-
(slide, i) => React21.cloneElement(slide, {
|
|
3539
|
-
key: slide.key ?? slide.props.blockId,
|
|
3540
|
-
hidden: i !== visibleIndex,
|
|
3541
|
-
slideIndex: i,
|
|
3542
|
-
parentType: "SlideDeck"
|
|
3543
|
-
})
|
|
3544
|
-
) }),
|
|
3545
|
-
/* @__PURE__ */ jsxs13("nav", { "aria-label": "Slide navigation", children: [
|
|
3546
|
-
/* @__PURE__ */ jsx20(
|
|
3547
|
-
"button",
|
|
3548
|
-
{
|
|
3549
|
-
type: "button",
|
|
3550
|
-
"data-testid": "slide-prev",
|
|
3551
|
-
disabled: visibleIndex === 0 || slides.length === 0,
|
|
3552
|
-
onClick: goPrev,
|
|
3553
|
-
children: "Previous slide"
|
|
3554
|
-
}
|
|
3555
|
-
),
|
|
3556
|
-
/* @__PURE__ */ jsx20(
|
|
3557
|
-
"button",
|
|
3558
|
-
{
|
|
3559
|
-
type: "button",
|
|
3560
|
-
"data-testid": "slide-next",
|
|
3561
|
-
disabled: visibleIndex >= slides.length - 1 || slides.length === 0,
|
|
3562
|
-
onClick: goNext,
|
|
3563
|
-
children: "Next slide"
|
|
3564
|
-
}
|
|
3565
|
-
)
|
|
3566
|
-
] })
|
|
3567
|
-
]
|
|
3568
|
-
}
|
|
3569
|
-
);
|
|
3570
|
-
});
|
|
3571
|
-
var SlideDeck = forwardRef9(function SlideDeck2(props, ref) {
|
|
3572
|
-
const blockId = useMemo15(
|
|
3573
|
-
() => normalizeComponentId(props.blockId, "blockId"),
|
|
110
|
+
function Scenario(props) {
|
|
111
|
+
const blockId = useMemo(
|
|
112
|
+
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
3574
113
|
[props.blockId]
|
|
3575
114
|
);
|
|
3576
|
-
|
|
3577
|
-
React21.isValidElement
|
|
3578
|
-
);
|
|
3579
|
-
const { config, storage } = useLessonkit();
|
|
3580
|
-
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
3581
|
-
const initialIndex = useCompoundInitialIndex({
|
|
3582
|
-
courseId: config.courseId,
|
|
3583
|
-
compoundId: blockId,
|
|
3584
|
-
pageCount: slides.length,
|
|
3585
|
-
persistEnabled,
|
|
3586
|
-
storage
|
|
3587
|
-
});
|
|
3588
|
-
const [index, setIndex] = useState12(initialIndex);
|
|
3589
|
-
const setIndexStable = useCallback9((i) => setIndex(i), []);
|
|
3590
|
-
useEffect17(() => {
|
|
3591
|
-
setIndex(initialIndex);
|
|
3592
|
-
}, [config.courseId, blockId, initialIndex]);
|
|
3593
|
-
return /* @__PURE__ */ jsx20(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx20(
|
|
3594
|
-
SlideDeckInner,
|
|
3595
|
-
{
|
|
3596
|
-
...props,
|
|
3597
|
-
ref,
|
|
3598
|
-
blockId,
|
|
3599
|
-
slides,
|
|
3600
|
-
index,
|
|
3601
|
-
setIndex,
|
|
3602
|
-
persistEnabled
|
|
3603
|
-
}
|
|
3604
|
-
) });
|
|
3605
|
-
});
|
|
3606
|
-
setLessonkitBlockType(SlideDeck, "SlideDeck");
|
|
3607
|
-
|
|
3608
|
-
// src/blocks/Accordion.tsx
|
|
3609
|
-
import { useId as useId4, useState as useState13 } from "react";
|
|
3610
|
-
import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
3611
|
-
function Accordion(props) {
|
|
3612
|
-
if (isDevEnvironment4()) {
|
|
3613
|
-
validateAccordionSections(props.sections);
|
|
3614
|
-
}
|
|
3615
|
-
const [open, setOpen] = useState13(/* @__PURE__ */ new Set());
|
|
3616
|
-
const { track } = useLessonkit();
|
|
3617
|
-
const lessonId = useEnclosingLessonId();
|
|
3618
|
-
const baseId = useId4();
|
|
3619
|
-
const toggle = (sectionId) => {
|
|
3620
|
-
setOpen((prev) => {
|
|
3621
|
-
const next = new Set(prev);
|
|
3622
|
-
const expanded = !next.has(sectionId);
|
|
3623
|
-
if (expanded) next.add(sectionId);
|
|
3624
|
-
else next.delete(sectionId);
|
|
3625
|
-
track(
|
|
3626
|
-
"accordion_section_toggled",
|
|
3627
|
-
{ blockId: props.blockId, sectionId, expanded },
|
|
3628
|
-
lessonId ? { lessonId } : void 0
|
|
3629
|
-
);
|
|
3630
|
-
return next;
|
|
3631
|
-
});
|
|
3632
|
-
};
|
|
3633
|
-
return /* @__PURE__ */ jsx21("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
|
|
3634
|
-
const expanded = open.has(section.id);
|
|
3635
|
-
const panelId = `${baseId}-${section.id}`;
|
|
3636
|
-
const triggerId = `${baseId}-trigger-${section.id}`;
|
|
3637
|
-
return /* @__PURE__ */ jsxs14("div", { "data-testid": `accordion-section-${section.id}`, children: [
|
|
3638
|
-
/* @__PURE__ */ jsx21("h4", { children: /* @__PURE__ */ jsx21(
|
|
3639
|
-
"button",
|
|
3640
|
-
{
|
|
3641
|
-
id: triggerId,
|
|
3642
|
-
type: "button",
|
|
3643
|
-
"aria-expanded": expanded,
|
|
3644
|
-
"aria-controls": panelId,
|
|
3645
|
-
"data-testid": `accordion-trigger-${section.id}`,
|
|
3646
|
-
onClick: () => toggle(section.id),
|
|
3647
|
-
children: section.title
|
|
3648
|
-
}
|
|
3649
|
-
) }),
|
|
3650
|
-
expanded ? /* @__PURE__ */ jsx21("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
|
|
3651
|
-
] }, section.id);
|
|
3652
|
-
}) });
|
|
3653
|
-
}
|
|
3654
|
-
setLessonkitBlockType(Accordion, "Accordion");
|
|
3655
|
-
|
|
3656
|
-
// src/blocks/DialogCards.tsx
|
|
3657
|
-
import { useState as useState14 } from "react";
|
|
3658
|
-
import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
3659
|
-
function DialogCards(props) {
|
|
3660
|
-
const [index, setIndex] = useState14(0);
|
|
3661
|
-
const [flipped, setFlipped] = useState14(false);
|
|
3662
|
-
const card = props.cards[index];
|
|
3663
|
-
if (!card) return null;
|
|
3664
|
-
return /* @__PURE__ */ jsxs15("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
|
|
3665
|
-
/* @__PURE__ */ jsxs15("p", { children: [
|
|
3666
|
-
"Card ",
|
|
3667
|
-
index + 1,
|
|
3668
|
-
" of ",
|
|
3669
|
-
props.cards.length
|
|
3670
|
-
] }),
|
|
3671
|
-
/* @__PURE__ */ jsx22(
|
|
3672
|
-
"button",
|
|
3673
|
-
{
|
|
3674
|
-
type: "button",
|
|
3675
|
-
"data-testid": "dialog-card-flip",
|
|
3676
|
-
"aria-pressed": flipped,
|
|
3677
|
-
onClick: () => setFlipped((f) => !f),
|
|
3678
|
-
style: { minHeight: "6rem", width: "100%" },
|
|
3679
|
-
children: flipped ? card.back : card.front
|
|
3680
|
-
}
|
|
3681
|
-
),
|
|
3682
|
-
/* @__PURE__ */ jsxs15("nav", { "aria-label": "Card navigation", children: [
|
|
3683
|
-
/* @__PURE__ */ jsx22(
|
|
3684
|
-
"button",
|
|
3685
|
-
{
|
|
3686
|
-
type: "button",
|
|
3687
|
-
"data-testid": "dialog-prev",
|
|
3688
|
-
disabled: index === 0,
|
|
3689
|
-
onClick: () => {
|
|
3690
|
-
setIndex((i) => i - 1);
|
|
3691
|
-
setFlipped(false);
|
|
3692
|
-
},
|
|
3693
|
-
children: "Previous"
|
|
3694
|
-
}
|
|
3695
|
-
),
|
|
3696
|
-
/* @__PURE__ */ jsx22(
|
|
3697
|
-
"button",
|
|
3698
|
-
{
|
|
3699
|
-
type: "button",
|
|
3700
|
-
"data-testid": "dialog-next",
|
|
3701
|
-
disabled: index >= props.cards.length - 1,
|
|
3702
|
-
onClick: () => {
|
|
3703
|
-
setIndex((i) => i + 1);
|
|
3704
|
-
setFlipped(false);
|
|
3705
|
-
},
|
|
3706
|
-
children: "Next"
|
|
3707
|
-
}
|
|
3708
|
-
)
|
|
3709
|
-
] })
|
|
3710
|
-
] });
|
|
115
|
+
return /* @__PURE__ */ jsx("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
3711
116
|
}
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
const
|
|
3719
|
-
const [
|
|
3720
|
-
const
|
|
3721
|
-
const
|
|
3722
|
-
const
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
const next = face === "front" ? "back" : "front";
|
|
3726
|
-
setFace(next);
|
|
3727
|
-
track(
|
|
3728
|
-
"flashcard_flipped",
|
|
3729
|
-
{ blockId: props.blockId, cardIndex: index, face: next },
|
|
3730
|
-
lessonId ? { lessonId } : void 0
|
|
3731
|
-
);
|
|
117
|
+
function Reflection(props) {
|
|
118
|
+
const blockId = useMemo(
|
|
119
|
+
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
120
|
+
[props.blockId]
|
|
121
|
+
);
|
|
122
|
+
const promptId = useId();
|
|
123
|
+
const hintId = useId();
|
|
124
|
+
const [internalValue, setInternalValue] = useState("");
|
|
125
|
+
const isControlled = props.value !== void 0;
|
|
126
|
+
const value = isControlled ? props.value : internalValue;
|
|
127
|
+
const handleChange = (event) => {
|
|
128
|
+
if (!isControlled) setInternalValue(event.target.value);
|
|
129
|
+
props.onChange?.(event.target.value);
|
|
3732
130
|
};
|
|
3733
|
-
return /* @__PURE__ */
|
|
3734
|
-
/* @__PURE__ */
|
|
3735
|
-
props.
|
|
3736
|
-
|
|
3737
|
-
|
|
131
|
+
return /* @__PURE__ */ jsxs("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
|
|
132
|
+
props.prompt ? /* @__PURE__ */ jsx("p", { id: promptId, children: props.prompt }) : null,
|
|
133
|
+
props.hint ? /* @__PURE__ */ jsx("p", { id: hintId, style: visuallyHiddenStyle, children: props.hint }) : null,
|
|
134
|
+
props.children,
|
|
135
|
+
/* @__PURE__ */ jsx(
|
|
136
|
+
"textarea",
|
|
3738
137
|
{
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
setFace("front");
|
|
3745
|
-
},
|
|
3746
|
-
children: "Next card"
|
|
138
|
+
value,
|
|
139
|
+
onChange: handleChange,
|
|
140
|
+
"aria-labelledby": props.prompt ? promptId : void 0,
|
|
141
|
+
"aria-describedby": props.hint ? hintId : void 0,
|
|
142
|
+
"aria-label": props.prompt ? void 0 : "Reflection response"
|
|
3747
143
|
}
|
|
3748
144
|
)
|
|
3749
145
|
] });
|
|
3750
146
|
}
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
props.hotspots.map((h) => /* @__PURE__ */ jsx24(
|
|
3772
|
-
"button",
|
|
3773
|
-
{
|
|
3774
|
-
type: "button",
|
|
3775
|
-
"aria-expanded": active === h.id,
|
|
3776
|
-
"aria-label": h.label,
|
|
3777
|
-
"data-testid": `hotspot-${h.id}`,
|
|
3778
|
-
style: {
|
|
3779
|
-
position: "absolute",
|
|
3780
|
-
left: `${h.x}%`,
|
|
3781
|
-
top: `${h.y}%`,
|
|
3782
|
-
transform: "translate(-50%, -50%)"
|
|
3783
|
-
},
|
|
3784
|
-
onClick: () => open(h.id),
|
|
3785
|
-
children: "+"
|
|
3786
|
-
},
|
|
3787
|
-
h.id
|
|
3788
|
-
))
|
|
3789
|
-
] }),
|
|
3790
|
-
active ? /* @__PURE__ */ jsxs17("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
|
|
3791
|
-
props.hotspots.find((h) => h.id === active)?.content,
|
|
3792
|
-
/* @__PURE__ */ jsx24("button", { type: "button", onClick: () => setActive(null), children: "Close" })
|
|
3793
|
-
] }) : null
|
|
3794
|
-
] });
|
|
3795
|
-
}
|
|
3796
|
-
setLessonkitBlockType(ImageHotspots, "ImageHotspots");
|
|
3797
|
-
|
|
3798
|
-
// src/blocks/ImageSlider.tsx
|
|
3799
|
-
import { useState as useState17 } from "react";
|
|
3800
|
-
import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
3801
|
-
function ImageSlider(props) {
|
|
3802
|
-
const [index, setIndex] = useState17(0);
|
|
3803
|
-
const { track } = useLessonkit();
|
|
3804
|
-
const lessonId = useEnclosingLessonId();
|
|
3805
|
-
const slide = props.slides[index];
|
|
3806
|
-
if (!slide) return null;
|
|
3807
|
-
const goTo = (next) => {
|
|
3808
|
-
setIndex(next);
|
|
3809
|
-
track(
|
|
3810
|
-
"image_slider_changed",
|
|
3811
|
-
{ blockId: props.blockId, slideIndex: next },
|
|
3812
|
-
lessonId ? { lessonId } : void 0
|
|
3813
|
-
);
|
|
3814
|
-
};
|
|
3815
|
-
return /* @__PURE__ */ jsxs18("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
|
|
3816
|
-
/* @__PURE__ */ jsx25("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
|
|
3817
|
-
slide.caption ? /* @__PURE__ */ jsx25("p", { children: slide.caption }) : null,
|
|
3818
|
-
/* @__PURE__ */ jsxs18("nav", { "aria-label": "Slide navigation", children: [
|
|
3819
|
-
/* @__PURE__ */ jsx25(
|
|
3820
|
-
"button",
|
|
3821
|
-
{
|
|
3822
|
-
type: "button",
|
|
3823
|
-
"data-testid": "slider-prev",
|
|
3824
|
-
disabled: index === 0,
|
|
3825
|
-
onClick: () => goTo(index - 1),
|
|
3826
|
-
children: "Previous"
|
|
3827
|
-
}
|
|
3828
|
-
),
|
|
3829
|
-
/* @__PURE__ */ jsxs18("span", { children: [
|
|
3830
|
-
index + 1,
|
|
3831
|
-
" / ",
|
|
3832
|
-
props.slides.length
|
|
3833
|
-
] }),
|
|
3834
|
-
/* @__PURE__ */ jsx25(
|
|
3835
|
-
"button",
|
|
3836
|
-
{
|
|
3837
|
-
type: "button",
|
|
3838
|
-
"data-testid": "slider-next",
|
|
3839
|
-
disabled: index >= props.slides.length - 1,
|
|
3840
|
-
onClick: () => goTo(index + 1),
|
|
3841
|
-
children: "Next"
|
|
3842
|
-
}
|
|
3843
|
-
)
|
|
3844
|
-
] })
|
|
3845
|
-
] });
|
|
3846
|
-
}
|
|
3847
|
-
setLessonkitBlockType(ImageSlider, "ImageSlider");
|
|
3848
|
-
|
|
3849
|
-
// src/blocks/FindHotspot.tsx
|
|
3850
|
-
import { forwardRef as forwardRef10, useEffect as useEffect18, useMemo as useMemo16, useState as useState18 } from "react";
|
|
3851
|
-
import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
3852
|
-
var INTERACTION6 = "findHotspot";
|
|
3853
|
-
function FindHotspotInner(props, ref) {
|
|
3854
|
-
const checkId = useMemo16(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
3855
|
-
const [selected, setSelected] = useState18(null);
|
|
3856
|
-
const [checked, setChecked] = useState18(false);
|
|
3857
|
-
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
3858
|
-
const targetIdsKey = props.targets.map((t) => t.id).join("\0");
|
|
3859
|
-
useEffect18(() => {
|
|
3860
|
-
setSelected(null);
|
|
3861
|
-
setChecked(false);
|
|
3862
|
-
}, [checkId, props.correctTargetId, targetIdsKey]);
|
|
3863
|
-
const correct = selected === props.correctTargetId;
|
|
3864
|
-
const handle = useMemo16(
|
|
3865
|
-
() => buildAssessmentHandle({
|
|
3866
|
-
checkId,
|
|
3867
|
-
getScore: () => checked && correct ? 1 : 0,
|
|
3868
|
-
getMaxScore: () => 1,
|
|
3869
|
-
getAnswerGiven: () => selected !== null,
|
|
3870
|
-
resetTask: () => {
|
|
3871
|
-
setSelected(null);
|
|
3872
|
-
setChecked(false);
|
|
3873
|
-
},
|
|
3874
|
-
showSolutions: () => setSelected(props.correctTargetId),
|
|
3875
|
-
getXAPIData: () => ({
|
|
3876
|
-
checkId,
|
|
3877
|
-
interactionType: INTERACTION6,
|
|
3878
|
-
response: selected ?? void 0,
|
|
3879
|
-
correct: checked ? correct : void 0,
|
|
3880
|
-
score: checked && correct ? 1 : 0,
|
|
3881
|
-
maxScore: 1
|
|
3882
|
-
}),
|
|
3883
|
-
getCurrentState: () => ({ selected, checked }),
|
|
3884
|
-
resume: (state) => {
|
|
3885
|
-
const nextSelected = readStringField(state, "selected");
|
|
3886
|
-
if (typeof nextSelected === "string" || nextSelected === null) {
|
|
3887
|
-
const valid = nextSelected === null || props.targets.some((t) => t.id === nextSelected);
|
|
3888
|
-
setSelected(valid ? nextSelected : null);
|
|
3889
|
-
}
|
|
3890
|
-
readBooleanStateField(state, "checked", setChecked);
|
|
3891
|
-
}
|
|
3892
|
-
}),
|
|
3893
|
-
[checkId, selected, checked, correct, props.correctTargetId, props.targets]
|
|
3894
|
-
);
|
|
3895
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3896
|
-
const selectTarget = (id) => {
|
|
3897
|
-
setSelected(id);
|
|
3898
|
-
setChecked(false);
|
|
3899
|
-
};
|
|
3900
|
-
const submit = () => {
|
|
3901
|
-
if (!selected || checked) return;
|
|
3902
|
-
setChecked(true);
|
|
3903
|
-
assessment.answer({
|
|
3904
|
-
checkId,
|
|
3905
|
-
interactionType: INTERACTION6,
|
|
3906
|
-
response: selected,
|
|
3907
|
-
correct
|
|
3908
|
-
});
|
|
3909
|
-
if (correct) {
|
|
3910
|
-
assessment.complete({
|
|
3911
|
-
checkId,
|
|
3912
|
-
interactionType: INTERACTION6,
|
|
3913
|
-
score: 1,
|
|
3914
|
-
maxScore: 1,
|
|
3915
|
-
passingScore: props.passingScore ?? 1
|
|
3916
|
-
});
|
|
3917
|
-
}
|
|
3918
|
-
};
|
|
3919
|
-
return /* @__PURE__ */ jsxs19("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
|
|
3920
|
-
/* @__PURE__ */ jsxs19("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
3921
|
-
/* @__PURE__ */ jsx26("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
3922
|
-
props.targets.map((t) => /* @__PURE__ */ jsx26(
|
|
3923
|
-
"button",
|
|
3924
|
-
{
|
|
3925
|
-
type: "button",
|
|
3926
|
-
"aria-label": t.label,
|
|
3927
|
-
"aria-pressed": selected === t.id,
|
|
3928
|
-
"data-testid": `target-${t.id}`,
|
|
3929
|
-
style: {
|
|
3930
|
-
position: "absolute",
|
|
3931
|
-
left: `${t.x}%`,
|
|
3932
|
-
top: `${t.y}%`,
|
|
3933
|
-
transform: "translate(-50%, -50%)"
|
|
3934
|
-
},
|
|
3935
|
-
onClick: () => selectTarget(t.id),
|
|
3936
|
-
children: t.label
|
|
3937
|
-
},
|
|
3938
|
-
t.id
|
|
3939
|
-
))
|
|
3940
|
-
] }),
|
|
3941
|
-
/* @__PURE__ */ jsx26("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
|
|
3942
|
-
checked ? /* @__PURE__ */ jsx26("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
3943
|
-
] });
|
|
3944
|
-
}
|
|
3945
|
-
var FindHotspotInnerForwarded = forwardRef10(FindHotspotInner);
|
|
3946
|
-
var FindHotspot = forwardRef10(function FindHotspot2(props, ref) {
|
|
3947
|
-
return /* @__PURE__ */ jsx26(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx26(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
3948
|
-
});
|
|
3949
|
-
setLessonkitBlockType(FindHotspot, "FindHotspot");
|
|
3950
|
-
|
|
3951
|
-
// src/blocks/FindMultipleHotspots.tsx
|
|
3952
|
-
import { forwardRef as forwardRef11, useMemo as useMemo17, useState as useState19 } from "react";
|
|
3953
|
-
import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
3954
|
-
var INTERACTION7 = "findMultipleHotspots";
|
|
3955
|
-
function FindMultipleHotspotsInner(props, ref) {
|
|
3956
|
-
const checkId = useMemo17(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
3957
|
-
const [selected, setSelected] = useState19(/* @__PURE__ */ new Set());
|
|
3958
|
-
const [checked, setChecked] = useState19(false);
|
|
3959
|
-
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
3960
|
-
const toggle = (id) => {
|
|
3961
|
-
setSelected((prev) => {
|
|
3962
|
-
const next = new Set(prev);
|
|
3963
|
-
if (next.has(id)) next.delete(id);
|
|
3964
|
-
else next.add(id);
|
|
3965
|
-
return next;
|
|
3966
|
-
});
|
|
3967
|
-
setChecked(false);
|
|
3968
|
-
};
|
|
3969
|
-
const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
|
|
3970
|
-
const handle = useMemo17(
|
|
3971
|
-
() => buildAssessmentHandle({
|
|
3972
|
-
checkId,
|
|
3973
|
-
getScore: () => checked && correct ? 1 : 0,
|
|
3974
|
-
getMaxScore: () => 1,
|
|
3975
|
-
getAnswerGiven: () => selected.size > 0,
|
|
3976
|
-
resetTask: () => {
|
|
3977
|
-
setSelected(/* @__PURE__ */ new Set());
|
|
3978
|
-
setChecked(false);
|
|
3979
|
-
},
|
|
3980
|
-
showSolutions: () => setSelected(new Set(props.correctTargetIds)),
|
|
3981
|
-
getXAPIData: () => ({
|
|
3982
|
-
checkId,
|
|
3983
|
-
interactionType: INTERACTION7,
|
|
3984
|
-
response: [...selected],
|
|
3985
|
-
correct: checked ? correct : void 0,
|
|
3986
|
-
score: checked && correct ? 1 : 0,
|
|
3987
|
-
maxScore: 1
|
|
3988
|
-
}),
|
|
3989
|
-
getCurrentState: () => ({ selected: [...selected], checked }),
|
|
3990
|
-
resume: (state) => {
|
|
3991
|
-
const raw = state.selected;
|
|
3992
|
-
if (Array.isArray(raw)) setSelected(new Set(raw.filter((id) => typeof id === "string")));
|
|
3993
|
-
readBooleanStateField(state, "checked", setChecked);
|
|
147
|
+
function ProgressTracker(props) {
|
|
148
|
+
const { progress } = useLessonkit();
|
|
149
|
+
const completed = progress.completedLessonIds.size;
|
|
150
|
+
if (props.totalLessons != null) {
|
|
151
|
+
const total = props.totalLessons;
|
|
152
|
+
const displayed = Math.min(completed, total);
|
|
153
|
+
return /* @__PURE__ */ jsx("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx(
|
|
154
|
+
"div",
|
|
155
|
+
{
|
|
156
|
+
role: "progressbar",
|
|
157
|
+
"aria-valuemin": 0,
|
|
158
|
+
"aria-valuemax": total,
|
|
159
|
+
"aria-valuenow": displayed,
|
|
160
|
+
"aria-label": "Lessons completed",
|
|
161
|
+
children: /* @__PURE__ */ jsxs("p", { children: [
|
|
162
|
+
"Lessons completed: ",
|
|
163
|
+
displayed,
|
|
164
|
+
" of ",
|
|
165
|
+
total
|
|
166
|
+
] })
|
|
3994
167
|
}
|
|
3995
|
-
})
|
|
3996
|
-
[checkId, selected, checked, correct, props.correctTargetIds]
|
|
3997
|
-
);
|
|
3998
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3999
|
-
const submit = () => {
|
|
4000
|
-
if (selected.size === 0 || checked) return;
|
|
4001
|
-
setChecked(true);
|
|
4002
|
-
assessment.answer({
|
|
4003
|
-
checkId,
|
|
4004
|
-
interactionType: INTERACTION7,
|
|
4005
|
-
response: [...selected],
|
|
4006
|
-
correct
|
|
4007
|
-
});
|
|
4008
|
-
if (correct) {
|
|
4009
|
-
assessment.complete({
|
|
4010
|
-
checkId,
|
|
4011
|
-
interactionType: INTERACTION7,
|
|
4012
|
-
score: 1,
|
|
4013
|
-
maxScore: 1,
|
|
4014
|
-
passingScore: props.passingScore ?? 1
|
|
4015
|
-
});
|
|
4016
|
-
}
|
|
4017
|
-
};
|
|
4018
|
-
return /* @__PURE__ */ jsxs20("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
|
|
4019
|
-
/* @__PURE__ */ jsxs20("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
4020
|
-
/* @__PURE__ */ jsx27("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
4021
|
-
props.targets.map((t) => /* @__PURE__ */ jsx27(
|
|
4022
|
-
"button",
|
|
4023
|
-
{
|
|
4024
|
-
type: "button",
|
|
4025
|
-
"aria-label": t.label,
|
|
4026
|
-
"aria-pressed": selected.has(t.id),
|
|
4027
|
-
"data-testid": `target-${t.id}`,
|
|
4028
|
-
style: {
|
|
4029
|
-
position: "absolute",
|
|
4030
|
-
left: `${t.x}%`,
|
|
4031
|
-
top: `${t.y}%`,
|
|
4032
|
-
transform: "translate(-50%, -50%)"
|
|
4033
|
-
},
|
|
4034
|
-
onClick: () => toggle(t.id),
|
|
4035
|
-
children: t.label
|
|
4036
|
-
},
|
|
4037
|
-
t.id
|
|
4038
|
-
))
|
|
4039
|
-
] }),
|
|
4040
|
-
/* @__PURE__ */ jsx27("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
|
|
4041
|
-
checked ? /* @__PURE__ */ jsx27("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
4042
|
-
] });
|
|
4043
|
-
}
|
|
4044
|
-
var FindMultipleHotspotsInnerForwarded = forwardRef11(FindMultipleHotspotsInner);
|
|
4045
|
-
var FindMultipleHotspots = forwardRef11(
|
|
4046
|
-
function FindMultipleHotspots2(props, ref) {
|
|
4047
|
-
return /* @__PURE__ */ jsx27(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx27(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
168
|
+
) });
|
|
4048
169
|
}
|
|
4049
|
-
|
|
4050
|
-
|
|
170
|
+
return /* @__PURE__ */ jsx("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs("p", { children: [
|
|
171
|
+
"Lessons completed: ",
|
|
172
|
+
completed
|
|
173
|
+
] }) });
|
|
174
|
+
}
|
|
4051
175
|
|
|
4052
176
|
// src/index.tsx
|
|
4053
177
|
import {
|
|
4054
|
-
buildTelemetryEvent
|
|
4055
|
-
createLessonkitRuntime
|
|
4056
|
-
createPluginRegistry
|
|
4057
|
-
createTelemetryPipeline
|
|
178
|
+
buildTelemetryEvent,
|
|
179
|
+
createLessonkitRuntime,
|
|
180
|
+
createPluginRegistry,
|
|
181
|
+
createTelemetryPipeline,
|
|
4058
182
|
defineAssessmentPlugin,
|
|
4059
183
|
defineLifecyclePlugin,
|
|
4060
184
|
defineTelemetryPlugin
|
|
4061
185
|
} from "@lessonkit/core";
|
|
4062
186
|
|
|
4063
187
|
// src/theme/ThemeProvider.tsx
|
|
4064
|
-
import
|
|
4065
|
-
createContext
|
|
4066
|
-
useCallback
|
|
4067
|
-
useContext
|
|
4068
|
-
useLayoutEffect
|
|
4069
|
-
useMemo as
|
|
4070
|
-
useRef as
|
|
4071
|
-
useState as
|
|
188
|
+
import React2, {
|
|
189
|
+
createContext,
|
|
190
|
+
useCallback,
|
|
191
|
+
useContext,
|
|
192
|
+
useLayoutEffect,
|
|
193
|
+
useMemo as useMemo2,
|
|
194
|
+
useRef as useRef2,
|
|
195
|
+
useState as useState2
|
|
4072
196
|
} from "react";
|
|
4073
197
|
import {
|
|
4074
198
|
brandThemeOverrides,
|
|
@@ -4095,11 +219,11 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
4095
219
|
}
|
|
4096
220
|
|
|
4097
221
|
// src/theme/ThemeProvider.tsx
|
|
4098
|
-
import { jsx as
|
|
4099
|
-
var ThemeContext =
|
|
4100
|
-
var
|
|
222
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
223
|
+
var ThemeContext = createContext(null);
|
|
224
|
+
var useIsoLayoutEffect = (
|
|
4101
225
|
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
4102
|
-
typeof window !== "undefined" ?
|
|
226
|
+
typeof window !== "undefined" ? useLayoutEffect : React2.useEffect
|
|
4103
227
|
);
|
|
4104
228
|
function getSystemMode() {
|
|
4105
229
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
@@ -4118,10 +242,10 @@ function ThemeProvider(props) {
|
|
|
4118
242
|
const preset = props.preset ?? "default";
|
|
4119
243
|
const mode = props.mode ?? "light";
|
|
4120
244
|
const targetKind = props.target ?? "document";
|
|
4121
|
-
const [resolvedMode, setResolvedMode] =
|
|
245
|
+
const [resolvedMode, setResolvedMode] = useState2(
|
|
4122
246
|
() => mode === "system" ? getSystemMode() : mode
|
|
4123
247
|
);
|
|
4124
|
-
|
|
248
|
+
useIsoLayoutEffect(() => {
|
|
4125
249
|
if (mode !== "system") {
|
|
4126
250
|
setResolvedMode(mode);
|
|
4127
251
|
return;
|
|
@@ -4134,26 +258,26 @@ function ThemeProvider(props) {
|
|
|
4134
258
|
return () => mq.removeEventListener("change", onChange);
|
|
4135
259
|
}, [mode]);
|
|
4136
260
|
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
4137
|
-
const effectiveTheme =
|
|
261
|
+
const effectiveTheme = useMemo2(() => {
|
|
4138
262
|
const modeBase = resolveModeBase(mode, dataTheme);
|
|
4139
263
|
const base = preset === "default" ? modeBase : preset === "brand" ? mergeThemes(modeBase, brandThemeOverrides) : mergeThemes(modeBase, getPresetTheme(preset));
|
|
4140
264
|
return mergeThemes(base, props.theme ?? {});
|
|
4141
265
|
}, [preset, mode, dataTheme, props.theme]);
|
|
4142
|
-
const hostRef =
|
|
4143
|
-
const appliedKeysRef =
|
|
4144
|
-
|
|
266
|
+
const hostRef = useRef2(null);
|
|
267
|
+
const appliedKeysRef = useRef2(/* @__PURE__ */ new Set());
|
|
268
|
+
useIsoLayoutEffect(() => {
|
|
4145
269
|
if (targetKind === "document" && typeof document !== "undefined") {
|
|
4146
270
|
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
4147
271
|
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
4148
272
|
}
|
|
4149
273
|
}, [targetKind, dataTheme]);
|
|
4150
|
-
const inject =
|
|
274
|
+
const inject = useCallback(() => {
|
|
4151
275
|
const vars = themeToCssVariables(effectiveTheme);
|
|
4152
276
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
4153
277
|
if (!el) return;
|
|
4154
278
|
appliedKeysRef.current = applyCssVariables(el, vars, appliedKeysRef.current);
|
|
4155
279
|
}, [effectiveTheme, targetKind]);
|
|
4156
|
-
|
|
280
|
+
useIsoLayoutEffect(() => {
|
|
4157
281
|
inject();
|
|
4158
282
|
return () => {
|
|
4159
283
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
@@ -4164,7 +288,7 @@ function ThemeProvider(props) {
|
|
|
4164
288
|
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
4165
289
|
};
|
|
4166
290
|
}, [inject, targetKind]);
|
|
4167
|
-
const value =
|
|
291
|
+
const value = useMemo2(
|
|
4168
292
|
() => ({
|
|
4169
293
|
theme: effectiveTheme,
|
|
4170
294
|
preset,
|
|
@@ -4174,12 +298,12 @@ function ThemeProvider(props) {
|
|
|
4174
298
|
[effectiveTheme, preset, mode, dataTheme]
|
|
4175
299
|
);
|
|
4176
300
|
if (targetKind === "document") {
|
|
4177
|
-
return /* @__PURE__ */
|
|
301
|
+
return /* @__PURE__ */ jsx2(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx2("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
|
|
4178
302
|
}
|
|
4179
|
-
return /* @__PURE__ */
|
|
303
|
+
return /* @__PURE__ */ jsx2(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx2("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
4180
304
|
}
|
|
4181
305
|
function useTheme() {
|
|
4182
|
-
const ctx =
|
|
306
|
+
const ctx = useContext(ThemeContext);
|
|
4183
307
|
if (!ctx) {
|
|
4184
308
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
4185
309
|
}
|
|
@@ -4190,10 +314,12 @@ function useTheme() {
|
|
|
4190
314
|
import {
|
|
4191
315
|
ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
4192
316
|
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
317
|
+
INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
|
|
4193
318
|
PAGE_ALLOWED_CHILD_TYPES,
|
|
4194
319
|
SLIDE_ALLOWED_CHILD_TYPES,
|
|
4195
320
|
SLIDE_DECK_ALLOWED_CHILD_TYPES,
|
|
4196
|
-
|
|
321
|
+
TIMED_CUE_ALLOWED_CHILD_TYPES,
|
|
322
|
+
COMPOUND_MAX_NESTING_DEPTH
|
|
4197
323
|
} from "@lessonkit/core";
|
|
4198
324
|
var COMPOUND_PARENTS = [
|
|
4199
325
|
"Lesson",
|
|
@@ -4201,6 +327,8 @@ var COMPOUND_PARENTS = [
|
|
|
4201
327
|
"InteractiveBook",
|
|
4202
328
|
"Slide",
|
|
4203
329
|
"SlideDeck",
|
|
330
|
+
"TimedCue",
|
|
331
|
+
"InteractiveVideo",
|
|
4204
332
|
"AssessmentSequence"
|
|
4205
333
|
];
|
|
4206
334
|
function extendParents(entry) {
|
|
@@ -4259,6 +387,23 @@ var v3CompoundAndContentEntries = [
|
|
|
4259
387
|
theming: { surface: "global-inherit", stylingNotes: "Responsive max-width." },
|
|
4260
388
|
telemetry: { emits: [] }
|
|
4261
389
|
},
|
|
390
|
+
{
|
|
391
|
+
type: "Video",
|
|
392
|
+
category: "content",
|
|
393
|
+
description: "Self-hosted video with native controls and optional captions.",
|
|
394
|
+
props: [
|
|
395
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
396
|
+
{ name: "src", type: "string", required: true, description: "Video URL." },
|
|
397
|
+
{ name: "poster", type: "string", required: false, description: "Poster image URL." },
|
|
398
|
+
{ name: "captions", type: "string", required: false, description: "WebVTT captions URL." },
|
|
399
|
+
{ name: "title", type: "string", required: false, description: "Accessible title." }
|
|
400
|
+
],
|
|
401
|
+
requiredIds: ["blockId"],
|
|
402
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
403
|
+
a11y: { element: "video", ariaLabel: "Video", keyboard: "Native video controls.", notes: "No autoplay with sound." },
|
|
404
|
+
theming: { surface: "global-inherit", stylingNotes: "Responsive video." },
|
|
405
|
+
telemetry: { emits: [] }
|
|
406
|
+
},
|
|
4262
407
|
{
|
|
4263
408
|
type: "Page",
|
|
4264
409
|
category: "container",
|
|
@@ -4267,7 +412,7 @@ var v3CompoundAndContentEntries = [
|
|
|
4267
412
|
h5pAlias: "Column",
|
|
4268
413
|
description: "Column layout container (H5P Column / Page).",
|
|
4269
414
|
allowedChildTypes: [...PAGE_ALLOWED_CHILD_TYPES],
|
|
4270
|
-
maxNestingDepth:
|
|
415
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.Page,
|
|
4271
416
|
props: [
|
|
4272
417
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4273
418
|
{ name: "title", type: "string", required: false, description: "Page title." },
|
|
@@ -4288,7 +433,7 @@ var v3CompoundAndContentEntries = [
|
|
|
4288
433
|
h5pAlias: "Interactive Book",
|
|
4289
434
|
description: "Multi-page book with chapter navigation.",
|
|
4290
435
|
allowedChildTypes: [...INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
|
|
4291
|
-
maxNestingDepth:
|
|
436
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.InteractiveBook,
|
|
4292
437
|
props: [
|
|
4293
438
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4294
439
|
{ name: "title", type: "string", required: true, description: "Book title." },
|
|
@@ -4312,9 +457,9 @@ var v3CompoundAndContentEntries = [
|
|
|
4312
457
|
compoundContract: true,
|
|
4313
458
|
h5pMachineName: "H5P.CoursePresentation",
|
|
4314
459
|
h5pAlias: "Course Presentation slide",
|
|
4315
|
-
description: "Single slide row in a SlideDeck.
|
|
460
|
+
description: "Single slide row in a SlideDeck. Supports Video, Summary, and 1.4 blocks.",
|
|
4316
461
|
allowedChildTypes: [...SLIDE_ALLOWED_CHILD_TYPES],
|
|
4317
|
-
maxNestingDepth:
|
|
462
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.Slide,
|
|
4318
463
|
props: [
|
|
4319
464
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4320
465
|
{ name: "title", type: "string", required: false, description: "Slide title." },
|
|
@@ -4335,7 +480,7 @@ var v3CompoundAndContentEntries = [
|
|
|
4335
480
|
h5pAlias: "Course Presentation",
|
|
4336
481
|
description: "Multi-slide presentation with keyboard navigation.",
|
|
4337
482
|
allowedChildTypes: [...SLIDE_DECK_ALLOWED_CHILD_TYPES],
|
|
4338
|
-
maxNestingDepth:
|
|
483
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.SlideDeck,
|
|
4339
484
|
props: [
|
|
4340
485
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4341
486
|
{ name: "title", type: "string", required: true, description: "Deck title." },
|
|
@@ -4353,6 +498,121 @@ var v3CompoundAndContentEntries = [
|
|
|
4353
498
|
theming: { surface: "global-inherit", stylingNotes: "Deck chrome." },
|
|
4354
499
|
telemetry: { emits: ["slide_viewed"], requiresActiveLesson: true }
|
|
4355
500
|
},
|
|
501
|
+
{
|
|
502
|
+
type: "TimedCue",
|
|
503
|
+
category: "container",
|
|
504
|
+
compoundContract: true,
|
|
505
|
+
h5pMachineName: "H5P.InteractiveVideo",
|
|
506
|
+
h5pAlias: "Interactive Video timed cue",
|
|
507
|
+
description: "Timed overlay cue within InteractiveVideo.",
|
|
508
|
+
allowedChildTypes: [...TIMED_CUE_ALLOWED_CHILD_TYPES],
|
|
509
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.TimedCue,
|
|
510
|
+
props: [
|
|
511
|
+
{ name: "atSeconds", type: "number", required: true, description: "Cue time in seconds." },
|
|
512
|
+
{ name: "label", type: "string", required: false, description: "Cue label." },
|
|
513
|
+
{ name: "mustComplete", type: "boolean", required: false, description: "Block seek until completed." },
|
|
514
|
+
{ name: "children", type: "ReactNode", required: true, description: "Single allowed child block." }
|
|
515
|
+
],
|
|
516
|
+
requiredIds: [],
|
|
517
|
+
parentConstraints: ["InteractiveVideo"],
|
|
518
|
+
a11y: { element: "dialog", ariaLabel: "Timed cue", keyboard: "Focus moves to overlay content.", notes: "Pauses parent video." },
|
|
519
|
+
theming: { surface: "global-inherit", stylingNotes: "Overlay panel." },
|
|
520
|
+
telemetry: { emits: [] }
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
type: "InteractiveVideo",
|
|
524
|
+
category: "container",
|
|
525
|
+
compoundContract: true,
|
|
526
|
+
h5pMachineName: "H5P.InteractiveVideo",
|
|
527
|
+
h5pAlias: "Interactive Video",
|
|
528
|
+
description: "Video with timed interaction overlays.",
|
|
529
|
+
allowedChildTypes: [...INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES],
|
|
530
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.InteractiveVideo,
|
|
531
|
+
props: [
|
|
532
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
533
|
+
{ name: "title", type: "string", required: true, description: "Video title." },
|
|
534
|
+
{ name: "src", type: "string", required: true, description: "Video URL." },
|
|
535
|
+
{ name: "poster", type: "string", required: false, description: "Poster image." },
|
|
536
|
+
{ name: "captions", type: "string", required: false, description: "WebVTT captions." },
|
|
537
|
+
{ name: "showVideoScore", type: "boolean", required: false, description: "Show aggregate score." },
|
|
538
|
+
{ name: "children", type: "TimedCue[]", required: true, description: "Timed cues." }
|
|
539
|
+
],
|
|
540
|
+
requiredIds: ["blockId"],
|
|
541
|
+
parentConstraints: ["Lesson"],
|
|
542
|
+
a11y: {
|
|
543
|
+
element: "section",
|
|
544
|
+
ariaLabel: "Interactive video",
|
|
545
|
+
keyboard: "Native video controls; overlay interactions when paused.",
|
|
546
|
+
notes: "H5P Interactive Video equivalent."
|
|
547
|
+
},
|
|
548
|
+
theming: { surface: "global-inherit", stylingNotes: "Video + overlay." },
|
|
549
|
+
telemetry: { emits: ["video_cue_reached", "video_segment_completed"], requiresActiveLesson: true }
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
type: "Questionnaire",
|
|
553
|
+
category: "content",
|
|
554
|
+
h5pMachineName: "H5P.Questionnaire",
|
|
555
|
+
h5pAlias: "Questionnaire",
|
|
556
|
+
description: "Unscored multi-field survey.",
|
|
557
|
+
props: [
|
|
558
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
559
|
+
{ name: "fields", type: "QuestionnaireField[]", required: true, description: "Form fields." }
|
|
560
|
+
],
|
|
561
|
+
requiredIds: ["blockId"],
|
|
562
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
563
|
+
a11y: { element: "form", ariaLabel: "Questionnaire", keyboard: "Tab through fields.", notes: "Unscored survey." },
|
|
564
|
+
theming: { surface: "global-inherit", stylingNotes: "Form layout." },
|
|
565
|
+
telemetry: { emits: ["questionnaire_submitted"], requiresActiveLesson: true }
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
type: "MemoryGame",
|
|
569
|
+
category: "content",
|
|
570
|
+
h5pMachineName: "H5P.MemoryGame",
|
|
571
|
+
h5pAlias: "Memory Game",
|
|
572
|
+
description: "Card flip memory matching game.",
|
|
573
|
+
props: [
|
|
574
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
575
|
+
{ name: "pairs", type: "MemoryPair[]", required: true, description: "Card pairs." },
|
|
576
|
+
{ name: "selfScore", type: "boolean", required: false, description: "Optional self-score mode." }
|
|
577
|
+
],
|
|
578
|
+
requiredIds: ["blockId"],
|
|
579
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
580
|
+
a11y: { element: "section", ariaLabel: "Memory game", keyboard: "Flip cards with buttons.", notes: "Reduced motion safe." },
|
|
581
|
+
theming: { surface: "global-inherit", stylingNotes: "Card grid." },
|
|
582
|
+
telemetry: { emits: ["memory_card_flipped"] }
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
type: "InformationWall",
|
|
586
|
+
category: "content",
|
|
587
|
+
h5pMachineName: "H5P.InformationWall",
|
|
588
|
+
h5pAlias: "Information Wall",
|
|
589
|
+
description: "Searchable information panels.",
|
|
590
|
+
props: [
|
|
591
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
592
|
+
{ name: "panels", type: "InformationPanel[]", required: true, description: "Content panels." }
|
|
593
|
+
],
|
|
594
|
+
requiredIds: ["blockId"],
|
|
595
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
596
|
+
a11y: { element: "section", ariaLabel: "Information wall", keyboard: "Search and browse panels.", notes: "Filterable grid." },
|
|
597
|
+
theming: { surface: "global-inherit", stylingNotes: "Panel grid." },
|
|
598
|
+
telemetry: { emits: ["information_wall_search"] }
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
type: "ParallaxSlideshow",
|
|
602
|
+
category: "content",
|
|
603
|
+
h5pMachineName: "H5P.ImpressivePresentation",
|
|
604
|
+
h5pAlias: "Slideshow (parallax)",
|
|
605
|
+
description: "Slideshow with parallax; static fallback when reduced motion.",
|
|
606
|
+
props: [
|
|
607
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
608
|
+
{ name: "slides", type: "ParallaxSlide[]", required: true, description: "Slides." }
|
|
609
|
+
],
|
|
610
|
+
requiredIds: ["blockId"],
|
|
611
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
612
|
+
a11y: { element: "section", ariaLabel: "Slideshow", keyboard: "Previous/next slide.", notes: "prefers-reduced-motion fallback." },
|
|
613
|
+
theming: { surface: "global-inherit", stylingNotes: "Slide deck." },
|
|
614
|
+
telemetry: { emits: ["parallax_slide_viewed"] }
|
|
615
|
+
},
|
|
4356
616
|
{
|
|
4357
617
|
type: "Accordion",
|
|
4358
618
|
category: "content",
|
|
@@ -4487,7 +747,7 @@ function buildV3CatalogFromV2(v2) {
|
|
|
4487
747
|
...base,
|
|
4488
748
|
compoundContract: true,
|
|
4489
749
|
allowedChildTypes: [...ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
|
|
4490
|
-
maxNestingDepth:
|
|
750
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.AssessmentSequence
|
|
4491
751
|
};
|
|
4492
752
|
}
|
|
4493
753
|
return base;
|
|
@@ -4834,6 +1094,100 @@ var v2AssessmentEntries = [
|
|
|
4834
1094
|
},
|
|
4835
1095
|
theming: { surface: "global-inherit", stylingNotes: "Container for assessments." },
|
|
4836
1096
|
telemetry: { emits: [], manualTracking: "Child assessments emit assessment_* events." }
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
type: "Summary",
|
|
1100
|
+
category: "assessment",
|
|
1101
|
+
assessmentContract: true,
|
|
1102
|
+
h5pMachineName: "H5P.Summary",
|
|
1103
|
+
h5pAlias: "Summary",
|
|
1104
|
+
description: "Construct a summary from a statement bank in correct order.",
|
|
1105
|
+
props: [
|
|
1106
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
1107
|
+
{ name: "statements", type: "string[]", required: true, description: "Available statements." },
|
|
1108
|
+
{ name: "correct", type: "string[]", required: true, description: "Correct ordered summary." },
|
|
1109
|
+
...assessmentBehaviourProps2
|
|
1110
|
+
],
|
|
1111
|
+
requiredIds: ["checkId"],
|
|
1112
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
1113
|
+
a11y: { element: "section", ariaLabel: "Summary", keyboard: "Select statements in order.", notes: "H5P Summary equivalent." },
|
|
1114
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
1115
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
type: "ImagePairing",
|
|
1119
|
+
category: "assessment",
|
|
1120
|
+
assessmentContract: true,
|
|
1121
|
+
h5pMachineName: "H5P.ImagePair",
|
|
1122
|
+
h5pAlias: "Image Pairing",
|
|
1123
|
+
description: "Match image pairs in a memory-style task.",
|
|
1124
|
+
props: [
|
|
1125
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
1126
|
+
{ name: "pairs", type: "ImagePair[]", required: true, description: "Image pairs to match." },
|
|
1127
|
+
...assessmentBehaviourProps2
|
|
1128
|
+
],
|
|
1129
|
+
requiredIds: ["checkId"],
|
|
1130
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
1131
|
+
a11y: { element: "section", ariaLabel: "Image Pairing", keyboard: "Select two cards to match.", notes: "H5P Image Pairing equivalent." },
|
|
1132
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
1133
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
type: "ImageSequencing",
|
|
1137
|
+
category: "assessment",
|
|
1138
|
+
assessmentContract: true,
|
|
1139
|
+
h5pMachineName: "H5P.ImageSequencing",
|
|
1140
|
+
h5pAlias: "Image Sequencing",
|
|
1141
|
+
description: "Order images in the correct sequence.",
|
|
1142
|
+
props: [
|
|
1143
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
1144
|
+
{ name: "images", type: "SequencingImage[]", required: true, description: "Images to order." },
|
|
1145
|
+
{ name: "correctOrder", type: "string[]", required: true, description: "Correct id order." },
|
|
1146
|
+
...assessmentBehaviourProps2
|
|
1147
|
+
],
|
|
1148
|
+
requiredIds: ["checkId"],
|
|
1149
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
1150
|
+
a11y: { element: "section", ariaLabel: "Image Sequencing", keyboard: "Reorder with up/down.", notes: "H5P Image Sequencing equivalent." },
|
|
1151
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
1152
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
1153
|
+
},
|
|
1154
|
+
{
|
|
1155
|
+
type: "ArithmeticQuiz",
|
|
1156
|
+
category: "assessment",
|
|
1157
|
+
assessmentContract: true,
|
|
1158
|
+
h5pMachineName: "H5P.ArithmeticQuiz",
|
|
1159
|
+
h5pAlias: "Arithmetic Quiz",
|
|
1160
|
+
description: "Timed arithmetic problems with optional timer.",
|
|
1161
|
+
props: [
|
|
1162
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
1163
|
+
{ name: "problems", type: "ArithmeticProblem[]", required: true, description: "Math problems." },
|
|
1164
|
+
{ name: "timeLimitSeconds", type: "number", required: false, description: "Optional time limit." },
|
|
1165
|
+
...assessmentBehaviourProps2
|
|
1166
|
+
],
|
|
1167
|
+
requiredIds: ["checkId"],
|
|
1168
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
1169
|
+
a11y: { element: "section", ariaLabel: "Arithmetic Quiz", keyboard: "Text input per problem.", notes: "H5P Arithmetic Quiz equivalent." },
|
|
1170
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
1171
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
type: "Essay",
|
|
1175
|
+
category: "assessment",
|
|
1176
|
+
assessmentContract: true,
|
|
1177
|
+
h5pMachineName: "H5P.Essay",
|
|
1178
|
+
h5pAlias: "Essay",
|
|
1179
|
+
description: "Open text response; manual or plugin grading.",
|
|
1180
|
+
props: [
|
|
1181
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
1182
|
+
{ name: "question", type: "string", required: true, description: "Essay prompt." },
|
|
1183
|
+
{ name: "minLength", type: "number", required: false, description: "Minimum character length." },
|
|
1184
|
+
...assessmentBehaviourProps2
|
|
1185
|
+
],
|
|
1186
|
+
requiredIds: ["checkId"],
|
|
1187
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
1188
|
+
a11y: { element: "section", ariaLabel: "Essay", keyboard: "Textarea input.", notes: "H5P Essay equivalent." },
|
|
1189
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
1190
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
4837
1191
|
}
|
|
4838
1192
|
];
|
|
4839
1193
|
var BLOCK_CATALOG_V2 = [
|
|
@@ -4872,6 +1226,7 @@ function getBlockCatalogEntry(type, opts) {
|
|
|
4872
1226
|
}
|
|
4873
1227
|
export {
|
|
4874
1228
|
Accordion,
|
|
1229
|
+
ArithmeticQuiz,
|
|
4875
1230
|
AssessmentSequence,
|
|
4876
1231
|
BLOCK_CATALOG,
|
|
4877
1232
|
BLOCK_CATALOG_V2,
|
|
@@ -4880,6 +1235,7 @@ export {
|
|
|
4880
1235
|
DialogCards,
|
|
4881
1236
|
DragAndDrop,
|
|
4882
1237
|
DragTheWords,
|
|
1238
|
+
Essay,
|
|
4883
1239
|
FillInTheBlanks,
|
|
4884
1240
|
FindHotspot,
|
|
4885
1241
|
FindMultipleHotspots,
|
|
@@ -4887,36 +1243,48 @@ export {
|
|
|
4887
1243
|
Heading,
|
|
4888
1244
|
Image,
|
|
4889
1245
|
ImageHotspots,
|
|
1246
|
+
ImagePairing,
|
|
1247
|
+
ImageSequencing,
|
|
4890
1248
|
ImageSlider,
|
|
1249
|
+
InformationWall,
|
|
4891
1250
|
InteractiveBook,
|
|
1251
|
+
InteractiveVideo,
|
|
4892
1252
|
KnowledgeCheck,
|
|
4893
1253
|
Lesson,
|
|
4894
1254
|
LessonkitProvider,
|
|
4895
1255
|
MarkTheWords,
|
|
1256
|
+
MemoryGame,
|
|
4896
1257
|
Page,
|
|
1258
|
+
ParallaxSlideshow,
|
|
4897
1259
|
ProgressTracker,
|
|
1260
|
+
Questionnaire,
|
|
4898
1261
|
Quiz,
|
|
4899
1262
|
Reflection,
|
|
4900
1263
|
Scenario,
|
|
4901
1264
|
Slide,
|
|
4902
1265
|
SlideDeck,
|
|
1266
|
+
Summary,
|
|
4903
1267
|
Text,
|
|
4904
1268
|
ThemeProvider,
|
|
1269
|
+
TimedCue,
|
|
4905
1270
|
TrueFalse,
|
|
1271
|
+
Video,
|
|
1272
|
+
assertProductionCourseConfig,
|
|
4906
1273
|
blockCatalogV2Version,
|
|
4907
1274
|
blockCatalogV3Version,
|
|
4908
1275
|
blockCatalogVersion,
|
|
4909
1276
|
buildBlockCatalog,
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
1277
|
+
buildTelemetryEvent,
|
|
1278
|
+
createLessonkitRuntime,
|
|
1279
|
+
createPluginRegistry,
|
|
1280
|
+
createTelemetryPipeline,
|
|
4914
1281
|
defineAssessmentPlugin,
|
|
4915
1282
|
defineLifecyclePlugin,
|
|
4916
1283
|
defineTelemetryPlugin,
|
|
4917
1284
|
getBlockCatalogEntry,
|
|
4918
1285
|
resetAssessmentWarningsForTests,
|
|
4919
1286
|
resetQuizWarningsForTests,
|
|
1287
|
+
shouldEnforceProductionGuard,
|
|
4920
1288
|
useAssessmentState,
|
|
4921
1289
|
useCompletion,
|
|
4922
1290
|
useLessonkit,
|