@lessonkit/react 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/block-catalog.v3.json +180 -5
- package/dist/index.cjs +1525 -606
- package/dist/index.d.cts +31 -2
- package/dist/index.d.ts +31 -2
- package/dist/index.js +1352 -434
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/components.tsx
|
|
2
|
-
import { useEffect as useEffect4, useId as useId2, useMemo as useMemo6, useRef as
|
|
2
|
+
import { useEffect as useEffect4, useId as useId2, useMemo as useMemo6, useRef as useRef5, useState as useState4 } from "react";
|
|
3
3
|
import { visuallyHiddenStyle as visuallyHiddenStyle2 } from "@lessonkit/accessibility";
|
|
4
4
|
|
|
5
5
|
// src/context.tsx
|
|
@@ -28,6 +28,44 @@ function createXapiQueueFromObservability(observability) {
|
|
|
28
28
|
}
|
|
29
29
|
return createInMemoryXAPIQueue(opts);
|
|
30
30
|
}
|
|
31
|
+
function wrapBatchSink(batchSink, observability) {
|
|
32
|
+
if (!batchSink || !observability?.onTelemetrySinkError) return batchSink;
|
|
33
|
+
const onError = observability.onTelemetrySinkError;
|
|
34
|
+
return async (events) => {
|
|
35
|
+
try {
|
|
36
|
+
await batchSink(events);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
onError(err, { sinkId: "tracking-batch" });
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function warnMissingProductionObservability(observability, opts) {
|
|
44
|
+
let isProduction = false;
|
|
45
|
+
try {
|
|
46
|
+
isProduction = import.meta.env?.PROD === true;
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
if (!isProduction) {
|
|
50
|
+
const g = globalThis;
|
|
51
|
+
isProduction = typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
|
|
52
|
+
}
|
|
53
|
+
if (!isProduction) return;
|
|
54
|
+
if (!opts.trackingEnabled && !opts.xapiEnabled) return;
|
|
55
|
+
const hooks = [
|
|
56
|
+
observability?.onTelemetrySinkError,
|
|
57
|
+
observability?.onTelemetryBufferDrop,
|
|
58
|
+
observability?.onXapiQueueDepth,
|
|
59
|
+
observability?.onXapiQueueCap,
|
|
60
|
+
observability?.onLxpackBridgeMiss
|
|
61
|
+
];
|
|
62
|
+
if (hooks.some(Boolean)) return;
|
|
63
|
+
if (typeof console !== "undefined") {
|
|
64
|
+
console.warn(
|
|
65
|
+
"[lessonkit] Production deployment without observability hooks \u2014 telemetry/xAPI failures and buffer drops will be silent. See https://lessonkit.readthedocs.io/en/latest/guides/react-developers/production-checklist.html"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
31
69
|
function wrapTrackingSink(sink, observability) {
|
|
32
70
|
if (!sink || !observability?.onTelemetrySinkError) return sink;
|
|
33
71
|
const onError = observability.onTelemetrySinkError;
|
|
@@ -152,6 +190,9 @@ import {
|
|
|
152
190
|
resetStoragePortForTests
|
|
153
191
|
} from "@lessonkit/core";
|
|
154
192
|
|
|
193
|
+
// src/provider/useLessonkitProviderRuntime.ts
|
|
194
|
+
import { resetSharedVolatileSessionIdForTests } from "@lessonkit/core";
|
|
195
|
+
|
|
155
196
|
// src/runtime/progress.ts
|
|
156
197
|
import { createProgressController } from "@lessonkit/core";
|
|
157
198
|
|
|
@@ -166,6 +207,7 @@ function createXapiClientFromConfig(config, queue) {
|
|
|
166
207
|
return createXAPIClient({
|
|
167
208
|
courseId: config.courseId,
|
|
168
209
|
transport: config.xapi?.transport,
|
|
210
|
+
exitTransport: config.xapi?.exitTransport,
|
|
169
211
|
queue
|
|
170
212
|
});
|
|
171
213
|
}
|
|
@@ -225,6 +267,7 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
|
|
|
225
267
|
const statement = telemetryEventToXAPIStatement2(opts.event);
|
|
226
268
|
if (statement) {
|
|
227
269
|
opts.xapi.send(statement);
|
|
270
|
+
await opts.xapi.flush();
|
|
228
271
|
xapiStatementSent = true;
|
|
229
272
|
}
|
|
230
273
|
}
|
|
@@ -260,7 +303,7 @@ function emitTelemetryWithPlugins(opts) {
|
|
|
260
303
|
}
|
|
261
304
|
|
|
262
305
|
// src/provider/courseStarted/emit.ts
|
|
263
|
-
var
|
|
306
|
+
var courseStartedTrackingFlights = /* @__PURE__ */ new Map();
|
|
264
307
|
function isTrackingActive(tracking) {
|
|
265
308
|
return tracking?.enabled !== false;
|
|
266
309
|
}
|
|
@@ -288,24 +331,40 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
|
|
|
288
331
|
if (hasCourseStartedEmittedToTracking(storage, sessionId, courseId)) {
|
|
289
332
|
return true;
|
|
290
333
|
}
|
|
291
|
-
|
|
292
|
-
|
|
334
|
+
const existing = courseStartedTrackingFlights.get(flightKey);
|
|
335
|
+
if (existing) {
|
|
336
|
+
const settled = await existing;
|
|
337
|
+
if (settled) return true;
|
|
293
338
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
339
|
+
let resolveFlight;
|
|
340
|
+
const flight = new Promise((resolve) => {
|
|
341
|
+
resolveFlight = resolve;
|
|
342
|
+
});
|
|
343
|
+
courseStartedTrackingFlights.set(flightKey, flight);
|
|
344
|
+
void (async () => {
|
|
345
|
+
try {
|
|
346
|
+
if (shouldCommit && !shouldCommit()) {
|
|
347
|
+
resolveFlight(false);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
tracking.track(event);
|
|
351
|
+
const delivered = await tracking.flush?.();
|
|
352
|
+
if (delivered === false) {
|
|
353
|
+
resolveFlight(false);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (markCourseStartedEmittedToTracking(storage, sessionId, courseId) === false) {
|
|
357
|
+
resolveFlight(false);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
resolveFlight(true);
|
|
361
|
+
} catch {
|
|
362
|
+
resolveFlight(false);
|
|
363
|
+
} finally {
|
|
364
|
+
courseStartedTrackingFlights.delete(flightKey);
|
|
307
365
|
}
|
|
308
|
-
}
|
|
366
|
+
})();
|
|
367
|
+
return flight;
|
|
309
368
|
}
|
|
310
369
|
async function emitCourseStartedPipelineOnly(opts) {
|
|
311
370
|
try {
|
|
@@ -319,8 +378,10 @@ async function emitCourseStartedPipelineOnly(opts) {
|
|
|
319
378
|
skipXapi: opts.skipXapi
|
|
320
379
|
});
|
|
321
380
|
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
322
|
-
markCourseStarted(opts.storage, opts.sessionId, opts.courseId);
|
|
323
|
-
markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId)
|
|
381
|
+
if (markCourseStarted(opts.storage, opts.sessionId, opts.courseId) === false) return "failed";
|
|
382
|
+
if (markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId) === false) {
|
|
383
|
+
return "failed";
|
|
384
|
+
}
|
|
324
385
|
if (xapiStatementSent) {
|
|
325
386
|
opts.onXapiStatementSent?.();
|
|
326
387
|
}
|
|
@@ -371,7 +432,9 @@ async function emitCourseStartedToTrackingOnly(opts) {
|
|
|
371
432
|
extraSinks: opts.extraSinks,
|
|
372
433
|
skipXapi: true
|
|
373
434
|
});
|
|
374
|
-
markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId)
|
|
435
|
+
if (markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId) === false) {
|
|
436
|
+
return "failed";
|
|
437
|
+
}
|
|
375
438
|
return "emitted";
|
|
376
439
|
} catch {
|
|
377
440
|
return "failed";
|
|
@@ -424,13 +487,15 @@ function assertTrackingSinkConfig(tracking) {
|
|
|
424
487
|
|
|
425
488
|
// src/runtime/telemetry.ts
|
|
426
489
|
import { createTrackingClient } from "@lessonkit/core";
|
|
427
|
-
function createTrackingClientFromConfig(config) {
|
|
490
|
+
function createTrackingClientFromConfig(config, observability) {
|
|
428
491
|
if (config.tracking?.enabled === false) return createTrackingClient();
|
|
429
492
|
if (config.tracking?.createClient) return config.tracking.createClient();
|
|
430
493
|
return createTrackingClient({
|
|
431
494
|
sink: config.tracking?.sink,
|
|
432
495
|
batchSink: config.tracking?.batchSink,
|
|
433
|
-
batch: config.tracking?.batch
|
|
496
|
+
batch: config.tracking?.batch,
|
|
497
|
+
exitBatchSink: config.tracking?.exitBatchSink,
|
|
498
|
+
onBufferDrop: observability?.onTelemetryBufferDrop
|
|
434
499
|
});
|
|
435
500
|
}
|
|
436
501
|
async function disposeTrackingClient(client) {
|
|
@@ -505,6 +570,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
505
570
|
const pendingCourseIdResetRef = useRef(false);
|
|
506
571
|
const prevUseV2RuntimeRef = useRef(useV2Runtime);
|
|
507
572
|
const xapiCourseStartedSentOnClientRef = useRef(false);
|
|
573
|
+
const xapiBootstrapSendRef = useRef(false);
|
|
508
574
|
if (prevUseV2RuntimeRef.current !== useV2Runtime) {
|
|
509
575
|
prevUseV2RuntimeRef.current = useV2Runtime;
|
|
510
576
|
if (useV2Runtime) {
|
|
@@ -512,7 +578,8 @@ function useLessonkitProviderRuntime(config) {
|
|
|
512
578
|
courseId: normalizedCourseId,
|
|
513
579
|
runtimeVersion: "v2",
|
|
514
580
|
session: normalizedConfig.session,
|
|
515
|
-
plugins: pluginHostRef.current ?? normalizedConfig.plugins
|
|
581
|
+
plugins: pluginHostRef.current ?? normalizedConfig.plugins,
|
|
582
|
+
deferPluginSetup: true
|
|
516
583
|
});
|
|
517
584
|
progressRef.current = headlessRef.current.progress;
|
|
518
585
|
} else {
|
|
@@ -527,7 +594,8 @@ function useLessonkitProviderRuntime(config) {
|
|
|
527
594
|
courseId: normalizedCourseId,
|
|
528
595
|
runtimeVersion: "v2",
|
|
529
596
|
session: normalizedConfig.session,
|
|
530
|
-
plugins: pluginHostRef.current ?? normalizedConfig.plugins
|
|
597
|
+
plugins: pluginHostRef.current ?? normalizedConfig.plugins,
|
|
598
|
+
deferPluginSetup: true
|
|
531
599
|
});
|
|
532
600
|
}
|
|
533
601
|
if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
|
|
@@ -575,19 +643,22 @@ function useLessonkitProviderRuntime(config) {
|
|
|
575
643
|
xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
|
|
576
644
|
prevXapiCourseIdRef.current = courseId;
|
|
577
645
|
xapiCourseStartedSentOnClientRef.current = false;
|
|
646
|
+
xapiBootstrapSendRef.current = false;
|
|
578
647
|
}
|
|
579
648
|
const prev = xapiRef.current;
|
|
580
649
|
const next = createXapiClientFromConfig(normalizedConfig, xapiQueueRef.current);
|
|
581
650
|
xapiRef.current = next;
|
|
582
651
|
setXapi(next);
|
|
652
|
+
let bootstrapSent = false;
|
|
653
|
+
let bootstrapAlreadyStarted = false;
|
|
583
654
|
if (next) {
|
|
584
655
|
const sessionId = sessionIdRef.current;
|
|
585
656
|
const cid = courseIdRef.current;
|
|
586
657
|
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
587
|
-
|
|
658
|
+
bootstrapAlreadyStarted = hasCourseStarted(defaultStorage, sessionId, cid);
|
|
588
659
|
const clientChanged = !prev || prev !== next;
|
|
589
|
-
const skipBootstrap = trackingActive && !
|
|
590
|
-
const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!
|
|
660
|
+
const skipBootstrap = trackingActive && !bootstrapAlreadyStarted;
|
|
661
|
+
const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && !xapiBootstrapSendRef.current && (!bootstrapAlreadyStarted || clientChanged);
|
|
591
662
|
if (needsBootstrap) {
|
|
592
663
|
try {
|
|
593
664
|
const event = buildCourseStartedEvent({
|
|
@@ -598,15 +669,12 @@ function useLessonkitProviderRuntime(config) {
|
|
|
598
669
|
user: userRef.current,
|
|
599
670
|
lxpackBridge: lxpackBridgeModeRef.current
|
|
600
671
|
});
|
|
601
|
-
if (event
|
|
602
|
-
} else {
|
|
672
|
+
if (event !== null) {
|
|
603
673
|
const statement = telemetryEventToXAPIStatement3(event);
|
|
604
674
|
if (statement) {
|
|
605
675
|
next.send(statement);
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
xapiCourseStartedSentOnClientRef.current = true;
|
|
676
|
+
xapiBootstrapSendRef.current = true;
|
|
677
|
+
bootstrapSent = true;
|
|
610
678
|
}
|
|
611
679
|
}
|
|
612
680
|
} catch {
|
|
@@ -624,6 +692,12 @@ function useLessonkitProviderRuntime(config) {
|
|
|
624
692
|
if (cancelled) return;
|
|
625
693
|
try {
|
|
626
694
|
await next?.flush();
|
|
695
|
+
if (bootstrapSent && !cancelled) {
|
|
696
|
+
if (!bootstrapAlreadyStarted) {
|
|
697
|
+
markCourseStarted(defaultStorage, sessionIdRef.current, courseIdRef.current);
|
|
698
|
+
}
|
|
699
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
700
|
+
}
|
|
627
701
|
} catch {
|
|
628
702
|
}
|
|
629
703
|
})();
|
|
@@ -652,7 +726,10 @@ function useLessonkitProviderRuntime(config) {
|
|
|
652
726
|
useIsoLayoutEffect(() => {
|
|
653
727
|
const prev = trackingRef.current;
|
|
654
728
|
const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
|
|
655
|
-
const userBatchSink =
|
|
729
|
+
const userBatchSink = wrapBatchSink(
|
|
730
|
+
normalizedConfig.tracking?.batchSink,
|
|
731
|
+
observabilityRef.current
|
|
732
|
+
);
|
|
656
733
|
assertTrackingSinkConfig(normalizedConfig.tracking);
|
|
657
734
|
const sink = pluginHostRef.current && baseSink ? (
|
|
658
735
|
/* v8 ignore next -- composeTrackingSink may return null; fall back to base sink */
|
|
@@ -672,9 +749,12 @@ function useLessonkitProviderRuntime(config) {
|
|
|
672
749
|
}
|
|
673
750
|
return userBatchSink(perEventForBatch);
|
|
674
751
|
} : userBatchSink;
|
|
675
|
-
const next = createTrackingClientFromConfig(
|
|
676
|
-
|
|
677
|
-
|
|
752
|
+
const next = createTrackingClientFromConfig(
|
|
753
|
+
{
|
|
754
|
+
tracking: { ...normalizedConfig.tracking, sink, batchSink }
|
|
755
|
+
},
|
|
756
|
+
observabilityRef.current
|
|
757
|
+
);
|
|
678
758
|
trackingRef.current = next;
|
|
679
759
|
trackingClientForUnmountRef.current = next;
|
|
680
760
|
setTracking(next);
|
|
@@ -803,7 +883,11 @@ function useLessonkitProviderRuntime(config) {
|
|
|
803
883
|
user: userRef.current,
|
|
804
884
|
lxpackBridge: lxpackBridgeModeRef.current,
|
|
805
885
|
onLxpackBridgeMiss,
|
|
806
|
-
extraSinks: extraSinksRef.current
|
|
886
|
+
extraSinks: extraSinksRef.current,
|
|
887
|
+
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
888
|
+
onXapiStatementSent: () => {
|
|
889
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
890
|
+
}
|
|
807
891
|
});
|
|
808
892
|
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
809
893
|
}
|
|
@@ -859,20 +943,39 @@ function useLessonkitProviderRuntime(config) {
|
|
|
859
943
|
}, []);
|
|
860
944
|
useEffect(() => {
|
|
861
945
|
if (typeof document === "undefined") return;
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
946
|
+
const flushOnPageExit = () => {
|
|
947
|
+
try {
|
|
948
|
+
xapiRef.current?.flushOnExit?.();
|
|
949
|
+
trackingRef.current?.flushOnExit?.();
|
|
950
|
+
} finally {
|
|
951
|
+
void xapiRef.current?.flush();
|
|
952
|
+
void trackingRef.current?.flush?.();
|
|
953
|
+
}
|
|
865
954
|
};
|
|
866
955
|
const onVisibilityChange = () => {
|
|
867
|
-
if (document.visibilityState === "hidden")
|
|
956
|
+
if (document.visibilityState === "hidden") flushOnPageExit();
|
|
868
957
|
};
|
|
869
958
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
870
|
-
window.addEventListener("pagehide",
|
|
959
|
+
window.addEventListener("pagehide", flushOnPageExit);
|
|
871
960
|
return () => {
|
|
872
961
|
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
873
|
-
window.removeEventListener("pagehide",
|
|
962
|
+
window.removeEventListener("pagehide", flushOnPageExit);
|
|
874
963
|
};
|
|
875
964
|
}, []);
|
|
965
|
+
useEffect(() => {
|
|
966
|
+
warnMissingProductionObservability(observabilityRef.current, {
|
|
967
|
+
trackingEnabled: isTrackingActive(normalizedConfig.tracking),
|
|
968
|
+
xapiEnabled: normalizedConfig.xapi?.enabled !== false && Boolean(
|
|
969
|
+
normalizedConfig.xapi?.client || normalizedConfig.xapi?.transport || normalizedConfig.xapi?.enabled === true
|
|
970
|
+
)
|
|
971
|
+
});
|
|
972
|
+
}, [
|
|
973
|
+
normalizedConfig.tracking,
|
|
974
|
+
normalizedConfig.xapi?.enabled,
|
|
975
|
+
normalizedConfig.xapi?.client,
|
|
976
|
+
normalizedConfig.xapi?.transport,
|
|
977
|
+
normalizedConfig.observability
|
|
978
|
+
]);
|
|
876
979
|
const setActiveLesson = useCallback(
|
|
877
980
|
(lessonId) => {
|
|
878
981
|
if (useV2Runtime && headlessRef.current) {
|
|
@@ -992,6 +1095,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
992
1095
|
config: normalizedConfig,
|
|
993
1096
|
tracking,
|
|
994
1097
|
xapi,
|
|
1098
|
+
storage: defaultStorage,
|
|
995
1099
|
session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
|
|
996
1100
|
progress,
|
|
997
1101
|
setActiveLesson,
|
|
@@ -1127,7 +1231,7 @@ function getLessonMountCount(lessonId) {
|
|
|
1127
1231
|
}
|
|
1128
1232
|
|
|
1129
1233
|
// src/components/Quiz.tsx
|
|
1130
|
-
import { forwardRef, useEffect as useEffect3, useId, useMemo as useMemo5, useRef as
|
|
1234
|
+
import { forwardRef, useEffect as useEffect3, useId, useMemo as useMemo5, useRef as useRef4, useState as useState3 } from "react";
|
|
1131
1235
|
import { visuallyHiddenStyle } from "@lessonkit/accessibility";
|
|
1132
1236
|
|
|
1133
1237
|
// src/assessment/AssessmentLessonGuard.tsx
|
|
@@ -1186,6 +1290,12 @@ function readStringField(state, key) {
|
|
|
1186
1290
|
if (typeof value === "string" || value === null) return value;
|
|
1187
1291
|
return void 0;
|
|
1188
1292
|
}
|
|
1293
|
+
function readNumberField(state, key) {
|
|
1294
|
+
const value = state[key];
|
|
1295
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1296
|
+
if (value === null) return null;
|
|
1297
|
+
return void 0;
|
|
1298
|
+
}
|
|
1189
1299
|
function readBooleanStateField(state, key, apply) {
|
|
1190
1300
|
const value = state[key];
|
|
1191
1301
|
if (typeof value === "boolean") apply(value);
|
|
@@ -1195,53 +1305,81 @@ function readBooleanStateField(state, key, apply) {
|
|
|
1195
1305
|
import { useImperativeHandle as useImperativeHandle2 } from "react";
|
|
1196
1306
|
|
|
1197
1307
|
// src/compound/CompoundProvider.tsx
|
|
1198
|
-
import
|
|
1308
|
+
import React5, { createContext as createContext5, useCallback as useCallback2, useContext as useContext5, useImperativeHandle, useMemo as useMemo4, useRef as useRef3, useState as useState2 } from "react";
|
|
1199
1309
|
import { clampCompoundPageIndex, createCompoundResumeState } from "@lessonkit/core";
|
|
1200
1310
|
|
|
1201
1311
|
// src/compound/aggregateScores.ts
|
|
1202
|
-
function aggregateAssessmentScores(handles) {
|
|
1312
|
+
function aggregateAssessmentScores(handles, opts) {
|
|
1203
1313
|
let score = 0;
|
|
1204
1314
|
let maxScore = 0;
|
|
1205
1315
|
let allAnswered = true;
|
|
1206
|
-
for (const
|
|
1316
|
+
for (const entry of handles) {
|
|
1317
|
+
const handle = "handle" in entry ? entry.handle : entry;
|
|
1318
|
+
const pageIndex = "handle" in entry ? entry.pageIndex : void 0;
|
|
1207
1319
|
score += handle.getScore();
|
|
1208
1320
|
maxScore += handle.getMaxScore();
|
|
1209
|
-
|
|
1321
|
+
const countsForAnswerGiven = opts?.answerPageIndex === void 0 || pageIndex === void 0 || pageIndex === opts.answerPageIndex;
|
|
1322
|
+
if (countsForAnswerGiven && !handle.getAnswerGiven()) allAnswered = false;
|
|
1210
1323
|
}
|
|
1211
1324
|
return { score, maxScore, allAnswered };
|
|
1212
1325
|
}
|
|
1213
1326
|
|
|
1214
|
-
// src/compound/
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1327
|
+
// src/compound/CompoundHydrationBridge.tsx
|
|
1328
|
+
import { createContext as createContext3, useContext as useContext3, useRef as useRef2 } from "react";
|
|
1329
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
1330
|
+
var CompoundHydrationBridgeContext = createContext3(
|
|
1331
|
+
null
|
|
1332
|
+
);
|
|
1333
|
+
function CompoundHydrationBridgeProvider({ children }) {
|
|
1334
|
+
const bridgeRef = useRef2(null);
|
|
1335
|
+
return /* @__PURE__ */ jsx3(CompoundHydrationBridgeContext.Provider, { value: bridgeRef, children });
|
|
1336
|
+
}
|
|
1337
|
+
function useCompoundHydrationBridgeRef() {
|
|
1338
|
+
return useContext3(CompoundHydrationBridgeContext);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// src/compound/CompoundPageIndexContext.tsx
|
|
1342
|
+
import { createContext as createContext4, useContext as useContext4 } from "react";
|
|
1343
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1344
|
+
var CompoundPageIndexContext = createContext4(void 0);
|
|
1345
|
+
function CompoundPageIndexProvider({
|
|
1346
|
+
pageIndex,
|
|
1347
|
+
children
|
|
1348
|
+
}) {
|
|
1349
|
+
return /* @__PURE__ */ jsx4(CompoundPageIndexContext.Provider, { value: pageIndex, children });
|
|
1350
|
+
}
|
|
1351
|
+
function useCompoundPageIndex() {
|
|
1352
|
+
return useContext4(CompoundPageIndexContext);
|
|
1224
1353
|
}
|
|
1225
1354
|
|
|
1226
1355
|
// src/compound/CompoundProvider.tsx
|
|
1227
|
-
import { jsx as
|
|
1228
|
-
var CompoundRegistryContext =
|
|
1229
|
-
var CompoundHandlesVersionContext =
|
|
1356
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1357
|
+
var CompoundRegistryContext = createContext5(null);
|
|
1358
|
+
var CompoundHandlesVersionContext = createContext5(0);
|
|
1230
1359
|
function CompoundProvider({
|
|
1231
1360
|
children,
|
|
1232
1361
|
activePageIndex: _activePageIndex,
|
|
1233
1362
|
onActivePageIndexChange: _onActivePageIndexChange
|
|
1234
1363
|
}) {
|
|
1235
|
-
const registryRef =
|
|
1364
|
+
const registryRef = useRef3(/* @__PURE__ */ new Map());
|
|
1236
1365
|
const [handlesVersion, setHandlesVersion] = useState2(0);
|
|
1237
|
-
const register = useCallback2((checkId, handle) => {
|
|
1366
|
+
const register = useCallback2((checkId, handle, pageIndex) => {
|
|
1238
1367
|
const prev = registryRef.current.get(checkId);
|
|
1239
|
-
|
|
1240
|
-
|
|
1368
|
+
if (prev && prev.handle !== handle) {
|
|
1369
|
+
const message = `[lessonkit] duplicate checkId "${checkId}" registered in the same compound container; the previous handle was replaced.`;
|
|
1370
|
+
if (isDevEnvironment4()) {
|
|
1371
|
+
console.error(message);
|
|
1372
|
+
} else {
|
|
1373
|
+
console.warn(message);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
registryRef.current.set(checkId, { handle, pageIndex });
|
|
1377
|
+
if (prev?.handle !== handle || prev?.pageIndex !== pageIndex) {
|
|
1241
1378
|
setHandlesVersion((v) => v + 1);
|
|
1242
1379
|
}
|
|
1243
1380
|
return () => {
|
|
1244
|
-
|
|
1381
|
+
const current = registryRef.current.get(checkId);
|
|
1382
|
+
if (current?.handle === handle) {
|
|
1245
1383
|
registryRef.current.delete(checkId);
|
|
1246
1384
|
setHandlesVersion((v) => v + 1);
|
|
1247
1385
|
}
|
|
@@ -1250,30 +1388,39 @@ function CompoundProvider({
|
|
|
1250
1388
|
const registryValue = useMemo4(
|
|
1251
1389
|
() => ({
|
|
1252
1390
|
register,
|
|
1253
|
-
getHandles: () =>
|
|
1391
|
+
getHandles: () => {
|
|
1392
|
+
const handles = /* @__PURE__ */ new Map();
|
|
1393
|
+
for (const [checkId, entry] of registryRef.current) {
|
|
1394
|
+
handles.set(checkId, entry.handle);
|
|
1395
|
+
}
|
|
1396
|
+
return handles;
|
|
1397
|
+
},
|
|
1398
|
+
getRegisteredHandles: () => registryRef.current
|
|
1254
1399
|
}),
|
|
1255
1400
|
[register]
|
|
1256
1401
|
);
|
|
1257
|
-
return /* @__PURE__ */
|
|
1402
|
+
return /* @__PURE__ */ jsx5(CompoundHydrationBridgeProvider, { children: /* @__PURE__ */ jsx5(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ jsx5(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) }) });
|
|
1258
1403
|
}
|
|
1259
1404
|
function useCompoundRegistry() {
|
|
1260
|
-
const registry =
|
|
1261
|
-
const handlesVersion =
|
|
1405
|
+
const registry = useContext5(CompoundRegistryContext);
|
|
1406
|
+
const handlesVersion = useContext5(CompoundHandlesVersionContext);
|
|
1262
1407
|
if (!registry) return null;
|
|
1263
1408
|
return { ...registry, handlesVersion };
|
|
1264
1409
|
}
|
|
1265
1410
|
function useCompoundHandlesVersion() {
|
|
1266
|
-
return
|
|
1411
|
+
return useContext5(CompoundHandlesVersionContext);
|
|
1267
1412
|
}
|
|
1268
1413
|
function useRegisterAssessmentHandle(checkId, handle) {
|
|
1269
|
-
const registry =
|
|
1270
|
-
|
|
1414
|
+
const registry = useContext5(CompoundRegistryContext);
|
|
1415
|
+
const pageIndex = useCompoundPageIndex();
|
|
1416
|
+
React5.useLayoutEffect(() => {
|
|
1271
1417
|
if (!registry || !handle) return;
|
|
1272
|
-
return registry.register(checkId, handle);
|
|
1273
|
-
}, [registry, checkId, handle]);
|
|
1418
|
+
return registry.register(checkId, handle, pageIndex);
|
|
1419
|
+
}, [registry, checkId, handle, pageIndex]);
|
|
1274
1420
|
}
|
|
1275
1421
|
function useCompoundHandleRef(ref, opts) {
|
|
1276
|
-
const { activePageIndex, setActivePageIndex, getHandles, pageCount } = opts;
|
|
1422
|
+
const { activePageIndex, setActivePageIndex, getHandles, getRegisteredHandles, pageCount } = opts;
|
|
1423
|
+
const bridgeRef = useCompoundHydrationBridgeRef();
|
|
1277
1424
|
const setIndexClamped = useCallback2(
|
|
1278
1425
|
(index) => {
|
|
1279
1426
|
const next = pageCount !== void 0 ? clampCompoundPageIndex(index, pageCount) : Math.max(0, Math.floor(index));
|
|
@@ -1284,31 +1431,32 @@ function useCompoundHandleRef(ref, opts) {
|
|
|
1284
1431
|
useImperativeHandle(
|
|
1285
1432
|
ref,
|
|
1286
1433
|
() => ({
|
|
1287
|
-
getScore: () => aggregateAssessmentScores(
|
|
1288
|
-
getMaxScore: () => aggregateAssessmentScores(
|
|
1289
|
-
getAnswerGiven: () => aggregateAssessmentScores(
|
|
1434
|
+
getScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).score,
|
|
1435
|
+
getMaxScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).maxScore,
|
|
1436
|
+
getAnswerGiven: () => aggregateAssessmentScores(getRegisteredHandles().values(), {
|
|
1437
|
+
answerPageIndex: activePageIndex
|
|
1438
|
+
}).allAnswered,
|
|
1290
1439
|
resetTask: () => {
|
|
1291
|
-
for (const
|
|
1440
|
+
for (const entry of getRegisteredHandles().values()) entry.handle.resetTask();
|
|
1292
1441
|
},
|
|
1293
1442
|
showSolutions: () => {
|
|
1294
1443
|
if (!opts.enableSolutionsButton) return;
|
|
1295
|
-
for (const
|
|
1444
|
+
for (const entry of getRegisteredHandles().values()) entry.handle.showSolutions();
|
|
1296
1445
|
},
|
|
1297
1446
|
getCurrentState: () => {
|
|
1298
1447
|
const childStates = {};
|
|
1299
|
-
for (const [checkId,
|
|
1300
|
-
if (handle.getCurrentState) {
|
|
1301
|
-
childStates[checkId] = handle.getCurrentState();
|
|
1448
|
+
for (const [checkId, entry] of getRegisteredHandles()) {
|
|
1449
|
+
if (entry.handle.getCurrentState) {
|
|
1450
|
+
childStates[checkId] = entry.handle.getCurrentState();
|
|
1302
1451
|
}
|
|
1303
1452
|
}
|
|
1304
1453
|
return createCompoundResumeState({ activePageIndex, childStates });
|
|
1305
1454
|
},
|
|
1306
1455
|
resume: (state) => {
|
|
1307
|
-
|
|
1308
|
-
resumeChildHandles(getHandles(), state.childStates);
|
|
1456
|
+
bridgeRef?.current?.notifyImperativeResume(state);
|
|
1309
1457
|
}
|
|
1310
1458
|
}),
|
|
1311
|
-
[activePageIndex, setIndexClamped, getHandles, opts.enableSolutionsButton]
|
|
1459
|
+
[activePageIndex, setIndexClamped, getHandles, getRegisteredHandles, opts.enableSolutionsButton, bridgeRef]
|
|
1312
1460
|
);
|
|
1313
1461
|
}
|
|
1314
1462
|
|
|
@@ -1378,7 +1526,7 @@ function usePluginScoring(checkId, lessonId) {
|
|
|
1378
1526
|
}
|
|
1379
1527
|
|
|
1380
1528
|
// src/components/Quiz.tsx
|
|
1381
|
-
import { jsx as
|
|
1529
|
+
import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1382
1530
|
function QuizInner(props, ref) {
|
|
1383
1531
|
const { enclosingLessonId } = props;
|
|
1384
1532
|
const checkId = useMemo5(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
@@ -1387,44 +1535,85 @@ function QuizInner(props, ref) {
|
|
|
1387
1535
|
const [selected, setSelected] = useState3(null);
|
|
1388
1536
|
const [selectionCorrect, setSelectionCorrect] = useState3(null);
|
|
1389
1537
|
const [quizPassed, setQuizPassed] = useState3(false);
|
|
1390
|
-
const
|
|
1538
|
+
const [completedScore, setCompletedScore] = useState3(null);
|
|
1539
|
+
const [completedMaxScore, setCompletedMaxScore] = useState3(null);
|
|
1540
|
+
const completedRef = useRef4(false);
|
|
1541
|
+
const telemetryReplayedRef = useRef4(false);
|
|
1391
1542
|
const questionId = useId();
|
|
1392
1543
|
const choicesKey = props.choices.join("\0");
|
|
1393
1544
|
useEffect3(() => {
|
|
1394
1545
|
completedRef.current = false;
|
|
1546
|
+
telemetryReplayedRef.current = false;
|
|
1395
1547
|
setQuizPassed(false);
|
|
1396
1548
|
setSelected(null);
|
|
1397
1549
|
setSelectionCorrect(null);
|
|
1550
|
+
setCompletedScore(null);
|
|
1551
|
+
setCompletedMaxScore(null);
|
|
1398
1552
|
}, [checkId, props.answer, props.question, choicesKey]);
|
|
1399
1553
|
const passed = quizPassed;
|
|
1554
|
+
const resolveScores = () => {
|
|
1555
|
+
const maxScore = completedMaxScore ?? 1;
|
|
1556
|
+
if (quizPassed) {
|
|
1557
|
+
return { score: completedScore ?? maxScore, maxScore };
|
|
1558
|
+
}
|
|
1559
|
+
if (selected !== null && selectionCorrect) {
|
|
1560
|
+
return { score: completedMaxScore ?? maxScore, maxScore };
|
|
1561
|
+
}
|
|
1562
|
+
return { score: 0, maxScore };
|
|
1563
|
+
};
|
|
1564
|
+
const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
|
|
1565
|
+
if (!nextPassed || telemetryReplayedRef.current) return;
|
|
1566
|
+
telemetryReplayedRef.current = true;
|
|
1567
|
+
if (nextSelected !== null) {
|
|
1568
|
+
quiz.answer({
|
|
1569
|
+
checkId,
|
|
1570
|
+
question: props.question,
|
|
1571
|
+
choice: nextSelected,
|
|
1572
|
+
correct: nextCorrect ?? false
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
quiz.complete({
|
|
1576
|
+
checkId,
|
|
1577
|
+
score: nextScore,
|
|
1578
|
+
maxScore: nextMaxScore,
|
|
1579
|
+
passingScore: props.passingScore ?? nextMaxScore
|
|
1580
|
+
});
|
|
1581
|
+
};
|
|
1400
1582
|
const handle = useMemo5(
|
|
1401
1583
|
() => buildAssessmentHandle({
|
|
1402
1584
|
checkId,
|
|
1403
|
-
getScore: () =>
|
|
1404
|
-
|
|
1405
|
-
if (quizPassed && selected !== null) return maxScore;
|
|
1406
|
-
if (selected === null) return 0;
|
|
1407
|
-
return selectionCorrect ? maxScore : 0;
|
|
1408
|
-
},
|
|
1409
|
-
getMaxScore: () => 1,
|
|
1585
|
+
getScore: () => resolveScores().score,
|
|
1586
|
+
getMaxScore: () => resolveScores().maxScore,
|
|
1410
1587
|
getAnswerGiven: () => selected !== null,
|
|
1411
1588
|
resetTask: () => {
|
|
1412
1589
|
completedRef.current = false;
|
|
1590
|
+
telemetryReplayedRef.current = false;
|
|
1413
1591
|
setQuizPassed(false);
|
|
1414
1592
|
setSelected(null);
|
|
1415
1593
|
setSelectionCorrect(null);
|
|
1594
|
+
setCompletedScore(null);
|
|
1595
|
+
setCompletedMaxScore(null);
|
|
1416
1596
|
},
|
|
1417
1597
|
showSolutions: () => {
|
|
1418
1598
|
},
|
|
1419
|
-
getXAPIData: () =>
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1599
|
+
getXAPIData: () => {
|
|
1600
|
+
const { score, maxScore } = resolveScores();
|
|
1601
|
+
return {
|
|
1602
|
+
checkId,
|
|
1603
|
+
interactionType: "mcq",
|
|
1604
|
+
response: selected ?? void 0,
|
|
1605
|
+
correct: selectionCorrect ?? void 0,
|
|
1606
|
+
score,
|
|
1607
|
+
maxScore
|
|
1608
|
+
};
|
|
1609
|
+
},
|
|
1610
|
+
getCurrentState: () => ({
|
|
1611
|
+
selected,
|
|
1612
|
+
selectionCorrect,
|
|
1613
|
+
quizPassed,
|
|
1614
|
+
completedScore,
|
|
1615
|
+
completedMaxScore
|
|
1426
1616
|
}),
|
|
1427
|
-
getCurrentState: () => ({ selected, selectionCorrect, quizPassed }),
|
|
1428
1617
|
resume: (state) => {
|
|
1429
1618
|
const nextSelected = readStringField(state, "selected");
|
|
1430
1619
|
if (typeof nextSelected === "string" || nextSelected === null) setSelected(nextSelected);
|
|
@@ -1432,21 +1621,47 @@ function QuizInner(props, ref) {
|
|
|
1432
1621
|
if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
|
|
1433
1622
|
setSelectionCorrect(nextCorrect);
|
|
1434
1623
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1624
|
+
const nextCompletedScore = readNumberField(state, "completedScore");
|
|
1625
|
+
if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
|
|
1626
|
+
const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
|
|
1627
|
+
if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
|
|
1628
|
+
const nextPassed = readBooleanField(state, "quizPassed");
|
|
1629
|
+
if (nextPassed === true || nextPassed === false) {
|
|
1630
|
+
setQuizPassed(nextPassed);
|
|
1631
|
+
completedRef.current = nextPassed;
|
|
1632
|
+
if (nextPassed) {
|
|
1633
|
+
const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
|
|
1634
|
+
const score = nextCompletedScore ?? completedScore ?? maxScore;
|
|
1635
|
+
replayTelemetry(
|
|
1636
|
+
nextSelected ?? null,
|
|
1637
|
+
nextCorrect ?? null,
|
|
1638
|
+
nextPassed,
|
|
1639
|
+
score,
|
|
1640
|
+
maxScore
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1439
1644
|
}
|
|
1440
1645
|
}),
|
|
1441
|
-
[
|
|
1646
|
+
[
|
|
1647
|
+
checkId,
|
|
1648
|
+
completedMaxScore,
|
|
1649
|
+
completedScore,
|
|
1650
|
+
props.passingScore,
|
|
1651
|
+
props.question,
|
|
1652
|
+
quiz,
|
|
1653
|
+
quizPassed,
|
|
1654
|
+
selected,
|
|
1655
|
+
selectionCorrect
|
|
1656
|
+
]
|
|
1442
1657
|
);
|
|
1443
1658
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1444
1659
|
return /* @__PURE__ */ jsxs2("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
1445
|
-
/* @__PURE__ */
|
|
1660
|
+
/* @__PURE__ */ jsx6("p", { id: questionId, children: props.question }),
|
|
1446
1661
|
/* @__PURE__ */ jsxs2("fieldset", { "aria-labelledby": questionId, children: [
|
|
1447
|
-
/* @__PURE__ */
|
|
1662
|
+
/* @__PURE__ */ jsx6("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
|
|
1448
1663
|
props.choices.map((c, i) => /* @__PURE__ */ jsxs2("label", { style: { display: "block" }, children: [
|
|
1449
|
-
/* @__PURE__ */
|
|
1664
|
+
/* @__PURE__ */ jsx6(
|
|
1450
1665
|
"input",
|
|
1451
1666
|
{
|
|
1452
1667
|
type: "radio",
|
|
@@ -1471,9 +1686,12 @@ function QuizInner(props, ref) {
|
|
|
1471
1686
|
completedRef.current = true;
|
|
1472
1687
|
setQuizPassed(true);
|
|
1473
1688
|
const maxScore = custom?.maxScore ?? 1;
|
|
1689
|
+
const score = custom?.score ?? maxScore;
|
|
1690
|
+
setCompletedScore(score);
|
|
1691
|
+
setCompletedMaxScore(maxScore);
|
|
1474
1692
|
quiz.complete({
|
|
1475
1693
|
checkId,
|
|
1476
|
-
score
|
|
1694
|
+
score,
|
|
1477
1695
|
maxScore,
|
|
1478
1696
|
passingScore: props.passingScore ?? maxScore
|
|
1479
1697
|
});
|
|
@@ -1484,15 +1702,15 @@ function QuizInner(props, ref) {
|
|
|
1484
1702
|
c
|
|
1485
1703
|
] }, `${questionId}-${i}`))
|
|
1486
1704
|
] }),
|
|
1487
|
-
selected && selectionCorrect !== null ? /* @__PURE__ */
|
|
1705
|
+
selected && selectionCorrect !== null ? /* @__PURE__ */ jsx6("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
1488
1706
|
] });
|
|
1489
1707
|
}
|
|
1490
1708
|
var QuizInnerForwarded = forwardRef(QuizInner);
|
|
1491
1709
|
var Quiz = forwardRef(function Quiz2(props, ref) {
|
|
1492
|
-
return /* @__PURE__ */
|
|
1710
|
+
return /* @__PURE__ */ jsx6(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx6(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1493
1711
|
});
|
|
1494
1712
|
function KnowledgeCheck(props) {
|
|
1495
|
-
return /* @__PURE__ */
|
|
1713
|
+
return /* @__PURE__ */ jsx6(
|
|
1496
1714
|
Quiz,
|
|
1497
1715
|
{
|
|
1498
1716
|
checkId: props.checkId,
|
|
@@ -1508,16 +1726,16 @@ function resetQuizWarningsForTests() {
|
|
|
1508
1726
|
}
|
|
1509
1727
|
|
|
1510
1728
|
// src/components.tsx
|
|
1511
|
-
import { jsx as
|
|
1729
|
+
import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1512
1730
|
function Course(props) {
|
|
1513
1731
|
const courseId = useMemo6(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
|
|
1514
1732
|
const providerConfig = useMemo6(
|
|
1515
1733
|
() => ({ ...props.config, courseId }),
|
|
1516
1734
|
[props.config, courseId]
|
|
1517
1735
|
);
|
|
1518
|
-
return /* @__PURE__ */
|
|
1519
|
-
/* @__PURE__ */
|
|
1520
|
-
/* @__PURE__ */
|
|
1736
|
+
return /* @__PURE__ */ jsx7(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs3("section", { "aria-label": props.title, children: [
|
|
1737
|
+
/* @__PURE__ */ jsx7("h1", { children: props.title }),
|
|
1738
|
+
/* @__PURE__ */ jsx7("div", { children: props.children })
|
|
1521
1739
|
] }) });
|
|
1522
1740
|
}
|
|
1523
1741
|
function Lesson(props) {
|
|
@@ -1525,8 +1743,8 @@ function Lesson(props) {
|
|
|
1525
1743
|
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
1526
1744
|
const { setActiveLesson, config } = useLessonkit();
|
|
1527
1745
|
const { completeLesson } = useCompletion();
|
|
1528
|
-
const lessonMountGenerationRef =
|
|
1529
|
-
const liveCourseIdRef =
|
|
1746
|
+
const lessonMountGenerationRef = useRef5(0);
|
|
1747
|
+
const liveCourseIdRef = useRef5(config.courseId);
|
|
1530
1748
|
liveCourseIdRef.current = config.courseId;
|
|
1531
1749
|
useEffect4(() => {
|
|
1532
1750
|
const unregister = registerLessonMount(lessonId);
|
|
@@ -1553,9 +1771,9 @@ function Lesson(props) {
|
|
|
1553
1771
|
});
|
|
1554
1772
|
};
|
|
1555
1773
|
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
1556
|
-
return /* @__PURE__ */
|
|
1557
|
-
/* @__PURE__ */
|
|
1558
|
-
/* @__PURE__ */
|
|
1774
|
+
return /* @__PURE__ */ jsx7(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs3("article", { "aria-label": props.title, children: [
|
|
1775
|
+
/* @__PURE__ */ jsx7("h2", { children: props.title }),
|
|
1776
|
+
/* @__PURE__ */ jsx7("div", { children: props.children })
|
|
1559
1777
|
] }) });
|
|
1560
1778
|
}
|
|
1561
1779
|
function Scenario(props) {
|
|
@@ -1563,7 +1781,7 @@ function Scenario(props) {
|
|
|
1563
1781
|
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1564
1782
|
[props.blockId]
|
|
1565
1783
|
);
|
|
1566
|
-
return /* @__PURE__ */
|
|
1784
|
+
return /* @__PURE__ */ jsx7("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
1567
1785
|
}
|
|
1568
1786
|
function Reflection(props) {
|
|
1569
1787
|
const blockId = useMemo6(
|
|
@@ -1580,10 +1798,10 @@ function Reflection(props) {
|
|
|
1580
1798
|
props.onChange?.(event.target.value);
|
|
1581
1799
|
};
|
|
1582
1800
|
return /* @__PURE__ */ jsxs3("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
|
|
1583
|
-
props.prompt ? /* @__PURE__ */
|
|
1584
|
-
props.hint ? /* @__PURE__ */
|
|
1801
|
+
props.prompt ? /* @__PURE__ */ jsx7("p", { id: promptId, children: props.prompt }) : null,
|
|
1802
|
+
props.hint ? /* @__PURE__ */ jsx7("p", { id: hintId, style: visuallyHiddenStyle2, children: props.hint }) : null,
|
|
1585
1803
|
props.children,
|
|
1586
|
-
/* @__PURE__ */
|
|
1804
|
+
/* @__PURE__ */ jsx7(
|
|
1587
1805
|
"textarea",
|
|
1588
1806
|
{
|
|
1589
1807
|
value,
|
|
@@ -1601,7 +1819,7 @@ function ProgressTracker(props) {
|
|
|
1601
1819
|
if (props.totalLessons != null) {
|
|
1602
1820
|
const total = props.totalLessons;
|
|
1603
1821
|
const displayed = Math.min(completed, total);
|
|
1604
|
-
return /* @__PURE__ */
|
|
1822
|
+
return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx7(
|
|
1605
1823
|
"div",
|
|
1606
1824
|
{
|
|
1607
1825
|
role: "progressbar",
|
|
@@ -1618,15 +1836,15 @@ function ProgressTracker(props) {
|
|
|
1618
1836
|
}
|
|
1619
1837
|
) });
|
|
1620
1838
|
}
|
|
1621
|
-
return /* @__PURE__ */
|
|
1839
|
+
return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs3("p", { children: [
|
|
1622
1840
|
"Lessons completed: ",
|
|
1623
1841
|
completed
|
|
1624
1842
|
] }) });
|
|
1625
1843
|
}
|
|
1626
1844
|
|
|
1627
1845
|
// src/blocks/TrueFalse.tsx
|
|
1628
|
-
import
|
|
1629
|
-
import { jsx as
|
|
1846
|
+
import React9, { forwardRef as forwardRef2, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef6, useState as useState5 } from "react";
|
|
1847
|
+
import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1630
1848
|
var INTERACTION = "trueFalse";
|
|
1631
1849
|
function TrueFalseInner(props, ref) {
|
|
1632
1850
|
const { enclosingLessonId } = props;
|
|
@@ -1638,38 +1856,81 @@ function TrueFalseInner(props, ref) {
|
|
|
1638
1856
|
const [selectionCorrect, setSelectionCorrect] = useState5(null);
|
|
1639
1857
|
const [showSolutions, setShowSolutions] = useState5(false);
|
|
1640
1858
|
const [passed, setPassed] = useState5(false);
|
|
1641
|
-
const
|
|
1642
|
-
const
|
|
1859
|
+
const [completedScore, setCompletedScore] = useState5(null);
|
|
1860
|
+
const [completedMaxScore, setCompletedMaxScore] = useState5(null);
|
|
1861
|
+
const completedRef = useRef6(false);
|
|
1862
|
+
const telemetryReplayedRef = useRef6(false);
|
|
1863
|
+
const questionId = React9.useId();
|
|
1643
1864
|
const reset = () => {
|
|
1644
1865
|
completedRef.current = false;
|
|
1866
|
+
telemetryReplayedRef.current = false;
|
|
1645
1867
|
setPassed(false);
|
|
1646
1868
|
setSelected(null);
|
|
1647
1869
|
setSelectionCorrect(null);
|
|
1648
1870
|
setShowSolutions(false);
|
|
1871
|
+
setCompletedScore(null);
|
|
1872
|
+
setCompletedMaxScore(null);
|
|
1649
1873
|
};
|
|
1650
1874
|
useEffect5(() => {
|
|
1651
1875
|
reset();
|
|
1652
1876
|
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
|
|
1877
|
+
const resolveScores = () => {
|
|
1878
|
+
const maxScore = completedMaxScore ?? 1;
|
|
1879
|
+
if (passed) {
|
|
1880
|
+
return { score: completedScore ?? maxScore, maxScore };
|
|
1881
|
+
}
|
|
1882
|
+
if (selectionCorrect) {
|
|
1883
|
+
return { score: completedMaxScore ?? maxScore, maxScore };
|
|
1884
|
+
}
|
|
1885
|
+
return { score: 0, maxScore };
|
|
1886
|
+
};
|
|
1887
|
+
const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
|
|
1888
|
+
if (!nextPassed || telemetryReplayedRef.current) return;
|
|
1889
|
+
telemetryReplayedRef.current = true;
|
|
1890
|
+
if (nextSelected !== null) {
|
|
1891
|
+
assessment.answer({
|
|
1892
|
+
checkId,
|
|
1893
|
+
interactionType: INTERACTION,
|
|
1894
|
+
question: props.question,
|
|
1895
|
+
response: nextSelected,
|
|
1896
|
+
correct: nextCorrect ?? false
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
assessment.complete({
|
|
1900
|
+
checkId,
|
|
1901
|
+
interactionType: INTERACTION,
|
|
1902
|
+
score: nextScore,
|
|
1903
|
+
maxScore: nextMaxScore,
|
|
1904
|
+
passingScore: props.passingScore ?? nextMaxScore
|
|
1905
|
+
});
|
|
1906
|
+
};
|
|
1653
1907
|
const handle = useMemo7(
|
|
1654
1908
|
() => buildAssessmentHandle({
|
|
1655
1909
|
checkId,
|
|
1656
|
-
getScore: () =>
|
|
1657
|
-
|
|
1658
|
-
return passed ? maxScore : selected === null ? 0 : selected === props.answer ? maxScore : 0;
|
|
1659
|
-
},
|
|
1660
|
-
getMaxScore: () => 1,
|
|
1910
|
+
getScore: () => resolveScores().score,
|
|
1911
|
+
getMaxScore: () => resolveScores().maxScore,
|
|
1661
1912
|
getAnswerGiven: () => selected !== null,
|
|
1662
1913
|
resetTask: reset,
|
|
1663
1914
|
showSolutions: () => setShowSolutions(true),
|
|
1664
|
-
getXAPIData: () =>
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1915
|
+
getXAPIData: () => {
|
|
1916
|
+
const { score, maxScore } = resolveScores();
|
|
1917
|
+
return {
|
|
1918
|
+
checkId,
|
|
1919
|
+
interactionType: INTERACTION,
|
|
1920
|
+
response: selected ?? void 0,
|
|
1921
|
+
correct: selectionCorrect ?? void 0,
|
|
1922
|
+
score,
|
|
1923
|
+
maxScore
|
|
1924
|
+
};
|
|
1925
|
+
},
|
|
1926
|
+
getCurrentState: () => ({
|
|
1927
|
+
selected,
|
|
1928
|
+
selectionCorrect,
|
|
1929
|
+
passed,
|
|
1930
|
+
showSolutions,
|
|
1931
|
+
completedScore,
|
|
1932
|
+
completedMaxScore
|
|
1671
1933
|
}),
|
|
1672
|
-
getCurrentState: () => ({ selected, selectionCorrect, passed, showSolutions }),
|
|
1673
1934
|
resume: (state) => {
|
|
1674
1935
|
const nextSelected = readBooleanField(state, "selected");
|
|
1675
1936
|
if (nextSelected === true || nextSelected === false || nextSelected === null) {
|
|
@@ -1679,14 +1940,35 @@ function TrueFalseInner(props, ref) {
|
|
|
1679
1940
|
if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
|
|
1680
1941
|
setSelectionCorrect(nextCorrect);
|
|
1681
1942
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1943
|
+
const nextCompletedScore = readNumberField(state, "completedScore");
|
|
1944
|
+
if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
|
|
1945
|
+
const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
|
|
1946
|
+
if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
|
|
1947
|
+
const nextPassed = readBooleanField(state, "passed");
|
|
1948
|
+
if (nextPassed === true || nextPassed === false) {
|
|
1949
|
+
setPassed(nextPassed);
|
|
1950
|
+
completedRef.current = nextPassed;
|
|
1951
|
+
if (nextPassed) {
|
|
1952
|
+
const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
|
|
1953
|
+
const score = nextCompletedScore ?? completedScore ?? maxScore;
|
|
1954
|
+
replayTelemetry(nextSelected ?? null, nextCorrect ?? null, nextPassed, score, maxScore);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1686
1957
|
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
1687
1958
|
}
|
|
1688
1959
|
}),
|
|
1689
|
-
[
|
|
1960
|
+
[
|
|
1961
|
+
assessment,
|
|
1962
|
+
checkId,
|
|
1963
|
+
completedMaxScore,
|
|
1964
|
+
completedScore,
|
|
1965
|
+
passed,
|
|
1966
|
+
props.passingScore,
|
|
1967
|
+
props.question,
|
|
1968
|
+
selected,
|
|
1969
|
+
selectionCorrect,
|
|
1970
|
+
showSolutions
|
|
1971
|
+
]
|
|
1690
1972
|
);
|
|
1691
1973
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1692
1974
|
const submit = (value) => {
|
|
@@ -1705,6 +1987,8 @@ function TrueFalseInner(props, ref) {
|
|
|
1705
1987
|
if (scored.passed && !completedRef.current) {
|
|
1706
1988
|
completedRef.current = true;
|
|
1707
1989
|
setPassed(true);
|
|
1990
|
+
setCompletedScore(scored.score);
|
|
1991
|
+
setCompletedMaxScore(scored.maxScore);
|
|
1708
1992
|
assessment.complete({
|
|
1709
1993
|
checkId,
|
|
1710
1994
|
interactionType: INTERACTION,
|
|
@@ -1716,11 +2000,11 @@ function TrueFalseInner(props, ref) {
|
|
|
1716
2000
|
};
|
|
1717
2001
|
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
1718
2002
|
return /* @__PURE__ */ jsxs4("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
|
|
1719
|
-
/* @__PURE__ */
|
|
2003
|
+
/* @__PURE__ */ jsx8("p", { id: questionId, children: props.question }),
|
|
1720
2004
|
/* @__PURE__ */ jsxs4("fieldset", { "aria-labelledby": questionId, children: [
|
|
1721
|
-
/* @__PURE__ */
|
|
2005
|
+
/* @__PURE__ */ jsx8("legend", { className: "lk-visually-hidden", children: "True or False" }),
|
|
1722
2006
|
/* @__PURE__ */ jsxs4("label", { style: { display: "block", marginRight: "1rem" }, children: [
|
|
1723
|
-
/* @__PURE__ */
|
|
2007
|
+
/* @__PURE__ */ jsx8(
|
|
1724
2008
|
"input",
|
|
1725
2009
|
{
|
|
1726
2010
|
type: "radio",
|
|
@@ -1733,7 +2017,7 @@ function TrueFalseInner(props, ref) {
|
|
|
1733
2017
|
"True"
|
|
1734
2018
|
] }),
|
|
1735
2019
|
/* @__PURE__ */ jsxs4("label", { style: { display: "block" }, children: [
|
|
1736
|
-
/* @__PURE__ */
|
|
2020
|
+
/* @__PURE__ */ jsx8(
|
|
1737
2021
|
"input",
|
|
1738
2022
|
{
|
|
1739
2023
|
type: "radio",
|
|
@@ -1748,21 +2032,21 @@ function TrueFalseInner(props, ref) {
|
|
|
1748
2032
|
] }),
|
|
1749
2033
|
reveal ? /* @__PURE__ */ jsxs4("p", { children: [
|
|
1750
2034
|
"Correct answer: ",
|
|
1751
|
-
/* @__PURE__ */
|
|
2035
|
+
/* @__PURE__ */ jsx8("strong", { children: props.answer ? "True" : "False" })
|
|
1752
2036
|
] }) : null,
|
|
1753
|
-
selected !== null && selectionCorrect !== null ? /* @__PURE__ */
|
|
1754
|
-
props.enableRetry && passed ? /* @__PURE__ */
|
|
1755
|
-
props.enableSolutionsButton && !reveal ? /* @__PURE__ */
|
|
2037
|
+
selected !== null && selectionCorrect !== null ? /* @__PURE__ */ jsx8("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
|
|
2038
|
+
props.enableRetry && passed ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2039
|
+
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1756
2040
|
] });
|
|
1757
2041
|
}
|
|
1758
2042
|
var TrueFalseInnerForwarded = forwardRef2(TrueFalseInner);
|
|
1759
2043
|
var TrueFalse = forwardRef2(function TrueFalse2(props, ref) {
|
|
1760
|
-
return /* @__PURE__ */
|
|
2044
|
+
return /* @__PURE__ */ jsx8(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx8(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1761
2045
|
});
|
|
1762
2046
|
|
|
1763
2047
|
// src/blocks/MarkTheWords.tsx
|
|
1764
|
-
import
|
|
1765
|
-
import { jsx as
|
|
2048
|
+
import React10, { forwardRef as forwardRef3, useEffect as useEffect6, useMemo as useMemo8, useRef as useRef7, useState as useState6 } from "react";
|
|
2049
|
+
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1766
2050
|
var INTERACTION2 = "markTheWords";
|
|
1767
2051
|
function tokenize(text) {
|
|
1768
2052
|
return text.split(/(\s+)/).filter((t) => t.length > 0);
|
|
@@ -1778,7 +2062,7 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1778
2062
|
const [marked, setMarked] = useState6(() => /* @__PURE__ */ new Set());
|
|
1779
2063
|
const [passed, setPassed] = useState6(false);
|
|
1780
2064
|
const [showSolutions, setShowSolutions] = useState6(false);
|
|
1781
|
-
const completedRef =
|
|
2065
|
+
const completedRef = useRef7(false);
|
|
1782
2066
|
const reset = () => {
|
|
1783
2067
|
completedRef.current = false;
|
|
1784
2068
|
setPassed(false);
|
|
@@ -1857,7 +2141,7 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1857
2141
|
interactionType: INTERACTION2,
|
|
1858
2142
|
question: props.text,
|
|
1859
2143
|
response: [...marked].map((i) => tokens[i]),
|
|
1860
|
-
correct:
|
|
2144
|
+
correct: passedThreshold
|
|
1861
2145
|
});
|
|
1862
2146
|
assessment.complete({
|
|
1863
2147
|
checkId,
|
|
@@ -1882,17 +2166,17 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1882
2166
|
return /* @__PURE__ */ jsxs5("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
|
|
1883
2167
|
!hasTargets ? /* @__PURE__ */ jsxs5("p", { role: "alert", children: [
|
|
1884
2168
|
"No words in this sentence match ",
|
|
1885
|
-
/* @__PURE__ */
|
|
2169
|
+
/* @__PURE__ */ jsx9("code", { children: "correctWords" }),
|
|
1886
2170
|
". Check spelling and capitalization in the source text."
|
|
1887
2171
|
] }) : null,
|
|
1888
|
-
/* @__PURE__ */
|
|
1889
|
-
/* @__PURE__ */
|
|
2172
|
+
/* @__PURE__ */ jsx9("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
|
|
2173
|
+
/* @__PURE__ */ jsx9("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
|
|
1890
2174
|
const isWord = !/^\s+$/.test(token);
|
|
1891
2175
|
const isTarget = isWord && correctSet.has(token.toLowerCase());
|
|
1892
|
-
if (!isTarget) return /* @__PURE__ */
|
|
2176
|
+
if (!isTarget) return /* @__PURE__ */ jsx9(React10.Fragment, { children: token }, i);
|
|
1893
2177
|
const selected = marked.has(i);
|
|
1894
2178
|
const solution = showSolutions || passed && props.enableSolutionsButton;
|
|
1895
|
-
return /* @__PURE__ */
|
|
2179
|
+
return /* @__PURE__ */ jsx9(
|
|
1896
2180
|
"button",
|
|
1897
2181
|
{
|
|
1898
2182
|
type: "button",
|
|
@@ -1910,18 +2194,18 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1910
2194
|
i
|
|
1911
2195
|
);
|
|
1912
2196
|
}) }),
|
|
1913
|
-
allMarked ? /* @__PURE__ */
|
|
1914
|
-
props.enableRetry && passed ? /* @__PURE__ */
|
|
1915
|
-
props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */
|
|
2197
|
+
allMarked ? /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
|
|
2198
|
+
props.enableRetry && passed ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2199
|
+
props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1916
2200
|
] });
|
|
1917
2201
|
}
|
|
1918
2202
|
var MarkTheWordsInnerForwarded = forwardRef3(MarkTheWordsInner);
|
|
1919
2203
|
var MarkTheWords = forwardRef3(function MarkTheWords2(props, ref) {
|
|
1920
|
-
return /* @__PURE__ */
|
|
2204
|
+
return /* @__PURE__ */ jsx9(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx9(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1921
2205
|
});
|
|
1922
2206
|
|
|
1923
2207
|
// src/blocks/FillInTheBlanks.tsx
|
|
1924
|
-
import
|
|
2208
|
+
import React11, { forwardRef as forwardRef4, useEffect as useEffect7, useMemo as useMemo9, useRef as useRef8, useState as useState7 } from "react";
|
|
1925
2209
|
|
|
1926
2210
|
// src/assessment/internal/parseStarDelimitedTemplate.ts
|
|
1927
2211
|
function parseStarDelimitedTemplate(template, idPrefix) {
|
|
@@ -1942,7 +2226,7 @@ function parseStarDelimitedTemplate(template, idPrefix) {
|
|
|
1942
2226
|
}
|
|
1943
2227
|
|
|
1944
2228
|
// src/blocks/FillInTheBlanks.tsx
|
|
1945
|
-
import { jsx as
|
|
2229
|
+
import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1946
2230
|
var INTERACTION3 = "fillInBlanks";
|
|
1947
2231
|
function parseTemplate(template) {
|
|
1948
2232
|
const { parts, values } = parseStarDelimitedTemplate(template, "blank");
|
|
@@ -1961,14 +2245,20 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
1961
2245
|
);
|
|
1962
2246
|
const [passed, setPassed] = useState7(false);
|
|
1963
2247
|
const [showSolutions, setShowSolutions] = useState7(false);
|
|
1964
|
-
const
|
|
1965
|
-
const
|
|
2248
|
+
const [submitted, setSubmitted] = useState7(false);
|
|
2249
|
+
const completedRef = useRef8(false);
|
|
2250
|
+
const answeredRef = useRef8(false);
|
|
2251
|
+
const checkSnapshotRef = useRef8(null);
|
|
2252
|
+
const telemetryReplayedRef = useRef8(false);
|
|
1966
2253
|
const reset = () => {
|
|
1967
2254
|
completedRef.current = false;
|
|
1968
2255
|
answeredRef.current = false;
|
|
2256
|
+
checkSnapshotRef.current = null;
|
|
2257
|
+
telemetryReplayedRef.current = false;
|
|
1969
2258
|
setPassed(false);
|
|
1970
2259
|
setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
|
|
1971
2260
|
setShowSolutions(false);
|
|
2261
|
+
setSubmitted(false);
|
|
1972
2262
|
};
|
|
1973
2263
|
useEffect7(() => {
|
|
1974
2264
|
reset();
|
|
@@ -1981,6 +2271,31 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
1981
2271
|
});
|
|
1982
2272
|
const maxScore = blanks.length;
|
|
1983
2273
|
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
2274
|
+
const replayTelemetry = (nextValues, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
|
|
2275
|
+
if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
|
|
2276
|
+
telemetryReplayedRef.current = true;
|
|
2277
|
+
const nextPassedThreshold = meetsPassingThreshold(
|
|
2278
|
+
nextScore,
|
|
2279
|
+
nextMaxScore || 1,
|
|
2280
|
+
props.passingScore
|
|
2281
|
+
);
|
|
2282
|
+
assessment.answer({
|
|
2283
|
+
checkId,
|
|
2284
|
+
interactionType: INTERACTION3,
|
|
2285
|
+
question: props.template,
|
|
2286
|
+
response: nextValues,
|
|
2287
|
+
correct: nextPassedThreshold
|
|
2288
|
+
});
|
|
2289
|
+
if (nextPassed || nextPassedThreshold) {
|
|
2290
|
+
assessment.complete({
|
|
2291
|
+
checkId,
|
|
2292
|
+
interactionType: INTERACTION3,
|
|
2293
|
+
score: nextScore,
|
|
2294
|
+
maxScore: nextMaxScore,
|
|
2295
|
+
passingScore: props.passingScore ?? nextMaxScore
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
};
|
|
1984
2299
|
const handle = useMemo9(
|
|
1985
2300
|
() => buildAssessmentHandle({
|
|
1986
2301
|
checkId,
|
|
@@ -1997,19 +2312,36 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
1997
2312
|
score,
|
|
1998
2313
|
maxScore: maxScore || 1
|
|
1999
2314
|
}),
|
|
2000
|
-
getCurrentState: () => ({ values, passed, showSolutions }),
|
|
2315
|
+
getCurrentState: () => ({ values, passed, showSolutions, submitted }),
|
|
2001
2316
|
resume: (state) => {
|
|
2002
2317
|
const raw = state.values;
|
|
2003
|
-
|
|
2318
|
+
let nextValues = values;
|
|
2319
|
+
if (raw && typeof raw === "object") {
|
|
2320
|
+
nextValues = { ...raw };
|
|
2321
|
+
setValues(nextValues);
|
|
2322
|
+
}
|
|
2323
|
+
let nextPassed = passed;
|
|
2324
|
+
let nextSubmitted = submitted;
|
|
2004
2325
|
readBooleanStateField(state, "passed", (value) => {
|
|
2326
|
+
nextPassed = value;
|
|
2005
2327
|
setPassed(value);
|
|
2006
2328
|
completedRef.current = value;
|
|
2007
2329
|
answeredRef.current = value;
|
|
2008
2330
|
});
|
|
2009
2331
|
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
2332
|
+
readBooleanStateField(state, "submitted", (value) => {
|
|
2333
|
+
nextSubmitted = value;
|
|
2334
|
+
setSubmitted(value);
|
|
2335
|
+
if (value) answeredRef.current = true;
|
|
2336
|
+
});
|
|
2337
|
+
let nextScore = 0;
|
|
2338
|
+
blanks.forEach((b) => {
|
|
2339
|
+
if ((nextValues[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) nextScore += 1;
|
|
2340
|
+
});
|
|
2341
|
+
replayTelemetry(nextValues, nextPassed, nextSubmitted, nextScore, blanks.length);
|
|
2010
2342
|
}
|
|
2011
2343
|
}),
|
|
2012
|
-
[allFilled, checkId, maxScore, passed, passedThreshold, score, showSolutions, values]
|
|
2344
|
+
[allFilled, assessment, blanks, checkId, maxScore, passed, passedThreshold, props.passingScore, props.template, score, showSolutions, submitted, values]
|
|
2013
2345
|
);
|
|
2014
2346
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2015
2347
|
const check = () => {
|
|
@@ -2020,16 +2352,19 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2020
2352
|
return;
|
|
2021
2353
|
}
|
|
2022
2354
|
if (!allFilled) return;
|
|
2023
|
-
if (
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2355
|
+
if (passed) return;
|
|
2356
|
+
const snapshot = JSON.stringify(values);
|
|
2357
|
+
if (checkSnapshotRef.current === snapshot) return;
|
|
2358
|
+
checkSnapshotRef.current = snapshot;
|
|
2359
|
+
answeredRef.current = true;
|
|
2360
|
+
setSubmitted(true);
|
|
2361
|
+
assessment.answer({
|
|
2362
|
+
checkId,
|
|
2363
|
+
interactionType: INTERACTION3,
|
|
2364
|
+
question: props.template,
|
|
2365
|
+
response: values,
|
|
2366
|
+
correct: passedThreshold
|
|
2367
|
+
});
|
|
2033
2368
|
if (passedThreshold && !completedRef.current) {
|
|
2034
2369
|
completedRef.current = true;
|
|
2035
2370
|
setPassed(true);
|
|
@@ -2043,19 +2378,23 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2043
2378
|
}
|
|
2044
2379
|
};
|
|
2045
2380
|
useEffect7(() => {
|
|
2046
|
-
if (!allFilled)
|
|
2381
|
+
if (!allFilled) {
|
|
2382
|
+
answeredRef.current = false;
|
|
2383
|
+
checkSnapshotRef.current = null;
|
|
2384
|
+
setSubmitted(false);
|
|
2385
|
+
}
|
|
2047
2386
|
}, [allFilled]);
|
|
2048
2387
|
useEffect7(() => {
|
|
2049
|
-
if (props.autoCheck && allFilled) check();
|
|
2050
|
-
}, [allFilled, props.autoCheck, values, passedThreshold]);
|
|
2388
|
+
if (props.autoCheck && allFilled && !passed) check();
|
|
2389
|
+
}, [allFilled, props.autoCheck, values, passedThreshold, passed]);
|
|
2051
2390
|
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
2052
2391
|
return /* @__PURE__ */ jsxs6("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
|
|
2053
|
-
/* @__PURE__ */
|
|
2392
|
+
/* @__PURE__ */ jsx10("p", { children: parsed.parts.map((part, i) => {
|
|
2054
2393
|
const blank = blanks.find((b) => b.id === part);
|
|
2055
|
-
if (!blank) return /* @__PURE__ */
|
|
2394
|
+
if (!blank) return /* @__PURE__ */ jsx10(React11.Fragment, { children: part }, i);
|
|
2056
2395
|
return /* @__PURE__ */ jsxs6("label", { style: { margin: "0 0.25em" }, children: [
|
|
2057
|
-
/* @__PURE__ */
|
|
2058
|
-
/* @__PURE__ */
|
|
2396
|
+
/* @__PURE__ */ jsx10("span", { className: "lk-visually-hidden", children: blank.answer }),
|
|
2397
|
+
/* @__PURE__ */ jsx10(
|
|
2059
2398
|
"input",
|
|
2060
2399
|
{
|
|
2061
2400
|
type: "text",
|
|
@@ -2071,23 +2410,23 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2071
2410
|
)
|
|
2072
2411
|
] }, blank.id);
|
|
2073
2412
|
}) }),
|
|
2074
|
-
!props.autoCheck ? /* @__PURE__ */
|
|
2075
|
-
!hasBlanks ? /* @__PURE__ */
|
|
2076
|
-
|
|
2077
|
-
props.enableRetry && passed ? /* @__PURE__ */
|
|
2078
|
-
props.enableSolutionsButton && !reveal ? /* @__PURE__ */
|
|
2413
|
+
!props.autoCheck ? /* @__PURE__ */ jsx10("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
|
|
2414
|
+
!hasBlanks ? /* @__PURE__ */ jsx10("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
|
|
2415
|
+
submitted ? /* @__PURE__ */ jsx10("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
|
|
2416
|
+
props.enableRetry && passed ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2417
|
+
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
2079
2418
|
] });
|
|
2080
2419
|
}
|
|
2081
2420
|
var FillInTheBlanksInnerForwarded = forwardRef4(FillInTheBlanksInner);
|
|
2082
2421
|
var FillInTheBlanks = forwardRef4(
|
|
2083
2422
|
function FillInTheBlanks2(props, ref) {
|
|
2084
|
-
return /* @__PURE__ */
|
|
2423
|
+
return /* @__PURE__ */ jsx10(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx10(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2085
2424
|
}
|
|
2086
2425
|
);
|
|
2087
2426
|
|
|
2088
2427
|
// src/blocks/DragTheWords.tsx
|
|
2089
|
-
import
|
|
2090
|
-
import { jsx as
|
|
2428
|
+
import React12, { forwardRef as forwardRef5, useEffect as useEffect8, useMemo as useMemo10, useRef as useRef9, useState as useState8 } from "react";
|
|
2429
|
+
import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2091
2430
|
var INTERACTION4 = "dragTheWords";
|
|
2092
2431
|
function parseZones(template) {
|
|
2093
2432
|
const { parts, values } = parseStarDelimitedTemplate(template, "zone");
|
|
@@ -2103,12 +2442,18 @@ function DragTheWordsInner(props, ref) {
|
|
|
2103
2442
|
const [pool, setPool] = useState8(() => [...props.words]);
|
|
2104
2443
|
const [keyboardWord, setKeyboardWord] = useState8(null);
|
|
2105
2444
|
const [passed, setPassed] = useState8(false);
|
|
2106
|
-
const
|
|
2107
|
-
const
|
|
2445
|
+
const [submitted, setSubmitted] = useState8(false);
|
|
2446
|
+
const completedRef = useRef9(false);
|
|
2447
|
+
const answeredRef = useRef9(false);
|
|
2448
|
+
const checkSnapshotRef = useRef9(null);
|
|
2449
|
+
const telemetryReplayedRef = useRef9(false);
|
|
2108
2450
|
const reset = () => {
|
|
2109
2451
|
completedRef.current = false;
|
|
2110
2452
|
answeredRef.current = false;
|
|
2453
|
+
checkSnapshotRef.current = null;
|
|
2454
|
+
telemetryReplayedRef.current = false;
|
|
2111
2455
|
setPassed(false);
|
|
2456
|
+
setSubmitted(false);
|
|
2112
2457
|
setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
|
|
2113
2458
|
setPool([...props.words]);
|
|
2114
2459
|
setKeyboardWord(null);
|
|
@@ -2124,6 +2469,31 @@ function DragTheWordsInner(props, ref) {
|
|
|
2124
2469
|
});
|
|
2125
2470
|
const maxScore = answers.length;
|
|
2126
2471
|
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
2472
|
+
const replayTelemetry = (nextZones, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
|
|
2473
|
+
if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
|
|
2474
|
+
telemetryReplayedRef.current = true;
|
|
2475
|
+
const nextPassedThreshold = meetsPassingThreshold(
|
|
2476
|
+
nextScore,
|
|
2477
|
+
nextMaxScore || 1,
|
|
2478
|
+
props.passingScore
|
|
2479
|
+
);
|
|
2480
|
+
assessment.answer({
|
|
2481
|
+
checkId,
|
|
2482
|
+
interactionType: INTERACTION4,
|
|
2483
|
+
question: props.template,
|
|
2484
|
+
response: nextZones,
|
|
2485
|
+
correct: nextPassedThreshold
|
|
2486
|
+
});
|
|
2487
|
+
if (nextPassed || nextPassedThreshold) {
|
|
2488
|
+
assessment.complete({
|
|
2489
|
+
checkId,
|
|
2490
|
+
interactionType: INTERACTION4,
|
|
2491
|
+
score: nextScore,
|
|
2492
|
+
maxScore: nextMaxScore,
|
|
2493
|
+
passingScore: props.passingScore ?? nextMaxScore
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2127
2497
|
const handle = useMemo10(
|
|
2128
2498
|
() => buildAssessmentHandle({
|
|
2129
2499
|
checkId,
|
|
@@ -2141,21 +2511,38 @@ function DragTheWordsInner(props, ref) {
|
|
|
2141
2511
|
score,
|
|
2142
2512
|
maxScore: maxScore || 1
|
|
2143
2513
|
}),
|
|
2144
|
-
getCurrentState: () => ({ zones, pool, passed, keyboardWord }),
|
|
2514
|
+
getCurrentState: () => ({ zones, pool, passed, keyboardWord, submitted }),
|
|
2145
2515
|
resume: (state) => {
|
|
2146
2516
|
const rawZones = state.zones;
|
|
2147
|
-
|
|
2517
|
+
let nextZones = zones;
|
|
2518
|
+
if (rawZones && typeof rawZones === "object") {
|
|
2519
|
+
nextZones = { ...rawZones };
|
|
2520
|
+
setZones(nextZones);
|
|
2521
|
+
}
|
|
2148
2522
|
if (Array.isArray(state.pool)) setPool([...state.pool]);
|
|
2523
|
+
let nextPassed = passed;
|
|
2524
|
+
let nextSubmitted = submitted;
|
|
2149
2525
|
readBooleanStateField(state, "passed", (value) => {
|
|
2526
|
+
nextPassed = value;
|
|
2150
2527
|
setPassed(value);
|
|
2151
2528
|
completedRef.current = value;
|
|
2152
2529
|
answeredRef.current = value;
|
|
2153
2530
|
});
|
|
2531
|
+
readBooleanStateField(state, "submitted", (value) => {
|
|
2532
|
+
nextSubmitted = value;
|
|
2533
|
+
setSubmitted(value);
|
|
2534
|
+
if (value) answeredRef.current = true;
|
|
2535
|
+
});
|
|
2154
2536
|
const kw = state.keyboardWord;
|
|
2155
2537
|
if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
|
|
2538
|
+
let nextScore = 0;
|
|
2539
|
+
answers.forEach((ans, i) => {
|
|
2540
|
+
if ((nextZones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) nextScore += 1;
|
|
2541
|
+
});
|
|
2542
|
+
replayTelemetry(nextZones, nextPassed, nextSubmitted, nextScore, answers.length);
|
|
2156
2543
|
}
|
|
2157
2544
|
}),
|
|
2158
|
-
[allFilled, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, score, zones]
|
|
2545
|
+
[allFilled, answers, assessment, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, props.passingScore, props.template, score, submitted, zones]
|
|
2159
2546
|
);
|
|
2160
2547
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2161
2548
|
const placeInZone = (zoneId, word) => {
|
|
@@ -2185,16 +2572,19 @@ function DragTheWordsInner(props, ref) {
|
|
|
2185
2572
|
return;
|
|
2186
2573
|
}
|
|
2187
2574
|
if (!allFilled) return;
|
|
2188
|
-
if (
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2575
|
+
if (passed) return;
|
|
2576
|
+
const snapshot = JSON.stringify(zones);
|
|
2577
|
+
if (checkSnapshotRef.current === snapshot) return;
|
|
2578
|
+
checkSnapshotRef.current = snapshot;
|
|
2579
|
+
answeredRef.current = true;
|
|
2580
|
+
setSubmitted(true);
|
|
2581
|
+
assessment.answer({
|
|
2582
|
+
checkId,
|
|
2583
|
+
interactionType: INTERACTION4,
|
|
2584
|
+
question: props.template,
|
|
2585
|
+
response: zones,
|
|
2586
|
+
correct: passedThreshold
|
|
2587
|
+
});
|
|
2198
2588
|
if (passedThreshold && !completedRef.current) {
|
|
2199
2589
|
completedRef.current = true;
|
|
2200
2590
|
setPassed(true);
|
|
@@ -2208,14 +2598,18 @@ function DragTheWordsInner(props, ref) {
|
|
|
2208
2598
|
}
|
|
2209
2599
|
};
|
|
2210
2600
|
useEffect8(() => {
|
|
2211
|
-
if (!allFilled)
|
|
2601
|
+
if (!allFilled) {
|
|
2602
|
+
answeredRef.current = false;
|
|
2603
|
+
checkSnapshotRef.current = null;
|
|
2604
|
+
setSubmitted(false);
|
|
2605
|
+
}
|
|
2212
2606
|
}, [allFilled]);
|
|
2213
2607
|
useEffect8(() => {
|
|
2214
|
-
if (props.autoCheck && allFilled) check();
|
|
2215
|
-
}, [allFilled, props.autoCheck, zones, passedThreshold]);
|
|
2608
|
+
if (props.autoCheck && allFilled && !passed) check();
|
|
2609
|
+
}, [allFilled, props.autoCheck, zones, passedThreshold, passed]);
|
|
2216
2610
|
return /* @__PURE__ */ jsxs7("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
|
|
2217
|
-
/* @__PURE__ */
|
|
2218
|
-
/* @__PURE__ */
|
|
2611
|
+
/* @__PURE__ */ jsx11("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
|
|
2612
|
+
/* @__PURE__ */ jsx11("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ jsx11(
|
|
2219
2613
|
"button",
|
|
2220
2614
|
{
|
|
2221
2615
|
type: "button",
|
|
@@ -2229,9 +2623,9 @@ function DragTheWordsInner(props, ref) {
|
|
|
2229
2623
|
},
|
|
2230
2624
|
word
|
|
2231
2625
|
)) }),
|
|
2232
|
-
/* @__PURE__ */
|
|
2233
|
-
if (!part.startsWith("zone-")) return /* @__PURE__ */
|
|
2234
|
-
return /* @__PURE__ */
|
|
2626
|
+
/* @__PURE__ */ jsx11("p", { children: parts.map((part, i) => {
|
|
2627
|
+
if (!part.startsWith("zone-")) return /* @__PURE__ */ jsx11(React12.Fragment, { children: part }, i);
|
|
2628
|
+
return /* @__PURE__ */ jsx11(
|
|
2235
2629
|
"span",
|
|
2236
2630
|
{
|
|
2237
2631
|
role: "button",
|
|
@@ -2255,19 +2649,19 @@ function DragTheWordsInner(props, ref) {
|
|
|
2255
2649
|
part
|
|
2256
2650
|
);
|
|
2257
2651
|
}) }),
|
|
2258
|
-
/* @__PURE__ */
|
|
2259
|
-
!hasZones ? /* @__PURE__ */
|
|
2260
|
-
|
|
2652
|
+
/* @__PURE__ */ jsx11("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
|
|
2653
|
+
!hasZones ? /* @__PURE__ */ jsx11("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
|
|
2654
|
+
submitted ? /* @__PURE__ */ jsx11("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
|
|
2261
2655
|
] });
|
|
2262
2656
|
}
|
|
2263
2657
|
var DragTheWordsInnerForwarded = forwardRef5(DragTheWordsInner);
|
|
2264
2658
|
var DragTheWords = forwardRef5(function DragTheWords2(props, ref) {
|
|
2265
|
-
return /* @__PURE__ */
|
|
2659
|
+
return /* @__PURE__ */ jsx11(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx11(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2266
2660
|
});
|
|
2267
2661
|
|
|
2268
2662
|
// src/blocks/DragAndDrop.tsx
|
|
2269
|
-
import { forwardRef as forwardRef6, useEffect as useEffect9, useMemo as useMemo11, useRef as
|
|
2270
|
-
import { jsx as
|
|
2663
|
+
import { forwardRef as forwardRef6, useEffect as useEffect9, useMemo as useMemo11, useRef as useRef10, useState as useState9 } from "react";
|
|
2664
|
+
import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2271
2665
|
var INTERACTION5 = "dragAndDrop";
|
|
2272
2666
|
function DragAndDropInner(props, ref) {
|
|
2273
2667
|
const checkId = useMemo11(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
@@ -2278,10 +2672,12 @@ function DragAndDropInner(props, ref) {
|
|
|
2278
2672
|
const [pool, setPool] = useState9(() => props.items.map((i) => i.id));
|
|
2279
2673
|
const [keyboardItem, setKeyboardItem] = useState9(null);
|
|
2280
2674
|
const [passed, setPassed] = useState9(false);
|
|
2281
|
-
const
|
|
2675
|
+
const [checked, setChecked] = useState9(false);
|
|
2676
|
+
const completedRef = useRef10(false);
|
|
2282
2677
|
const reset = () => {
|
|
2283
2678
|
completedRef.current = false;
|
|
2284
2679
|
setPassed(false);
|
|
2680
|
+
setChecked(false);
|
|
2285
2681
|
setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
|
|
2286
2682
|
setPool(props.items.map((i) => i.id));
|
|
2287
2683
|
setKeyboardItem(null);
|
|
@@ -2289,19 +2685,20 @@ function DragAndDropInner(props, ref) {
|
|
|
2289
2685
|
useEffect9(() => {
|
|
2290
2686
|
reset();
|
|
2291
2687
|
}, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
|
|
2292
|
-
const
|
|
2293
|
-
const
|
|
2688
|
+
const hasTargets = props.targets.length > 0;
|
|
2689
|
+
const allFilled = hasTargets && props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
|
|
2690
|
+
let score = 0;
|
|
2691
|
+
props.targets.forEach((t) => {
|
|
2692
|
+
if (assignments[t.id] === t.accepts) score += 1;
|
|
2693
|
+
});
|
|
2694
|
+
const maxScore = props.targets.length || 1;
|
|
2695
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
2294
2696
|
const handle = useMemo11(() => {
|
|
2295
|
-
const maxScore = props.targets.length || 1;
|
|
2296
|
-
let score = 0;
|
|
2297
|
-
props.targets.forEach((t) => {
|
|
2298
|
-
if (assignments[t.id] === t.accepts) score += 1;
|
|
2299
|
-
});
|
|
2300
2697
|
return buildAssessmentHandle({
|
|
2301
2698
|
checkId,
|
|
2302
2699
|
getScore: () => score,
|
|
2303
2700
|
getMaxScore: () => maxScore,
|
|
2304
|
-
getAnswerGiven: () => allFilled,
|
|
2701
|
+
getAnswerGiven: () => hasTargets && allFilled,
|
|
2305
2702
|
resetTask: reset,
|
|
2306
2703
|
showSolutions: () => {
|
|
2307
2704
|
},
|
|
@@ -2309,11 +2706,11 @@ function DragAndDropInner(props, ref) {
|
|
|
2309
2706
|
checkId,
|
|
2310
2707
|
interactionType: INTERACTION5,
|
|
2311
2708
|
response: assignments,
|
|
2312
|
-
correct:
|
|
2709
|
+
correct: passedThreshold,
|
|
2313
2710
|
score,
|
|
2314
2711
|
maxScore
|
|
2315
2712
|
}),
|
|
2316
|
-
getCurrentState: () => ({ assignments, pool, passed, keyboardItem }),
|
|
2713
|
+
getCurrentState: () => ({ assignments, pool, passed, checked, keyboardItem }),
|
|
2317
2714
|
resume: (state) => {
|
|
2318
2715
|
const rawAssignments = state.assignments;
|
|
2319
2716
|
if (rawAssignments && typeof rawAssignments === "object") {
|
|
@@ -2324,14 +2721,16 @@ function DragAndDropInner(props, ref) {
|
|
|
2324
2721
|
setPassed(value);
|
|
2325
2722
|
completedRef.current = value;
|
|
2326
2723
|
});
|
|
2724
|
+
readBooleanStateField(state, "checked", setChecked);
|
|
2327
2725
|
const item = state.keyboardItem;
|
|
2328
2726
|
if (item === null || typeof item === "string") setKeyboardItem(item ?? null);
|
|
2329
2727
|
}
|
|
2330
2728
|
});
|
|
2331
|
-
}, [
|
|
2729
|
+
}, [allFilled, assignments, checkId, checked, hasTargets, keyboardItem, maxScore, passed, passedThreshold, pool, props.targets, score]);
|
|
2332
2730
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2333
2731
|
const place = (targetId, itemId) => {
|
|
2334
2732
|
if (passed && !props.enableRetry) return;
|
|
2733
|
+
setChecked(false);
|
|
2335
2734
|
const prev = assignments[targetId];
|
|
2336
2735
|
setAssignments((a) => ({ ...a, [targetId]: itemId }));
|
|
2337
2736
|
setPool((p) => {
|
|
@@ -2343,29 +2742,31 @@ function DragAndDropInner(props, ref) {
|
|
|
2343
2742
|
};
|
|
2344
2743
|
const check = () => {
|
|
2345
2744
|
if (!allFilled) return;
|
|
2745
|
+
setChecked(true);
|
|
2346
2746
|
assessment.answer({
|
|
2347
2747
|
checkId,
|
|
2348
2748
|
interactionType: INTERACTION5,
|
|
2349
2749
|
response: assignments,
|
|
2350
|
-
correct:
|
|
2750
|
+
correct: passedThreshold
|
|
2351
2751
|
});
|
|
2352
|
-
if (
|
|
2752
|
+
if (passedThreshold && !completedRef.current) {
|
|
2353
2753
|
completedRef.current = true;
|
|
2354
2754
|
setPassed(true);
|
|
2355
2755
|
assessment.complete({
|
|
2356
2756
|
checkId,
|
|
2357
2757
|
interactionType: INTERACTION5,
|
|
2358
|
-
score
|
|
2359
|
-
maxScore
|
|
2360
|
-
passingScore: props.passingScore ??
|
|
2758
|
+
score,
|
|
2759
|
+
maxScore,
|
|
2760
|
+
passingScore: props.passingScore ?? maxScore
|
|
2361
2761
|
});
|
|
2362
2762
|
}
|
|
2363
2763
|
};
|
|
2364
2764
|
return /* @__PURE__ */ jsxs8("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
|
|
2365
|
-
/* @__PURE__ */
|
|
2366
|
-
/* @__PURE__ */
|
|
2765
|
+
/* @__PURE__ */ jsx12("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
|
|
2766
|
+
/* @__PURE__ */ jsx12("div", { role: "list", "aria-label": "Draggable items", children: pool.flatMap((id) => {
|
|
2367
2767
|
const item = props.items.find((i) => i.id === id);
|
|
2368
|
-
|
|
2768
|
+
if (!item) return [];
|
|
2769
|
+
return /* @__PURE__ */ jsx12(
|
|
2369
2770
|
"button",
|
|
2370
2771
|
{
|
|
2371
2772
|
type: "button",
|
|
@@ -2380,13 +2781,13 @@ function DragAndDropInner(props, ref) {
|
|
|
2380
2781
|
id
|
|
2381
2782
|
);
|
|
2382
2783
|
}) }),
|
|
2383
|
-
/* @__PURE__ */
|
|
2784
|
+
/* @__PURE__ */ jsx12("ul", { children: props.targets.map((target) => {
|
|
2384
2785
|
const assigned = assignments[target.id];
|
|
2385
2786
|
const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
|
|
2386
2787
|
return /* @__PURE__ */ jsxs8("li", { children: [
|
|
2387
|
-
/* @__PURE__ */
|
|
2788
|
+
/* @__PURE__ */ jsx12("strong", { children: target.label }),
|
|
2388
2789
|
" ",
|
|
2389
|
-
/* @__PURE__ */
|
|
2790
|
+
/* @__PURE__ */ jsx12(
|
|
2390
2791
|
"span",
|
|
2391
2792
|
{
|
|
2392
2793
|
role: "button",
|
|
@@ -2413,17 +2814,18 @@ function DragAndDropInner(props, ref) {
|
|
|
2413
2814
|
)
|
|
2414
2815
|
] }, target.id);
|
|
2415
2816
|
}) }),
|
|
2416
|
-
/* @__PURE__ */
|
|
2417
|
-
|
|
2817
|
+
/* @__PURE__ */ jsx12("button", { type: "button", "data-testid": "check-drag-drop", disabled: !hasTargets || !allFilled || passed, onClick: check, children: "Check" }),
|
|
2818
|
+
checked ? /* @__PURE__ */ jsx12("p", { role: "status", "aria-live": "polite", children: passedThreshold ? "Correct" : "Try again" }) : null
|
|
2418
2819
|
] });
|
|
2419
2820
|
}
|
|
2420
2821
|
var DragAndDropInnerForwarded = forwardRef6(DragAndDropInner);
|
|
2421
2822
|
var DragAndDrop = forwardRef6(function DragAndDrop2(props, ref) {
|
|
2422
|
-
return /* @__PURE__ */
|
|
2823
|
+
return /* @__PURE__ */ jsx12(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx12(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2423
2824
|
});
|
|
2424
2825
|
|
|
2425
2826
|
// src/blocks/AssessmentSequence.tsx
|
|
2426
|
-
import
|
|
2827
|
+
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";
|
|
2828
|
+
import { deriveId } from "@lessonkit/core";
|
|
2427
2829
|
|
|
2428
2830
|
// src/compound/useCompoundShell.ts
|
|
2429
2831
|
import { useMemo as useMemo12 } from "react";
|
|
@@ -2450,7 +2852,7 @@ function useCompoundNavigation(pageCount, index, setIndex) {
|
|
|
2450
2852
|
}
|
|
2451
2853
|
|
|
2452
2854
|
// src/compound/useCompoundPersistence.ts
|
|
2453
|
-
import { useCallback as useCallback6, useEffect as useEffect11, useRef as
|
|
2855
|
+
import { useCallback as useCallback6, useContext as useContext7, useEffect as useEffect11, useRef as useRef12 } from "react";
|
|
2454
2856
|
import {
|
|
2455
2857
|
clampCompoundPageIndex as clampCompoundPageIndex2,
|
|
2456
2858
|
createCompoundResumeState as createCompoundResumeState2,
|
|
@@ -2458,14 +2860,80 @@ import {
|
|
|
2458
2860
|
loadCompoundState as loadCompoundState2
|
|
2459
2861
|
} from "@lessonkit/core";
|
|
2460
2862
|
|
|
2863
|
+
// src/compound/resumeChildHandles.ts
|
|
2864
|
+
function filterRegisteredChildStates(handles, childStates) {
|
|
2865
|
+
const filtered = {};
|
|
2866
|
+
for (const [key, value] of Object.entries(childStates)) {
|
|
2867
|
+
if (handles.has(key)) {
|
|
2868
|
+
filtered[key] = value;
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
return filtered;
|
|
2872
|
+
}
|
|
2873
|
+
function resumeChildHandles(handles, childStates, opts) {
|
|
2874
|
+
const pendingKeys = Object.keys(childStates);
|
|
2875
|
+
const alreadyResumed = opts?.alreadyResumed;
|
|
2876
|
+
if (opts?.waitForHandles && pendingKeys.length > 0) {
|
|
2877
|
+
if (handles.size === 0) return false;
|
|
2878
|
+
const registeredPending = pendingKeys.filter((k) => handles.has(k));
|
|
2879
|
+
if (registeredPending.length === 0) {
|
|
2880
|
+
return false;
|
|
2881
|
+
}
|
|
2882
|
+
if (registeredPending.length < pendingKeys.length) {
|
|
2883
|
+
for (const key of registeredPending) {
|
|
2884
|
+
if (alreadyResumed?.has(key)) continue;
|
|
2885
|
+
const handle = handles.get(key);
|
|
2886
|
+
const child = childStates[key];
|
|
2887
|
+
if (handle?.resume && child) {
|
|
2888
|
+
handle.resume(child);
|
|
2889
|
+
alreadyResumed?.add(key);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
return false;
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
for (const [checkId, handle] of handles) {
|
|
2896
|
+
if (alreadyResumed?.has(checkId)) continue;
|
|
2897
|
+
const child = childStates[checkId];
|
|
2898
|
+
if (child && handle.resume) {
|
|
2899
|
+
handle.resume(child);
|
|
2900
|
+
alreadyResumed?.add(checkId);
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
return true;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2461
2906
|
// src/compound/useCompoundResume.ts
|
|
2462
|
-
import { useCallback as useCallback5, useEffect as useEffect10, useRef as
|
|
2907
|
+
import { useCallback as useCallback5, useContext as useContext6, useEffect as useEffect10, useRef as useRef11 } from "react";
|
|
2463
2908
|
import { loadCompoundState, saveCompoundState } from "@lessonkit/core";
|
|
2464
2909
|
import { createSessionStoragePort as createSessionStoragePort2 } from "@lessonkit/core";
|
|
2910
|
+
var warnedCompoundPersistFailure = false;
|
|
2911
|
+
function warnCompoundPersistFailure() {
|
|
2912
|
+
if (warnedCompoundPersistFailure || !isDevEnvironment4()) return;
|
|
2913
|
+
warnedCompoundPersistFailure = true;
|
|
2914
|
+
console.warn(
|
|
2915
|
+
"[lessonkit] compound resume state could not be saved to sessionStorage (quota or privacy mode); progress may be lost on reload."
|
|
2916
|
+
);
|
|
2917
|
+
}
|
|
2465
2918
|
function useCompoundResume(opts) {
|
|
2466
|
-
const
|
|
2467
|
-
const
|
|
2919
|
+
const lessonkitCtx = useContext6(LessonkitContext);
|
|
2920
|
+
const storageRef = useRef11(opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2());
|
|
2921
|
+
const resumedRef = useRef11(false);
|
|
2922
|
+
const resumeKeyRef = useRef11("");
|
|
2923
|
+
const prevEnabledRef = useRef11(opts.enabled);
|
|
2924
|
+
useEffect10(() => {
|
|
2925
|
+
storageRef.current = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2();
|
|
2926
|
+
}, [opts.storage, lessonkitCtx?.storage]);
|
|
2468
2927
|
useEffect10(() => {
|
|
2928
|
+
if (!prevEnabledRef.current && opts.enabled) {
|
|
2929
|
+
resumedRef.current = false;
|
|
2930
|
+
}
|
|
2931
|
+
prevEnabledRef.current = opts.enabled;
|
|
2932
|
+
const key = `${opts.courseId ?? ""}:${opts.compoundId}`;
|
|
2933
|
+
if (resumeKeyRef.current !== key) {
|
|
2934
|
+
resumeKeyRef.current = key;
|
|
2935
|
+
resumedRef.current = false;
|
|
2936
|
+
}
|
|
2469
2937
|
if (!opts.enabled || !opts.courseId || resumedRef.current) return;
|
|
2470
2938
|
const saved = loadCompoundState(storageRef.current, opts.courseId, opts.compoundId);
|
|
2471
2939
|
if (saved) {
|
|
@@ -2476,7 +2944,8 @@ function useCompoundResume(opts) {
|
|
|
2476
2944
|
return useCallback5(
|
|
2477
2945
|
(state) => {
|
|
2478
2946
|
if (!opts.enabled || !opts.courseId) return;
|
|
2479
|
-
saveCompoundState(storageRef.current, opts.courseId, opts.compoundId, state);
|
|
2947
|
+
const persisted = saveCompoundState(storageRef.current, opts.courseId, opts.compoundId, state);
|
|
2948
|
+
if (!persisted) warnCompoundPersistFailure();
|
|
2480
2949
|
},
|
|
2481
2950
|
[opts.enabled, opts.courseId, opts.compoundId]
|
|
2482
2951
|
);
|
|
@@ -2489,19 +2958,46 @@ function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, stor
|
|
|
2489
2958
|
if (!saved) return 0;
|
|
2490
2959
|
return clampCompoundPageIndex2(saved.activePageIndex, pageCount);
|
|
2491
2960
|
}
|
|
2961
|
+
function stripOrphanChildStates(handles, childStates) {
|
|
2962
|
+
return filterRegisteredChildStates(handles, childStates);
|
|
2963
|
+
}
|
|
2492
2964
|
function useCompoundPersistence(opts) {
|
|
2493
|
-
const
|
|
2965
|
+
const lessonkitCtx = useContext7(LessonkitContext);
|
|
2966
|
+
const storage = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort3();
|
|
2494
2967
|
const ctx = useCompoundRegistry();
|
|
2495
2968
|
const handlesVersion = useCompoundHandlesVersion();
|
|
2496
|
-
const
|
|
2497
|
-
const
|
|
2498
|
-
const
|
|
2969
|
+
const bridgeRef = useCompoundHydrationBridgeRef();
|
|
2970
|
+
const pendingChildResumeRef = useRef12(null);
|
|
2971
|
+
const resumedChildKeysRef = useRef12(/* @__PURE__ */ new Set());
|
|
2972
|
+
const loadedChildStatesRef = useRef12({});
|
|
2973
|
+
const skipSaveUntilHydratedRef = useRef12(false);
|
|
2974
|
+
const hydrationKeyRef = useRef12("");
|
|
2975
|
+
const hydrationInitRef = useRef12(false);
|
|
2976
|
+
const hydrationKey = `${opts.courseId ?? ""}:${opts.compoundId}`;
|
|
2977
|
+
if (hydrationKeyRef.current !== hydrationKey) {
|
|
2978
|
+
hydrationKeyRef.current = hydrationKey;
|
|
2979
|
+
hydrationInitRef.current = false;
|
|
2980
|
+
loadedChildStatesRef.current = {};
|
|
2981
|
+
skipSaveUntilHydratedRef.current = false;
|
|
2982
|
+
pendingChildResumeRef.current = null;
|
|
2983
|
+
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
2984
|
+
}
|
|
2985
|
+
if (!hydrationInitRef.current && opts.enabled && opts.courseId) {
|
|
2986
|
+
hydrationInitRef.current = true;
|
|
2987
|
+
const saved = loadCompoundState2(storage, opts.courseId, opts.compoundId);
|
|
2988
|
+
if (saved && Object.keys(saved.childStates).length > 0) {
|
|
2989
|
+
loadedChildStatesRef.current = { ...saved.childStates };
|
|
2990
|
+
skipSaveUntilHydratedRef.current = true;
|
|
2991
|
+
pendingChildResumeRef.current = saved;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2499
2994
|
const buildState = useCallback6(() => {
|
|
2500
2995
|
const childStates = {
|
|
2501
2996
|
...loadedChildStatesRef.current
|
|
2502
2997
|
};
|
|
2503
2998
|
if (ctx) {
|
|
2504
|
-
for (const [checkId,
|
|
2999
|
+
for (const [checkId, entry] of ctx.getRegisteredHandles()) {
|
|
3000
|
+
const handle = entry.handle;
|
|
2505
3001
|
if (handle.getCurrentState) {
|
|
2506
3002
|
childStates[checkId] = handle.getCurrentState();
|
|
2507
3003
|
delete loadedChildStatesRef.current[checkId];
|
|
@@ -2513,14 +3009,55 @@ function useCompoundPersistence(opts) {
|
|
|
2513
3009
|
childStates
|
|
2514
3010
|
});
|
|
2515
3011
|
}, [ctx, opts.index, opts.pageCount]);
|
|
3012
|
+
const buildStateRef = useRef12(buildState);
|
|
3013
|
+
buildStateRef.current = buildState;
|
|
3014
|
+
const persistNowRef = useRef12(() => {
|
|
3015
|
+
});
|
|
3016
|
+
const finalizeHydration = useCallback6(
|
|
3017
|
+
(childStates) => {
|
|
3018
|
+
loadedChildStatesRef.current = {
|
|
3019
|
+
...loadedChildStatesRef.current,
|
|
3020
|
+
...childStates
|
|
3021
|
+
};
|
|
3022
|
+
skipSaveUntilHydratedRef.current = false;
|
|
3023
|
+
pendingChildResumeRef.current = null;
|
|
3024
|
+
queueMicrotask(() => persistNowRef.current());
|
|
3025
|
+
},
|
|
3026
|
+
[]
|
|
3027
|
+
);
|
|
2516
3028
|
const applyPendingChildResume = useCallback6(() => {
|
|
2517
3029
|
const pending = pendingChildResumeRef.current;
|
|
2518
3030
|
if (!pending || !ctx) return;
|
|
2519
|
-
const
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
3031
|
+
const handles = ctx.getHandles();
|
|
3032
|
+
const applied = resumeChildHandles(handles, pending.childStates, {
|
|
3033
|
+
waitForHandles: true,
|
|
3034
|
+
alreadyResumed: resumedChildKeysRef.current
|
|
3035
|
+
});
|
|
3036
|
+
if (!applied) {
|
|
3037
|
+
if (handles.size === 0) {
|
|
3038
|
+
const registeredOnly2 = stripOrphanChildStates(handles, pending.childStates);
|
|
3039
|
+
resumeChildHandles(handles, registeredOnly2, {
|
|
3040
|
+
alreadyResumed: resumedChildKeysRef.current
|
|
3041
|
+
});
|
|
3042
|
+
finalizeHydration(registeredOnly2);
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
const handlesAtWait = handles.size;
|
|
3046
|
+
queueMicrotask(() => {
|
|
3047
|
+
if (pendingChildResumeRef.current !== pending) return;
|
|
3048
|
+
const handlesNow = ctx.getHandles();
|
|
3049
|
+
if (handlesNow.size !== handlesAtWait) return;
|
|
3050
|
+
const registeredOnly2 = stripOrphanChildStates(handlesNow, pending.childStates);
|
|
3051
|
+
resumeChildHandles(handlesNow, registeredOnly2, {
|
|
3052
|
+
alreadyResumed: resumedChildKeysRef.current
|
|
3053
|
+
});
|
|
3054
|
+
finalizeHydration(registeredOnly2);
|
|
3055
|
+
});
|
|
3056
|
+
return;
|
|
3057
|
+
}
|
|
3058
|
+
const registeredOnly = stripOrphanChildStates(handles, pending.childStates);
|
|
3059
|
+
finalizeHydration(registeredOnly);
|
|
3060
|
+
}, [ctx, finalizeHydration]);
|
|
2524
3061
|
const saveResume = useCompoundResume({
|
|
2525
3062
|
courseId: opts.courseId,
|
|
2526
3063
|
compoundId: opts.compoundId,
|
|
@@ -2531,26 +3068,58 @@ function useCompoundPersistence(opts) {
|
|
|
2531
3068
|
loadedChildStatesRef.current = { ...state.childStates };
|
|
2532
3069
|
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
2533
3070
|
opts.setIndex(clamped);
|
|
2534
|
-
|
|
3071
|
+
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
3072
|
+
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
2535
3073
|
queueMicrotask(() => applyPendingChildResume());
|
|
2536
3074
|
}
|
|
2537
3075
|
});
|
|
2538
|
-
|
|
3076
|
+
const persistNow = useCallback6(() => {
|
|
2539
3077
|
if (!opts.enabled || !opts.courseId) return;
|
|
2540
3078
|
if (skipSaveUntilHydratedRef.current) return;
|
|
2541
|
-
saveResume(
|
|
2542
|
-
}, [
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
3079
|
+
saveResume(buildStateRef.current());
|
|
3080
|
+
}, [opts.enabled, opts.courseId, saveResume]);
|
|
3081
|
+
useEffect11(() => {
|
|
3082
|
+
persistNowRef.current = persistNow;
|
|
3083
|
+
}, [persistNow]);
|
|
3084
|
+
const notifyImperativeResume = useCallback6(
|
|
3085
|
+
(state) => {
|
|
3086
|
+
const clamped = clampCompoundPageIndex2(state.activePageIndex, opts.pageCount);
|
|
3087
|
+
loadedChildStatesRef.current = { ...state.childStates };
|
|
3088
|
+
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
3089
|
+
opts.setIndex(clamped);
|
|
3090
|
+
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
3091
|
+
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
3092
|
+
queueMicrotask(() => applyPendingChildResume());
|
|
3093
|
+
},
|
|
3094
|
+
[opts.pageCount, opts.setIndex, applyPendingChildResume]
|
|
3095
|
+
);
|
|
3096
|
+
useEffect11(() => {
|
|
3097
|
+
if (!bridgeRef) return;
|
|
3098
|
+
bridgeRef.current = { notifyImperativeResume };
|
|
3099
|
+
return () => {
|
|
3100
|
+
if (bridgeRef.current?.notifyImperativeResume === notifyImperativeResume) {
|
|
3101
|
+
bridgeRef.current = null;
|
|
3102
|
+
}
|
|
3103
|
+
};
|
|
3104
|
+
}, [bridgeRef, notifyImperativeResume]);
|
|
2551
3105
|
useEffect11(() => {
|
|
2552
3106
|
applyPendingChildResume();
|
|
2553
3107
|
}, [opts.index, handlesVersion, applyPendingChildResume]);
|
|
3108
|
+
useEffect11(() => {
|
|
3109
|
+
persistNow();
|
|
3110
|
+
}, [persistNow, opts.index, opts.pageCount, handlesVersion]);
|
|
3111
|
+
useEffect11(() => {
|
|
3112
|
+
if (!opts.enabled || !opts.courseId || typeof document === "undefined") return;
|
|
3113
|
+
const flushOnExit = () => {
|
|
3114
|
+
if (document.visibilityState === "hidden") persistNow();
|
|
3115
|
+
};
|
|
3116
|
+
document.addEventListener("visibilitychange", flushOnExit);
|
|
3117
|
+
window.addEventListener("pagehide", flushOnExit);
|
|
3118
|
+
return () => {
|
|
3119
|
+
document.removeEventListener("visibilitychange", flushOnExit);
|
|
3120
|
+
window.removeEventListener("pagehide", flushOnExit);
|
|
3121
|
+
};
|
|
3122
|
+
}, [opts.enabled, opts.courseId, persistNow]);
|
|
2554
3123
|
}
|
|
2555
3124
|
|
|
2556
3125
|
// src/compound/useCompoundShell.ts
|
|
@@ -2571,6 +3140,7 @@ function useCompoundShell(opts) {
|
|
|
2571
3140
|
activePageIndex: visibleIndex,
|
|
2572
3141
|
setActivePageIndex: opts.setIndex,
|
|
2573
3142
|
getHandles: () => ctx?.getHandles() ?? /* @__PURE__ */ new Map(),
|
|
3143
|
+
getRegisteredHandles: () => ctx?.getRegisteredHandles() ?? /* @__PURE__ */ new Map(),
|
|
2574
3144
|
pageCount: opts.pageCount,
|
|
2575
3145
|
enableSolutionsButton: opts.enableSolutionsButton
|
|
2576
3146
|
});
|
|
@@ -2590,7 +3160,7 @@ function useCompoundInitialIndex(opts) {
|
|
|
2590
3160
|
}
|
|
2591
3161
|
|
|
2592
3162
|
// src/compound/validateChildren.ts
|
|
2593
|
-
import
|
|
3163
|
+
import React15 from "react";
|
|
2594
3164
|
import {
|
|
2595
3165
|
ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
2596
3166
|
COMPOUND_MAX_NESTING_DEPTH,
|
|
@@ -2619,6 +3189,8 @@ var warnedPairs = /* @__PURE__ */ new Set();
|
|
|
2619
3189
|
var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
2620
3190
|
"Page",
|
|
2621
3191
|
"InteractiveBook",
|
|
3192
|
+
"Slide",
|
|
3193
|
+
"SlideDeck",
|
|
2622
3194
|
"AssessmentSequence"
|
|
2623
3195
|
]);
|
|
2624
3196
|
function warnOrThrow(msg, strict) {
|
|
@@ -2629,8 +3201,8 @@ function warnOrThrow(msg, strict) {
|
|
|
2629
3201
|
}
|
|
2630
3202
|
}
|
|
2631
3203
|
function validateNode(parent, node, depth, strict) {
|
|
2632
|
-
|
|
2633
|
-
if (!
|
|
3204
|
+
React15.Children.forEach(node, (child) => {
|
|
3205
|
+
if (!React15.isValidElement(child)) return;
|
|
2634
3206
|
const blockType = getLessonkitBlockType(child.type);
|
|
2635
3207
|
if (!blockType) {
|
|
2636
3208
|
if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
@@ -2670,8 +3242,8 @@ function validateNode(parent, node, depth, strict) {
|
|
|
2670
3242
|
});
|
|
2671
3243
|
}
|
|
2672
3244
|
function validateSubtreeForForbidden(node, forbidden, strict) {
|
|
2673
|
-
|
|
2674
|
-
if (!
|
|
3245
|
+
React15.Children.forEach(node, (child) => {
|
|
3246
|
+
if (!React15.isValidElement(child)) return;
|
|
2675
3247
|
const blockType = getLessonkitBlockType(child.type);
|
|
2676
3248
|
if (blockType && forbidden.includes(blockType)) {
|
|
2677
3249
|
warnOrThrow(`[lessonkit] Block "${blockType}" must not nest inside Accordion`, strict);
|
|
@@ -2711,7 +3283,7 @@ function warnSharedCompoundStorageKey(opts) {
|
|
|
2711
3283
|
}
|
|
2712
3284
|
|
|
2713
3285
|
// src/blocks/AssessmentSequence.tsx
|
|
2714
|
-
import { jsx as
|
|
3286
|
+
import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2715
3287
|
var AssessmentSequenceInner = forwardRef7(
|
|
2716
3288
|
function AssessmentSequenceInner2(props, ref) {
|
|
2717
3289
|
const { compoundId, childArray, index, setIndex, persistEnabled } = props;
|
|
@@ -2729,7 +3301,7 @@ var AssessmentSequenceInner = forwardRef7(
|
|
|
2729
3301
|
});
|
|
2730
3302
|
validateCompoundChildren("AssessmentSequence", props.children);
|
|
2731
3303
|
if (!sequential) {
|
|
2732
|
-
return /* @__PURE__ */
|
|
3304
|
+
return /* @__PURE__ */ jsx13("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
|
|
2733
3305
|
}
|
|
2734
3306
|
return /* @__PURE__ */ jsxs9("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
|
|
2735
3307
|
/* @__PURE__ */ jsxs9("p", { children: [
|
|
@@ -2738,9 +3310,9 @@ var AssessmentSequenceInner = forwardRef7(
|
|
|
2738
3310
|
" of ",
|
|
2739
3311
|
progress.total
|
|
2740
3312
|
] }),
|
|
2741
|
-
/* @__PURE__ */
|
|
3313
|
+
/* @__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)) }),
|
|
2742
3314
|
/* @__PURE__ */ jsxs9("nav", { "aria-label": "Sequence navigation", children: [
|
|
2743
|
-
/* @__PURE__ */
|
|
3315
|
+
/* @__PURE__ */ jsx13(
|
|
2744
3316
|
"button",
|
|
2745
3317
|
{
|
|
2746
3318
|
type: "button",
|
|
@@ -2750,7 +3322,7 @@ var AssessmentSequenceInner = forwardRef7(
|
|
|
2750
3322
|
children: "Previous"
|
|
2751
3323
|
}
|
|
2752
3324
|
),
|
|
2753
|
-
/* @__PURE__ */
|
|
3325
|
+
/* @__PURE__ */ jsx13(
|
|
2754
3326
|
"button",
|
|
2755
3327
|
{
|
|
2756
3328
|
type: "button",
|
|
@@ -2766,14 +3338,19 @@ var AssessmentSequenceInner = forwardRef7(
|
|
|
2766
3338
|
);
|
|
2767
3339
|
var AssessmentSequence = forwardRef7(
|
|
2768
3340
|
function AssessmentSequence2(props, ref) {
|
|
3341
|
+
const reactInstanceId = useId3();
|
|
3342
|
+
const autoCompoundIdRef = useRef13(null);
|
|
3343
|
+
if (!props.blockId && !autoCompoundIdRef.current) {
|
|
3344
|
+
autoCompoundIdRef.current = deriveId(`assessment-sequence-${reactInstanceId}`);
|
|
3345
|
+
}
|
|
2769
3346
|
const compoundId = useMemo13(
|
|
2770
|
-
() => props.blockId ? normalizeComponentId(props.blockId, "blockId") : DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
|
|
3347
|
+
() => props.blockId ? normalizeComponentId(props.blockId, "blockId") : autoCompoundIdRef.current ?? DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
|
|
2771
3348
|
[props.blockId]
|
|
2772
3349
|
);
|
|
2773
|
-
const childArray =
|
|
2774
|
-
|
|
3350
|
+
const childArray = React16.Children.toArray(props.children).filter(
|
|
3351
|
+
React16.isValidElement
|
|
2775
3352
|
);
|
|
2776
|
-
const { config } = useLessonkit();
|
|
3353
|
+
const { config, storage } = useLessonkit();
|
|
2777
3354
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
2778
3355
|
useEffect12(() => {
|
|
2779
3356
|
warnSharedCompoundStorageKey({
|
|
@@ -2786,11 +3363,15 @@ var AssessmentSequence = forwardRef7(
|
|
|
2786
3363
|
courseId: config.courseId,
|
|
2787
3364
|
compoundId,
|
|
2788
3365
|
pageCount: childArray.length,
|
|
2789
|
-
persistEnabled
|
|
3366
|
+
persistEnabled,
|
|
3367
|
+
storage
|
|
2790
3368
|
});
|
|
2791
3369
|
const [index, setIndex] = useState10(initialIndex);
|
|
2792
3370
|
const setIndexStable = useCallback7((i) => setIndex(i), []);
|
|
2793
|
-
|
|
3371
|
+
useEffect12(() => {
|
|
3372
|
+
setIndex(initialIndex);
|
|
3373
|
+
}, [config.courseId, compoundId, initialIndex]);
|
|
3374
|
+
return /* @__PURE__ */ jsx13(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx13(
|
|
2794
3375
|
AssessmentSequenceInner,
|
|
2795
3376
|
{
|
|
2796
3377
|
...props,
|
|
@@ -2808,24 +3389,24 @@ setLessonkitBlockType(AssessmentSequence, "AssessmentSequence");
|
|
|
2808
3389
|
|
|
2809
3390
|
// src/blocks/Text.tsx
|
|
2810
3391
|
import "react";
|
|
2811
|
-
import { jsx as
|
|
3392
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
2812
3393
|
function Text(props) {
|
|
2813
|
-
return /* @__PURE__ */
|
|
3394
|
+
return /* @__PURE__ */ jsx14("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
|
|
2814
3395
|
}
|
|
2815
3396
|
setLessonkitBlockType(Text, "Text");
|
|
2816
3397
|
|
|
2817
3398
|
// src/blocks/Heading.tsx
|
|
2818
|
-
import { jsx as
|
|
3399
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
2819
3400
|
function Heading(props) {
|
|
2820
3401
|
const Tag = `h${props.level}`;
|
|
2821
|
-
return /* @__PURE__ */
|
|
3402
|
+
return /* @__PURE__ */ jsx15(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
|
|
2822
3403
|
}
|
|
2823
3404
|
setLessonkitBlockType(Heading, "Heading");
|
|
2824
3405
|
|
|
2825
3406
|
// src/blocks/Image.tsx
|
|
2826
|
-
import { jsx as
|
|
3407
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
2827
3408
|
function Image(props) {
|
|
2828
|
-
return /* @__PURE__ */
|
|
3409
|
+
return /* @__PURE__ */ jsx16(
|
|
2829
3410
|
"img",
|
|
2830
3411
|
{
|
|
2831
3412
|
src: props.src,
|
|
@@ -2840,13 +3421,13 @@ setLessonkitBlockType(Image, "Image");
|
|
|
2840
3421
|
|
|
2841
3422
|
// src/blocks/Page.tsx
|
|
2842
3423
|
import { useEffect as useEffect13 } from "react";
|
|
2843
|
-
import { jsx as
|
|
3424
|
+
import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2844
3425
|
function Page(props) {
|
|
2845
3426
|
validateCompoundChildren("Page", props.children);
|
|
2846
3427
|
const { track } = useLessonkit();
|
|
2847
3428
|
const lessonId = useEnclosingLessonId();
|
|
2848
3429
|
useEffect13(() => {
|
|
2849
|
-
if (props.hidden || !lessonId) return;
|
|
3430
|
+
if (props.hidden || !lessonId || props.parentType) return;
|
|
2850
3431
|
track(
|
|
2851
3432
|
"compound_page_viewed",
|
|
2852
3433
|
{
|
|
@@ -2865,8 +3446,8 @@ function Page(props) {
|
|
|
2865
3446
|
"data-testid": `page-${props.blockId}`,
|
|
2866
3447
|
hidden: props.hidden ? true : void 0,
|
|
2867
3448
|
children: [
|
|
2868
|
-
props.title ? /* @__PURE__ */
|
|
2869
|
-
/* @__PURE__ */
|
|
3449
|
+
props.title ? /* @__PURE__ */ jsx17("h3", { children: props.title }) : null,
|
|
3450
|
+
/* @__PURE__ */ jsx17(CompoundPageIndexProvider, { pageIndex: props.pageIndex ?? 0, children: /* @__PURE__ */ jsx17("div", { children: props.children }) })
|
|
2870
3451
|
]
|
|
2871
3452
|
}
|
|
2872
3453
|
);
|
|
@@ -2874,8 +3455,8 @@ function Page(props) {
|
|
|
2874
3455
|
setLessonkitBlockType(Page, "Page");
|
|
2875
3456
|
|
|
2876
3457
|
// src/blocks/InteractiveBook.tsx
|
|
2877
|
-
import
|
|
2878
|
-
import { jsx as
|
|
3458
|
+
import React19, { forwardRef as forwardRef8, useCallback as useCallback8, useEffect as useEffect14, useMemo as useMemo14, useState as useState11 } from "react";
|
|
3459
|
+
import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2879
3460
|
var InteractiveBookInner = forwardRef8(
|
|
2880
3461
|
function InteractiveBookInner2(props, ref) {
|
|
2881
3462
|
const { blockId, pages, index, setIndex, persistEnabled } = props;
|
|
@@ -2908,7 +3489,7 @@ var InteractiveBookInner = forwardRef8(
|
|
|
2908
3489
|
);
|
|
2909
3490
|
}, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
|
|
2910
3491
|
return /* @__PURE__ */ jsxs11("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
|
|
2911
|
-
/* @__PURE__ */
|
|
3492
|
+
/* @__PURE__ */ jsx18("h3", { children: props.title }),
|
|
2912
3493
|
/* @__PURE__ */ jsxs11("p", { children: [
|
|
2913
3494
|
"Page ",
|
|
2914
3495
|
progress.current,
|
|
@@ -2922,8 +3503,8 @@ var InteractiveBookInner = forwardRef8(
|
|
|
2922
3503
|
" ",
|
|
2923
3504
|
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
2924
3505
|
] }) : null,
|
|
2925
|
-
/* @__PURE__ */
|
|
2926
|
-
(page, i) =>
|
|
3506
|
+
/* @__PURE__ */ jsx18("div", { "data-testid": "interactive-book-page", children: pages.map(
|
|
3507
|
+
(page, i) => React19.cloneElement(page, {
|
|
2927
3508
|
key: page.key ?? page.props.blockId,
|
|
2928
3509
|
hidden: i !== visibleIndex,
|
|
2929
3510
|
pageIndex: i,
|
|
@@ -2931,7 +3512,7 @@ var InteractiveBookInner = forwardRef8(
|
|
|
2931
3512
|
})
|
|
2932
3513
|
) }),
|
|
2933
3514
|
/* @__PURE__ */ jsxs11("nav", { "aria-label": "Book navigation", children: [
|
|
2934
|
-
/* @__PURE__ */
|
|
3515
|
+
/* @__PURE__ */ jsx18(
|
|
2935
3516
|
"button",
|
|
2936
3517
|
{
|
|
2937
3518
|
type: "button",
|
|
@@ -2941,7 +3522,7 @@ var InteractiveBookInner = forwardRef8(
|
|
|
2941
3522
|
children: "Previous"
|
|
2942
3523
|
}
|
|
2943
3524
|
),
|
|
2944
|
-
/* @__PURE__ */
|
|
3525
|
+
/* @__PURE__ */ jsx18(
|
|
2945
3526
|
"button",
|
|
2946
3527
|
{
|
|
2947
3528
|
type: "button",
|
|
@@ -2960,20 +3541,24 @@ var InteractiveBook = forwardRef8(function InteractiveBook2(props, ref) {
|
|
|
2960
3541
|
() => normalizeComponentId(props.blockId, "blockId"),
|
|
2961
3542
|
[props.blockId]
|
|
2962
3543
|
);
|
|
2963
|
-
const pages =
|
|
2964
|
-
|
|
3544
|
+
const pages = React19.Children.toArray(props.children).filter(
|
|
3545
|
+
React19.isValidElement
|
|
2965
3546
|
);
|
|
2966
|
-
const { config } = useLessonkit();
|
|
3547
|
+
const { config, storage } = useLessonkit();
|
|
2967
3548
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
2968
3549
|
const initialIndex = useCompoundInitialIndex({
|
|
2969
3550
|
courseId: config.courseId,
|
|
2970
3551
|
compoundId: blockId,
|
|
2971
3552
|
pageCount: pages.length,
|
|
2972
|
-
persistEnabled
|
|
3553
|
+
persistEnabled,
|
|
3554
|
+
storage
|
|
2973
3555
|
});
|
|
2974
3556
|
const [index, setIndex] = useState11(initialIndex);
|
|
2975
3557
|
const setIndexStable = useCallback8((i) => setIndex(i), []);
|
|
2976
|
-
|
|
3558
|
+
useEffect14(() => {
|
|
3559
|
+
setIndex(initialIndex);
|
|
3560
|
+
}, [config.courseId, blockId, initialIndex]);
|
|
3561
|
+
return /* @__PURE__ */ jsx18(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx18(
|
|
2977
3562
|
InteractiveBookInner,
|
|
2978
3563
|
{
|
|
2979
3564
|
...props,
|
|
@@ -2988,17 +3573,249 @@ var InteractiveBook = forwardRef8(function InteractiveBook2(props, ref) {
|
|
|
2988
3573
|
});
|
|
2989
3574
|
setLessonkitBlockType(InteractiveBook, "InteractiveBook");
|
|
2990
3575
|
|
|
3576
|
+
// src/blocks/Slide.tsx
|
|
3577
|
+
import { useEffect as useEffect15 } from "react";
|
|
3578
|
+
import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3579
|
+
function Slide(props) {
|
|
3580
|
+
validateCompoundChildren("Slide", props.children);
|
|
3581
|
+
const { track } = useLessonkit();
|
|
3582
|
+
const lessonId = useEnclosingLessonId();
|
|
3583
|
+
useEffect15(() => {
|
|
3584
|
+
if (props.hidden || !lessonId || props.parentType) return;
|
|
3585
|
+
track(
|
|
3586
|
+
"compound_page_viewed",
|
|
3587
|
+
{
|
|
3588
|
+
blockId: props.blockId,
|
|
3589
|
+
pageIndex: props.slideIndex ?? 0,
|
|
3590
|
+
parentType: props.parentType
|
|
3591
|
+
},
|
|
3592
|
+
{ lessonId }
|
|
3593
|
+
);
|
|
3594
|
+
}, [props.hidden, props.slideIndex, props.parentType, props.blockId, lessonId, track]);
|
|
3595
|
+
return /* @__PURE__ */ jsxs12(
|
|
3596
|
+
"section",
|
|
3597
|
+
{
|
|
3598
|
+
"aria-label": props.title ?? "Slide",
|
|
3599
|
+
"data-lk-block-id": props.blockId,
|
|
3600
|
+
"data-testid": `slide-${props.blockId}`,
|
|
3601
|
+
hidden: props.hidden ? true : void 0,
|
|
3602
|
+
children: [
|
|
3603
|
+
props.title ? /* @__PURE__ */ jsx19("h3", { children: props.title }) : null,
|
|
3604
|
+
/* @__PURE__ */ jsx19(CompoundPageIndexProvider, { pageIndex: props.slideIndex ?? 0, children: /* @__PURE__ */ jsx19("div", { children: props.children }) })
|
|
3605
|
+
]
|
|
3606
|
+
}
|
|
3607
|
+
);
|
|
3608
|
+
}
|
|
3609
|
+
setLessonkitBlockType(Slide, "Slide");
|
|
3610
|
+
|
|
3611
|
+
// src/blocks/SlideDeck.tsx
|
|
3612
|
+
import React21, { forwardRef as forwardRef9, useCallback as useCallback9, useEffect as useEffect17, useMemo as useMemo15, useRef as useRef14, useState as useState12 } from "react";
|
|
3613
|
+
|
|
3614
|
+
// src/compound/useCompoundKeyboardNav.ts
|
|
3615
|
+
import { useEffect as useEffect16 } from "react";
|
|
3616
|
+
var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT", "BUTTON"]);
|
|
3617
|
+
function isEditableTarget(target) {
|
|
3618
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
3619
|
+
if (INTERACTIVE_TAGS.has(target.tagName)) return true;
|
|
3620
|
+
if (target.isContentEditable) return true;
|
|
3621
|
+
if (target.closest("[role='slider'], [role='listbox'], [data-lk-assessment-interactive]")) {
|
|
3622
|
+
return true;
|
|
3623
|
+
}
|
|
3624
|
+
return false;
|
|
3625
|
+
}
|
|
3626
|
+
function useCompoundKeyboardNav(opts) {
|
|
3627
|
+
const { containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex } = opts;
|
|
3628
|
+
useEffect16(() => {
|
|
3629
|
+
const el = containerRef.current;
|
|
3630
|
+
if (!el || pageCount === 0) return;
|
|
3631
|
+
const onKeyDown = (event) => {
|
|
3632
|
+
if (!el.contains(document.activeElement) && document.activeElement !== document.body) {
|
|
3633
|
+
return;
|
|
3634
|
+
}
|
|
3635
|
+
if (isEditableTarget(event.target)) return;
|
|
3636
|
+
switch (event.key) {
|
|
3637
|
+
case "ArrowRight":
|
|
3638
|
+
case "ArrowDown":
|
|
3639
|
+
if (visibleIndex < pageCount - 1) {
|
|
3640
|
+
event.preventDefault();
|
|
3641
|
+
goNext();
|
|
3642
|
+
}
|
|
3643
|
+
break;
|
|
3644
|
+
case "ArrowLeft":
|
|
3645
|
+
case "ArrowUp":
|
|
3646
|
+
if (visibleIndex > 0) {
|
|
3647
|
+
event.preventDefault();
|
|
3648
|
+
goPrev();
|
|
3649
|
+
}
|
|
3650
|
+
break;
|
|
3651
|
+
case "Home":
|
|
3652
|
+
if (visibleIndex !== 0) {
|
|
3653
|
+
event.preventDefault();
|
|
3654
|
+
setIndex(0);
|
|
3655
|
+
}
|
|
3656
|
+
break;
|
|
3657
|
+
case "End":
|
|
3658
|
+
if (visibleIndex !== pageCount - 1) {
|
|
3659
|
+
event.preventDefault();
|
|
3660
|
+
setIndex(pageCount - 1);
|
|
3661
|
+
}
|
|
3662
|
+
break;
|
|
3663
|
+
default:
|
|
3664
|
+
break;
|
|
3665
|
+
}
|
|
3666
|
+
};
|
|
3667
|
+
el.addEventListener("keydown", onKeyDown);
|
|
3668
|
+
return () => el.removeEventListener("keydown", onKeyDown);
|
|
3669
|
+
}, [containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex]);
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
// src/blocks/SlideDeck.tsx
|
|
3673
|
+
import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3674
|
+
var SlideDeckInner = forwardRef9(function SlideDeckInner2(props, ref) {
|
|
3675
|
+
const { blockId, slides, index, setIndex, persistEnabled } = props;
|
|
3676
|
+
validateCompoundChildren("SlideDeck", slides);
|
|
3677
|
+
const { config, track } = useLessonkit();
|
|
3678
|
+
const lessonId = useEnclosingLessonId();
|
|
3679
|
+
const containerRef = useRef14(null);
|
|
3680
|
+
const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
|
|
3681
|
+
courseId: config.courseId,
|
|
3682
|
+
compoundId: blockId,
|
|
3683
|
+
pageCount: slides.length,
|
|
3684
|
+
index,
|
|
3685
|
+
setIndex,
|
|
3686
|
+
persistEnabled,
|
|
3687
|
+
ref
|
|
3688
|
+
});
|
|
3689
|
+
const setIndexStable = useCallback9((i) => setIndex(i), [setIndex]);
|
|
3690
|
+
useCompoundKeyboardNav({
|
|
3691
|
+
containerRef,
|
|
3692
|
+
visibleIndex,
|
|
3693
|
+
pageCount: slides.length,
|
|
3694
|
+
goNext,
|
|
3695
|
+
goPrev,
|
|
3696
|
+
setIndex: setIndexStable
|
|
3697
|
+
});
|
|
3698
|
+
const slideTitles = useMemo15(
|
|
3699
|
+
() => slides.map((slide) => slide.props.title),
|
|
3700
|
+
[slides]
|
|
3701
|
+
);
|
|
3702
|
+
useEffect17(() => {
|
|
3703
|
+
if (!lessonId || slides.length === 0) return;
|
|
3704
|
+
track(
|
|
3705
|
+
"slide_viewed",
|
|
3706
|
+
{
|
|
3707
|
+
blockId,
|
|
3708
|
+
slideIndex: visibleIndex,
|
|
3709
|
+
slideTitle: slideTitles[visibleIndex]
|
|
3710
|
+
},
|
|
3711
|
+
{ lessonId }
|
|
3712
|
+
);
|
|
3713
|
+
}, [visibleIndex, blockId, lessonId, slides.length, slideTitles, track]);
|
|
3714
|
+
return /* @__PURE__ */ jsxs13(
|
|
3715
|
+
"section",
|
|
3716
|
+
{
|
|
3717
|
+
ref: containerRef,
|
|
3718
|
+
tabIndex: -1,
|
|
3719
|
+
"aria-label": props.title,
|
|
3720
|
+
"data-testid": "slide-deck",
|
|
3721
|
+
"data-lk-block-id": blockId,
|
|
3722
|
+
children: [
|
|
3723
|
+
/* @__PURE__ */ jsx20("h3", { children: props.title }),
|
|
3724
|
+
/* @__PURE__ */ jsxs13("p", { children: [
|
|
3725
|
+
"Slide ",
|
|
3726
|
+
progress.current,
|
|
3727
|
+
" of ",
|
|
3728
|
+
progress.total
|
|
3729
|
+
] }),
|
|
3730
|
+
props.showDeckScore && ctx ? /* @__PURE__ */ jsxs13("p", { "data-testid": "deck-score", children: [
|
|
3731
|
+
"Score: ",
|
|
3732
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
3733
|
+
" /",
|
|
3734
|
+
" ",
|
|
3735
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
3736
|
+
] }) : null,
|
|
3737
|
+
/* @__PURE__ */ jsx20("div", { "data-testid": "slide-deck-slide", children: slides.map(
|
|
3738
|
+
(slide, i) => React21.cloneElement(slide, {
|
|
3739
|
+
key: slide.key ?? slide.props.blockId,
|
|
3740
|
+
hidden: i !== visibleIndex,
|
|
3741
|
+
slideIndex: i,
|
|
3742
|
+
parentType: "SlideDeck"
|
|
3743
|
+
})
|
|
3744
|
+
) }),
|
|
3745
|
+
/* @__PURE__ */ jsxs13("nav", { "aria-label": "Slide navigation", children: [
|
|
3746
|
+
/* @__PURE__ */ jsx20(
|
|
3747
|
+
"button",
|
|
3748
|
+
{
|
|
3749
|
+
type: "button",
|
|
3750
|
+
"data-testid": "slide-prev",
|
|
3751
|
+
disabled: visibleIndex === 0 || slides.length === 0,
|
|
3752
|
+
onClick: goPrev,
|
|
3753
|
+
children: "Previous slide"
|
|
3754
|
+
}
|
|
3755
|
+
),
|
|
3756
|
+
/* @__PURE__ */ jsx20(
|
|
3757
|
+
"button",
|
|
3758
|
+
{
|
|
3759
|
+
type: "button",
|
|
3760
|
+
"data-testid": "slide-next",
|
|
3761
|
+
disabled: visibleIndex >= slides.length - 1 || slides.length === 0,
|
|
3762
|
+
onClick: goNext,
|
|
3763
|
+
children: "Next slide"
|
|
3764
|
+
}
|
|
3765
|
+
)
|
|
3766
|
+
] })
|
|
3767
|
+
]
|
|
3768
|
+
}
|
|
3769
|
+
);
|
|
3770
|
+
});
|
|
3771
|
+
var SlideDeck = forwardRef9(function SlideDeck2(props, ref) {
|
|
3772
|
+
const blockId = useMemo15(
|
|
3773
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3774
|
+
[props.blockId]
|
|
3775
|
+
);
|
|
3776
|
+
const slides = React21.Children.toArray(props.children).filter(
|
|
3777
|
+
React21.isValidElement
|
|
3778
|
+
);
|
|
3779
|
+
const { config, storage } = useLessonkit();
|
|
3780
|
+
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
3781
|
+
const initialIndex = useCompoundInitialIndex({
|
|
3782
|
+
courseId: config.courseId,
|
|
3783
|
+
compoundId: blockId,
|
|
3784
|
+
pageCount: slides.length,
|
|
3785
|
+
persistEnabled,
|
|
3786
|
+
storage
|
|
3787
|
+
});
|
|
3788
|
+
const [index, setIndex] = useState12(initialIndex);
|
|
3789
|
+
const setIndexStable = useCallback9((i) => setIndex(i), []);
|
|
3790
|
+
useEffect17(() => {
|
|
3791
|
+
setIndex(initialIndex);
|
|
3792
|
+
}, [config.courseId, blockId, initialIndex]);
|
|
3793
|
+
return /* @__PURE__ */ jsx20(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx20(
|
|
3794
|
+
SlideDeckInner,
|
|
3795
|
+
{
|
|
3796
|
+
...props,
|
|
3797
|
+
ref,
|
|
3798
|
+
blockId,
|
|
3799
|
+
slides,
|
|
3800
|
+
index,
|
|
3801
|
+
setIndex,
|
|
3802
|
+
persistEnabled
|
|
3803
|
+
}
|
|
3804
|
+
) });
|
|
3805
|
+
});
|
|
3806
|
+
setLessonkitBlockType(SlideDeck, "SlideDeck");
|
|
3807
|
+
|
|
2991
3808
|
// src/blocks/Accordion.tsx
|
|
2992
|
-
import { useId as
|
|
2993
|
-
import { jsx as
|
|
3809
|
+
import { useId as useId4, useState as useState13 } from "react";
|
|
3810
|
+
import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2994
3811
|
function Accordion(props) {
|
|
2995
3812
|
if (isDevEnvironment4()) {
|
|
2996
3813
|
validateAccordionSections(props.sections);
|
|
2997
3814
|
}
|
|
2998
|
-
const [open, setOpen] =
|
|
3815
|
+
const [open, setOpen] = useState13(/* @__PURE__ */ new Set());
|
|
2999
3816
|
const { track } = useLessonkit();
|
|
3000
3817
|
const lessonId = useEnclosingLessonId();
|
|
3001
|
-
const baseId =
|
|
3818
|
+
const baseId = useId4();
|
|
3002
3819
|
const toggle = (sectionId) => {
|
|
3003
3820
|
setOpen((prev) => {
|
|
3004
3821
|
const next = new Set(prev);
|
|
@@ -3013,12 +3830,12 @@ function Accordion(props) {
|
|
|
3013
3830
|
return next;
|
|
3014
3831
|
});
|
|
3015
3832
|
};
|
|
3016
|
-
return /* @__PURE__ */
|
|
3833
|
+
return /* @__PURE__ */ jsx21("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
|
|
3017
3834
|
const expanded = open.has(section.id);
|
|
3018
3835
|
const panelId = `${baseId}-${section.id}`;
|
|
3019
3836
|
const triggerId = `${baseId}-trigger-${section.id}`;
|
|
3020
|
-
return /* @__PURE__ */
|
|
3021
|
-
/* @__PURE__ */
|
|
3837
|
+
return /* @__PURE__ */ jsxs14("div", { "data-testid": `accordion-section-${section.id}`, children: [
|
|
3838
|
+
/* @__PURE__ */ jsx21("h4", { children: /* @__PURE__ */ jsx21(
|
|
3022
3839
|
"button",
|
|
3023
3840
|
{
|
|
3024
3841
|
id: triggerId,
|
|
@@ -3030,28 +3847,28 @@ function Accordion(props) {
|
|
|
3030
3847
|
children: section.title
|
|
3031
3848
|
}
|
|
3032
3849
|
) }),
|
|
3033
|
-
expanded ? /* @__PURE__ */
|
|
3850
|
+
expanded ? /* @__PURE__ */ jsx21("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
|
|
3034
3851
|
] }, section.id);
|
|
3035
3852
|
}) });
|
|
3036
3853
|
}
|
|
3037
3854
|
setLessonkitBlockType(Accordion, "Accordion");
|
|
3038
3855
|
|
|
3039
3856
|
// src/blocks/DialogCards.tsx
|
|
3040
|
-
import { useState as
|
|
3041
|
-
import { jsx as
|
|
3857
|
+
import { useState as useState14 } from "react";
|
|
3858
|
+
import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
3042
3859
|
function DialogCards(props) {
|
|
3043
|
-
const [index, setIndex] =
|
|
3044
|
-
const [flipped, setFlipped] =
|
|
3860
|
+
const [index, setIndex] = useState14(0);
|
|
3861
|
+
const [flipped, setFlipped] = useState14(false);
|
|
3045
3862
|
const card = props.cards[index];
|
|
3046
3863
|
if (!card) return null;
|
|
3047
|
-
return /* @__PURE__ */
|
|
3048
|
-
/* @__PURE__ */
|
|
3864
|
+
return /* @__PURE__ */ jsxs15("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
|
|
3865
|
+
/* @__PURE__ */ jsxs15("p", { children: [
|
|
3049
3866
|
"Card ",
|
|
3050
3867
|
index + 1,
|
|
3051
3868
|
" of ",
|
|
3052
3869
|
props.cards.length
|
|
3053
3870
|
] }),
|
|
3054
|
-
/* @__PURE__ */
|
|
3871
|
+
/* @__PURE__ */ jsx22(
|
|
3055
3872
|
"button",
|
|
3056
3873
|
{
|
|
3057
3874
|
type: "button",
|
|
@@ -3062,8 +3879,8 @@ function DialogCards(props) {
|
|
|
3062
3879
|
children: flipped ? card.back : card.front
|
|
3063
3880
|
}
|
|
3064
3881
|
),
|
|
3065
|
-
/* @__PURE__ */
|
|
3066
|
-
/* @__PURE__ */
|
|
3882
|
+
/* @__PURE__ */ jsxs15("nav", { "aria-label": "Card navigation", children: [
|
|
3883
|
+
/* @__PURE__ */ jsx22(
|
|
3067
3884
|
"button",
|
|
3068
3885
|
{
|
|
3069
3886
|
type: "button",
|
|
@@ -3076,7 +3893,7 @@ function DialogCards(props) {
|
|
|
3076
3893
|
children: "Previous"
|
|
3077
3894
|
}
|
|
3078
3895
|
),
|
|
3079
|
-
/* @__PURE__ */
|
|
3896
|
+
/* @__PURE__ */ jsx22(
|
|
3080
3897
|
"button",
|
|
3081
3898
|
{
|
|
3082
3899
|
type: "button",
|
|
@@ -3095,11 +3912,11 @@ function DialogCards(props) {
|
|
|
3095
3912
|
setLessonkitBlockType(DialogCards, "DialogCards");
|
|
3096
3913
|
|
|
3097
3914
|
// src/blocks/Flashcards.tsx
|
|
3098
|
-
import { useState as
|
|
3099
|
-
import { jsx as
|
|
3915
|
+
import { useState as useState15 } from "react";
|
|
3916
|
+
import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
3100
3917
|
function Flashcards(props) {
|
|
3101
|
-
const [index, setIndex] =
|
|
3102
|
-
const [face, setFace] =
|
|
3918
|
+
const [index, setIndex] = useState15(0);
|
|
3919
|
+
const [face, setFace] = useState15("front");
|
|
3103
3920
|
const { track } = useLessonkit();
|
|
3104
3921
|
const lessonId = useEnclosingLessonId();
|
|
3105
3922
|
const card = props.cards[index];
|
|
@@ -3113,10 +3930,10 @@ function Flashcards(props) {
|
|
|
3113
3930
|
lessonId ? { lessonId } : void 0
|
|
3114
3931
|
);
|
|
3115
3932
|
};
|
|
3116
|
-
return /* @__PURE__ */
|
|
3117
|
-
/* @__PURE__ */
|
|
3118
|
-
props.selfScore ? /* @__PURE__ */
|
|
3119
|
-
/* @__PURE__ */
|
|
3933
|
+
return /* @__PURE__ */ jsxs16("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
|
|
3934
|
+
/* @__PURE__ */ jsx23("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
|
|
3935
|
+
props.selfScore ? /* @__PURE__ */ jsx23("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
|
|
3936
|
+
/* @__PURE__ */ jsx23(
|
|
3120
3937
|
"button",
|
|
3121
3938
|
{
|
|
3122
3939
|
type: "button",
|
|
@@ -3134,10 +3951,10 @@ function Flashcards(props) {
|
|
|
3134
3951
|
setLessonkitBlockType(Flashcards, "Flashcards");
|
|
3135
3952
|
|
|
3136
3953
|
// src/blocks/ImageHotspots.tsx
|
|
3137
|
-
import { useState as
|
|
3138
|
-
import { jsx as
|
|
3954
|
+
import { useState as useState16 } from "react";
|
|
3955
|
+
import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
3139
3956
|
function ImageHotspots(props) {
|
|
3140
|
-
const [active, setActive] =
|
|
3957
|
+
const [active, setActive] = useState16(null);
|
|
3141
3958
|
const { track } = useLessonkit();
|
|
3142
3959
|
const lessonId = useEnclosingLessonId();
|
|
3143
3960
|
const open = (hotspotId) => {
|
|
@@ -3148,10 +3965,10 @@ function ImageHotspots(props) {
|
|
|
3148
3965
|
lessonId ? { lessonId } : void 0
|
|
3149
3966
|
);
|
|
3150
3967
|
};
|
|
3151
|
-
return /* @__PURE__ */
|
|
3152
|
-
/* @__PURE__ */
|
|
3153
|
-
/* @__PURE__ */
|
|
3154
|
-
props.hotspots.map((h) => /* @__PURE__ */
|
|
3968
|
+
return /* @__PURE__ */ jsxs17("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
|
|
3969
|
+
/* @__PURE__ */ jsxs17("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
3970
|
+
/* @__PURE__ */ jsx24("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
3971
|
+
props.hotspots.map((h) => /* @__PURE__ */ jsx24(
|
|
3155
3972
|
"button",
|
|
3156
3973
|
{
|
|
3157
3974
|
type: "button",
|
|
@@ -3170,19 +3987,19 @@ function ImageHotspots(props) {
|
|
|
3170
3987
|
h.id
|
|
3171
3988
|
))
|
|
3172
3989
|
] }),
|
|
3173
|
-
active ? /* @__PURE__ */
|
|
3990
|
+
active ? /* @__PURE__ */ jsxs17("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
|
|
3174
3991
|
props.hotspots.find((h) => h.id === active)?.content,
|
|
3175
|
-
/* @__PURE__ */
|
|
3992
|
+
/* @__PURE__ */ jsx24("button", { type: "button", onClick: () => setActive(null), children: "Close" })
|
|
3176
3993
|
] }) : null
|
|
3177
3994
|
] });
|
|
3178
3995
|
}
|
|
3179
3996
|
setLessonkitBlockType(ImageHotspots, "ImageHotspots");
|
|
3180
3997
|
|
|
3181
3998
|
// src/blocks/ImageSlider.tsx
|
|
3182
|
-
import { useState as
|
|
3183
|
-
import { jsx as
|
|
3999
|
+
import { useState as useState17 } from "react";
|
|
4000
|
+
import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
3184
4001
|
function ImageSlider(props) {
|
|
3185
|
-
const [index, setIndex] =
|
|
4002
|
+
const [index, setIndex] = useState17(0);
|
|
3186
4003
|
const { track } = useLessonkit();
|
|
3187
4004
|
const lessonId = useEnclosingLessonId();
|
|
3188
4005
|
const slide = props.slides[index];
|
|
@@ -3195,11 +4012,11 @@ function ImageSlider(props) {
|
|
|
3195
4012
|
lessonId ? { lessonId } : void 0
|
|
3196
4013
|
);
|
|
3197
4014
|
};
|
|
3198
|
-
return /* @__PURE__ */
|
|
3199
|
-
/* @__PURE__ */
|
|
3200
|
-
slide.caption ? /* @__PURE__ */
|
|
3201
|
-
/* @__PURE__ */
|
|
3202
|
-
/* @__PURE__ */
|
|
4015
|
+
return /* @__PURE__ */ jsxs18("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
|
|
4016
|
+
/* @__PURE__ */ jsx25("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
|
|
4017
|
+
slide.caption ? /* @__PURE__ */ jsx25("p", { children: slide.caption }) : null,
|
|
4018
|
+
/* @__PURE__ */ jsxs18("nav", { "aria-label": "Slide navigation", children: [
|
|
4019
|
+
/* @__PURE__ */ jsx25(
|
|
3203
4020
|
"button",
|
|
3204
4021
|
{
|
|
3205
4022
|
type: "button",
|
|
@@ -3209,12 +4026,12 @@ function ImageSlider(props) {
|
|
|
3209
4026
|
children: "Previous"
|
|
3210
4027
|
}
|
|
3211
4028
|
),
|
|
3212
|
-
/* @__PURE__ */
|
|
4029
|
+
/* @__PURE__ */ jsxs18("span", { children: [
|
|
3213
4030
|
index + 1,
|
|
3214
4031
|
" / ",
|
|
3215
4032
|
props.slides.length
|
|
3216
4033
|
] }),
|
|
3217
|
-
/* @__PURE__ */
|
|
4034
|
+
/* @__PURE__ */ jsx25(
|
|
3218
4035
|
"button",
|
|
3219
4036
|
{
|
|
3220
4037
|
type: "button",
|
|
@@ -3230,16 +4047,42 @@ function ImageSlider(props) {
|
|
|
3230
4047
|
setLessonkitBlockType(ImageSlider, "ImageSlider");
|
|
3231
4048
|
|
|
3232
4049
|
// src/blocks/FindHotspot.tsx
|
|
3233
|
-
import { forwardRef as
|
|
3234
|
-
import { jsx as
|
|
4050
|
+
import { forwardRef as forwardRef10, useEffect as useEffect18, useMemo as useMemo16, useRef as useRef15, useState as useState18 } from "react";
|
|
4051
|
+
import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
3235
4052
|
var INTERACTION6 = "findHotspot";
|
|
3236
4053
|
function FindHotspotInner(props, ref) {
|
|
3237
|
-
const checkId =
|
|
3238
|
-
const [selected, setSelected] =
|
|
3239
|
-
const [checked, setChecked] =
|
|
4054
|
+
const checkId = useMemo16(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4055
|
+
const [selected, setSelected] = useState18(null);
|
|
4056
|
+
const [checked, setChecked] = useState18(false);
|
|
4057
|
+
const telemetryReplayedRef = useRef15(false);
|
|
3240
4058
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4059
|
+
const targetIdsKey = props.targets.map((t) => t.id).join("\0");
|
|
4060
|
+
useEffect18(() => {
|
|
4061
|
+
setSelected(null);
|
|
4062
|
+
setChecked(false);
|
|
4063
|
+
telemetryReplayedRef.current = false;
|
|
4064
|
+
}, [checkId, props.correctTargetId, targetIdsKey]);
|
|
3241
4065
|
const correct = selected === props.correctTargetId;
|
|
3242
|
-
const
|
|
4066
|
+
const replayTelemetry = (nextSelected, nextChecked, nextCorrect) => {
|
|
4067
|
+
if (telemetryReplayedRef.current || !nextChecked || nextSelected === null) return;
|
|
4068
|
+
telemetryReplayedRef.current = true;
|
|
4069
|
+
assessment.answer({
|
|
4070
|
+
checkId,
|
|
4071
|
+
interactionType: INTERACTION6,
|
|
4072
|
+
response: nextSelected,
|
|
4073
|
+
correct: nextCorrect
|
|
4074
|
+
});
|
|
4075
|
+
if (nextCorrect) {
|
|
4076
|
+
assessment.complete({
|
|
4077
|
+
checkId,
|
|
4078
|
+
interactionType: INTERACTION6,
|
|
4079
|
+
score: 1,
|
|
4080
|
+
maxScore: 1,
|
|
4081
|
+
passingScore: props.passingScore ?? 1
|
|
4082
|
+
});
|
|
4083
|
+
}
|
|
4084
|
+
};
|
|
4085
|
+
const handle = useMemo16(
|
|
3243
4086
|
() => buildAssessmentHandle({
|
|
3244
4087
|
checkId,
|
|
3245
4088
|
getScore: () => checked && correct ? 1 : 0,
|
|
@@ -3248,6 +4091,7 @@ function FindHotspotInner(props, ref) {
|
|
|
3248
4091
|
resetTask: () => {
|
|
3249
4092
|
setSelected(null);
|
|
3250
4093
|
setChecked(false);
|
|
4094
|
+
telemetryReplayedRef.current = false;
|
|
3251
4095
|
},
|
|
3252
4096
|
showSolutions: () => setSelected(props.correctTargetId),
|
|
3253
4097
|
getXAPIData: () => ({
|
|
@@ -3260,16 +4104,31 @@ function FindHotspotInner(props, ref) {
|
|
|
3260
4104
|
}),
|
|
3261
4105
|
getCurrentState: () => ({ selected, checked }),
|
|
3262
4106
|
resume: (state) => {
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
4107
|
+
let nextSelected = selected;
|
|
4108
|
+
const rawSelected = readStringField(state, "selected");
|
|
4109
|
+
if (typeof rawSelected === "string" || rawSelected === null) {
|
|
4110
|
+
const valid = rawSelected === null || props.targets.some((t) => t.id === rawSelected);
|
|
4111
|
+
nextSelected = valid ? rawSelected : null;
|
|
4112
|
+
setSelected(nextSelected);
|
|
4113
|
+
}
|
|
4114
|
+
let nextChecked = checked;
|
|
4115
|
+
readBooleanStateField(state, "checked", (value) => {
|
|
4116
|
+
nextChecked = value;
|
|
4117
|
+
setChecked(value);
|
|
4118
|
+
});
|
|
4119
|
+
const nextCorrect = nextSelected === props.correctTargetId;
|
|
4120
|
+
replayTelemetry(nextSelected, nextChecked, nextCorrect);
|
|
3266
4121
|
}
|
|
3267
4122
|
}),
|
|
3268
|
-
[
|
|
4123
|
+
[assessment, checkId, checked, correct, props.correctTargetId, props.passingScore, props.targets, selected]
|
|
3269
4124
|
);
|
|
3270
4125
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
4126
|
+
const selectTarget = (id) => {
|
|
4127
|
+
setSelected(id);
|
|
4128
|
+
setChecked(false);
|
|
4129
|
+
};
|
|
3271
4130
|
const submit = () => {
|
|
3272
|
-
if (!selected) return;
|
|
4131
|
+
if (!selected || checked) return;
|
|
3273
4132
|
setChecked(true);
|
|
3274
4133
|
assessment.answer({
|
|
3275
4134
|
checkId,
|
|
@@ -3283,14 +4142,14 @@ function FindHotspotInner(props, ref) {
|
|
|
3283
4142
|
interactionType: INTERACTION6,
|
|
3284
4143
|
score: 1,
|
|
3285
4144
|
maxScore: 1,
|
|
3286
|
-
passingScore: props.passingScore
|
|
4145
|
+
passingScore: props.passingScore ?? 1
|
|
3287
4146
|
});
|
|
3288
4147
|
}
|
|
3289
4148
|
};
|
|
3290
|
-
return /* @__PURE__ */
|
|
3291
|
-
/* @__PURE__ */
|
|
3292
|
-
/* @__PURE__ */
|
|
3293
|
-
props.targets.map((t) => /* @__PURE__ */
|
|
4149
|
+
return /* @__PURE__ */ jsxs19("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
|
|
4150
|
+
/* @__PURE__ */ jsxs19("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
4151
|
+
/* @__PURE__ */ jsx26("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
4152
|
+
props.targets.map((t) => /* @__PURE__ */ jsx26(
|
|
3294
4153
|
"button",
|
|
3295
4154
|
{
|
|
3296
4155
|
type: "button",
|
|
@@ -3303,30 +4162,30 @@ function FindHotspotInner(props, ref) {
|
|
|
3303
4162
|
top: `${t.y}%`,
|
|
3304
4163
|
transform: "translate(-50%, -50%)"
|
|
3305
4164
|
},
|
|
3306
|
-
onClick: () =>
|
|
4165
|
+
onClick: () => selectTarget(t.id),
|
|
3307
4166
|
children: t.label
|
|
3308
4167
|
},
|
|
3309
4168
|
t.id
|
|
3310
4169
|
))
|
|
3311
4170
|
] }),
|
|
3312
|
-
/* @__PURE__ */
|
|
3313
|
-
checked ? /* @__PURE__ */
|
|
4171
|
+
/* @__PURE__ */ jsx26("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
|
|
4172
|
+
checked ? /* @__PURE__ */ jsx26("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
3314
4173
|
] });
|
|
3315
4174
|
}
|
|
3316
|
-
var FindHotspotInnerForwarded =
|
|
3317
|
-
var FindHotspot =
|
|
3318
|
-
return /* @__PURE__ */
|
|
4175
|
+
var FindHotspotInnerForwarded = forwardRef10(FindHotspotInner);
|
|
4176
|
+
var FindHotspot = forwardRef10(function FindHotspot2(props, ref) {
|
|
4177
|
+
return /* @__PURE__ */ jsx26(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx26(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
3319
4178
|
});
|
|
3320
4179
|
setLessonkitBlockType(FindHotspot, "FindHotspot");
|
|
3321
4180
|
|
|
3322
4181
|
// src/blocks/FindMultipleHotspots.tsx
|
|
3323
|
-
import { forwardRef as
|
|
3324
|
-
import { jsx as
|
|
4182
|
+
import { forwardRef as forwardRef11, useMemo as useMemo17, useState as useState19 } from "react";
|
|
4183
|
+
import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
3325
4184
|
var INTERACTION7 = "findMultipleHotspots";
|
|
3326
4185
|
function FindMultipleHotspotsInner(props, ref) {
|
|
3327
|
-
const checkId =
|
|
3328
|
-
const [selected, setSelected] =
|
|
3329
|
-
const [checked, setChecked] =
|
|
4186
|
+
const checkId = useMemo17(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4187
|
+
const [selected, setSelected] = useState19(/* @__PURE__ */ new Set());
|
|
4188
|
+
const [checked, setChecked] = useState19(false);
|
|
3330
4189
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
3331
4190
|
const toggle = (id) => {
|
|
3332
4191
|
setSelected((prev) => {
|
|
@@ -3335,9 +4194,10 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
3335
4194
|
else next.add(id);
|
|
3336
4195
|
return next;
|
|
3337
4196
|
});
|
|
4197
|
+
setChecked(false);
|
|
3338
4198
|
};
|
|
3339
4199
|
const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
|
|
3340
|
-
const handle =
|
|
4200
|
+
const handle = useMemo17(
|
|
3341
4201
|
() => buildAssessmentHandle({
|
|
3342
4202
|
checkId,
|
|
3343
4203
|
getScore: () => checked && correct ? 1 : 0,
|
|
@@ -3367,7 +4227,7 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
3367
4227
|
);
|
|
3368
4228
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3369
4229
|
const submit = () => {
|
|
3370
|
-
if (selected.size === 0) return;
|
|
4230
|
+
if (selected.size === 0 || checked) return;
|
|
3371
4231
|
setChecked(true);
|
|
3372
4232
|
assessment.answer({
|
|
3373
4233
|
checkId,
|
|
@@ -3381,14 +4241,14 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
3381
4241
|
interactionType: INTERACTION7,
|
|
3382
4242
|
score: 1,
|
|
3383
4243
|
maxScore: 1,
|
|
3384
|
-
passingScore: props.passingScore
|
|
4244
|
+
passingScore: props.passingScore ?? 1
|
|
3385
4245
|
});
|
|
3386
4246
|
}
|
|
3387
4247
|
};
|
|
3388
|
-
return /* @__PURE__ */
|
|
3389
|
-
/* @__PURE__ */
|
|
3390
|
-
/* @__PURE__ */
|
|
3391
|
-
props.targets.map((t) => /* @__PURE__ */
|
|
4248
|
+
return /* @__PURE__ */ jsxs20("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
|
|
4249
|
+
/* @__PURE__ */ jsxs20("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
4250
|
+
/* @__PURE__ */ jsx27("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
4251
|
+
props.targets.map((t) => /* @__PURE__ */ jsx27(
|
|
3392
4252
|
"button",
|
|
3393
4253
|
{
|
|
3394
4254
|
type: "button",
|
|
@@ -3407,14 +4267,14 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
3407
4267
|
t.id
|
|
3408
4268
|
))
|
|
3409
4269
|
] }),
|
|
3410
|
-
/* @__PURE__ */
|
|
3411
|
-
checked ? /* @__PURE__ */
|
|
4270
|
+
/* @__PURE__ */ jsx27("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
|
|
4271
|
+
checked ? /* @__PURE__ */ jsx27("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
3412
4272
|
] });
|
|
3413
4273
|
}
|
|
3414
|
-
var FindMultipleHotspotsInnerForwarded =
|
|
3415
|
-
var FindMultipleHotspots =
|
|
4274
|
+
var FindMultipleHotspotsInnerForwarded = forwardRef11(FindMultipleHotspotsInner);
|
|
4275
|
+
var FindMultipleHotspots = forwardRef11(
|
|
3416
4276
|
function FindMultipleHotspots2(props, ref) {
|
|
3417
|
-
return /* @__PURE__ */
|
|
4277
|
+
return /* @__PURE__ */ jsx27(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx27(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
3418
4278
|
}
|
|
3419
4279
|
);
|
|
3420
4280
|
setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
|
|
@@ -3431,14 +4291,14 @@ import {
|
|
|
3431
4291
|
} from "@lessonkit/core";
|
|
3432
4292
|
|
|
3433
4293
|
// src/theme/ThemeProvider.tsx
|
|
3434
|
-
import
|
|
3435
|
-
createContext as
|
|
3436
|
-
useCallback as
|
|
3437
|
-
useContext as
|
|
4294
|
+
import React29, {
|
|
4295
|
+
createContext as createContext6,
|
|
4296
|
+
useCallback as useCallback10,
|
|
4297
|
+
useContext as useContext8,
|
|
3438
4298
|
useLayoutEffect as useLayoutEffect2,
|
|
3439
|
-
useMemo as
|
|
3440
|
-
useRef as
|
|
3441
|
-
useState as
|
|
4299
|
+
useMemo as useMemo18,
|
|
4300
|
+
useRef as useRef16,
|
|
4301
|
+
useState as useState20
|
|
3442
4302
|
} from "react";
|
|
3443
4303
|
import {
|
|
3444
4304
|
brandThemeOverrides,
|
|
@@ -3465,11 +4325,11 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
3465
4325
|
}
|
|
3466
4326
|
|
|
3467
4327
|
// src/theme/ThemeProvider.tsx
|
|
3468
|
-
import { jsx as
|
|
3469
|
-
var ThemeContext =
|
|
4328
|
+
import { jsx as jsx28 } from "react/jsx-runtime";
|
|
4329
|
+
var ThemeContext = createContext6(null);
|
|
3470
4330
|
var useIsoLayoutEffect2 = (
|
|
3471
4331
|
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
3472
|
-
typeof window !== "undefined" ? useLayoutEffect2 :
|
|
4332
|
+
typeof window !== "undefined" ? useLayoutEffect2 : React29.useEffect
|
|
3473
4333
|
);
|
|
3474
4334
|
function getSystemMode() {
|
|
3475
4335
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
@@ -3488,7 +4348,7 @@ function ThemeProvider(props) {
|
|
|
3488
4348
|
const preset = props.preset ?? "default";
|
|
3489
4349
|
const mode = props.mode ?? "light";
|
|
3490
4350
|
const targetKind = props.target ?? "document";
|
|
3491
|
-
const [resolvedMode, setResolvedMode] =
|
|
4351
|
+
const [resolvedMode, setResolvedMode] = useState20(
|
|
3492
4352
|
() => mode === "system" ? getSystemMode() : mode
|
|
3493
4353
|
);
|
|
3494
4354
|
useIsoLayoutEffect2(() => {
|
|
@@ -3504,20 +4364,20 @@ function ThemeProvider(props) {
|
|
|
3504
4364
|
return () => mq.removeEventListener("change", onChange);
|
|
3505
4365
|
}, [mode]);
|
|
3506
4366
|
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
3507
|
-
const effectiveTheme =
|
|
4367
|
+
const effectiveTheme = useMemo18(() => {
|
|
3508
4368
|
const modeBase = resolveModeBase(mode, dataTheme);
|
|
3509
4369
|
const base = preset === "default" ? modeBase : preset === "brand" ? mergeThemes(modeBase, brandThemeOverrides) : mergeThemes(modeBase, getPresetTheme(preset));
|
|
3510
4370
|
return mergeThemes(base, props.theme ?? {});
|
|
3511
4371
|
}, [preset, mode, dataTheme, props.theme]);
|
|
3512
|
-
const hostRef =
|
|
3513
|
-
const appliedKeysRef =
|
|
4372
|
+
const hostRef = useRef16(null);
|
|
4373
|
+
const appliedKeysRef = useRef16(/* @__PURE__ */ new Set());
|
|
3514
4374
|
useIsoLayoutEffect2(() => {
|
|
3515
4375
|
if (targetKind === "document" && typeof document !== "undefined") {
|
|
3516
4376
|
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
3517
4377
|
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
3518
4378
|
}
|
|
3519
4379
|
}, [targetKind, dataTheme]);
|
|
3520
|
-
const inject =
|
|
4380
|
+
const inject = useCallback10(() => {
|
|
3521
4381
|
const vars = themeToCssVariables(effectiveTheme);
|
|
3522
4382
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
3523
4383
|
if (!el) return;
|
|
@@ -3534,7 +4394,7 @@ function ThemeProvider(props) {
|
|
|
3534
4394
|
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
3535
4395
|
};
|
|
3536
4396
|
}, [inject, targetKind]);
|
|
3537
|
-
const value =
|
|
4397
|
+
const value = useMemo18(
|
|
3538
4398
|
() => ({
|
|
3539
4399
|
theme: effectiveTheme,
|
|
3540
4400
|
preset,
|
|
@@ -3544,12 +4404,12 @@ function ThemeProvider(props) {
|
|
|
3544
4404
|
[effectiveTheme, preset, mode, dataTheme]
|
|
3545
4405
|
);
|
|
3546
4406
|
if (targetKind === "document") {
|
|
3547
|
-
return /* @__PURE__ */
|
|
4407
|
+
return /* @__PURE__ */ jsx28(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx28("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
|
|
3548
4408
|
}
|
|
3549
|
-
return /* @__PURE__ */
|
|
4409
|
+
return /* @__PURE__ */ jsx28(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx28("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
3550
4410
|
}
|
|
3551
4411
|
function useTheme() {
|
|
3552
|
-
const ctx =
|
|
4412
|
+
const ctx = useContext8(ThemeContext);
|
|
3553
4413
|
if (!ctx) {
|
|
3554
4414
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
3555
4415
|
}
|
|
@@ -3561,9 +4421,18 @@ import {
|
|
|
3561
4421
|
ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
3562
4422
|
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
3563
4423
|
PAGE_ALLOWED_CHILD_TYPES,
|
|
4424
|
+
SLIDE_ALLOWED_CHILD_TYPES,
|
|
4425
|
+
SLIDE_DECK_ALLOWED_CHILD_TYPES,
|
|
3564
4426
|
COMPOUND_MAX_NESTING_DEPTH as COMPOUND_MAX_NESTING_DEPTH2
|
|
3565
4427
|
} from "@lessonkit/core";
|
|
3566
|
-
var COMPOUND_PARENTS = [
|
|
4428
|
+
var COMPOUND_PARENTS = [
|
|
4429
|
+
"Lesson",
|
|
4430
|
+
"Page",
|
|
4431
|
+
"InteractiveBook",
|
|
4432
|
+
"Slide",
|
|
4433
|
+
"SlideDeck",
|
|
4434
|
+
"AssessmentSequence"
|
|
4435
|
+
];
|
|
3567
4436
|
function extendParents(entry) {
|
|
3568
4437
|
if (!entry.parentConstraints?.length) return entry;
|
|
3569
4438
|
const merged = /* @__PURE__ */ new Set([...entry.parentConstraints, ...COMPOUND_PARENTS]);
|
|
@@ -3667,6 +4536,53 @@ var v3CompoundAndContentEntries = [
|
|
|
3667
4536
|
theming: { surface: "global-inherit", stylingNotes: "Book chrome." },
|
|
3668
4537
|
telemetry: { emits: ["book_page_viewed"], requiresActiveLesson: true }
|
|
3669
4538
|
},
|
|
4539
|
+
{
|
|
4540
|
+
type: "Slide",
|
|
4541
|
+
category: "container",
|
|
4542
|
+
compoundContract: true,
|
|
4543
|
+
h5pMachineName: "H5P.CoursePresentation",
|
|
4544
|
+
h5pAlias: "Course Presentation slide",
|
|
4545
|
+
description: "Single slide row in a SlideDeck. Planned allowlist expansion: Video, Summary.",
|
|
4546
|
+
allowedChildTypes: [...SLIDE_ALLOWED_CHILD_TYPES],
|
|
4547
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.Slide,
|
|
4548
|
+
props: [
|
|
4549
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4550
|
+
{ name: "title", type: "string", required: false, description: "Slide title." },
|
|
4551
|
+
{ name: "children", type: "ReactNode", required: true, description: "Slide content." }
|
|
4552
|
+
],
|
|
4553
|
+
requiredIds: [],
|
|
4554
|
+
optionalIds: ["blockId"],
|
|
4555
|
+
parentConstraints: ["SlideDeck"],
|
|
4556
|
+
a11y: { element: "section", ariaLabel: "Slide", keyboard: "N/A", notes: "H5P Course Presentation slide row." },
|
|
4557
|
+
theming: { surface: "global-inherit", stylingNotes: "Container." },
|
|
4558
|
+
telemetry: { emits: ["compound_page_viewed"], requiresActiveLesson: true }
|
|
4559
|
+
},
|
|
4560
|
+
{
|
|
4561
|
+
type: "SlideDeck",
|
|
4562
|
+
category: "container",
|
|
4563
|
+
compoundContract: true,
|
|
4564
|
+
h5pMachineName: "H5P.CoursePresentation",
|
|
4565
|
+
h5pAlias: "Course Presentation",
|
|
4566
|
+
description: "Multi-slide presentation with keyboard navigation.",
|
|
4567
|
+
allowedChildTypes: [...SLIDE_DECK_ALLOWED_CHILD_TYPES],
|
|
4568
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.SlideDeck,
|
|
4569
|
+
props: [
|
|
4570
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4571
|
+
{ name: "title", type: "string", required: true, description: "Deck title." },
|
|
4572
|
+
{ name: "showDeckScore", type: "boolean", required: false, description: "Show aggregate score." },
|
|
4573
|
+
{ name: "children", type: "Slide[]", required: true, description: "Slides." }
|
|
4574
|
+
],
|
|
4575
|
+
requiredIds: ["blockId"],
|
|
4576
|
+
parentConstraints: ["Lesson"],
|
|
4577
|
+
a11y: {
|
|
4578
|
+
element: "section",
|
|
4579
|
+
ariaLabel: "Slide deck",
|
|
4580
|
+
keyboard: "Arrow keys, Home, End, Previous/Next slide buttons.",
|
|
4581
|
+
notes: "H5P Course Presentation equivalent."
|
|
4582
|
+
},
|
|
4583
|
+
theming: { surface: "global-inherit", stylingNotes: "Deck chrome." },
|
|
4584
|
+
telemetry: { emits: ["slide_viewed"], requiresActiveLesson: true }
|
|
4585
|
+
},
|
|
3670
4586
|
{
|
|
3671
4587
|
type: "Accordion",
|
|
3672
4588
|
category: "content",
|
|
@@ -4212,6 +5128,8 @@ export {
|
|
|
4212
5128
|
Quiz,
|
|
4213
5129
|
Reflection,
|
|
4214
5130
|
Scenario,
|
|
5131
|
+
Slide,
|
|
5132
|
+
SlideDeck,
|
|
4215
5133
|
Text,
|
|
4216
5134
|
ThemeProvider,
|
|
4217
5135
|
TrueFalse,
|