@lessonkit/react 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/block-catalog.v2.json +770 -0
- package/block-catalog.v3.json +1504 -0
- package/block-contract.v2.json +104 -0
- package/block-contract.v3.json +110 -0
- package/dist/index.cjs +2960 -336
- package/dist/index.d.cts +293 -18
- package/dist/index.d.ts +293 -18
- package/dist/index.js +2929 -315
- package/package.json +17 -9
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/components.tsx
|
|
2
|
-
import { useEffect as
|
|
3
|
-
import { visuallyHiddenStyle } from "@lessonkit/accessibility";
|
|
2
|
+
import { useEffect as useEffect4, useId as useId2, useMemo as useMemo6, useRef as useRef4, useState as useState4 } from "react";
|
|
3
|
+
import { visuallyHiddenStyle as visuallyHiddenStyle2 } from "@lessonkit/accessibility";
|
|
4
4
|
|
|
5
5
|
// src/context.tsx
|
|
6
6
|
import { createContext } from "react";
|
|
@@ -15,7 +15,39 @@ import {
|
|
|
15
15
|
useState
|
|
16
16
|
} from "react";
|
|
17
17
|
import { createLessonkitRuntime, createTrackingClient as createTrackingClient2, assertValidId } from "@lessonkit/core";
|
|
18
|
+
|
|
19
|
+
// src/runtime/observability.ts
|
|
18
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
|
|
19
51
|
import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement3 } from "@lessonkit/xapi";
|
|
20
52
|
|
|
21
53
|
// src/runtime/emitTelemetry.ts
|
|
@@ -36,7 +68,16 @@ import {
|
|
|
36
68
|
mapLessonkitTelemetryToBridgeAction,
|
|
37
69
|
telemetryEventToLessonkit
|
|
38
70
|
} from "@lessonkit/lxpack/bridge";
|
|
39
|
-
|
|
71
|
+
var BRIDGE_MISS_EVENT_NAMES = /* @__PURE__ */ new Set([
|
|
72
|
+
"course_completed",
|
|
73
|
+
"lesson_completed",
|
|
74
|
+
"assessment_completed",
|
|
75
|
+
"quiz_completed"
|
|
76
|
+
]);
|
|
77
|
+
function forwardTelemetryToLxpack(event, mode = "auto", opts) {
|
|
78
|
+
if (mode === "auto" && opts?.onBridgeMiss && BRIDGE_MISS_EVENT_NAMES.has(event.name) && !getLxpackBridge()) {
|
|
79
|
+
opts.onBridgeMiss(event);
|
|
80
|
+
}
|
|
40
81
|
forwardTelemetryToBridge(event, mode);
|
|
41
82
|
}
|
|
42
83
|
|
|
@@ -67,7 +108,9 @@ function createLegacyPipeline(opts, extraSinks = []) {
|
|
|
67
108
|
{
|
|
68
109
|
id: "lxpack-bridge",
|
|
69
110
|
emit(event) {
|
|
70
|
-
forwardTelemetryToLxpack(event, opts.lxpackBridge
|
|
111
|
+
forwardTelemetryToLxpack(event, opts.lxpackBridge, {
|
|
112
|
+
onBridgeMiss: opts.onLxpackBridgeMiss
|
|
113
|
+
});
|
|
71
114
|
}
|
|
72
115
|
},
|
|
73
116
|
...extraSinks
|
|
@@ -94,7 +137,8 @@ function emitTelemetry(tracking, xapi, event, opts) {
|
|
|
94
137
|
const legacy = {
|
|
95
138
|
tracking,
|
|
96
139
|
xapi,
|
|
97
|
-
lxpackBridge: opts?.lxpackBridge ?? "auto"
|
|
140
|
+
lxpackBridge: opts?.lxpackBridge ?? "auto",
|
|
141
|
+
onLxpackBridgeMiss: opts?.onLxpackBridgeMiss
|
|
98
142
|
};
|
|
99
143
|
emitThroughPipeline(event, legacy, opts?.extraSinks);
|
|
100
144
|
}
|
|
@@ -184,7 +228,9 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
|
|
|
184
228
|
xapiStatementSent = true;
|
|
185
229
|
}
|
|
186
230
|
}
|
|
187
|
-
forwardTelemetryToLxpack(opts.event, opts.lxpackBridge
|
|
231
|
+
forwardTelemetryToLxpack(opts.event, opts.lxpackBridge, {
|
|
232
|
+
onBridgeMiss: opts.onLxpackBridgeMiss
|
|
233
|
+
});
|
|
188
234
|
const emitCtx = {
|
|
189
235
|
courseId: opts.event.courseId,
|
|
190
236
|
sessionId: opts.event.sessionId,
|
|
@@ -195,53 +241,25 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
|
|
|
195
241
|
}
|
|
196
242
|
|
|
197
243
|
// src/runtime/plugins.ts
|
|
198
|
-
import { createPluginRegistry } from "@lessonkit/core";
|
|
244
|
+
import { buildPluginContext as buildPluginContextFromCore, createPluginRegistry } from "@lessonkit/core";
|
|
199
245
|
function createReactPluginHost(plugins) {
|
|
200
246
|
if (!plugins?.length) return null;
|
|
201
247
|
return createPluginRegistry(plugins);
|
|
202
248
|
}
|
|
203
249
|
function buildPluginContext(opts) {
|
|
204
|
-
return
|
|
205
|
-
courseId: opts.courseId,
|
|
206
|
-
sessionId: opts.sessionId,
|
|
207
|
-
attemptId: opts.attemptId,
|
|
208
|
-
user: opts.user
|
|
209
|
-
};
|
|
250
|
+
return buildPluginContextFromCore(opts);
|
|
210
251
|
}
|
|
211
252
|
function emitTelemetryWithPlugins(opts) {
|
|
212
253
|
const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
|
|
213
254
|
if (next === null) return;
|
|
214
255
|
emitTelemetry(opts.tracking, opts.xapi, next, {
|
|
215
256
|
lxpackBridge: opts.lxpackBridge ?? "auto",
|
|
216
|
-
extraSinks: opts.extraSinks
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// src/runtime/telemetry.ts
|
|
221
|
-
import { createTrackingClient } from "@lessonkit/core";
|
|
222
|
-
function createTrackingClientFromConfig(config) {
|
|
223
|
-
if (config.tracking?.enabled === false) return createTrackingClient();
|
|
224
|
-
if (config.tracking?.createClient) return config.tracking.createClient();
|
|
225
|
-
return createTrackingClient({
|
|
226
|
-
sink: config.tracking?.sink,
|
|
227
|
-
batchSink: config.tracking?.batchSink,
|
|
228
|
-
batch: config.tracking?.batch
|
|
257
|
+
extraSinks: opts.extraSinks,
|
|
258
|
+
onLxpackBridgeMiss: opts.onLxpackBridgeMiss
|
|
229
259
|
});
|
|
230
260
|
}
|
|
231
|
-
async function disposeTrackingClient(client) {
|
|
232
|
-
try {
|
|
233
|
-
await client?.flush?.();
|
|
234
|
-
} catch {
|
|
235
|
-
}
|
|
236
|
-
try {
|
|
237
|
-
await client?.dispose?.();
|
|
238
|
-
} catch {
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
261
|
|
|
242
|
-
// src/provider/
|
|
243
|
-
var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
244
|
-
var defaultStorage = createSessionStoragePort();
|
|
262
|
+
// src/provider/courseStarted/emit.ts
|
|
245
263
|
var courseStartedTrackingFlightKey = null;
|
|
246
264
|
function isTrackingActive(tracking) {
|
|
247
265
|
return tracking?.enabled !== false;
|
|
@@ -296,6 +314,7 @@ async function emitCourseStartedPipelineOnly(opts) {
|
|
|
296
314
|
event: opts.event,
|
|
297
315
|
xapi: opts.xapi,
|
|
298
316
|
lxpackBridge: opts.lxpackBridge,
|
|
317
|
+
onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
|
|
299
318
|
extraSinks: opts.extraSinks,
|
|
300
319
|
skipXapi: opts.skipXapi
|
|
301
320
|
});
|
|
@@ -313,22 +332,15 @@ async function emitCourseStartedPipelineOnly(opts) {
|
|
|
313
332
|
async function emitCourseStarted(opts) {
|
|
314
333
|
const event = buildCourseStartedEvent(opts);
|
|
315
334
|
if (event === null) return "filtered";
|
|
316
|
-
const
|
|
335
|
+
const tracked = await emitCourseStartedToTracking(
|
|
336
|
+
opts.tracking,
|
|
317
337
|
opts.storage,
|
|
318
338
|
opts.sessionId,
|
|
319
|
-
opts.courseId
|
|
339
|
+
opts.courseId,
|
|
340
|
+
event,
|
|
341
|
+
opts.shouldCommit
|
|
320
342
|
);
|
|
321
|
-
if (!
|
|
322
|
-
const tracked = await emitCourseStartedToTracking(
|
|
323
|
-
opts.tracking,
|
|
324
|
-
opts.storage,
|
|
325
|
-
opts.sessionId,
|
|
326
|
-
opts.courseId,
|
|
327
|
-
event,
|
|
328
|
-
opts.shouldCommit
|
|
329
|
-
);
|
|
330
|
-
if (!tracked) return "failed";
|
|
331
|
-
}
|
|
343
|
+
if (!tracked) return "failed";
|
|
332
344
|
return emitCourseStartedPipelineOnly({
|
|
333
345
|
...opts,
|
|
334
346
|
event,
|
|
@@ -340,28 +352,22 @@ async function emitCourseStarted(opts) {
|
|
|
340
352
|
async function emitCourseStartedToTrackingOnly(opts) {
|
|
341
353
|
const event = buildCourseStartedEvent(opts);
|
|
342
354
|
if (event === null) return "filtered";
|
|
343
|
-
const
|
|
355
|
+
const tracked = await emitCourseStartedToTracking(
|
|
356
|
+
opts.tracking,
|
|
344
357
|
opts.storage,
|
|
345
358
|
opts.sessionId,
|
|
346
|
-
opts.courseId
|
|
359
|
+
opts.courseId,
|
|
360
|
+
event,
|
|
361
|
+
opts.shouldCommit
|
|
347
362
|
);
|
|
348
|
-
if (!
|
|
349
|
-
const tracked = await emitCourseStartedToTracking(
|
|
350
|
-
opts.tracking,
|
|
351
|
-
opts.storage,
|
|
352
|
-
opts.sessionId,
|
|
353
|
-
opts.courseId,
|
|
354
|
-
event,
|
|
355
|
-
opts.shouldCommit
|
|
356
|
-
);
|
|
357
|
-
if (!tracked) return "failed";
|
|
358
|
-
}
|
|
363
|
+
if (!tracked) return "failed";
|
|
359
364
|
try {
|
|
360
365
|
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
361
366
|
await emitCourseStartedNonTrackingPipeline({
|
|
362
367
|
event,
|
|
363
368
|
xapi: null,
|
|
364
369
|
lxpackBridge: opts.lxpackBridge,
|
|
370
|
+
onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
|
|
365
371
|
extraSinks: opts.extraSinks,
|
|
366
372
|
skipXapi: true
|
|
367
373
|
});
|
|
@@ -394,6 +400,9 @@ async function emitPendingCourseStarted(opts) {
|
|
|
394
400
|
opts.sessionId,
|
|
395
401
|
opts.courseId
|
|
396
402
|
);
|
|
403
|
+
if (sessionStarted && trackingEmitted && pipelineDelivered) {
|
|
404
|
+
return "emitted";
|
|
405
|
+
}
|
|
397
406
|
if (sessionStarted && trackingEmitted && !pipelineDelivered) {
|
|
398
407
|
const event = buildCourseStartedEvent(opts);
|
|
399
408
|
if (event === null) return "filtered";
|
|
@@ -412,6 +421,35 @@ function assertTrackingSinkConfig(tracking) {
|
|
|
412
421
|
"[lessonkit] tracking.sink and tracking.batchSink cannot both be set; use batchSink alone for batched delivery"
|
|
413
422
|
);
|
|
414
423
|
}
|
|
424
|
+
|
|
425
|
+
// src/runtime/telemetry.ts
|
|
426
|
+
import { createTrackingClient } from "@lessonkit/core";
|
|
427
|
+
function createTrackingClientFromConfig(config) {
|
|
428
|
+
if (config.tracking?.enabled === false) return createTrackingClient();
|
|
429
|
+
if (config.tracking?.createClient) return config.tracking.createClient();
|
|
430
|
+
return createTrackingClient({
|
|
431
|
+
sink: config.tracking?.sink,
|
|
432
|
+
batchSink: config.tracking?.batchSink,
|
|
433
|
+
batch: config.tracking?.batch
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
async function disposeTrackingClient(client) {
|
|
437
|
+
try {
|
|
438
|
+
await client?.flush?.();
|
|
439
|
+
} catch {
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
await client?.dispose?.();
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/provider/useLessonkitProviderRuntime.ts
|
|
448
|
+
var useIsoLayoutEffect = (
|
|
449
|
+
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
450
|
+
typeof window !== "undefined" ? useLayoutEffect : useEffect
|
|
451
|
+
);
|
|
452
|
+
var defaultStorage = createSessionStoragePort();
|
|
415
453
|
function useLessonkitProviderRuntime(config) {
|
|
416
454
|
const normalizedCourseId = useMemo(
|
|
417
455
|
() => assertValidId(config.courseId, "courseId"),
|
|
@@ -422,6 +460,14 @@ function useLessonkitProviderRuntime(config) {
|
|
|
422
460
|
[config, normalizedCourseId]
|
|
423
461
|
);
|
|
424
462
|
const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
|
|
463
|
+
useEffect(() => {
|
|
464
|
+
if (useV2Runtime) return;
|
|
465
|
+
const g = globalThis;
|
|
466
|
+
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
|
|
467
|
+
console.warn(
|
|
468
|
+
'[lessonkit] LessonkitProvider runtimeVersion "v1" is deprecated; omit or use "v2" (default). v1 will be removed in LessonKit 2.0.'
|
|
469
|
+
);
|
|
470
|
+
}, [useV2Runtime]);
|
|
425
471
|
const extraSinksRef = useRef(normalizedConfig.sinks);
|
|
426
472
|
extraSinksRef.current = normalizedConfig.sinks;
|
|
427
473
|
const headlessRef = useRef(null);
|
|
@@ -440,7 +486,16 @@ function useLessonkitProviderRuntime(config) {
|
|
|
440
486
|
courseIdRef.current = normalizedCourseId;
|
|
441
487
|
const lxpackBridgeModeRef = useRef(normalizedConfig.lxpack?.bridge ?? "auto");
|
|
442
488
|
lxpackBridgeModeRef.current = normalizedConfig.lxpack?.bridge ?? "auto";
|
|
443
|
-
const
|
|
489
|
+
const observabilityRef = useRef(normalizedConfig.observability);
|
|
490
|
+
observabilityRef.current = normalizedConfig.observability;
|
|
491
|
+
const onLxpackBridgeMiss = useCallback((event) => {
|
|
492
|
+
observabilityRef.current?.onLxpackBridgeMiss?.(event);
|
|
493
|
+
}, []);
|
|
494
|
+
const pluginsFingerprint = normalizedConfig.plugins?.map((p) => `${p.id}\0${p.version}`).join("|") ?? "";
|
|
495
|
+
const pluginHost = useMemo(
|
|
496
|
+
() => createReactPluginHost(normalizedConfig.plugins),
|
|
497
|
+
[pluginsFingerprint]
|
|
498
|
+
);
|
|
444
499
|
const pluginHostRef = useRef(pluginHost);
|
|
445
500
|
pluginHostRef.current = pluginHost;
|
|
446
501
|
const progressRef = useRef(createProgressController());
|
|
@@ -456,7 +511,8 @@ function useLessonkitProviderRuntime(config) {
|
|
|
456
511
|
headlessRef.current = createLessonkitRuntime({
|
|
457
512
|
courseId: normalizedCourseId,
|
|
458
513
|
runtimeVersion: "v2",
|
|
459
|
-
session: normalizedConfig.session
|
|
514
|
+
session: normalizedConfig.session,
|
|
515
|
+
plugins: pluginHostRef.current ?? normalizedConfig.plugins
|
|
460
516
|
});
|
|
461
517
|
progressRef.current = headlessRef.current.progress;
|
|
462
518
|
} else {
|
|
@@ -470,7 +526,8 @@ function useLessonkitProviderRuntime(config) {
|
|
|
470
526
|
headlessRef.current = createLessonkitRuntime({
|
|
471
527
|
courseId: normalizedCourseId,
|
|
472
528
|
runtimeVersion: "v2",
|
|
473
|
-
session: normalizedConfig.session
|
|
529
|
+
session: normalizedConfig.session,
|
|
530
|
+
plugins: pluginHostRef.current ?? normalizedConfig.plugins
|
|
474
531
|
});
|
|
475
532
|
}
|
|
476
533
|
if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
|
|
@@ -494,7 +551,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
494
551
|
}, []);
|
|
495
552
|
const activeLessonIdRef = useRef(progress.activeLessonId);
|
|
496
553
|
activeLessonIdRef.current = progress.activeLessonId;
|
|
497
|
-
const xapiQueueRef = useRef(
|
|
554
|
+
const xapiQueueRef = useRef(createXapiQueueFromObservability(normalizedConfig.observability));
|
|
498
555
|
const xapiRef = useRef(null);
|
|
499
556
|
const [xapi, setXapi] = useState(null);
|
|
500
557
|
const prevXapiCourseIdRef = useRef(normalizedCourseId);
|
|
@@ -515,7 +572,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
515
572
|
}
|
|
516
573
|
void xapiRef.current?.flush();
|
|
517
574
|
}
|
|
518
|
-
xapiQueueRef.current =
|
|
575
|
+
xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
|
|
519
576
|
prevXapiCourseIdRef.current = courseId;
|
|
520
577
|
xapiCourseStartedSentOnClientRef.current = false;
|
|
521
578
|
}
|
|
@@ -594,10 +651,13 @@ function useLessonkitProviderRuntime(config) {
|
|
|
594
651
|
);
|
|
595
652
|
useIsoLayoutEffect(() => {
|
|
596
653
|
const prev = trackingRef.current;
|
|
597
|
-
const baseSink = normalizedConfig.tracking?.sink;
|
|
654
|
+
const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
|
|
598
655
|
const userBatchSink = normalizedConfig.tracking?.batchSink;
|
|
599
656
|
assertTrackingSinkConfig(normalizedConfig.tracking);
|
|
600
|
-
const sink = pluginHostRef.current && baseSink ?
|
|
657
|
+
const sink = pluginHostRef.current && baseSink ? (
|
|
658
|
+
/* v8 ignore next -- composeTrackingSink may return null; fall back to base sink */
|
|
659
|
+
pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink
|
|
660
|
+
) : baseSink;
|
|
601
661
|
const batchSink = pluginHostRef.current && userBatchSink ? async (events) => {
|
|
602
662
|
const host = pluginHostRef.current;
|
|
603
663
|
const ctx = buildCurrentPluginCtx();
|
|
@@ -641,6 +701,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
641
701
|
attemptId: attemptIdRef.current,
|
|
642
702
|
user: userRef.current,
|
|
643
703
|
lxpackBridge: lxpackBridgeModeRef.current,
|
|
704
|
+
onLxpackBridgeMiss,
|
|
644
705
|
extraSinks: extraSinksRef.current,
|
|
645
706
|
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
646
707
|
onXapiStatementSent: () => {
|
|
@@ -682,9 +743,10 @@ function useLessonkitProviderRuntime(config) {
|
|
|
682
743
|
user: userRef.current
|
|
683
744
|
}),
|
|
684
745
|
lxpackBridge: lxpackBridgeModeRef.current,
|
|
746
|
+
onLxpackBridgeMiss,
|
|
685
747
|
extraSinks: extraSinksRef.current
|
|
686
748
|
});
|
|
687
|
-
}, []);
|
|
749
|
+
}, [onLxpackBridgeMiss]);
|
|
688
750
|
const emitLifecycleEvent = useCallback(
|
|
689
751
|
(name, data, lessonId) => {
|
|
690
752
|
const event = tryBuildTelemetryEvent({
|
|
@@ -740,12 +802,13 @@ function useLessonkitProviderRuntime(config) {
|
|
|
740
802
|
attemptId: attemptIdRef.current,
|
|
741
803
|
user: userRef.current,
|
|
742
804
|
lxpackBridge: lxpackBridgeModeRef.current,
|
|
805
|
+
onLxpackBridgeMiss,
|
|
743
806
|
extraSinks: extraSinksRef.current
|
|
744
807
|
});
|
|
745
808
|
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
746
809
|
}
|
|
747
810
|
})();
|
|
748
|
-
}, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
|
|
811
|
+
}, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress, onLxpackBridgeMiss]);
|
|
749
812
|
const emitLessonCompleted = useCallback(
|
|
750
813
|
(lessonId, durationMs) => {
|
|
751
814
|
track("lesson_completed", { lessonId, durationMs }, { lessonId });
|
|
@@ -794,6 +857,22 @@ function useLessonkitProviderRuntime(config) {
|
|
|
794
857
|
})();
|
|
795
858
|
};
|
|
796
859
|
}, []);
|
|
860
|
+
useEffect(() => {
|
|
861
|
+
if (typeof document === "undefined") return;
|
|
862
|
+
const flushOnExit = () => {
|
|
863
|
+
void xapiRef.current?.flush();
|
|
864
|
+
void trackingRef.current?.flush?.();
|
|
865
|
+
};
|
|
866
|
+
const onVisibilityChange = () => {
|
|
867
|
+
if (document.visibilityState === "hidden") flushOnExit();
|
|
868
|
+
};
|
|
869
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
870
|
+
window.addEventListener("pagehide", flushOnExit);
|
|
871
|
+
return () => {
|
|
872
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
873
|
+
window.removeEventListener("pagehide", flushOnExit);
|
|
874
|
+
};
|
|
875
|
+
}, []);
|
|
797
876
|
const setActiveLesson = useCallback(
|
|
798
877
|
(lessonId) => {
|
|
799
878
|
if (useV2Runtime && headlessRef.current) {
|
|
@@ -857,20 +936,34 @@ function useLessonkitProviderRuntime(config) {
|
|
|
857
936
|
session: normalizedConfig.session
|
|
858
937
|
});
|
|
859
938
|
}
|
|
860
|
-
}, [
|
|
939
|
+
}, [
|
|
940
|
+
useV2Runtime,
|
|
941
|
+
normalizedCourseId,
|
|
942
|
+
sessionAttemptId,
|
|
943
|
+
sessionConfiguredId,
|
|
944
|
+
sessionUserKey,
|
|
945
|
+
normalizedConfig.session
|
|
946
|
+
]);
|
|
947
|
+
useEffect(() => {
|
|
948
|
+
if (!useV2Runtime || !headlessRef.current) return;
|
|
949
|
+
headlessRef.current.updateConfig({
|
|
950
|
+
plugins: pluginHostRef.current ?? normalizedConfig.plugins
|
|
951
|
+
});
|
|
952
|
+
}, [useV2Runtime, pluginHost]);
|
|
861
953
|
useEffect(() => {
|
|
862
|
-
|
|
954
|
+
const host = useV2Runtime ? headlessRef.current?.pluginHost ?? null : pluginHost;
|
|
955
|
+
if (!host) return;
|
|
863
956
|
const ctx = buildPluginContext({
|
|
864
957
|
courseId: courseIdRef.current,
|
|
865
958
|
sessionId: sessionIdRef.current,
|
|
866
959
|
attemptId: attemptIdRef.current,
|
|
867
960
|
user: userRef.current
|
|
868
961
|
});
|
|
869
|
-
|
|
962
|
+
host.setupAll(ctx);
|
|
870
963
|
return () => {
|
|
871
|
-
|
|
964
|
+
host.disposeAll();
|
|
872
965
|
};
|
|
873
|
-
}, [pluginHost, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
|
|
966
|
+
}, [pluginHost, useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
|
|
874
967
|
useEffect(() => {
|
|
875
968
|
const nextConfigured = normalizedConfig.session?.sessionId;
|
|
876
969
|
const prevConfigured = prevConfiguredSessionIdRef.current;
|
|
@@ -934,7 +1027,27 @@ function LessonkitProvider(props) {
|
|
|
934
1027
|
}
|
|
935
1028
|
|
|
936
1029
|
// src/hooks.ts
|
|
937
|
-
import { useContext, useMemo as
|
|
1030
|
+
import { useContext, useMemo as useMemo3 } from "react";
|
|
1031
|
+
|
|
1032
|
+
// src/assessment/useAssessmentState.ts
|
|
1033
|
+
import { useMemo as useMemo2 } from "react";
|
|
1034
|
+
function useAssessmentState(enclosingLessonId) {
|
|
1035
|
+
const { track } = useLessonkit();
|
|
1036
|
+
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
1037
|
+
return useMemo2(
|
|
1038
|
+
() => ({
|
|
1039
|
+
answer: (data) => {
|
|
1040
|
+
track("assessment_answered", data, trackOpts);
|
|
1041
|
+
},
|
|
1042
|
+
complete: (data) => {
|
|
1043
|
+
track("assessment_completed", data, trackOpts);
|
|
1044
|
+
}
|
|
1045
|
+
}),
|
|
1046
|
+
[track, enclosingLessonId]
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// src/hooks.ts
|
|
938
1051
|
function useLessonkit() {
|
|
939
1052
|
const ctx = useContext(LessonkitContext);
|
|
940
1053
|
if (!ctx) throw new Error("LessonKit: missing LessonkitProvider");
|
|
@@ -946,16 +1059,16 @@ function useProgress() {
|
|
|
946
1059
|
}
|
|
947
1060
|
function useTracking() {
|
|
948
1061
|
const { track } = useLessonkit();
|
|
949
|
-
return
|
|
1062
|
+
return useMemo3(() => ({ track }), [track]);
|
|
950
1063
|
}
|
|
951
1064
|
function useCompletion() {
|
|
952
1065
|
const { completeLesson, completeCourse } = useLessonkit();
|
|
953
|
-
return
|
|
1066
|
+
return useMemo3(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
|
|
954
1067
|
}
|
|
955
1068
|
function useQuizState(enclosingLessonId) {
|
|
956
1069
|
const { track } = useLessonkit();
|
|
957
1070
|
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
958
|
-
return
|
|
1071
|
+
return useMemo3(
|
|
959
1072
|
() => ({
|
|
960
1073
|
answer: (opts) => {
|
|
961
1074
|
track("quiz_answered", opts, trackOpts);
|
|
@@ -1013,230 +1126,490 @@ function getLessonMountCount(lessonId) {
|
|
|
1013
1126
|
return mountCounts.get(lessonId) ?? 0;
|
|
1014
1127
|
}
|
|
1015
1128
|
|
|
1016
|
-
// src/components.tsx
|
|
1017
|
-
import {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
[props.config, courseId]
|
|
1027
|
-
);
|
|
1028
|
-
return /* @__PURE__ */ jsx2(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs("section", { "aria-label": props.title, children: [
|
|
1029
|
-
/* @__PURE__ */ jsx2("h1", { children: props.title }),
|
|
1030
|
-
/* @__PURE__ */ jsx2("div", { children: props.children })
|
|
1031
|
-
] }) });
|
|
1032
|
-
}
|
|
1033
|
-
function Lesson(props) {
|
|
1034
|
-
const lessonId = useMemo3(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
1035
|
-
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
1036
|
-
const { setActiveLesson, config } = useLessonkit();
|
|
1037
|
-
const { completeLesson } = useCompletion();
|
|
1038
|
-
const lessonMountGenerationRef = useRef2(0);
|
|
1039
|
-
const liveCourseIdRef = useRef2(config.courseId);
|
|
1040
|
-
liveCourseIdRef.current = config.courseId;
|
|
1041
|
-
useEffect2(() => {
|
|
1042
|
-
const unregister = registerLessonMount(lessonId);
|
|
1043
|
-
const generation = ++lessonMountGenerationRef.current;
|
|
1044
|
-
const mountedCourseId = config.courseId;
|
|
1045
|
-
let effectSurvivedTick = false;
|
|
1046
|
-
queueMicrotask(() => {
|
|
1047
|
-
queueMicrotask(() => {
|
|
1048
|
-
effectSurvivedTick = true;
|
|
1049
|
-
});
|
|
1050
|
-
});
|
|
1051
|
-
setActiveLesson(lessonId);
|
|
1052
|
-
return () => {
|
|
1053
|
-
unregister();
|
|
1054
|
-
if (getLessonMountCount(lessonId) > 0) {
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
if (!autoComplete) return;
|
|
1058
|
-
queueMicrotask(() => {
|
|
1059
|
-
if (!effectSurvivedTick) return;
|
|
1060
|
-
if (lessonMountGenerationRef.current !== generation) return;
|
|
1061
|
-
if (liveCourseIdRef.current !== mountedCourseId) return;
|
|
1062
|
-
completeLesson(lessonId, { courseId: mountedCourseId });
|
|
1063
|
-
});
|
|
1064
|
-
};
|
|
1065
|
-
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
1066
|
-
return /* @__PURE__ */ jsx2(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs("article", { "aria-label": props.title, children: [
|
|
1067
|
-
/* @__PURE__ */ jsx2("h2", { children: props.title }),
|
|
1068
|
-
/* @__PURE__ */ jsx2("div", { children: props.children })
|
|
1069
|
-
] }) });
|
|
1070
|
-
}
|
|
1071
|
-
function Scenario(props) {
|
|
1072
|
-
const blockId = useMemo3(
|
|
1073
|
-
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1074
|
-
[props.blockId]
|
|
1075
|
-
);
|
|
1076
|
-
return /* @__PURE__ */ jsx2("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
1077
|
-
}
|
|
1078
|
-
function Reflection(props) {
|
|
1079
|
-
const blockId = useMemo3(
|
|
1080
|
-
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1081
|
-
[props.blockId]
|
|
1082
|
-
);
|
|
1083
|
-
const promptId = useId();
|
|
1084
|
-
const hintId = useId();
|
|
1085
|
-
const [internalValue, setInternalValue] = useState2("");
|
|
1086
|
-
const isControlled = props.value !== void 0;
|
|
1087
|
-
const value = isControlled ? props.value : internalValue;
|
|
1088
|
-
const handleChange = (event) => {
|
|
1089
|
-
if (!isControlled) setInternalValue(event.target.value);
|
|
1090
|
-
props.onChange?.(event.target.value);
|
|
1091
|
-
};
|
|
1092
|
-
return /* @__PURE__ */ jsxs("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
|
|
1093
|
-
props.prompt ? /* @__PURE__ */ jsx2("p", { id: promptId, children: props.prompt }) : null,
|
|
1094
|
-
props.hint ? /* @__PURE__ */ jsx2("p", { id: hintId, style: visuallyHiddenStyle, children: props.hint }) : null,
|
|
1095
|
-
props.children,
|
|
1096
|
-
/* @__PURE__ */ jsx2(
|
|
1097
|
-
"textarea",
|
|
1098
|
-
{
|
|
1099
|
-
value,
|
|
1100
|
-
onChange: handleChange,
|
|
1101
|
-
"aria-labelledby": props.prompt ? promptId : void 0,
|
|
1102
|
-
"aria-describedby": props.hint ? hintId : void 0,
|
|
1103
|
-
"aria-label": props.prompt ? void 0 : "Reflection response"
|
|
1104
|
-
}
|
|
1105
|
-
)
|
|
1106
|
-
] });
|
|
1107
|
-
}
|
|
1108
|
-
function KnowledgeCheck(props) {
|
|
1109
|
-
return /* @__PURE__ */ jsx2(
|
|
1110
|
-
Quiz,
|
|
1111
|
-
{
|
|
1112
|
-
checkId: props.checkId,
|
|
1113
|
-
question: props.question,
|
|
1114
|
-
choices: props.choices,
|
|
1115
|
-
answer: props.answer,
|
|
1116
|
-
passingScore: props.passingScore
|
|
1117
|
-
}
|
|
1118
|
-
);
|
|
1129
|
+
// src/components/Quiz.tsx
|
|
1130
|
+
import { forwardRef, useEffect as useEffect3, useId, useMemo as useMemo5, useRef as useRef3, useState as useState3 } from "react";
|
|
1131
|
+
import { visuallyHiddenStyle } from "@lessonkit/accessibility";
|
|
1132
|
+
|
|
1133
|
+
// src/assessment/AssessmentLessonGuard.tsx
|
|
1134
|
+
import { useEffect as useEffect2 } from "react";
|
|
1135
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
1136
|
+
var warnedAssessmentOutsideLesson = false;
|
|
1137
|
+
function resetAssessmentWarningsForTests() {
|
|
1138
|
+
warnedAssessmentOutsideLesson = false;
|
|
1119
1139
|
}
|
|
1120
|
-
function
|
|
1140
|
+
function AssessmentLessonGuard(props) {
|
|
1121
1141
|
const enclosingLessonId = useEnclosingLessonId();
|
|
1122
1142
|
const missingLesson = enclosingLessonId === void 0;
|
|
1123
1143
|
useEffect2(() => {
|
|
1124
1144
|
if (!missingLesson || isDevEnvironment4()) return;
|
|
1125
|
-
if (!
|
|
1126
|
-
|
|
1145
|
+
if (!warnedAssessmentOutsideLesson) {
|
|
1146
|
+
warnedAssessmentOutsideLesson = true;
|
|
1127
1147
|
console.error(
|
|
1128
|
-
|
|
1148
|
+
`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>; assessment telemetry will not be emitted.`
|
|
1129
1149
|
);
|
|
1130
1150
|
}
|
|
1131
|
-
}, [missingLesson]);
|
|
1151
|
+
}, [missingLesson, props.blockLabel]);
|
|
1132
1152
|
if (missingLesson && isDevEnvironment4()) {
|
|
1133
|
-
throw new Error(
|
|
1153
|
+
throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
|
|
1134
1154
|
}
|
|
1135
1155
|
if (missingLesson) {
|
|
1136
|
-
return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label":
|
|
1156
|
+
return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label": `${props.blockLabel} configuration error`, "data-lk-check-id": props.checkId, children: /* @__PURE__ */ jsxs("p", { children: [
|
|
1157
|
+
props.blockLabel,
|
|
1158
|
+
" must be placed inside a Lesson."
|
|
1159
|
+
] }) });
|
|
1137
1160
|
}
|
|
1138
|
-
return /* @__PURE__ */ jsx2(
|
|
1161
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: props.children(enclosingLessonId) });
|
|
1139
1162
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
completedRef.current = false;
|
|
1153
|
-
setQuizPassed(false);
|
|
1154
|
-
setSelected(null);
|
|
1155
|
-
setSelectionCorrect(null);
|
|
1156
|
-
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId, choicesKey]);
|
|
1157
|
-
const isChoiceCorrect = (choice, custom) => {
|
|
1158
|
-
if (!custom) return choice === props.answer;
|
|
1159
|
-
if (custom.passed !== void 0) return custom.passed;
|
|
1160
|
-
if (custom.maxScore != null && custom.maxScore > 0) {
|
|
1161
|
-
return custom.score / custom.maxScore >= 1;
|
|
1162
|
-
}
|
|
1163
|
-
return choice === props.answer;
|
|
1163
|
+
|
|
1164
|
+
// src/assessment/internal/buildAssessmentHandle.ts
|
|
1165
|
+
function buildAssessmentHandle(opts) {
|
|
1166
|
+
return {
|
|
1167
|
+
getScore: opts.getScore,
|
|
1168
|
+
getMaxScore: opts.getMaxScore,
|
|
1169
|
+
getAnswerGiven: opts.getAnswerGiven,
|
|
1170
|
+
resetTask: opts.resetTask,
|
|
1171
|
+
showSolutions: opts.showSolutions,
|
|
1172
|
+
getXAPIData: opts.getXAPIData,
|
|
1173
|
+
...opts.getCurrentState ? { getCurrentState: opts.getCurrentState } : {},
|
|
1174
|
+
...opts.resume ? { resume: opts.resume } : {}
|
|
1164
1175
|
};
|
|
1165
|
-
const passed = quizPassed;
|
|
1166
|
-
return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
1167
|
-
/* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
|
|
1168
|
-
/* @__PURE__ */ jsxs("fieldset", { "aria-labelledby": questionId, children: [
|
|
1169
|
-
/* @__PURE__ */ jsx2("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
|
|
1170
|
-
props.choices.map((c, i) => /* @__PURE__ */ jsxs("label", { style: { display: "block" }, children: [
|
|
1171
|
-
/* @__PURE__ */ jsx2(
|
|
1172
|
-
"input",
|
|
1173
|
-
{
|
|
1174
|
-
type: "radio",
|
|
1175
|
-
name: questionId,
|
|
1176
|
-
value: c,
|
|
1177
|
-
checked: selected === c,
|
|
1178
|
-
disabled: passed,
|
|
1179
|
-
"aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
|
|
1180
|
-
onChange: () => {
|
|
1181
|
-
if (passed) return;
|
|
1182
|
-
setSelected(c);
|
|
1183
|
-
const pluginCtx = buildPluginContext({
|
|
1184
|
-
courseId: config.courseId,
|
|
1185
|
-
sessionId: session.sessionId,
|
|
1186
|
-
attemptId: session.attemptId,
|
|
1187
|
-
user: session.user
|
|
1188
|
-
});
|
|
1189
|
-
const custom = plugins?.scoreAssessment(
|
|
1190
|
-
{
|
|
1191
|
-
checkId,
|
|
1192
|
-
lessonId: enclosingLessonId,
|
|
1193
|
-
response: c
|
|
1194
|
-
},
|
|
1195
|
-
pluginCtx
|
|
1196
|
-
) ?? null;
|
|
1197
|
-
const correct = isChoiceCorrect(c, custom);
|
|
1198
|
-
setSelectionCorrect(correct);
|
|
1199
|
-
quiz.answer({
|
|
1200
|
-
checkId,
|
|
1201
|
-
question: props.question,
|
|
1202
|
-
choice: c,
|
|
1203
|
-
correct
|
|
1204
|
-
});
|
|
1205
|
-
if (correct && !completedRef.current) {
|
|
1206
|
-
completedRef.current = true;
|
|
1207
|
-
setQuizPassed(true);
|
|
1208
|
-
const maxScore = custom?.maxScore ?? 1;
|
|
1209
|
-
quiz.complete({
|
|
1210
|
-
checkId,
|
|
1211
|
-
score: custom?.score ?? 1,
|
|
1212
|
-
maxScore,
|
|
1213
|
-
passingScore: props.passingScore ?? maxScore
|
|
1214
|
-
});
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
),
|
|
1219
|
-
c
|
|
1220
|
-
] }, `${questionId}-${i}`))
|
|
1221
|
-
] }),
|
|
1222
|
-
selected && selectionCorrect !== null ? /* @__PURE__ */ jsx2("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
1223
|
-
] });
|
|
1224
1176
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1177
|
+
|
|
1178
|
+
// src/assessment/internal/resumeState.ts
|
|
1179
|
+
function readBooleanField(state, key) {
|
|
1180
|
+
const value = state[key];
|
|
1181
|
+
if (value === true || value === false || value === null) return value;
|
|
1182
|
+
return void 0;
|
|
1183
|
+
}
|
|
1184
|
+
function readStringField(state, key) {
|
|
1185
|
+
const value = state[key];
|
|
1186
|
+
if (typeof value === "string" || value === null) return value;
|
|
1187
|
+
return void 0;
|
|
1188
|
+
}
|
|
1189
|
+
function readBooleanStateField(state, key, apply) {
|
|
1190
|
+
const value = state[key];
|
|
1191
|
+
if (typeof value === "boolean") apply(value);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// src/assessment/internal/useAssessmentHandleRegistration.ts
|
|
1195
|
+
import { useImperativeHandle as useImperativeHandle2 } from "react";
|
|
1196
|
+
|
|
1197
|
+
// src/compound/CompoundProvider.tsx
|
|
1198
|
+
import React3, { createContext as createContext3, useCallback as useCallback2, useContext as useContext3, useImperativeHandle, useMemo as useMemo4, useRef as useRef2, useState as useState2 } from "react";
|
|
1199
|
+
import { clampCompoundPageIndex, createCompoundResumeState } from "@lessonkit/core";
|
|
1200
|
+
|
|
1201
|
+
// src/compound/aggregateScores.ts
|
|
1202
|
+
function aggregateAssessmentScores(handles) {
|
|
1203
|
+
let score = 0;
|
|
1204
|
+
let maxScore = 0;
|
|
1205
|
+
let allAnswered = true;
|
|
1206
|
+
for (const handle of handles) {
|
|
1207
|
+
score += handle.getScore();
|
|
1208
|
+
maxScore += handle.getMaxScore();
|
|
1209
|
+
if (!handle.getAnswerGiven()) allAnswered = false;
|
|
1210
|
+
}
|
|
1211
|
+
return { score, maxScore, allAnswered };
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// src/compound/resumeChildHandles.ts
|
|
1215
|
+
function resumeChildHandles(handles, childStates, opts) {
|
|
1216
|
+
if (opts?.waitForHandles && handles.size === 0 && Object.keys(childStates).length > 0) {
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
for (const [checkId, handle] of handles) {
|
|
1220
|
+
const child = childStates[checkId];
|
|
1221
|
+
if (child && handle.resume) handle.resume(child);
|
|
1222
|
+
}
|
|
1223
|
+
return true;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// src/compound/CompoundProvider.tsx
|
|
1227
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
1228
|
+
var CompoundRegistryContext = createContext3(null);
|
|
1229
|
+
var CompoundHandlesVersionContext = createContext3(0);
|
|
1230
|
+
function CompoundProvider({
|
|
1231
|
+
children,
|
|
1232
|
+
activePageIndex: _activePageIndex,
|
|
1233
|
+
onActivePageIndexChange: _onActivePageIndexChange
|
|
1234
|
+
}) {
|
|
1235
|
+
const registryRef = useRef2(/* @__PURE__ */ new Map());
|
|
1236
|
+
const [handlesVersion, setHandlesVersion] = useState2(0);
|
|
1237
|
+
const register = useCallback2((checkId, handle) => {
|
|
1238
|
+
const prev = registryRef.current.get(checkId);
|
|
1239
|
+
registryRef.current.set(checkId, handle);
|
|
1240
|
+
if (prev !== handle) {
|
|
1241
|
+
setHandlesVersion((v) => v + 1);
|
|
1242
|
+
}
|
|
1243
|
+
return () => {
|
|
1244
|
+
if (registryRef.current.get(checkId) === handle) {
|
|
1245
|
+
registryRef.current.delete(checkId);
|
|
1246
|
+
setHandlesVersion((v) => v + 1);
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
}, []);
|
|
1250
|
+
const registryValue = useMemo4(
|
|
1251
|
+
() => ({
|
|
1252
|
+
register,
|
|
1253
|
+
getHandles: () => registryRef.current
|
|
1254
|
+
}),
|
|
1255
|
+
[register]
|
|
1256
|
+
);
|
|
1257
|
+
return /* @__PURE__ */ jsx3(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ jsx3(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) });
|
|
1258
|
+
}
|
|
1259
|
+
function useCompoundRegistry() {
|
|
1260
|
+
const registry = useContext3(CompoundRegistryContext);
|
|
1261
|
+
const handlesVersion = useContext3(CompoundHandlesVersionContext);
|
|
1262
|
+
if (!registry) return null;
|
|
1263
|
+
return { ...registry, handlesVersion };
|
|
1264
|
+
}
|
|
1265
|
+
function useCompoundHandlesVersion() {
|
|
1266
|
+
return useContext3(CompoundHandlesVersionContext);
|
|
1267
|
+
}
|
|
1268
|
+
function useRegisterAssessmentHandle(checkId, handle) {
|
|
1269
|
+
const registry = useContext3(CompoundRegistryContext);
|
|
1270
|
+
React3.useEffect(() => {
|
|
1271
|
+
if (!registry || !handle) return;
|
|
1272
|
+
return registry.register(checkId, handle);
|
|
1273
|
+
}, [registry, checkId, handle]);
|
|
1274
|
+
}
|
|
1275
|
+
function useCompoundHandleRef(ref, opts) {
|
|
1276
|
+
const { activePageIndex, setActivePageIndex, getHandles, pageCount } = opts;
|
|
1277
|
+
const setIndexClamped = useCallback2(
|
|
1278
|
+
(index) => {
|
|
1279
|
+
const next = pageCount !== void 0 ? clampCompoundPageIndex(index, pageCount) : Math.max(0, Math.floor(index));
|
|
1280
|
+
setActivePageIndex(next);
|
|
1281
|
+
},
|
|
1282
|
+
[pageCount, setActivePageIndex]
|
|
1283
|
+
);
|
|
1284
|
+
useImperativeHandle(
|
|
1285
|
+
ref,
|
|
1286
|
+
() => ({
|
|
1287
|
+
getScore: () => aggregateAssessmentScores(getHandles().values()).score,
|
|
1288
|
+
getMaxScore: () => aggregateAssessmentScores(getHandles().values()).maxScore,
|
|
1289
|
+
getAnswerGiven: () => aggregateAssessmentScores(getHandles().values()).allAnswered,
|
|
1290
|
+
resetTask: () => {
|
|
1291
|
+
for (const handle of getHandles().values()) handle.resetTask();
|
|
1292
|
+
},
|
|
1293
|
+
showSolutions: () => {
|
|
1294
|
+
if (!opts.enableSolutionsButton) return;
|
|
1295
|
+
for (const handle of getHandles().values()) handle.showSolutions();
|
|
1296
|
+
},
|
|
1297
|
+
getCurrentState: () => {
|
|
1298
|
+
const childStates = {};
|
|
1299
|
+
for (const [checkId, handle] of getHandles()) {
|
|
1300
|
+
if (handle.getCurrentState) {
|
|
1301
|
+
childStates[checkId] = handle.getCurrentState();
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return createCompoundResumeState({ activePageIndex, childStates });
|
|
1305
|
+
},
|
|
1306
|
+
resume: (state) => {
|
|
1307
|
+
setIndexClamped(state.activePageIndex);
|
|
1308
|
+
resumeChildHandles(getHandles(), state.childStates);
|
|
1309
|
+
}
|
|
1310
|
+
}),
|
|
1311
|
+
[activePageIndex, setIndexClamped, getHandles, opts.enableSolutionsButton]
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/assessment/internal/useAssessmentHandleRegistration.ts
|
|
1316
|
+
function useAssessmentHandleRegistration(checkId, handle, ref) {
|
|
1317
|
+
useImperativeHandle2(ref, () => handle, [handle]);
|
|
1318
|
+
useRegisterAssessmentHandle(checkId, handle);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// src/assessment/internal/usePluginScoring.ts
|
|
1322
|
+
import { useCallback as useCallback3 } from "react";
|
|
1323
|
+
|
|
1324
|
+
// src/assessment/scoring.ts
|
|
1325
|
+
function resolvePassingThreshold(passingScore, maxScore) {
|
|
1326
|
+
return passingScore ?? maxScore;
|
|
1327
|
+
}
|
|
1328
|
+
function meetsPassingThreshold(score, maxScore, passingScore) {
|
|
1329
|
+
const threshold = resolvePassingThreshold(passingScore, maxScore);
|
|
1330
|
+
return score >= threshold;
|
|
1331
|
+
}
|
|
1332
|
+
function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
|
|
1333
|
+
const maxScore = custom?.maxScore ?? fallbackMax;
|
|
1334
|
+
if (custom?.passed !== void 0) {
|
|
1335
|
+
const score2 = custom.passed ? custom.score ?? maxScore : custom.score ?? 0;
|
|
1336
|
+
return { score: score2, maxScore, passed: custom.passed };
|
|
1337
|
+
}
|
|
1338
|
+
if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
1339
|
+
const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
|
|
1340
|
+
return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
|
|
1341
|
+
}
|
|
1342
|
+
const score = fallbackCorrect ? maxScore : 0;
|
|
1343
|
+
const passed = meetsPassingThreshold(score, maxScore, passingScore);
|
|
1344
|
+
return { score, maxScore, passed };
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/assessment/internal/usePluginScoring.ts
|
|
1348
|
+
function usePluginScoring(checkId, lessonId) {
|
|
1349
|
+
const { plugins, config, session } = useLessonkit();
|
|
1350
|
+
const getPluginScore = useCallback3(
|
|
1351
|
+
(response) => {
|
|
1352
|
+
const pluginCtx = buildPluginContext({
|
|
1353
|
+
courseId: config.courseId,
|
|
1354
|
+
sessionId: session.sessionId,
|
|
1355
|
+
attemptId: session.attemptId,
|
|
1356
|
+
user: session.user
|
|
1357
|
+
});
|
|
1358
|
+
return plugins?.scoreAssessment({ checkId, lessonId, response }, pluginCtx) ?? null;
|
|
1359
|
+
},
|
|
1360
|
+
[checkId, config.courseId, lessonId, plugins, session.attemptId, session.sessionId, session.user]
|
|
1361
|
+
);
|
|
1362
|
+
const scoreResponse = useCallback3(
|
|
1363
|
+
(response, defaultCorrect, maxScore = 1, passingScore) => scoreFromCustom(getPluginScore(response), defaultCorrect, maxScore, passingScore),
|
|
1364
|
+
[getPluginScore]
|
|
1365
|
+
);
|
|
1366
|
+
const isChoiceCorrect = useCallback3(
|
|
1367
|
+
(choice, answer, custom, passingScore) => {
|
|
1368
|
+
if (!custom) return choice === answer;
|
|
1369
|
+
if (custom.passed !== void 0) return custom.passed;
|
|
1370
|
+
if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
1371
|
+
return meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
|
|
1372
|
+
}
|
|
1373
|
+
return choice === answer;
|
|
1374
|
+
},
|
|
1375
|
+
[]
|
|
1376
|
+
);
|
|
1377
|
+
return { getPluginScore, scoreResponse, isChoiceCorrect };
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/components/Quiz.tsx
|
|
1381
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1382
|
+
function QuizInner(props, ref) {
|
|
1383
|
+
const { enclosingLessonId } = props;
|
|
1384
|
+
const checkId = useMemo5(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1385
|
+
const quiz = useQuizState(enclosingLessonId);
|
|
1386
|
+
const { getPluginScore, isChoiceCorrect } = usePluginScoring(checkId, enclosingLessonId);
|
|
1387
|
+
const [selected, setSelected] = useState3(null);
|
|
1388
|
+
const [selectionCorrect, setSelectionCorrect] = useState3(null);
|
|
1389
|
+
const [quizPassed, setQuizPassed] = useState3(false);
|
|
1390
|
+
const completedRef = useRef3(false);
|
|
1391
|
+
const questionId = useId();
|
|
1392
|
+
const choicesKey = props.choices.join("\0");
|
|
1393
|
+
useEffect3(() => {
|
|
1394
|
+
completedRef.current = false;
|
|
1395
|
+
setQuizPassed(false);
|
|
1396
|
+
setSelected(null);
|
|
1397
|
+
setSelectionCorrect(null);
|
|
1398
|
+
}, [checkId, props.answer, props.question, choicesKey]);
|
|
1399
|
+
const passed = quizPassed;
|
|
1400
|
+
const handle = useMemo5(
|
|
1401
|
+
() => buildAssessmentHandle({
|
|
1402
|
+
checkId,
|
|
1403
|
+
getScore: () => {
|
|
1404
|
+
const maxScore = 1;
|
|
1405
|
+
if (quizPassed && selected !== null) return maxScore;
|
|
1406
|
+
if (selected === null) return 0;
|
|
1407
|
+
return selectionCorrect ? maxScore : 0;
|
|
1408
|
+
},
|
|
1409
|
+
getMaxScore: () => 1,
|
|
1410
|
+
getAnswerGiven: () => selected !== null,
|
|
1411
|
+
resetTask: () => {
|
|
1412
|
+
completedRef.current = false;
|
|
1413
|
+
setQuizPassed(false);
|
|
1414
|
+
setSelected(null);
|
|
1415
|
+
setSelectionCorrect(null);
|
|
1416
|
+
},
|
|
1417
|
+
showSolutions: () => {
|
|
1418
|
+
},
|
|
1419
|
+
getXAPIData: () => ({
|
|
1420
|
+
checkId,
|
|
1421
|
+
interactionType: "mcq",
|
|
1422
|
+
response: selected ?? void 0,
|
|
1423
|
+
correct: selectionCorrect ?? void 0,
|
|
1424
|
+
score: quizPassed && selected !== null ? 1 : selected === null ? 0 : selectionCorrect ? 1 : 0,
|
|
1425
|
+
maxScore: 1
|
|
1426
|
+
}),
|
|
1427
|
+
getCurrentState: () => ({ selected, selectionCorrect, quizPassed }),
|
|
1428
|
+
resume: (state) => {
|
|
1429
|
+
const nextSelected = readStringField(state, "selected");
|
|
1430
|
+
if (typeof nextSelected === "string" || nextSelected === null) setSelected(nextSelected);
|
|
1431
|
+
const nextCorrect = readBooleanField(state, "selectionCorrect");
|
|
1432
|
+
if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
|
|
1433
|
+
setSelectionCorrect(nextCorrect);
|
|
1434
|
+
}
|
|
1435
|
+
readBooleanStateField(state, "quizPassed", (value) => {
|
|
1436
|
+
setQuizPassed(value);
|
|
1437
|
+
completedRef.current = value;
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
}),
|
|
1441
|
+
[checkId, quizPassed, selected, selectionCorrect]
|
|
1442
|
+
);
|
|
1443
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1444
|
+
return /* @__PURE__ */ jsxs2("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
1445
|
+
/* @__PURE__ */ jsx4("p", { id: questionId, children: props.question }),
|
|
1446
|
+
/* @__PURE__ */ jsxs2("fieldset", { "aria-labelledby": questionId, children: [
|
|
1447
|
+
/* @__PURE__ */ jsx4("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
|
|
1448
|
+
props.choices.map((c, i) => /* @__PURE__ */ jsxs2("label", { style: { display: "block" }, children: [
|
|
1449
|
+
/* @__PURE__ */ jsx4(
|
|
1450
|
+
"input",
|
|
1451
|
+
{
|
|
1452
|
+
type: "radio",
|
|
1453
|
+
name: questionId,
|
|
1454
|
+
value: c,
|
|
1455
|
+
checked: selected === c,
|
|
1456
|
+
disabled: passed,
|
|
1457
|
+
"aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
|
|
1458
|
+
onChange: () => {
|
|
1459
|
+
if (passed) return;
|
|
1460
|
+
setSelected(c);
|
|
1461
|
+
const custom = getPluginScore(c);
|
|
1462
|
+
const correct = isChoiceCorrect(c, props.answer, custom, props.passingScore);
|
|
1463
|
+
setSelectionCorrect(correct);
|
|
1464
|
+
quiz.answer({
|
|
1465
|
+
checkId,
|
|
1466
|
+
question: props.question,
|
|
1467
|
+
choice: c,
|
|
1468
|
+
correct
|
|
1469
|
+
});
|
|
1470
|
+
if (correct && !completedRef.current) {
|
|
1471
|
+
completedRef.current = true;
|
|
1472
|
+
setQuizPassed(true);
|
|
1473
|
+
const maxScore = custom?.maxScore ?? 1;
|
|
1474
|
+
quiz.complete({
|
|
1475
|
+
checkId,
|
|
1476
|
+
score: custom?.score ?? maxScore,
|
|
1477
|
+
maxScore,
|
|
1478
|
+
passingScore: props.passingScore ?? maxScore
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
),
|
|
1484
|
+
c
|
|
1485
|
+
] }, `${questionId}-${i}`))
|
|
1486
|
+
] }),
|
|
1487
|
+
selected && selectionCorrect !== null ? /* @__PURE__ */ jsx4("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
1488
|
+
] });
|
|
1489
|
+
}
|
|
1490
|
+
var QuizInnerForwarded = forwardRef(QuizInner);
|
|
1491
|
+
var Quiz = forwardRef(function Quiz2(props, ref) {
|
|
1492
|
+
return /* @__PURE__ */ jsx4(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx4(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1493
|
+
});
|
|
1494
|
+
function KnowledgeCheck(props) {
|
|
1495
|
+
return /* @__PURE__ */ jsx4(
|
|
1496
|
+
Quiz,
|
|
1497
|
+
{
|
|
1498
|
+
checkId: props.checkId,
|
|
1499
|
+
question: props.question,
|
|
1500
|
+
choices: props.choices,
|
|
1501
|
+
answer: props.answer,
|
|
1502
|
+
passingScore: props.passingScore
|
|
1503
|
+
}
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
function resetQuizWarningsForTests() {
|
|
1507
|
+
resetAssessmentWarningsForTests();
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// src/components.tsx
|
|
1511
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1512
|
+
function Course(props) {
|
|
1513
|
+
const courseId = useMemo6(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
|
|
1514
|
+
const providerConfig = useMemo6(
|
|
1515
|
+
() => ({ ...props.config, courseId }),
|
|
1516
|
+
[props.config, courseId]
|
|
1517
|
+
);
|
|
1518
|
+
return /* @__PURE__ */ jsx5(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs3("section", { "aria-label": props.title, children: [
|
|
1519
|
+
/* @__PURE__ */ jsx5("h1", { children: props.title }),
|
|
1520
|
+
/* @__PURE__ */ jsx5("div", { children: props.children })
|
|
1521
|
+
] }) });
|
|
1522
|
+
}
|
|
1523
|
+
function Lesson(props) {
|
|
1524
|
+
const lessonId = useMemo6(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
1525
|
+
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
1526
|
+
const { setActiveLesson, config } = useLessonkit();
|
|
1527
|
+
const { completeLesson } = useCompletion();
|
|
1528
|
+
const lessonMountGenerationRef = useRef4(0);
|
|
1529
|
+
const liveCourseIdRef = useRef4(config.courseId);
|
|
1530
|
+
liveCourseIdRef.current = config.courseId;
|
|
1531
|
+
useEffect4(() => {
|
|
1532
|
+
const unregister = registerLessonMount(lessonId);
|
|
1533
|
+
const generation = ++lessonMountGenerationRef.current;
|
|
1534
|
+
const mountedCourseId = config.courseId;
|
|
1535
|
+
let effectSurvivedTick = false;
|
|
1536
|
+
queueMicrotask(() => {
|
|
1537
|
+
queueMicrotask(() => {
|
|
1538
|
+
effectSurvivedTick = true;
|
|
1539
|
+
});
|
|
1540
|
+
});
|
|
1541
|
+
setActiveLesson(lessonId);
|
|
1542
|
+
return () => {
|
|
1543
|
+
unregister();
|
|
1544
|
+
if (getLessonMountCount(lessonId) > 0) {
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
if (!autoComplete) return;
|
|
1548
|
+
queueMicrotask(() => {
|
|
1549
|
+
if (!effectSurvivedTick) return;
|
|
1550
|
+
if (lessonMountGenerationRef.current !== generation) return;
|
|
1551
|
+
if (liveCourseIdRef.current !== mountedCourseId) return;
|
|
1552
|
+
completeLesson(lessonId, { courseId: mountedCourseId });
|
|
1553
|
+
});
|
|
1554
|
+
};
|
|
1555
|
+
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
1556
|
+
return /* @__PURE__ */ jsx5(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs3("article", { "aria-label": props.title, children: [
|
|
1557
|
+
/* @__PURE__ */ jsx5("h2", { children: props.title }),
|
|
1558
|
+
/* @__PURE__ */ jsx5("div", { children: props.children })
|
|
1559
|
+
] }) });
|
|
1560
|
+
}
|
|
1561
|
+
function Scenario(props) {
|
|
1562
|
+
const blockId = useMemo6(
|
|
1563
|
+
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1564
|
+
[props.blockId]
|
|
1565
|
+
);
|
|
1566
|
+
return /* @__PURE__ */ jsx5("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
1567
|
+
}
|
|
1568
|
+
function Reflection(props) {
|
|
1569
|
+
const blockId = useMemo6(
|
|
1570
|
+
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1571
|
+
[props.blockId]
|
|
1572
|
+
);
|
|
1573
|
+
const promptId = useId2();
|
|
1574
|
+
const hintId = useId2();
|
|
1575
|
+
const [internalValue, setInternalValue] = useState4("");
|
|
1576
|
+
const isControlled = props.value !== void 0;
|
|
1577
|
+
const value = isControlled ? props.value : internalValue;
|
|
1578
|
+
const handleChange = (event) => {
|
|
1579
|
+
if (!isControlled) setInternalValue(event.target.value);
|
|
1580
|
+
props.onChange?.(event.target.value);
|
|
1581
|
+
};
|
|
1582
|
+
return /* @__PURE__ */ jsxs3("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
|
|
1583
|
+
props.prompt ? /* @__PURE__ */ jsx5("p", { id: promptId, children: props.prompt }) : null,
|
|
1584
|
+
props.hint ? /* @__PURE__ */ jsx5("p", { id: hintId, style: visuallyHiddenStyle2, children: props.hint }) : null,
|
|
1585
|
+
props.children,
|
|
1586
|
+
/* @__PURE__ */ jsx5(
|
|
1587
|
+
"textarea",
|
|
1588
|
+
{
|
|
1589
|
+
value,
|
|
1590
|
+
onChange: handleChange,
|
|
1591
|
+
"aria-labelledby": props.prompt ? promptId : void 0,
|
|
1592
|
+
"aria-describedby": props.hint ? hintId : void 0,
|
|
1593
|
+
"aria-label": props.prompt ? void 0 : "Reflection response"
|
|
1594
|
+
}
|
|
1595
|
+
)
|
|
1596
|
+
] });
|
|
1597
|
+
}
|
|
1598
|
+
function ProgressTracker(props) {
|
|
1599
|
+
const { progress } = useLessonkit();
|
|
1600
|
+
const completed = progress.completedLessonIds.size;
|
|
1601
|
+
if (props.totalLessons != null) {
|
|
1602
|
+
const total = props.totalLessons;
|
|
1603
|
+
const displayed = Math.min(completed, total);
|
|
1604
|
+
return /* @__PURE__ */ jsx5("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx5(
|
|
1605
|
+
"div",
|
|
1606
|
+
{
|
|
1234
1607
|
role: "progressbar",
|
|
1235
1608
|
"aria-valuemin": 0,
|
|
1236
1609
|
"aria-valuemax": total,
|
|
1237
1610
|
"aria-valuenow": displayed,
|
|
1238
1611
|
"aria-label": "Lessons completed",
|
|
1239
|
-
children: /* @__PURE__ */
|
|
1612
|
+
children: /* @__PURE__ */ jsxs3("p", { children: [
|
|
1240
1613
|
"Lessons completed: ",
|
|
1241
1614
|
displayed,
|
|
1242
1615
|
" of ",
|
|
@@ -1245,11 +1618,1806 @@ function ProgressTracker(props) {
|
|
|
1245
1618
|
}
|
|
1246
1619
|
) });
|
|
1247
1620
|
}
|
|
1248
|
-
return /* @__PURE__ */
|
|
1249
|
-
"Lessons completed: ",
|
|
1250
|
-
completed
|
|
1251
|
-
] }) });
|
|
1621
|
+
return /* @__PURE__ */ jsx5("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs3("p", { children: [
|
|
1622
|
+
"Lessons completed: ",
|
|
1623
|
+
completed
|
|
1624
|
+
] }) });
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// src/blocks/TrueFalse.tsx
|
|
1628
|
+
import React7, { forwardRef as forwardRef2, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef5, useState as useState5 } from "react";
|
|
1629
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1630
|
+
var INTERACTION = "trueFalse";
|
|
1631
|
+
function TrueFalseInner(props, ref) {
|
|
1632
|
+
const { enclosingLessonId } = props;
|
|
1633
|
+
const checkId = useMemo7(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1634
|
+
const assessment = useAssessmentState(enclosingLessonId);
|
|
1635
|
+
const { config } = useLessonkit();
|
|
1636
|
+
const { scoreResponse } = usePluginScoring(checkId, enclosingLessonId);
|
|
1637
|
+
const [selected, setSelected] = useState5(null);
|
|
1638
|
+
const [selectionCorrect, setSelectionCorrect] = useState5(null);
|
|
1639
|
+
const [showSolutions, setShowSolutions] = useState5(false);
|
|
1640
|
+
const [passed, setPassed] = useState5(false);
|
|
1641
|
+
const completedRef = useRef5(false);
|
|
1642
|
+
const questionId = React7.useId();
|
|
1643
|
+
const reset = () => {
|
|
1644
|
+
completedRef.current = false;
|
|
1645
|
+
setPassed(false);
|
|
1646
|
+
setSelected(null);
|
|
1647
|
+
setSelectionCorrect(null);
|
|
1648
|
+
setShowSolutions(false);
|
|
1649
|
+
};
|
|
1650
|
+
useEffect5(() => {
|
|
1651
|
+
reset();
|
|
1652
|
+
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
|
|
1653
|
+
const handle = useMemo7(
|
|
1654
|
+
() => buildAssessmentHandle({
|
|
1655
|
+
checkId,
|
|
1656
|
+
getScore: () => {
|
|
1657
|
+
const maxScore = 1;
|
|
1658
|
+
return passed ? maxScore : selected === null ? 0 : selected === props.answer ? maxScore : 0;
|
|
1659
|
+
},
|
|
1660
|
+
getMaxScore: () => 1,
|
|
1661
|
+
getAnswerGiven: () => selected !== null,
|
|
1662
|
+
resetTask: reset,
|
|
1663
|
+
showSolutions: () => setShowSolutions(true),
|
|
1664
|
+
getXAPIData: () => ({
|
|
1665
|
+
checkId,
|
|
1666
|
+
interactionType: INTERACTION,
|
|
1667
|
+
response: selected ?? void 0,
|
|
1668
|
+
correct: selected === props.answer,
|
|
1669
|
+
score: passed ? 1 : selected === null ? 0 : selected === props.answer ? 1 : 0,
|
|
1670
|
+
maxScore: 1
|
|
1671
|
+
}),
|
|
1672
|
+
getCurrentState: () => ({ selected, selectionCorrect, passed, showSolutions }),
|
|
1673
|
+
resume: (state) => {
|
|
1674
|
+
const nextSelected = readBooleanField(state, "selected");
|
|
1675
|
+
if (nextSelected === true || nextSelected === false || nextSelected === null) {
|
|
1676
|
+
setSelected(nextSelected);
|
|
1677
|
+
}
|
|
1678
|
+
const nextCorrect = readBooleanField(state, "selectionCorrect");
|
|
1679
|
+
if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
|
|
1680
|
+
setSelectionCorrect(nextCorrect);
|
|
1681
|
+
}
|
|
1682
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
1683
|
+
setPassed(value);
|
|
1684
|
+
completedRef.current = value;
|
|
1685
|
+
});
|
|
1686
|
+
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
1687
|
+
}
|
|
1688
|
+
}),
|
|
1689
|
+
[checkId, passed, props.answer, selected, selectionCorrect, showSolutions]
|
|
1690
|
+
);
|
|
1691
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1692
|
+
const submit = (value) => {
|
|
1693
|
+
if (passed && !props.enableRetry) return;
|
|
1694
|
+
setSelected(value);
|
|
1695
|
+
const correct = value === props.answer;
|
|
1696
|
+
const scored = scoreResponse(value, correct, 1, props.passingScore);
|
|
1697
|
+
setSelectionCorrect(scored.passed);
|
|
1698
|
+
assessment.answer({
|
|
1699
|
+
checkId,
|
|
1700
|
+
interactionType: INTERACTION,
|
|
1701
|
+
question: props.question,
|
|
1702
|
+
response: value,
|
|
1703
|
+
correct: scored.passed
|
|
1704
|
+
});
|
|
1705
|
+
if (scored.passed && !completedRef.current) {
|
|
1706
|
+
completedRef.current = true;
|
|
1707
|
+
setPassed(true);
|
|
1708
|
+
assessment.complete({
|
|
1709
|
+
checkId,
|
|
1710
|
+
interactionType: INTERACTION,
|
|
1711
|
+
score: scored.score,
|
|
1712
|
+
maxScore: scored.maxScore,
|
|
1713
|
+
passingScore: props.passingScore ?? scored.maxScore
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
};
|
|
1717
|
+
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
1718
|
+
return /* @__PURE__ */ jsxs4("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
|
|
1719
|
+
/* @__PURE__ */ jsx6("p", { id: questionId, children: props.question }),
|
|
1720
|
+
/* @__PURE__ */ jsxs4("fieldset", { "aria-labelledby": questionId, children: [
|
|
1721
|
+
/* @__PURE__ */ jsx6("legend", { className: "lk-visually-hidden", children: "True or False" }),
|
|
1722
|
+
/* @__PURE__ */ jsxs4("label", { style: { display: "block", marginRight: "1rem" }, children: [
|
|
1723
|
+
/* @__PURE__ */ jsx6(
|
|
1724
|
+
"input",
|
|
1725
|
+
{
|
|
1726
|
+
type: "radio",
|
|
1727
|
+
name: `${questionId}-tf`,
|
|
1728
|
+
checked: selected === true,
|
|
1729
|
+
disabled: passed && !props.enableRetry,
|
|
1730
|
+
onChange: () => submit(true)
|
|
1731
|
+
}
|
|
1732
|
+
),
|
|
1733
|
+
"True"
|
|
1734
|
+
] }),
|
|
1735
|
+
/* @__PURE__ */ jsxs4("label", { style: { display: "block" }, children: [
|
|
1736
|
+
/* @__PURE__ */ jsx6(
|
|
1737
|
+
"input",
|
|
1738
|
+
{
|
|
1739
|
+
type: "radio",
|
|
1740
|
+
name: `${questionId}-tf`,
|
|
1741
|
+
checked: selected === false,
|
|
1742
|
+
disabled: passed && !props.enableRetry,
|
|
1743
|
+
onChange: () => submit(false)
|
|
1744
|
+
}
|
|
1745
|
+
),
|
|
1746
|
+
"False"
|
|
1747
|
+
] })
|
|
1748
|
+
] }),
|
|
1749
|
+
reveal ? /* @__PURE__ */ jsxs4("p", { children: [
|
|
1750
|
+
"Correct answer: ",
|
|
1751
|
+
/* @__PURE__ */ jsx6("strong", { children: props.answer ? "True" : "False" })
|
|
1752
|
+
] }) : null,
|
|
1753
|
+
selected !== null && selectionCorrect !== null ? /* @__PURE__ */ jsx6("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
|
|
1754
|
+
props.enableRetry && passed ? /* @__PURE__ */ jsx6("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
1755
|
+
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx6("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1756
|
+
] });
|
|
1757
|
+
}
|
|
1758
|
+
var TrueFalseInnerForwarded = forwardRef2(TrueFalseInner);
|
|
1759
|
+
var TrueFalse = forwardRef2(function TrueFalse2(props, ref) {
|
|
1760
|
+
return /* @__PURE__ */ jsx6(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx6(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
// src/blocks/MarkTheWords.tsx
|
|
1764
|
+
import React8, { forwardRef as forwardRef3, useEffect as useEffect6, useMemo as useMemo8, useRef as useRef6, useState as useState6 } from "react";
|
|
1765
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1766
|
+
var INTERACTION2 = "markTheWords";
|
|
1767
|
+
function tokenize(text) {
|
|
1768
|
+
return text.split(/(\s+)/).filter((t) => t.length > 0);
|
|
1769
|
+
}
|
|
1770
|
+
function MarkTheWordsInner(props, ref) {
|
|
1771
|
+
const checkId = useMemo8(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1772
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1773
|
+
const tokens = useMemo8(() => tokenize(props.text), [props.text]);
|
|
1774
|
+
const correctSet = useMemo8(
|
|
1775
|
+
() => new Set(props.correctWords.map((w) => w.toLowerCase())),
|
|
1776
|
+
[props.correctWords]
|
|
1777
|
+
);
|
|
1778
|
+
const [marked, setMarked] = useState6(() => /* @__PURE__ */ new Set());
|
|
1779
|
+
const [passed, setPassed] = useState6(false);
|
|
1780
|
+
const [showSolutions, setShowSolutions] = useState6(false);
|
|
1781
|
+
const completedRef = useRef6(false);
|
|
1782
|
+
const reset = () => {
|
|
1783
|
+
completedRef.current = false;
|
|
1784
|
+
setPassed(false);
|
|
1785
|
+
setMarked(/* @__PURE__ */ new Set());
|
|
1786
|
+
setShowSolutions(false);
|
|
1787
|
+
};
|
|
1788
|
+
useEffect6(() => {
|
|
1789
|
+
reset();
|
|
1790
|
+
}, [checkId, props.text, props.correctWords.join("\0")]);
|
|
1791
|
+
const selectableIndices = useMemo8(() => {
|
|
1792
|
+
const indices = [];
|
|
1793
|
+
tokens.forEach((t, i) => {
|
|
1794
|
+
if (!/^\s+$/.test(t) && correctSet.has(t.toLowerCase())) indices.push(i);
|
|
1795
|
+
});
|
|
1796
|
+
return indices;
|
|
1797
|
+
}, [tokens, correctSet]);
|
|
1798
|
+
const hasTargets = selectableIndices.length > 0;
|
|
1799
|
+
const allMarked = hasTargets && selectableIndices.every((i) => marked.has(i));
|
|
1800
|
+
const maxScore = selectableIndices.length;
|
|
1801
|
+
const score = allMarked ? maxScore : marked.size;
|
|
1802
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
1803
|
+
const handle = useMemo8(
|
|
1804
|
+
() => buildAssessmentHandle({
|
|
1805
|
+
checkId,
|
|
1806
|
+
getScore: () => score,
|
|
1807
|
+
getMaxScore: () => maxScore || 1,
|
|
1808
|
+
getAnswerGiven: () => marked.size > 0,
|
|
1809
|
+
resetTask: reset,
|
|
1810
|
+
showSolutions: () => setShowSolutions(true),
|
|
1811
|
+
getXAPIData: () => ({
|
|
1812
|
+
checkId,
|
|
1813
|
+
interactionType: INTERACTION2,
|
|
1814
|
+
response: [...marked].map((i) => tokens[i]),
|
|
1815
|
+
correct: passedThreshold,
|
|
1816
|
+
score,
|
|
1817
|
+
maxScore: maxScore || 1
|
|
1818
|
+
}),
|
|
1819
|
+
getCurrentState: () => ({ marked: [...marked], passed, showSolutions }),
|
|
1820
|
+
resume: (state) => {
|
|
1821
|
+
const raw = state.marked;
|
|
1822
|
+
if (Array.isArray(raw)) setMarked(new Set(raw.filter((i) => typeof i === "number")));
|
|
1823
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
1824
|
+
setPassed(value);
|
|
1825
|
+
completedRef.current = value;
|
|
1826
|
+
});
|
|
1827
|
+
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
1828
|
+
}
|
|
1829
|
+
}),
|
|
1830
|
+
[checkId, marked, maxScore, passed, passedThreshold, score, showSolutions, tokens]
|
|
1831
|
+
);
|
|
1832
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1833
|
+
const toggle = (index) => {
|
|
1834
|
+
if (passed && !props.enableRetry) return;
|
|
1835
|
+
setMarked((prev) => {
|
|
1836
|
+
const next = new Set(prev);
|
|
1837
|
+
if (next.has(index)) next.delete(index);
|
|
1838
|
+
else next.add(index);
|
|
1839
|
+
return next;
|
|
1840
|
+
});
|
|
1841
|
+
};
|
|
1842
|
+
useEffect6(() => {
|
|
1843
|
+
if (!hasTargets) {
|
|
1844
|
+
if (isDevEnvironment4()) {
|
|
1845
|
+
console.warn(
|
|
1846
|
+
"[lessonkit] MarkTheWords: no tokens match correctWords",
|
|
1847
|
+
props.correctWords
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
if (!passedThreshold || completedRef.current) return;
|
|
1853
|
+
completedRef.current = true;
|
|
1854
|
+
setPassed(true);
|
|
1855
|
+
assessment.answer({
|
|
1856
|
+
checkId,
|
|
1857
|
+
interactionType: INTERACTION2,
|
|
1858
|
+
question: props.text,
|
|
1859
|
+
response: [...marked].map((i) => tokens[i]),
|
|
1860
|
+
correct: true
|
|
1861
|
+
});
|
|
1862
|
+
assessment.complete({
|
|
1863
|
+
checkId,
|
|
1864
|
+
interactionType: INTERACTION2,
|
|
1865
|
+
score,
|
|
1866
|
+
maxScore,
|
|
1867
|
+
passingScore: props.passingScore ?? maxScore
|
|
1868
|
+
});
|
|
1869
|
+
}, [
|
|
1870
|
+
assessment,
|
|
1871
|
+
checkId,
|
|
1872
|
+
hasTargets,
|
|
1873
|
+
marked,
|
|
1874
|
+
maxScore,
|
|
1875
|
+
passedThreshold,
|
|
1876
|
+
props.passingScore,
|
|
1877
|
+
props.correctWords,
|
|
1878
|
+
props.text,
|
|
1879
|
+
score,
|
|
1880
|
+
tokens
|
|
1881
|
+
]);
|
|
1882
|
+
return /* @__PURE__ */ jsxs5("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
|
|
1883
|
+
!hasTargets ? /* @__PURE__ */ jsxs5("p", { role: "alert", children: [
|
|
1884
|
+
"No words in this sentence match ",
|
|
1885
|
+
/* @__PURE__ */ jsx7("code", { children: "correctWords" }),
|
|
1886
|
+
". Check spelling and capitalization in the source text."
|
|
1887
|
+
] }) : null,
|
|
1888
|
+
/* @__PURE__ */ jsx7("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
|
|
1889
|
+
/* @__PURE__ */ jsx7("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
|
|
1890
|
+
const isWord = !/^\s+$/.test(token);
|
|
1891
|
+
const isTarget = isWord && correctSet.has(token.toLowerCase());
|
|
1892
|
+
if (!isTarget) return /* @__PURE__ */ jsx7(React8.Fragment, { children: token }, i);
|
|
1893
|
+
const selected = marked.has(i);
|
|
1894
|
+
const solution = showSolutions || passed && props.enableSolutionsButton;
|
|
1895
|
+
return /* @__PURE__ */ jsx7(
|
|
1896
|
+
"button",
|
|
1897
|
+
{
|
|
1898
|
+
type: "button",
|
|
1899
|
+
"data-testid": `mark-word-${i}`,
|
|
1900
|
+
"aria-pressed": selected,
|
|
1901
|
+
disabled: passed && !props.enableRetry,
|
|
1902
|
+
onClick: () => toggle(i),
|
|
1903
|
+
style: {
|
|
1904
|
+
margin: "0 0.1em",
|
|
1905
|
+
textDecoration: solution ? "underline" : void 0,
|
|
1906
|
+
fontWeight: selected || solution ? "bold" : void 0
|
|
1907
|
+
},
|
|
1908
|
+
children: token
|
|
1909
|
+
},
|
|
1910
|
+
i
|
|
1911
|
+
);
|
|
1912
|
+
}) }),
|
|
1913
|
+
allMarked ? /* @__PURE__ */ jsx7("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
|
|
1914
|
+
props.enableRetry && passed ? /* @__PURE__ */ jsx7("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
1915
|
+
props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ jsx7("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1916
|
+
] });
|
|
1917
|
+
}
|
|
1918
|
+
var MarkTheWordsInnerForwarded = forwardRef3(MarkTheWordsInner);
|
|
1919
|
+
var MarkTheWords = forwardRef3(function MarkTheWords2(props, ref) {
|
|
1920
|
+
return /* @__PURE__ */ jsx7(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx7(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1921
|
+
});
|
|
1922
|
+
|
|
1923
|
+
// src/blocks/FillInTheBlanks.tsx
|
|
1924
|
+
import React9, { forwardRef as forwardRef4, useEffect as useEffect7, useMemo as useMemo9, useRef as useRef7, useState as useState7 } from "react";
|
|
1925
|
+
|
|
1926
|
+
// src/assessment/internal/parseStarDelimitedTemplate.ts
|
|
1927
|
+
function parseStarDelimitedTemplate(template, idPrefix) {
|
|
1928
|
+
const parts = [];
|
|
1929
|
+
const values = [];
|
|
1930
|
+
const re = /\*([^*]+)\*/g;
|
|
1931
|
+
let last = 0;
|
|
1932
|
+
let match;
|
|
1933
|
+
let n = 0;
|
|
1934
|
+
while ((match = re.exec(template)) !== null) {
|
|
1935
|
+
parts.push(template.slice(last, match.index));
|
|
1936
|
+
values.push(match[1].trim());
|
|
1937
|
+
parts.push(`${idPrefix}-${n++}`);
|
|
1938
|
+
last = match.index + match[0].length;
|
|
1939
|
+
}
|
|
1940
|
+
parts.push(template.slice(last));
|
|
1941
|
+
return { parts, values };
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// src/blocks/FillInTheBlanks.tsx
|
|
1945
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1946
|
+
var INTERACTION3 = "fillInBlanks";
|
|
1947
|
+
function parseTemplate(template) {
|
|
1948
|
+
const { parts, values } = parseStarDelimitedTemplate(template, "blank");
|
|
1949
|
+
return {
|
|
1950
|
+
parts,
|
|
1951
|
+
blanks: values.map((answer, i) => ({ id: `blank-${i}`, answer }))
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
function FillInTheBlanksInner(props, ref) {
|
|
1955
|
+
const checkId = useMemo9(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1956
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1957
|
+
const parsed = useMemo9(() => parseTemplate(props.template), [props.template]);
|
|
1958
|
+
const blanks = props.blanks ?? parsed.blanks;
|
|
1959
|
+
const [values, setValues] = useState7(
|
|
1960
|
+
() => Object.fromEntries(blanks.map((b) => [b.id, ""]))
|
|
1961
|
+
);
|
|
1962
|
+
const [passed, setPassed] = useState7(false);
|
|
1963
|
+
const [showSolutions, setShowSolutions] = useState7(false);
|
|
1964
|
+
const completedRef = useRef7(false);
|
|
1965
|
+
const answeredRef = useRef7(false);
|
|
1966
|
+
const reset = () => {
|
|
1967
|
+
completedRef.current = false;
|
|
1968
|
+
answeredRef.current = false;
|
|
1969
|
+
setPassed(false);
|
|
1970
|
+
setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
|
|
1971
|
+
setShowSolutions(false);
|
|
1972
|
+
};
|
|
1973
|
+
useEffect7(() => {
|
|
1974
|
+
reset();
|
|
1975
|
+
}, [checkId, props.template, blanks.map((b) => b.answer).join("\0")]);
|
|
1976
|
+
const hasBlanks = blanks.length > 0;
|
|
1977
|
+
const allFilled = hasBlanks && blanks.every((b) => (values[b.id] ?? "").trim().length > 0);
|
|
1978
|
+
let score = 0;
|
|
1979
|
+
blanks.forEach((b) => {
|
|
1980
|
+
if ((values[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) score += 1;
|
|
1981
|
+
});
|
|
1982
|
+
const maxScore = blanks.length;
|
|
1983
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
1984
|
+
const handle = useMemo9(
|
|
1985
|
+
() => buildAssessmentHandle({
|
|
1986
|
+
checkId,
|
|
1987
|
+
getScore: () => score,
|
|
1988
|
+
getMaxScore: () => maxScore || 1,
|
|
1989
|
+
getAnswerGiven: () => allFilled,
|
|
1990
|
+
resetTask: reset,
|
|
1991
|
+
showSolutions: () => setShowSolutions(true),
|
|
1992
|
+
getXAPIData: () => ({
|
|
1993
|
+
checkId,
|
|
1994
|
+
interactionType: INTERACTION3,
|
|
1995
|
+
response: values,
|
|
1996
|
+
correct: passedThreshold,
|
|
1997
|
+
score,
|
|
1998
|
+
maxScore: maxScore || 1
|
|
1999
|
+
}),
|
|
2000
|
+
getCurrentState: () => ({ values, passed, showSolutions }),
|
|
2001
|
+
resume: (state) => {
|
|
2002
|
+
const raw = state.values;
|
|
2003
|
+
if (raw && typeof raw === "object") setValues({ ...raw });
|
|
2004
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
2005
|
+
setPassed(value);
|
|
2006
|
+
completedRef.current = value;
|
|
2007
|
+
answeredRef.current = value;
|
|
2008
|
+
});
|
|
2009
|
+
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
2010
|
+
}
|
|
2011
|
+
}),
|
|
2012
|
+
[allFilled, checkId, maxScore, passed, passedThreshold, score, showSolutions, values]
|
|
2013
|
+
);
|
|
2014
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2015
|
+
const check = () => {
|
|
2016
|
+
if (!hasBlanks) {
|
|
2017
|
+
if (isDevEnvironment4()) {
|
|
2018
|
+
console.warn("[lessonkit] FillInTheBlanks has no blanks in template");
|
|
2019
|
+
}
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
if (!allFilled) return;
|
|
2023
|
+
if (!answeredRef.current) {
|
|
2024
|
+
answeredRef.current = true;
|
|
2025
|
+
assessment.answer({
|
|
2026
|
+
checkId,
|
|
2027
|
+
interactionType: INTERACTION3,
|
|
2028
|
+
question: props.template,
|
|
2029
|
+
response: values,
|
|
2030
|
+
correct: passedThreshold
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
if (passedThreshold && !completedRef.current) {
|
|
2034
|
+
completedRef.current = true;
|
|
2035
|
+
setPassed(true);
|
|
2036
|
+
assessment.complete({
|
|
2037
|
+
checkId,
|
|
2038
|
+
interactionType: INTERACTION3,
|
|
2039
|
+
score,
|
|
2040
|
+
maxScore,
|
|
2041
|
+
passingScore: props.passingScore ?? maxScore
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
};
|
|
2045
|
+
useEffect7(() => {
|
|
2046
|
+
if (!allFilled) answeredRef.current = false;
|
|
2047
|
+
}, [allFilled]);
|
|
2048
|
+
useEffect7(() => {
|
|
2049
|
+
if (props.autoCheck && allFilled) check();
|
|
2050
|
+
}, [allFilled, props.autoCheck, values, passedThreshold]);
|
|
2051
|
+
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
2052
|
+
return /* @__PURE__ */ jsxs6("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
|
|
2053
|
+
/* @__PURE__ */ jsx8("p", { children: parsed.parts.map((part, i) => {
|
|
2054
|
+
const blank = blanks.find((b) => b.id === part);
|
|
2055
|
+
if (!blank) return /* @__PURE__ */ jsx8(React9.Fragment, { children: part }, i);
|
|
2056
|
+
return /* @__PURE__ */ jsxs6("label", { style: { margin: "0 0.25em" }, children: [
|
|
2057
|
+
/* @__PURE__ */ jsx8("span", { className: "lk-visually-hidden", children: blank.answer }),
|
|
2058
|
+
/* @__PURE__ */ jsx8(
|
|
2059
|
+
"input",
|
|
2060
|
+
{
|
|
2061
|
+
type: "text",
|
|
2062
|
+
"data-testid": `blank-${blank.id}`,
|
|
2063
|
+
"aria-label": `Blank ${blank.id}`,
|
|
2064
|
+
value: reveal ? blank.answer : values[blank.id] ?? "",
|
|
2065
|
+
readOnly: reveal,
|
|
2066
|
+
disabled: passed && !props.enableRetry,
|
|
2067
|
+
onChange: (e) => setValues((v) => ({ ...v, [blank.id]: e.target.value })),
|
|
2068
|
+
onBlur: () => props.autoCheck && check(),
|
|
2069
|
+
size: Math.max(8, blank.answer.length + 2)
|
|
2070
|
+
}
|
|
2071
|
+
)
|
|
2072
|
+
] }, blank.id);
|
|
2073
|
+
}) }),
|
|
2074
|
+
!props.autoCheck ? /* @__PURE__ */ jsx8("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
|
|
2075
|
+
!hasBlanks ? /* @__PURE__ */ jsx8("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
|
|
2076
|
+
allFilled ? /* @__PURE__ */ jsx8("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
|
|
2077
|
+
props.enableRetry && passed ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2078
|
+
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
2079
|
+
] });
|
|
2080
|
+
}
|
|
2081
|
+
var FillInTheBlanksInnerForwarded = forwardRef4(FillInTheBlanksInner);
|
|
2082
|
+
var FillInTheBlanks = forwardRef4(
|
|
2083
|
+
function FillInTheBlanks2(props, ref) {
|
|
2084
|
+
return /* @__PURE__ */ jsx8(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx8(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2085
|
+
}
|
|
2086
|
+
);
|
|
2087
|
+
|
|
2088
|
+
// src/blocks/DragTheWords.tsx
|
|
2089
|
+
import React10, { forwardRef as forwardRef5, useEffect as useEffect8, useMemo as useMemo10, useRef as useRef8, useState as useState8 } from "react";
|
|
2090
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2091
|
+
var INTERACTION4 = "dragTheWords";
|
|
2092
|
+
function parseZones(template) {
|
|
2093
|
+
const { parts, values } = parseStarDelimitedTemplate(template, "zone");
|
|
2094
|
+
return { parts, answers: values };
|
|
2095
|
+
}
|
|
2096
|
+
function DragTheWordsInner(props, ref) {
|
|
2097
|
+
const checkId = useMemo10(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
2098
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2099
|
+
const { parts, answers } = useMemo10(() => parseZones(props.template), [props.template]);
|
|
2100
|
+
const [zones, setZones] = useState8(
|
|
2101
|
+
() => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
|
|
2102
|
+
);
|
|
2103
|
+
const [pool, setPool] = useState8(() => [...props.words]);
|
|
2104
|
+
const [keyboardWord, setKeyboardWord] = useState8(null);
|
|
2105
|
+
const [passed, setPassed] = useState8(false);
|
|
2106
|
+
const completedRef = useRef8(false);
|
|
2107
|
+
const answeredRef = useRef8(false);
|
|
2108
|
+
const reset = () => {
|
|
2109
|
+
completedRef.current = false;
|
|
2110
|
+
answeredRef.current = false;
|
|
2111
|
+
setPassed(false);
|
|
2112
|
+
setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
|
|
2113
|
+
setPool([...props.words]);
|
|
2114
|
+
setKeyboardWord(null);
|
|
2115
|
+
};
|
|
2116
|
+
useEffect8(() => {
|
|
2117
|
+
reset();
|
|
2118
|
+
}, [checkId, props.template, props.words.join("\0")]);
|
|
2119
|
+
const hasZones = answers.length > 0;
|
|
2120
|
+
const allFilled = hasZones && answers.every((_, i) => (zones[`zone-${i}`] ?? "").length > 0);
|
|
2121
|
+
let score = 0;
|
|
2122
|
+
answers.forEach((ans, i) => {
|
|
2123
|
+
if ((zones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) score += 1;
|
|
2124
|
+
});
|
|
2125
|
+
const maxScore = answers.length;
|
|
2126
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
2127
|
+
const handle = useMemo10(
|
|
2128
|
+
() => buildAssessmentHandle({
|
|
2129
|
+
checkId,
|
|
2130
|
+
getScore: () => score,
|
|
2131
|
+
getMaxScore: () => maxScore || 1,
|
|
2132
|
+
getAnswerGiven: () => allFilled,
|
|
2133
|
+
resetTask: reset,
|
|
2134
|
+
showSolutions: () => {
|
|
2135
|
+
},
|
|
2136
|
+
getXAPIData: () => ({
|
|
2137
|
+
checkId,
|
|
2138
|
+
interactionType: INTERACTION4,
|
|
2139
|
+
response: zones,
|
|
2140
|
+
correct: passedThreshold,
|
|
2141
|
+
score,
|
|
2142
|
+
maxScore: maxScore || 1
|
|
2143
|
+
}),
|
|
2144
|
+
getCurrentState: () => ({ zones, pool, passed, keyboardWord }),
|
|
2145
|
+
resume: (state) => {
|
|
2146
|
+
const rawZones = state.zones;
|
|
2147
|
+
if (rawZones && typeof rawZones === "object") setZones({ ...rawZones });
|
|
2148
|
+
if (Array.isArray(state.pool)) setPool([...state.pool]);
|
|
2149
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
2150
|
+
setPassed(value);
|
|
2151
|
+
completedRef.current = value;
|
|
2152
|
+
answeredRef.current = value;
|
|
2153
|
+
});
|
|
2154
|
+
const kw = state.keyboardWord;
|
|
2155
|
+
if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
|
|
2156
|
+
}
|
|
2157
|
+
}),
|
|
2158
|
+
[allFilled, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, score, zones]
|
|
2159
|
+
);
|
|
2160
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2161
|
+
const placeInZone = (zoneId, word) => {
|
|
2162
|
+
if (passed && !props.enableRetry) return;
|
|
2163
|
+
const prev = zones[zoneId];
|
|
2164
|
+
setZones((z) => ({ ...z, [zoneId]: word }));
|
|
2165
|
+
setPool((p) => {
|
|
2166
|
+
const next = p.filter((w) => w !== word);
|
|
2167
|
+
if (prev) next.push(prev);
|
|
2168
|
+
return next;
|
|
2169
|
+
});
|
|
2170
|
+
setKeyboardWord(null);
|
|
2171
|
+
};
|
|
2172
|
+
const onDragStart = (word) => (e) => {
|
|
2173
|
+
e.dataTransfer.setData("text/plain", word);
|
|
2174
|
+
};
|
|
2175
|
+
const onDrop = (zoneId) => (e) => {
|
|
2176
|
+
e.preventDefault();
|
|
2177
|
+
const word = e.dataTransfer.getData("text/plain");
|
|
2178
|
+
if (word) placeInZone(zoneId, word);
|
|
2179
|
+
};
|
|
2180
|
+
const check = () => {
|
|
2181
|
+
if (!hasZones) {
|
|
2182
|
+
if (isDevEnvironment4()) {
|
|
2183
|
+
console.warn("[lessonkit] DragTheWords has no drop zones in template");
|
|
2184
|
+
}
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
if (!allFilled) return;
|
|
2188
|
+
if (!answeredRef.current) {
|
|
2189
|
+
answeredRef.current = true;
|
|
2190
|
+
assessment.answer({
|
|
2191
|
+
checkId,
|
|
2192
|
+
interactionType: INTERACTION4,
|
|
2193
|
+
question: props.template,
|
|
2194
|
+
response: zones,
|
|
2195
|
+
correct: passedThreshold
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
if (passedThreshold && !completedRef.current) {
|
|
2199
|
+
completedRef.current = true;
|
|
2200
|
+
setPassed(true);
|
|
2201
|
+
assessment.complete({
|
|
2202
|
+
checkId,
|
|
2203
|
+
interactionType: INTERACTION4,
|
|
2204
|
+
score,
|
|
2205
|
+
maxScore,
|
|
2206
|
+
passingScore: props.passingScore ?? maxScore
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
};
|
|
2210
|
+
useEffect8(() => {
|
|
2211
|
+
if (!allFilled) answeredRef.current = false;
|
|
2212
|
+
}, [allFilled]);
|
|
2213
|
+
useEffect8(() => {
|
|
2214
|
+
if (props.autoCheck && allFilled) check();
|
|
2215
|
+
}, [allFilled, props.autoCheck, zones, passedThreshold]);
|
|
2216
|
+
return /* @__PURE__ */ jsxs7("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
|
|
2217
|
+
/* @__PURE__ */ jsx9("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
|
|
2218
|
+
/* @__PURE__ */ jsx9("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ jsx9(
|
|
2219
|
+
"button",
|
|
2220
|
+
{
|
|
2221
|
+
type: "button",
|
|
2222
|
+
draggable: true,
|
|
2223
|
+
"data-testid": `word-${word}`,
|
|
2224
|
+
"aria-pressed": keyboardWord === word,
|
|
2225
|
+
onDragStart: onDragStart(word),
|
|
2226
|
+
onClick: () => setKeyboardWord(keyboardWord === word ? null : word),
|
|
2227
|
+
style: { margin: "0.25rem" },
|
|
2228
|
+
children: word
|
|
2229
|
+
},
|
|
2230
|
+
word
|
|
2231
|
+
)) }),
|
|
2232
|
+
/* @__PURE__ */ jsx9("p", { children: parts.map((part, i) => {
|
|
2233
|
+
if (!part.startsWith("zone-")) return /* @__PURE__ */ jsx9(React10.Fragment, { children: part }, i);
|
|
2234
|
+
return /* @__PURE__ */ jsx9(
|
|
2235
|
+
"span",
|
|
2236
|
+
{
|
|
2237
|
+
role: "button",
|
|
2238
|
+
tabIndex: 0,
|
|
2239
|
+
"data-testid": part,
|
|
2240
|
+
onDragOver: (e) => e.preventDefault(),
|
|
2241
|
+
onDrop: onDrop(part),
|
|
2242
|
+
onClick: () => keyboardWord && placeInZone(part, keyboardWord),
|
|
2243
|
+
onKeyDown: (e) => {
|
|
2244
|
+
if (e.key === "Enter" && keyboardWord) placeInZone(part, keyboardWord);
|
|
2245
|
+
},
|
|
2246
|
+
style: {
|
|
2247
|
+
display: "inline-block",
|
|
2248
|
+
minWidth: "6em",
|
|
2249
|
+
border: "1px dashed currentColor",
|
|
2250
|
+
padding: "0.2em 0.5em",
|
|
2251
|
+
margin: "0 0.2em"
|
|
2252
|
+
},
|
|
2253
|
+
children: zones[part] || "___"
|
|
2254
|
+
},
|
|
2255
|
+
part
|
|
2256
|
+
);
|
|
2257
|
+
}) }),
|
|
2258
|
+
/* @__PURE__ */ jsx9("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
|
|
2259
|
+
!hasZones ? /* @__PURE__ */ jsx9("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
|
|
2260
|
+
allFilled ? /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
|
|
2261
|
+
] });
|
|
2262
|
+
}
|
|
2263
|
+
var DragTheWordsInnerForwarded = forwardRef5(DragTheWordsInner);
|
|
2264
|
+
var DragTheWords = forwardRef5(function DragTheWords2(props, ref) {
|
|
2265
|
+
return /* @__PURE__ */ jsx9(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx9(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2266
|
+
});
|
|
2267
|
+
|
|
2268
|
+
// src/blocks/DragAndDrop.tsx
|
|
2269
|
+
import { forwardRef as forwardRef6, useEffect as useEffect9, useMemo as useMemo11, useRef as useRef9, useState as useState9 } from "react";
|
|
2270
|
+
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2271
|
+
var INTERACTION5 = "dragAndDrop";
|
|
2272
|
+
function DragAndDropInner(props, ref) {
|
|
2273
|
+
const checkId = useMemo11(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
2274
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2275
|
+
const [assignments, setAssignments] = useState9(
|
|
2276
|
+
() => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
|
|
2277
|
+
);
|
|
2278
|
+
const [pool, setPool] = useState9(() => props.items.map((i) => i.id));
|
|
2279
|
+
const [keyboardItem, setKeyboardItem] = useState9(null);
|
|
2280
|
+
const [passed, setPassed] = useState9(false);
|
|
2281
|
+
const completedRef = useRef9(false);
|
|
2282
|
+
const reset = () => {
|
|
2283
|
+
completedRef.current = false;
|
|
2284
|
+
setPassed(false);
|
|
2285
|
+
setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
|
|
2286
|
+
setPool(props.items.map((i) => i.id));
|
|
2287
|
+
setKeyboardItem(null);
|
|
2288
|
+
};
|
|
2289
|
+
useEffect9(() => {
|
|
2290
|
+
reset();
|
|
2291
|
+
}, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
|
|
2292
|
+
const allFilled = props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
|
|
2293
|
+
const allCorrect = props.targets.every((t) => assignments[t.id] === t.accepts);
|
|
2294
|
+
const handle = useMemo11(() => {
|
|
2295
|
+
const maxScore = props.targets.length || 1;
|
|
2296
|
+
let score = 0;
|
|
2297
|
+
props.targets.forEach((t) => {
|
|
2298
|
+
if (assignments[t.id] === t.accepts) score += 1;
|
|
2299
|
+
});
|
|
2300
|
+
return buildAssessmentHandle({
|
|
2301
|
+
checkId,
|
|
2302
|
+
getScore: () => score,
|
|
2303
|
+
getMaxScore: () => maxScore,
|
|
2304
|
+
getAnswerGiven: () => allFilled,
|
|
2305
|
+
resetTask: reset,
|
|
2306
|
+
showSolutions: () => {
|
|
2307
|
+
},
|
|
2308
|
+
getXAPIData: () => ({
|
|
2309
|
+
checkId,
|
|
2310
|
+
interactionType: INTERACTION5,
|
|
2311
|
+
response: assignments,
|
|
2312
|
+
correct: allCorrect,
|
|
2313
|
+
score,
|
|
2314
|
+
maxScore
|
|
2315
|
+
}),
|
|
2316
|
+
getCurrentState: () => ({ assignments, pool, passed, keyboardItem }),
|
|
2317
|
+
resume: (state) => {
|
|
2318
|
+
const rawAssignments = state.assignments;
|
|
2319
|
+
if (rawAssignments && typeof rawAssignments === "object") {
|
|
2320
|
+
setAssignments({ ...rawAssignments });
|
|
2321
|
+
}
|
|
2322
|
+
if (Array.isArray(state.pool)) setPool([...state.pool]);
|
|
2323
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
2324
|
+
setPassed(value);
|
|
2325
|
+
completedRef.current = value;
|
|
2326
|
+
});
|
|
2327
|
+
const item = state.keyboardItem;
|
|
2328
|
+
if (item === null || typeof item === "string") setKeyboardItem(item ?? null);
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
}, [allCorrect, allFilled, assignments, checkId, keyboardItem, passed, pool, props.targets]);
|
|
2332
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2333
|
+
const place = (targetId, itemId) => {
|
|
2334
|
+
if (passed && !props.enableRetry) return;
|
|
2335
|
+
const prev = assignments[targetId];
|
|
2336
|
+
setAssignments((a) => ({ ...a, [targetId]: itemId }));
|
|
2337
|
+
setPool((p) => {
|
|
2338
|
+
const next = p.filter((id) => id !== itemId);
|
|
2339
|
+
if (prev) next.push(prev);
|
|
2340
|
+
return next;
|
|
2341
|
+
});
|
|
2342
|
+
setKeyboardItem(null);
|
|
2343
|
+
};
|
|
2344
|
+
const check = () => {
|
|
2345
|
+
if (!allFilled) return;
|
|
2346
|
+
assessment.answer({
|
|
2347
|
+
checkId,
|
|
2348
|
+
interactionType: INTERACTION5,
|
|
2349
|
+
response: assignments,
|
|
2350
|
+
correct: allCorrect
|
|
2351
|
+
});
|
|
2352
|
+
if (allCorrect && !completedRef.current) {
|
|
2353
|
+
completedRef.current = true;
|
|
2354
|
+
setPassed(true);
|
|
2355
|
+
assessment.complete({
|
|
2356
|
+
checkId,
|
|
2357
|
+
interactionType: INTERACTION5,
|
|
2358
|
+
score: props.targets.length,
|
|
2359
|
+
maxScore: props.targets.length,
|
|
2360
|
+
passingScore: props.passingScore ?? props.targets.length
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
return /* @__PURE__ */ jsxs8("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
|
|
2365
|
+
/* @__PURE__ */ jsx10("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
|
|
2366
|
+
/* @__PURE__ */ jsx10("div", { role: "list", "aria-label": "Draggable items", children: pool.map((id) => {
|
|
2367
|
+
const item = props.items.find((i) => i.id === id);
|
|
2368
|
+
return /* @__PURE__ */ jsx10(
|
|
2369
|
+
"button",
|
|
2370
|
+
{
|
|
2371
|
+
type: "button",
|
|
2372
|
+
draggable: true,
|
|
2373
|
+
"data-testid": `drag-item-${id}`,
|
|
2374
|
+
"aria-pressed": keyboardItem === id,
|
|
2375
|
+
onDragStart: (e) => e.dataTransfer.setData("text/plain", id),
|
|
2376
|
+
onClick: () => setKeyboardItem(keyboardItem === id ? null : id),
|
|
2377
|
+
style: { margin: "0.25rem" },
|
|
2378
|
+
children: item.label
|
|
2379
|
+
},
|
|
2380
|
+
id
|
|
2381
|
+
);
|
|
2382
|
+
}) }),
|
|
2383
|
+
/* @__PURE__ */ jsx10("ul", { children: props.targets.map((target) => {
|
|
2384
|
+
const assigned = assignments[target.id];
|
|
2385
|
+
const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
|
|
2386
|
+
return /* @__PURE__ */ jsxs8("li", { children: [
|
|
2387
|
+
/* @__PURE__ */ jsx10("strong", { children: target.label }),
|
|
2388
|
+
" ",
|
|
2389
|
+
/* @__PURE__ */ jsx10(
|
|
2390
|
+
"span",
|
|
2391
|
+
{
|
|
2392
|
+
role: "button",
|
|
2393
|
+
tabIndex: 0,
|
|
2394
|
+
"data-testid": `drop-${target.id}`,
|
|
2395
|
+
onDragOver: (e) => e.preventDefault(),
|
|
2396
|
+
onDrop: (e) => {
|
|
2397
|
+
e.preventDefault();
|
|
2398
|
+
const id = e.dataTransfer.getData("text/plain");
|
|
2399
|
+
if (id) place(target.id, id);
|
|
2400
|
+
},
|
|
2401
|
+
onClick: () => keyboardItem && place(target.id, keyboardItem),
|
|
2402
|
+
onKeyDown: (e) => {
|
|
2403
|
+
if (e.key === "Enter" && keyboardItem) place(target.id, keyboardItem);
|
|
2404
|
+
},
|
|
2405
|
+
style: {
|
|
2406
|
+
display: "inline-block",
|
|
2407
|
+
minWidth: "8em",
|
|
2408
|
+
border: "1px dashed currentColor",
|
|
2409
|
+
padding: "0.25em"
|
|
2410
|
+
},
|
|
2411
|
+
children: label
|
|
2412
|
+
}
|
|
2413
|
+
)
|
|
2414
|
+
] }, target.id);
|
|
2415
|
+
}) }),
|
|
2416
|
+
/* @__PURE__ */ jsx10("button", { type: "button", "data-testid": "check-drag-drop", disabled: !allFilled || passed, onClick: check, children: "Check" }),
|
|
2417
|
+
allFilled ? /* @__PURE__ */ jsx10("p", { role: "status", "aria-live": "polite", children: passed || allCorrect ? "Correct" : "Try again" }) : null
|
|
2418
|
+
] });
|
|
2419
|
+
}
|
|
2420
|
+
var DragAndDropInnerForwarded = forwardRef6(DragAndDropInner);
|
|
2421
|
+
var DragAndDrop = forwardRef6(function DragAndDrop2(props, ref) {
|
|
2422
|
+
return /* @__PURE__ */ jsx10(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx10(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2423
|
+
});
|
|
2424
|
+
|
|
2425
|
+
// src/blocks/AssessmentSequence.tsx
|
|
2426
|
+
import React14, { forwardRef as forwardRef7, useCallback as useCallback7, useEffect as useEffect12, useMemo as useMemo13, useState as useState10 } from "react";
|
|
2427
|
+
|
|
2428
|
+
// src/compound/useCompoundShell.ts
|
|
2429
|
+
import { useMemo as useMemo12 } from "react";
|
|
2430
|
+
import { clampCompoundPageIndex as clampCompoundPageIndex3 } from "@lessonkit/core";
|
|
2431
|
+
|
|
2432
|
+
// src/compound/useCompoundNavigation.ts
|
|
2433
|
+
import { useCallback as useCallback4 } from "react";
|
|
2434
|
+
function useCompoundNavigation(pageCount, index, setIndex) {
|
|
2435
|
+
const goNext = useCallback4(() => {
|
|
2436
|
+
if (pageCount < 1) return;
|
|
2437
|
+
setIndex((i) => Math.min(i + 1, pageCount - 1));
|
|
2438
|
+
}, [pageCount, setIndex]);
|
|
2439
|
+
const goPrev = useCallback4(() => {
|
|
2440
|
+
setIndex((i) => Math.max(i - 1, 0));
|
|
2441
|
+
}, [setIndex]);
|
|
2442
|
+
const clampedIndex = pageCount < 1 ? 0 : Math.min(index, pageCount - 1);
|
|
2443
|
+
return {
|
|
2444
|
+
index: clampedIndex,
|
|
2445
|
+
setIndex,
|
|
2446
|
+
goNext,
|
|
2447
|
+
goPrev,
|
|
2448
|
+
progress: { current: pageCount < 1 ? 0 : clampedIndex + 1, total: pageCount }
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
// src/compound/useCompoundPersistence.ts
|
|
2453
|
+
import { useCallback as useCallback6, useEffect as useEffect11, useRef as useRef11 } from "react";
|
|
2454
|
+
import {
|
|
2455
|
+
clampCompoundPageIndex as clampCompoundPageIndex2,
|
|
2456
|
+
createCompoundResumeState as createCompoundResumeState2,
|
|
2457
|
+
createSessionStoragePort as createSessionStoragePort3,
|
|
2458
|
+
loadCompoundState as loadCompoundState2
|
|
2459
|
+
} from "@lessonkit/core";
|
|
2460
|
+
|
|
2461
|
+
// src/compound/useCompoundResume.ts
|
|
2462
|
+
import { useCallback as useCallback5, useEffect as useEffect10, useRef as useRef10 } from "react";
|
|
2463
|
+
import { loadCompoundState, saveCompoundState } from "@lessonkit/core";
|
|
2464
|
+
import { createSessionStoragePort as createSessionStoragePort2 } from "@lessonkit/core";
|
|
2465
|
+
function useCompoundResume(opts) {
|
|
2466
|
+
const storageRef = useRef10(opts.storage ?? createSessionStoragePort2());
|
|
2467
|
+
const resumedRef = useRef10(false);
|
|
2468
|
+
useEffect10(() => {
|
|
2469
|
+
if (!opts.enabled || !opts.courseId || resumedRef.current) return;
|
|
2470
|
+
const saved = loadCompoundState(storageRef.current, opts.courseId, opts.compoundId);
|
|
2471
|
+
if (saved) {
|
|
2472
|
+
resumedRef.current = true;
|
|
2473
|
+
opts.onResume?.(saved);
|
|
2474
|
+
}
|
|
2475
|
+
}, [opts.enabled, opts.courseId, opts.compoundId, opts.onResume]);
|
|
2476
|
+
return useCallback5(
|
|
2477
|
+
(state) => {
|
|
2478
|
+
if (!opts.enabled || !opts.courseId) return;
|
|
2479
|
+
saveCompoundState(storageRef.current, opts.courseId, opts.compoundId, state);
|
|
2480
|
+
},
|
|
2481
|
+
[opts.enabled, opts.courseId, opts.compoundId]
|
|
2482
|
+
);
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
// src/compound/useCompoundPersistence.ts
|
|
2486
|
+
function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, storage = createSessionStoragePort3()) {
|
|
2487
|
+
if (!enabled || !courseId || pageCount < 1) return 0;
|
|
2488
|
+
const saved = loadCompoundState2(storage, courseId, compoundId);
|
|
2489
|
+
if (!saved) return 0;
|
|
2490
|
+
return clampCompoundPageIndex2(saved.activePageIndex, pageCount);
|
|
2491
|
+
}
|
|
2492
|
+
function useCompoundPersistence(opts) {
|
|
2493
|
+
const storage = opts.storage ?? createSessionStoragePort3();
|
|
2494
|
+
const ctx = useCompoundRegistry();
|
|
2495
|
+
const handlesVersion = useCompoundHandlesVersion();
|
|
2496
|
+
const pendingChildResumeRef = useRef11(null);
|
|
2497
|
+
const loadedChildStatesRef = useRef11({});
|
|
2498
|
+
const skipSaveUntilHydratedRef = useRef11(false);
|
|
2499
|
+
const buildState = useCallback6(() => {
|
|
2500
|
+
const childStates = {
|
|
2501
|
+
...loadedChildStatesRef.current
|
|
2502
|
+
};
|
|
2503
|
+
if (ctx) {
|
|
2504
|
+
for (const [checkId, handle] of ctx.getHandles()) {
|
|
2505
|
+
if (handle.getCurrentState) {
|
|
2506
|
+
childStates[checkId] = handle.getCurrentState();
|
|
2507
|
+
delete loadedChildStatesRef.current[checkId];
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
return createCompoundResumeState2({
|
|
2512
|
+
activePageIndex: clampCompoundPageIndex2(opts.index, opts.pageCount),
|
|
2513
|
+
childStates
|
|
2514
|
+
});
|
|
2515
|
+
}, [ctx, opts.index, opts.pageCount]);
|
|
2516
|
+
const applyPendingChildResume = useCallback6(() => {
|
|
2517
|
+
const pending = pendingChildResumeRef.current;
|
|
2518
|
+
if (!pending || !ctx) return;
|
|
2519
|
+
const applied = resumeChildHandles(ctx.getHandles(), pending.childStates, { waitForHandles: true });
|
|
2520
|
+
if (!applied) return;
|
|
2521
|
+
pendingChildResumeRef.current = null;
|
|
2522
|
+
skipSaveUntilHydratedRef.current = false;
|
|
2523
|
+
}, [ctx]);
|
|
2524
|
+
const saveResume = useCompoundResume({
|
|
2525
|
+
courseId: opts.courseId,
|
|
2526
|
+
compoundId: opts.compoundId,
|
|
2527
|
+
enabled: opts.enabled,
|
|
2528
|
+
storage,
|
|
2529
|
+
onResume: (state) => {
|
|
2530
|
+
const clamped = clampCompoundPageIndex2(state.activePageIndex, opts.pageCount);
|
|
2531
|
+
loadedChildStatesRef.current = { ...state.childStates };
|
|
2532
|
+
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
2533
|
+
opts.setIndex(clamped);
|
|
2534
|
+
pendingChildResumeRef.current = { ...state, activePageIndex: clamped };
|
|
2535
|
+
queueMicrotask(() => applyPendingChildResume());
|
|
2536
|
+
}
|
|
2537
|
+
});
|
|
2538
|
+
useEffect11(() => {
|
|
2539
|
+
if (!opts.enabled || !opts.courseId) return;
|
|
2540
|
+
if (skipSaveUntilHydratedRef.current) return;
|
|
2541
|
+
saveResume(buildState());
|
|
2542
|
+
}, [
|
|
2543
|
+
opts.enabled,
|
|
2544
|
+
opts.courseId,
|
|
2545
|
+
opts.index,
|
|
2546
|
+
opts.pageCount,
|
|
2547
|
+
handlesVersion,
|
|
2548
|
+
saveResume,
|
|
2549
|
+
buildState
|
|
2550
|
+
]);
|
|
2551
|
+
useEffect11(() => {
|
|
2552
|
+
applyPendingChildResume();
|
|
2553
|
+
}, [opts.index, handlesVersion, applyPendingChildResume]);
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
// src/compound/useCompoundShell.ts
|
|
2557
|
+
function useCompoundShell(opts) {
|
|
2558
|
+
const ctx = useCompoundRegistry();
|
|
2559
|
+
useCompoundPersistence({
|
|
2560
|
+
courseId: opts.courseId,
|
|
2561
|
+
compoundId: opts.compoundId,
|
|
2562
|
+
pageCount: opts.pageCount,
|
|
2563
|
+
index: opts.index,
|
|
2564
|
+
setIndex: opts.setIndex,
|
|
2565
|
+
enabled: opts.persistEnabled,
|
|
2566
|
+
storage: opts.storage
|
|
2567
|
+
});
|
|
2568
|
+
const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
|
|
2569
|
+
const visibleIndex = clampCompoundPageIndex3(opts.index, opts.pageCount);
|
|
2570
|
+
useCompoundHandleRef(opts.ref, {
|
|
2571
|
+
activePageIndex: visibleIndex,
|
|
2572
|
+
setActivePageIndex: opts.setIndex,
|
|
2573
|
+
getHandles: () => ctx?.getHandles() ?? /* @__PURE__ */ new Map(),
|
|
2574
|
+
pageCount: opts.pageCount,
|
|
2575
|
+
enableSolutionsButton: opts.enableSolutionsButton
|
|
2576
|
+
});
|
|
2577
|
+
return { visibleIndex, goNext, goPrev, progress, ctx };
|
|
2578
|
+
}
|
|
2579
|
+
function useCompoundInitialIndex(opts) {
|
|
2580
|
+
return useMemo12(
|
|
2581
|
+
() => readCompoundInitialIndex(
|
|
2582
|
+
opts.courseId,
|
|
2583
|
+
opts.compoundId,
|
|
2584
|
+
opts.pageCount,
|
|
2585
|
+
opts.persistEnabled,
|
|
2586
|
+
opts.storage
|
|
2587
|
+
),
|
|
2588
|
+
[opts.courseId, opts.compoundId, opts.pageCount, opts.persistEnabled, opts.storage]
|
|
2589
|
+
);
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
// src/compound/validateChildren.ts
|
|
2593
|
+
import React13 from "react";
|
|
2594
|
+
import {
|
|
2595
|
+
ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
2596
|
+
COMPOUND_MAX_NESTING_DEPTH,
|
|
2597
|
+
isChildTypeAllowed
|
|
2598
|
+
} from "@lessonkit/core";
|
|
2599
|
+
|
|
2600
|
+
// src/compound/blockType.ts
|
|
2601
|
+
var LESSONKIT_BLOCK_TYPE = /* @__PURE__ */ Symbol.for("lessonkit.blockType");
|
|
2602
|
+
function setLessonkitBlockType(component, blockType) {
|
|
2603
|
+
component[LESSONKIT_BLOCK_TYPE] = blockType;
|
|
2604
|
+
if (!component.displayName) {
|
|
2605
|
+
component.displayName = blockType;
|
|
2606
|
+
}
|
|
2607
|
+
return component;
|
|
2608
|
+
}
|
|
2609
|
+
function getLessonkitBlockType(component) {
|
|
2610
|
+
if (!component || typeof component !== "object" && typeof component !== "function") {
|
|
2611
|
+
return void 0;
|
|
2612
|
+
}
|
|
2613
|
+
const typed = component;
|
|
2614
|
+
return typed[LESSONKIT_BLOCK_TYPE] ?? typed.displayName;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
// src/compound/validateChildren.ts
|
|
2618
|
+
var warnedPairs = /* @__PURE__ */ new Set();
|
|
2619
|
+
var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
2620
|
+
"Page",
|
|
2621
|
+
"InteractiveBook",
|
|
2622
|
+
"AssessmentSequence"
|
|
2623
|
+
]);
|
|
2624
|
+
function warnOrThrow(msg, strict) {
|
|
2625
|
+
if (strict) throw new Error(msg);
|
|
2626
|
+
if (!warnedPairs.has(msg)) {
|
|
2627
|
+
warnedPairs.add(msg);
|
|
2628
|
+
console.warn(msg);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
function validateNode(parent, node, depth, strict) {
|
|
2632
|
+
React13.Children.forEach(node, (child) => {
|
|
2633
|
+
if (!React13.isValidElement(child)) return;
|
|
2634
|
+
const blockType = getLessonkitBlockType(child.type);
|
|
2635
|
+
if (!blockType) {
|
|
2636
|
+
if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
2637
|
+
validateNode(parent, child.props.children, depth, strict);
|
|
2638
|
+
}
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
if (!isChildTypeAllowed(parent, blockType)) {
|
|
2642
|
+
const key = `${parent}:${blockType}`;
|
|
2643
|
+
if (!warnedPairs.has(key)) {
|
|
2644
|
+
warnedPairs.add(key);
|
|
2645
|
+
const msg = `[lessonkit] Block "${blockType}" is not in the allowlist for "${parent}"`;
|
|
2646
|
+
if (strict) throw new Error(msg);
|
|
2647
|
+
console.warn(msg);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
if (COMPOUND_CONTAINER_TYPES.has(blockType)) {
|
|
2651
|
+
const maxDepth = COMPOUND_MAX_NESTING_DEPTH[parent];
|
|
2652
|
+
if (depth >= maxDepth) {
|
|
2653
|
+
warnOrThrow(
|
|
2654
|
+
`[lessonkit] Block "${blockType}" exceeds max nesting depth (${maxDepth}) for "${parent}"`,
|
|
2655
|
+
strict
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2658
|
+
const nestedParent = blockType;
|
|
2659
|
+
validateNode(nestedParent, child.props.children, depth + 1, strict);
|
|
2660
|
+
} else if (blockType === "Accordion") {
|
|
2661
|
+
const sections = child.props.sections;
|
|
2662
|
+
if (sections) validateAccordionSections(sections, strict);
|
|
2663
|
+
} else if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
2664
|
+
validateSubtreeForForbidden(
|
|
2665
|
+
child.props.children,
|
|
2666
|
+
ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
2667
|
+
strict
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
function validateSubtreeForForbidden(node, forbidden, strict) {
|
|
2673
|
+
React13.Children.forEach(node, (child) => {
|
|
2674
|
+
if (!React13.isValidElement(child)) return;
|
|
2675
|
+
const blockType = getLessonkitBlockType(child.type);
|
|
2676
|
+
if (blockType && forbidden.includes(blockType)) {
|
|
2677
|
+
warnOrThrow(`[lessonkit] Block "${blockType}" must not nest inside Accordion`, strict);
|
|
2678
|
+
}
|
|
2679
|
+
if (blockType === "Accordion") {
|
|
2680
|
+
const sections = child.props.sections;
|
|
2681
|
+
if (sections) validateAccordionSections(sections, strict);
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
2685
|
+
validateSubtreeForForbidden(
|
|
2686
|
+
child.props.children,
|
|
2687
|
+
forbidden,
|
|
2688
|
+
strict
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
function validateAccordionSections(sections, strict) {
|
|
2694
|
+
if (!isDevEnvironment4() && !strict) return;
|
|
2695
|
+
for (const section of sections) {
|
|
2696
|
+
validateSubtreeForForbidden(section.content, ACCORDION_FORBIDDEN_CHILD_TYPES, strict);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
function validateCompoundChildren(parent, children, strict) {
|
|
2700
|
+
if (!isDevEnvironment4() && !strict) return;
|
|
2701
|
+
validateNode(parent, children, 0, strict);
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
// src/compound/warnPersistence.ts
|
|
2705
|
+
var DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID = "assessment-sequence";
|
|
2706
|
+
function warnSharedCompoundStorageKey(opts) {
|
|
2707
|
+
if (!opts.persistEnabled || opts.hasExplicitBlockId || !isDevEnvironment4()) return;
|
|
2708
|
+
console.warn(
|
|
2709
|
+
`[lessonkit] <${opts.componentName}> without blockId shares one sessionStorage key when persistCompoundState is enabled; set a unique blockId per instance.`
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
// src/blocks/AssessmentSequence.tsx
|
|
2714
|
+
import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2715
|
+
var AssessmentSequenceInner = forwardRef7(
|
|
2716
|
+
function AssessmentSequenceInner2(props, ref) {
|
|
2717
|
+
const { compoundId, childArray, index, setIndex, persistEnabled } = props;
|
|
2718
|
+
const sequential = props.sequential !== false;
|
|
2719
|
+
const { config } = useLessonkit();
|
|
2720
|
+
const { visibleIndex, goNext, goPrev, progress } = useCompoundShell({
|
|
2721
|
+
courseId: config.courseId,
|
|
2722
|
+
compoundId,
|
|
2723
|
+
pageCount: childArray.length,
|
|
2724
|
+
index,
|
|
2725
|
+
setIndex,
|
|
2726
|
+
persistEnabled,
|
|
2727
|
+
ref,
|
|
2728
|
+
enableSolutionsButton: props.enableSolutionsButton
|
|
2729
|
+
});
|
|
2730
|
+
validateCompoundChildren("AssessmentSequence", props.children);
|
|
2731
|
+
if (!sequential) {
|
|
2732
|
+
return /* @__PURE__ */ jsx11("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
|
|
2733
|
+
}
|
|
2734
|
+
return /* @__PURE__ */ jsxs9("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
|
|
2735
|
+
/* @__PURE__ */ jsxs9("p", { children: [
|
|
2736
|
+
"Question ",
|
|
2737
|
+
progress.current,
|
|
2738
|
+
" of ",
|
|
2739
|
+
progress.total
|
|
2740
|
+
] }),
|
|
2741
|
+
/* @__PURE__ */ jsx11("div", { "data-testid": "assessment-sequence-step", children: childArray.map((child, i) => /* @__PURE__ */ jsx11("div", { hidden: i !== visibleIndex, children: child }, child.key ?? i)) }),
|
|
2742
|
+
/* @__PURE__ */ jsxs9("nav", { "aria-label": "Sequence navigation", children: [
|
|
2743
|
+
/* @__PURE__ */ jsx11(
|
|
2744
|
+
"button",
|
|
2745
|
+
{
|
|
2746
|
+
type: "button",
|
|
2747
|
+
"data-testid": "sequence-prev",
|
|
2748
|
+
disabled: visibleIndex === 0 || childArray.length === 0,
|
|
2749
|
+
onClick: goPrev,
|
|
2750
|
+
children: "Previous"
|
|
2751
|
+
}
|
|
2752
|
+
),
|
|
2753
|
+
/* @__PURE__ */ jsx11(
|
|
2754
|
+
"button",
|
|
2755
|
+
{
|
|
2756
|
+
type: "button",
|
|
2757
|
+
"data-testid": "sequence-next",
|
|
2758
|
+
disabled: visibleIndex >= childArray.length - 1 || childArray.length === 0,
|
|
2759
|
+
onClick: goNext,
|
|
2760
|
+
children: "Next"
|
|
2761
|
+
}
|
|
2762
|
+
)
|
|
2763
|
+
] })
|
|
2764
|
+
] });
|
|
2765
|
+
}
|
|
2766
|
+
);
|
|
2767
|
+
var AssessmentSequence = forwardRef7(
|
|
2768
|
+
function AssessmentSequence2(props, ref) {
|
|
2769
|
+
const compoundId = useMemo13(
|
|
2770
|
+
() => props.blockId ? normalizeComponentId(props.blockId, "blockId") : DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
|
|
2771
|
+
[props.blockId]
|
|
2772
|
+
);
|
|
2773
|
+
const childArray = React14.Children.toArray(props.children).filter(
|
|
2774
|
+
React14.isValidElement
|
|
2775
|
+
);
|
|
2776
|
+
const { config } = useLessonkit();
|
|
2777
|
+
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
2778
|
+
useEffect12(() => {
|
|
2779
|
+
warnSharedCompoundStorageKey({
|
|
2780
|
+
persistEnabled,
|
|
2781
|
+
hasExplicitBlockId: Boolean(props.blockId),
|
|
2782
|
+
componentName: "AssessmentSequence"
|
|
2783
|
+
});
|
|
2784
|
+
}, [persistEnabled, props.blockId]);
|
|
2785
|
+
const initialIndex = useCompoundInitialIndex({
|
|
2786
|
+
courseId: config.courseId,
|
|
2787
|
+
compoundId,
|
|
2788
|
+
pageCount: childArray.length,
|
|
2789
|
+
persistEnabled
|
|
2790
|
+
});
|
|
2791
|
+
const [index, setIndex] = useState10(initialIndex);
|
|
2792
|
+
const setIndexStable = useCallback7((i) => setIndex(i), []);
|
|
2793
|
+
return /* @__PURE__ */ jsx11(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx11(
|
|
2794
|
+
AssessmentSequenceInner,
|
|
2795
|
+
{
|
|
2796
|
+
...props,
|
|
2797
|
+
ref,
|
|
2798
|
+
compoundId,
|
|
2799
|
+
childArray,
|
|
2800
|
+
index,
|
|
2801
|
+
setIndex,
|
|
2802
|
+
persistEnabled
|
|
2803
|
+
}
|
|
2804
|
+
) });
|
|
2805
|
+
}
|
|
2806
|
+
);
|
|
2807
|
+
setLessonkitBlockType(AssessmentSequence, "AssessmentSequence");
|
|
2808
|
+
|
|
2809
|
+
// src/blocks/Text.tsx
|
|
2810
|
+
import "react";
|
|
2811
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
2812
|
+
function Text(props) {
|
|
2813
|
+
return /* @__PURE__ */ jsx12("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
|
|
2814
|
+
}
|
|
2815
|
+
setLessonkitBlockType(Text, "Text");
|
|
2816
|
+
|
|
2817
|
+
// src/blocks/Heading.tsx
|
|
2818
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
2819
|
+
function Heading(props) {
|
|
2820
|
+
const Tag = `h${props.level}`;
|
|
2821
|
+
return /* @__PURE__ */ jsx13(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
|
|
2822
|
+
}
|
|
2823
|
+
setLessonkitBlockType(Heading, "Heading");
|
|
2824
|
+
|
|
2825
|
+
// src/blocks/Image.tsx
|
|
2826
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
2827
|
+
function Image(props) {
|
|
2828
|
+
return /* @__PURE__ */ jsx14(
|
|
2829
|
+
"img",
|
|
2830
|
+
{
|
|
2831
|
+
src: props.src,
|
|
2832
|
+
alt: props.alt,
|
|
2833
|
+
"data-lk-block-id": props.blockId,
|
|
2834
|
+
"data-testid": props.blockId ? `image-${props.blockId}` : "image",
|
|
2835
|
+
style: { maxWidth: "100%", height: "auto" }
|
|
2836
|
+
}
|
|
2837
|
+
);
|
|
2838
|
+
}
|
|
2839
|
+
setLessonkitBlockType(Image, "Image");
|
|
2840
|
+
|
|
2841
|
+
// src/blocks/Page.tsx
|
|
2842
|
+
import { useEffect as useEffect13 } from "react";
|
|
2843
|
+
import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2844
|
+
function Page(props) {
|
|
2845
|
+
validateCompoundChildren("Page", props.children);
|
|
2846
|
+
const { track } = useLessonkit();
|
|
2847
|
+
const lessonId = useEnclosingLessonId();
|
|
2848
|
+
useEffect13(() => {
|
|
2849
|
+
if (props.hidden || !lessonId) return;
|
|
2850
|
+
track(
|
|
2851
|
+
"compound_page_viewed",
|
|
2852
|
+
{
|
|
2853
|
+
blockId: props.blockId,
|
|
2854
|
+
pageIndex: props.pageIndex ?? 0,
|
|
2855
|
+
parentType: props.parentType
|
|
2856
|
+
},
|
|
2857
|
+
{ lessonId }
|
|
2858
|
+
);
|
|
2859
|
+
}, [props.hidden, props.pageIndex, props.parentType, props.blockId, lessonId, track]);
|
|
2860
|
+
return /* @__PURE__ */ jsxs10(
|
|
2861
|
+
"section",
|
|
2862
|
+
{
|
|
2863
|
+
"aria-label": props.title ?? "Page",
|
|
2864
|
+
"data-lk-block-id": props.blockId,
|
|
2865
|
+
"data-testid": `page-${props.blockId}`,
|
|
2866
|
+
hidden: props.hidden ? true : void 0,
|
|
2867
|
+
children: [
|
|
2868
|
+
props.title ? /* @__PURE__ */ jsx15("h3", { children: props.title }) : null,
|
|
2869
|
+
/* @__PURE__ */ jsx15("div", { children: props.children })
|
|
2870
|
+
]
|
|
2871
|
+
}
|
|
2872
|
+
);
|
|
2873
|
+
}
|
|
2874
|
+
setLessonkitBlockType(Page, "Page");
|
|
2875
|
+
|
|
2876
|
+
// src/blocks/InteractiveBook.tsx
|
|
2877
|
+
import React17, { forwardRef as forwardRef8, useCallback as useCallback8, useEffect as useEffect14, useMemo as useMemo14, useState as useState11 } from "react";
|
|
2878
|
+
import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2879
|
+
var InteractiveBookInner = forwardRef8(
|
|
2880
|
+
function InteractiveBookInner2(props, ref) {
|
|
2881
|
+
const { blockId, pages, index, setIndex, persistEnabled } = props;
|
|
2882
|
+
validateCompoundChildren("InteractiveBook", pages);
|
|
2883
|
+
const { config, track } = useLessonkit();
|
|
2884
|
+
const lessonId = useEnclosingLessonId();
|
|
2885
|
+
const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
|
|
2886
|
+
courseId: config.courseId,
|
|
2887
|
+
compoundId: blockId,
|
|
2888
|
+
pageCount: pages.length,
|
|
2889
|
+
index,
|
|
2890
|
+
setIndex,
|
|
2891
|
+
persistEnabled,
|
|
2892
|
+
ref
|
|
2893
|
+
});
|
|
2894
|
+
const pageTitles = useMemo14(
|
|
2895
|
+
() => pages.map((page) => page.props.title),
|
|
2896
|
+
[pages]
|
|
2897
|
+
);
|
|
2898
|
+
useEffect14(() => {
|
|
2899
|
+
if (!lessonId || pages.length === 0) return;
|
|
2900
|
+
track(
|
|
2901
|
+
"book_page_viewed",
|
|
2902
|
+
{
|
|
2903
|
+
blockId,
|
|
2904
|
+
pageIndex: visibleIndex,
|
|
2905
|
+
pageTitle: pageTitles[visibleIndex]
|
|
2906
|
+
},
|
|
2907
|
+
{ lessonId }
|
|
2908
|
+
);
|
|
2909
|
+
}, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
|
|
2910
|
+
return /* @__PURE__ */ jsxs11("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
|
|
2911
|
+
/* @__PURE__ */ jsx16("h3", { children: props.title }),
|
|
2912
|
+
/* @__PURE__ */ jsxs11("p", { children: [
|
|
2913
|
+
"Page ",
|
|
2914
|
+
progress.current,
|
|
2915
|
+
" of ",
|
|
2916
|
+
progress.total
|
|
2917
|
+
] }),
|
|
2918
|
+
props.showBookScore && ctx ? /* @__PURE__ */ jsxs11("p", { "data-testid": "book-score", children: [
|
|
2919
|
+
"Score: ",
|
|
2920
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
2921
|
+
" /",
|
|
2922
|
+
" ",
|
|
2923
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
2924
|
+
] }) : null,
|
|
2925
|
+
/* @__PURE__ */ jsx16("div", { "data-testid": "interactive-book-page", children: pages.map(
|
|
2926
|
+
(page, i) => React17.cloneElement(page, {
|
|
2927
|
+
key: page.key ?? page.props.blockId,
|
|
2928
|
+
hidden: i !== visibleIndex,
|
|
2929
|
+
pageIndex: i,
|
|
2930
|
+
parentType: "InteractiveBook"
|
|
2931
|
+
})
|
|
2932
|
+
) }),
|
|
2933
|
+
/* @__PURE__ */ jsxs11("nav", { "aria-label": "Book navigation", children: [
|
|
2934
|
+
/* @__PURE__ */ jsx16(
|
|
2935
|
+
"button",
|
|
2936
|
+
{
|
|
2937
|
+
type: "button",
|
|
2938
|
+
"data-testid": "book-prev",
|
|
2939
|
+
disabled: visibleIndex === 0 || pages.length === 0,
|
|
2940
|
+
onClick: goPrev,
|
|
2941
|
+
children: "Previous"
|
|
2942
|
+
}
|
|
2943
|
+
),
|
|
2944
|
+
/* @__PURE__ */ jsx16(
|
|
2945
|
+
"button",
|
|
2946
|
+
{
|
|
2947
|
+
type: "button",
|
|
2948
|
+
"data-testid": "book-next",
|
|
2949
|
+
disabled: visibleIndex >= pages.length - 1 || pages.length === 0,
|
|
2950
|
+
onClick: goNext,
|
|
2951
|
+
children: "Next"
|
|
2952
|
+
}
|
|
2953
|
+
)
|
|
2954
|
+
] })
|
|
2955
|
+
] });
|
|
2956
|
+
}
|
|
2957
|
+
);
|
|
2958
|
+
var InteractiveBook = forwardRef8(function InteractiveBook2(props, ref) {
|
|
2959
|
+
const blockId = useMemo14(
|
|
2960
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
2961
|
+
[props.blockId]
|
|
2962
|
+
);
|
|
2963
|
+
const pages = React17.Children.toArray(props.children).filter(
|
|
2964
|
+
React17.isValidElement
|
|
2965
|
+
);
|
|
2966
|
+
const { config } = useLessonkit();
|
|
2967
|
+
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
2968
|
+
const initialIndex = useCompoundInitialIndex({
|
|
2969
|
+
courseId: config.courseId,
|
|
2970
|
+
compoundId: blockId,
|
|
2971
|
+
pageCount: pages.length,
|
|
2972
|
+
persistEnabled
|
|
2973
|
+
});
|
|
2974
|
+
const [index, setIndex] = useState11(initialIndex);
|
|
2975
|
+
const setIndexStable = useCallback8((i) => setIndex(i), []);
|
|
2976
|
+
return /* @__PURE__ */ jsx16(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx16(
|
|
2977
|
+
InteractiveBookInner,
|
|
2978
|
+
{
|
|
2979
|
+
...props,
|
|
2980
|
+
ref,
|
|
2981
|
+
blockId,
|
|
2982
|
+
pages,
|
|
2983
|
+
index,
|
|
2984
|
+
setIndex,
|
|
2985
|
+
persistEnabled
|
|
2986
|
+
}
|
|
2987
|
+
) });
|
|
2988
|
+
});
|
|
2989
|
+
setLessonkitBlockType(InteractiveBook, "InteractiveBook");
|
|
2990
|
+
|
|
2991
|
+
// src/blocks/Accordion.tsx
|
|
2992
|
+
import { useId as useId3, useState as useState12 } from "react";
|
|
2993
|
+
import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2994
|
+
function Accordion(props) {
|
|
2995
|
+
if (isDevEnvironment4()) {
|
|
2996
|
+
validateAccordionSections(props.sections);
|
|
2997
|
+
}
|
|
2998
|
+
const [open, setOpen] = useState12(/* @__PURE__ */ new Set());
|
|
2999
|
+
const { track } = useLessonkit();
|
|
3000
|
+
const lessonId = useEnclosingLessonId();
|
|
3001
|
+
const baseId = useId3();
|
|
3002
|
+
const toggle = (sectionId) => {
|
|
3003
|
+
setOpen((prev) => {
|
|
3004
|
+
const next = new Set(prev);
|
|
3005
|
+
const expanded = !next.has(sectionId);
|
|
3006
|
+
if (expanded) next.add(sectionId);
|
|
3007
|
+
else next.delete(sectionId);
|
|
3008
|
+
track(
|
|
3009
|
+
"accordion_section_toggled",
|
|
3010
|
+
{ blockId: props.blockId, sectionId, expanded },
|
|
3011
|
+
lessonId ? { lessonId } : void 0
|
|
3012
|
+
);
|
|
3013
|
+
return next;
|
|
3014
|
+
});
|
|
3015
|
+
};
|
|
3016
|
+
return /* @__PURE__ */ jsx17("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
|
|
3017
|
+
const expanded = open.has(section.id);
|
|
3018
|
+
const panelId = `${baseId}-${section.id}`;
|
|
3019
|
+
const triggerId = `${baseId}-trigger-${section.id}`;
|
|
3020
|
+
return /* @__PURE__ */ jsxs12("div", { "data-testid": `accordion-section-${section.id}`, children: [
|
|
3021
|
+
/* @__PURE__ */ jsx17("h4", { children: /* @__PURE__ */ jsx17(
|
|
3022
|
+
"button",
|
|
3023
|
+
{
|
|
3024
|
+
id: triggerId,
|
|
3025
|
+
type: "button",
|
|
3026
|
+
"aria-expanded": expanded,
|
|
3027
|
+
"aria-controls": panelId,
|
|
3028
|
+
"data-testid": `accordion-trigger-${section.id}`,
|
|
3029
|
+
onClick: () => toggle(section.id),
|
|
3030
|
+
children: section.title
|
|
3031
|
+
}
|
|
3032
|
+
) }),
|
|
3033
|
+
expanded ? /* @__PURE__ */ jsx17("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
|
|
3034
|
+
] }, section.id);
|
|
3035
|
+
}) });
|
|
3036
|
+
}
|
|
3037
|
+
setLessonkitBlockType(Accordion, "Accordion");
|
|
3038
|
+
|
|
3039
|
+
// src/blocks/DialogCards.tsx
|
|
3040
|
+
import { useState as useState13 } from "react";
|
|
3041
|
+
import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3042
|
+
function DialogCards(props) {
|
|
3043
|
+
const [index, setIndex] = useState13(0);
|
|
3044
|
+
const [flipped, setFlipped] = useState13(false);
|
|
3045
|
+
const card = props.cards[index];
|
|
3046
|
+
if (!card) return null;
|
|
3047
|
+
return /* @__PURE__ */ jsxs13("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
|
|
3048
|
+
/* @__PURE__ */ jsxs13("p", { children: [
|
|
3049
|
+
"Card ",
|
|
3050
|
+
index + 1,
|
|
3051
|
+
" of ",
|
|
3052
|
+
props.cards.length
|
|
3053
|
+
] }),
|
|
3054
|
+
/* @__PURE__ */ jsx18(
|
|
3055
|
+
"button",
|
|
3056
|
+
{
|
|
3057
|
+
type: "button",
|
|
3058
|
+
"data-testid": "dialog-card-flip",
|
|
3059
|
+
"aria-pressed": flipped,
|
|
3060
|
+
onClick: () => setFlipped((f) => !f),
|
|
3061
|
+
style: { minHeight: "6rem", width: "100%" },
|
|
3062
|
+
children: flipped ? card.back : card.front
|
|
3063
|
+
}
|
|
3064
|
+
),
|
|
3065
|
+
/* @__PURE__ */ jsxs13("nav", { "aria-label": "Card navigation", children: [
|
|
3066
|
+
/* @__PURE__ */ jsx18(
|
|
3067
|
+
"button",
|
|
3068
|
+
{
|
|
3069
|
+
type: "button",
|
|
3070
|
+
"data-testid": "dialog-prev",
|
|
3071
|
+
disabled: index === 0,
|
|
3072
|
+
onClick: () => {
|
|
3073
|
+
setIndex((i) => i - 1);
|
|
3074
|
+
setFlipped(false);
|
|
3075
|
+
},
|
|
3076
|
+
children: "Previous"
|
|
3077
|
+
}
|
|
3078
|
+
),
|
|
3079
|
+
/* @__PURE__ */ jsx18(
|
|
3080
|
+
"button",
|
|
3081
|
+
{
|
|
3082
|
+
type: "button",
|
|
3083
|
+
"data-testid": "dialog-next",
|
|
3084
|
+
disabled: index >= props.cards.length - 1,
|
|
3085
|
+
onClick: () => {
|
|
3086
|
+
setIndex((i) => i + 1);
|
|
3087
|
+
setFlipped(false);
|
|
3088
|
+
},
|
|
3089
|
+
children: "Next"
|
|
3090
|
+
}
|
|
3091
|
+
)
|
|
3092
|
+
] })
|
|
3093
|
+
] });
|
|
3094
|
+
}
|
|
3095
|
+
setLessonkitBlockType(DialogCards, "DialogCards");
|
|
3096
|
+
|
|
3097
|
+
// src/blocks/Flashcards.tsx
|
|
3098
|
+
import { useState as useState14 } from "react";
|
|
3099
|
+
import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
3100
|
+
function Flashcards(props) {
|
|
3101
|
+
const [index, setIndex] = useState14(0);
|
|
3102
|
+
const [face, setFace] = useState14("front");
|
|
3103
|
+
const { track } = useLessonkit();
|
|
3104
|
+
const lessonId = useEnclosingLessonId();
|
|
3105
|
+
const card = props.cards[index];
|
|
3106
|
+
if (!card) return null;
|
|
3107
|
+
const flip = () => {
|
|
3108
|
+
const next = face === "front" ? "back" : "front";
|
|
3109
|
+
setFace(next);
|
|
3110
|
+
track(
|
|
3111
|
+
"flashcard_flipped",
|
|
3112
|
+
{ blockId: props.blockId, cardIndex: index, face: next },
|
|
3113
|
+
lessonId ? { lessonId } : void 0
|
|
3114
|
+
);
|
|
3115
|
+
};
|
|
3116
|
+
return /* @__PURE__ */ jsxs14("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
|
|
3117
|
+
/* @__PURE__ */ jsx19("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
|
|
3118
|
+
props.selfScore ? /* @__PURE__ */ jsx19("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
|
|
3119
|
+
/* @__PURE__ */ jsx19(
|
|
3120
|
+
"button",
|
|
3121
|
+
{
|
|
3122
|
+
type: "button",
|
|
3123
|
+
"data-testid": "flashcard-next",
|
|
3124
|
+
disabled: index >= props.cards.length - 1,
|
|
3125
|
+
onClick: () => {
|
|
3126
|
+
setIndex((i) => i + 1);
|
|
3127
|
+
setFace("front");
|
|
3128
|
+
},
|
|
3129
|
+
children: "Next card"
|
|
3130
|
+
}
|
|
3131
|
+
)
|
|
3132
|
+
] });
|
|
3133
|
+
}
|
|
3134
|
+
setLessonkitBlockType(Flashcards, "Flashcards");
|
|
3135
|
+
|
|
3136
|
+
// src/blocks/ImageHotspots.tsx
|
|
3137
|
+
import { useState as useState15 } from "react";
|
|
3138
|
+
import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
3139
|
+
function ImageHotspots(props) {
|
|
3140
|
+
const [active, setActive] = useState15(null);
|
|
3141
|
+
const { track } = useLessonkit();
|
|
3142
|
+
const lessonId = useEnclosingLessonId();
|
|
3143
|
+
const open = (hotspotId) => {
|
|
3144
|
+
setActive(hotspotId);
|
|
3145
|
+
track(
|
|
3146
|
+
"hotspot_opened",
|
|
3147
|
+
{ blockId: props.blockId, hotspotId },
|
|
3148
|
+
lessonId ? { lessonId } : void 0
|
|
3149
|
+
);
|
|
3150
|
+
};
|
|
3151
|
+
return /* @__PURE__ */ jsxs15("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
|
|
3152
|
+
/* @__PURE__ */ jsxs15("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
3153
|
+
/* @__PURE__ */ jsx20("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
3154
|
+
props.hotspots.map((h) => /* @__PURE__ */ jsx20(
|
|
3155
|
+
"button",
|
|
3156
|
+
{
|
|
3157
|
+
type: "button",
|
|
3158
|
+
"aria-expanded": active === h.id,
|
|
3159
|
+
"aria-label": h.label,
|
|
3160
|
+
"data-testid": `hotspot-${h.id}`,
|
|
3161
|
+
style: {
|
|
3162
|
+
position: "absolute",
|
|
3163
|
+
left: `${h.x}%`,
|
|
3164
|
+
top: `${h.y}%`,
|
|
3165
|
+
transform: "translate(-50%, -50%)"
|
|
3166
|
+
},
|
|
3167
|
+
onClick: () => open(h.id),
|
|
3168
|
+
children: "+"
|
|
3169
|
+
},
|
|
3170
|
+
h.id
|
|
3171
|
+
))
|
|
3172
|
+
] }),
|
|
3173
|
+
active ? /* @__PURE__ */ jsxs15("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
|
|
3174
|
+
props.hotspots.find((h) => h.id === active)?.content,
|
|
3175
|
+
/* @__PURE__ */ jsx20("button", { type: "button", onClick: () => setActive(null), children: "Close" })
|
|
3176
|
+
] }) : null
|
|
3177
|
+
] });
|
|
3178
|
+
}
|
|
3179
|
+
setLessonkitBlockType(ImageHotspots, "ImageHotspots");
|
|
3180
|
+
|
|
3181
|
+
// src/blocks/ImageSlider.tsx
|
|
3182
|
+
import { useState as useState16 } from "react";
|
|
3183
|
+
import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
3184
|
+
function ImageSlider(props) {
|
|
3185
|
+
const [index, setIndex] = useState16(0);
|
|
3186
|
+
const { track } = useLessonkit();
|
|
3187
|
+
const lessonId = useEnclosingLessonId();
|
|
3188
|
+
const slide = props.slides[index];
|
|
3189
|
+
if (!slide) return null;
|
|
3190
|
+
const goTo = (next) => {
|
|
3191
|
+
setIndex(next);
|
|
3192
|
+
track(
|
|
3193
|
+
"image_slider_changed",
|
|
3194
|
+
{ blockId: props.blockId, slideIndex: next },
|
|
3195
|
+
lessonId ? { lessonId } : void 0
|
|
3196
|
+
);
|
|
3197
|
+
};
|
|
3198
|
+
return /* @__PURE__ */ jsxs16("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
|
|
3199
|
+
/* @__PURE__ */ jsx21("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
|
|
3200
|
+
slide.caption ? /* @__PURE__ */ jsx21("p", { children: slide.caption }) : null,
|
|
3201
|
+
/* @__PURE__ */ jsxs16("nav", { "aria-label": "Slide navigation", children: [
|
|
3202
|
+
/* @__PURE__ */ jsx21(
|
|
3203
|
+
"button",
|
|
3204
|
+
{
|
|
3205
|
+
type: "button",
|
|
3206
|
+
"data-testid": "slider-prev",
|
|
3207
|
+
disabled: index === 0,
|
|
3208
|
+
onClick: () => goTo(index - 1),
|
|
3209
|
+
children: "Previous"
|
|
3210
|
+
}
|
|
3211
|
+
),
|
|
3212
|
+
/* @__PURE__ */ jsxs16("span", { children: [
|
|
3213
|
+
index + 1,
|
|
3214
|
+
" / ",
|
|
3215
|
+
props.slides.length
|
|
3216
|
+
] }),
|
|
3217
|
+
/* @__PURE__ */ jsx21(
|
|
3218
|
+
"button",
|
|
3219
|
+
{
|
|
3220
|
+
type: "button",
|
|
3221
|
+
"data-testid": "slider-next",
|
|
3222
|
+
disabled: index >= props.slides.length - 1,
|
|
3223
|
+
onClick: () => goTo(index + 1),
|
|
3224
|
+
children: "Next"
|
|
3225
|
+
}
|
|
3226
|
+
)
|
|
3227
|
+
] })
|
|
3228
|
+
] });
|
|
3229
|
+
}
|
|
3230
|
+
setLessonkitBlockType(ImageSlider, "ImageSlider");
|
|
3231
|
+
|
|
3232
|
+
// src/blocks/FindHotspot.tsx
|
|
3233
|
+
import { forwardRef as forwardRef9, useMemo as useMemo15, useState as useState17 } from "react";
|
|
3234
|
+
import { jsx as jsx22, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
3235
|
+
var INTERACTION6 = "findHotspot";
|
|
3236
|
+
function FindHotspotInner(props, ref) {
|
|
3237
|
+
const checkId = useMemo15(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
3238
|
+
const [selected, setSelected] = useState17(null);
|
|
3239
|
+
const [checked, setChecked] = useState17(false);
|
|
3240
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
3241
|
+
const correct = selected === props.correctTargetId;
|
|
3242
|
+
const handle = useMemo15(
|
|
3243
|
+
() => buildAssessmentHandle({
|
|
3244
|
+
checkId,
|
|
3245
|
+
getScore: () => checked && correct ? 1 : 0,
|
|
3246
|
+
getMaxScore: () => 1,
|
|
3247
|
+
getAnswerGiven: () => selected !== null,
|
|
3248
|
+
resetTask: () => {
|
|
3249
|
+
setSelected(null);
|
|
3250
|
+
setChecked(false);
|
|
3251
|
+
},
|
|
3252
|
+
showSolutions: () => setSelected(props.correctTargetId),
|
|
3253
|
+
getXAPIData: () => ({
|
|
3254
|
+
checkId,
|
|
3255
|
+
interactionType: INTERACTION6,
|
|
3256
|
+
response: selected ?? void 0,
|
|
3257
|
+
correct: checked ? correct : void 0,
|
|
3258
|
+
score: checked && correct ? 1 : 0,
|
|
3259
|
+
maxScore: 1
|
|
3260
|
+
}),
|
|
3261
|
+
getCurrentState: () => ({ selected, checked }),
|
|
3262
|
+
resume: (state) => {
|
|
3263
|
+
const nextSelected = readStringField(state, "selected");
|
|
3264
|
+
if (typeof nextSelected === "string") setSelected(nextSelected);
|
|
3265
|
+
readBooleanStateField(state, "checked", setChecked);
|
|
3266
|
+
}
|
|
3267
|
+
}),
|
|
3268
|
+
[checkId, selected, checked, correct, props.correctTargetId]
|
|
3269
|
+
);
|
|
3270
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3271
|
+
const submit = () => {
|
|
3272
|
+
if (!selected) return;
|
|
3273
|
+
setChecked(true);
|
|
3274
|
+
assessment.answer({
|
|
3275
|
+
checkId,
|
|
3276
|
+
interactionType: INTERACTION6,
|
|
3277
|
+
response: selected,
|
|
3278
|
+
correct
|
|
3279
|
+
});
|
|
3280
|
+
if (correct) {
|
|
3281
|
+
assessment.complete({
|
|
3282
|
+
checkId,
|
|
3283
|
+
interactionType: INTERACTION6,
|
|
3284
|
+
score: 1,
|
|
3285
|
+
maxScore: 1,
|
|
3286
|
+
passingScore: props.passingScore
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
};
|
|
3290
|
+
return /* @__PURE__ */ jsxs17("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
|
|
3291
|
+
/* @__PURE__ */ jsxs17("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
3292
|
+
/* @__PURE__ */ jsx22("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
3293
|
+
props.targets.map((t) => /* @__PURE__ */ jsx22(
|
|
3294
|
+
"button",
|
|
3295
|
+
{
|
|
3296
|
+
type: "button",
|
|
3297
|
+
"aria-label": t.label,
|
|
3298
|
+
"aria-pressed": selected === t.id,
|
|
3299
|
+
"data-testid": `target-${t.id}`,
|
|
3300
|
+
style: {
|
|
3301
|
+
position: "absolute",
|
|
3302
|
+
left: `${t.x}%`,
|
|
3303
|
+
top: `${t.y}%`,
|
|
3304
|
+
transform: "translate(-50%, -50%)"
|
|
3305
|
+
},
|
|
3306
|
+
onClick: () => setSelected(t.id),
|
|
3307
|
+
children: t.label
|
|
3308
|
+
},
|
|
3309
|
+
t.id
|
|
3310
|
+
))
|
|
3311
|
+
] }),
|
|
3312
|
+
/* @__PURE__ */ jsx22("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
|
|
3313
|
+
checked ? /* @__PURE__ */ jsx22("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
3314
|
+
] });
|
|
1252
3315
|
}
|
|
3316
|
+
var FindHotspotInnerForwarded = forwardRef9(FindHotspotInner);
|
|
3317
|
+
var FindHotspot = forwardRef9(function FindHotspot2(props, ref) {
|
|
3318
|
+
return /* @__PURE__ */ jsx22(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx22(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
3319
|
+
});
|
|
3320
|
+
setLessonkitBlockType(FindHotspot, "FindHotspot");
|
|
3321
|
+
|
|
3322
|
+
// src/blocks/FindMultipleHotspots.tsx
|
|
3323
|
+
import { forwardRef as forwardRef10, useMemo as useMemo16, useState as useState18 } from "react";
|
|
3324
|
+
import { jsx as jsx23, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
3325
|
+
var INTERACTION7 = "findMultipleHotspots";
|
|
3326
|
+
function FindMultipleHotspotsInner(props, ref) {
|
|
3327
|
+
const checkId = useMemo16(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
3328
|
+
const [selected, setSelected] = useState18(/* @__PURE__ */ new Set());
|
|
3329
|
+
const [checked, setChecked] = useState18(false);
|
|
3330
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
3331
|
+
const toggle = (id) => {
|
|
3332
|
+
setSelected((prev) => {
|
|
3333
|
+
const next = new Set(prev);
|
|
3334
|
+
if (next.has(id)) next.delete(id);
|
|
3335
|
+
else next.add(id);
|
|
3336
|
+
return next;
|
|
3337
|
+
});
|
|
3338
|
+
};
|
|
3339
|
+
const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
|
|
3340
|
+
const handle = useMemo16(
|
|
3341
|
+
() => buildAssessmentHandle({
|
|
3342
|
+
checkId,
|
|
3343
|
+
getScore: () => checked && correct ? 1 : 0,
|
|
3344
|
+
getMaxScore: () => 1,
|
|
3345
|
+
getAnswerGiven: () => selected.size > 0,
|
|
3346
|
+
resetTask: () => {
|
|
3347
|
+
setSelected(/* @__PURE__ */ new Set());
|
|
3348
|
+
setChecked(false);
|
|
3349
|
+
},
|
|
3350
|
+
showSolutions: () => setSelected(new Set(props.correctTargetIds)),
|
|
3351
|
+
getXAPIData: () => ({
|
|
3352
|
+
checkId,
|
|
3353
|
+
interactionType: INTERACTION7,
|
|
3354
|
+
response: [...selected],
|
|
3355
|
+
correct: checked ? correct : void 0,
|
|
3356
|
+
score: checked && correct ? 1 : 0,
|
|
3357
|
+
maxScore: 1
|
|
3358
|
+
}),
|
|
3359
|
+
getCurrentState: () => ({ selected: [...selected], checked }),
|
|
3360
|
+
resume: (state) => {
|
|
3361
|
+
const raw = state.selected;
|
|
3362
|
+
if (Array.isArray(raw)) setSelected(new Set(raw.filter((id) => typeof id === "string")));
|
|
3363
|
+
readBooleanStateField(state, "checked", setChecked);
|
|
3364
|
+
}
|
|
3365
|
+
}),
|
|
3366
|
+
[checkId, selected, checked, correct, props.correctTargetIds]
|
|
3367
|
+
);
|
|
3368
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3369
|
+
const submit = () => {
|
|
3370
|
+
if (selected.size === 0) return;
|
|
3371
|
+
setChecked(true);
|
|
3372
|
+
assessment.answer({
|
|
3373
|
+
checkId,
|
|
3374
|
+
interactionType: INTERACTION7,
|
|
3375
|
+
response: [...selected],
|
|
3376
|
+
correct
|
|
3377
|
+
});
|
|
3378
|
+
if (correct) {
|
|
3379
|
+
assessment.complete({
|
|
3380
|
+
checkId,
|
|
3381
|
+
interactionType: INTERACTION7,
|
|
3382
|
+
score: 1,
|
|
3383
|
+
maxScore: 1,
|
|
3384
|
+
passingScore: props.passingScore
|
|
3385
|
+
});
|
|
3386
|
+
}
|
|
3387
|
+
};
|
|
3388
|
+
return /* @__PURE__ */ jsxs18("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
|
|
3389
|
+
/* @__PURE__ */ jsxs18("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
3390
|
+
/* @__PURE__ */ jsx23("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
3391
|
+
props.targets.map((t) => /* @__PURE__ */ jsx23(
|
|
3392
|
+
"button",
|
|
3393
|
+
{
|
|
3394
|
+
type: "button",
|
|
3395
|
+
"aria-label": t.label,
|
|
3396
|
+
"aria-pressed": selected.has(t.id),
|
|
3397
|
+
"data-testid": `target-${t.id}`,
|
|
3398
|
+
style: {
|
|
3399
|
+
position: "absolute",
|
|
3400
|
+
left: `${t.x}%`,
|
|
3401
|
+
top: `${t.y}%`,
|
|
3402
|
+
transform: "translate(-50%, -50%)"
|
|
3403
|
+
},
|
|
3404
|
+
onClick: () => toggle(t.id),
|
|
3405
|
+
children: t.label
|
|
3406
|
+
},
|
|
3407
|
+
t.id
|
|
3408
|
+
))
|
|
3409
|
+
] }),
|
|
3410
|
+
/* @__PURE__ */ jsx23("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
|
|
3411
|
+
checked ? /* @__PURE__ */ jsx23("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
3412
|
+
] });
|
|
3413
|
+
}
|
|
3414
|
+
var FindMultipleHotspotsInnerForwarded = forwardRef10(FindMultipleHotspotsInner);
|
|
3415
|
+
var FindMultipleHotspots = forwardRef10(
|
|
3416
|
+
function FindMultipleHotspots2(props, ref) {
|
|
3417
|
+
return /* @__PURE__ */ jsx23(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx23(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
3418
|
+
}
|
|
3419
|
+
);
|
|
3420
|
+
setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
|
|
1253
3421
|
|
|
1254
3422
|
// src/index.tsx
|
|
1255
3423
|
import {
|
|
@@ -1263,14 +3431,14 @@ import {
|
|
|
1263
3431
|
} from "@lessonkit/core";
|
|
1264
3432
|
|
|
1265
3433
|
// src/theme/ThemeProvider.tsx
|
|
1266
|
-
import
|
|
1267
|
-
createContext as
|
|
1268
|
-
useCallback as
|
|
1269
|
-
useContext as
|
|
3434
|
+
import React25, {
|
|
3435
|
+
createContext as createContext4,
|
|
3436
|
+
useCallback as useCallback9,
|
|
3437
|
+
useContext as useContext4,
|
|
1270
3438
|
useLayoutEffect as useLayoutEffect2,
|
|
1271
|
-
useMemo as
|
|
1272
|
-
useRef as
|
|
1273
|
-
useState as
|
|
3439
|
+
useMemo as useMemo17,
|
|
3440
|
+
useRef as useRef12,
|
|
3441
|
+
useState as useState19
|
|
1274
3442
|
} from "react";
|
|
1275
3443
|
import {
|
|
1276
3444
|
brandThemeOverrides,
|
|
@@ -1297,9 +3465,12 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
1297
3465
|
}
|
|
1298
3466
|
|
|
1299
3467
|
// src/theme/ThemeProvider.tsx
|
|
1300
|
-
import { jsx as
|
|
1301
|
-
var ThemeContext =
|
|
1302
|
-
var useIsoLayoutEffect2 =
|
|
3468
|
+
import { jsx as jsx24 } from "react/jsx-runtime";
|
|
3469
|
+
var ThemeContext = createContext4(null);
|
|
3470
|
+
var useIsoLayoutEffect2 = (
|
|
3471
|
+
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
3472
|
+
typeof window !== "undefined" ? useLayoutEffect2 : React25.useEffect
|
|
3473
|
+
);
|
|
1303
3474
|
function getSystemMode() {
|
|
1304
3475
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
1305
3476
|
return "light";
|
|
@@ -1317,7 +3488,7 @@ function ThemeProvider(props) {
|
|
|
1317
3488
|
const preset = props.preset ?? "default";
|
|
1318
3489
|
const mode = props.mode ?? "light";
|
|
1319
3490
|
const targetKind = props.target ?? "document";
|
|
1320
|
-
const [resolvedMode, setResolvedMode] =
|
|
3491
|
+
const [resolvedMode, setResolvedMode] = useState19(
|
|
1321
3492
|
() => mode === "system" ? getSystemMode() : mode
|
|
1322
3493
|
);
|
|
1323
3494
|
useIsoLayoutEffect2(() => {
|
|
@@ -1333,20 +3504,20 @@ function ThemeProvider(props) {
|
|
|
1333
3504
|
return () => mq.removeEventListener("change", onChange);
|
|
1334
3505
|
}, [mode]);
|
|
1335
3506
|
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
1336
|
-
const effectiveTheme =
|
|
3507
|
+
const effectiveTheme = useMemo17(() => {
|
|
1337
3508
|
const modeBase = resolveModeBase(mode, dataTheme);
|
|
1338
3509
|
const base = preset === "default" ? modeBase : preset === "brand" ? mergeThemes(modeBase, brandThemeOverrides) : mergeThemes(modeBase, getPresetTheme(preset));
|
|
1339
3510
|
return mergeThemes(base, props.theme ?? {});
|
|
1340
3511
|
}, [preset, mode, dataTheme, props.theme]);
|
|
1341
|
-
const hostRef =
|
|
1342
|
-
const appliedKeysRef =
|
|
3512
|
+
const hostRef = useRef12(null);
|
|
3513
|
+
const appliedKeysRef = useRef12(/* @__PURE__ */ new Set());
|
|
1343
3514
|
useIsoLayoutEffect2(() => {
|
|
1344
3515
|
if (targetKind === "document" && typeof document !== "undefined") {
|
|
1345
3516
|
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
1346
3517
|
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
1347
3518
|
}
|
|
1348
3519
|
}, [targetKind, dataTheme]);
|
|
1349
|
-
const inject =
|
|
3520
|
+
const inject = useCallback9(() => {
|
|
1350
3521
|
const vars = themeToCssVariables(effectiveTheme);
|
|
1351
3522
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
1352
3523
|
if (!el) return;
|
|
@@ -1363,7 +3534,7 @@ function ThemeProvider(props) {
|
|
|
1363
3534
|
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
1364
3535
|
};
|
|
1365
3536
|
}, [inject, targetKind]);
|
|
1366
|
-
const value =
|
|
3537
|
+
const value = useMemo17(
|
|
1367
3538
|
() => ({
|
|
1368
3539
|
theme: effectiveTheme,
|
|
1369
3540
|
preset,
|
|
@@ -1373,20 +3544,275 @@ function ThemeProvider(props) {
|
|
|
1373
3544
|
[effectiveTheme, preset, mode, dataTheme]
|
|
1374
3545
|
);
|
|
1375
3546
|
if (targetKind === "document") {
|
|
1376
|
-
return /* @__PURE__ */
|
|
3547
|
+
return /* @__PURE__ */ jsx24(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx24("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
|
|
1377
3548
|
}
|
|
1378
|
-
return /* @__PURE__ */
|
|
3549
|
+
return /* @__PURE__ */ jsx24(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx24("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
1379
3550
|
}
|
|
1380
3551
|
function useTheme() {
|
|
1381
|
-
const ctx =
|
|
3552
|
+
const ctx = useContext4(ThemeContext);
|
|
1382
3553
|
if (!ctx) {
|
|
1383
3554
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
1384
3555
|
}
|
|
1385
3556
|
return ctx;
|
|
1386
3557
|
}
|
|
1387
3558
|
|
|
3559
|
+
// src/catalogV3Entries.ts
|
|
3560
|
+
import {
|
|
3561
|
+
ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
3562
|
+
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
3563
|
+
PAGE_ALLOWED_CHILD_TYPES,
|
|
3564
|
+
COMPOUND_MAX_NESTING_DEPTH as COMPOUND_MAX_NESTING_DEPTH2
|
|
3565
|
+
} from "@lessonkit/core";
|
|
3566
|
+
var COMPOUND_PARENTS = ["Lesson", "Page", "InteractiveBook", "AssessmentSequence"];
|
|
3567
|
+
function extendParents(entry) {
|
|
3568
|
+
if (!entry.parentConstraints?.length) return entry;
|
|
3569
|
+
const merged = /* @__PURE__ */ new Set([...entry.parentConstraints, ...COMPOUND_PARENTS]);
|
|
3570
|
+
return { ...entry, parentConstraints: [...merged] };
|
|
3571
|
+
}
|
|
3572
|
+
var assessmentBehaviourProps = [
|
|
3573
|
+
{ name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
|
|
3574
|
+
{ name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
|
|
3575
|
+
{ name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
|
|
3576
|
+
{ name: "passingScore", type: "number", required: false, description: "Minimum score to pass." }
|
|
3577
|
+
];
|
|
3578
|
+
var v3CompoundAndContentEntries = [
|
|
3579
|
+
{
|
|
3580
|
+
type: "Text",
|
|
3581
|
+
category: "content",
|
|
3582
|
+
description: "Paragraph text content.",
|
|
3583
|
+
props: [
|
|
3584
|
+
{ name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
|
|
3585
|
+
{ name: "children", type: "ReactNode", required: true, description: "Text body." }
|
|
3586
|
+
],
|
|
3587
|
+
requiredIds: [],
|
|
3588
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
3589
|
+
a11y: { element: "p", ariaLabel: "Text", keyboard: "N/A", notes: "Semantic paragraph." },
|
|
3590
|
+
theming: { surface: "global-inherit", stylingNotes: "Inherits theme." },
|
|
3591
|
+
telemetry: { emits: [] }
|
|
3592
|
+
},
|
|
3593
|
+
{
|
|
3594
|
+
type: "Heading",
|
|
3595
|
+
category: "content",
|
|
3596
|
+
description: "Heading levels 1\u20133.",
|
|
3597
|
+
props: [
|
|
3598
|
+
{ name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
|
|
3599
|
+
{ name: "level", type: "1 | 2 | 3", required: true, description: "Heading level." },
|
|
3600
|
+
{ name: "children", type: "ReactNode", required: true, description: "Heading text." }
|
|
3601
|
+
],
|
|
3602
|
+
requiredIds: [],
|
|
3603
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
3604
|
+
a11y: { element: "h1-h3", ariaLabel: "Heading", keyboard: "N/A", notes: "Use one level per outline." },
|
|
3605
|
+
theming: { surface: "global-inherit", stylingNotes: "Inherits theme." },
|
|
3606
|
+
telemetry: { emits: [] }
|
|
3607
|
+
},
|
|
3608
|
+
{
|
|
3609
|
+
type: "Image",
|
|
3610
|
+
category: "content",
|
|
3611
|
+
description: "Image with required alt text.",
|
|
3612
|
+
props: [
|
|
3613
|
+
{ name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
|
|
3614
|
+
{ name: "src", type: "string", required: true, description: "Image URL." },
|
|
3615
|
+
{ name: "alt", type: "string", required: true, description: "Alt text." }
|
|
3616
|
+
],
|
|
3617
|
+
requiredIds: [],
|
|
3618
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
3619
|
+
a11y: { element: "img", ariaLabel: "Image", keyboard: "N/A", notes: "Requires alt." },
|
|
3620
|
+
theming: { surface: "global-inherit", stylingNotes: "Responsive max-width." },
|
|
3621
|
+
telemetry: { emits: [] }
|
|
3622
|
+
},
|
|
3623
|
+
{
|
|
3624
|
+
type: "Page",
|
|
3625
|
+
category: "container",
|
|
3626
|
+
compoundContract: true,
|
|
3627
|
+
h5pMachineName: "H5P.Column",
|
|
3628
|
+
h5pAlias: "Column",
|
|
3629
|
+
description: "Column layout container (H5P Column / Page).",
|
|
3630
|
+
allowedChildTypes: [...PAGE_ALLOWED_CHILD_TYPES],
|
|
3631
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.Page,
|
|
3632
|
+
props: [
|
|
3633
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
3634
|
+
{ name: "title", type: "string", required: false, description: "Page title." },
|
|
3635
|
+
{ name: "children", type: "ReactNode", required: true, description: "Page content." }
|
|
3636
|
+
],
|
|
3637
|
+
requiredIds: [],
|
|
3638
|
+
optionalIds: ["blockId"],
|
|
3639
|
+
parentConstraints: ["Lesson", "InteractiveBook"],
|
|
3640
|
+
a11y: { element: "section", ariaLabel: "Page", keyboard: "N/A", notes: "H5P Column equivalent." },
|
|
3641
|
+
theming: { surface: "global-inherit", stylingNotes: "Container." },
|
|
3642
|
+
telemetry: { emits: ["compound_page_viewed"], requiresActiveLesson: true }
|
|
3643
|
+
},
|
|
3644
|
+
{
|
|
3645
|
+
type: "InteractiveBook",
|
|
3646
|
+
category: "container",
|
|
3647
|
+
compoundContract: true,
|
|
3648
|
+
h5pMachineName: "H5P.InteractiveBook",
|
|
3649
|
+
h5pAlias: "Interactive Book",
|
|
3650
|
+
description: "Multi-page book with chapter navigation.",
|
|
3651
|
+
allowedChildTypes: [...INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
|
|
3652
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.InteractiveBook,
|
|
3653
|
+
props: [
|
|
3654
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
3655
|
+
{ name: "title", type: "string", required: true, description: "Book title." },
|
|
3656
|
+
{ name: "showBookScore", type: "boolean", required: false, description: "Show aggregate score." },
|
|
3657
|
+
{ name: "children", type: "Page[]", required: true, description: "Page chapters." }
|
|
3658
|
+
],
|
|
3659
|
+
requiredIds: ["blockId"],
|
|
3660
|
+
parentConstraints: ["Lesson"],
|
|
3661
|
+
a11y: {
|
|
3662
|
+
element: "section",
|
|
3663
|
+
ariaLabel: "Interactive book",
|
|
3664
|
+
keyboard: "Previous/Next chapter navigation.",
|
|
3665
|
+
notes: "H5P Interactive Book equivalent."
|
|
3666
|
+
},
|
|
3667
|
+
theming: { surface: "global-inherit", stylingNotes: "Book chrome." },
|
|
3668
|
+
telemetry: { emits: ["book_page_viewed"], requiresActiveLesson: true }
|
|
3669
|
+
},
|
|
3670
|
+
{
|
|
3671
|
+
type: "Accordion",
|
|
3672
|
+
category: "content",
|
|
3673
|
+
h5pMachineName: "H5P.Accordion",
|
|
3674
|
+
h5pAlias: "Accordion",
|
|
3675
|
+
description: "Expandable sections.",
|
|
3676
|
+
props: [
|
|
3677
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
3678
|
+
{ name: "sections", type: "AccordionSection[]", required: true, description: "Sections." }
|
|
3679
|
+
],
|
|
3680
|
+
requiredIds: ["blockId"],
|
|
3681
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
3682
|
+
a11y: { element: "section", ariaLabel: "Accordion", keyboard: "Button toggles sections.", notes: "No nested accordions." },
|
|
3683
|
+
theming: { surface: "global-inherit", stylingNotes: "Disclosure pattern." },
|
|
3684
|
+
telemetry: { emits: ["accordion_section_toggled"] }
|
|
3685
|
+
},
|
|
3686
|
+
{
|
|
3687
|
+
type: "DialogCards",
|
|
3688
|
+
category: "content",
|
|
3689
|
+
h5pMachineName: "H5P.Dialogcards",
|
|
3690
|
+
h5pAlias: "Dialog Cards",
|
|
3691
|
+
description: "Flip cards with front/back text.",
|
|
3692
|
+
props: [
|
|
3693
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
3694
|
+
{ name: "cards", type: "DialogCard[]", required: true, description: "Cards." }
|
|
3695
|
+
],
|
|
3696
|
+
requiredIds: ["blockId"],
|
|
3697
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
3698
|
+
a11y: { element: "section", ariaLabel: "Dialog cards", keyboard: "Flip and navigate cards.", notes: "Reduced motion safe." },
|
|
3699
|
+
theming: { surface: "global-inherit", stylingNotes: "Card flip." },
|
|
3700
|
+
telemetry: { emits: [] }
|
|
3701
|
+
},
|
|
3702
|
+
{
|
|
3703
|
+
type: "Flashcards",
|
|
3704
|
+
category: "content",
|
|
3705
|
+
h5pMachineName: "H5P.Flashcards",
|
|
3706
|
+
h5pAlias: "Flashcards",
|
|
3707
|
+
description: "Study flashcards with optional self-score.",
|
|
3708
|
+
props: [
|
|
3709
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
3710
|
+
{ name: "cards", type: "Flashcard[]", required: true, description: "Cards." },
|
|
3711
|
+
{ name: "selfScore", type: "boolean", required: false, description: "Self-score mode." }
|
|
3712
|
+
],
|
|
3713
|
+
requiredIds: ["blockId"],
|
|
3714
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
3715
|
+
a11y: { element: "section", ariaLabel: "Flashcards", keyboard: "Flip and next.", notes: "Not LMS-scored by default." },
|
|
3716
|
+
theming: { surface: "global-inherit", stylingNotes: "Study mode." },
|
|
3717
|
+
telemetry: { emits: ["flashcard_flipped"] }
|
|
3718
|
+
},
|
|
3719
|
+
{
|
|
3720
|
+
type: "ImageHotspots",
|
|
3721
|
+
category: "content",
|
|
3722
|
+
h5pMachineName: "H5P.ImageHotspots",
|
|
3723
|
+
h5pAlias: "Image Hotspots",
|
|
3724
|
+
description: "Image with clickable hotspot popovers.",
|
|
3725
|
+
props: [
|
|
3726
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
3727
|
+
{ name: "src", type: "string", required: true, description: "Image URL." },
|
|
3728
|
+
{ name: "alt", type: "string", required: true, description: "Alt text." },
|
|
3729
|
+
{ name: "hotspots", type: "HotspotSpec[]", required: true, description: "Hotspots." }
|
|
3730
|
+
],
|
|
3731
|
+
requiredIds: ["blockId"],
|
|
3732
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
3733
|
+
a11y: { element: "section", ariaLabel: "Image hotspots", keyboard: "Buttons on image.", notes: "Popover dialog." },
|
|
3734
|
+
theming: { surface: "global-inherit", stylingNotes: "Positioned hotspots." },
|
|
3735
|
+
telemetry: { emits: ["hotspot_opened"] }
|
|
3736
|
+
},
|
|
3737
|
+
{
|
|
3738
|
+
type: "ImageSlider",
|
|
3739
|
+
category: "content",
|
|
3740
|
+
h5pMachineName: "H5P.ImageSlider",
|
|
3741
|
+
h5pAlias: "Image Slider",
|
|
3742
|
+
description: "Carousel of images.",
|
|
3743
|
+
props: [
|
|
3744
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
3745
|
+
{ name: "slides", type: "ImageSlide[]", required: true, description: "Slides." }
|
|
3746
|
+
],
|
|
3747
|
+
requiredIds: ["blockId"],
|
|
3748
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
3749
|
+
a11y: { element: "section", ariaLabel: "Image slider", keyboard: "Previous/next slide.", notes: "Carousel." },
|
|
3750
|
+
theming: { surface: "global-inherit", stylingNotes: "Slider." },
|
|
3751
|
+
telemetry: { emits: ["image_slider_changed"] }
|
|
3752
|
+
},
|
|
3753
|
+
{
|
|
3754
|
+
type: "FindHotspot",
|
|
3755
|
+
category: "assessment",
|
|
3756
|
+
assessmentContract: true,
|
|
3757
|
+
h5pMachineName: "H5P.ImageHotspotQuestion",
|
|
3758
|
+
h5pAlias: "Find the Hotspot",
|
|
3759
|
+
description: "Select the correct region on an image.",
|
|
3760
|
+
props: [
|
|
3761
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
3762
|
+
{ name: "src", type: "string", required: true, description: "Image URL." },
|
|
3763
|
+
{ name: "alt", type: "string", required: true, description: "Alt text." },
|
|
3764
|
+
{ name: "targets", type: "HotspotTarget[]", required: true, description: "Targets." },
|
|
3765
|
+
{ name: "correctTargetId", type: "string", required: true, description: "Correct target id." },
|
|
3766
|
+
...assessmentBehaviourProps
|
|
3767
|
+
],
|
|
3768
|
+
requiredIds: ["checkId"],
|
|
3769
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
3770
|
+
a11y: { element: "section", ariaLabel: "Find the hotspot", keyboard: "Select target buttons.", notes: "Scored." },
|
|
3771
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
3772
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
3773
|
+
},
|
|
3774
|
+
{
|
|
3775
|
+
type: "FindMultipleHotspots",
|
|
3776
|
+
category: "assessment",
|
|
3777
|
+
assessmentContract: true,
|
|
3778
|
+
h5pMachineName: "H5P.ImageMultipleHotspotQuestion",
|
|
3779
|
+
h5pAlias: "Find Multiple Hotspots",
|
|
3780
|
+
description: "Select all correct regions on an image.",
|
|
3781
|
+
props: [
|
|
3782
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
3783
|
+
{ name: "src", type: "string", required: true, description: "Image URL." },
|
|
3784
|
+
{ name: "alt", type: "string", required: true, description: "Alt text." },
|
|
3785
|
+
{ name: "targets", type: "HotspotTarget[]", required: true, description: "Targets." },
|
|
3786
|
+
{ name: "correctTargetIds", type: "string[]", required: true, description: "Correct target ids." },
|
|
3787
|
+
...assessmentBehaviourProps
|
|
3788
|
+
],
|
|
3789
|
+
requiredIds: ["checkId"],
|
|
3790
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
3791
|
+
a11y: { element: "section", ariaLabel: "Find multiple hotspots", keyboard: "Toggle targets.", notes: "Scored." },
|
|
3792
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
3793
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
3794
|
+
}
|
|
3795
|
+
];
|
|
3796
|
+
function buildV3CatalogFromV2(v2) {
|
|
3797
|
+
const patched = v2.map((entry) => {
|
|
3798
|
+
const base = extendParents(entry);
|
|
3799
|
+
if (entry.type === "AssessmentSequence") {
|
|
3800
|
+
return {
|
|
3801
|
+
...base,
|
|
3802
|
+
compoundContract: true,
|
|
3803
|
+
allowedChildTypes: [...ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
|
|
3804
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.AssessmentSequence
|
|
3805
|
+
};
|
|
3806
|
+
}
|
|
3807
|
+
return base;
|
|
3808
|
+
});
|
|
3809
|
+
return [...patched, ...v3CompoundAndContentEntries];
|
|
3810
|
+
}
|
|
3811
|
+
|
|
1388
3812
|
// src/blockCatalog.ts
|
|
1389
3813
|
var blockCatalogVersion = 1;
|
|
3814
|
+
var blockCatalogV2Version = 2;
|
|
3815
|
+
var blockCatalogV3Version = 3;
|
|
1390
3816
|
var BLOCK_CATALOG = [
|
|
1391
3817
|
{
|
|
1392
3818
|
type: "Course",
|
|
@@ -1573,13 +3999,170 @@ var BLOCK_CATALOG = [
|
|
|
1573
3999
|
}
|
|
1574
4000
|
}
|
|
1575
4001
|
];
|
|
1576
|
-
|
|
1577
|
-
|
|
4002
|
+
var assessmentBehaviourProps2 = [
|
|
4003
|
+
{ name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
|
|
4004
|
+
{ name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
|
|
4005
|
+
{ name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
|
|
4006
|
+
{ name: "passingScore", type: "number", required: false, description: "Minimum score to pass." }
|
|
4007
|
+
];
|
|
4008
|
+
var v2AssessmentEntries = [
|
|
4009
|
+
{
|
|
4010
|
+
type: "TrueFalse",
|
|
4011
|
+
category: "assessment",
|
|
4012
|
+
assessmentContract: true,
|
|
4013
|
+
h5pMachineName: "H5P.TrueFalse",
|
|
4014
|
+
h5pAlias: "True/False",
|
|
4015
|
+
description: "Binary true/false question with assessment contract.",
|
|
4016
|
+
props: [
|
|
4017
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
4018
|
+
{ name: "question", type: "string", required: true, description: "Question text." },
|
|
4019
|
+
{ name: "answer", type: "boolean", required: true, description: "Correct answer." },
|
|
4020
|
+
...assessmentBehaviourProps2
|
|
4021
|
+
],
|
|
4022
|
+
requiredIds: ["checkId"],
|
|
4023
|
+
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
4024
|
+
a11y: {
|
|
4025
|
+
element: "section",
|
|
4026
|
+
ariaLabel: "True or False",
|
|
4027
|
+
keyboard: "Radio group with True/False options.",
|
|
4028
|
+
liveRegions: "role='status' for feedback.",
|
|
4029
|
+
notes: "H5P True/False equivalent."
|
|
4030
|
+
},
|
|
4031
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
4032
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
4033
|
+
},
|
|
4034
|
+
{
|
|
4035
|
+
type: "FillInTheBlanks",
|
|
4036
|
+
category: "assessment",
|
|
4037
|
+
assessmentContract: true,
|
|
4038
|
+
h5pMachineName: "H5P.Blanks",
|
|
4039
|
+
h5pAlias: "Fill in the Blanks",
|
|
4040
|
+
description: "Fill-in-the-blank text with *answer* markers in template.",
|
|
4041
|
+
props: [
|
|
4042
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
4043
|
+
{ name: "template", type: "string", required: true, description: "Text with *blank* markers." },
|
|
4044
|
+
{ name: "blanks", type: "FillInBlankSpec[]", required: false, description: "Explicit blank specs." },
|
|
4045
|
+
...assessmentBehaviourProps2
|
|
4046
|
+
],
|
|
4047
|
+
requiredIds: ["checkId"],
|
|
4048
|
+
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
4049
|
+
a11y: {
|
|
4050
|
+
element: "section",
|
|
4051
|
+
ariaLabel: "Fill in the Blanks",
|
|
4052
|
+
keyboard: "Tab between text inputs.",
|
|
4053
|
+
notes: "H5P Fill in the Blanks equivalent."
|
|
4054
|
+
},
|
|
4055
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
4056
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
4057
|
+
},
|
|
4058
|
+
{
|
|
4059
|
+
type: "DragAndDrop",
|
|
4060
|
+
category: "assessment",
|
|
4061
|
+
assessmentContract: true,
|
|
4062
|
+
h5pMachineName: "H5P.DragQuestion",
|
|
4063
|
+
h5pAlias: "Drag and Drop",
|
|
4064
|
+
description: "Drag items onto labeled targets.",
|
|
4065
|
+
props: [
|
|
4066
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
4067
|
+
{ name: "items", type: "DragItem[]", required: true, description: "Draggable items." },
|
|
4068
|
+
{ name: "targets", type: "DropTarget[]", required: true, description: "Drop targets." },
|
|
4069
|
+
...assessmentBehaviourProps2
|
|
4070
|
+
],
|
|
4071
|
+
requiredIds: ["checkId"],
|
|
4072
|
+
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
4073
|
+
a11y: {
|
|
4074
|
+
element: "section",
|
|
4075
|
+
ariaLabel: "Drag and Drop",
|
|
4076
|
+
keyboard: "Select item then activate target; drag also supported.",
|
|
4077
|
+
notes: "H5P Drag and Drop equivalent."
|
|
4078
|
+
},
|
|
4079
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
4080
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
4081
|
+
},
|
|
4082
|
+
{
|
|
4083
|
+
type: "DragTheWords",
|
|
4084
|
+
category: "assessment",
|
|
4085
|
+
assessmentContract: true,
|
|
4086
|
+
h5pMachineName: "H5P.DragText",
|
|
4087
|
+
h5pAlias: "Drag the Words",
|
|
4088
|
+
description: "Drag words into inline blanks.",
|
|
4089
|
+
props: [
|
|
4090
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
4091
|
+
{ name: "template", type: "string", required: true, description: "Sentence with *blank* zones." },
|
|
4092
|
+
{ name: "words", type: "string[]", required: true, description: "Draggable word bank." },
|
|
4093
|
+
...assessmentBehaviourProps2
|
|
4094
|
+
],
|
|
4095
|
+
requiredIds: ["checkId"],
|
|
4096
|
+
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
4097
|
+
a11y: {
|
|
4098
|
+
element: "section",
|
|
4099
|
+
ariaLabel: "Drag the Words",
|
|
4100
|
+
keyboard: "Select word then activate zone.",
|
|
4101
|
+
notes: "H5P Drag the Words equivalent."
|
|
4102
|
+
},
|
|
4103
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
4104
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
4105
|
+
},
|
|
4106
|
+
{
|
|
4107
|
+
type: "MarkTheWords",
|
|
4108
|
+
category: "assessment",
|
|
4109
|
+
assessmentContract: true,
|
|
4110
|
+
h5pMachineName: "H5P.MarkTheWords",
|
|
4111
|
+
h5pAlias: "Mark the Words",
|
|
4112
|
+
description: "Select correct words in a sentence.",
|
|
4113
|
+
props: [
|
|
4114
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
4115
|
+
{ name: "text", type: "string", required: true, description: "Source text." },
|
|
4116
|
+
{ name: "correctWords", type: "string[]", required: true, description: "Words to mark." },
|
|
4117
|
+
...assessmentBehaviourProps2
|
|
4118
|
+
],
|
|
4119
|
+
requiredIds: ["checkId"],
|
|
4120
|
+
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
4121
|
+
a11y: {
|
|
4122
|
+
element: "section",
|
|
4123
|
+
ariaLabel: "Mark the Words",
|
|
4124
|
+
keyboard: "Toggle words with buttons.",
|
|
4125
|
+
notes: "H5P Mark the Words equivalent."
|
|
4126
|
+
},
|
|
4127
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
4128
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
4129
|
+
},
|
|
4130
|
+
{
|
|
4131
|
+
type: "AssessmentSequence",
|
|
4132
|
+
category: "container",
|
|
4133
|
+
h5pMachineName: "H5P.QuestionSet",
|
|
4134
|
+
h5pAlias: "Question Set",
|
|
4135
|
+
description: "Ordered sequence of contract-compliant assessments.",
|
|
4136
|
+
props: [
|
|
4137
|
+
{ name: "children", type: "ReactNode", required: true, description: "Assessment blocks." },
|
|
4138
|
+
{ name: "sequential", type: "boolean", required: false, description: "One question at a time." },
|
|
4139
|
+
...assessmentBehaviourProps2.filter((p) => p.name !== "passingScore")
|
|
4140
|
+
],
|
|
4141
|
+
requiredIds: [],
|
|
4142
|
+
parentConstraints: ["Lesson"],
|
|
4143
|
+
a11y: {
|
|
4144
|
+
element: "section",
|
|
4145
|
+
ariaLabel: "Assessment sequence",
|
|
4146
|
+
keyboard: "Previous/Next navigation between steps.",
|
|
4147
|
+
notes: "H5P Question Set equivalent."
|
|
4148
|
+
},
|
|
4149
|
+
theming: { surface: "global-inherit", stylingNotes: "Container for assessments." },
|
|
4150
|
+
telemetry: { emits: [], manualTracking: "Child assessments emit assessment_* events." }
|
|
4151
|
+
}
|
|
4152
|
+
];
|
|
4153
|
+
var BLOCK_CATALOG_V2 = [
|
|
4154
|
+
...BLOCK_CATALOG,
|
|
4155
|
+
...v2AssessmentEntries
|
|
4156
|
+
];
|
|
4157
|
+
var BLOCK_CATALOG_V3 = buildV3CatalogFromV2(BLOCK_CATALOG_V2);
|
|
4158
|
+
function cloneCatalogEntry(entry) {
|
|
4159
|
+
return {
|
|
1578
4160
|
...entry,
|
|
1579
4161
|
props: entry.props.map((p) => ({ ...p })),
|
|
1580
4162
|
aliases: entry.aliases ? [...entry.aliases] : void 0,
|
|
1581
4163
|
optionalIds: entry.optionalIds ? [...entry.optionalIds] : void 0,
|
|
1582
4164
|
parentConstraints: entry.parentConstraints ? [...entry.parentConstraints] : void 0,
|
|
4165
|
+
allowedChildTypes: entry.allowedChildTypes ? [...entry.allowedChildTypes] : void 0,
|
|
1583
4166
|
a11y: { ...entry.a11y },
|
|
1584
4167
|
theming: {
|
|
1585
4168
|
...entry.theming,
|
|
@@ -1589,22 +4172,51 @@ function buildBlockCatalog() {
|
|
|
1589
4172
|
...entry.telemetry,
|
|
1590
4173
|
emits: [...entry.telemetry.emits]
|
|
1591
4174
|
}
|
|
1592
|
-
}
|
|
4175
|
+
};
|
|
4176
|
+
}
|
|
4177
|
+
function buildBlockCatalog(opts) {
|
|
4178
|
+
const version = opts?.version ?? 3;
|
|
4179
|
+
const source = version === 3 ? BLOCK_CATALOG_V3 : version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
|
|
4180
|
+
return source.map((entry) => cloneCatalogEntry(entry));
|
|
1593
4181
|
}
|
|
1594
|
-
function getBlockCatalogEntry(type) {
|
|
1595
|
-
|
|
4182
|
+
function getBlockCatalogEntry(type, opts) {
|
|
4183
|
+
const version = opts?.version ?? 3;
|
|
4184
|
+
const source = version === 3 ? BLOCK_CATALOG_V3 : version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
|
|
4185
|
+
return source.find((entry) => entry.type === type || entry.aliases?.includes(type));
|
|
1596
4186
|
}
|
|
1597
4187
|
export {
|
|
4188
|
+
Accordion,
|
|
4189
|
+
AssessmentSequence,
|
|
1598
4190
|
BLOCK_CATALOG,
|
|
4191
|
+
BLOCK_CATALOG_V2,
|
|
4192
|
+
BLOCK_CATALOG_V3,
|
|
1599
4193
|
Course,
|
|
4194
|
+
DialogCards,
|
|
4195
|
+
DragAndDrop,
|
|
4196
|
+
DragTheWords,
|
|
4197
|
+
FillInTheBlanks,
|
|
4198
|
+
FindHotspot,
|
|
4199
|
+
FindMultipleHotspots,
|
|
4200
|
+
Flashcards,
|
|
4201
|
+
Heading,
|
|
4202
|
+
Image,
|
|
4203
|
+
ImageHotspots,
|
|
4204
|
+
ImageSlider,
|
|
4205
|
+
InteractiveBook,
|
|
1600
4206
|
KnowledgeCheck,
|
|
1601
4207
|
Lesson,
|
|
1602
4208
|
LessonkitProvider,
|
|
4209
|
+
MarkTheWords,
|
|
4210
|
+
Page,
|
|
1603
4211
|
ProgressTracker,
|
|
1604
4212
|
Quiz,
|
|
1605
4213
|
Reflection,
|
|
1606
4214
|
Scenario,
|
|
4215
|
+
Text,
|
|
1607
4216
|
ThemeProvider,
|
|
4217
|
+
TrueFalse,
|
|
4218
|
+
blockCatalogV2Version,
|
|
4219
|
+
blockCatalogV3Version,
|
|
1608
4220
|
blockCatalogVersion,
|
|
1609
4221
|
buildBlockCatalog,
|
|
1610
4222
|
buildTelemetryEvent2 as buildTelemetryEvent,
|
|
@@ -1615,7 +4227,9 @@ export {
|
|
|
1615
4227
|
defineLifecyclePlugin,
|
|
1616
4228
|
defineTelemetryPlugin,
|
|
1617
4229
|
getBlockCatalogEntry,
|
|
4230
|
+
resetAssessmentWarningsForTests,
|
|
1618
4231
|
resetQuizWarningsForTests,
|
|
4232
|
+
useAssessmentState,
|
|
1619
4233
|
useCompletion,
|
|
1620
4234
|
useLessonkit,
|
|
1621
4235
|
useProgress,
|