@lessonkit/react 1.1.0 → 1.3.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.v3.json +1679 -0
- package/block-contract.v3.json +110 -0
- package/dist/index.cjs +2981 -746
- package/dist/index.d.cts +220 -28
- package/dist/index.d.ts +220 -28
- package/dist/index.js +3063 -828
- package/package.json +13 -9
package/dist/index.js
CHANGED
|
@@ -1,29 +1,6 @@
|
|
|
1
1
|
// src/components.tsx
|
|
2
|
-
import { useEffect as
|
|
3
|
-
import { visuallyHiddenStyle } from "@lessonkit/accessibility";
|
|
4
|
-
|
|
5
|
-
// src/assessment/scoring.ts
|
|
6
|
-
function resolvePassingThreshold(passingScore, maxScore) {
|
|
7
|
-
return passingScore ?? maxScore;
|
|
8
|
-
}
|
|
9
|
-
function meetsPassingThreshold(score, maxScore, passingScore) {
|
|
10
|
-
const threshold = resolvePassingThreshold(passingScore, maxScore);
|
|
11
|
-
return score >= threshold;
|
|
12
|
-
}
|
|
13
|
-
function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
|
|
14
|
-
const maxScore = custom?.maxScore ?? fallbackMax;
|
|
15
|
-
if (custom?.passed !== void 0) {
|
|
16
|
-
const score2 = custom.passed ? custom.score ?? maxScore : custom.score ?? 0;
|
|
17
|
-
return { score: score2, maxScore, passed: custom.passed };
|
|
18
|
-
}
|
|
19
|
-
if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
20
|
-
const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
|
|
21
|
-
return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
|
|
22
|
-
}
|
|
23
|
-
const score = fallbackCorrect ? maxScore : 0;
|
|
24
|
-
const passed = meetsPassingThreshold(score, maxScore, passingScore);
|
|
25
|
-
return { score, maxScore, passed };
|
|
26
|
-
}
|
|
2
|
+
import { useEffect as useEffect4, useId as useId2, useMemo as useMemo6, useRef as useRef5, useState as useState4 } from "react";
|
|
3
|
+
import { visuallyHiddenStyle as visuallyHiddenStyle2 } from "@lessonkit/accessibility";
|
|
27
4
|
|
|
28
5
|
// src/context.tsx
|
|
29
6
|
import { createContext } from "react";
|
|
@@ -38,7 +15,39 @@ import {
|
|
|
38
15
|
useState
|
|
39
16
|
} from "react";
|
|
40
17
|
import { createLessonkitRuntime, createTrackingClient as createTrackingClient2, assertValidId } from "@lessonkit/core";
|
|
18
|
+
|
|
19
|
+
// src/runtime/observability.ts
|
|
41
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
|
|
42
51
|
import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement3 } from "@lessonkit/xapi";
|
|
43
52
|
|
|
44
53
|
// src/runtime/emitTelemetry.ts
|
|
@@ -59,7 +68,16 @@ import {
|
|
|
59
68
|
mapLessonkitTelemetryToBridgeAction,
|
|
60
69
|
telemetryEventToLessonkit
|
|
61
70
|
} from "@lessonkit/lxpack/bridge";
|
|
62
|
-
|
|
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
|
+
}
|
|
63
81
|
forwardTelemetryToBridge(event, mode);
|
|
64
82
|
}
|
|
65
83
|
|
|
@@ -90,7 +108,9 @@ function createLegacyPipeline(opts, extraSinks = []) {
|
|
|
90
108
|
{
|
|
91
109
|
id: "lxpack-bridge",
|
|
92
110
|
emit(event) {
|
|
93
|
-
forwardTelemetryToLxpack(event, opts.lxpackBridge
|
|
111
|
+
forwardTelemetryToLxpack(event, opts.lxpackBridge, {
|
|
112
|
+
onBridgeMiss: opts.onLxpackBridgeMiss
|
|
113
|
+
});
|
|
94
114
|
}
|
|
95
115
|
},
|
|
96
116
|
...extraSinks
|
|
@@ -117,7 +137,8 @@ function emitTelemetry(tracking, xapi, event, opts) {
|
|
|
117
137
|
const legacy = {
|
|
118
138
|
tracking,
|
|
119
139
|
xapi,
|
|
120
|
-
lxpackBridge: opts?.lxpackBridge ?? "auto"
|
|
140
|
+
lxpackBridge: opts?.lxpackBridge ?? "auto",
|
|
141
|
+
onLxpackBridgeMiss: opts?.onLxpackBridgeMiss
|
|
121
142
|
};
|
|
122
143
|
emitThroughPipeline(event, legacy, opts?.extraSinks);
|
|
123
144
|
}
|
|
@@ -131,6 +152,9 @@ import {
|
|
|
131
152
|
resetStoragePortForTests
|
|
132
153
|
} from "@lessonkit/core";
|
|
133
154
|
|
|
155
|
+
// src/provider/useLessonkitProviderRuntime.ts
|
|
156
|
+
import { resetSharedVolatileSessionIdForTests } from "@lessonkit/core";
|
|
157
|
+
|
|
134
158
|
// src/runtime/progress.ts
|
|
135
159
|
import { createProgressController } from "@lessonkit/core";
|
|
136
160
|
|
|
@@ -207,7 +231,9 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
|
|
|
207
231
|
xapiStatementSent = true;
|
|
208
232
|
}
|
|
209
233
|
}
|
|
210
|
-
forwardTelemetryToLxpack(opts.event, opts.lxpackBridge
|
|
234
|
+
forwardTelemetryToLxpack(opts.event, opts.lxpackBridge, {
|
|
235
|
+
onBridgeMiss: opts.onLxpackBridgeMiss
|
|
236
|
+
});
|
|
211
237
|
const emitCtx = {
|
|
212
238
|
courseId: opts.event.courseId,
|
|
213
239
|
sessionId: opts.event.sessionId,
|
|
@@ -218,56 +244,25 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
|
|
|
218
244
|
}
|
|
219
245
|
|
|
220
246
|
// src/runtime/plugins.ts
|
|
221
|
-
import { createPluginRegistry } from "@lessonkit/core";
|
|
247
|
+
import { buildPluginContext as buildPluginContextFromCore, createPluginRegistry } from "@lessonkit/core";
|
|
222
248
|
function createReactPluginHost(plugins) {
|
|
223
249
|
if (!plugins?.length) return null;
|
|
224
250
|
return createPluginRegistry(plugins);
|
|
225
251
|
}
|
|
226
252
|
function buildPluginContext(opts) {
|
|
227
|
-
return
|
|
228
|
-
courseId: opts.courseId,
|
|
229
|
-
sessionId: opts.sessionId,
|
|
230
|
-
attemptId: opts.attemptId,
|
|
231
|
-
user: opts.user
|
|
232
|
-
};
|
|
253
|
+
return buildPluginContextFromCore(opts);
|
|
233
254
|
}
|
|
234
255
|
function emitTelemetryWithPlugins(opts) {
|
|
235
256
|
const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
|
|
236
257
|
if (next === null) return;
|
|
237
258
|
emitTelemetry(opts.tracking, opts.xapi, next, {
|
|
238
259
|
lxpackBridge: opts.lxpackBridge ?? "auto",
|
|
239
|
-
extraSinks: opts.extraSinks
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// src/runtime/telemetry.ts
|
|
244
|
-
import { createTrackingClient } from "@lessonkit/core";
|
|
245
|
-
function createTrackingClientFromConfig(config) {
|
|
246
|
-
if (config.tracking?.enabled === false) return createTrackingClient();
|
|
247
|
-
if (config.tracking?.createClient) return config.tracking.createClient();
|
|
248
|
-
return createTrackingClient({
|
|
249
|
-
sink: config.tracking?.sink,
|
|
250
|
-
batchSink: config.tracking?.batchSink,
|
|
251
|
-
batch: config.tracking?.batch
|
|
260
|
+
extraSinks: opts.extraSinks,
|
|
261
|
+
onLxpackBridgeMiss: opts.onLxpackBridgeMiss
|
|
252
262
|
});
|
|
253
263
|
}
|
|
254
|
-
async function disposeTrackingClient(client) {
|
|
255
|
-
try {
|
|
256
|
-
await client?.flush?.();
|
|
257
|
-
} catch {
|
|
258
|
-
}
|
|
259
|
-
try {
|
|
260
|
-
await client?.dispose?.();
|
|
261
|
-
} catch {
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
264
|
|
|
265
|
-
// src/provider/
|
|
266
|
-
var useIsoLayoutEffect = (
|
|
267
|
-
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
268
|
-
typeof window !== "undefined" ? useLayoutEffect : useEffect
|
|
269
|
-
);
|
|
270
|
-
var defaultStorage = createSessionStoragePort();
|
|
265
|
+
// src/provider/courseStarted/emit.ts
|
|
271
266
|
var courseStartedTrackingFlightKey = null;
|
|
272
267
|
function isTrackingActive(tracking) {
|
|
273
268
|
return tracking?.enabled !== false;
|
|
@@ -303,9 +298,10 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
|
|
|
303
298
|
try {
|
|
304
299
|
if (shouldCommit && !shouldCommit()) return false;
|
|
305
300
|
tracking.track(event);
|
|
306
|
-
await tracking.flush?.();
|
|
307
|
-
if (shouldCommit && !shouldCommit()) return false;
|
|
308
301
|
markCourseStartedEmittedToTracking(storage, sessionId, courseId);
|
|
302
|
+
const delivered = await tracking.flush?.();
|
|
303
|
+
if (delivered === false) return false;
|
|
304
|
+
if (shouldCommit && !shouldCommit()) return false;
|
|
309
305
|
return true;
|
|
310
306
|
} catch {
|
|
311
307
|
return false;
|
|
@@ -322,6 +318,7 @@ async function emitCourseStartedPipelineOnly(opts) {
|
|
|
322
318
|
event: opts.event,
|
|
323
319
|
xapi: opts.xapi,
|
|
324
320
|
lxpackBridge: opts.lxpackBridge,
|
|
321
|
+
onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
|
|
325
322
|
extraSinks: opts.extraSinks,
|
|
326
323
|
skipXapi: opts.skipXapi
|
|
327
324
|
});
|
|
@@ -374,6 +371,7 @@ async function emitCourseStartedToTrackingOnly(opts) {
|
|
|
374
371
|
event,
|
|
375
372
|
xapi: null,
|
|
376
373
|
lxpackBridge: opts.lxpackBridge,
|
|
374
|
+
onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
|
|
377
375
|
extraSinks: opts.extraSinks,
|
|
378
376
|
skipXapi: true
|
|
379
377
|
});
|
|
@@ -427,6 +425,36 @@ function assertTrackingSinkConfig(tracking) {
|
|
|
427
425
|
"[lessonkit] tracking.sink and tracking.batchSink cannot both be set; use batchSink alone for batched delivery"
|
|
428
426
|
);
|
|
429
427
|
}
|
|
428
|
+
|
|
429
|
+
// src/runtime/telemetry.ts
|
|
430
|
+
import { createTrackingClient } from "@lessonkit/core";
|
|
431
|
+
function createTrackingClientFromConfig(config, observability) {
|
|
432
|
+
if (config.tracking?.enabled === false) return createTrackingClient();
|
|
433
|
+
if (config.tracking?.createClient) return config.tracking.createClient();
|
|
434
|
+
return createTrackingClient({
|
|
435
|
+
sink: config.tracking?.sink,
|
|
436
|
+
batchSink: config.tracking?.batchSink,
|
|
437
|
+
batch: config.tracking?.batch,
|
|
438
|
+
onBufferDrop: observability?.onTelemetryBufferDrop
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
async function disposeTrackingClient(client) {
|
|
442
|
+
try {
|
|
443
|
+
await client?.flush?.();
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
await client?.dispose?.();
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/provider/useLessonkitProviderRuntime.ts
|
|
453
|
+
var useIsoLayoutEffect = (
|
|
454
|
+
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
455
|
+
typeof window !== "undefined" ? useLayoutEffect : useEffect
|
|
456
|
+
);
|
|
457
|
+
var defaultStorage = createSessionStoragePort();
|
|
430
458
|
function useLessonkitProviderRuntime(config) {
|
|
431
459
|
const normalizedCourseId = useMemo(
|
|
432
460
|
() => assertValidId(config.courseId, "courseId"),
|
|
@@ -437,6 +465,14 @@ function useLessonkitProviderRuntime(config) {
|
|
|
437
465
|
[config, normalizedCourseId]
|
|
438
466
|
);
|
|
439
467
|
const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
|
|
468
|
+
useEffect(() => {
|
|
469
|
+
if (useV2Runtime) return;
|
|
470
|
+
const g = globalThis;
|
|
471
|
+
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
|
|
472
|
+
console.warn(
|
|
473
|
+
'[lessonkit] LessonkitProvider runtimeVersion "v1" is deprecated; omit or use "v2" (default). v1 will be removed in LessonKit 2.0.'
|
|
474
|
+
);
|
|
475
|
+
}, [useV2Runtime]);
|
|
440
476
|
const extraSinksRef = useRef(normalizedConfig.sinks);
|
|
441
477
|
extraSinksRef.current = normalizedConfig.sinks;
|
|
442
478
|
const headlessRef = useRef(null);
|
|
@@ -455,7 +491,16 @@ function useLessonkitProviderRuntime(config) {
|
|
|
455
491
|
courseIdRef.current = normalizedCourseId;
|
|
456
492
|
const lxpackBridgeModeRef = useRef(normalizedConfig.lxpack?.bridge ?? "auto");
|
|
457
493
|
lxpackBridgeModeRef.current = normalizedConfig.lxpack?.bridge ?? "auto";
|
|
458
|
-
const
|
|
494
|
+
const observabilityRef = useRef(normalizedConfig.observability);
|
|
495
|
+
observabilityRef.current = normalizedConfig.observability;
|
|
496
|
+
const onLxpackBridgeMiss = useCallback((event) => {
|
|
497
|
+
observabilityRef.current?.onLxpackBridgeMiss?.(event);
|
|
498
|
+
}, []);
|
|
499
|
+
const pluginsFingerprint = normalizedConfig.plugins?.map((p) => `${p.id}\0${p.version}`).join("|") ?? "";
|
|
500
|
+
const pluginHost = useMemo(
|
|
501
|
+
() => createReactPluginHost(normalizedConfig.plugins),
|
|
502
|
+
[pluginsFingerprint]
|
|
503
|
+
);
|
|
459
504
|
const pluginHostRef = useRef(pluginHost);
|
|
460
505
|
pluginHostRef.current = pluginHost;
|
|
461
506
|
const progressRef = useRef(createProgressController());
|
|
@@ -471,7 +516,9 @@ function useLessonkitProviderRuntime(config) {
|
|
|
471
516
|
headlessRef.current = createLessonkitRuntime({
|
|
472
517
|
courseId: normalizedCourseId,
|
|
473
518
|
runtimeVersion: "v2",
|
|
474
|
-
session: normalizedConfig.session
|
|
519
|
+
session: normalizedConfig.session,
|
|
520
|
+
plugins: pluginHostRef.current ?? normalizedConfig.plugins,
|
|
521
|
+
deferPluginSetup: true
|
|
475
522
|
});
|
|
476
523
|
progressRef.current = headlessRef.current.progress;
|
|
477
524
|
} else {
|
|
@@ -485,7 +532,9 @@ function useLessonkitProviderRuntime(config) {
|
|
|
485
532
|
headlessRef.current = createLessonkitRuntime({
|
|
486
533
|
courseId: normalizedCourseId,
|
|
487
534
|
runtimeVersion: "v2",
|
|
488
|
-
session: normalizedConfig.session
|
|
535
|
+
session: normalizedConfig.session,
|
|
536
|
+
plugins: pluginHostRef.current ?? normalizedConfig.plugins,
|
|
537
|
+
deferPluginSetup: true
|
|
489
538
|
});
|
|
490
539
|
}
|
|
491
540
|
if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
|
|
@@ -509,7 +558,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
509
558
|
}, []);
|
|
510
559
|
const activeLessonIdRef = useRef(progress.activeLessonId);
|
|
511
560
|
activeLessonIdRef.current = progress.activeLessonId;
|
|
512
|
-
const xapiQueueRef = useRef(
|
|
561
|
+
const xapiQueueRef = useRef(createXapiQueueFromObservability(normalizedConfig.observability));
|
|
513
562
|
const xapiRef = useRef(null);
|
|
514
563
|
const [xapi, setXapi] = useState(null);
|
|
515
564
|
const prevXapiCourseIdRef = useRef(normalizedCourseId);
|
|
@@ -530,7 +579,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
530
579
|
}
|
|
531
580
|
void xapiRef.current?.flush();
|
|
532
581
|
}
|
|
533
|
-
xapiQueueRef.current =
|
|
582
|
+
xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
|
|
534
583
|
prevXapiCourseIdRef.current = courseId;
|
|
535
584
|
xapiCourseStartedSentOnClientRef.current = false;
|
|
536
585
|
}
|
|
@@ -609,7 +658,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
609
658
|
);
|
|
610
659
|
useIsoLayoutEffect(() => {
|
|
611
660
|
const prev = trackingRef.current;
|
|
612
|
-
const baseSink = normalizedConfig.tracking?.sink;
|
|
661
|
+
const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
|
|
613
662
|
const userBatchSink = normalizedConfig.tracking?.batchSink;
|
|
614
663
|
assertTrackingSinkConfig(normalizedConfig.tracking);
|
|
615
664
|
const sink = pluginHostRef.current && baseSink ? (
|
|
@@ -630,9 +679,12 @@ function useLessonkitProviderRuntime(config) {
|
|
|
630
679
|
}
|
|
631
680
|
return userBatchSink(perEventForBatch);
|
|
632
681
|
} : userBatchSink;
|
|
633
|
-
const next = createTrackingClientFromConfig(
|
|
634
|
-
|
|
635
|
-
|
|
682
|
+
const next = createTrackingClientFromConfig(
|
|
683
|
+
{
|
|
684
|
+
tracking: { ...normalizedConfig.tracking, sink, batchSink }
|
|
685
|
+
},
|
|
686
|
+
observabilityRef.current
|
|
687
|
+
);
|
|
636
688
|
trackingRef.current = next;
|
|
637
689
|
trackingClientForUnmountRef.current = next;
|
|
638
690
|
setTracking(next);
|
|
@@ -659,6 +711,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
659
711
|
attemptId: attemptIdRef.current,
|
|
660
712
|
user: userRef.current,
|
|
661
713
|
lxpackBridge: lxpackBridgeModeRef.current,
|
|
714
|
+
onLxpackBridgeMiss,
|
|
662
715
|
extraSinks: extraSinksRef.current,
|
|
663
716
|
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
664
717
|
onXapiStatementSent: () => {
|
|
@@ -700,9 +753,10 @@ function useLessonkitProviderRuntime(config) {
|
|
|
700
753
|
user: userRef.current
|
|
701
754
|
}),
|
|
702
755
|
lxpackBridge: lxpackBridgeModeRef.current,
|
|
756
|
+
onLxpackBridgeMiss,
|
|
703
757
|
extraSinks: extraSinksRef.current
|
|
704
758
|
});
|
|
705
|
-
}, []);
|
|
759
|
+
}, [onLxpackBridgeMiss]);
|
|
706
760
|
const emitLifecycleEvent = useCallback(
|
|
707
761
|
(name, data, lessonId) => {
|
|
708
762
|
const event = tryBuildTelemetryEvent({
|
|
@@ -758,12 +812,13 @@ function useLessonkitProviderRuntime(config) {
|
|
|
758
812
|
attemptId: attemptIdRef.current,
|
|
759
813
|
user: userRef.current,
|
|
760
814
|
lxpackBridge: lxpackBridgeModeRef.current,
|
|
815
|
+
onLxpackBridgeMiss,
|
|
761
816
|
extraSinks: extraSinksRef.current
|
|
762
817
|
});
|
|
763
818
|
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
764
819
|
}
|
|
765
820
|
})();
|
|
766
|
-
}, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
|
|
821
|
+
}, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress, onLxpackBridgeMiss]);
|
|
767
822
|
const emitLessonCompleted = useCallback(
|
|
768
823
|
(lessonId, durationMs) => {
|
|
769
824
|
track("lesson_completed", { lessonId, durationMs }, { lessonId });
|
|
@@ -812,6 +867,22 @@ function useLessonkitProviderRuntime(config) {
|
|
|
812
867
|
})();
|
|
813
868
|
};
|
|
814
869
|
}, []);
|
|
870
|
+
useEffect(() => {
|
|
871
|
+
if (typeof document === "undefined") return;
|
|
872
|
+
const flushOnExit = () => {
|
|
873
|
+
void xapiRef.current?.flush();
|
|
874
|
+
void trackingRef.current?.flush?.();
|
|
875
|
+
};
|
|
876
|
+
const onVisibilityChange = () => {
|
|
877
|
+
if (document.visibilityState === "hidden") flushOnExit();
|
|
878
|
+
};
|
|
879
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
880
|
+
window.addEventListener("pagehide", flushOnExit);
|
|
881
|
+
return () => {
|
|
882
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
883
|
+
window.removeEventListener("pagehide", flushOnExit);
|
|
884
|
+
};
|
|
885
|
+
}, []);
|
|
815
886
|
const setActiveLesson = useCallback(
|
|
816
887
|
(lessonId) => {
|
|
817
888
|
if (useV2Runtime && headlessRef.current) {
|
|
@@ -875,20 +946,34 @@ function useLessonkitProviderRuntime(config) {
|
|
|
875
946
|
session: normalizedConfig.session
|
|
876
947
|
});
|
|
877
948
|
}
|
|
878
|
-
}, [
|
|
949
|
+
}, [
|
|
950
|
+
useV2Runtime,
|
|
951
|
+
normalizedCourseId,
|
|
952
|
+
sessionAttemptId,
|
|
953
|
+
sessionConfiguredId,
|
|
954
|
+
sessionUserKey,
|
|
955
|
+
normalizedConfig.session
|
|
956
|
+
]);
|
|
957
|
+
useEffect(() => {
|
|
958
|
+
if (!useV2Runtime || !headlessRef.current) return;
|
|
959
|
+
headlessRef.current.updateConfig({
|
|
960
|
+
plugins: pluginHostRef.current ?? normalizedConfig.plugins
|
|
961
|
+
});
|
|
962
|
+
}, [useV2Runtime, pluginHost]);
|
|
879
963
|
useEffect(() => {
|
|
880
|
-
|
|
964
|
+
const host = useV2Runtime ? headlessRef.current?.pluginHost ?? null : pluginHost;
|
|
965
|
+
if (!host) return;
|
|
881
966
|
const ctx = buildPluginContext({
|
|
882
967
|
courseId: courseIdRef.current,
|
|
883
968
|
sessionId: sessionIdRef.current,
|
|
884
969
|
attemptId: attemptIdRef.current,
|
|
885
970
|
user: userRef.current
|
|
886
971
|
});
|
|
887
|
-
|
|
972
|
+
host.setupAll(ctx);
|
|
888
973
|
return () => {
|
|
889
|
-
|
|
974
|
+
host.disposeAll();
|
|
890
975
|
};
|
|
891
|
-
}, [pluginHost, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
|
|
976
|
+
}, [pluginHost, useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
|
|
892
977
|
useEffect(() => {
|
|
893
978
|
const nextConfigured = normalizedConfig.session?.sessionId;
|
|
894
979
|
const prevConfigured = prevConfiguredSessionIdRef.current;
|
|
@@ -917,6 +1002,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
917
1002
|
config: normalizedConfig,
|
|
918
1003
|
tracking,
|
|
919
1004
|
xapi,
|
|
1005
|
+
storage: defaultStorage,
|
|
920
1006
|
session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
|
|
921
1007
|
progress,
|
|
922
1008
|
setActiveLesson,
|
|
@@ -1051,465 +1137,849 @@ function getLessonMountCount(lessonId) {
|
|
|
1051
1137
|
return mountCounts.get(lessonId) ?? 0;
|
|
1052
1138
|
}
|
|
1053
1139
|
|
|
1054
|
-
// src/components.tsx
|
|
1055
|
-
import {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
[props.config, courseId]
|
|
1065
|
-
);
|
|
1066
|
-
return /* @__PURE__ */ jsx2(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs("section", { "aria-label": props.title, children: [
|
|
1067
|
-
/* @__PURE__ */ jsx2("h1", { children: props.title }),
|
|
1068
|
-
/* @__PURE__ */ jsx2("div", { children: props.children })
|
|
1069
|
-
] }) });
|
|
1070
|
-
}
|
|
1071
|
-
function Lesson(props) {
|
|
1072
|
-
const lessonId = useMemo4(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
1073
|
-
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
1074
|
-
const { setActiveLesson, config } = useLessonkit();
|
|
1075
|
-
const { completeLesson } = useCompletion();
|
|
1076
|
-
const lessonMountGenerationRef = useRef2(0);
|
|
1077
|
-
const liveCourseIdRef = useRef2(config.courseId);
|
|
1078
|
-
liveCourseIdRef.current = config.courseId;
|
|
1079
|
-
useEffect2(() => {
|
|
1080
|
-
const unregister = registerLessonMount(lessonId);
|
|
1081
|
-
const generation = ++lessonMountGenerationRef.current;
|
|
1082
|
-
const mountedCourseId = config.courseId;
|
|
1083
|
-
let effectSurvivedTick = false;
|
|
1084
|
-
queueMicrotask(() => {
|
|
1085
|
-
queueMicrotask(() => {
|
|
1086
|
-
effectSurvivedTick = true;
|
|
1087
|
-
});
|
|
1088
|
-
});
|
|
1089
|
-
setActiveLesson(lessonId);
|
|
1090
|
-
return () => {
|
|
1091
|
-
unregister();
|
|
1092
|
-
if (getLessonMountCount(lessonId) > 0) {
|
|
1093
|
-
return;
|
|
1094
|
-
}
|
|
1095
|
-
if (!autoComplete) return;
|
|
1096
|
-
queueMicrotask(() => {
|
|
1097
|
-
if (!effectSurvivedTick) return;
|
|
1098
|
-
if (lessonMountGenerationRef.current !== generation) return;
|
|
1099
|
-
if (liveCourseIdRef.current !== mountedCourseId) return;
|
|
1100
|
-
completeLesson(lessonId, { courseId: mountedCourseId });
|
|
1101
|
-
});
|
|
1102
|
-
};
|
|
1103
|
-
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
1104
|
-
return /* @__PURE__ */ jsx2(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs("article", { "aria-label": props.title, children: [
|
|
1105
|
-
/* @__PURE__ */ jsx2("h2", { children: props.title }),
|
|
1106
|
-
/* @__PURE__ */ jsx2("div", { children: props.children })
|
|
1107
|
-
] }) });
|
|
1108
|
-
}
|
|
1109
|
-
function Scenario(props) {
|
|
1110
|
-
const blockId = useMemo4(
|
|
1111
|
-
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1112
|
-
[props.blockId]
|
|
1113
|
-
);
|
|
1114
|
-
return /* @__PURE__ */ jsx2("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
1115
|
-
}
|
|
1116
|
-
function Reflection(props) {
|
|
1117
|
-
const blockId = useMemo4(
|
|
1118
|
-
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1119
|
-
[props.blockId]
|
|
1120
|
-
);
|
|
1121
|
-
const promptId = useId();
|
|
1122
|
-
const hintId = useId();
|
|
1123
|
-
const [internalValue, setInternalValue] = useState2("");
|
|
1124
|
-
const isControlled = props.value !== void 0;
|
|
1125
|
-
const value = isControlled ? props.value : internalValue;
|
|
1126
|
-
const handleChange = (event) => {
|
|
1127
|
-
if (!isControlled) setInternalValue(event.target.value);
|
|
1128
|
-
props.onChange?.(event.target.value);
|
|
1129
|
-
};
|
|
1130
|
-
return /* @__PURE__ */ jsxs("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
|
|
1131
|
-
props.prompt ? /* @__PURE__ */ jsx2("p", { id: promptId, children: props.prompt }) : null,
|
|
1132
|
-
props.hint ? /* @__PURE__ */ jsx2("p", { id: hintId, style: visuallyHiddenStyle, children: props.hint }) : null,
|
|
1133
|
-
props.children,
|
|
1134
|
-
/* @__PURE__ */ jsx2(
|
|
1135
|
-
"textarea",
|
|
1136
|
-
{
|
|
1137
|
-
value,
|
|
1138
|
-
onChange: handleChange,
|
|
1139
|
-
"aria-labelledby": props.prompt ? promptId : void 0,
|
|
1140
|
-
"aria-describedby": props.hint ? hintId : void 0,
|
|
1141
|
-
"aria-label": props.prompt ? void 0 : "Reflection response"
|
|
1142
|
-
}
|
|
1143
|
-
)
|
|
1144
|
-
] });
|
|
1145
|
-
}
|
|
1146
|
-
function KnowledgeCheck(props) {
|
|
1147
|
-
return /* @__PURE__ */ jsx2(
|
|
1148
|
-
Quiz,
|
|
1149
|
-
{
|
|
1150
|
-
checkId: props.checkId,
|
|
1151
|
-
question: props.question,
|
|
1152
|
-
choices: props.choices,
|
|
1153
|
-
answer: props.answer,
|
|
1154
|
-
passingScore: props.passingScore
|
|
1155
|
-
}
|
|
1156
|
-
);
|
|
1140
|
+
// src/components/Quiz.tsx
|
|
1141
|
+
import { forwardRef, useEffect as useEffect3, useId, useMemo as useMemo5, useRef as useRef4, useState as useState3 } from "react";
|
|
1142
|
+
import { visuallyHiddenStyle } from "@lessonkit/accessibility";
|
|
1143
|
+
|
|
1144
|
+
// src/assessment/AssessmentLessonGuard.tsx
|
|
1145
|
+
import { useEffect as useEffect2 } from "react";
|
|
1146
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
1147
|
+
var warnedAssessmentOutsideLesson = false;
|
|
1148
|
+
function resetAssessmentWarningsForTests() {
|
|
1149
|
+
warnedAssessmentOutsideLesson = false;
|
|
1157
1150
|
}
|
|
1158
|
-
function
|
|
1151
|
+
function AssessmentLessonGuard(props) {
|
|
1159
1152
|
const enclosingLessonId = useEnclosingLessonId();
|
|
1160
1153
|
const missingLesson = enclosingLessonId === void 0;
|
|
1161
1154
|
useEffect2(() => {
|
|
1162
1155
|
if (!missingLesson || isDevEnvironment4()) return;
|
|
1163
|
-
if (!
|
|
1164
|
-
|
|
1156
|
+
if (!warnedAssessmentOutsideLesson) {
|
|
1157
|
+
warnedAssessmentOutsideLesson = true;
|
|
1165
1158
|
console.error(
|
|
1166
|
-
|
|
1159
|
+
`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>; assessment telemetry will not be emitted.`
|
|
1167
1160
|
);
|
|
1168
1161
|
}
|
|
1169
|
-
}, [missingLesson]);
|
|
1162
|
+
}, [missingLesson, props.blockLabel]);
|
|
1170
1163
|
if (missingLesson && isDevEnvironment4()) {
|
|
1171
|
-
throw new Error(
|
|
1164
|
+
throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
|
|
1172
1165
|
}
|
|
1173
1166
|
if (missingLesson) {
|
|
1174
|
-
return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label":
|
|
1167
|
+
return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label": `${props.blockLabel} configuration error`, "data-lk-check-id": props.checkId, children: /* @__PURE__ */ jsxs("p", { children: [
|
|
1168
|
+
props.blockLabel,
|
|
1169
|
+
" must be placed inside a Lesson."
|
|
1170
|
+
] }) });
|
|
1175
1171
|
}
|
|
1176
|
-
return /* @__PURE__ */ jsx2(
|
|
1172
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: props.children(enclosingLessonId) });
|
|
1177
1173
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
completedRef.current = false;
|
|
1191
|
-
setQuizPassed(false);
|
|
1192
|
-
setSelected(null);
|
|
1193
|
-
setSelectionCorrect(null);
|
|
1194
|
-
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId, choicesKey]);
|
|
1195
|
-
const isChoiceCorrect = (choice, custom) => {
|
|
1196
|
-
if (!custom) return choice === props.answer;
|
|
1197
|
-
if (custom.passed !== void 0) return custom.passed;
|
|
1198
|
-
if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
1199
|
-
return meetsPassingThreshold(custom.score, custom.maxScore, props.passingScore);
|
|
1200
|
-
}
|
|
1201
|
-
return choice === props.answer;
|
|
1174
|
+
|
|
1175
|
+
// src/assessment/internal/buildAssessmentHandle.ts
|
|
1176
|
+
function buildAssessmentHandle(opts) {
|
|
1177
|
+
return {
|
|
1178
|
+
getScore: opts.getScore,
|
|
1179
|
+
getMaxScore: opts.getMaxScore,
|
|
1180
|
+
getAnswerGiven: opts.getAnswerGiven,
|
|
1181
|
+
resetTask: opts.resetTask,
|
|
1182
|
+
showSolutions: opts.showSolutions,
|
|
1183
|
+
getXAPIData: opts.getXAPIData,
|
|
1184
|
+
...opts.getCurrentState ? { getCurrentState: opts.getCurrentState } : {},
|
|
1185
|
+
...opts.resume ? { resume: opts.resume } : {}
|
|
1202
1186
|
};
|
|
1203
|
-
const passed = quizPassed;
|
|
1204
|
-
return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
1205
|
-
/* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
|
|
1206
|
-
/* @__PURE__ */ jsxs("fieldset", { "aria-labelledby": questionId, children: [
|
|
1207
|
-
/* @__PURE__ */ jsx2("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
|
|
1208
|
-
props.choices.map((c, i) => /* @__PURE__ */ jsxs("label", { style: { display: "block" }, children: [
|
|
1209
|
-
/* @__PURE__ */ jsx2(
|
|
1210
|
-
"input",
|
|
1211
|
-
{
|
|
1212
|
-
type: "radio",
|
|
1213
|
-
name: questionId,
|
|
1214
|
-
value: c,
|
|
1215
|
-
checked: selected === c,
|
|
1216
|
-
disabled: passed,
|
|
1217
|
-
"aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
|
|
1218
|
-
onChange: () => {
|
|
1219
|
-
if (passed) return;
|
|
1220
|
-
setSelected(c);
|
|
1221
|
-
const pluginCtx = buildPluginContext({
|
|
1222
|
-
courseId: config.courseId,
|
|
1223
|
-
sessionId: session.sessionId,
|
|
1224
|
-
attemptId: session.attemptId,
|
|
1225
|
-
user: session.user
|
|
1226
|
-
});
|
|
1227
|
-
const custom = plugins?.scoreAssessment(
|
|
1228
|
-
{
|
|
1229
|
-
checkId,
|
|
1230
|
-
lessonId: enclosingLessonId,
|
|
1231
|
-
response: c
|
|
1232
|
-
},
|
|
1233
|
-
pluginCtx
|
|
1234
|
-
) ?? null;
|
|
1235
|
-
const correct = isChoiceCorrect(c, custom);
|
|
1236
|
-
setSelectionCorrect(correct);
|
|
1237
|
-
quiz.answer({
|
|
1238
|
-
checkId,
|
|
1239
|
-
question: props.question,
|
|
1240
|
-
choice: c,
|
|
1241
|
-
correct
|
|
1242
|
-
});
|
|
1243
|
-
if (correct && !completedRef.current) {
|
|
1244
|
-
completedRef.current = true;
|
|
1245
|
-
setQuizPassed(true);
|
|
1246
|
-
const maxScore = custom?.maxScore ?? 1;
|
|
1247
|
-
quiz.complete({
|
|
1248
|
-
checkId,
|
|
1249
|
-
score: custom?.score ?? maxScore,
|
|
1250
|
-
maxScore,
|
|
1251
|
-
passingScore: props.passingScore ?? maxScore
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
),
|
|
1257
|
-
c
|
|
1258
|
-
] }, `${questionId}-${i}`))
|
|
1259
|
-
] }),
|
|
1260
|
-
selected && selectionCorrect !== null ? /* @__PURE__ */ jsx2("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
1261
|
-
] });
|
|
1262
1187
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
) });
|
|
1285
|
-
}
|
|
1286
|
-
return /* @__PURE__ */ jsx2("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs("p", { children: [
|
|
1287
|
-
"Lessons completed: ",
|
|
1288
|
-
completed
|
|
1289
|
-
] }) });
|
|
1188
|
+
|
|
1189
|
+
// src/assessment/internal/resumeState.ts
|
|
1190
|
+
function readBooleanField(state, key) {
|
|
1191
|
+
const value = state[key];
|
|
1192
|
+
if (value === true || value === false || value === null) return value;
|
|
1193
|
+
return void 0;
|
|
1194
|
+
}
|
|
1195
|
+
function readStringField(state, key) {
|
|
1196
|
+
const value = state[key];
|
|
1197
|
+
if (typeof value === "string" || value === null) return value;
|
|
1198
|
+
return void 0;
|
|
1199
|
+
}
|
|
1200
|
+
function readNumberField(state, key) {
|
|
1201
|
+
const value = state[key];
|
|
1202
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1203
|
+
if (value === null) return null;
|
|
1204
|
+
return void 0;
|
|
1205
|
+
}
|
|
1206
|
+
function readBooleanStateField(state, key, apply) {
|
|
1207
|
+
const value = state[key];
|
|
1208
|
+
if (typeof value === "boolean") apply(value);
|
|
1290
1209
|
}
|
|
1291
1210
|
|
|
1292
|
-
// src/
|
|
1293
|
-
import
|
|
1211
|
+
// src/assessment/internal/useAssessmentHandleRegistration.ts
|
|
1212
|
+
import { useImperativeHandle as useImperativeHandle2 } from "react";
|
|
1294
1213
|
|
|
1295
|
-
// src/
|
|
1296
|
-
import {
|
|
1297
|
-
import {
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
);
|
|
1312
|
-
}
|
|
1313
|
-
}, [missingLesson, props.blockLabel]);
|
|
1314
|
-
if (missingLesson && isDevEnvironment4()) {
|
|
1315
|
-
throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
|
|
1316
|
-
}
|
|
1317
|
-
if (missingLesson) {
|
|
1318
|
-
return /* @__PURE__ */ jsx3("section", { role: "alert", "aria-label": `${props.blockLabel} configuration error`, "data-lk-check-id": props.checkId, children: /* @__PURE__ */ jsxs2("p", { children: [
|
|
1319
|
-
props.blockLabel,
|
|
1320
|
-
" must be placed inside a Lesson."
|
|
1321
|
-
] }) });
|
|
1214
|
+
// src/compound/CompoundProvider.tsx
|
|
1215
|
+
import React5, { createContext as createContext5, useCallback as useCallback2, useContext as useContext5, useImperativeHandle, useMemo as useMemo4, useRef as useRef3, useState as useState2 } from "react";
|
|
1216
|
+
import { clampCompoundPageIndex, createCompoundResumeState } from "@lessonkit/core";
|
|
1217
|
+
|
|
1218
|
+
// src/compound/aggregateScores.ts
|
|
1219
|
+
function aggregateAssessmentScores(handles, opts) {
|
|
1220
|
+
let score = 0;
|
|
1221
|
+
let maxScore = 0;
|
|
1222
|
+
let allAnswered = true;
|
|
1223
|
+
for (const entry of handles) {
|
|
1224
|
+
const handle = "handle" in entry ? entry.handle : entry;
|
|
1225
|
+
const pageIndex = "handle" in entry ? entry.pageIndex : void 0;
|
|
1226
|
+
score += handle.getScore();
|
|
1227
|
+
maxScore += handle.getMaxScore();
|
|
1228
|
+
const countsForAnswerGiven = opts?.answerPageIndex === void 0 || pageIndex === void 0 || pageIndex === opts.answerPageIndex;
|
|
1229
|
+
if (countsForAnswerGiven && !handle.getAnswerGiven()) allAnswered = false;
|
|
1322
1230
|
}
|
|
1323
|
-
return
|
|
1231
|
+
return { score, maxScore, allAnswered };
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/compound/CompoundHydrationBridge.tsx
|
|
1235
|
+
import { createContext as createContext3, useContext as useContext3, useRef as useRef2 } from "react";
|
|
1236
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
1237
|
+
var CompoundHydrationBridgeContext = createContext3(
|
|
1238
|
+
null
|
|
1239
|
+
);
|
|
1240
|
+
function CompoundHydrationBridgeProvider({ children }) {
|
|
1241
|
+
const bridgeRef = useRef2(null);
|
|
1242
|
+
return /* @__PURE__ */ jsx3(CompoundHydrationBridgeContext.Provider, { value: bridgeRef, children });
|
|
1243
|
+
}
|
|
1244
|
+
function useCompoundHydrationBridgeRef() {
|
|
1245
|
+
return useContext3(CompoundHydrationBridgeContext);
|
|
1324
1246
|
}
|
|
1325
1247
|
|
|
1326
|
-
// src/
|
|
1327
|
-
import
|
|
1248
|
+
// src/compound/CompoundPageIndexContext.tsx
|
|
1249
|
+
import { createContext as createContext4, useContext as useContext4 } from "react";
|
|
1328
1250
|
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1329
|
-
var
|
|
1330
|
-
function
|
|
1251
|
+
var CompoundPageIndexContext = createContext4(void 0);
|
|
1252
|
+
function CompoundPageIndexProvider({
|
|
1253
|
+
pageIndex,
|
|
1254
|
+
children
|
|
1255
|
+
}) {
|
|
1256
|
+
return /* @__PURE__ */ jsx4(CompoundPageIndexContext.Provider, { value: pageIndex, children });
|
|
1257
|
+
}
|
|
1258
|
+
function useCompoundPageIndex() {
|
|
1259
|
+
return useContext4(CompoundPageIndexContext);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// src/compound/CompoundProvider.tsx
|
|
1263
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1264
|
+
var CompoundRegistryContext = createContext5(null);
|
|
1265
|
+
var CompoundHandlesVersionContext = createContext5(0);
|
|
1266
|
+
function CompoundProvider({
|
|
1267
|
+
children,
|
|
1268
|
+
activePageIndex: _activePageIndex,
|
|
1269
|
+
onActivePageIndexChange: _onActivePageIndexChange
|
|
1270
|
+
}) {
|
|
1331
1271
|
const registryRef = useRef3(/* @__PURE__ */ new Map());
|
|
1332
|
-
const
|
|
1333
|
-
|
|
1272
|
+
const [handlesVersion, setHandlesVersion] = useState2(0);
|
|
1273
|
+
const register = useCallback2((checkId, handle, pageIndex) => {
|
|
1274
|
+
const prev = registryRef.current.get(checkId);
|
|
1275
|
+
if (prev && prev.handle !== handle) {
|
|
1276
|
+
const message = `[lessonkit] duplicate checkId "${checkId}" registered in the same compound container; the previous handle was replaced.`;
|
|
1277
|
+
if (isDevEnvironment4()) {
|
|
1278
|
+
console.error(message);
|
|
1279
|
+
} else {
|
|
1280
|
+
console.warn(message);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
registryRef.current.set(checkId, { handle, pageIndex });
|
|
1284
|
+
if (prev?.handle !== handle || prev?.pageIndex !== pageIndex) {
|
|
1285
|
+
setHandlesVersion((v) => v + 1);
|
|
1286
|
+
}
|
|
1334
1287
|
return () => {
|
|
1335
|
-
registryRef.current.
|
|
1288
|
+
const current = registryRef.current.get(checkId);
|
|
1289
|
+
if (current?.handle === handle) {
|
|
1290
|
+
registryRef.current.delete(checkId);
|
|
1291
|
+
setHandlesVersion((v) => v + 1);
|
|
1292
|
+
}
|
|
1336
1293
|
};
|
|
1337
1294
|
}, []);
|
|
1338
|
-
const
|
|
1295
|
+
const registryValue = useMemo4(
|
|
1339
1296
|
() => ({
|
|
1340
1297
|
register,
|
|
1341
|
-
getHandles: () =>
|
|
1298
|
+
getHandles: () => {
|
|
1299
|
+
const handles = /* @__PURE__ */ new Map();
|
|
1300
|
+
for (const [checkId, entry] of registryRef.current) {
|
|
1301
|
+
handles.set(checkId, entry.handle);
|
|
1302
|
+
}
|
|
1303
|
+
return handles;
|
|
1304
|
+
},
|
|
1305
|
+
getRegisteredHandles: () => registryRef.current
|
|
1342
1306
|
}),
|
|
1343
1307
|
[register]
|
|
1344
1308
|
);
|
|
1345
|
-
return /* @__PURE__ */
|
|
1309
|
+
return /* @__PURE__ */ jsx5(CompoundHydrationBridgeProvider, { children: /* @__PURE__ */ jsx5(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ jsx5(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) }) });
|
|
1346
1310
|
}
|
|
1347
|
-
function
|
|
1348
|
-
|
|
1311
|
+
function useCompoundRegistry() {
|
|
1312
|
+
const registry = useContext5(CompoundRegistryContext);
|
|
1313
|
+
const handlesVersion = useContext5(CompoundHandlesVersionContext);
|
|
1314
|
+
if (!registry) return null;
|
|
1315
|
+
return { ...registry, handlesVersion };
|
|
1316
|
+
}
|
|
1317
|
+
function useCompoundHandlesVersion() {
|
|
1318
|
+
return useContext5(CompoundHandlesVersionContext);
|
|
1349
1319
|
}
|
|
1350
1320
|
function useRegisterAssessmentHandle(checkId, handle) {
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1321
|
+
const registry = useContext5(CompoundRegistryContext);
|
|
1322
|
+
const pageIndex = useCompoundPageIndex();
|
|
1323
|
+
React5.useLayoutEffect(() => {
|
|
1324
|
+
if (!registry || !handle) return;
|
|
1325
|
+
return registry.register(checkId, handle, pageIndex);
|
|
1326
|
+
}, [registry, checkId, handle, pageIndex]);
|
|
1327
|
+
}
|
|
1328
|
+
function useCompoundHandleRef(ref, opts) {
|
|
1329
|
+
const { activePageIndex, setActivePageIndex, getHandles, getRegisteredHandles, pageCount } = opts;
|
|
1330
|
+
const bridgeRef = useCompoundHydrationBridgeRef();
|
|
1331
|
+
const setIndexClamped = useCallback2(
|
|
1332
|
+
(index) => {
|
|
1333
|
+
const next = pageCount !== void 0 ? clampCompoundPageIndex(index, pageCount) : Math.max(0, Math.floor(index));
|
|
1334
|
+
setActivePageIndex(next);
|
|
1335
|
+
},
|
|
1336
|
+
[pageCount, setActivePageIndex]
|
|
1337
|
+
);
|
|
1338
|
+
useImperativeHandle(
|
|
1339
|
+
ref,
|
|
1340
|
+
() => ({
|
|
1341
|
+
getScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).score,
|
|
1342
|
+
getMaxScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).maxScore,
|
|
1343
|
+
getAnswerGiven: () => aggregateAssessmentScores(getRegisteredHandles().values(), {
|
|
1344
|
+
answerPageIndex: activePageIndex
|
|
1345
|
+
}).allAnswered,
|
|
1346
|
+
resetTask: () => {
|
|
1347
|
+
for (const entry of getRegisteredHandles().values()) entry.handle.resetTask();
|
|
1348
|
+
},
|
|
1349
|
+
showSolutions: () => {
|
|
1350
|
+
if (!opts.enableSolutionsButton) return;
|
|
1351
|
+
for (const entry of getRegisteredHandles().values()) entry.handle.showSolutions();
|
|
1352
|
+
},
|
|
1353
|
+
getCurrentState: () => {
|
|
1354
|
+
const childStates = {};
|
|
1355
|
+
for (const [checkId, entry] of getRegisteredHandles()) {
|
|
1356
|
+
if (entry.handle.getCurrentState) {
|
|
1357
|
+
childStates[checkId] = entry.handle.getCurrentState();
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return createCompoundResumeState({ activePageIndex, childStates });
|
|
1361
|
+
},
|
|
1362
|
+
resume: (state) => {
|
|
1363
|
+
bridgeRef?.current?.notifyImperativeResume(state);
|
|
1364
|
+
}
|
|
1365
|
+
}),
|
|
1366
|
+
[activePageIndex, setIndexClamped, getHandles, getRegisteredHandles, opts.enableSolutionsButton, bridgeRef]
|
|
1367
|
+
);
|
|
1356
1368
|
}
|
|
1357
1369
|
|
|
1358
|
-
// src/
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1370
|
+
// src/assessment/internal/useAssessmentHandleRegistration.ts
|
|
1371
|
+
function useAssessmentHandleRegistration(checkId, handle, ref) {
|
|
1372
|
+
useImperativeHandle2(ref, () => handle, [handle]);
|
|
1373
|
+
useRegisterAssessmentHandle(checkId, handle);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// src/assessment/internal/usePluginScoring.ts
|
|
1377
|
+
import { useCallback as useCallback3 } from "react";
|
|
1378
|
+
|
|
1379
|
+
// src/assessment/scoring.ts
|
|
1380
|
+
function resolvePassingThreshold(passingScore, maxScore) {
|
|
1381
|
+
return passingScore ?? maxScore;
|
|
1382
|
+
}
|
|
1383
|
+
function meetsPassingThreshold(score, maxScore, passingScore) {
|
|
1384
|
+
const threshold = resolvePassingThreshold(passingScore, maxScore);
|
|
1385
|
+
return score >= threshold;
|
|
1386
|
+
}
|
|
1387
|
+
function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
|
|
1388
|
+
const maxScore = custom?.maxScore ?? fallbackMax;
|
|
1389
|
+
if (custom?.passed !== void 0) {
|
|
1390
|
+
const score2 = custom.passed ? custom.score ?? maxScore : custom.score ?? 0;
|
|
1391
|
+
return { score: score2, maxScore, passed: custom.passed };
|
|
1392
|
+
}
|
|
1393
|
+
if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
1394
|
+
const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
|
|
1395
|
+
return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
|
|
1396
|
+
}
|
|
1397
|
+
const score = fallbackCorrect ? maxScore : 0;
|
|
1398
|
+
const passed = meetsPassingThreshold(score, maxScore, passingScore);
|
|
1399
|
+
return { score, maxScore, passed };
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// src/assessment/internal/usePluginScoring.ts
|
|
1403
|
+
function usePluginScoring(checkId, lessonId) {
|
|
1365
1404
|
const { plugins, config, session } = useLessonkit();
|
|
1405
|
+
const getPluginScore = useCallback3(
|
|
1406
|
+
(response) => {
|
|
1407
|
+
const pluginCtx = buildPluginContext({
|
|
1408
|
+
courseId: config.courseId,
|
|
1409
|
+
sessionId: session.sessionId,
|
|
1410
|
+
attemptId: session.attemptId,
|
|
1411
|
+
user: session.user
|
|
1412
|
+
});
|
|
1413
|
+
return plugins?.scoreAssessment({ checkId, lessonId, response }, pluginCtx) ?? null;
|
|
1414
|
+
},
|
|
1415
|
+
[checkId, config.courseId, lessonId, plugins, session.attemptId, session.sessionId, session.user]
|
|
1416
|
+
);
|
|
1417
|
+
const scoreResponse = useCallback3(
|
|
1418
|
+
(response, defaultCorrect, maxScore = 1, passingScore) => scoreFromCustom(getPluginScore(response), defaultCorrect, maxScore, passingScore),
|
|
1419
|
+
[getPluginScore]
|
|
1420
|
+
);
|
|
1421
|
+
const isChoiceCorrect = useCallback3(
|
|
1422
|
+
(choice, answer, custom, passingScore) => {
|
|
1423
|
+
if (!custom) return choice === answer;
|
|
1424
|
+
if (custom.passed !== void 0) return custom.passed;
|
|
1425
|
+
if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
1426
|
+
return meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
|
|
1427
|
+
}
|
|
1428
|
+
return choice === answer;
|
|
1429
|
+
},
|
|
1430
|
+
[]
|
|
1431
|
+
);
|
|
1432
|
+
return { getPluginScore, scoreResponse, isChoiceCorrect };
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// src/components/Quiz.tsx
|
|
1436
|
+
import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1437
|
+
function QuizInner(props, ref) {
|
|
1438
|
+
const { enclosingLessonId } = props;
|
|
1439
|
+
const checkId = useMemo5(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1440
|
+
const quiz = useQuizState(enclosingLessonId);
|
|
1441
|
+
const { getPluginScore, isChoiceCorrect } = usePluginScoring(checkId, enclosingLessonId);
|
|
1366
1442
|
const [selected, setSelected] = useState3(null);
|
|
1367
1443
|
const [selectionCorrect, setSelectionCorrect] = useState3(null);
|
|
1368
|
-
const [
|
|
1369
|
-
const [
|
|
1444
|
+
const [quizPassed, setQuizPassed] = useState3(false);
|
|
1445
|
+
const [completedScore, setCompletedScore] = useState3(null);
|
|
1446
|
+
const [completedMaxScore, setCompletedMaxScore] = useState3(null);
|
|
1370
1447
|
const completedRef = useRef4(false);
|
|
1371
|
-
const
|
|
1372
|
-
const
|
|
1448
|
+
const telemetryReplayedRef = useRef4(false);
|
|
1449
|
+
const questionId = useId();
|
|
1450
|
+
const choicesKey = props.choices.join("\0");
|
|
1451
|
+
useEffect3(() => {
|
|
1373
1452
|
completedRef.current = false;
|
|
1374
|
-
|
|
1453
|
+
telemetryReplayedRef.current = false;
|
|
1454
|
+
setQuizPassed(false);
|
|
1375
1455
|
setSelected(null);
|
|
1376
1456
|
setSelectionCorrect(null);
|
|
1377
|
-
|
|
1457
|
+
setCompletedScore(null);
|
|
1458
|
+
setCompletedMaxScore(null);
|
|
1459
|
+
}, [checkId, props.answer, props.question, choicesKey]);
|
|
1460
|
+
const passed = quizPassed;
|
|
1461
|
+
const resolveScores = () => {
|
|
1462
|
+
const maxScore = completedMaxScore ?? 1;
|
|
1463
|
+
if (quizPassed) {
|
|
1464
|
+
return { score: completedScore ?? maxScore, maxScore };
|
|
1465
|
+
}
|
|
1466
|
+
if (selected !== null && selectionCorrect) {
|
|
1467
|
+
return { score: completedMaxScore ?? maxScore, maxScore };
|
|
1468
|
+
}
|
|
1469
|
+
return { score: 0, maxScore };
|
|
1378
1470
|
};
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
const score = passed ? maxScore : selected === null ? 0 : selected === props.answer ? maxScore : 0;
|
|
1385
|
-
return {
|
|
1386
|
-
getScore: () => score,
|
|
1387
|
-
getMaxScore: () => maxScore,
|
|
1388
|
-
getAnswerGiven: () => selected !== null,
|
|
1389
|
-
resetTask: reset,
|
|
1390
|
-
showSolutions: () => setShowSolutions(true),
|
|
1391
|
-
getXAPIData: () => ({
|
|
1392
|
-
checkId,
|
|
1393
|
-
interactionType: INTERACTION,
|
|
1394
|
-
response: selected ?? void 0,
|
|
1395
|
-
correct: selected === props.answer,
|
|
1396
|
-
score,
|
|
1397
|
-
maxScore
|
|
1398
|
-
})
|
|
1399
|
-
};
|
|
1400
|
-
}, [checkId, passed, props.answer, selected]);
|
|
1401
|
-
useImperativeHandle(ref, () => handle, [handle]);
|
|
1402
|
-
useRegisterAssessmentHandle(checkId, handle);
|
|
1403
|
-
const submit = (value) => {
|
|
1404
|
-
if (passed && !props.enableRetry) return;
|
|
1405
|
-
setSelected(value);
|
|
1406
|
-
const pluginCtx = buildPluginContext({
|
|
1407
|
-
courseId: config.courseId,
|
|
1408
|
-
sessionId: session.sessionId,
|
|
1409
|
-
attemptId: session.attemptId,
|
|
1410
|
-
user: session.user
|
|
1411
|
-
});
|
|
1412
|
-
const custom = plugins?.scoreAssessment(
|
|
1413
|
-
{ checkId, lessonId: enclosingLessonId, response: value },
|
|
1414
|
-
pluginCtx
|
|
1415
|
-
) ?? null;
|
|
1416
|
-
const correct = value === props.answer;
|
|
1417
|
-
const scored = scoreFromCustom(custom, correct, 1, props.passingScore);
|
|
1418
|
-
setSelectionCorrect(scored.passed);
|
|
1419
|
-
assessment.answer({
|
|
1420
|
-
checkId,
|
|
1421
|
-
interactionType: INTERACTION,
|
|
1422
|
-
question: props.question,
|
|
1423
|
-
response: value,
|
|
1424
|
-
correct: scored.passed
|
|
1425
|
-
});
|
|
1426
|
-
if (scored.passed && !completedRef.current) {
|
|
1427
|
-
completedRef.current = true;
|
|
1428
|
-
setPassed(true);
|
|
1429
|
-
assessment.complete({
|
|
1471
|
+
const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
|
|
1472
|
+
if (!nextPassed || telemetryReplayedRef.current) return;
|
|
1473
|
+
telemetryReplayedRef.current = true;
|
|
1474
|
+
if (nextSelected !== null) {
|
|
1475
|
+
quiz.answer({
|
|
1430
1476
|
checkId,
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
passingScore: props.passingScore ?? scored.maxScore
|
|
1477
|
+
question: props.question,
|
|
1478
|
+
choice: nextSelected,
|
|
1479
|
+
correct: nextCorrect ?? false
|
|
1435
1480
|
});
|
|
1436
1481
|
}
|
|
1482
|
+
quiz.complete({
|
|
1483
|
+
checkId,
|
|
1484
|
+
score: nextScore,
|
|
1485
|
+
maxScore: nextMaxScore,
|
|
1486
|
+
passingScore: props.passingScore ?? nextMaxScore
|
|
1487
|
+
});
|
|
1437
1488
|
};
|
|
1438
|
-
const
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1489
|
+
const handle = useMemo5(
|
|
1490
|
+
() => buildAssessmentHandle({
|
|
1491
|
+
checkId,
|
|
1492
|
+
getScore: () => resolveScores().score,
|
|
1493
|
+
getMaxScore: () => resolveScores().maxScore,
|
|
1494
|
+
getAnswerGiven: () => selected !== null,
|
|
1495
|
+
resetTask: () => {
|
|
1496
|
+
completedRef.current = false;
|
|
1497
|
+
telemetryReplayedRef.current = false;
|
|
1498
|
+
setQuizPassed(false);
|
|
1499
|
+
setSelected(null);
|
|
1500
|
+
setSelectionCorrect(null);
|
|
1501
|
+
setCompletedScore(null);
|
|
1502
|
+
setCompletedMaxScore(null);
|
|
1503
|
+
},
|
|
1504
|
+
showSolutions: () => {
|
|
1505
|
+
},
|
|
1506
|
+
getXAPIData: () => {
|
|
1507
|
+
const { score, maxScore } = resolveScores();
|
|
1508
|
+
return {
|
|
1509
|
+
checkId,
|
|
1510
|
+
interactionType: "mcq",
|
|
1511
|
+
response: selected ?? void 0,
|
|
1512
|
+
correct: selectionCorrect ?? void 0,
|
|
1513
|
+
score,
|
|
1514
|
+
maxScore
|
|
1515
|
+
};
|
|
1516
|
+
},
|
|
1517
|
+
getCurrentState: () => ({
|
|
1518
|
+
selected,
|
|
1519
|
+
selectionCorrect,
|
|
1520
|
+
quizPassed,
|
|
1521
|
+
completedScore,
|
|
1522
|
+
completedMaxScore
|
|
1523
|
+
}),
|
|
1524
|
+
resume: (state) => {
|
|
1525
|
+
const nextSelected = readStringField(state, "selected");
|
|
1526
|
+
if (typeof nextSelected === "string" || nextSelected === null) setSelected(nextSelected);
|
|
1527
|
+
const nextCorrect = readBooleanField(state, "selectionCorrect");
|
|
1528
|
+
if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
|
|
1529
|
+
setSelectionCorrect(nextCorrect);
|
|
1530
|
+
}
|
|
1531
|
+
const nextCompletedScore = readNumberField(state, "completedScore");
|
|
1532
|
+
if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
|
|
1533
|
+
const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
|
|
1534
|
+
if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
|
|
1535
|
+
const nextPassed = readBooleanField(state, "quizPassed");
|
|
1536
|
+
if (nextPassed === true || nextPassed === false) {
|
|
1537
|
+
setQuizPassed(nextPassed);
|
|
1538
|
+
completedRef.current = nextPassed;
|
|
1539
|
+
if (nextPassed) {
|
|
1540
|
+
const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
|
|
1541
|
+
const score = nextCompletedScore ?? completedScore ?? maxScore;
|
|
1542
|
+
replayTelemetry(
|
|
1543
|
+
nextSelected ?? null,
|
|
1544
|
+
nextCorrect ?? null,
|
|
1545
|
+
nextPassed,
|
|
1546
|
+
score,
|
|
1547
|
+
maxScore
|
|
1548
|
+
);
|
|
1452
1549
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}),
|
|
1553
|
+
[
|
|
1554
|
+
checkId,
|
|
1555
|
+
completedMaxScore,
|
|
1556
|
+
completedScore,
|
|
1557
|
+
props.passingScore,
|
|
1558
|
+
props.question,
|
|
1559
|
+
quiz,
|
|
1560
|
+
quizPassed,
|
|
1561
|
+
selected,
|
|
1562
|
+
selectionCorrect
|
|
1563
|
+
]
|
|
1564
|
+
);
|
|
1565
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1566
|
+
return /* @__PURE__ */ jsxs2("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
1567
|
+
/* @__PURE__ */ jsx6("p", { id: questionId, children: props.question }),
|
|
1568
|
+
/* @__PURE__ */ jsxs2("fieldset", { "aria-labelledby": questionId, children: [
|
|
1569
|
+
/* @__PURE__ */ jsx6("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
|
|
1570
|
+
props.choices.map((c, i) => /* @__PURE__ */ jsxs2("label", { style: { display: "block" }, children: [
|
|
1571
|
+
/* @__PURE__ */ jsx6(
|
|
1458
1572
|
"input",
|
|
1459
1573
|
{
|
|
1460
1574
|
type: "radio",
|
|
1461
|
-
name:
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1575
|
+
name: questionId,
|
|
1576
|
+
value: c,
|
|
1577
|
+
checked: selected === c,
|
|
1578
|
+
disabled: passed,
|
|
1579
|
+
"aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
|
|
1580
|
+
onChange: () => {
|
|
1581
|
+
if (passed) return;
|
|
1582
|
+
setSelected(c);
|
|
1583
|
+
const custom = getPluginScore(c);
|
|
1584
|
+
const correct = isChoiceCorrect(c, props.answer, custom, props.passingScore);
|
|
1585
|
+
setSelectionCorrect(correct);
|
|
1586
|
+
quiz.answer({
|
|
1587
|
+
checkId,
|
|
1588
|
+
question: props.question,
|
|
1589
|
+
choice: c,
|
|
1590
|
+
correct
|
|
1591
|
+
});
|
|
1592
|
+
if (correct && !completedRef.current) {
|
|
1593
|
+
completedRef.current = true;
|
|
1594
|
+
setQuizPassed(true);
|
|
1595
|
+
const maxScore = custom?.maxScore ?? 1;
|
|
1596
|
+
const score = custom?.score ?? maxScore;
|
|
1597
|
+
setCompletedScore(score);
|
|
1598
|
+
setCompletedMaxScore(maxScore);
|
|
1599
|
+
quiz.complete({
|
|
1600
|
+
checkId,
|
|
1601
|
+
score,
|
|
1602
|
+
maxScore,
|
|
1603
|
+
passingScore: props.passingScore ?? maxScore
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1465
1607
|
}
|
|
1466
1608
|
),
|
|
1467
|
-
|
|
1468
|
-
] })
|
|
1609
|
+
c
|
|
1610
|
+
] }, `${questionId}-${i}`))
|
|
1469
1611
|
] }),
|
|
1470
|
-
|
|
1471
|
-
"Correct answer: ",
|
|
1472
|
-
/* @__PURE__ */ jsx5("strong", { children: props.answer ? "True" : "False" })
|
|
1473
|
-
] }) : null,
|
|
1474
|
-
selected !== null && selectionCorrect !== null ? /* @__PURE__ */ jsx5("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
|
|
1475
|
-
props.enableRetry && passed ? /* @__PURE__ */ jsx5("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
1476
|
-
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx5("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1612
|
+
selected && selectionCorrect !== null ? /* @__PURE__ */ jsx6("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
1477
1613
|
] });
|
|
1478
1614
|
}
|
|
1479
|
-
var
|
|
1480
|
-
var
|
|
1481
|
-
return /* @__PURE__ */
|
|
1615
|
+
var QuizInnerForwarded = forwardRef(QuizInner);
|
|
1616
|
+
var Quiz = forwardRef(function Quiz2(props, ref) {
|
|
1617
|
+
return /* @__PURE__ */ jsx6(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx6(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1482
1618
|
});
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1619
|
+
function KnowledgeCheck(props) {
|
|
1620
|
+
return /* @__PURE__ */ jsx6(
|
|
1621
|
+
Quiz,
|
|
1622
|
+
{
|
|
1623
|
+
checkId: props.checkId,
|
|
1624
|
+
question: props.question,
|
|
1625
|
+
choices: props.choices,
|
|
1626
|
+
answer: props.answer,
|
|
1627
|
+
passingScore: props.passingScore
|
|
1628
|
+
}
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
function resetQuizWarningsForTests() {
|
|
1632
|
+
resetAssessmentWarningsForTests();
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/components.tsx
|
|
1636
|
+
import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1637
|
+
function Course(props) {
|
|
1638
|
+
const courseId = useMemo6(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
|
|
1639
|
+
const providerConfig = useMemo6(
|
|
1640
|
+
() => ({ ...props.config, courseId }),
|
|
1641
|
+
[props.config, courseId]
|
|
1642
|
+
);
|
|
1643
|
+
return /* @__PURE__ */ jsx7(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs3("section", { "aria-label": props.title, children: [
|
|
1644
|
+
/* @__PURE__ */ jsx7("h1", { children: props.title }),
|
|
1645
|
+
/* @__PURE__ */ jsx7("div", { children: props.children })
|
|
1646
|
+
] }) });
|
|
1647
|
+
}
|
|
1648
|
+
function Lesson(props) {
|
|
1649
|
+
const lessonId = useMemo6(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
1650
|
+
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
1651
|
+
const { setActiveLesson, config } = useLessonkit();
|
|
1652
|
+
const { completeLesson } = useCompletion();
|
|
1653
|
+
const lessonMountGenerationRef = useRef5(0);
|
|
1654
|
+
const liveCourseIdRef = useRef5(config.courseId);
|
|
1655
|
+
liveCourseIdRef.current = config.courseId;
|
|
1656
|
+
useEffect4(() => {
|
|
1657
|
+
const unregister = registerLessonMount(lessonId);
|
|
1658
|
+
const generation = ++lessonMountGenerationRef.current;
|
|
1659
|
+
const mountedCourseId = config.courseId;
|
|
1660
|
+
let effectSurvivedTick = false;
|
|
1661
|
+
queueMicrotask(() => {
|
|
1662
|
+
queueMicrotask(() => {
|
|
1663
|
+
effectSurvivedTick = true;
|
|
1664
|
+
});
|
|
1665
|
+
});
|
|
1666
|
+
setActiveLesson(lessonId);
|
|
1667
|
+
return () => {
|
|
1668
|
+
unregister();
|
|
1669
|
+
if (getLessonMountCount(lessonId) > 0) {
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
if (!autoComplete) return;
|
|
1673
|
+
queueMicrotask(() => {
|
|
1674
|
+
if (!effectSurvivedTick) return;
|
|
1675
|
+
if (lessonMountGenerationRef.current !== generation) return;
|
|
1676
|
+
if (liveCourseIdRef.current !== mountedCourseId) return;
|
|
1677
|
+
completeLesson(lessonId, { courseId: mountedCourseId });
|
|
1678
|
+
});
|
|
1679
|
+
};
|
|
1680
|
+
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
1681
|
+
return /* @__PURE__ */ jsx7(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs3("article", { "aria-label": props.title, children: [
|
|
1682
|
+
/* @__PURE__ */ jsx7("h2", { children: props.title }),
|
|
1683
|
+
/* @__PURE__ */ jsx7("div", { children: props.children })
|
|
1684
|
+
] }) });
|
|
1685
|
+
}
|
|
1686
|
+
function Scenario(props) {
|
|
1687
|
+
const blockId = useMemo6(
|
|
1688
|
+
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1689
|
+
[props.blockId]
|
|
1690
|
+
);
|
|
1691
|
+
return /* @__PURE__ */ jsx7("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
1692
|
+
}
|
|
1693
|
+
function Reflection(props) {
|
|
1694
|
+
const blockId = useMemo6(
|
|
1695
|
+
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1696
|
+
[props.blockId]
|
|
1697
|
+
);
|
|
1698
|
+
const promptId = useId2();
|
|
1699
|
+
const hintId = useId2();
|
|
1700
|
+
const [internalValue, setInternalValue] = useState4("");
|
|
1701
|
+
const isControlled = props.value !== void 0;
|
|
1702
|
+
const value = isControlled ? props.value : internalValue;
|
|
1703
|
+
const handleChange = (event) => {
|
|
1704
|
+
if (!isControlled) setInternalValue(event.target.value);
|
|
1705
|
+
props.onChange?.(event.target.value);
|
|
1706
|
+
};
|
|
1707
|
+
return /* @__PURE__ */ jsxs3("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
|
|
1708
|
+
props.prompt ? /* @__PURE__ */ jsx7("p", { id: promptId, children: props.prompt }) : null,
|
|
1709
|
+
props.hint ? /* @__PURE__ */ jsx7("p", { id: hintId, style: visuallyHiddenStyle2, children: props.hint }) : null,
|
|
1710
|
+
props.children,
|
|
1711
|
+
/* @__PURE__ */ jsx7(
|
|
1712
|
+
"textarea",
|
|
1713
|
+
{
|
|
1714
|
+
value,
|
|
1715
|
+
onChange: handleChange,
|
|
1716
|
+
"aria-labelledby": props.prompt ? promptId : void 0,
|
|
1717
|
+
"aria-describedby": props.hint ? hintId : void 0,
|
|
1718
|
+
"aria-label": props.prompt ? void 0 : "Reflection response"
|
|
1719
|
+
}
|
|
1720
|
+
)
|
|
1721
|
+
] });
|
|
1722
|
+
}
|
|
1723
|
+
function ProgressTracker(props) {
|
|
1724
|
+
const { progress } = useLessonkit();
|
|
1725
|
+
const completed = progress.completedLessonIds.size;
|
|
1726
|
+
if (props.totalLessons != null) {
|
|
1727
|
+
const total = props.totalLessons;
|
|
1728
|
+
const displayed = Math.min(completed, total);
|
|
1729
|
+
return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx7(
|
|
1730
|
+
"div",
|
|
1731
|
+
{
|
|
1732
|
+
role: "progressbar",
|
|
1733
|
+
"aria-valuemin": 0,
|
|
1734
|
+
"aria-valuemax": total,
|
|
1735
|
+
"aria-valuenow": displayed,
|
|
1736
|
+
"aria-label": "Lessons completed",
|
|
1737
|
+
children: /* @__PURE__ */ jsxs3("p", { children: [
|
|
1738
|
+
"Lessons completed: ",
|
|
1739
|
+
displayed,
|
|
1740
|
+
" of ",
|
|
1741
|
+
total
|
|
1742
|
+
] })
|
|
1743
|
+
}
|
|
1744
|
+
) });
|
|
1745
|
+
}
|
|
1746
|
+
return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs3("p", { children: [
|
|
1747
|
+
"Lessons completed: ",
|
|
1748
|
+
completed
|
|
1749
|
+
] }) });
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// src/blocks/TrueFalse.tsx
|
|
1753
|
+
import React9, { forwardRef as forwardRef2, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef6, useState as useState5 } from "react";
|
|
1754
|
+
import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1755
|
+
var INTERACTION = "trueFalse";
|
|
1756
|
+
function TrueFalseInner(props, ref) {
|
|
1757
|
+
const { enclosingLessonId } = props;
|
|
1758
|
+
const checkId = useMemo7(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1759
|
+
const assessment = useAssessmentState(enclosingLessonId);
|
|
1760
|
+
const { config } = useLessonkit();
|
|
1761
|
+
const { scoreResponse } = usePluginScoring(checkId, enclosingLessonId);
|
|
1762
|
+
const [selected, setSelected] = useState5(null);
|
|
1763
|
+
const [selectionCorrect, setSelectionCorrect] = useState5(null);
|
|
1764
|
+
const [showSolutions, setShowSolutions] = useState5(false);
|
|
1765
|
+
const [passed, setPassed] = useState5(false);
|
|
1766
|
+
const [completedScore, setCompletedScore] = useState5(null);
|
|
1767
|
+
const [completedMaxScore, setCompletedMaxScore] = useState5(null);
|
|
1768
|
+
const completedRef = useRef6(false);
|
|
1769
|
+
const telemetryReplayedRef = useRef6(false);
|
|
1770
|
+
const questionId = React9.useId();
|
|
1771
|
+
const reset = () => {
|
|
1772
|
+
completedRef.current = false;
|
|
1773
|
+
telemetryReplayedRef.current = false;
|
|
1774
|
+
setPassed(false);
|
|
1775
|
+
setSelected(null);
|
|
1776
|
+
setSelectionCorrect(null);
|
|
1777
|
+
setShowSolutions(false);
|
|
1778
|
+
setCompletedScore(null);
|
|
1779
|
+
setCompletedMaxScore(null);
|
|
1780
|
+
};
|
|
1781
|
+
useEffect5(() => {
|
|
1782
|
+
reset();
|
|
1783
|
+
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
|
|
1784
|
+
const resolveScores = () => {
|
|
1785
|
+
const maxScore = completedMaxScore ?? 1;
|
|
1786
|
+
if (passed) {
|
|
1787
|
+
return { score: completedScore ?? maxScore, maxScore };
|
|
1788
|
+
}
|
|
1789
|
+
if (selectionCorrect) {
|
|
1790
|
+
return { score: completedMaxScore ?? maxScore, maxScore };
|
|
1791
|
+
}
|
|
1792
|
+
return { score: 0, maxScore };
|
|
1793
|
+
};
|
|
1794
|
+
const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
|
|
1795
|
+
if (!nextPassed || telemetryReplayedRef.current) return;
|
|
1796
|
+
telemetryReplayedRef.current = true;
|
|
1797
|
+
if (nextSelected !== null) {
|
|
1798
|
+
assessment.answer({
|
|
1799
|
+
checkId,
|
|
1800
|
+
interactionType: INTERACTION,
|
|
1801
|
+
question: props.question,
|
|
1802
|
+
response: nextSelected,
|
|
1803
|
+
correct: nextCorrect ?? false
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
assessment.complete({
|
|
1807
|
+
checkId,
|
|
1808
|
+
interactionType: INTERACTION,
|
|
1809
|
+
score: nextScore,
|
|
1810
|
+
maxScore: nextMaxScore,
|
|
1811
|
+
passingScore: props.passingScore ?? nextMaxScore
|
|
1812
|
+
});
|
|
1813
|
+
};
|
|
1814
|
+
const handle = useMemo7(
|
|
1815
|
+
() => buildAssessmentHandle({
|
|
1816
|
+
checkId,
|
|
1817
|
+
getScore: () => resolveScores().score,
|
|
1818
|
+
getMaxScore: () => resolveScores().maxScore,
|
|
1819
|
+
getAnswerGiven: () => selected !== null,
|
|
1820
|
+
resetTask: reset,
|
|
1821
|
+
showSolutions: () => setShowSolutions(true),
|
|
1822
|
+
getXAPIData: () => {
|
|
1823
|
+
const { score, maxScore } = resolveScores();
|
|
1824
|
+
return {
|
|
1825
|
+
checkId,
|
|
1826
|
+
interactionType: INTERACTION,
|
|
1827
|
+
response: selected ?? void 0,
|
|
1828
|
+
correct: selectionCorrect ?? void 0,
|
|
1829
|
+
score,
|
|
1830
|
+
maxScore
|
|
1831
|
+
};
|
|
1832
|
+
},
|
|
1833
|
+
getCurrentState: () => ({
|
|
1834
|
+
selected,
|
|
1835
|
+
selectionCorrect,
|
|
1836
|
+
passed,
|
|
1837
|
+
showSolutions,
|
|
1838
|
+
completedScore,
|
|
1839
|
+
completedMaxScore
|
|
1840
|
+
}),
|
|
1841
|
+
resume: (state) => {
|
|
1842
|
+
const nextSelected = readBooleanField(state, "selected");
|
|
1843
|
+
if (nextSelected === true || nextSelected === false || nextSelected === null) {
|
|
1844
|
+
setSelected(nextSelected);
|
|
1845
|
+
}
|
|
1846
|
+
const nextCorrect = readBooleanField(state, "selectionCorrect");
|
|
1847
|
+
if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
|
|
1848
|
+
setSelectionCorrect(nextCorrect);
|
|
1849
|
+
}
|
|
1850
|
+
const nextCompletedScore = readNumberField(state, "completedScore");
|
|
1851
|
+
if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
|
|
1852
|
+
const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
|
|
1853
|
+
if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
|
|
1854
|
+
const nextPassed = readBooleanField(state, "passed");
|
|
1855
|
+
if (nextPassed === true || nextPassed === false) {
|
|
1856
|
+
setPassed(nextPassed);
|
|
1857
|
+
completedRef.current = nextPassed;
|
|
1858
|
+
if (nextPassed) {
|
|
1859
|
+
const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
|
|
1860
|
+
const score = nextCompletedScore ?? completedScore ?? maxScore;
|
|
1861
|
+
replayTelemetry(nextSelected ?? null, nextCorrect ?? null, nextPassed, score, maxScore);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
1865
|
+
}
|
|
1866
|
+
}),
|
|
1867
|
+
[
|
|
1868
|
+
assessment,
|
|
1869
|
+
checkId,
|
|
1870
|
+
completedMaxScore,
|
|
1871
|
+
completedScore,
|
|
1872
|
+
passed,
|
|
1873
|
+
props.passingScore,
|
|
1874
|
+
props.question,
|
|
1875
|
+
selected,
|
|
1876
|
+
selectionCorrect,
|
|
1877
|
+
showSolutions
|
|
1878
|
+
]
|
|
1879
|
+
);
|
|
1880
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1881
|
+
const submit = (value) => {
|
|
1882
|
+
if (passed && !props.enableRetry) return;
|
|
1883
|
+
setSelected(value);
|
|
1884
|
+
const correct = value === props.answer;
|
|
1885
|
+
const scored = scoreResponse(value, correct, 1, props.passingScore);
|
|
1886
|
+
setSelectionCorrect(scored.passed);
|
|
1887
|
+
assessment.answer({
|
|
1888
|
+
checkId,
|
|
1889
|
+
interactionType: INTERACTION,
|
|
1890
|
+
question: props.question,
|
|
1891
|
+
response: value,
|
|
1892
|
+
correct: scored.passed
|
|
1893
|
+
});
|
|
1894
|
+
if (scored.passed && !completedRef.current) {
|
|
1895
|
+
completedRef.current = true;
|
|
1896
|
+
setPassed(true);
|
|
1897
|
+
setCompletedScore(scored.score);
|
|
1898
|
+
setCompletedMaxScore(scored.maxScore);
|
|
1899
|
+
assessment.complete({
|
|
1900
|
+
checkId,
|
|
1901
|
+
interactionType: INTERACTION,
|
|
1902
|
+
score: scored.score,
|
|
1903
|
+
maxScore: scored.maxScore,
|
|
1904
|
+
passingScore: props.passingScore ?? scored.maxScore
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
};
|
|
1908
|
+
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
1909
|
+
return /* @__PURE__ */ jsxs4("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
|
|
1910
|
+
/* @__PURE__ */ jsx8("p", { id: questionId, children: props.question }),
|
|
1911
|
+
/* @__PURE__ */ jsxs4("fieldset", { "aria-labelledby": questionId, children: [
|
|
1912
|
+
/* @__PURE__ */ jsx8("legend", { className: "lk-visually-hidden", children: "True or False" }),
|
|
1913
|
+
/* @__PURE__ */ jsxs4("label", { style: { display: "block", marginRight: "1rem" }, children: [
|
|
1914
|
+
/* @__PURE__ */ jsx8(
|
|
1915
|
+
"input",
|
|
1916
|
+
{
|
|
1917
|
+
type: "radio",
|
|
1918
|
+
name: `${questionId}-tf`,
|
|
1919
|
+
checked: selected === true,
|
|
1920
|
+
disabled: passed && !props.enableRetry,
|
|
1921
|
+
onChange: () => submit(true)
|
|
1922
|
+
}
|
|
1923
|
+
),
|
|
1924
|
+
"True"
|
|
1925
|
+
] }),
|
|
1926
|
+
/* @__PURE__ */ jsxs4("label", { style: { display: "block" }, children: [
|
|
1927
|
+
/* @__PURE__ */ jsx8(
|
|
1928
|
+
"input",
|
|
1929
|
+
{
|
|
1930
|
+
type: "radio",
|
|
1931
|
+
name: `${questionId}-tf`,
|
|
1932
|
+
checked: selected === false,
|
|
1933
|
+
disabled: passed && !props.enableRetry,
|
|
1934
|
+
onChange: () => submit(false)
|
|
1935
|
+
}
|
|
1936
|
+
),
|
|
1937
|
+
"False"
|
|
1938
|
+
] })
|
|
1939
|
+
] }),
|
|
1940
|
+
reveal ? /* @__PURE__ */ jsxs4("p", { children: [
|
|
1941
|
+
"Correct answer: ",
|
|
1942
|
+
/* @__PURE__ */ jsx8("strong", { children: props.answer ? "True" : "False" })
|
|
1943
|
+
] }) : null,
|
|
1944
|
+
selected !== null && selectionCorrect !== null ? /* @__PURE__ */ jsx8("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
|
|
1945
|
+
props.enableRetry && passed ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
1946
|
+
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1947
|
+
] });
|
|
1948
|
+
}
|
|
1949
|
+
var TrueFalseInnerForwarded = forwardRef2(TrueFalseInner);
|
|
1950
|
+
var TrueFalse = forwardRef2(function TrueFalse2(props, ref) {
|
|
1951
|
+
return /* @__PURE__ */ jsx8(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx8(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1954
|
+
// src/blocks/MarkTheWords.tsx
|
|
1955
|
+
import React10, { forwardRef as forwardRef3, useEffect as useEffect6, useMemo as useMemo8, useRef as useRef7, useState as useState6 } from "react";
|
|
1956
|
+
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1957
|
+
var INTERACTION2 = "markTheWords";
|
|
1958
|
+
function tokenize(text) {
|
|
1489
1959
|
return text.split(/(\s+)/).filter((t) => t.length > 0);
|
|
1490
1960
|
}
|
|
1491
1961
|
function MarkTheWordsInner(props, ref) {
|
|
1492
|
-
const checkId =
|
|
1962
|
+
const checkId = useMemo8(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1493
1963
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1494
|
-
const tokens =
|
|
1495
|
-
const correctSet =
|
|
1964
|
+
const tokens = useMemo8(() => tokenize(props.text), [props.text]);
|
|
1965
|
+
const correctSet = useMemo8(
|
|
1496
1966
|
() => new Set(props.correctWords.map((w) => w.toLowerCase())),
|
|
1497
1967
|
[props.correctWords]
|
|
1498
1968
|
);
|
|
1499
|
-
const [marked, setMarked] =
|
|
1500
|
-
const [passed, setPassed] =
|
|
1501
|
-
const [showSolutions, setShowSolutions] =
|
|
1502
|
-
const completedRef =
|
|
1969
|
+
const [marked, setMarked] = useState6(() => /* @__PURE__ */ new Set());
|
|
1970
|
+
const [passed, setPassed] = useState6(false);
|
|
1971
|
+
const [showSolutions, setShowSolutions] = useState6(false);
|
|
1972
|
+
const completedRef = useRef7(false);
|
|
1503
1973
|
const reset = () => {
|
|
1504
1974
|
completedRef.current = false;
|
|
1505
1975
|
setPassed(false);
|
|
1506
1976
|
setMarked(/* @__PURE__ */ new Set());
|
|
1507
1977
|
setShowSolutions(false);
|
|
1508
1978
|
};
|
|
1509
|
-
|
|
1979
|
+
useEffect6(() => {
|
|
1510
1980
|
reset();
|
|
1511
1981
|
}, [checkId, props.text, props.correctWords.join("\0")]);
|
|
1512
|
-
const selectableIndices =
|
|
1982
|
+
const selectableIndices = useMemo8(() => {
|
|
1513
1983
|
const indices = [];
|
|
1514
1984
|
tokens.forEach((t, i) => {
|
|
1515
1985
|
if (!/^\s+$/.test(t) && correctSet.has(t.toLowerCase())) indices.push(i);
|
|
@@ -1521,11 +1991,11 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1521
1991
|
const maxScore = selectableIndices.length;
|
|
1522
1992
|
const score = allMarked ? maxScore : marked.size;
|
|
1523
1993
|
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
1524
|
-
const handle =
|
|
1525
|
-
|
|
1526
|
-
|
|
1994
|
+
const handle = useMemo8(
|
|
1995
|
+
() => buildAssessmentHandle({
|
|
1996
|
+
checkId,
|
|
1527
1997
|
getScore: () => score,
|
|
1528
|
-
getMaxScore: () =>
|
|
1998
|
+
getMaxScore: () => maxScore || 1,
|
|
1529
1999
|
getAnswerGiven: () => marked.size > 0,
|
|
1530
2000
|
resetTask: reset,
|
|
1531
2001
|
showSolutions: () => setShowSolutions(true),
|
|
@@ -1535,12 +2005,22 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1535
2005
|
response: [...marked].map((i) => tokens[i]),
|
|
1536
2006
|
correct: passedThreshold,
|
|
1537
2007
|
score,
|
|
1538
|
-
maxScore:
|
|
1539
|
-
})
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
2008
|
+
maxScore: maxScore || 1
|
|
2009
|
+
}),
|
|
2010
|
+
getCurrentState: () => ({ marked: [...marked], passed, showSolutions }),
|
|
2011
|
+
resume: (state) => {
|
|
2012
|
+
const raw = state.marked;
|
|
2013
|
+
if (Array.isArray(raw)) setMarked(new Set(raw.filter((i) => typeof i === "number")));
|
|
2014
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
2015
|
+
setPassed(value);
|
|
2016
|
+
completedRef.current = value;
|
|
2017
|
+
});
|
|
2018
|
+
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
2019
|
+
}
|
|
2020
|
+
}),
|
|
2021
|
+
[checkId, marked, maxScore, passed, passedThreshold, score, showSolutions, tokens]
|
|
2022
|
+
);
|
|
2023
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1544
2024
|
const toggle = (index) => {
|
|
1545
2025
|
if (passed && !props.enableRetry) return;
|
|
1546
2026
|
setMarked((prev) => {
|
|
@@ -1550,7 +2030,7 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1550
2030
|
return next;
|
|
1551
2031
|
});
|
|
1552
2032
|
};
|
|
1553
|
-
|
|
2033
|
+
useEffect6(() => {
|
|
1554
2034
|
if (!hasTargets) {
|
|
1555
2035
|
if (isDevEnvironment4()) {
|
|
1556
2036
|
console.warn(
|
|
@@ -1568,7 +2048,7 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1568
2048
|
interactionType: INTERACTION2,
|
|
1569
2049
|
question: props.text,
|
|
1570
2050
|
response: [...marked].map((i) => tokens[i]),
|
|
1571
|
-
correct:
|
|
2051
|
+
correct: passedThreshold
|
|
1572
2052
|
});
|
|
1573
2053
|
assessment.complete({
|
|
1574
2054
|
checkId,
|
|
@@ -1590,20 +2070,20 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1590
2070
|
score,
|
|
1591
2071
|
tokens
|
|
1592
2072
|
]);
|
|
1593
|
-
return /* @__PURE__ */
|
|
1594
|
-
!hasTargets ? /* @__PURE__ */
|
|
2073
|
+
return /* @__PURE__ */ jsxs5("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
|
|
2074
|
+
!hasTargets ? /* @__PURE__ */ jsxs5("p", { role: "alert", children: [
|
|
1595
2075
|
"No words in this sentence match ",
|
|
1596
|
-
/* @__PURE__ */
|
|
2076
|
+
/* @__PURE__ */ jsx9("code", { children: "correctWords" }),
|
|
1597
2077
|
". Check spelling and capitalization in the source text."
|
|
1598
2078
|
] }) : null,
|
|
1599
|
-
/* @__PURE__ */
|
|
1600
|
-
/* @__PURE__ */
|
|
2079
|
+
/* @__PURE__ */ jsx9("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
|
|
2080
|
+
/* @__PURE__ */ jsx9("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
|
|
1601
2081
|
const isWord = !/^\s+$/.test(token);
|
|
1602
2082
|
const isTarget = isWord && correctSet.has(token.toLowerCase());
|
|
1603
|
-
if (!isTarget) return /* @__PURE__ */
|
|
2083
|
+
if (!isTarget) return /* @__PURE__ */ jsx9(React10.Fragment, { children: token }, i);
|
|
1604
2084
|
const selected = marked.has(i);
|
|
1605
2085
|
const solution = showSolutions || passed && props.enableSolutionsButton;
|
|
1606
|
-
return /* @__PURE__ */
|
|
2086
|
+
return /* @__PURE__ */ jsx9(
|
|
1607
2087
|
"button",
|
|
1608
2088
|
{
|
|
1609
2089
|
type: "button",
|
|
@@ -1621,57 +2101,69 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1621
2101
|
i
|
|
1622
2102
|
);
|
|
1623
2103
|
}) }),
|
|
1624
|
-
allMarked ? /* @__PURE__ */
|
|
1625
|
-
props.enableRetry && passed ? /* @__PURE__ */
|
|
1626
|
-
props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */
|
|
2104
|
+
allMarked ? /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
|
|
2105
|
+
props.enableRetry && passed ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2106
|
+
props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1627
2107
|
] });
|
|
1628
2108
|
}
|
|
1629
|
-
var MarkTheWordsInnerForwarded =
|
|
1630
|
-
var MarkTheWords =
|
|
1631
|
-
return /* @__PURE__ */
|
|
2109
|
+
var MarkTheWordsInnerForwarded = forwardRef3(MarkTheWordsInner);
|
|
2110
|
+
var MarkTheWords = forwardRef3(function MarkTheWords2(props, ref) {
|
|
2111
|
+
return /* @__PURE__ */ jsx9(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx9(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1632
2112
|
});
|
|
1633
2113
|
|
|
1634
2114
|
// src/blocks/FillInTheBlanks.tsx
|
|
1635
|
-
import
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
function
|
|
2115
|
+
import React11, { forwardRef as forwardRef4, useEffect as useEffect7, useMemo as useMemo9, useRef as useRef8, useState as useState7 } from "react";
|
|
2116
|
+
|
|
2117
|
+
// src/assessment/internal/parseStarDelimitedTemplate.ts
|
|
2118
|
+
function parseStarDelimitedTemplate(template, idPrefix) {
|
|
1639
2119
|
const parts = [];
|
|
1640
|
-
const
|
|
2120
|
+
const values = [];
|
|
1641
2121
|
const re = /\*([^*]+)\*/g;
|
|
1642
2122
|
let last = 0;
|
|
1643
2123
|
let match;
|
|
1644
2124
|
let n = 0;
|
|
1645
2125
|
while ((match = re.exec(template)) !== null) {
|
|
1646
2126
|
parts.push(template.slice(last, match.index));
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
parts.push(id);
|
|
2127
|
+
values.push(match[1].trim());
|
|
2128
|
+
parts.push(`${idPrefix}-${n++}`);
|
|
1650
2129
|
last = match.index + match[0].length;
|
|
1651
2130
|
}
|
|
1652
2131
|
parts.push(template.slice(last));
|
|
1653
|
-
return { parts,
|
|
2132
|
+
return { parts, values };
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// src/blocks/FillInTheBlanks.tsx
|
|
2136
|
+
import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2137
|
+
var INTERACTION3 = "fillInBlanks";
|
|
2138
|
+
function parseTemplate(template) {
|
|
2139
|
+
const { parts, values } = parseStarDelimitedTemplate(template, "blank");
|
|
2140
|
+
return {
|
|
2141
|
+
parts,
|
|
2142
|
+
blanks: values.map((answer, i) => ({ id: `blank-${i}`, answer }))
|
|
2143
|
+
};
|
|
1654
2144
|
}
|
|
1655
2145
|
function FillInTheBlanksInner(props, ref) {
|
|
1656
|
-
const checkId =
|
|
2146
|
+
const checkId = useMemo9(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1657
2147
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1658
|
-
const parsed =
|
|
2148
|
+
const parsed = useMemo9(() => parseTemplate(props.template), [props.template]);
|
|
1659
2149
|
const blanks = props.blanks ?? parsed.blanks;
|
|
1660
|
-
const [values, setValues] =
|
|
2150
|
+
const [values, setValues] = useState7(
|
|
1661
2151
|
() => Object.fromEntries(blanks.map((b) => [b.id, ""]))
|
|
1662
2152
|
);
|
|
1663
|
-
const [passed, setPassed] =
|
|
1664
|
-
const [showSolutions, setShowSolutions] =
|
|
1665
|
-
const
|
|
1666
|
-
const
|
|
2153
|
+
const [passed, setPassed] = useState7(false);
|
|
2154
|
+
const [showSolutions, setShowSolutions] = useState7(false);
|
|
2155
|
+
const [submitted, setSubmitted] = useState7(false);
|
|
2156
|
+
const completedRef = useRef8(false);
|
|
2157
|
+
const answeredRef = useRef8(false);
|
|
1667
2158
|
const reset = () => {
|
|
1668
2159
|
completedRef.current = false;
|
|
1669
2160
|
answeredRef.current = false;
|
|
1670
2161
|
setPassed(false);
|
|
1671
2162
|
setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
|
|
1672
2163
|
setShowSolutions(false);
|
|
2164
|
+
setSubmitted(false);
|
|
1673
2165
|
};
|
|
1674
|
-
|
|
2166
|
+
useEffect7(() => {
|
|
1675
2167
|
reset();
|
|
1676
2168
|
}, [checkId, props.template, blanks.map((b) => b.answer).join("\0")]);
|
|
1677
2169
|
const hasBlanks = blanks.length > 0;
|
|
@@ -1682,11 +2174,11 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
1682
2174
|
});
|
|
1683
2175
|
const maxScore = blanks.length;
|
|
1684
2176
|
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
1685
|
-
const handle =
|
|
1686
|
-
|
|
1687
|
-
|
|
2177
|
+
const handle = useMemo9(
|
|
2178
|
+
() => buildAssessmentHandle({
|
|
2179
|
+
checkId,
|
|
1688
2180
|
getScore: () => score,
|
|
1689
|
-
getMaxScore: () =>
|
|
2181
|
+
getMaxScore: () => maxScore || 1,
|
|
1690
2182
|
getAnswerGiven: () => allFilled,
|
|
1691
2183
|
resetTask: reset,
|
|
1692
2184
|
showSolutions: () => setShowSolutions(true),
|
|
@@ -1696,30 +2188,45 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
1696
2188
|
response: values,
|
|
1697
2189
|
correct: passedThreshold,
|
|
1698
2190
|
score,
|
|
1699
|
-
maxScore:
|
|
1700
|
-
})
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
2191
|
+
maxScore: maxScore || 1
|
|
2192
|
+
}),
|
|
2193
|
+
getCurrentState: () => ({ values, passed, showSolutions, submitted }),
|
|
2194
|
+
resume: (state) => {
|
|
2195
|
+
const raw = state.values;
|
|
2196
|
+
if (raw && typeof raw === "object") setValues({ ...raw });
|
|
2197
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
2198
|
+
setPassed(value);
|
|
2199
|
+
completedRef.current = value;
|
|
2200
|
+
answeredRef.current = value;
|
|
2201
|
+
});
|
|
2202
|
+
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
2203
|
+
readBooleanStateField(state, "submitted", (value) => {
|
|
2204
|
+
setSubmitted(value);
|
|
2205
|
+
if (value) answeredRef.current = true;
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
}),
|
|
2209
|
+
[allFilled, checkId, maxScore, passed, passedThreshold, score, showSolutions, submitted, values]
|
|
2210
|
+
);
|
|
2211
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2212
|
+
const check = () => {
|
|
2213
|
+
if (!hasBlanks) {
|
|
2214
|
+
if (isDevEnvironment4()) {
|
|
2215
|
+
console.warn("[lessonkit] FillInTheBlanks has no blanks in template");
|
|
2216
|
+
}
|
|
1710
2217
|
return;
|
|
1711
2218
|
}
|
|
1712
2219
|
if (!allFilled) return;
|
|
1713
|
-
if (
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
}
|
|
2220
|
+
if (answeredRef.current || submitted) return;
|
|
2221
|
+
answeredRef.current = true;
|
|
2222
|
+
setSubmitted(true);
|
|
2223
|
+
assessment.answer({
|
|
2224
|
+
checkId,
|
|
2225
|
+
interactionType: INTERACTION3,
|
|
2226
|
+
question: props.template,
|
|
2227
|
+
response: values,
|
|
2228
|
+
correct: passedThreshold
|
|
2229
|
+
});
|
|
1723
2230
|
if (passedThreshold && !completedRef.current) {
|
|
1724
2231
|
completedRef.current = true;
|
|
1725
2232
|
setPassed(true);
|
|
@@ -1732,20 +2239,23 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
1732
2239
|
});
|
|
1733
2240
|
}
|
|
1734
2241
|
};
|
|
1735
|
-
|
|
1736
|
-
if (!allFilled)
|
|
2242
|
+
useEffect7(() => {
|
|
2243
|
+
if (!allFilled) {
|
|
2244
|
+
answeredRef.current = false;
|
|
2245
|
+
setSubmitted(false);
|
|
2246
|
+
}
|
|
1737
2247
|
}, [allFilled]);
|
|
1738
|
-
|
|
2248
|
+
useEffect7(() => {
|
|
1739
2249
|
if (props.autoCheck && allFilled) check();
|
|
1740
2250
|
}, [allFilled, props.autoCheck, values, passedThreshold]);
|
|
1741
2251
|
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
1742
|
-
return /* @__PURE__ */
|
|
1743
|
-
/* @__PURE__ */
|
|
2252
|
+
return /* @__PURE__ */ jsxs6("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
|
|
2253
|
+
/* @__PURE__ */ jsx10("p", { children: parsed.parts.map((part, i) => {
|
|
1744
2254
|
const blank = blanks.find((b) => b.id === part);
|
|
1745
|
-
if (!blank) return /* @__PURE__ */
|
|
1746
|
-
return /* @__PURE__ */
|
|
1747
|
-
/* @__PURE__ */
|
|
1748
|
-
/* @__PURE__ */
|
|
2255
|
+
if (!blank) return /* @__PURE__ */ jsx10(React11.Fragment, { children: part }, i);
|
|
2256
|
+
return /* @__PURE__ */ jsxs6("label", { style: { margin: "0 0.25em" }, children: [
|
|
2257
|
+
/* @__PURE__ */ jsx10("span", { className: "lk-visually-hidden", children: blank.answer }),
|
|
2258
|
+
/* @__PURE__ */ jsx10(
|
|
1749
2259
|
"input",
|
|
1750
2260
|
{
|
|
1751
2261
|
type: "text",
|
|
@@ -1761,61 +2271,51 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
1761
2271
|
)
|
|
1762
2272
|
] }, blank.id);
|
|
1763
2273
|
}) }),
|
|
1764
|
-
!props.autoCheck ? /* @__PURE__ */
|
|
1765
|
-
!hasBlanks ? /* @__PURE__ */
|
|
1766
|
-
|
|
1767
|
-
props.enableRetry && passed ? /* @__PURE__ */
|
|
1768
|
-
props.enableSolutionsButton && !reveal ? /* @__PURE__ */
|
|
2274
|
+
!props.autoCheck ? /* @__PURE__ */ jsx10("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
|
|
2275
|
+
!hasBlanks ? /* @__PURE__ */ jsx10("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
|
|
2276
|
+
submitted ? /* @__PURE__ */ jsx10("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
|
|
2277
|
+
props.enableRetry && passed ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2278
|
+
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1769
2279
|
] });
|
|
1770
2280
|
}
|
|
1771
|
-
var FillInTheBlanksInnerForwarded =
|
|
1772
|
-
var FillInTheBlanks =
|
|
2281
|
+
var FillInTheBlanksInnerForwarded = forwardRef4(FillInTheBlanksInner);
|
|
2282
|
+
var FillInTheBlanks = forwardRef4(
|
|
1773
2283
|
function FillInTheBlanks2(props, ref) {
|
|
1774
|
-
return /* @__PURE__ */
|
|
2284
|
+
return /* @__PURE__ */ jsx10(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx10(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1775
2285
|
}
|
|
1776
2286
|
);
|
|
1777
2287
|
|
|
1778
2288
|
// src/blocks/DragTheWords.tsx
|
|
1779
|
-
import
|
|
1780
|
-
import { jsx as
|
|
2289
|
+
import React12, { forwardRef as forwardRef5, useEffect as useEffect8, useMemo as useMemo10, useRef as useRef9, useState as useState8 } from "react";
|
|
2290
|
+
import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1781
2291
|
var INTERACTION4 = "dragTheWords";
|
|
1782
2292
|
function parseZones(template) {
|
|
1783
|
-
const parts =
|
|
1784
|
-
|
|
1785
|
-
const re = /\*([^*]+)\*/g;
|
|
1786
|
-
let last = 0;
|
|
1787
|
-
let match;
|
|
1788
|
-
let n = 0;
|
|
1789
|
-
while ((match = re.exec(template)) !== null) {
|
|
1790
|
-
parts.push(template.slice(last, match.index));
|
|
1791
|
-
answers.push(match[1].trim());
|
|
1792
|
-
parts.push(`zone-${n++}`);
|
|
1793
|
-
last = match.index + match[0].length;
|
|
1794
|
-
}
|
|
1795
|
-
parts.push(template.slice(last));
|
|
1796
|
-
return { parts, answers };
|
|
2293
|
+
const { parts, values } = parseStarDelimitedTemplate(template, "zone");
|
|
2294
|
+
return { parts, answers: values };
|
|
1797
2295
|
}
|
|
1798
2296
|
function DragTheWordsInner(props, ref) {
|
|
1799
|
-
const checkId =
|
|
2297
|
+
const checkId = useMemo10(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1800
2298
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1801
|
-
const { parts, answers } =
|
|
1802
|
-
const [zones, setZones] =
|
|
2299
|
+
const { parts, answers } = useMemo10(() => parseZones(props.template), [props.template]);
|
|
2300
|
+
const [zones, setZones] = useState8(
|
|
1803
2301
|
() => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
|
|
1804
2302
|
);
|
|
1805
|
-
const [pool, setPool] =
|
|
1806
|
-
const [keyboardWord, setKeyboardWord] =
|
|
1807
|
-
const [passed, setPassed] =
|
|
1808
|
-
const
|
|
1809
|
-
const
|
|
2303
|
+
const [pool, setPool] = useState8(() => [...props.words]);
|
|
2304
|
+
const [keyboardWord, setKeyboardWord] = useState8(null);
|
|
2305
|
+
const [passed, setPassed] = useState8(false);
|
|
2306
|
+
const [submitted, setSubmitted] = useState8(false);
|
|
2307
|
+
const completedRef = useRef9(false);
|
|
2308
|
+
const answeredRef = useRef9(false);
|
|
1810
2309
|
const reset = () => {
|
|
1811
2310
|
completedRef.current = false;
|
|
1812
2311
|
answeredRef.current = false;
|
|
1813
2312
|
setPassed(false);
|
|
2313
|
+
setSubmitted(false);
|
|
1814
2314
|
setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
|
|
1815
2315
|
setPool([...props.words]);
|
|
1816
2316
|
setKeyboardWord(null);
|
|
1817
2317
|
};
|
|
1818
|
-
|
|
2318
|
+
useEffect8(() => {
|
|
1819
2319
|
reset();
|
|
1820
2320
|
}, [checkId, props.template, props.words.join("\0")]);
|
|
1821
2321
|
const hasZones = answers.length > 0;
|
|
@@ -1826,11 +2326,11 @@ function DragTheWordsInner(props, ref) {
|
|
|
1826
2326
|
});
|
|
1827
2327
|
const maxScore = answers.length;
|
|
1828
2328
|
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
1829
|
-
const handle =
|
|
1830
|
-
|
|
1831
|
-
|
|
2329
|
+
const handle = useMemo10(
|
|
2330
|
+
() => buildAssessmentHandle({
|
|
2331
|
+
checkId,
|
|
1832
2332
|
getScore: () => score,
|
|
1833
|
-
getMaxScore: () =>
|
|
2333
|
+
getMaxScore: () => maxScore || 1,
|
|
1834
2334
|
getAnswerGiven: () => allFilled,
|
|
1835
2335
|
resetTask: reset,
|
|
1836
2336
|
showSolutions: () => {
|
|
@@ -1841,12 +2341,29 @@ function DragTheWordsInner(props, ref) {
|
|
|
1841
2341
|
response: zones,
|
|
1842
2342
|
correct: passedThreshold,
|
|
1843
2343
|
score,
|
|
1844
|
-
maxScore:
|
|
1845
|
-
})
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
2344
|
+
maxScore: maxScore || 1
|
|
2345
|
+
}),
|
|
2346
|
+
getCurrentState: () => ({ zones, pool, passed, keyboardWord, submitted }),
|
|
2347
|
+
resume: (state) => {
|
|
2348
|
+
const rawZones = state.zones;
|
|
2349
|
+
if (rawZones && typeof rawZones === "object") setZones({ ...rawZones });
|
|
2350
|
+
if (Array.isArray(state.pool)) setPool([...state.pool]);
|
|
2351
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
2352
|
+
setPassed(value);
|
|
2353
|
+
completedRef.current = value;
|
|
2354
|
+
answeredRef.current = value;
|
|
2355
|
+
});
|
|
2356
|
+
readBooleanStateField(state, "submitted", (value) => {
|
|
2357
|
+
setSubmitted(value);
|
|
2358
|
+
if (value) answeredRef.current = true;
|
|
2359
|
+
});
|
|
2360
|
+
const kw = state.keyboardWord;
|
|
2361
|
+
if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
|
|
2362
|
+
}
|
|
2363
|
+
}),
|
|
2364
|
+
[allFilled, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, score, submitted, zones]
|
|
2365
|
+
);
|
|
2366
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1850
2367
|
const placeInZone = (zoneId, word) => {
|
|
1851
2368
|
if (passed && !props.enableRetry) return;
|
|
1852
2369
|
const prev = zones[zoneId];
|
|
@@ -1874,16 +2391,16 @@ function DragTheWordsInner(props, ref) {
|
|
|
1874
2391
|
return;
|
|
1875
2392
|
}
|
|
1876
2393
|
if (!allFilled) return;
|
|
1877
|
-
if (
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
}
|
|
2394
|
+
if (answeredRef.current || submitted) return;
|
|
2395
|
+
answeredRef.current = true;
|
|
2396
|
+
setSubmitted(true);
|
|
2397
|
+
assessment.answer({
|
|
2398
|
+
checkId,
|
|
2399
|
+
interactionType: INTERACTION4,
|
|
2400
|
+
question: props.template,
|
|
2401
|
+
response: zones,
|
|
2402
|
+
correct: passedThreshold
|
|
2403
|
+
});
|
|
1887
2404
|
if (passedThreshold && !completedRef.current) {
|
|
1888
2405
|
completedRef.current = true;
|
|
1889
2406
|
setPassed(true);
|
|
@@ -1896,15 +2413,18 @@ function DragTheWordsInner(props, ref) {
|
|
|
1896
2413
|
});
|
|
1897
2414
|
}
|
|
1898
2415
|
};
|
|
1899
|
-
|
|
1900
|
-
if (!allFilled)
|
|
2416
|
+
useEffect8(() => {
|
|
2417
|
+
if (!allFilled) {
|
|
2418
|
+
answeredRef.current = false;
|
|
2419
|
+
setSubmitted(false);
|
|
2420
|
+
}
|
|
1901
2421
|
}, [allFilled]);
|
|
1902
|
-
|
|
2422
|
+
useEffect8(() => {
|
|
1903
2423
|
if (props.autoCheck && allFilled) check();
|
|
1904
2424
|
}, [allFilled, props.autoCheck, zones, passedThreshold]);
|
|
1905
|
-
return /* @__PURE__ */
|
|
1906
|
-
/* @__PURE__ */
|
|
1907
|
-
/* @__PURE__ */
|
|
2425
|
+
return /* @__PURE__ */ jsxs7("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
|
|
2426
|
+
/* @__PURE__ */ jsx11("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
|
|
2427
|
+
/* @__PURE__ */ jsx11("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ jsx11(
|
|
1908
2428
|
"button",
|
|
1909
2429
|
{
|
|
1910
2430
|
type: "button",
|
|
@@ -1918,229 +2438,1616 @@ function DragTheWordsInner(props, ref) {
|
|
|
1918
2438
|
},
|
|
1919
2439
|
word
|
|
1920
2440
|
)) }),
|
|
1921
|
-
/* @__PURE__ */
|
|
1922
|
-
if (!part.startsWith("zone-")) return /* @__PURE__ */
|
|
1923
|
-
return /* @__PURE__ */
|
|
2441
|
+
/* @__PURE__ */ jsx11("p", { children: parts.map((part, i) => {
|
|
2442
|
+
if (!part.startsWith("zone-")) return /* @__PURE__ */ jsx11(React12.Fragment, { children: part }, i);
|
|
2443
|
+
return /* @__PURE__ */ jsx11(
|
|
1924
2444
|
"span",
|
|
1925
2445
|
{
|
|
1926
|
-
role: "button",
|
|
1927
|
-
tabIndex: 0,
|
|
1928
|
-
"data-testid": part,
|
|
1929
|
-
onDragOver: (e) => e.preventDefault(),
|
|
1930
|
-
onDrop: onDrop(part),
|
|
1931
|
-
onClick: () => keyboardWord && placeInZone(part, keyboardWord),
|
|
1932
|
-
onKeyDown: (e) => {
|
|
1933
|
-
if (e.key === "Enter" && keyboardWord) placeInZone(part, keyboardWord);
|
|
1934
|
-
},
|
|
2446
|
+
role: "button",
|
|
2447
|
+
tabIndex: 0,
|
|
2448
|
+
"data-testid": part,
|
|
2449
|
+
onDragOver: (e) => e.preventDefault(),
|
|
2450
|
+
onDrop: onDrop(part),
|
|
2451
|
+
onClick: () => keyboardWord && placeInZone(part, keyboardWord),
|
|
2452
|
+
onKeyDown: (e) => {
|
|
2453
|
+
if (e.key === "Enter" && keyboardWord) placeInZone(part, keyboardWord);
|
|
2454
|
+
},
|
|
2455
|
+
style: {
|
|
2456
|
+
display: "inline-block",
|
|
2457
|
+
minWidth: "6em",
|
|
2458
|
+
border: "1px dashed currentColor",
|
|
2459
|
+
padding: "0.2em 0.5em",
|
|
2460
|
+
margin: "0 0.2em"
|
|
2461
|
+
},
|
|
2462
|
+
children: zones[part] || "___"
|
|
2463
|
+
},
|
|
2464
|
+
part
|
|
2465
|
+
);
|
|
2466
|
+
}) }),
|
|
2467
|
+
/* @__PURE__ */ jsx11("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
|
|
2468
|
+
!hasZones ? /* @__PURE__ */ jsx11("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
|
|
2469
|
+
submitted ? /* @__PURE__ */ jsx11("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
|
|
2470
|
+
] });
|
|
2471
|
+
}
|
|
2472
|
+
var DragTheWordsInnerForwarded = forwardRef5(DragTheWordsInner);
|
|
2473
|
+
var DragTheWords = forwardRef5(function DragTheWords2(props, ref) {
|
|
2474
|
+
return /* @__PURE__ */ jsx11(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx11(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2475
|
+
});
|
|
2476
|
+
|
|
2477
|
+
// src/blocks/DragAndDrop.tsx
|
|
2478
|
+
import { forwardRef as forwardRef6, useEffect as useEffect9, useMemo as useMemo11, useRef as useRef10, useState as useState9 } from "react";
|
|
2479
|
+
import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2480
|
+
var INTERACTION5 = "dragAndDrop";
|
|
2481
|
+
function DragAndDropInner(props, ref) {
|
|
2482
|
+
const checkId = useMemo11(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
2483
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2484
|
+
const [assignments, setAssignments] = useState9(
|
|
2485
|
+
() => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
|
|
2486
|
+
);
|
|
2487
|
+
const [pool, setPool] = useState9(() => props.items.map((i) => i.id));
|
|
2488
|
+
const [keyboardItem, setKeyboardItem] = useState9(null);
|
|
2489
|
+
const [passed, setPassed] = useState9(false);
|
|
2490
|
+
const [checked, setChecked] = useState9(false);
|
|
2491
|
+
const completedRef = useRef10(false);
|
|
2492
|
+
const reset = () => {
|
|
2493
|
+
completedRef.current = false;
|
|
2494
|
+
setPassed(false);
|
|
2495
|
+
setChecked(false);
|
|
2496
|
+
setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
|
|
2497
|
+
setPool(props.items.map((i) => i.id));
|
|
2498
|
+
setKeyboardItem(null);
|
|
2499
|
+
};
|
|
2500
|
+
useEffect9(() => {
|
|
2501
|
+
reset();
|
|
2502
|
+
}, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
|
|
2503
|
+
const hasTargets = props.targets.length > 0;
|
|
2504
|
+
const allFilled = hasTargets && props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
|
|
2505
|
+
let score = 0;
|
|
2506
|
+
props.targets.forEach((t) => {
|
|
2507
|
+
if (assignments[t.id] === t.accepts) score += 1;
|
|
2508
|
+
});
|
|
2509
|
+
const maxScore = props.targets.length || 1;
|
|
2510
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
2511
|
+
const handle = useMemo11(() => {
|
|
2512
|
+
return buildAssessmentHandle({
|
|
2513
|
+
checkId,
|
|
2514
|
+
getScore: () => score,
|
|
2515
|
+
getMaxScore: () => maxScore,
|
|
2516
|
+
getAnswerGiven: () => hasTargets && allFilled,
|
|
2517
|
+
resetTask: reset,
|
|
2518
|
+
showSolutions: () => {
|
|
2519
|
+
},
|
|
2520
|
+
getXAPIData: () => ({
|
|
2521
|
+
checkId,
|
|
2522
|
+
interactionType: INTERACTION5,
|
|
2523
|
+
response: assignments,
|
|
2524
|
+
correct: passedThreshold,
|
|
2525
|
+
score,
|
|
2526
|
+
maxScore
|
|
2527
|
+
}),
|
|
2528
|
+
getCurrentState: () => ({ assignments, pool, passed, checked, keyboardItem }),
|
|
2529
|
+
resume: (state) => {
|
|
2530
|
+
const rawAssignments = state.assignments;
|
|
2531
|
+
if (rawAssignments && typeof rawAssignments === "object") {
|
|
2532
|
+
setAssignments({ ...rawAssignments });
|
|
2533
|
+
}
|
|
2534
|
+
if (Array.isArray(state.pool)) setPool([...state.pool]);
|
|
2535
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
2536
|
+
setPassed(value);
|
|
2537
|
+
completedRef.current = value;
|
|
2538
|
+
});
|
|
2539
|
+
readBooleanStateField(state, "checked", setChecked);
|
|
2540
|
+
const item = state.keyboardItem;
|
|
2541
|
+
if (item === null || typeof item === "string") setKeyboardItem(item ?? null);
|
|
2542
|
+
}
|
|
2543
|
+
});
|
|
2544
|
+
}, [allFilled, assignments, checkId, checked, hasTargets, keyboardItem, maxScore, passed, passedThreshold, pool, props.targets, score]);
|
|
2545
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2546
|
+
const place = (targetId, itemId) => {
|
|
2547
|
+
if (passed && !props.enableRetry) return;
|
|
2548
|
+
setChecked(false);
|
|
2549
|
+
const prev = assignments[targetId];
|
|
2550
|
+
setAssignments((a) => ({ ...a, [targetId]: itemId }));
|
|
2551
|
+
setPool((p) => {
|
|
2552
|
+
const next = p.filter((id) => id !== itemId);
|
|
2553
|
+
if (prev) next.push(prev);
|
|
2554
|
+
return next;
|
|
2555
|
+
});
|
|
2556
|
+
setKeyboardItem(null);
|
|
2557
|
+
};
|
|
2558
|
+
const check = () => {
|
|
2559
|
+
if (!allFilled) return;
|
|
2560
|
+
setChecked(true);
|
|
2561
|
+
assessment.answer({
|
|
2562
|
+
checkId,
|
|
2563
|
+
interactionType: INTERACTION5,
|
|
2564
|
+
response: assignments,
|
|
2565
|
+
correct: passedThreshold
|
|
2566
|
+
});
|
|
2567
|
+
if (passedThreshold && !completedRef.current) {
|
|
2568
|
+
completedRef.current = true;
|
|
2569
|
+
setPassed(true);
|
|
2570
|
+
assessment.complete({
|
|
2571
|
+
checkId,
|
|
2572
|
+
interactionType: INTERACTION5,
|
|
2573
|
+
score,
|
|
2574
|
+
maxScore,
|
|
2575
|
+
passingScore: props.passingScore ?? maxScore
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
};
|
|
2579
|
+
return /* @__PURE__ */ jsxs8("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
|
|
2580
|
+
/* @__PURE__ */ jsx12("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
|
|
2581
|
+
/* @__PURE__ */ jsx12("div", { role: "list", "aria-label": "Draggable items", children: pool.flatMap((id) => {
|
|
2582
|
+
const item = props.items.find((i) => i.id === id);
|
|
2583
|
+
if (!item) return [];
|
|
2584
|
+
return /* @__PURE__ */ jsx12(
|
|
2585
|
+
"button",
|
|
2586
|
+
{
|
|
2587
|
+
type: "button",
|
|
2588
|
+
draggable: true,
|
|
2589
|
+
"data-testid": `drag-item-${id}`,
|
|
2590
|
+
"aria-pressed": keyboardItem === id,
|
|
2591
|
+
onDragStart: (e) => e.dataTransfer.setData("text/plain", id),
|
|
2592
|
+
onClick: () => setKeyboardItem(keyboardItem === id ? null : id),
|
|
2593
|
+
style: { margin: "0.25rem" },
|
|
2594
|
+
children: item.label
|
|
2595
|
+
},
|
|
2596
|
+
id
|
|
2597
|
+
);
|
|
2598
|
+
}) }),
|
|
2599
|
+
/* @__PURE__ */ jsx12("ul", { children: props.targets.map((target) => {
|
|
2600
|
+
const assigned = assignments[target.id];
|
|
2601
|
+
const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
|
|
2602
|
+
return /* @__PURE__ */ jsxs8("li", { children: [
|
|
2603
|
+
/* @__PURE__ */ jsx12("strong", { children: target.label }),
|
|
2604
|
+
" ",
|
|
2605
|
+
/* @__PURE__ */ jsx12(
|
|
2606
|
+
"span",
|
|
2607
|
+
{
|
|
2608
|
+
role: "button",
|
|
2609
|
+
tabIndex: 0,
|
|
2610
|
+
"data-testid": `drop-${target.id}`,
|
|
2611
|
+
onDragOver: (e) => e.preventDefault(),
|
|
2612
|
+
onDrop: (e) => {
|
|
2613
|
+
e.preventDefault();
|
|
2614
|
+
const id = e.dataTransfer.getData("text/plain");
|
|
2615
|
+
if (id) place(target.id, id);
|
|
2616
|
+
},
|
|
2617
|
+
onClick: () => keyboardItem && place(target.id, keyboardItem),
|
|
2618
|
+
onKeyDown: (e) => {
|
|
2619
|
+
if (e.key === "Enter" && keyboardItem) place(target.id, keyboardItem);
|
|
2620
|
+
},
|
|
2621
|
+
style: {
|
|
2622
|
+
display: "inline-block",
|
|
2623
|
+
minWidth: "8em",
|
|
2624
|
+
border: "1px dashed currentColor",
|
|
2625
|
+
padding: "0.25em"
|
|
2626
|
+
},
|
|
2627
|
+
children: label
|
|
2628
|
+
}
|
|
2629
|
+
)
|
|
2630
|
+
] }, target.id);
|
|
2631
|
+
}) }),
|
|
2632
|
+
/* @__PURE__ */ jsx12("button", { type: "button", "data-testid": "check-drag-drop", disabled: !hasTargets || !allFilled || passed, onClick: check, children: "Check" }),
|
|
2633
|
+
checked ? /* @__PURE__ */ jsx12("p", { role: "status", "aria-live": "polite", children: passedThreshold ? "Correct" : "Try again" }) : null
|
|
2634
|
+
] });
|
|
2635
|
+
}
|
|
2636
|
+
var DragAndDropInnerForwarded = forwardRef6(DragAndDropInner);
|
|
2637
|
+
var DragAndDrop = forwardRef6(function DragAndDrop2(props, ref) {
|
|
2638
|
+
return /* @__PURE__ */ jsx12(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx12(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2639
|
+
});
|
|
2640
|
+
|
|
2641
|
+
// src/blocks/AssessmentSequence.tsx
|
|
2642
|
+
import React16, { forwardRef as forwardRef7, useCallback as useCallback7, useEffect as useEffect12, useId as useId3, useMemo as useMemo13, useRef as useRef13, useState as useState10 } from "react";
|
|
2643
|
+
import { deriveId } from "@lessonkit/core";
|
|
2644
|
+
|
|
2645
|
+
// src/compound/useCompoundShell.ts
|
|
2646
|
+
import { useMemo as useMemo12 } from "react";
|
|
2647
|
+
import { clampCompoundPageIndex as clampCompoundPageIndex3 } from "@lessonkit/core";
|
|
2648
|
+
|
|
2649
|
+
// src/compound/useCompoundNavigation.ts
|
|
2650
|
+
import { useCallback as useCallback4 } from "react";
|
|
2651
|
+
function useCompoundNavigation(pageCount, index, setIndex) {
|
|
2652
|
+
const goNext = useCallback4(() => {
|
|
2653
|
+
if (pageCount < 1) return;
|
|
2654
|
+
setIndex((i) => Math.min(i + 1, pageCount - 1));
|
|
2655
|
+
}, [pageCount, setIndex]);
|
|
2656
|
+
const goPrev = useCallback4(() => {
|
|
2657
|
+
setIndex((i) => Math.max(i - 1, 0));
|
|
2658
|
+
}, [setIndex]);
|
|
2659
|
+
const clampedIndex = pageCount < 1 ? 0 : Math.min(index, pageCount - 1);
|
|
2660
|
+
return {
|
|
2661
|
+
index: clampedIndex,
|
|
2662
|
+
setIndex,
|
|
2663
|
+
goNext,
|
|
2664
|
+
goPrev,
|
|
2665
|
+
progress: { current: pageCount < 1 ? 0 : clampedIndex + 1, total: pageCount }
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
// src/compound/useCompoundPersistence.ts
|
|
2670
|
+
import { useCallback as useCallback6, useContext as useContext7, useEffect as useEffect11, useRef as useRef12 } from "react";
|
|
2671
|
+
import {
|
|
2672
|
+
clampCompoundPageIndex as clampCompoundPageIndex2,
|
|
2673
|
+
createCompoundResumeState as createCompoundResumeState2,
|
|
2674
|
+
createSessionStoragePort as createSessionStoragePort3,
|
|
2675
|
+
loadCompoundState as loadCompoundState2
|
|
2676
|
+
} from "@lessonkit/core";
|
|
2677
|
+
|
|
2678
|
+
// src/compound/resumeChildHandles.ts
|
|
2679
|
+
function filterRegisteredChildStates(handles, childStates) {
|
|
2680
|
+
const filtered = {};
|
|
2681
|
+
for (const [key, value] of Object.entries(childStates)) {
|
|
2682
|
+
if (handles.has(key)) {
|
|
2683
|
+
filtered[key] = value;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
return filtered;
|
|
2687
|
+
}
|
|
2688
|
+
function resumeChildHandles(handles, childStates, opts) {
|
|
2689
|
+
const pendingKeys = Object.keys(childStates);
|
|
2690
|
+
const alreadyResumed = opts?.alreadyResumed;
|
|
2691
|
+
if (opts?.waitForHandles && pendingKeys.length > 0) {
|
|
2692
|
+
if (handles.size === 0) return false;
|
|
2693
|
+
const registeredPending = pendingKeys.filter((k) => handles.has(k));
|
|
2694
|
+
if (registeredPending.length === 0) {
|
|
2695
|
+
return false;
|
|
2696
|
+
}
|
|
2697
|
+
if (registeredPending.length < pendingKeys.length) {
|
|
2698
|
+
for (const key of registeredPending) {
|
|
2699
|
+
if (alreadyResumed?.has(key)) continue;
|
|
2700
|
+
const handle = handles.get(key);
|
|
2701
|
+
const child = childStates[key];
|
|
2702
|
+
if (handle?.resume && child) {
|
|
2703
|
+
handle.resume(child);
|
|
2704
|
+
alreadyResumed?.add(key);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
return false;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
for (const [checkId, handle] of handles) {
|
|
2711
|
+
if (alreadyResumed?.has(checkId)) continue;
|
|
2712
|
+
const child = childStates[checkId];
|
|
2713
|
+
if (child && handle.resume) {
|
|
2714
|
+
handle.resume(child);
|
|
2715
|
+
alreadyResumed?.add(checkId);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
return true;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// src/compound/useCompoundResume.ts
|
|
2722
|
+
import { useCallback as useCallback5, useContext as useContext6, useEffect as useEffect10, useRef as useRef11 } from "react";
|
|
2723
|
+
import { loadCompoundState, saveCompoundState } from "@lessonkit/core";
|
|
2724
|
+
import { createSessionStoragePort as createSessionStoragePort2 } from "@lessonkit/core";
|
|
2725
|
+
var warnedCompoundPersistFailure = false;
|
|
2726
|
+
function warnCompoundPersistFailure() {
|
|
2727
|
+
if (warnedCompoundPersistFailure || !isDevEnvironment4()) return;
|
|
2728
|
+
warnedCompoundPersistFailure = true;
|
|
2729
|
+
console.warn(
|
|
2730
|
+
"[lessonkit] compound resume state could not be saved to sessionStorage (quota or privacy mode); progress may be lost on reload."
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
function useCompoundResume(opts) {
|
|
2734
|
+
const lessonkitCtx = useContext6(LessonkitContext);
|
|
2735
|
+
const storageRef = useRef11(opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2());
|
|
2736
|
+
const resumedRef = useRef11(false);
|
|
2737
|
+
const resumeKeyRef = useRef11("");
|
|
2738
|
+
const prevEnabledRef = useRef11(opts.enabled);
|
|
2739
|
+
useEffect10(() => {
|
|
2740
|
+
storageRef.current = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2();
|
|
2741
|
+
}, [opts.storage, lessonkitCtx?.storage]);
|
|
2742
|
+
useEffect10(() => {
|
|
2743
|
+
if (!prevEnabledRef.current && opts.enabled) {
|
|
2744
|
+
resumedRef.current = false;
|
|
2745
|
+
}
|
|
2746
|
+
prevEnabledRef.current = opts.enabled;
|
|
2747
|
+
const key = `${opts.courseId ?? ""}:${opts.compoundId}`;
|
|
2748
|
+
if (resumeKeyRef.current !== key) {
|
|
2749
|
+
resumeKeyRef.current = key;
|
|
2750
|
+
resumedRef.current = false;
|
|
2751
|
+
}
|
|
2752
|
+
if (!opts.enabled || !opts.courseId || resumedRef.current) return;
|
|
2753
|
+
const saved = loadCompoundState(storageRef.current, opts.courseId, opts.compoundId);
|
|
2754
|
+
if (saved) {
|
|
2755
|
+
resumedRef.current = true;
|
|
2756
|
+
opts.onResume?.(saved);
|
|
2757
|
+
}
|
|
2758
|
+
}, [opts.enabled, opts.courseId, opts.compoundId, opts.onResume]);
|
|
2759
|
+
return useCallback5(
|
|
2760
|
+
(state) => {
|
|
2761
|
+
if (!opts.enabled || !opts.courseId) return;
|
|
2762
|
+
const persisted = saveCompoundState(storageRef.current, opts.courseId, opts.compoundId, state);
|
|
2763
|
+
if (!persisted) warnCompoundPersistFailure();
|
|
2764
|
+
},
|
|
2765
|
+
[opts.enabled, opts.courseId, opts.compoundId]
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
// src/compound/useCompoundPersistence.ts
|
|
2770
|
+
function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, storage = createSessionStoragePort3()) {
|
|
2771
|
+
if (!enabled || !courseId || pageCount < 1) return 0;
|
|
2772
|
+
const saved = loadCompoundState2(storage, courseId, compoundId);
|
|
2773
|
+
if (!saved) return 0;
|
|
2774
|
+
return clampCompoundPageIndex2(saved.activePageIndex, pageCount);
|
|
2775
|
+
}
|
|
2776
|
+
function stripOrphanChildStates(handles, childStates) {
|
|
2777
|
+
return filterRegisteredChildStates(handles, childStates);
|
|
2778
|
+
}
|
|
2779
|
+
function useCompoundPersistence(opts) {
|
|
2780
|
+
const lessonkitCtx = useContext7(LessonkitContext);
|
|
2781
|
+
const storage = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort3();
|
|
2782
|
+
const ctx = useCompoundRegistry();
|
|
2783
|
+
const handlesVersion = useCompoundHandlesVersion();
|
|
2784
|
+
const bridgeRef = useCompoundHydrationBridgeRef();
|
|
2785
|
+
const pendingChildResumeRef = useRef12(null);
|
|
2786
|
+
const resumedChildKeysRef = useRef12(/* @__PURE__ */ new Set());
|
|
2787
|
+
const loadedChildStatesRef = useRef12({});
|
|
2788
|
+
const skipSaveUntilHydratedRef = useRef12(false);
|
|
2789
|
+
const hydrationKeyRef = useRef12("");
|
|
2790
|
+
const hydrationInitRef = useRef12(false);
|
|
2791
|
+
const hydrationKey = `${opts.courseId ?? ""}:${opts.compoundId}`;
|
|
2792
|
+
if (hydrationKeyRef.current !== hydrationKey) {
|
|
2793
|
+
hydrationKeyRef.current = hydrationKey;
|
|
2794
|
+
hydrationInitRef.current = false;
|
|
2795
|
+
loadedChildStatesRef.current = {};
|
|
2796
|
+
skipSaveUntilHydratedRef.current = false;
|
|
2797
|
+
pendingChildResumeRef.current = null;
|
|
2798
|
+
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
2799
|
+
}
|
|
2800
|
+
if (!hydrationInitRef.current && opts.enabled && opts.courseId) {
|
|
2801
|
+
hydrationInitRef.current = true;
|
|
2802
|
+
const saved = loadCompoundState2(storage, opts.courseId, opts.compoundId);
|
|
2803
|
+
if (saved && Object.keys(saved.childStates).length > 0) {
|
|
2804
|
+
loadedChildStatesRef.current = { ...saved.childStates };
|
|
2805
|
+
skipSaveUntilHydratedRef.current = true;
|
|
2806
|
+
pendingChildResumeRef.current = saved;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
const buildState = useCallback6(() => {
|
|
2810
|
+
const childStates = {
|
|
2811
|
+
...loadedChildStatesRef.current
|
|
2812
|
+
};
|
|
2813
|
+
if (ctx) {
|
|
2814
|
+
for (const [checkId, entry] of ctx.getRegisteredHandles()) {
|
|
2815
|
+
const handle = entry.handle;
|
|
2816
|
+
if (handle.getCurrentState) {
|
|
2817
|
+
childStates[checkId] = handle.getCurrentState();
|
|
2818
|
+
delete loadedChildStatesRef.current[checkId];
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
return createCompoundResumeState2({
|
|
2823
|
+
activePageIndex: clampCompoundPageIndex2(opts.index, opts.pageCount),
|
|
2824
|
+
childStates
|
|
2825
|
+
});
|
|
2826
|
+
}, [ctx, opts.index, opts.pageCount]);
|
|
2827
|
+
const buildStateRef = useRef12(buildState);
|
|
2828
|
+
buildStateRef.current = buildState;
|
|
2829
|
+
const finalizeHydration = useCallback6(
|
|
2830
|
+
(childStates) => {
|
|
2831
|
+
loadedChildStatesRef.current = {
|
|
2832
|
+
...loadedChildStatesRef.current,
|
|
2833
|
+
...childStates
|
|
2834
|
+
};
|
|
2835
|
+
skipSaveUntilHydratedRef.current = false;
|
|
2836
|
+
pendingChildResumeRef.current = null;
|
|
2837
|
+
},
|
|
2838
|
+
[]
|
|
2839
|
+
);
|
|
2840
|
+
const applyPendingChildResume = useCallback6(() => {
|
|
2841
|
+
const pending = pendingChildResumeRef.current;
|
|
2842
|
+
if (!pending || !ctx) return;
|
|
2843
|
+
const handles = ctx.getHandles();
|
|
2844
|
+
const applied = resumeChildHandles(handles, pending.childStates, {
|
|
2845
|
+
waitForHandles: true,
|
|
2846
|
+
alreadyResumed: resumedChildKeysRef.current
|
|
2847
|
+
});
|
|
2848
|
+
if (!applied) {
|
|
2849
|
+
const handlesAtWait = handles.size;
|
|
2850
|
+
queueMicrotask(() => {
|
|
2851
|
+
if (pendingChildResumeRef.current !== pending) return;
|
|
2852
|
+
const handlesNow = ctx.getHandles();
|
|
2853
|
+
if (handlesNow.size !== handlesAtWait) return;
|
|
2854
|
+
const registeredOnly2 = stripOrphanChildStates(handlesNow, pending.childStates);
|
|
2855
|
+
resumeChildHandles(handlesNow, registeredOnly2, {
|
|
2856
|
+
alreadyResumed: resumedChildKeysRef.current
|
|
2857
|
+
});
|
|
2858
|
+
finalizeHydration(registeredOnly2);
|
|
2859
|
+
});
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
const registeredOnly = stripOrphanChildStates(handles, pending.childStates);
|
|
2863
|
+
finalizeHydration(registeredOnly);
|
|
2864
|
+
}, [ctx, finalizeHydration]);
|
|
2865
|
+
const saveResume = useCompoundResume({
|
|
2866
|
+
courseId: opts.courseId,
|
|
2867
|
+
compoundId: opts.compoundId,
|
|
2868
|
+
enabled: opts.enabled,
|
|
2869
|
+
storage,
|
|
2870
|
+
onResume: (state) => {
|
|
2871
|
+
const clamped = clampCompoundPageIndex2(state.activePageIndex, opts.pageCount);
|
|
2872
|
+
loadedChildStatesRef.current = { ...state.childStates };
|
|
2873
|
+
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
2874
|
+
opts.setIndex(clamped);
|
|
2875
|
+
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
2876
|
+
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
2877
|
+
queueMicrotask(() => applyPendingChildResume());
|
|
2878
|
+
}
|
|
2879
|
+
});
|
|
2880
|
+
const persistNow = useCallback6(() => {
|
|
2881
|
+
if (!opts.enabled || !opts.courseId) return;
|
|
2882
|
+
saveResume(buildStateRef.current());
|
|
2883
|
+
}, [opts.enabled, opts.courseId, saveResume]);
|
|
2884
|
+
const notifyImperativeResume = useCallback6(
|
|
2885
|
+
(state) => {
|
|
2886
|
+
const clamped = clampCompoundPageIndex2(state.activePageIndex, opts.pageCount);
|
|
2887
|
+
loadedChildStatesRef.current = { ...state.childStates };
|
|
2888
|
+
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
2889
|
+
opts.setIndex(clamped);
|
|
2890
|
+
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
2891
|
+
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
2892
|
+
queueMicrotask(() => applyPendingChildResume());
|
|
2893
|
+
},
|
|
2894
|
+
[opts.pageCount, opts.setIndex, applyPendingChildResume]
|
|
2895
|
+
);
|
|
2896
|
+
useEffect11(() => {
|
|
2897
|
+
if (!bridgeRef) return;
|
|
2898
|
+
bridgeRef.current = { notifyImperativeResume };
|
|
2899
|
+
return () => {
|
|
2900
|
+
if (bridgeRef.current?.notifyImperativeResume === notifyImperativeResume) {
|
|
2901
|
+
bridgeRef.current = null;
|
|
2902
|
+
}
|
|
2903
|
+
};
|
|
2904
|
+
}, [bridgeRef, notifyImperativeResume]);
|
|
2905
|
+
useEffect11(() => {
|
|
2906
|
+
persistNow();
|
|
2907
|
+
}, [persistNow, opts.index, opts.pageCount, handlesVersion]);
|
|
2908
|
+
useEffect11(() => {
|
|
2909
|
+
applyPendingChildResume();
|
|
2910
|
+
}, [opts.index, handlesVersion, applyPendingChildResume]);
|
|
2911
|
+
useEffect11(() => {
|
|
2912
|
+
if (!opts.enabled || !opts.courseId || typeof document === "undefined") return;
|
|
2913
|
+
const flushOnExit = () => {
|
|
2914
|
+
if (document.visibilityState === "hidden") persistNow();
|
|
2915
|
+
};
|
|
2916
|
+
document.addEventListener("visibilitychange", flushOnExit);
|
|
2917
|
+
window.addEventListener("pagehide", flushOnExit);
|
|
2918
|
+
return () => {
|
|
2919
|
+
document.removeEventListener("visibilitychange", flushOnExit);
|
|
2920
|
+
window.removeEventListener("pagehide", flushOnExit);
|
|
2921
|
+
};
|
|
2922
|
+
}, [opts.enabled, opts.courseId, persistNow]);
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
// src/compound/useCompoundShell.ts
|
|
2926
|
+
function useCompoundShell(opts) {
|
|
2927
|
+
const ctx = useCompoundRegistry();
|
|
2928
|
+
useCompoundPersistence({
|
|
2929
|
+
courseId: opts.courseId,
|
|
2930
|
+
compoundId: opts.compoundId,
|
|
2931
|
+
pageCount: opts.pageCount,
|
|
2932
|
+
index: opts.index,
|
|
2933
|
+
setIndex: opts.setIndex,
|
|
2934
|
+
enabled: opts.persistEnabled,
|
|
2935
|
+
storage: opts.storage
|
|
2936
|
+
});
|
|
2937
|
+
const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
|
|
2938
|
+
const visibleIndex = clampCompoundPageIndex3(opts.index, opts.pageCount);
|
|
2939
|
+
useCompoundHandleRef(opts.ref, {
|
|
2940
|
+
activePageIndex: visibleIndex,
|
|
2941
|
+
setActivePageIndex: opts.setIndex,
|
|
2942
|
+
getHandles: () => ctx?.getHandles() ?? /* @__PURE__ */ new Map(),
|
|
2943
|
+
getRegisteredHandles: () => ctx?.getRegisteredHandles() ?? /* @__PURE__ */ new Map(),
|
|
2944
|
+
pageCount: opts.pageCount,
|
|
2945
|
+
enableSolutionsButton: opts.enableSolutionsButton
|
|
2946
|
+
});
|
|
2947
|
+
return { visibleIndex, goNext, goPrev, progress, ctx };
|
|
2948
|
+
}
|
|
2949
|
+
function useCompoundInitialIndex(opts) {
|
|
2950
|
+
return useMemo12(
|
|
2951
|
+
() => readCompoundInitialIndex(
|
|
2952
|
+
opts.courseId,
|
|
2953
|
+
opts.compoundId,
|
|
2954
|
+
opts.pageCount,
|
|
2955
|
+
opts.persistEnabled,
|
|
2956
|
+
opts.storage
|
|
2957
|
+
),
|
|
2958
|
+
[opts.courseId, opts.compoundId, opts.pageCount, opts.persistEnabled, opts.storage]
|
|
2959
|
+
);
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
// src/compound/validateChildren.ts
|
|
2963
|
+
import React15 from "react";
|
|
2964
|
+
import {
|
|
2965
|
+
ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
2966
|
+
COMPOUND_MAX_NESTING_DEPTH,
|
|
2967
|
+
isChildTypeAllowed
|
|
2968
|
+
} from "@lessonkit/core";
|
|
2969
|
+
|
|
2970
|
+
// src/compound/blockType.ts
|
|
2971
|
+
var LESSONKIT_BLOCK_TYPE = /* @__PURE__ */ Symbol.for("lessonkit.blockType");
|
|
2972
|
+
function setLessonkitBlockType(component, blockType) {
|
|
2973
|
+
component[LESSONKIT_BLOCK_TYPE] = blockType;
|
|
2974
|
+
if (!component.displayName) {
|
|
2975
|
+
component.displayName = blockType;
|
|
2976
|
+
}
|
|
2977
|
+
return component;
|
|
2978
|
+
}
|
|
2979
|
+
function getLessonkitBlockType(component) {
|
|
2980
|
+
if (!component || typeof component !== "object" && typeof component !== "function") {
|
|
2981
|
+
return void 0;
|
|
2982
|
+
}
|
|
2983
|
+
const typed = component;
|
|
2984
|
+
return typed[LESSONKIT_BLOCK_TYPE] ?? typed.displayName;
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
// src/compound/validateChildren.ts
|
|
2988
|
+
var warnedPairs = /* @__PURE__ */ new Set();
|
|
2989
|
+
var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
2990
|
+
"Page",
|
|
2991
|
+
"InteractiveBook",
|
|
2992
|
+
"Slide",
|
|
2993
|
+
"SlideDeck",
|
|
2994
|
+
"AssessmentSequence"
|
|
2995
|
+
]);
|
|
2996
|
+
function warnOrThrow(msg, strict) {
|
|
2997
|
+
if (strict) throw new Error(msg);
|
|
2998
|
+
if (!warnedPairs.has(msg)) {
|
|
2999
|
+
warnedPairs.add(msg);
|
|
3000
|
+
console.warn(msg);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
function validateNode(parent, node, depth, strict) {
|
|
3004
|
+
React15.Children.forEach(node, (child) => {
|
|
3005
|
+
if (!React15.isValidElement(child)) return;
|
|
3006
|
+
const blockType = getLessonkitBlockType(child.type);
|
|
3007
|
+
if (!blockType) {
|
|
3008
|
+
if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
3009
|
+
validateNode(parent, child.props.children, depth, strict);
|
|
3010
|
+
}
|
|
3011
|
+
return;
|
|
3012
|
+
}
|
|
3013
|
+
if (!isChildTypeAllowed(parent, blockType)) {
|
|
3014
|
+
const key = `${parent}:${blockType}`;
|
|
3015
|
+
if (!warnedPairs.has(key)) {
|
|
3016
|
+
warnedPairs.add(key);
|
|
3017
|
+
const msg = `[lessonkit] Block "${blockType}" is not in the allowlist for "${parent}"`;
|
|
3018
|
+
if (strict) throw new Error(msg);
|
|
3019
|
+
console.warn(msg);
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
if (COMPOUND_CONTAINER_TYPES.has(blockType)) {
|
|
3023
|
+
const maxDepth = COMPOUND_MAX_NESTING_DEPTH[parent];
|
|
3024
|
+
if (depth >= maxDepth) {
|
|
3025
|
+
warnOrThrow(
|
|
3026
|
+
`[lessonkit] Block "${blockType}" exceeds max nesting depth (${maxDepth}) for "${parent}"`,
|
|
3027
|
+
strict
|
|
3028
|
+
);
|
|
3029
|
+
}
|
|
3030
|
+
const nestedParent = blockType;
|
|
3031
|
+
validateNode(nestedParent, child.props.children, depth + 1, strict);
|
|
3032
|
+
} else if (blockType === "Accordion") {
|
|
3033
|
+
const sections = child.props.sections;
|
|
3034
|
+
if (sections) validateAccordionSections(sections, strict);
|
|
3035
|
+
} else if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
3036
|
+
validateSubtreeForForbidden(
|
|
3037
|
+
child.props.children,
|
|
3038
|
+
ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
3039
|
+
strict
|
|
3040
|
+
);
|
|
3041
|
+
}
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
function validateSubtreeForForbidden(node, forbidden, strict) {
|
|
3045
|
+
React15.Children.forEach(node, (child) => {
|
|
3046
|
+
if (!React15.isValidElement(child)) return;
|
|
3047
|
+
const blockType = getLessonkitBlockType(child.type);
|
|
3048
|
+
if (blockType && forbidden.includes(blockType)) {
|
|
3049
|
+
warnOrThrow(`[lessonkit] Block "${blockType}" must not nest inside Accordion`, strict);
|
|
3050
|
+
}
|
|
3051
|
+
if (blockType === "Accordion") {
|
|
3052
|
+
const sections = child.props.sections;
|
|
3053
|
+
if (sections) validateAccordionSections(sections, strict);
|
|
3054
|
+
return;
|
|
3055
|
+
}
|
|
3056
|
+
if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
3057
|
+
validateSubtreeForForbidden(
|
|
3058
|
+
child.props.children,
|
|
3059
|
+
forbidden,
|
|
3060
|
+
strict
|
|
3061
|
+
);
|
|
3062
|
+
}
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
function validateAccordionSections(sections, strict) {
|
|
3066
|
+
if (!isDevEnvironment4() && !strict) return;
|
|
3067
|
+
for (const section of sections) {
|
|
3068
|
+
validateSubtreeForForbidden(section.content, ACCORDION_FORBIDDEN_CHILD_TYPES, strict);
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
function validateCompoundChildren(parent, children, strict) {
|
|
3072
|
+
if (!isDevEnvironment4() && !strict) return;
|
|
3073
|
+
validateNode(parent, children, 0, strict);
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
// src/compound/warnPersistence.ts
|
|
3077
|
+
var DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID = "assessment-sequence";
|
|
3078
|
+
function warnSharedCompoundStorageKey(opts) {
|
|
3079
|
+
if (!opts.persistEnabled || opts.hasExplicitBlockId || !isDevEnvironment4()) return;
|
|
3080
|
+
console.warn(
|
|
3081
|
+
`[lessonkit] <${opts.componentName}> without blockId shares one sessionStorage key when persistCompoundState is enabled; set a unique blockId per instance.`
|
|
3082
|
+
);
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
// src/blocks/AssessmentSequence.tsx
|
|
3086
|
+
import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
3087
|
+
var AssessmentSequenceInner = forwardRef7(
|
|
3088
|
+
function AssessmentSequenceInner2(props, ref) {
|
|
3089
|
+
const { compoundId, childArray, index, setIndex, persistEnabled } = props;
|
|
3090
|
+
const sequential = props.sequential !== false;
|
|
3091
|
+
const { config } = useLessonkit();
|
|
3092
|
+
const { visibleIndex, goNext, goPrev, progress } = useCompoundShell({
|
|
3093
|
+
courseId: config.courseId,
|
|
3094
|
+
compoundId,
|
|
3095
|
+
pageCount: childArray.length,
|
|
3096
|
+
index,
|
|
3097
|
+
setIndex,
|
|
3098
|
+
persistEnabled,
|
|
3099
|
+
ref,
|
|
3100
|
+
enableSolutionsButton: props.enableSolutionsButton
|
|
3101
|
+
});
|
|
3102
|
+
validateCompoundChildren("AssessmentSequence", props.children);
|
|
3103
|
+
if (!sequential) {
|
|
3104
|
+
return /* @__PURE__ */ jsx13("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
|
|
3105
|
+
}
|
|
3106
|
+
return /* @__PURE__ */ jsxs9("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
|
|
3107
|
+
/* @__PURE__ */ jsxs9("p", { children: [
|
|
3108
|
+
"Question ",
|
|
3109
|
+
progress.current,
|
|
3110
|
+
" of ",
|
|
3111
|
+
progress.total
|
|
3112
|
+
] }),
|
|
3113
|
+
/* @__PURE__ */ jsx13("div", { "data-testid": "assessment-sequence-step", children: childArray.map((child, i) => /* @__PURE__ */ jsx13("div", { hidden: i !== visibleIndex, children: /* @__PURE__ */ jsx13(CompoundPageIndexProvider, { pageIndex: i, children: child }) }, child.key ?? i)) }),
|
|
3114
|
+
/* @__PURE__ */ jsxs9("nav", { "aria-label": "Sequence navigation", children: [
|
|
3115
|
+
/* @__PURE__ */ jsx13(
|
|
3116
|
+
"button",
|
|
3117
|
+
{
|
|
3118
|
+
type: "button",
|
|
3119
|
+
"data-testid": "sequence-prev",
|
|
3120
|
+
disabled: visibleIndex === 0 || childArray.length === 0,
|
|
3121
|
+
onClick: goPrev,
|
|
3122
|
+
children: "Previous"
|
|
3123
|
+
}
|
|
3124
|
+
),
|
|
3125
|
+
/* @__PURE__ */ jsx13(
|
|
3126
|
+
"button",
|
|
3127
|
+
{
|
|
3128
|
+
type: "button",
|
|
3129
|
+
"data-testid": "sequence-next",
|
|
3130
|
+
disabled: visibleIndex >= childArray.length - 1 || childArray.length === 0,
|
|
3131
|
+
onClick: goNext,
|
|
3132
|
+
children: "Next"
|
|
3133
|
+
}
|
|
3134
|
+
)
|
|
3135
|
+
] })
|
|
3136
|
+
] });
|
|
3137
|
+
}
|
|
3138
|
+
);
|
|
3139
|
+
var AssessmentSequence = forwardRef7(
|
|
3140
|
+
function AssessmentSequence2(props, ref) {
|
|
3141
|
+
const reactInstanceId = useId3();
|
|
3142
|
+
const autoCompoundIdRef = useRef13(null);
|
|
3143
|
+
if (!props.blockId && !autoCompoundIdRef.current) {
|
|
3144
|
+
autoCompoundIdRef.current = deriveId(`assessment-sequence-${reactInstanceId}`);
|
|
3145
|
+
}
|
|
3146
|
+
const compoundId = useMemo13(
|
|
3147
|
+
() => props.blockId ? normalizeComponentId(props.blockId, "blockId") : autoCompoundIdRef.current ?? DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
|
|
3148
|
+
[props.blockId]
|
|
3149
|
+
);
|
|
3150
|
+
const childArray = React16.Children.toArray(props.children).filter(
|
|
3151
|
+
React16.isValidElement
|
|
3152
|
+
);
|
|
3153
|
+
const { config, storage } = useLessonkit();
|
|
3154
|
+
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
3155
|
+
useEffect12(() => {
|
|
3156
|
+
warnSharedCompoundStorageKey({
|
|
3157
|
+
persistEnabled,
|
|
3158
|
+
hasExplicitBlockId: Boolean(props.blockId),
|
|
3159
|
+
componentName: "AssessmentSequence"
|
|
3160
|
+
});
|
|
3161
|
+
}, [persistEnabled, props.blockId]);
|
|
3162
|
+
const initialIndex = useCompoundInitialIndex({
|
|
3163
|
+
courseId: config.courseId,
|
|
3164
|
+
compoundId,
|
|
3165
|
+
pageCount: childArray.length,
|
|
3166
|
+
persistEnabled,
|
|
3167
|
+
storage
|
|
3168
|
+
});
|
|
3169
|
+
const [index, setIndex] = useState10(initialIndex);
|
|
3170
|
+
const setIndexStable = useCallback7((i) => setIndex(i), []);
|
|
3171
|
+
useEffect12(() => {
|
|
3172
|
+
setIndex(initialIndex);
|
|
3173
|
+
}, [config.courseId, compoundId, initialIndex]);
|
|
3174
|
+
return /* @__PURE__ */ jsx13(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx13(
|
|
3175
|
+
AssessmentSequenceInner,
|
|
3176
|
+
{
|
|
3177
|
+
...props,
|
|
3178
|
+
ref,
|
|
3179
|
+
compoundId,
|
|
3180
|
+
childArray,
|
|
3181
|
+
index,
|
|
3182
|
+
setIndex,
|
|
3183
|
+
persistEnabled
|
|
3184
|
+
}
|
|
3185
|
+
) });
|
|
3186
|
+
}
|
|
3187
|
+
);
|
|
3188
|
+
setLessonkitBlockType(AssessmentSequence, "AssessmentSequence");
|
|
3189
|
+
|
|
3190
|
+
// src/blocks/Text.tsx
|
|
3191
|
+
import "react";
|
|
3192
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
3193
|
+
function Text(props) {
|
|
3194
|
+
return /* @__PURE__ */ jsx14("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
|
|
3195
|
+
}
|
|
3196
|
+
setLessonkitBlockType(Text, "Text");
|
|
3197
|
+
|
|
3198
|
+
// src/blocks/Heading.tsx
|
|
3199
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
3200
|
+
function Heading(props) {
|
|
3201
|
+
const Tag = `h${props.level}`;
|
|
3202
|
+
return /* @__PURE__ */ jsx15(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
|
|
3203
|
+
}
|
|
3204
|
+
setLessonkitBlockType(Heading, "Heading");
|
|
3205
|
+
|
|
3206
|
+
// src/blocks/Image.tsx
|
|
3207
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
3208
|
+
function Image(props) {
|
|
3209
|
+
return /* @__PURE__ */ jsx16(
|
|
3210
|
+
"img",
|
|
3211
|
+
{
|
|
3212
|
+
src: props.src,
|
|
3213
|
+
alt: props.alt,
|
|
3214
|
+
"data-lk-block-id": props.blockId,
|
|
3215
|
+
"data-testid": props.blockId ? `image-${props.blockId}` : "image",
|
|
3216
|
+
style: { maxWidth: "100%", height: "auto" }
|
|
3217
|
+
}
|
|
3218
|
+
);
|
|
3219
|
+
}
|
|
3220
|
+
setLessonkitBlockType(Image, "Image");
|
|
3221
|
+
|
|
3222
|
+
// src/blocks/Page.tsx
|
|
3223
|
+
import { useEffect as useEffect13 } from "react";
|
|
3224
|
+
import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3225
|
+
function Page(props) {
|
|
3226
|
+
validateCompoundChildren("Page", props.children);
|
|
3227
|
+
const { track } = useLessonkit();
|
|
3228
|
+
const lessonId = useEnclosingLessonId();
|
|
3229
|
+
useEffect13(() => {
|
|
3230
|
+
if (props.hidden || !lessonId || props.parentType) return;
|
|
3231
|
+
track(
|
|
3232
|
+
"compound_page_viewed",
|
|
3233
|
+
{
|
|
3234
|
+
blockId: props.blockId,
|
|
3235
|
+
pageIndex: props.pageIndex ?? 0,
|
|
3236
|
+
parentType: props.parentType
|
|
3237
|
+
},
|
|
3238
|
+
{ lessonId }
|
|
3239
|
+
);
|
|
3240
|
+
}, [props.hidden, props.pageIndex, props.parentType, props.blockId, lessonId, track]);
|
|
3241
|
+
return /* @__PURE__ */ jsxs10(
|
|
3242
|
+
"section",
|
|
3243
|
+
{
|
|
3244
|
+
"aria-label": props.title ?? "Page",
|
|
3245
|
+
"data-lk-block-id": props.blockId,
|
|
3246
|
+
"data-testid": `page-${props.blockId}`,
|
|
3247
|
+
hidden: props.hidden ? true : void 0,
|
|
3248
|
+
children: [
|
|
3249
|
+
props.title ? /* @__PURE__ */ jsx17("h3", { children: props.title }) : null,
|
|
3250
|
+
/* @__PURE__ */ jsx17(CompoundPageIndexProvider, { pageIndex: props.pageIndex ?? 0, children: /* @__PURE__ */ jsx17("div", { children: props.children }) })
|
|
3251
|
+
]
|
|
3252
|
+
}
|
|
3253
|
+
);
|
|
3254
|
+
}
|
|
3255
|
+
setLessonkitBlockType(Page, "Page");
|
|
3256
|
+
|
|
3257
|
+
// src/blocks/InteractiveBook.tsx
|
|
3258
|
+
import React19, { forwardRef as forwardRef8, useCallback as useCallback8, useEffect as useEffect14, useMemo as useMemo14, useState as useState11 } from "react";
|
|
3259
|
+
import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3260
|
+
var InteractiveBookInner = forwardRef8(
|
|
3261
|
+
function InteractiveBookInner2(props, ref) {
|
|
3262
|
+
const { blockId, pages, index, setIndex, persistEnabled } = props;
|
|
3263
|
+
validateCompoundChildren("InteractiveBook", pages);
|
|
3264
|
+
const { config, track } = useLessonkit();
|
|
3265
|
+
const lessonId = useEnclosingLessonId();
|
|
3266
|
+
const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
|
|
3267
|
+
courseId: config.courseId,
|
|
3268
|
+
compoundId: blockId,
|
|
3269
|
+
pageCount: pages.length,
|
|
3270
|
+
index,
|
|
3271
|
+
setIndex,
|
|
3272
|
+
persistEnabled,
|
|
3273
|
+
ref
|
|
3274
|
+
});
|
|
3275
|
+
const pageTitles = useMemo14(
|
|
3276
|
+
() => pages.map((page) => page.props.title),
|
|
3277
|
+
[pages]
|
|
3278
|
+
);
|
|
3279
|
+
useEffect14(() => {
|
|
3280
|
+
if (!lessonId || pages.length === 0) return;
|
|
3281
|
+
track(
|
|
3282
|
+
"book_page_viewed",
|
|
3283
|
+
{
|
|
3284
|
+
blockId,
|
|
3285
|
+
pageIndex: visibleIndex,
|
|
3286
|
+
pageTitle: pageTitles[visibleIndex]
|
|
3287
|
+
},
|
|
3288
|
+
{ lessonId }
|
|
3289
|
+
);
|
|
3290
|
+
}, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
|
|
3291
|
+
return /* @__PURE__ */ jsxs11("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
|
|
3292
|
+
/* @__PURE__ */ jsx18("h3", { children: props.title }),
|
|
3293
|
+
/* @__PURE__ */ jsxs11("p", { children: [
|
|
3294
|
+
"Page ",
|
|
3295
|
+
progress.current,
|
|
3296
|
+
" of ",
|
|
3297
|
+
progress.total
|
|
3298
|
+
] }),
|
|
3299
|
+
props.showBookScore && ctx ? /* @__PURE__ */ jsxs11("p", { "data-testid": "book-score", children: [
|
|
3300
|
+
"Score: ",
|
|
3301
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
3302
|
+
" /",
|
|
3303
|
+
" ",
|
|
3304
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
3305
|
+
] }) : null,
|
|
3306
|
+
/* @__PURE__ */ jsx18("div", { "data-testid": "interactive-book-page", children: pages.map(
|
|
3307
|
+
(page, i) => React19.cloneElement(page, {
|
|
3308
|
+
key: page.key ?? page.props.blockId,
|
|
3309
|
+
hidden: i !== visibleIndex,
|
|
3310
|
+
pageIndex: i,
|
|
3311
|
+
parentType: "InteractiveBook"
|
|
3312
|
+
})
|
|
3313
|
+
) }),
|
|
3314
|
+
/* @__PURE__ */ jsxs11("nav", { "aria-label": "Book navigation", children: [
|
|
3315
|
+
/* @__PURE__ */ jsx18(
|
|
3316
|
+
"button",
|
|
3317
|
+
{
|
|
3318
|
+
type: "button",
|
|
3319
|
+
"data-testid": "book-prev",
|
|
3320
|
+
disabled: visibleIndex === 0 || pages.length === 0,
|
|
3321
|
+
onClick: goPrev,
|
|
3322
|
+
children: "Previous"
|
|
3323
|
+
}
|
|
3324
|
+
),
|
|
3325
|
+
/* @__PURE__ */ jsx18(
|
|
3326
|
+
"button",
|
|
3327
|
+
{
|
|
3328
|
+
type: "button",
|
|
3329
|
+
"data-testid": "book-next",
|
|
3330
|
+
disabled: visibleIndex >= pages.length - 1 || pages.length === 0,
|
|
3331
|
+
onClick: goNext,
|
|
3332
|
+
children: "Next"
|
|
3333
|
+
}
|
|
3334
|
+
)
|
|
3335
|
+
] })
|
|
3336
|
+
] });
|
|
3337
|
+
}
|
|
3338
|
+
);
|
|
3339
|
+
var InteractiveBook = forwardRef8(function InteractiveBook2(props, ref) {
|
|
3340
|
+
const blockId = useMemo14(
|
|
3341
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3342
|
+
[props.blockId]
|
|
3343
|
+
);
|
|
3344
|
+
const pages = React19.Children.toArray(props.children).filter(
|
|
3345
|
+
React19.isValidElement
|
|
3346
|
+
);
|
|
3347
|
+
const { config, storage } = useLessonkit();
|
|
3348
|
+
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
3349
|
+
const initialIndex = useCompoundInitialIndex({
|
|
3350
|
+
courseId: config.courseId,
|
|
3351
|
+
compoundId: blockId,
|
|
3352
|
+
pageCount: pages.length,
|
|
3353
|
+
persistEnabled,
|
|
3354
|
+
storage
|
|
3355
|
+
});
|
|
3356
|
+
const [index, setIndex] = useState11(initialIndex);
|
|
3357
|
+
const setIndexStable = useCallback8((i) => setIndex(i), []);
|
|
3358
|
+
useEffect14(() => {
|
|
3359
|
+
setIndex(initialIndex);
|
|
3360
|
+
}, [config.courseId, blockId, initialIndex]);
|
|
3361
|
+
return /* @__PURE__ */ jsx18(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx18(
|
|
3362
|
+
InteractiveBookInner,
|
|
3363
|
+
{
|
|
3364
|
+
...props,
|
|
3365
|
+
ref,
|
|
3366
|
+
blockId,
|
|
3367
|
+
pages,
|
|
3368
|
+
index,
|
|
3369
|
+
setIndex,
|
|
3370
|
+
persistEnabled
|
|
3371
|
+
}
|
|
3372
|
+
) });
|
|
3373
|
+
});
|
|
3374
|
+
setLessonkitBlockType(InteractiveBook, "InteractiveBook");
|
|
3375
|
+
|
|
3376
|
+
// src/blocks/Slide.tsx
|
|
3377
|
+
import { useEffect as useEffect15 } from "react";
|
|
3378
|
+
import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3379
|
+
function Slide(props) {
|
|
3380
|
+
validateCompoundChildren("Slide", props.children);
|
|
3381
|
+
const { track } = useLessonkit();
|
|
3382
|
+
const lessonId = useEnclosingLessonId();
|
|
3383
|
+
useEffect15(() => {
|
|
3384
|
+
if (props.hidden || !lessonId || props.parentType) return;
|
|
3385
|
+
track(
|
|
3386
|
+
"compound_page_viewed",
|
|
3387
|
+
{
|
|
3388
|
+
blockId: props.blockId,
|
|
3389
|
+
pageIndex: props.slideIndex ?? 0,
|
|
3390
|
+
parentType: props.parentType
|
|
3391
|
+
},
|
|
3392
|
+
{ lessonId }
|
|
3393
|
+
);
|
|
3394
|
+
}, [props.hidden, props.slideIndex, props.parentType, props.blockId, lessonId, track]);
|
|
3395
|
+
return /* @__PURE__ */ jsxs12(
|
|
3396
|
+
"section",
|
|
3397
|
+
{
|
|
3398
|
+
"aria-label": props.title ?? "Slide",
|
|
3399
|
+
"data-lk-block-id": props.blockId,
|
|
3400
|
+
"data-testid": `slide-${props.blockId}`,
|
|
3401
|
+
hidden: props.hidden ? true : void 0,
|
|
3402
|
+
children: [
|
|
3403
|
+
props.title ? /* @__PURE__ */ jsx19("h3", { children: props.title }) : null,
|
|
3404
|
+
/* @__PURE__ */ jsx19(CompoundPageIndexProvider, { pageIndex: props.slideIndex ?? 0, children: /* @__PURE__ */ jsx19("div", { children: props.children }) })
|
|
3405
|
+
]
|
|
3406
|
+
}
|
|
3407
|
+
);
|
|
3408
|
+
}
|
|
3409
|
+
setLessonkitBlockType(Slide, "Slide");
|
|
3410
|
+
|
|
3411
|
+
// src/blocks/SlideDeck.tsx
|
|
3412
|
+
import React21, { forwardRef as forwardRef9, useCallback as useCallback9, useEffect as useEffect17, useMemo as useMemo15, useRef as useRef14, useState as useState12 } from "react";
|
|
3413
|
+
|
|
3414
|
+
// src/compound/useCompoundKeyboardNav.ts
|
|
3415
|
+
import { useEffect as useEffect16 } from "react";
|
|
3416
|
+
var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT", "BUTTON"]);
|
|
3417
|
+
function isEditableTarget(target) {
|
|
3418
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
3419
|
+
if (INTERACTIVE_TAGS.has(target.tagName)) return true;
|
|
3420
|
+
if (target.isContentEditable) return true;
|
|
3421
|
+
if (target.closest("[role='slider'], [role='listbox'], [data-lk-assessment-interactive]")) {
|
|
3422
|
+
return true;
|
|
3423
|
+
}
|
|
3424
|
+
return false;
|
|
3425
|
+
}
|
|
3426
|
+
function useCompoundKeyboardNav(opts) {
|
|
3427
|
+
const { containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex } = opts;
|
|
3428
|
+
useEffect16(() => {
|
|
3429
|
+
const el = containerRef.current;
|
|
3430
|
+
if (!el || pageCount === 0) return;
|
|
3431
|
+
const onKeyDown = (event) => {
|
|
3432
|
+
if (!el.contains(document.activeElement) && document.activeElement !== document.body) {
|
|
3433
|
+
return;
|
|
3434
|
+
}
|
|
3435
|
+
if (isEditableTarget(event.target)) return;
|
|
3436
|
+
switch (event.key) {
|
|
3437
|
+
case "ArrowRight":
|
|
3438
|
+
case "ArrowDown":
|
|
3439
|
+
if (visibleIndex < pageCount - 1) {
|
|
3440
|
+
event.preventDefault();
|
|
3441
|
+
goNext();
|
|
3442
|
+
}
|
|
3443
|
+
break;
|
|
3444
|
+
case "ArrowLeft":
|
|
3445
|
+
case "ArrowUp":
|
|
3446
|
+
if (visibleIndex > 0) {
|
|
3447
|
+
event.preventDefault();
|
|
3448
|
+
goPrev();
|
|
3449
|
+
}
|
|
3450
|
+
break;
|
|
3451
|
+
case "Home":
|
|
3452
|
+
if (visibleIndex !== 0) {
|
|
3453
|
+
event.preventDefault();
|
|
3454
|
+
setIndex(0);
|
|
3455
|
+
}
|
|
3456
|
+
break;
|
|
3457
|
+
case "End":
|
|
3458
|
+
if (visibleIndex !== pageCount - 1) {
|
|
3459
|
+
event.preventDefault();
|
|
3460
|
+
setIndex(pageCount - 1);
|
|
3461
|
+
}
|
|
3462
|
+
break;
|
|
3463
|
+
default:
|
|
3464
|
+
break;
|
|
3465
|
+
}
|
|
3466
|
+
};
|
|
3467
|
+
el.addEventListener("keydown", onKeyDown);
|
|
3468
|
+
return () => el.removeEventListener("keydown", onKeyDown);
|
|
3469
|
+
}, [containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex]);
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
// src/blocks/SlideDeck.tsx
|
|
3473
|
+
import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3474
|
+
var SlideDeckInner = forwardRef9(function SlideDeckInner2(props, ref) {
|
|
3475
|
+
const { blockId, slides, index, setIndex, persistEnabled } = props;
|
|
3476
|
+
validateCompoundChildren("SlideDeck", slides);
|
|
3477
|
+
const { config, track } = useLessonkit();
|
|
3478
|
+
const lessonId = useEnclosingLessonId();
|
|
3479
|
+
const containerRef = useRef14(null);
|
|
3480
|
+
const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
|
|
3481
|
+
courseId: config.courseId,
|
|
3482
|
+
compoundId: blockId,
|
|
3483
|
+
pageCount: slides.length,
|
|
3484
|
+
index,
|
|
3485
|
+
setIndex,
|
|
3486
|
+
persistEnabled,
|
|
3487
|
+
ref
|
|
3488
|
+
});
|
|
3489
|
+
const setIndexStable = useCallback9((i) => setIndex(i), [setIndex]);
|
|
3490
|
+
useCompoundKeyboardNav({
|
|
3491
|
+
containerRef,
|
|
3492
|
+
visibleIndex,
|
|
3493
|
+
pageCount: slides.length,
|
|
3494
|
+
goNext,
|
|
3495
|
+
goPrev,
|
|
3496
|
+
setIndex: setIndexStable
|
|
3497
|
+
});
|
|
3498
|
+
const slideTitles = useMemo15(
|
|
3499
|
+
() => slides.map((slide) => slide.props.title),
|
|
3500
|
+
[slides]
|
|
3501
|
+
);
|
|
3502
|
+
useEffect17(() => {
|
|
3503
|
+
if (!lessonId || slides.length === 0) return;
|
|
3504
|
+
track(
|
|
3505
|
+
"slide_viewed",
|
|
3506
|
+
{
|
|
3507
|
+
blockId,
|
|
3508
|
+
slideIndex: visibleIndex,
|
|
3509
|
+
slideTitle: slideTitles[visibleIndex]
|
|
3510
|
+
},
|
|
3511
|
+
{ lessonId }
|
|
3512
|
+
);
|
|
3513
|
+
}, [visibleIndex, blockId, lessonId, slides.length, slideTitles, track]);
|
|
3514
|
+
return /* @__PURE__ */ jsxs13(
|
|
3515
|
+
"section",
|
|
3516
|
+
{
|
|
3517
|
+
ref: containerRef,
|
|
3518
|
+
tabIndex: -1,
|
|
3519
|
+
"aria-label": props.title,
|
|
3520
|
+
"data-testid": "slide-deck",
|
|
3521
|
+
"data-lk-block-id": blockId,
|
|
3522
|
+
children: [
|
|
3523
|
+
/* @__PURE__ */ jsx20("h3", { children: props.title }),
|
|
3524
|
+
/* @__PURE__ */ jsxs13("p", { children: [
|
|
3525
|
+
"Slide ",
|
|
3526
|
+
progress.current,
|
|
3527
|
+
" of ",
|
|
3528
|
+
progress.total
|
|
3529
|
+
] }),
|
|
3530
|
+
props.showDeckScore && ctx ? /* @__PURE__ */ jsxs13("p", { "data-testid": "deck-score", children: [
|
|
3531
|
+
"Score: ",
|
|
3532
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
3533
|
+
" /",
|
|
3534
|
+
" ",
|
|
3535
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
3536
|
+
] }) : null,
|
|
3537
|
+
/* @__PURE__ */ jsx20("div", { "data-testid": "slide-deck-slide", children: slides.map(
|
|
3538
|
+
(slide, i) => React21.cloneElement(slide, {
|
|
3539
|
+
key: slide.key ?? slide.props.blockId,
|
|
3540
|
+
hidden: i !== visibleIndex,
|
|
3541
|
+
slideIndex: i,
|
|
3542
|
+
parentType: "SlideDeck"
|
|
3543
|
+
})
|
|
3544
|
+
) }),
|
|
3545
|
+
/* @__PURE__ */ jsxs13("nav", { "aria-label": "Slide navigation", children: [
|
|
3546
|
+
/* @__PURE__ */ jsx20(
|
|
3547
|
+
"button",
|
|
3548
|
+
{
|
|
3549
|
+
type: "button",
|
|
3550
|
+
"data-testid": "slide-prev",
|
|
3551
|
+
disabled: visibleIndex === 0 || slides.length === 0,
|
|
3552
|
+
onClick: goPrev,
|
|
3553
|
+
children: "Previous slide"
|
|
3554
|
+
}
|
|
3555
|
+
),
|
|
3556
|
+
/* @__PURE__ */ jsx20(
|
|
3557
|
+
"button",
|
|
3558
|
+
{
|
|
3559
|
+
type: "button",
|
|
3560
|
+
"data-testid": "slide-next",
|
|
3561
|
+
disabled: visibleIndex >= slides.length - 1 || slides.length === 0,
|
|
3562
|
+
onClick: goNext,
|
|
3563
|
+
children: "Next slide"
|
|
3564
|
+
}
|
|
3565
|
+
)
|
|
3566
|
+
] })
|
|
3567
|
+
]
|
|
3568
|
+
}
|
|
3569
|
+
);
|
|
3570
|
+
});
|
|
3571
|
+
var SlideDeck = forwardRef9(function SlideDeck2(props, ref) {
|
|
3572
|
+
const blockId = useMemo15(
|
|
3573
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3574
|
+
[props.blockId]
|
|
3575
|
+
);
|
|
3576
|
+
const slides = React21.Children.toArray(props.children).filter(
|
|
3577
|
+
React21.isValidElement
|
|
3578
|
+
);
|
|
3579
|
+
const { config, storage } = useLessonkit();
|
|
3580
|
+
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
3581
|
+
const initialIndex = useCompoundInitialIndex({
|
|
3582
|
+
courseId: config.courseId,
|
|
3583
|
+
compoundId: blockId,
|
|
3584
|
+
pageCount: slides.length,
|
|
3585
|
+
persistEnabled,
|
|
3586
|
+
storage
|
|
3587
|
+
});
|
|
3588
|
+
const [index, setIndex] = useState12(initialIndex);
|
|
3589
|
+
const setIndexStable = useCallback9((i) => setIndex(i), []);
|
|
3590
|
+
useEffect17(() => {
|
|
3591
|
+
setIndex(initialIndex);
|
|
3592
|
+
}, [config.courseId, blockId, initialIndex]);
|
|
3593
|
+
return /* @__PURE__ */ jsx20(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx20(
|
|
3594
|
+
SlideDeckInner,
|
|
3595
|
+
{
|
|
3596
|
+
...props,
|
|
3597
|
+
ref,
|
|
3598
|
+
blockId,
|
|
3599
|
+
slides,
|
|
3600
|
+
index,
|
|
3601
|
+
setIndex,
|
|
3602
|
+
persistEnabled
|
|
3603
|
+
}
|
|
3604
|
+
) });
|
|
3605
|
+
});
|
|
3606
|
+
setLessonkitBlockType(SlideDeck, "SlideDeck");
|
|
3607
|
+
|
|
3608
|
+
// src/blocks/Accordion.tsx
|
|
3609
|
+
import { useId as useId4, useState as useState13 } from "react";
|
|
3610
|
+
import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
3611
|
+
function Accordion(props) {
|
|
3612
|
+
if (isDevEnvironment4()) {
|
|
3613
|
+
validateAccordionSections(props.sections);
|
|
3614
|
+
}
|
|
3615
|
+
const [open, setOpen] = useState13(/* @__PURE__ */ new Set());
|
|
3616
|
+
const { track } = useLessonkit();
|
|
3617
|
+
const lessonId = useEnclosingLessonId();
|
|
3618
|
+
const baseId = useId4();
|
|
3619
|
+
const toggle = (sectionId) => {
|
|
3620
|
+
setOpen((prev) => {
|
|
3621
|
+
const next = new Set(prev);
|
|
3622
|
+
const expanded = !next.has(sectionId);
|
|
3623
|
+
if (expanded) next.add(sectionId);
|
|
3624
|
+
else next.delete(sectionId);
|
|
3625
|
+
track(
|
|
3626
|
+
"accordion_section_toggled",
|
|
3627
|
+
{ blockId: props.blockId, sectionId, expanded },
|
|
3628
|
+
lessonId ? { lessonId } : void 0
|
|
3629
|
+
);
|
|
3630
|
+
return next;
|
|
3631
|
+
});
|
|
3632
|
+
};
|
|
3633
|
+
return /* @__PURE__ */ jsx21("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
|
|
3634
|
+
const expanded = open.has(section.id);
|
|
3635
|
+
const panelId = `${baseId}-${section.id}`;
|
|
3636
|
+
const triggerId = `${baseId}-trigger-${section.id}`;
|
|
3637
|
+
return /* @__PURE__ */ jsxs14("div", { "data-testid": `accordion-section-${section.id}`, children: [
|
|
3638
|
+
/* @__PURE__ */ jsx21("h4", { children: /* @__PURE__ */ jsx21(
|
|
3639
|
+
"button",
|
|
3640
|
+
{
|
|
3641
|
+
id: triggerId,
|
|
3642
|
+
type: "button",
|
|
3643
|
+
"aria-expanded": expanded,
|
|
3644
|
+
"aria-controls": panelId,
|
|
3645
|
+
"data-testid": `accordion-trigger-${section.id}`,
|
|
3646
|
+
onClick: () => toggle(section.id),
|
|
3647
|
+
children: section.title
|
|
3648
|
+
}
|
|
3649
|
+
) }),
|
|
3650
|
+
expanded ? /* @__PURE__ */ jsx21("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
|
|
3651
|
+
] }, section.id);
|
|
3652
|
+
}) });
|
|
3653
|
+
}
|
|
3654
|
+
setLessonkitBlockType(Accordion, "Accordion");
|
|
3655
|
+
|
|
3656
|
+
// src/blocks/DialogCards.tsx
|
|
3657
|
+
import { useState as useState14 } from "react";
|
|
3658
|
+
import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
3659
|
+
function DialogCards(props) {
|
|
3660
|
+
const [index, setIndex] = useState14(0);
|
|
3661
|
+
const [flipped, setFlipped] = useState14(false);
|
|
3662
|
+
const card = props.cards[index];
|
|
3663
|
+
if (!card) return null;
|
|
3664
|
+
return /* @__PURE__ */ jsxs15("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
|
|
3665
|
+
/* @__PURE__ */ jsxs15("p", { children: [
|
|
3666
|
+
"Card ",
|
|
3667
|
+
index + 1,
|
|
3668
|
+
" of ",
|
|
3669
|
+
props.cards.length
|
|
3670
|
+
] }),
|
|
3671
|
+
/* @__PURE__ */ jsx22(
|
|
3672
|
+
"button",
|
|
3673
|
+
{
|
|
3674
|
+
type: "button",
|
|
3675
|
+
"data-testid": "dialog-card-flip",
|
|
3676
|
+
"aria-pressed": flipped,
|
|
3677
|
+
onClick: () => setFlipped((f) => !f),
|
|
3678
|
+
style: { minHeight: "6rem", width: "100%" },
|
|
3679
|
+
children: flipped ? card.back : card.front
|
|
3680
|
+
}
|
|
3681
|
+
),
|
|
3682
|
+
/* @__PURE__ */ jsxs15("nav", { "aria-label": "Card navigation", children: [
|
|
3683
|
+
/* @__PURE__ */ jsx22(
|
|
3684
|
+
"button",
|
|
3685
|
+
{
|
|
3686
|
+
type: "button",
|
|
3687
|
+
"data-testid": "dialog-prev",
|
|
3688
|
+
disabled: index === 0,
|
|
3689
|
+
onClick: () => {
|
|
3690
|
+
setIndex((i) => i - 1);
|
|
3691
|
+
setFlipped(false);
|
|
3692
|
+
},
|
|
3693
|
+
children: "Previous"
|
|
3694
|
+
}
|
|
3695
|
+
),
|
|
3696
|
+
/* @__PURE__ */ jsx22(
|
|
3697
|
+
"button",
|
|
3698
|
+
{
|
|
3699
|
+
type: "button",
|
|
3700
|
+
"data-testid": "dialog-next",
|
|
3701
|
+
disabled: index >= props.cards.length - 1,
|
|
3702
|
+
onClick: () => {
|
|
3703
|
+
setIndex((i) => i + 1);
|
|
3704
|
+
setFlipped(false);
|
|
3705
|
+
},
|
|
3706
|
+
children: "Next"
|
|
3707
|
+
}
|
|
3708
|
+
)
|
|
3709
|
+
] })
|
|
3710
|
+
] });
|
|
3711
|
+
}
|
|
3712
|
+
setLessonkitBlockType(DialogCards, "DialogCards");
|
|
3713
|
+
|
|
3714
|
+
// src/blocks/Flashcards.tsx
|
|
3715
|
+
import { useState as useState15 } from "react";
|
|
3716
|
+
import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
3717
|
+
function Flashcards(props) {
|
|
3718
|
+
const [index, setIndex] = useState15(0);
|
|
3719
|
+
const [face, setFace] = useState15("front");
|
|
3720
|
+
const { track } = useLessonkit();
|
|
3721
|
+
const lessonId = useEnclosingLessonId();
|
|
3722
|
+
const card = props.cards[index];
|
|
3723
|
+
if (!card) return null;
|
|
3724
|
+
const flip = () => {
|
|
3725
|
+
const next = face === "front" ? "back" : "front";
|
|
3726
|
+
setFace(next);
|
|
3727
|
+
track(
|
|
3728
|
+
"flashcard_flipped",
|
|
3729
|
+
{ blockId: props.blockId, cardIndex: index, face: next },
|
|
3730
|
+
lessonId ? { lessonId } : void 0
|
|
3731
|
+
);
|
|
3732
|
+
};
|
|
3733
|
+
return /* @__PURE__ */ jsxs16("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
|
|
3734
|
+
/* @__PURE__ */ jsx23("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
|
|
3735
|
+
props.selfScore ? /* @__PURE__ */ jsx23("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
|
|
3736
|
+
/* @__PURE__ */ jsx23(
|
|
3737
|
+
"button",
|
|
3738
|
+
{
|
|
3739
|
+
type: "button",
|
|
3740
|
+
"data-testid": "flashcard-next",
|
|
3741
|
+
disabled: index >= props.cards.length - 1,
|
|
3742
|
+
onClick: () => {
|
|
3743
|
+
setIndex((i) => i + 1);
|
|
3744
|
+
setFace("front");
|
|
3745
|
+
},
|
|
3746
|
+
children: "Next card"
|
|
3747
|
+
}
|
|
3748
|
+
)
|
|
3749
|
+
] });
|
|
3750
|
+
}
|
|
3751
|
+
setLessonkitBlockType(Flashcards, "Flashcards");
|
|
3752
|
+
|
|
3753
|
+
// src/blocks/ImageHotspots.tsx
|
|
3754
|
+
import { useState as useState16 } from "react";
|
|
3755
|
+
import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
3756
|
+
function ImageHotspots(props) {
|
|
3757
|
+
const [active, setActive] = useState16(null);
|
|
3758
|
+
const { track } = useLessonkit();
|
|
3759
|
+
const lessonId = useEnclosingLessonId();
|
|
3760
|
+
const open = (hotspotId) => {
|
|
3761
|
+
setActive(hotspotId);
|
|
3762
|
+
track(
|
|
3763
|
+
"hotspot_opened",
|
|
3764
|
+
{ blockId: props.blockId, hotspotId },
|
|
3765
|
+
lessonId ? { lessonId } : void 0
|
|
3766
|
+
);
|
|
3767
|
+
};
|
|
3768
|
+
return /* @__PURE__ */ jsxs17("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
|
|
3769
|
+
/* @__PURE__ */ jsxs17("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
3770
|
+
/* @__PURE__ */ jsx24("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
3771
|
+
props.hotspots.map((h) => /* @__PURE__ */ jsx24(
|
|
3772
|
+
"button",
|
|
3773
|
+
{
|
|
3774
|
+
type: "button",
|
|
3775
|
+
"aria-expanded": active === h.id,
|
|
3776
|
+
"aria-label": h.label,
|
|
3777
|
+
"data-testid": `hotspot-${h.id}`,
|
|
3778
|
+
style: {
|
|
3779
|
+
position: "absolute",
|
|
3780
|
+
left: `${h.x}%`,
|
|
3781
|
+
top: `${h.y}%`,
|
|
3782
|
+
transform: "translate(-50%, -50%)"
|
|
3783
|
+
},
|
|
3784
|
+
onClick: () => open(h.id),
|
|
3785
|
+
children: "+"
|
|
3786
|
+
},
|
|
3787
|
+
h.id
|
|
3788
|
+
))
|
|
3789
|
+
] }),
|
|
3790
|
+
active ? /* @__PURE__ */ jsxs17("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
|
|
3791
|
+
props.hotspots.find((h) => h.id === active)?.content,
|
|
3792
|
+
/* @__PURE__ */ jsx24("button", { type: "button", onClick: () => setActive(null), children: "Close" })
|
|
3793
|
+
] }) : null
|
|
3794
|
+
] });
|
|
3795
|
+
}
|
|
3796
|
+
setLessonkitBlockType(ImageHotspots, "ImageHotspots");
|
|
3797
|
+
|
|
3798
|
+
// src/blocks/ImageSlider.tsx
|
|
3799
|
+
import { useState as useState17 } from "react";
|
|
3800
|
+
import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
3801
|
+
function ImageSlider(props) {
|
|
3802
|
+
const [index, setIndex] = useState17(0);
|
|
3803
|
+
const { track } = useLessonkit();
|
|
3804
|
+
const lessonId = useEnclosingLessonId();
|
|
3805
|
+
const slide = props.slides[index];
|
|
3806
|
+
if (!slide) return null;
|
|
3807
|
+
const goTo = (next) => {
|
|
3808
|
+
setIndex(next);
|
|
3809
|
+
track(
|
|
3810
|
+
"image_slider_changed",
|
|
3811
|
+
{ blockId: props.blockId, slideIndex: next },
|
|
3812
|
+
lessonId ? { lessonId } : void 0
|
|
3813
|
+
);
|
|
3814
|
+
};
|
|
3815
|
+
return /* @__PURE__ */ jsxs18("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
|
|
3816
|
+
/* @__PURE__ */ jsx25("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
|
|
3817
|
+
slide.caption ? /* @__PURE__ */ jsx25("p", { children: slide.caption }) : null,
|
|
3818
|
+
/* @__PURE__ */ jsxs18("nav", { "aria-label": "Slide navigation", children: [
|
|
3819
|
+
/* @__PURE__ */ jsx25(
|
|
3820
|
+
"button",
|
|
3821
|
+
{
|
|
3822
|
+
type: "button",
|
|
3823
|
+
"data-testid": "slider-prev",
|
|
3824
|
+
disabled: index === 0,
|
|
3825
|
+
onClick: () => goTo(index - 1),
|
|
3826
|
+
children: "Previous"
|
|
3827
|
+
}
|
|
3828
|
+
),
|
|
3829
|
+
/* @__PURE__ */ jsxs18("span", { children: [
|
|
3830
|
+
index + 1,
|
|
3831
|
+
" / ",
|
|
3832
|
+
props.slides.length
|
|
3833
|
+
] }),
|
|
3834
|
+
/* @__PURE__ */ jsx25(
|
|
3835
|
+
"button",
|
|
3836
|
+
{
|
|
3837
|
+
type: "button",
|
|
3838
|
+
"data-testid": "slider-next",
|
|
3839
|
+
disabled: index >= props.slides.length - 1,
|
|
3840
|
+
onClick: () => goTo(index + 1),
|
|
3841
|
+
children: "Next"
|
|
3842
|
+
}
|
|
3843
|
+
)
|
|
3844
|
+
] })
|
|
3845
|
+
] });
|
|
3846
|
+
}
|
|
3847
|
+
setLessonkitBlockType(ImageSlider, "ImageSlider");
|
|
3848
|
+
|
|
3849
|
+
// src/blocks/FindHotspot.tsx
|
|
3850
|
+
import { forwardRef as forwardRef10, useEffect as useEffect18, useMemo as useMemo16, useState as useState18 } from "react";
|
|
3851
|
+
import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
3852
|
+
var INTERACTION6 = "findHotspot";
|
|
3853
|
+
function FindHotspotInner(props, ref) {
|
|
3854
|
+
const checkId = useMemo16(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
3855
|
+
const [selected, setSelected] = useState18(null);
|
|
3856
|
+
const [checked, setChecked] = useState18(false);
|
|
3857
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
3858
|
+
const targetIdsKey = props.targets.map((t) => t.id).join("\0");
|
|
3859
|
+
useEffect18(() => {
|
|
3860
|
+
setSelected(null);
|
|
3861
|
+
setChecked(false);
|
|
3862
|
+
}, [checkId, props.correctTargetId, targetIdsKey]);
|
|
3863
|
+
const correct = selected === props.correctTargetId;
|
|
3864
|
+
const handle = useMemo16(
|
|
3865
|
+
() => buildAssessmentHandle({
|
|
3866
|
+
checkId,
|
|
3867
|
+
getScore: () => checked && correct ? 1 : 0,
|
|
3868
|
+
getMaxScore: () => 1,
|
|
3869
|
+
getAnswerGiven: () => selected !== null,
|
|
3870
|
+
resetTask: () => {
|
|
3871
|
+
setSelected(null);
|
|
3872
|
+
setChecked(false);
|
|
3873
|
+
},
|
|
3874
|
+
showSolutions: () => setSelected(props.correctTargetId),
|
|
3875
|
+
getXAPIData: () => ({
|
|
3876
|
+
checkId,
|
|
3877
|
+
interactionType: INTERACTION6,
|
|
3878
|
+
response: selected ?? void 0,
|
|
3879
|
+
correct: checked ? correct : void 0,
|
|
3880
|
+
score: checked && correct ? 1 : 0,
|
|
3881
|
+
maxScore: 1
|
|
3882
|
+
}),
|
|
3883
|
+
getCurrentState: () => ({ selected, checked }),
|
|
3884
|
+
resume: (state) => {
|
|
3885
|
+
const nextSelected = readStringField(state, "selected");
|
|
3886
|
+
if (typeof nextSelected === "string" || nextSelected === null) {
|
|
3887
|
+
const valid = nextSelected === null || props.targets.some((t) => t.id === nextSelected);
|
|
3888
|
+
setSelected(valid ? nextSelected : null);
|
|
3889
|
+
}
|
|
3890
|
+
readBooleanStateField(state, "checked", setChecked);
|
|
3891
|
+
}
|
|
3892
|
+
}),
|
|
3893
|
+
[checkId, selected, checked, correct, props.correctTargetId, props.targets]
|
|
3894
|
+
);
|
|
3895
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3896
|
+
const selectTarget = (id) => {
|
|
3897
|
+
setSelected(id);
|
|
3898
|
+
setChecked(false);
|
|
3899
|
+
};
|
|
3900
|
+
const submit = () => {
|
|
3901
|
+
if (!selected || checked) return;
|
|
3902
|
+
setChecked(true);
|
|
3903
|
+
assessment.answer({
|
|
3904
|
+
checkId,
|
|
3905
|
+
interactionType: INTERACTION6,
|
|
3906
|
+
response: selected,
|
|
3907
|
+
correct
|
|
3908
|
+
});
|
|
3909
|
+
if (correct) {
|
|
3910
|
+
assessment.complete({
|
|
3911
|
+
checkId,
|
|
3912
|
+
interactionType: INTERACTION6,
|
|
3913
|
+
score: 1,
|
|
3914
|
+
maxScore: 1,
|
|
3915
|
+
passingScore: props.passingScore ?? 1
|
|
3916
|
+
});
|
|
3917
|
+
}
|
|
3918
|
+
};
|
|
3919
|
+
return /* @__PURE__ */ jsxs19("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
|
|
3920
|
+
/* @__PURE__ */ jsxs19("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
3921
|
+
/* @__PURE__ */ jsx26("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
3922
|
+
props.targets.map((t) => /* @__PURE__ */ jsx26(
|
|
3923
|
+
"button",
|
|
3924
|
+
{
|
|
3925
|
+
type: "button",
|
|
3926
|
+
"aria-label": t.label,
|
|
3927
|
+
"aria-pressed": selected === t.id,
|
|
3928
|
+
"data-testid": `target-${t.id}`,
|
|
1935
3929
|
style: {
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
margin: "0 0.2em"
|
|
3930
|
+
position: "absolute",
|
|
3931
|
+
left: `${t.x}%`,
|
|
3932
|
+
top: `${t.y}%`,
|
|
3933
|
+
transform: "translate(-50%, -50%)"
|
|
1941
3934
|
},
|
|
1942
|
-
|
|
3935
|
+
onClick: () => selectTarget(t.id),
|
|
3936
|
+
children: t.label
|
|
1943
3937
|
},
|
|
1944
|
-
|
|
1945
|
-
)
|
|
1946
|
-
|
|
1947
|
-
/* @__PURE__ */
|
|
1948
|
-
|
|
1949
|
-
allFilled ? /* @__PURE__ */ jsx8("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
|
|
3938
|
+
t.id
|
|
3939
|
+
))
|
|
3940
|
+
] }),
|
|
3941
|
+
/* @__PURE__ */ jsx26("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
|
|
3942
|
+
checked ? /* @__PURE__ */ jsx26("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
1950
3943
|
] });
|
|
1951
3944
|
}
|
|
1952
|
-
var
|
|
1953
|
-
var
|
|
1954
|
-
return /* @__PURE__ */
|
|
3945
|
+
var FindHotspotInnerForwarded = forwardRef10(FindHotspotInner);
|
|
3946
|
+
var FindHotspot = forwardRef10(function FindHotspot2(props, ref) {
|
|
3947
|
+
return /* @__PURE__ */ jsx26(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx26(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
1955
3948
|
});
|
|
3949
|
+
setLessonkitBlockType(FindHotspot, "FindHotspot");
|
|
1956
3950
|
|
|
1957
|
-
// src/blocks/
|
|
1958
|
-
import { forwardRef as
|
|
1959
|
-
import { jsx as
|
|
1960
|
-
var
|
|
1961
|
-
function
|
|
1962
|
-
const checkId =
|
|
3951
|
+
// src/blocks/FindMultipleHotspots.tsx
|
|
3952
|
+
import { forwardRef as forwardRef11, useMemo as useMemo17, useState as useState19 } from "react";
|
|
3953
|
+
import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
3954
|
+
var INTERACTION7 = "findMultipleHotspots";
|
|
3955
|
+
function FindMultipleHotspotsInner(props, ref) {
|
|
3956
|
+
const checkId = useMemo17(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
3957
|
+
const [selected, setSelected] = useState19(/* @__PURE__ */ new Set());
|
|
3958
|
+
const [checked, setChecked] = useState19(false);
|
|
1963
3959
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1964
|
-
const
|
|
1965
|
-
(
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
const completedRef = useRef8(false);
|
|
1971
|
-
const reset = () => {
|
|
1972
|
-
completedRef.current = false;
|
|
1973
|
-
setPassed(false);
|
|
1974
|
-
setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
|
|
1975
|
-
setPool(props.items.map((i) => i.id));
|
|
1976
|
-
setKeyboardItem(null);
|
|
1977
|
-
};
|
|
1978
|
-
useEffect8(() => {
|
|
1979
|
-
reset();
|
|
1980
|
-
}, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
|
|
1981
|
-
const allFilled = props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
|
|
1982
|
-
const allCorrect = props.targets.every((t) => assignments[t.id] === t.accepts);
|
|
1983
|
-
const handle = useMemo10(() => {
|
|
1984
|
-
const maxScore = props.targets.length || 1;
|
|
1985
|
-
let score = 0;
|
|
1986
|
-
props.targets.forEach((t) => {
|
|
1987
|
-
if (assignments[t.id] === t.accepts) score += 1;
|
|
3960
|
+
const toggle = (id) => {
|
|
3961
|
+
setSelected((prev) => {
|
|
3962
|
+
const next = new Set(prev);
|
|
3963
|
+
if (next.has(id)) next.delete(id);
|
|
3964
|
+
else next.add(id);
|
|
3965
|
+
return next;
|
|
1988
3966
|
});
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
3967
|
+
setChecked(false);
|
|
3968
|
+
};
|
|
3969
|
+
const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
|
|
3970
|
+
const handle = useMemo17(
|
|
3971
|
+
() => buildAssessmentHandle({
|
|
3972
|
+
checkId,
|
|
3973
|
+
getScore: () => checked && correct ? 1 : 0,
|
|
3974
|
+
getMaxScore: () => 1,
|
|
3975
|
+
getAnswerGiven: () => selected.size > 0,
|
|
3976
|
+
resetTask: () => {
|
|
3977
|
+
setSelected(/* @__PURE__ */ new Set());
|
|
3978
|
+
setChecked(false);
|
|
1995
3979
|
},
|
|
3980
|
+
showSolutions: () => setSelected(new Set(props.correctTargetIds)),
|
|
1996
3981
|
getXAPIData: () => ({
|
|
1997
3982
|
checkId,
|
|
1998
|
-
interactionType:
|
|
1999
|
-
response:
|
|
2000
|
-
correct:
|
|
2001
|
-
score,
|
|
2002
|
-
maxScore
|
|
2003
|
-
})
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
setKeyboardItem(null);
|
|
2018
|
-
};
|
|
2019
|
-
const check = () => {
|
|
2020
|
-
if (!allFilled) return;
|
|
3983
|
+
interactionType: INTERACTION7,
|
|
3984
|
+
response: [...selected],
|
|
3985
|
+
correct: checked ? correct : void 0,
|
|
3986
|
+
score: checked && correct ? 1 : 0,
|
|
3987
|
+
maxScore: 1
|
|
3988
|
+
}),
|
|
3989
|
+
getCurrentState: () => ({ selected: [...selected], checked }),
|
|
3990
|
+
resume: (state) => {
|
|
3991
|
+
const raw = state.selected;
|
|
3992
|
+
if (Array.isArray(raw)) setSelected(new Set(raw.filter((id) => typeof id === "string")));
|
|
3993
|
+
readBooleanStateField(state, "checked", setChecked);
|
|
3994
|
+
}
|
|
3995
|
+
}),
|
|
3996
|
+
[checkId, selected, checked, correct, props.correctTargetIds]
|
|
3997
|
+
);
|
|
3998
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3999
|
+
const submit = () => {
|
|
4000
|
+
if (selected.size === 0 || checked) return;
|
|
4001
|
+
setChecked(true);
|
|
2021
4002
|
assessment.answer({
|
|
2022
4003
|
checkId,
|
|
2023
|
-
interactionType:
|
|
2024
|
-
response:
|
|
2025
|
-
correct
|
|
4004
|
+
interactionType: INTERACTION7,
|
|
4005
|
+
response: [...selected],
|
|
4006
|
+
correct
|
|
2026
4007
|
});
|
|
2027
|
-
if (
|
|
2028
|
-
completedRef.current = true;
|
|
2029
|
-
setPassed(true);
|
|
4008
|
+
if (correct) {
|
|
2030
4009
|
assessment.complete({
|
|
2031
4010
|
checkId,
|
|
2032
|
-
interactionType:
|
|
2033
|
-
score:
|
|
2034
|
-
maxScore:
|
|
2035
|
-
passingScore: props.passingScore ??
|
|
4011
|
+
interactionType: INTERACTION7,
|
|
4012
|
+
score: 1,
|
|
4013
|
+
maxScore: 1,
|
|
4014
|
+
passingScore: props.passingScore ?? 1
|
|
2036
4015
|
});
|
|
2037
4016
|
}
|
|
2038
4017
|
};
|
|
2039
|
-
return /* @__PURE__ */
|
|
2040
|
-
/* @__PURE__ */
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
return /* @__PURE__ */ jsx9(
|
|
4018
|
+
return /* @__PURE__ */ jsxs20("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
|
|
4019
|
+
/* @__PURE__ */ jsxs20("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
4020
|
+
/* @__PURE__ */ jsx27("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
4021
|
+
props.targets.map((t) => /* @__PURE__ */ jsx27(
|
|
2044
4022
|
"button",
|
|
2045
4023
|
{
|
|
2046
4024
|
type: "button",
|
|
2047
|
-
|
|
2048
|
-
"
|
|
2049
|
-
"
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
4025
|
+
"aria-label": t.label,
|
|
4026
|
+
"aria-pressed": selected.has(t.id),
|
|
4027
|
+
"data-testid": `target-${t.id}`,
|
|
4028
|
+
style: {
|
|
4029
|
+
position: "absolute",
|
|
4030
|
+
left: `${t.x}%`,
|
|
4031
|
+
top: `${t.y}%`,
|
|
4032
|
+
transform: "translate(-50%, -50%)"
|
|
4033
|
+
},
|
|
4034
|
+
onClick: () => toggle(t.id),
|
|
4035
|
+
children: t.label
|
|
2054
4036
|
},
|
|
2055
|
-
id
|
|
2056
|
-
)
|
|
2057
|
-
|
|
2058
|
-
/* @__PURE__ */
|
|
2059
|
-
|
|
2060
|
-
const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
|
|
2061
|
-
return /* @__PURE__ */ jsxs7("li", { children: [
|
|
2062
|
-
/* @__PURE__ */ jsx9("strong", { children: target.label }),
|
|
2063
|
-
" ",
|
|
2064
|
-
/* @__PURE__ */ jsx9(
|
|
2065
|
-
"span",
|
|
2066
|
-
{
|
|
2067
|
-
role: "button",
|
|
2068
|
-
tabIndex: 0,
|
|
2069
|
-
"data-testid": `drop-${target.id}`,
|
|
2070
|
-
onDragOver: (e) => e.preventDefault(),
|
|
2071
|
-
onDrop: (e) => {
|
|
2072
|
-
e.preventDefault();
|
|
2073
|
-
const id = e.dataTransfer.getData("text/plain");
|
|
2074
|
-
if (id) place(target.id, id);
|
|
2075
|
-
},
|
|
2076
|
-
onClick: () => keyboardItem && place(target.id, keyboardItem),
|
|
2077
|
-
onKeyDown: (e) => {
|
|
2078
|
-
if (e.key === "Enter" && keyboardItem) place(target.id, keyboardItem);
|
|
2079
|
-
},
|
|
2080
|
-
style: {
|
|
2081
|
-
display: "inline-block",
|
|
2082
|
-
minWidth: "8em",
|
|
2083
|
-
border: "1px dashed currentColor",
|
|
2084
|
-
padding: "0.25em"
|
|
2085
|
-
},
|
|
2086
|
-
children: label
|
|
2087
|
-
}
|
|
2088
|
-
)
|
|
2089
|
-
] }, target.id);
|
|
2090
|
-
}) }),
|
|
2091
|
-
/* @__PURE__ */ jsx9("button", { type: "button", "data-testid": "check-drag-drop", disabled: !allFilled || passed, onClick: check, children: "Check" }),
|
|
2092
|
-
allFilled ? /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", children: passed || allCorrect ? "Correct" : "Try again" }) : null
|
|
4037
|
+
t.id
|
|
4038
|
+
))
|
|
4039
|
+
] }),
|
|
4040
|
+
/* @__PURE__ */ jsx27("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
|
|
4041
|
+
checked ? /* @__PURE__ */ jsx27("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
2093
4042
|
] });
|
|
2094
4043
|
}
|
|
2095
|
-
var
|
|
2096
|
-
var
|
|
2097
|
-
|
|
2098
|
-
});
|
|
2099
|
-
|
|
2100
|
-
// src/blocks/AssessmentSequence.tsx
|
|
2101
|
-
import React10, { useCallback as useCallback3, useMemo as useMemo11, useState as useState8 } from "react";
|
|
2102
|
-
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2103
|
-
function AssessmentSequence(props) {
|
|
2104
|
-
const sequential = props.sequential !== false;
|
|
2105
|
-
const childArray = React10.Children.toArray(props.children).filter(React10.isValidElement);
|
|
2106
|
-
const [index, setIndex] = useState8(0);
|
|
2107
|
-
const current = childArray[index] ?? null;
|
|
2108
|
-
const goNext = useCallback3(() => {
|
|
2109
|
-
setIndex((i) => Math.min(i + 1, childArray.length - 1));
|
|
2110
|
-
}, [childArray.length]);
|
|
2111
|
-
const goPrev = useCallback3(() => {
|
|
2112
|
-
setIndex((i) => Math.max(i - 1, 0));
|
|
2113
|
-
}, []);
|
|
2114
|
-
const progress = useMemo11(
|
|
2115
|
-
() => ({ current: index + 1, total: childArray.length }),
|
|
2116
|
-
[index, childArray.length]
|
|
2117
|
-
);
|
|
2118
|
-
if (!sequential) {
|
|
2119
|
-
return /* @__PURE__ */ jsx10(AssessmentSequenceProvider, { children: /* @__PURE__ */ jsx10("section", { "aria-label": "Assessment sequence", children: props.children }) });
|
|
4044
|
+
var FindMultipleHotspotsInnerForwarded = forwardRef11(FindMultipleHotspotsInner);
|
|
4045
|
+
var FindMultipleHotspots = forwardRef11(
|
|
4046
|
+
function FindMultipleHotspots2(props, ref) {
|
|
4047
|
+
return /* @__PURE__ */ jsx27(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx27(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
2120
4048
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
"Question ",
|
|
2124
|
-
progress.current,
|
|
2125
|
-
" of ",
|
|
2126
|
-
progress.total
|
|
2127
|
-
] }),
|
|
2128
|
-
/* @__PURE__ */ jsx10("div", { "data-testid": "assessment-sequence-step", children: current }),
|
|
2129
|
-
/* @__PURE__ */ jsxs8("nav", { "aria-label": "Sequence navigation", children: [
|
|
2130
|
-
/* @__PURE__ */ jsx10("button", { type: "button", "data-testid": "sequence-prev", disabled: index === 0, onClick: goPrev, children: "Previous" }),
|
|
2131
|
-
/* @__PURE__ */ jsx10(
|
|
2132
|
-
"button",
|
|
2133
|
-
{
|
|
2134
|
-
type: "button",
|
|
2135
|
-
"data-testid": "sequence-next",
|
|
2136
|
-
disabled: index >= childArray.length - 1,
|
|
2137
|
-
onClick: goNext,
|
|
2138
|
-
children: "Next"
|
|
2139
|
-
}
|
|
2140
|
-
)
|
|
2141
|
-
] })
|
|
2142
|
-
] }) });
|
|
2143
|
-
}
|
|
4049
|
+
);
|
|
4050
|
+
setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
|
|
2144
4051
|
|
|
2145
4052
|
// src/index.tsx
|
|
2146
4053
|
import {
|
|
@@ -2154,14 +4061,14 @@ import {
|
|
|
2154
4061
|
} from "@lessonkit/core";
|
|
2155
4062
|
|
|
2156
4063
|
// src/theme/ThemeProvider.tsx
|
|
2157
|
-
import
|
|
2158
|
-
createContext as
|
|
2159
|
-
useCallback as
|
|
2160
|
-
useContext as
|
|
4064
|
+
import React29, {
|
|
4065
|
+
createContext as createContext6,
|
|
4066
|
+
useCallback as useCallback10,
|
|
4067
|
+
useContext as useContext8,
|
|
2161
4068
|
useLayoutEffect as useLayoutEffect2,
|
|
2162
|
-
useMemo as
|
|
2163
|
-
useRef as
|
|
2164
|
-
useState as
|
|
4069
|
+
useMemo as useMemo18,
|
|
4070
|
+
useRef as useRef15,
|
|
4071
|
+
useState as useState20
|
|
2165
4072
|
} from "react";
|
|
2166
4073
|
import {
|
|
2167
4074
|
brandThemeOverrides,
|
|
@@ -2188,11 +4095,11 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
2188
4095
|
}
|
|
2189
4096
|
|
|
2190
4097
|
// src/theme/ThemeProvider.tsx
|
|
2191
|
-
import { jsx as
|
|
2192
|
-
var ThemeContext =
|
|
4098
|
+
import { jsx as jsx28 } from "react/jsx-runtime";
|
|
4099
|
+
var ThemeContext = createContext6(null);
|
|
2193
4100
|
var useIsoLayoutEffect2 = (
|
|
2194
4101
|
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
2195
|
-
typeof window !== "undefined" ? useLayoutEffect2 :
|
|
4102
|
+
typeof window !== "undefined" ? useLayoutEffect2 : React29.useEffect
|
|
2196
4103
|
);
|
|
2197
4104
|
function getSystemMode() {
|
|
2198
4105
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
@@ -2211,7 +4118,7 @@ function ThemeProvider(props) {
|
|
|
2211
4118
|
const preset = props.preset ?? "default";
|
|
2212
4119
|
const mode = props.mode ?? "light";
|
|
2213
4120
|
const targetKind = props.target ?? "document";
|
|
2214
|
-
const [resolvedMode, setResolvedMode] =
|
|
4121
|
+
const [resolvedMode, setResolvedMode] = useState20(
|
|
2215
4122
|
() => mode === "system" ? getSystemMode() : mode
|
|
2216
4123
|
);
|
|
2217
4124
|
useIsoLayoutEffect2(() => {
|
|
@@ -2227,20 +4134,20 @@ function ThemeProvider(props) {
|
|
|
2227
4134
|
return () => mq.removeEventListener("change", onChange);
|
|
2228
4135
|
}, [mode]);
|
|
2229
4136
|
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
2230
|
-
const effectiveTheme =
|
|
4137
|
+
const effectiveTheme = useMemo18(() => {
|
|
2231
4138
|
const modeBase = resolveModeBase(mode, dataTheme);
|
|
2232
4139
|
const base = preset === "default" ? modeBase : preset === "brand" ? mergeThemes(modeBase, brandThemeOverrides) : mergeThemes(modeBase, getPresetTheme(preset));
|
|
2233
4140
|
return mergeThemes(base, props.theme ?? {});
|
|
2234
4141
|
}, [preset, mode, dataTheme, props.theme]);
|
|
2235
|
-
const hostRef =
|
|
2236
|
-
const appliedKeysRef =
|
|
4142
|
+
const hostRef = useRef15(null);
|
|
4143
|
+
const appliedKeysRef = useRef15(/* @__PURE__ */ new Set());
|
|
2237
4144
|
useIsoLayoutEffect2(() => {
|
|
2238
4145
|
if (targetKind === "document" && typeof document !== "undefined") {
|
|
2239
4146
|
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
2240
4147
|
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
2241
4148
|
}
|
|
2242
4149
|
}, [targetKind, dataTheme]);
|
|
2243
|
-
const inject =
|
|
4150
|
+
const inject = useCallback10(() => {
|
|
2244
4151
|
const vars = themeToCssVariables(effectiveTheme);
|
|
2245
4152
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
2246
4153
|
if (!el) return;
|
|
@@ -2257,7 +4164,7 @@ function ThemeProvider(props) {
|
|
|
2257
4164
|
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
2258
4165
|
};
|
|
2259
4166
|
}, [inject, targetKind]);
|
|
2260
|
-
const value =
|
|
4167
|
+
const value = useMemo18(
|
|
2261
4168
|
() => ({
|
|
2262
4169
|
theme: effectiveTheme,
|
|
2263
4170
|
preset,
|
|
@@ -2267,21 +4174,331 @@ function ThemeProvider(props) {
|
|
|
2267
4174
|
[effectiveTheme, preset, mode, dataTheme]
|
|
2268
4175
|
);
|
|
2269
4176
|
if (targetKind === "document") {
|
|
2270
|
-
return /* @__PURE__ */
|
|
4177
|
+
return /* @__PURE__ */ jsx28(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx28("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
|
|
2271
4178
|
}
|
|
2272
|
-
return /* @__PURE__ */
|
|
4179
|
+
return /* @__PURE__ */ jsx28(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx28("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
2273
4180
|
}
|
|
2274
4181
|
function useTheme() {
|
|
2275
|
-
const ctx =
|
|
4182
|
+
const ctx = useContext8(ThemeContext);
|
|
2276
4183
|
if (!ctx) {
|
|
2277
4184
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
2278
4185
|
}
|
|
2279
4186
|
return ctx;
|
|
2280
4187
|
}
|
|
2281
4188
|
|
|
4189
|
+
// src/catalogV3Entries.ts
|
|
4190
|
+
import {
|
|
4191
|
+
ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
4192
|
+
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
4193
|
+
PAGE_ALLOWED_CHILD_TYPES,
|
|
4194
|
+
SLIDE_ALLOWED_CHILD_TYPES,
|
|
4195
|
+
SLIDE_DECK_ALLOWED_CHILD_TYPES,
|
|
4196
|
+
COMPOUND_MAX_NESTING_DEPTH as COMPOUND_MAX_NESTING_DEPTH2
|
|
4197
|
+
} from "@lessonkit/core";
|
|
4198
|
+
var COMPOUND_PARENTS = [
|
|
4199
|
+
"Lesson",
|
|
4200
|
+
"Page",
|
|
4201
|
+
"InteractiveBook",
|
|
4202
|
+
"Slide",
|
|
4203
|
+
"SlideDeck",
|
|
4204
|
+
"AssessmentSequence"
|
|
4205
|
+
];
|
|
4206
|
+
function extendParents(entry) {
|
|
4207
|
+
if (!entry.parentConstraints?.length) return entry;
|
|
4208
|
+
const merged = /* @__PURE__ */ new Set([...entry.parentConstraints, ...COMPOUND_PARENTS]);
|
|
4209
|
+
return { ...entry, parentConstraints: [...merged] };
|
|
4210
|
+
}
|
|
4211
|
+
var assessmentBehaviourProps = [
|
|
4212
|
+
{ name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
|
|
4213
|
+
{ name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
|
|
4214
|
+
{ name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
|
|
4215
|
+
{ name: "passingScore", type: "number", required: false, description: "Minimum score to pass." }
|
|
4216
|
+
];
|
|
4217
|
+
var v3CompoundAndContentEntries = [
|
|
4218
|
+
{
|
|
4219
|
+
type: "Text",
|
|
4220
|
+
category: "content",
|
|
4221
|
+
description: "Paragraph text content.",
|
|
4222
|
+
props: [
|
|
4223
|
+
{ name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
|
|
4224
|
+
{ name: "children", type: "ReactNode", required: true, description: "Text body." }
|
|
4225
|
+
],
|
|
4226
|
+
requiredIds: [],
|
|
4227
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
4228
|
+
a11y: { element: "p", ariaLabel: "Text", keyboard: "N/A", notes: "Semantic paragraph." },
|
|
4229
|
+
theming: { surface: "global-inherit", stylingNotes: "Inherits theme." },
|
|
4230
|
+
telemetry: { emits: [] }
|
|
4231
|
+
},
|
|
4232
|
+
{
|
|
4233
|
+
type: "Heading",
|
|
4234
|
+
category: "content",
|
|
4235
|
+
description: "Heading levels 1\u20133.",
|
|
4236
|
+
props: [
|
|
4237
|
+
{ name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
|
|
4238
|
+
{ name: "level", type: "1 | 2 | 3", required: true, description: "Heading level." },
|
|
4239
|
+
{ name: "children", type: "ReactNode", required: true, description: "Heading text." }
|
|
4240
|
+
],
|
|
4241
|
+
requiredIds: [],
|
|
4242
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
4243
|
+
a11y: { element: "h1-h3", ariaLabel: "Heading", keyboard: "N/A", notes: "Use one level per outline." },
|
|
4244
|
+
theming: { surface: "global-inherit", stylingNotes: "Inherits theme." },
|
|
4245
|
+
telemetry: { emits: [] }
|
|
4246
|
+
},
|
|
4247
|
+
{
|
|
4248
|
+
type: "Image",
|
|
4249
|
+
category: "content",
|
|
4250
|
+
description: "Image with required alt text.",
|
|
4251
|
+
props: [
|
|
4252
|
+
{ name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
|
|
4253
|
+
{ name: "src", type: "string", required: true, description: "Image URL." },
|
|
4254
|
+
{ name: "alt", type: "string", required: true, description: "Alt text." }
|
|
4255
|
+
],
|
|
4256
|
+
requiredIds: [],
|
|
4257
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
4258
|
+
a11y: { element: "img", ariaLabel: "Image", keyboard: "N/A", notes: "Requires alt." },
|
|
4259
|
+
theming: { surface: "global-inherit", stylingNotes: "Responsive max-width." },
|
|
4260
|
+
telemetry: { emits: [] }
|
|
4261
|
+
},
|
|
4262
|
+
{
|
|
4263
|
+
type: "Page",
|
|
4264
|
+
category: "container",
|
|
4265
|
+
compoundContract: true,
|
|
4266
|
+
h5pMachineName: "H5P.Column",
|
|
4267
|
+
h5pAlias: "Column",
|
|
4268
|
+
description: "Column layout container (H5P Column / Page).",
|
|
4269
|
+
allowedChildTypes: [...PAGE_ALLOWED_CHILD_TYPES],
|
|
4270
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.Page,
|
|
4271
|
+
props: [
|
|
4272
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4273
|
+
{ name: "title", type: "string", required: false, description: "Page title." },
|
|
4274
|
+
{ name: "children", type: "ReactNode", required: true, description: "Page content." }
|
|
4275
|
+
],
|
|
4276
|
+
requiredIds: [],
|
|
4277
|
+
optionalIds: ["blockId"],
|
|
4278
|
+
parentConstraints: ["Lesson", "InteractiveBook"],
|
|
4279
|
+
a11y: { element: "section", ariaLabel: "Page", keyboard: "N/A", notes: "H5P Column equivalent." },
|
|
4280
|
+
theming: { surface: "global-inherit", stylingNotes: "Container." },
|
|
4281
|
+
telemetry: { emits: ["compound_page_viewed"], requiresActiveLesson: true }
|
|
4282
|
+
},
|
|
4283
|
+
{
|
|
4284
|
+
type: "InteractiveBook",
|
|
4285
|
+
category: "container",
|
|
4286
|
+
compoundContract: true,
|
|
4287
|
+
h5pMachineName: "H5P.InteractiveBook",
|
|
4288
|
+
h5pAlias: "Interactive Book",
|
|
4289
|
+
description: "Multi-page book with chapter navigation.",
|
|
4290
|
+
allowedChildTypes: [...INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
|
|
4291
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.InteractiveBook,
|
|
4292
|
+
props: [
|
|
4293
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4294
|
+
{ name: "title", type: "string", required: true, description: "Book title." },
|
|
4295
|
+
{ name: "showBookScore", type: "boolean", required: false, description: "Show aggregate score." },
|
|
4296
|
+
{ name: "children", type: "Page[]", required: true, description: "Page chapters." }
|
|
4297
|
+
],
|
|
4298
|
+
requiredIds: ["blockId"],
|
|
4299
|
+
parentConstraints: ["Lesson"],
|
|
4300
|
+
a11y: {
|
|
4301
|
+
element: "section",
|
|
4302
|
+
ariaLabel: "Interactive book",
|
|
4303
|
+
keyboard: "Previous/Next chapter navigation.",
|
|
4304
|
+
notes: "H5P Interactive Book equivalent."
|
|
4305
|
+
},
|
|
4306
|
+
theming: { surface: "global-inherit", stylingNotes: "Book chrome." },
|
|
4307
|
+
telemetry: { emits: ["book_page_viewed"], requiresActiveLesson: true }
|
|
4308
|
+
},
|
|
4309
|
+
{
|
|
4310
|
+
type: "Slide",
|
|
4311
|
+
category: "container",
|
|
4312
|
+
compoundContract: true,
|
|
4313
|
+
h5pMachineName: "H5P.CoursePresentation",
|
|
4314
|
+
h5pAlias: "Course Presentation slide",
|
|
4315
|
+
description: "Single slide row in a SlideDeck. Planned allowlist expansion: Video, Summary.",
|
|
4316
|
+
allowedChildTypes: [...SLIDE_ALLOWED_CHILD_TYPES],
|
|
4317
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.Slide,
|
|
4318
|
+
props: [
|
|
4319
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4320
|
+
{ name: "title", type: "string", required: false, description: "Slide title." },
|
|
4321
|
+
{ name: "children", type: "ReactNode", required: true, description: "Slide content." }
|
|
4322
|
+
],
|
|
4323
|
+
requiredIds: [],
|
|
4324
|
+
optionalIds: ["blockId"],
|
|
4325
|
+
parentConstraints: ["SlideDeck"],
|
|
4326
|
+
a11y: { element: "section", ariaLabel: "Slide", keyboard: "N/A", notes: "H5P Course Presentation slide row." },
|
|
4327
|
+
theming: { surface: "global-inherit", stylingNotes: "Container." },
|
|
4328
|
+
telemetry: { emits: ["compound_page_viewed"], requiresActiveLesson: true }
|
|
4329
|
+
},
|
|
4330
|
+
{
|
|
4331
|
+
type: "SlideDeck",
|
|
4332
|
+
category: "container",
|
|
4333
|
+
compoundContract: true,
|
|
4334
|
+
h5pMachineName: "H5P.CoursePresentation",
|
|
4335
|
+
h5pAlias: "Course Presentation",
|
|
4336
|
+
description: "Multi-slide presentation with keyboard navigation.",
|
|
4337
|
+
allowedChildTypes: [...SLIDE_DECK_ALLOWED_CHILD_TYPES],
|
|
4338
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.SlideDeck,
|
|
4339
|
+
props: [
|
|
4340
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4341
|
+
{ name: "title", type: "string", required: true, description: "Deck title." },
|
|
4342
|
+
{ name: "showDeckScore", type: "boolean", required: false, description: "Show aggregate score." },
|
|
4343
|
+
{ name: "children", type: "Slide[]", required: true, description: "Slides." }
|
|
4344
|
+
],
|
|
4345
|
+
requiredIds: ["blockId"],
|
|
4346
|
+
parentConstraints: ["Lesson"],
|
|
4347
|
+
a11y: {
|
|
4348
|
+
element: "section",
|
|
4349
|
+
ariaLabel: "Slide deck",
|
|
4350
|
+
keyboard: "Arrow keys, Home, End, Previous/Next slide buttons.",
|
|
4351
|
+
notes: "H5P Course Presentation equivalent."
|
|
4352
|
+
},
|
|
4353
|
+
theming: { surface: "global-inherit", stylingNotes: "Deck chrome." },
|
|
4354
|
+
telemetry: { emits: ["slide_viewed"], requiresActiveLesson: true }
|
|
4355
|
+
},
|
|
4356
|
+
{
|
|
4357
|
+
type: "Accordion",
|
|
4358
|
+
category: "content",
|
|
4359
|
+
h5pMachineName: "H5P.Accordion",
|
|
4360
|
+
h5pAlias: "Accordion",
|
|
4361
|
+
description: "Expandable sections.",
|
|
4362
|
+
props: [
|
|
4363
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4364
|
+
{ name: "sections", type: "AccordionSection[]", required: true, description: "Sections." }
|
|
4365
|
+
],
|
|
4366
|
+
requiredIds: ["blockId"],
|
|
4367
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
4368
|
+
a11y: { element: "section", ariaLabel: "Accordion", keyboard: "Button toggles sections.", notes: "No nested accordions." },
|
|
4369
|
+
theming: { surface: "global-inherit", stylingNotes: "Disclosure pattern." },
|
|
4370
|
+
telemetry: { emits: ["accordion_section_toggled"] }
|
|
4371
|
+
},
|
|
4372
|
+
{
|
|
4373
|
+
type: "DialogCards",
|
|
4374
|
+
category: "content",
|
|
4375
|
+
h5pMachineName: "H5P.Dialogcards",
|
|
4376
|
+
h5pAlias: "Dialog Cards",
|
|
4377
|
+
description: "Flip cards with front/back text.",
|
|
4378
|
+
props: [
|
|
4379
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4380
|
+
{ name: "cards", type: "DialogCard[]", required: true, description: "Cards." }
|
|
4381
|
+
],
|
|
4382
|
+
requiredIds: ["blockId"],
|
|
4383
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
4384
|
+
a11y: { element: "section", ariaLabel: "Dialog cards", keyboard: "Flip and navigate cards.", notes: "Reduced motion safe." },
|
|
4385
|
+
theming: { surface: "global-inherit", stylingNotes: "Card flip." },
|
|
4386
|
+
telemetry: { emits: [] }
|
|
4387
|
+
},
|
|
4388
|
+
{
|
|
4389
|
+
type: "Flashcards",
|
|
4390
|
+
category: "content",
|
|
4391
|
+
h5pMachineName: "H5P.Flashcards",
|
|
4392
|
+
h5pAlias: "Flashcards",
|
|
4393
|
+
description: "Study flashcards with optional self-score.",
|
|
4394
|
+
props: [
|
|
4395
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4396
|
+
{ name: "cards", type: "Flashcard[]", required: true, description: "Cards." },
|
|
4397
|
+
{ name: "selfScore", type: "boolean", required: false, description: "Self-score mode." }
|
|
4398
|
+
],
|
|
4399
|
+
requiredIds: ["blockId"],
|
|
4400
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
4401
|
+
a11y: { element: "section", ariaLabel: "Flashcards", keyboard: "Flip and next.", notes: "Not LMS-scored by default." },
|
|
4402
|
+
theming: { surface: "global-inherit", stylingNotes: "Study mode." },
|
|
4403
|
+
telemetry: { emits: ["flashcard_flipped"] }
|
|
4404
|
+
},
|
|
4405
|
+
{
|
|
4406
|
+
type: "ImageHotspots",
|
|
4407
|
+
category: "content",
|
|
4408
|
+
h5pMachineName: "H5P.ImageHotspots",
|
|
4409
|
+
h5pAlias: "Image Hotspots",
|
|
4410
|
+
description: "Image with clickable hotspot popovers.",
|
|
4411
|
+
props: [
|
|
4412
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4413
|
+
{ name: "src", type: "string", required: true, description: "Image URL." },
|
|
4414
|
+
{ name: "alt", type: "string", required: true, description: "Alt text." },
|
|
4415
|
+
{ name: "hotspots", type: "HotspotSpec[]", required: true, description: "Hotspots." }
|
|
4416
|
+
],
|
|
4417
|
+
requiredIds: ["blockId"],
|
|
4418
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
4419
|
+
a11y: { element: "section", ariaLabel: "Image hotspots", keyboard: "Buttons on image.", notes: "Popover dialog." },
|
|
4420
|
+
theming: { surface: "global-inherit", stylingNotes: "Positioned hotspots." },
|
|
4421
|
+
telemetry: { emits: ["hotspot_opened"] }
|
|
4422
|
+
},
|
|
4423
|
+
{
|
|
4424
|
+
type: "ImageSlider",
|
|
4425
|
+
category: "content",
|
|
4426
|
+
h5pMachineName: "H5P.ImageSlider",
|
|
4427
|
+
h5pAlias: "Image Slider",
|
|
4428
|
+
description: "Carousel of images.",
|
|
4429
|
+
props: [
|
|
4430
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4431
|
+
{ name: "slides", type: "ImageSlide[]", required: true, description: "Slides." }
|
|
4432
|
+
],
|
|
4433
|
+
requiredIds: ["blockId"],
|
|
4434
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
4435
|
+
a11y: { element: "section", ariaLabel: "Image slider", keyboard: "Previous/next slide.", notes: "Carousel." },
|
|
4436
|
+
theming: { surface: "global-inherit", stylingNotes: "Slider." },
|
|
4437
|
+
telemetry: { emits: ["image_slider_changed"] }
|
|
4438
|
+
},
|
|
4439
|
+
{
|
|
4440
|
+
type: "FindHotspot",
|
|
4441
|
+
category: "assessment",
|
|
4442
|
+
assessmentContract: true,
|
|
4443
|
+
h5pMachineName: "H5P.ImageHotspotQuestion",
|
|
4444
|
+
h5pAlias: "Find the Hotspot",
|
|
4445
|
+
description: "Select the correct region on an image.",
|
|
4446
|
+
props: [
|
|
4447
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
4448
|
+
{ name: "src", type: "string", required: true, description: "Image URL." },
|
|
4449
|
+
{ name: "alt", type: "string", required: true, description: "Alt text." },
|
|
4450
|
+
{ name: "targets", type: "HotspotTarget[]", required: true, description: "Targets." },
|
|
4451
|
+
{ name: "correctTargetId", type: "string", required: true, description: "Correct target id." },
|
|
4452
|
+
...assessmentBehaviourProps
|
|
4453
|
+
],
|
|
4454
|
+
requiredIds: ["checkId"],
|
|
4455
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
4456
|
+
a11y: { element: "section", ariaLabel: "Find the hotspot", keyboard: "Select target buttons.", notes: "Scored." },
|
|
4457
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
4458
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
4459
|
+
},
|
|
4460
|
+
{
|
|
4461
|
+
type: "FindMultipleHotspots",
|
|
4462
|
+
category: "assessment",
|
|
4463
|
+
assessmentContract: true,
|
|
4464
|
+
h5pMachineName: "H5P.ImageMultipleHotspotQuestion",
|
|
4465
|
+
h5pAlias: "Find Multiple Hotspots",
|
|
4466
|
+
description: "Select all correct regions on an image.",
|
|
4467
|
+
props: [
|
|
4468
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
4469
|
+
{ name: "src", type: "string", required: true, description: "Image URL." },
|
|
4470
|
+
{ name: "alt", type: "string", required: true, description: "Alt text." },
|
|
4471
|
+
{ name: "targets", type: "HotspotTarget[]", required: true, description: "Targets." },
|
|
4472
|
+
{ name: "correctTargetIds", type: "string[]", required: true, description: "Correct target ids." },
|
|
4473
|
+
...assessmentBehaviourProps
|
|
4474
|
+
],
|
|
4475
|
+
requiredIds: ["checkId"],
|
|
4476
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
4477
|
+
a11y: { element: "section", ariaLabel: "Find multiple hotspots", keyboard: "Toggle targets.", notes: "Scored." },
|
|
4478
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
4479
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
4480
|
+
}
|
|
4481
|
+
];
|
|
4482
|
+
function buildV3CatalogFromV2(v2) {
|
|
4483
|
+
const patched = v2.map((entry) => {
|
|
4484
|
+
const base = extendParents(entry);
|
|
4485
|
+
if (entry.type === "AssessmentSequence") {
|
|
4486
|
+
return {
|
|
4487
|
+
...base,
|
|
4488
|
+
compoundContract: true,
|
|
4489
|
+
allowedChildTypes: [...ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
|
|
4490
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.AssessmentSequence
|
|
4491
|
+
};
|
|
4492
|
+
}
|
|
4493
|
+
return base;
|
|
4494
|
+
});
|
|
4495
|
+
return [...patched, ...v3CompoundAndContentEntries];
|
|
4496
|
+
}
|
|
4497
|
+
|
|
2282
4498
|
// src/blockCatalog.ts
|
|
2283
4499
|
var blockCatalogVersion = 1;
|
|
2284
4500
|
var blockCatalogV2Version = 2;
|
|
4501
|
+
var blockCatalogV3Version = 3;
|
|
2285
4502
|
var BLOCK_CATALOG = [
|
|
2286
4503
|
{
|
|
2287
4504
|
type: "Course",
|
|
@@ -2468,7 +4685,7 @@ var BLOCK_CATALOG = [
|
|
|
2468
4685
|
}
|
|
2469
4686
|
}
|
|
2470
4687
|
];
|
|
2471
|
-
var
|
|
4688
|
+
var assessmentBehaviourProps2 = [
|
|
2472
4689
|
{ name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
|
|
2473
4690
|
{ name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
|
|
2474
4691
|
{ name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
|
|
@@ -2486,7 +4703,7 @@ var v2AssessmentEntries = [
|
|
|
2486
4703
|
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
2487
4704
|
{ name: "question", type: "string", required: true, description: "Question text." },
|
|
2488
4705
|
{ name: "answer", type: "boolean", required: true, description: "Correct answer." },
|
|
2489
|
-
...
|
|
4706
|
+
...assessmentBehaviourProps2
|
|
2490
4707
|
],
|
|
2491
4708
|
requiredIds: ["checkId"],
|
|
2492
4709
|
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
@@ -2511,7 +4728,7 @@ var v2AssessmentEntries = [
|
|
|
2511
4728
|
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
2512
4729
|
{ name: "template", type: "string", required: true, description: "Text with *blank* markers." },
|
|
2513
4730
|
{ name: "blanks", type: "FillInBlankSpec[]", required: false, description: "Explicit blank specs." },
|
|
2514
|
-
...
|
|
4731
|
+
...assessmentBehaviourProps2
|
|
2515
4732
|
],
|
|
2516
4733
|
requiredIds: ["checkId"],
|
|
2517
4734
|
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
@@ -2535,7 +4752,7 @@ var v2AssessmentEntries = [
|
|
|
2535
4752
|
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
2536
4753
|
{ name: "items", type: "DragItem[]", required: true, description: "Draggable items." },
|
|
2537
4754
|
{ name: "targets", type: "DropTarget[]", required: true, description: "Drop targets." },
|
|
2538
|
-
...
|
|
4755
|
+
...assessmentBehaviourProps2
|
|
2539
4756
|
],
|
|
2540
4757
|
requiredIds: ["checkId"],
|
|
2541
4758
|
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
@@ -2559,7 +4776,7 @@ var v2AssessmentEntries = [
|
|
|
2559
4776
|
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
2560
4777
|
{ name: "template", type: "string", required: true, description: "Sentence with *blank* zones." },
|
|
2561
4778
|
{ name: "words", type: "string[]", required: true, description: "Draggable word bank." },
|
|
2562
|
-
...
|
|
4779
|
+
...assessmentBehaviourProps2
|
|
2563
4780
|
],
|
|
2564
4781
|
requiredIds: ["checkId"],
|
|
2565
4782
|
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
@@ -2583,7 +4800,7 @@ var v2AssessmentEntries = [
|
|
|
2583
4800
|
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
2584
4801
|
{ name: "text", type: "string", required: true, description: "Source text." },
|
|
2585
4802
|
{ name: "correctWords", type: "string[]", required: true, description: "Words to mark." },
|
|
2586
|
-
...
|
|
4803
|
+
...assessmentBehaviourProps2
|
|
2587
4804
|
],
|
|
2588
4805
|
requiredIds: ["checkId"],
|
|
2589
4806
|
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
@@ -2605,7 +4822,7 @@ var v2AssessmentEntries = [
|
|
|
2605
4822
|
props: [
|
|
2606
4823
|
{ name: "children", type: "ReactNode", required: true, description: "Assessment blocks." },
|
|
2607
4824
|
{ name: "sequential", type: "boolean", required: false, description: "One question at a time." },
|
|
2608
|
-
...
|
|
4825
|
+
...assessmentBehaviourProps2.filter((p) => p.name !== "passingScore")
|
|
2609
4826
|
],
|
|
2610
4827
|
requiredIds: [],
|
|
2611
4828
|
parentConstraints: ["Lesson"],
|
|
@@ -2623,6 +4840,7 @@ var BLOCK_CATALOG_V2 = [
|
|
|
2623
4840
|
...BLOCK_CATALOG,
|
|
2624
4841
|
...v2AssessmentEntries
|
|
2625
4842
|
];
|
|
4843
|
+
var BLOCK_CATALOG_V3 = buildV3CatalogFromV2(BLOCK_CATALOG_V2);
|
|
2626
4844
|
function cloneCatalogEntry(entry) {
|
|
2627
4845
|
return {
|
|
2628
4846
|
...entry,
|
|
@@ -2630,6 +4848,7 @@ function cloneCatalogEntry(entry) {
|
|
|
2630
4848
|
aliases: entry.aliases ? [...entry.aliases] : void 0,
|
|
2631
4849
|
optionalIds: entry.optionalIds ? [...entry.optionalIds] : void 0,
|
|
2632
4850
|
parentConstraints: entry.parentConstraints ? [...entry.parentConstraints] : void 0,
|
|
4851
|
+
allowedChildTypes: entry.allowedChildTypes ? [...entry.allowedChildTypes] : void 0,
|
|
2633
4852
|
a11y: { ...entry.a11y },
|
|
2634
4853
|
theming: {
|
|
2635
4854
|
...entry.theming,
|
|
@@ -2642,34 +4861,50 @@ function cloneCatalogEntry(entry) {
|
|
|
2642
4861
|
};
|
|
2643
4862
|
}
|
|
2644
4863
|
function buildBlockCatalog(opts) {
|
|
2645
|
-
const version = opts?.version ??
|
|
2646
|
-
const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
|
|
4864
|
+
const version = opts?.version ?? 3;
|
|
4865
|
+
const source = version === 3 ? BLOCK_CATALOG_V3 : version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
|
|
2647
4866
|
return source.map((entry) => cloneCatalogEntry(entry));
|
|
2648
4867
|
}
|
|
2649
4868
|
function getBlockCatalogEntry(type, opts) {
|
|
2650
|
-
const version = opts?.version ??
|
|
2651
|
-
const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
|
|
4869
|
+
const version = opts?.version ?? 3;
|
|
4870
|
+
const source = version === 3 ? BLOCK_CATALOG_V3 : version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
|
|
2652
4871
|
return source.find((entry) => entry.type === type || entry.aliases?.includes(type));
|
|
2653
4872
|
}
|
|
2654
4873
|
export {
|
|
4874
|
+
Accordion,
|
|
2655
4875
|
AssessmentSequence,
|
|
2656
4876
|
BLOCK_CATALOG,
|
|
2657
4877
|
BLOCK_CATALOG_V2,
|
|
4878
|
+
BLOCK_CATALOG_V3,
|
|
2658
4879
|
Course,
|
|
4880
|
+
DialogCards,
|
|
2659
4881
|
DragAndDrop,
|
|
2660
4882
|
DragTheWords,
|
|
2661
4883
|
FillInTheBlanks,
|
|
4884
|
+
FindHotspot,
|
|
4885
|
+
FindMultipleHotspots,
|
|
4886
|
+
Flashcards,
|
|
4887
|
+
Heading,
|
|
4888
|
+
Image,
|
|
4889
|
+
ImageHotspots,
|
|
4890
|
+
ImageSlider,
|
|
4891
|
+
InteractiveBook,
|
|
2662
4892
|
KnowledgeCheck,
|
|
2663
4893
|
Lesson,
|
|
2664
4894
|
LessonkitProvider,
|
|
2665
4895
|
MarkTheWords,
|
|
4896
|
+
Page,
|
|
2666
4897
|
ProgressTracker,
|
|
2667
4898
|
Quiz,
|
|
2668
4899
|
Reflection,
|
|
2669
4900
|
Scenario,
|
|
4901
|
+
Slide,
|
|
4902
|
+
SlideDeck,
|
|
4903
|
+
Text,
|
|
2670
4904
|
ThemeProvider,
|
|
2671
4905
|
TrueFalse,
|
|
2672
4906
|
blockCatalogV2Version,
|
|
4907
|
+
blockCatalogV3Version,
|
|
2673
4908
|
blockCatalogVersion,
|
|
2674
4909
|
buildBlockCatalog,
|
|
2675
4910
|
buildTelemetryEvent2 as buildTelemetryEvent,
|