@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.cjs
CHANGED
|
@@ -57,6 +57,8 @@ __export(index_exports, {
|
|
|
57
57
|
Quiz: () => Quiz,
|
|
58
58
|
Reflection: () => Reflection,
|
|
59
59
|
Scenario: () => Scenario,
|
|
60
|
+
Slide: () => Slide,
|
|
61
|
+
SlideDeck: () => SlideDeck,
|
|
60
62
|
Text: () => Text,
|
|
61
63
|
ThemeProvider: () => ThemeProvider,
|
|
62
64
|
TrueFalse: () => TrueFalse,
|
|
@@ -64,13 +66,13 @@ __export(index_exports, {
|
|
|
64
66
|
blockCatalogV3Version: () => blockCatalogV3Version,
|
|
65
67
|
blockCatalogVersion: () => blockCatalogVersion,
|
|
66
68
|
buildBlockCatalog: () => buildBlockCatalog,
|
|
67
|
-
buildTelemetryEvent: () =>
|
|
68
|
-
createLessonkitRuntime: () =>
|
|
69
|
-
createPluginRegistry: () =>
|
|
70
|
-
createTelemetryPipeline: () =>
|
|
71
|
-
defineAssessmentPlugin: () =>
|
|
72
|
-
defineLifecyclePlugin: () =>
|
|
73
|
-
defineTelemetryPlugin: () =>
|
|
69
|
+
buildTelemetryEvent: () => import_core19.buildTelemetryEvent,
|
|
70
|
+
createLessonkitRuntime: () => import_core19.createLessonkitRuntime,
|
|
71
|
+
createPluginRegistry: () => import_core19.createPluginRegistry,
|
|
72
|
+
createTelemetryPipeline: () => import_core19.createTelemetryPipeline,
|
|
73
|
+
defineAssessmentPlugin: () => import_core19.defineAssessmentPlugin,
|
|
74
|
+
defineLifecyclePlugin: () => import_core19.defineLifecyclePlugin,
|
|
75
|
+
defineTelemetryPlugin: () => import_core19.defineTelemetryPlugin,
|
|
74
76
|
getBlockCatalogEntry: () => getBlockCatalogEntry,
|
|
75
77
|
resetAssessmentWarningsForTests: () => resetAssessmentWarningsForTests,
|
|
76
78
|
resetQuizWarningsForTests: () => resetQuizWarningsForTests,
|
|
@@ -85,7 +87,7 @@ __export(index_exports, {
|
|
|
85
87
|
module.exports = __toCommonJS(index_exports);
|
|
86
88
|
|
|
87
89
|
// src/components.tsx
|
|
88
|
-
var
|
|
90
|
+
var import_react13 = require("react");
|
|
89
91
|
var import_accessibility2 = require("@lessonkit/accessibility");
|
|
90
92
|
|
|
91
93
|
// src/context.tsx
|
|
@@ -97,6 +99,7 @@ var import_core8 = require("@lessonkit/core");
|
|
|
97
99
|
|
|
98
100
|
// src/runtime/observability.ts
|
|
99
101
|
var import_xapi = require("@lessonkit/xapi");
|
|
102
|
+
var import_meta = {};
|
|
100
103
|
function createXapiQueueFromObservability(observability) {
|
|
101
104
|
const opts = {};
|
|
102
105
|
if (observability?.onXapiQueueDepth) {
|
|
@@ -107,6 +110,44 @@ function createXapiQueueFromObservability(observability) {
|
|
|
107
110
|
}
|
|
108
111
|
return (0, import_xapi.createInMemoryXAPIQueue)(opts);
|
|
109
112
|
}
|
|
113
|
+
function wrapBatchSink(batchSink, observability) {
|
|
114
|
+
if (!batchSink || !observability?.onTelemetrySinkError) return batchSink;
|
|
115
|
+
const onError = observability.onTelemetrySinkError;
|
|
116
|
+
return async (events) => {
|
|
117
|
+
try {
|
|
118
|
+
await batchSink(events);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
onError(err, { sinkId: "tracking-batch" });
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function warnMissingProductionObservability(observability, opts) {
|
|
126
|
+
let isProduction = false;
|
|
127
|
+
try {
|
|
128
|
+
isProduction = import_meta.env?.PROD === true;
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
if (!isProduction) {
|
|
132
|
+
const g = globalThis;
|
|
133
|
+
isProduction = typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
|
|
134
|
+
}
|
|
135
|
+
if (!isProduction) return;
|
|
136
|
+
if (!opts.trackingEnabled && !opts.xapiEnabled) return;
|
|
137
|
+
const hooks = [
|
|
138
|
+
observability?.onTelemetrySinkError,
|
|
139
|
+
observability?.onTelemetryBufferDrop,
|
|
140
|
+
observability?.onXapiQueueDepth,
|
|
141
|
+
observability?.onXapiQueueCap,
|
|
142
|
+
observability?.onLxpackBridgeMiss
|
|
143
|
+
];
|
|
144
|
+
if (hooks.some(Boolean)) return;
|
|
145
|
+
if (typeof console !== "undefined") {
|
|
146
|
+
console.warn(
|
|
147
|
+
"[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"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
110
151
|
function wrapTrackingSink(sink, observability) {
|
|
111
152
|
if (!sink || !observability?.onTelemetrySinkError) return sink;
|
|
112
153
|
const onError = observability.onTelemetrySinkError;
|
|
@@ -216,6 +257,9 @@ function emitTelemetry(tracking, xapi, event, opts) {
|
|
|
216
257
|
// src/runtime/ports.ts
|
|
217
258
|
var import_core3 = require("@lessonkit/core");
|
|
218
259
|
|
|
260
|
+
// src/provider/useLessonkitProviderRuntime.ts
|
|
261
|
+
var import_core9 = require("@lessonkit/core");
|
|
262
|
+
|
|
219
263
|
// src/runtime/progress.ts
|
|
220
264
|
var import_core4 = require("@lessonkit/core");
|
|
221
265
|
|
|
@@ -230,6 +274,7 @@ function createXapiClientFromConfig(config, queue) {
|
|
|
230
274
|
return (0, import_xapi3.createXAPIClient)({
|
|
231
275
|
courseId: config.courseId,
|
|
232
276
|
transport: config.xapi?.transport,
|
|
277
|
+
exitTransport: config.xapi?.exitTransport,
|
|
233
278
|
queue
|
|
234
279
|
});
|
|
235
280
|
}
|
|
@@ -278,6 +323,7 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
|
|
|
278
323
|
const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(opts.event);
|
|
279
324
|
if (statement) {
|
|
280
325
|
opts.xapi.send(statement);
|
|
326
|
+
await opts.xapi.flush();
|
|
281
327
|
xapiStatementSent = true;
|
|
282
328
|
}
|
|
283
329
|
}
|
|
@@ -313,7 +359,7 @@ function emitTelemetryWithPlugins(opts) {
|
|
|
313
359
|
}
|
|
314
360
|
|
|
315
361
|
// src/provider/courseStarted/emit.ts
|
|
316
|
-
var
|
|
362
|
+
var courseStartedTrackingFlights = /* @__PURE__ */ new Map();
|
|
317
363
|
function isTrackingActive(tracking) {
|
|
318
364
|
return tracking?.enabled !== false;
|
|
319
365
|
}
|
|
@@ -341,24 +387,40 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
|
|
|
341
387
|
if ((0, import_core5.hasCourseStartedEmittedToTracking)(storage, sessionId, courseId)) {
|
|
342
388
|
return true;
|
|
343
389
|
}
|
|
344
|
-
|
|
345
|
-
|
|
390
|
+
const existing = courseStartedTrackingFlights.get(flightKey);
|
|
391
|
+
if (existing) {
|
|
392
|
+
const settled = await existing;
|
|
393
|
+
if (settled) return true;
|
|
346
394
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
395
|
+
let resolveFlight;
|
|
396
|
+
const flight = new Promise((resolve) => {
|
|
397
|
+
resolveFlight = resolve;
|
|
398
|
+
});
|
|
399
|
+
courseStartedTrackingFlights.set(flightKey, flight);
|
|
400
|
+
void (async () => {
|
|
401
|
+
try {
|
|
402
|
+
if (shouldCommit && !shouldCommit()) {
|
|
403
|
+
resolveFlight(false);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
tracking.track(event);
|
|
407
|
+
const delivered = await tracking.flush?.();
|
|
408
|
+
if (delivered === false) {
|
|
409
|
+
resolveFlight(false);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if ((0, import_core5.markCourseStartedEmittedToTracking)(storage, sessionId, courseId) === false) {
|
|
413
|
+
resolveFlight(false);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
resolveFlight(true);
|
|
417
|
+
} catch {
|
|
418
|
+
resolveFlight(false);
|
|
419
|
+
} finally {
|
|
420
|
+
courseStartedTrackingFlights.delete(flightKey);
|
|
360
421
|
}
|
|
361
|
-
}
|
|
422
|
+
})();
|
|
423
|
+
return flight;
|
|
362
424
|
}
|
|
363
425
|
async function emitCourseStartedPipelineOnly(opts) {
|
|
364
426
|
try {
|
|
@@ -372,8 +434,10 @@ async function emitCourseStartedPipelineOnly(opts) {
|
|
|
372
434
|
skipXapi: opts.skipXapi
|
|
373
435
|
});
|
|
374
436
|
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
375
|
-
(0, import_core5.markCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
|
|
376
|
-
(0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId)
|
|
437
|
+
if ((0, import_core5.markCourseStarted)(opts.storage, opts.sessionId, opts.courseId) === false) return "failed";
|
|
438
|
+
if ((0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId) === false) {
|
|
439
|
+
return "failed";
|
|
440
|
+
}
|
|
377
441
|
if (xapiStatementSent) {
|
|
378
442
|
opts.onXapiStatementSent?.();
|
|
379
443
|
}
|
|
@@ -424,7 +488,9 @@ async function emitCourseStartedToTrackingOnly(opts) {
|
|
|
424
488
|
extraSinks: opts.extraSinks,
|
|
425
489
|
skipXapi: true
|
|
426
490
|
});
|
|
427
|
-
(0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId)
|
|
491
|
+
if ((0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId) === false) {
|
|
492
|
+
return "failed";
|
|
493
|
+
}
|
|
428
494
|
return "emitted";
|
|
429
495
|
} catch {
|
|
430
496
|
return "failed";
|
|
@@ -477,13 +543,15 @@ function assertTrackingSinkConfig(tracking) {
|
|
|
477
543
|
|
|
478
544
|
// src/runtime/telemetry.ts
|
|
479
545
|
var import_core7 = require("@lessonkit/core");
|
|
480
|
-
function createTrackingClientFromConfig(config) {
|
|
546
|
+
function createTrackingClientFromConfig(config, observability) {
|
|
481
547
|
if (config.tracking?.enabled === false) return (0, import_core7.createTrackingClient)();
|
|
482
548
|
if (config.tracking?.createClient) return config.tracking.createClient();
|
|
483
549
|
return (0, import_core7.createTrackingClient)({
|
|
484
550
|
sink: config.tracking?.sink,
|
|
485
551
|
batchSink: config.tracking?.batchSink,
|
|
486
|
-
batch: config.tracking?.batch
|
|
552
|
+
batch: config.tracking?.batch,
|
|
553
|
+
exitBatchSink: config.tracking?.exitBatchSink,
|
|
554
|
+
onBufferDrop: observability?.onTelemetryBufferDrop
|
|
487
555
|
});
|
|
488
556
|
}
|
|
489
557
|
async function disposeTrackingClient(client) {
|
|
@@ -558,6 +626,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
558
626
|
const pendingCourseIdResetRef = (0, import_react.useRef)(false);
|
|
559
627
|
const prevUseV2RuntimeRef = (0, import_react.useRef)(useV2Runtime);
|
|
560
628
|
const xapiCourseStartedSentOnClientRef = (0, import_react.useRef)(false);
|
|
629
|
+
const xapiBootstrapSendRef = (0, import_react.useRef)(false);
|
|
561
630
|
if (prevUseV2RuntimeRef.current !== useV2Runtime) {
|
|
562
631
|
prevUseV2RuntimeRef.current = useV2Runtime;
|
|
563
632
|
if (useV2Runtime) {
|
|
@@ -565,7 +634,8 @@ function useLessonkitProviderRuntime(config) {
|
|
|
565
634
|
courseId: normalizedCourseId,
|
|
566
635
|
runtimeVersion: "v2",
|
|
567
636
|
session: normalizedConfig.session,
|
|
568
|
-
plugins: pluginHostRef.current ?? normalizedConfig.plugins
|
|
637
|
+
plugins: pluginHostRef.current ?? normalizedConfig.plugins,
|
|
638
|
+
deferPluginSetup: true
|
|
569
639
|
});
|
|
570
640
|
progressRef.current = headlessRef.current.progress;
|
|
571
641
|
} else {
|
|
@@ -580,7 +650,8 @@ function useLessonkitProviderRuntime(config) {
|
|
|
580
650
|
courseId: normalizedCourseId,
|
|
581
651
|
runtimeVersion: "v2",
|
|
582
652
|
session: normalizedConfig.session,
|
|
583
|
-
plugins: pluginHostRef.current ?? normalizedConfig.plugins
|
|
653
|
+
plugins: pluginHostRef.current ?? normalizedConfig.plugins,
|
|
654
|
+
deferPluginSetup: true
|
|
584
655
|
});
|
|
585
656
|
}
|
|
586
657
|
if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
|
|
@@ -628,19 +699,22 @@ function useLessonkitProviderRuntime(config) {
|
|
|
628
699
|
xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
|
|
629
700
|
prevXapiCourseIdRef.current = courseId;
|
|
630
701
|
xapiCourseStartedSentOnClientRef.current = false;
|
|
702
|
+
xapiBootstrapSendRef.current = false;
|
|
631
703
|
}
|
|
632
704
|
const prev = xapiRef.current;
|
|
633
705
|
const next = createXapiClientFromConfig(normalizedConfig, xapiQueueRef.current);
|
|
634
706
|
xapiRef.current = next;
|
|
635
707
|
setXapi(next);
|
|
708
|
+
let bootstrapSent = false;
|
|
709
|
+
let bootstrapAlreadyStarted = false;
|
|
636
710
|
if (next) {
|
|
637
711
|
const sessionId = sessionIdRef.current;
|
|
638
712
|
const cid = courseIdRef.current;
|
|
639
713
|
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
640
|
-
|
|
714
|
+
bootstrapAlreadyStarted = (0, import_core5.hasCourseStarted)(defaultStorage, sessionId, cid);
|
|
641
715
|
const clientChanged = !prev || prev !== next;
|
|
642
|
-
const skipBootstrap = trackingActive && !
|
|
643
|
-
const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!
|
|
716
|
+
const skipBootstrap = trackingActive && !bootstrapAlreadyStarted;
|
|
717
|
+
const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && !xapiBootstrapSendRef.current && (!bootstrapAlreadyStarted || clientChanged);
|
|
644
718
|
if (needsBootstrap) {
|
|
645
719
|
try {
|
|
646
720
|
const event = buildCourseStartedEvent({
|
|
@@ -651,15 +725,12 @@ function useLessonkitProviderRuntime(config) {
|
|
|
651
725
|
user: userRef.current,
|
|
652
726
|
lxpackBridge: lxpackBridgeModeRef.current
|
|
653
727
|
});
|
|
654
|
-
if (event
|
|
655
|
-
} else {
|
|
728
|
+
if (event !== null) {
|
|
656
729
|
const statement = (0, import_xapi5.telemetryEventToXAPIStatement)(event);
|
|
657
730
|
if (statement) {
|
|
658
731
|
next.send(statement);
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
xapiCourseStartedSentOnClientRef.current = true;
|
|
732
|
+
xapiBootstrapSendRef.current = true;
|
|
733
|
+
bootstrapSent = true;
|
|
663
734
|
}
|
|
664
735
|
}
|
|
665
736
|
} catch {
|
|
@@ -677,6 +748,12 @@ function useLessonkitProviderRuntime(config) {
|
|
|
677
748
|
if (cancelled) return;
|
|
678
749
|
try {
|
|
679
750
|
await next?.flush();
|
|
751
|
+
if (bootstrapSent && !cancelled) {
|
|
752
|
+
if (!bootstrapAlreadyStarted) {
|
|
753
|
+
(0, import_core5.markCourseStarted)(defaultStorage, sessionIdRef.current, courseIdRef.current);
|
|
754
|
+
}
|
|
755
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
756
|
+
}
|
|
680
757
|
} catch {
|
|
681
758
|
}
|
|
682
759
|
})();
|
|
@@ -705,7 +782,10 @@ function useLessonkitProviderRuntime(config) {
|
|
|
705
782
|
useIsoLayoutEffect(() => {
|
|
706
783
|
const prev = trackingRef.current;
|
|
707
784
|
const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
|
|
708
|
-
const userBatchSink =
|
|
785
|
+
const userBatchSink = wrapBatchSink(
|
|
786
|
+
normalizedConfig.tracking?.batchSink,
|
|
787
|
+
observabilityRef.current
|
|
788
|
+
);
|
|
709
789
|
assertTrackingSinkConfig(normalizedConfig.tracking);
|
|
710
790
|
const sink = pluginHostRef.current && baseSink ? (
|
|
711
791
|
/* v8 ignore next -- composeTrackingSink may return null; fall back to base sink */
|
|
@@ -725,9 +805,12 @@ function useLessonkitProviderRuntime(config) {
|
|
|
725
805
|
}
|
|
726
806
|
return userBatchSink(perEventForBatch);
|
|
727
807
|
} : userBatchSink;
|
|
728
|
-
const next = createTrackingClientFromConfig(
|
|
729
|
-
|
|
730
|
-
|
|
808
|
+
const next = createTrackingClientFromConfig(
|
|
809
|
+
{
|
|
810
|
+
tracking: { ...normalizedConfig.tracking, sink, batchSink }
|
|
811
|
+
},
|
|
812
|
+
observabilityRef.current
|
|
813
|
+
);
|
|
731
814
|
trackingRef.current = next;
|
|
732
815
|
trackingClientForUnmountRef.current = next;
|
|
733
816
|
setTracking(next);
|
|
@@ -856,7 +939,11 @@ function useLessonkitProviderRuntime(config) {
|
|
|
856
939
|
user: userRef.current,
|
|
857
940
|
lxpackBridge: lxpackBridgeModeRef.current,
|
|
858
941
|
onLxpackBridgeMiss,
|
|
859
|
-
extraSinks: extraSinksRef.current
|
|
942
|
+
extraSinks: extraSinksRef.current,
|
|
943
|
+
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
944
|
+
onXapiStatementSent: () => {
|
|
945
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
946
|
+
}
|
|
860
947
|
});
|
|
861
948
|
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
862
949
|
}
|
|
@@ -912,20 +999,39 @@ function useLessonkitProviderRuntime(config) {
|
|
|
912
999
|
}, []);
|
|
913
1000
|
(0, import_react.useEffect)(() => {
|
|
914
1001
|
if (typeof document === "undefined") return;
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
1002
|
+
const flushOnPageExit = () => {
|
|
1003
|
+
try {
|
|
1004
|
+
xapiRef.current?.flushOnExit?.();
|
|
1005
|
+
trackingRef.current?.flushOnExit?.();
|
|
1006
|
+
} finally {
|
|
1007
|
+
void xapiRef.current?.flush();
|
|
1008
|
+
void trackingRef.current?.flush?.();
|
|
1009
|
+
}
|
|
918
1010
|
};
|
|
919
1011
|
const onVisibilityChange = () => {
|
|
920
|
-
if (document.visibilityState === "hidden")
|
|
1012
|
+
if (document.visibilityState === "hidden") flushOnPageExit();
|
|
921
1013
|
};
|
|
922
1014
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
923
|
-
window.addEventListener("pagehide",
|
|
1015
|
+
window.addEventListener("pagehide", flushOnPageExit);
|
|
924
1016
|
return () => {
|
|
925
1017
|
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
926
|
-
window.removeEventListener("pagehide",
|
|
1018
|
+
window.removeEventListener("pagehide", flushOnPageExit);
|
|
927
1019
|
};
|
|
928
1020
|
}, []);
|
|
1021
|
+
(0, import_react.useEffect)(() => {
|
|
1022
|
+
warnMissingProductionObservability(observabilityRef.current, {
|
|
1023
|
+
trackingEnabled: isTrackingActive(normalizedConfig.tracking),
|
|
1024
|
+
xapiEnabled: normalizedConfig.xapi?.enabled !== false && Boolean(
|
|
1025
|
+
normalizedConfig.xapi?.client || normalizedConfig.xapi?.transport || normalizedConfig.xapi?.enabled === true
|
|
1026
|
+
)
|
|
1027
|
+
});
|
|
1028
|
+
}, [
|
|
1029
|
+
normalizedConfig.tracking,
|
|
1030
|
+
normalizedConfig.xapi?.enabled,
|
|
1031
|
+
normalizedConfig.xapi?.client,
|
|
1032
|
+
normalizedConfig.xapi?.transport,
|
|
1033
|
+
normalizedConfig.observability
|
|
1034
|
+
]);
|
|
929
1035
|
const setActiveLesson = (0, import_react.useCallback)(
|
|
930
1036
|
(lessonId) => {
|
|
931
1037
|
if (useV2Runtime && headlessRef.current) {
|
|
@@ -1045,6 +1151,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
1045
1151
|
config: normalizedConfig,
|
|
1046
1152
|
tracking,
|
|
1047
1153
|
xapi,
|
|
1154
|
+
storage: defaultStorage,
|
|
1048
1155
|
session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
|
|
1049
1156
|
progress,
|
|
1050
1157
|
setActiveLesson,
|
|
@@ -1142,17 +1249,17 @@ function useEnclosingLessonId() {
|
|
|
1142
1249
|
}
|
|
1143
1250
|
|
|
1144
1251
|
// src/runtime/validateComponentId.ts
|
|
1145
|
-
var
|
|
1252
|
+
var import_core10 = require("@lessonkit/core");
|
|
1146
1253
|
function isDevEnvironment4() {
|
|
1147
1254
|
const g = globalThis;
|
|
1148
1255
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
1149
1256
|
}
|
|
1150
1257
|
function normalizeComponentId(id, path) {
|
|
1151
|
-
if (path === "courseId") return (0,
|
|
1152
|
-
if (path === "lessonId") return (0,
|
|
1153
|
-
if (path === "checkId") return (0,
|
|
1154
|
-
if (path === "blockId") return (0,
|
|
1155
|
-
return (0,
|
|
1258
|
+
if (path === "courseId") return (0, import_core10.assertValidId)(id, "courseId");
|
|
1259
|
+
if (path === "lessonId") return (0, import_core10.assertValidId)(id, "lessonId");
|
|
1260
|
+
if (path === "checkId") return (0, import_core10.assertValidId)(id, "checkId");
|
|
1261
|
+
if (path === "blockId") return (0, import_core10.assertValidId)(id, "blockId");
|
|
1262
|
+
return (0, import_core10.assertValidId)(id, path);
|
|
1156
1263
|
}
|
|
1157
1264
|
|
|
1158
1265
|
// src/runtime/lessonMountRegistry.ts
|
|
@@ -1180,7 +1287,7 @@ function getLessonMountCount(lessonId) {
|
|
|
1180
1287
|
}
|
|
1181
1288
|
|
|
1182
1289
|
// src/components/Quiz.tsx
|
|
1183
|
-
var
|
|
1290
|
+
var import_react12 = require("react");
|
|
1184
1291
|
var import_accessibility = require("@lessonkit/accessibility");
|
|
1185
1292
|
|
|
1186
1293
|
// src/assessment/AssessmentLessonGuard.tsx
|
|
@@ -1239,140 +1346,184 @@ function readStringField(state, key) {
|
|
|
1239
1346
|
if (typeof value === "string" || value === null) return value;
|
|
1240
1347
|
return void 0;
|
|
1241
1348
|
}
|
|
1349
|
+
function readNumberField(state, key) {
|
|
1350
|
+
const value = state[key];
|
|
1351
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1352
|
+
if (value === null) return null;
|
|
1353
|
+
return void 0;
|
|
1354
|
+
}
|
|
1242
1355
|
function readBooleanStateField(state, key, apply) {
|
|
1243
1356
|
const value = state[key];
|
|
1244
1357
|
if (typeof value === "boolean") apply(value);
|
|
1245
1358
|
}
|
|
1246
1359
|
|
|
1247
1360
|
// src/assessment/internal/useAssessmentHandleRegistration.ts
|
|
1248
|
-
var
|
|
1361
|
+
var import_react10 = require("react");
|
|
1249
1362
|
|
|
1250
1363
|
// src/compound/CompoundProvider.tsx
|
|
1251
|
-
var
|
|
1252
|
-
var
|
|
1364
|
+
var import_react9 = __toESM(require("react"), 1);
|
|
1365
|
+
var import_core11 = require("@lessonkit/core");
|
|
1253
1366
|
|
|
1254
1367
|
// src/compound/aggregateScores.ts
|
|
1255
|
-
function aggregateAssessmentScores(handles) {
|
|
1368
|
+
function aggregateAssessmentScores(handles, opts) {
|
|
1256
1369
|
let score = 0;
|
|
1257
1370
|
let maxScore = 0;
|
|
1258
1371
|
let allAnswered = true;
|
|
1259
|
-
for (const
|
|
1372
|
+
for (const entry of handles) {
|
|
1373
|
+
const handle = "handle" in entry ? entry.handle : entry;
|
|
1374
|
+
const pageIndex = "handle" in entry ? entry.pageIndex : void 0;
|
|
1260
1375
|
score += handle.getScore();
|
|
1261
1376
|
maxScore += handle.getMaxScore();
|
|
1262
|
-
|
|
1377
|
+
const countsForAnswerGiven = opts?.answerPageIndex === void 0 || pageIndex === void 0 || pageIndex === opts.answerPageIndex;
|
|
1378
|
+
if (countsForAnswerGiven && !handle.getAnswerGiven()) allAnswered = false;
|
|
1263
1379
|
}
|
|
1264
1380
|
return { score, maxScore, allAnswered };
|
|
1265
1381
|
}
|
|
1266
1382
|
|
|
1267
|
-
// src/compound/
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1383
|
+
// src/compound/CompoundHydrationBridge.tsx
|
|
1384
|
+
var import_react7 = require("react");
|
|
1385
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1386
|
+
var CompoundHydrationBridgeContext = (0, import_react7.createContext)(
|
|
1387
|
+
null
|
|
1388
|
+
);
|
|
1389
|
+
function CompoundHydrationBridgeProvider({ children }) {
|
|
1390
|
+
const bridgeRef = (0, import_react7.useRef)(null);
|
|
1391
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CompoundHydrationBridgeContext.Provider, { value: bridgeRef, children });
|
|
1392
|
+
}
|
|
1393
|
+
function useCompoundHydrationBridgeRef() {
|
|
1394
|
+
return (0, import_react7.useContext)(CompoundHydrationBridgeContext);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// src/compound/CompoundPageIndexContext.tsx
|
|
1398
|
+
var import_react8 = require("react");
|
|
1399
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1400
|
+
var CompoundPageIndexContext = (0, import_react8.createContext)(void 0);
|
|
1401
|
+
function CompoundPageIndexProvider({
|
|
1402
|
+
pageIndex,
|
|
1403
|
+
children
|
|
1404
|
+
}) {
|
|
1405
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CompoundPageIndexContext.Provider, { value: pageIndex, children });
|
|
1406
|
+
}
|
|
1407
|
+
function useCompoundPageIndex() {
|
|
1408
|
+
return (0, import_react8.useContext)(CompoundPageIndexContext);
|
|
1277
1409
|
}
|
|
1278
1410
|
|
|
1279
1411
|
// src/compound/CompoundProvider.tsx
|
|
1280
|
-
var
|
|
1281
|
-
var CompoundRegistryContext = (0,
|
|
1282
|
-
var CompoundHandlesVersionContext = (0,
|
|
1412
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1413
|
+
var CompoundRegistryContext = (0, import_react9.createContext)(null);
|
|
1414
|
+
var CompoundHandlesVersionContext = (0, import_react9.createContext)(0);
|
|
1283
1415
|
function CompoundProvider({
|
|
1284
1416
|
children,
|
|
1285
1417
|
activePageIndex: _activePageIndex,
|
|
1286
1418
|
onActivePageIndexChange: _onActivePageIndexChange
|
|
1287
1419
|
}) {
|
|
1288
|
-
const registryRef = (0,
|
|
1289
|
-
const [handlesVersion, setHandlesVersion] = (0,
|
|
1290
|
-
const register = (0,
|
|
1420
|
+
const registryRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
|
|
1421
|
+
const [handlesVersion, setHandlesVersion] = (0, import_react9.useState)(0);
|
|
1422
|
+
const register = (0, import_react9.useCallback)((checkId, handle, pageIndex) => {
|
|
1291
1423
|
const prev = registryRef.current.get(checkId);
|
|
1292
|
-
|
|
1293
|
-
|
|
1424
|
+
if (prev && prev.handle !== handle) {
|
|
1425
|
+
const message = `[lessonkit] duplicate checkId "${checkId}" registered in the same compound container; the previous handle was replaced.`;
|
|
1426
|
+
if (isDevEnvironment4()) {
|
|
1427
|
+
console.error(message);
|
|
1428
|
+
} else {
|
|
1429
|
+
console.warn(message);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
registryRef.current.set(checkId, { handle, pageIndex });
|
|
1433
|
+
if (prev?.handle !== handle || prev?.pageIndex !== pageIndex) {
|
|
1294
1434
|
setHandlesVersion((v) => v + 1);
|
|
1295
1435
|
}
|
|
1296
1436
|
return () => {
|
|
1297
|
-
|
|
1437
|
+
const current = registryRef.current.get(checkId);
|
|
1438
|
+
if (current?.handle === handle) {
|
|
1298
1439
|
registryRef.current.delete(checkId);
|
|
1299
1440
|
setHandlesVersion((v) => v + 1);
|
|
1300
1441
|
}
|
|
1301
1442
|
};
|
|
1302
1443
|
}, []);
|
|
1303
|
-
const registryValue = (0,
|
|
1444
|
+
const registryValue = (0, import_react9.useMemo)(
|
|
1304
1445
|
() => ({
|
|
1305
1446
|
register,
|
|
1306
|
-
getHandles: () =>
|
|
1447
|
+
getHandles: () => {
|
|
1448
|
+
const handles = /* @__PURE__ */ new Map();
|
|
1449
|
+
for (const [checkId, entry] of registryRef.current) {
|
|
1450
|
+
handles.set(checkId, entry.handle);
|
|
1451
|
+
}
|
|
1452
|
+
return handles;
|
|
1453
|
+
},
|
|
1454
|
+
getRegisteredHandles: () => registryRef.current
|
|
1307
1455
|
}),
|
|
1308
1456
|
[register]
|
|
1309
1457
|
);
|
|
1310
|
-
return /* @__PURE__ */ (0,
|
|
1458
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CompoundHydrationBridgeProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) }) });
|
|
1311
1459
|
}
|
|
1312
1460
|
function useCompoundRegistry() {
|
|
1313
|
-
const registry = (0,
|
|
1314
|
-
const handlesVersion = (0,
|
|
1461
|
+
const registry = (0, import_react9.useContext)(CompoundRegistryContext);
|
|
1462
|
+
const handlesVersion = (0, import_react9.useContext)(CompoundHandlesVersionContext);
|
|
1315
1463
|
if (!registry) return null;
|
|
1316
1464
|
return { ...registry, handlesVersion };
|
|
1317
1465
|
}
|
|
1318
1466
|
function useCompoundHandlesVersion() {
|
|
1319
|
-
return (0,
|
|
1467
|
+
return (0, import_react9.useContext)(CompoundHandlesVersionContext);
|
|
1320
1468
|
}
|
|
1321
1469
|
function useRegisterAssessmentHandle(checkId, handle) {
|
|
1322
|
-
const registry = (0,
|
|
1323
|
-
|
|
1470
|
+
const registry = (0, import_react9.useContext)(CompoundRegistryContext);
|
|
1471
|
+
const pageIndex = useCompoundPageIndex();
|
|
1472
|
+
import_react9.default.useLayoutEffect(() => {
|
|
1324
1473
|
if (!registry || !handle) return;
|
|
1325
|
-
return registry.register(checkId, handle);
|
|
1326
|
-
}, [registry, checkId, handle]);
|
|
1474
|
+
return registry.register(checkId, handle, pageIndex);
|
|
1475
|
+
}, [registry, checkId, handle, pageIndex]);
|
|
1327
1476
|
}
|
|
1328
1477
|
function useCompoundHandleRef(ref, opts) {
|
|
1329
|
-
const { activePageIndex, setActivePageIndex, getHandles, pageCount } = opts;
|
|
1330
|
-
const
|
|
1478
|
+
const { activePageIndex, setActivePageIndex, getHandles, getRegisteredHandles, pageCount } = opts;
|
|
1479
|
+
const bridgeRef = useCompoundHydrationBridgeRef();
|
|
1480
|
+
const setIndexClamped = (0, import_react9.useCallback)(
|
|
1331
1481
|
(index) => {
|
|
1332
|
-
const next = pageCount !== void 0 ? (0,
|
|
1482
|
+
const next = pageCount !== void 0 ? (0, import_core11.clampCompoundPageIndex)(index, pageCount) : Math.max(0, Math.floor(index));
|
|
1333
1483
|
setActivePageIndex(next);
|
|
1334
1484
|
},
|
|
1335
1485
|
[pageCount, setActivePageIndex]
|
|
1336
1486
|
);
|
|
1337
|
-
(0,
|
|
1487
|
+
(0, import_react9.useImperativeHandle)(
|
|
1338
1488
|
ref,
|
|
1339
1489
|
() => ({
|
|
1340
|
-
getScore: () => aggregateAssessmentScores(
|
|
1341
|
-
getMaxScore: () => aggregateAssessmentScores(
|
|
1342
|
-
getAnswerGiven: () => aggregateAssessmentScores(
|
|
1490
|
+
getScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).score,
|
|
1491
|
+
getMaxScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).maxScore,
|
|
1492
|
+
getAnswerGiven: () => aggregateAssessmentScores(getRegisteredHandles().values(), {
|
|
1493
|
+
answerPageIndex: activePageIndex
|
|
1494
|
+
}).allAnswered,
|
|
1343
1495
|
resetTask: () => {
|
|
1344
|
-
for (const
|
|
1496
|
+
for (const entry of getRegisteredHandles().values()) entry.handle.resetTask();
|
|
1345
1497
|
},
|
|
1346
1498
|
showSolutions: () => {
|
|
1347
1499
|
if (!opts.enableSolutionsButton) return;
|
|
1348
|
-
for (const
|
|
1500
|
+
for (const entry of getRegisteredHandles().values()) entry.handle.showSolutions();
|
|
1349
1501
|
},
|
|
1350
1502
|
getCurrentState: () => {
|
|
1351
1503
|
const childStates = {};
|
|
1352
|
-
for (const [checkId,
|
|
1353
|
-
if (handle.getCurrentState) {
|
|
1354
|
-
childStates[checkId] = handle.getCurrentState();
|
|
1504
|
+
for (const [checkId, entry] of getRegisteredHandles()) {
|
|
1505
|
+
if (entry.handle.getCurrentState) {
|
|
1506
|
+
childStates[checkId] = entry.handle.getCurrentState();
|
|
1355
1507
|
}
|
|
1356
1508
|
}
|
|
1357
|
-
return (0,
|
|
1509
|
+
return (0, import_core11.createCompoundResumeState)({ activePageIndex, childStates });
|
|
1358
1510
|
},
|
|
1359
1511
|
resume: (state) => {
|
|
1360
|
-
|
|
1361
|
-
resumeChildHandles(getHandles(), state.childStates);
|
|
1512
|
+
bridgeRef?.current?.notifyImperativeResume(state);
|
|
1362
1513
|
}
|
|
1363
1514
|
}),
|
|
1364
|
-
[activePageIndex, setIndexClamped, getHandles, opts.enableSolutionsButton]
|
|
1515
|
+
[activePageIndex, setIndexClamped, getHandles, getRegisteredHandles, opts.enableSolutionsButton, bridgeRef]
|
|
1365
1516
|
);
|
|
1366
1517
|
}
|
|
1367
1518
|
|
|
1368
1519
|
// src/assessment/internal/useAssessmentHandleRegistration.ts
|
|
1369
1520
|
function useAssessmentHandleRegistration(checkId, handle, ref) {
|
|
1370
|
-
(0,
|
|
1521
|
+
(0, import_react10.useImperativeHandle)(ref, () => handle, [handle]);
|
|
1371
1522
|
useRegisterAssessmentHandle(checkId, handle);
|
|
1372
1523
|
}
|
|
1373
1524
|
|
|
1374
1525
|
// src/assessment/internal/usePluginScoring.ts
|
|
1375
|
-
var
|
|
1526
|
+
var import_react11 = require("react");
|
|
1376
1527
|
|
|
1377
1528
|
// src/assessment/scoring.ts
|
|
1378
1529
|
function resolvePassingThreshold(passingScore, maxScore) {
|
|
@@ -1400,7 +1551,7 @@ function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore)
|
|
|
1400
1551
|
// src/assessment/internal/usePluginScoring.ts
|
|
1401
1552
|
function usePluginScoring(checkId, lessonId) {
|
|
1402
1553
|
const { plugins, config, session } = useLessonkit();
|
|
1403
|
-
const getPluginScore = (0,
|
|
1554
|
+
const getPluginScore = (0, import_react11.useCallback)(
|
|
1404
1555
|
(response) => {
|
|
1405
1556
|
const pluginCtx = buildPluginContext({
|
|
1406
1557
|
courseId: config.courseId,
|
|
@@ -1412,11 +1563,11 @@ function usePluginScoring(checkId, lessonId) {
|
|
|
1412
1563
|
},
|
|
1413
1564
|
[checkId, config.courseId, lessonId, plugins, session.attemptId, session.sessionId, session.user]
|
|
1414
1565
|
);
|
|
1415
|
-
const scoreResponse = (0,
|
|
1566
|
+
const scoreResponse = (0, import_react11.useCallback)(
|
|
1416
1567
|
(response, defaultCorrect, maxScore = 1, passingScore) => scoreFromCustom(getPluginScore(response), defaultCorrect, maxScore, passingScore),
|
|
1417
1568
|
[getPluginScore]
|
|
1418
1569
|
);
|
|
1419
|
-
const isChoiceCorrect = (0,
|
|
1570
|
+
const isChoiceCorrect = (0, import_react11.useCallback)(
|
|
1420
1571
|
(choice, answer, custom, passingScore) => {
|
|
1421
1572
|
if (!custom) return choice === answer;
|
|
1422
1573
|
if (custom.passed !== void 0) return custom.passed;
|
|
@@ -1431,53 +1582,94 @@ function usePluginScoring(checkId, lessonId) {
|
|
|
1431
1582
|
}
|
|
1432
1583
|
|
|
1433
1584
|
// src/components/Quiz.tsx
|
|
1434
|
-
var
|
|
1585
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1435
1586
|
function QuizInner(props, ref) {
|
|
1436
1587
|
const { enclosingLessonId } = props;
|
|
1437
|
-
const checkId = (0,
|
|
1588
|
+
const checkId = (0, import_react12.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1438
1589
|
const quiz = useQuizState(enclosingLessonId);
|
|
1439
1590
|
const { getPluginScore, isChoiceCorrect } = usePluginScoring(checkId, enclosingLessonId);
|
|
1440
|
-
const [selected, setSelected] = (0,
|
|
1441
|
-
const [selectionCorrect, setSelectionCorrect] = (0,
|
|
1442
|
-
const [quizPassed, setQuizPassed] = (0,
|
|
1443
|
-
const
|
|
1444
|
-
const
|
|
1591
|
+
const [selected, setSelected] = (0, import_react12.useState)(null);
|
|
1592
|
+
const [selectionCorrect, setSelectionCorrect] = (0, import_react12.useState)(null);
|
|
1593
|
+
const [quizPassed, setQuizPassed] = (0, import_react12.useState)(false);
|
|
1594
|
+
const [completedScore, setCompletedScore] = (0, import_react12.useState)(null);
|
|
1595
|
+
const [completedMaxScore, setCompletedMaxScore] = (0, import_react12.useState)(null);
|
|
1596
|
+
const completedRef = (0, import_react12.useRef)(false);
|
|
1597
|
+
const telemetryReplayedRef = (0, import_react12.useRef)(false);
|
|
1598
|
+
const questionId = (0, import_react12.useId)();
|
|
1445
1599
|
const choicesKey = props.choices.join("\0");
|
|
1446
|
-
(0,
|
|
1600
|
+
(0, import_react12.useEffect)(() => {
|
|
1447
1601
|
completedRef.current = false;
|
|
1602
|
+
telemetryReplayedRef.current = false;
|
|
1448
1603
|
setQuizPassed(false);
|
|
1449
1604
|
setSelected(null);
|
|
1450
1605
|
setSelectionCorrect(null);
|
|
1606
|
+
setCompletedScore(null);
|
|
1607
|
+
setCompletedMaxScore(null);
|
|
1451
1608
|
}, [checkId, props.answer, props.question, choicesKey]);
|
|
1452
1609
|
const passed = quizPassed;
|
|
1453
|
-
const
|
|
1610
|
+
const resolveScores = () => {
|
|
1611
|
+
const maxScore = completedMaxScore ?? 1;
|
|
1612
|
+
if (quizPassed) {
|
|
1613
|
+
return { score: completedScore ?? maxScore, maxScore };
|
|
1614
|
+
}
|
|
1615
|
+
if (selected !== null && selectionCorrect) {
|
|
1616
|
+
return { score: completedMaxScore ?? maxScore, maxScore };
|
|
1617
|
+
}
|
|
1618
|
+
return { score: 0, maxScore };
|
|
1619
|
+
};
|
|
1620
|
+
const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
|
|
1621
|
+
if (!nextPassed || telemetryReplayedRef.current) return;
|
|
1622
|
+
telemetryReplayedRef.current = true;
|
|
1623
|
+
if (nextSelected !== null) {
|
|
1624
|
+
quiz.answer({
|
|
1625
|
+
checkId,
|
|
1626
|
+
question: props.question,
|
|
1627
|
+
choice: nextSelected,
|
|
1628
|
+
correct: nextCorrect ?? false
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
quiz.complete({
|
|
1632
|
+
checkId,
|
|
1633
|
+
score: nextScore,
|
|
1634
|
+
maxScore: nextMaxScore,
|
|
1635
|
+
passingScore: props.passingScore ?? nextMaxScore
|
|
1636
|
+
});
|
|
1637
|
+
};
|
|
1638
|
+
const handle = (0, import_react12.useMemo)(
|
|
1454
1639
|
() => buildAssessmentHandle({
|
|
1455
1640
|
checkId,
|
|
1456
|
-
getScore: () =>
|
|
1457
|
-
|
|
1458
|
-
if (quizPassed && selected !== null) return maxScore;
|
|
1459
|
-
if (selected === null) return 0;
|
|
1460
|
-
return selectionCorrect ? maxScore : 0;
|
|
1461
|
-
},
|
|
1462
|
-
getMaxScore: () => 1,
|
|
1641
|
+
getScore: () => resolveScores().score,
|
|
1642
|
+
getMaxScore: () => resolveScores().maxScore,
|
|
1463
1643
|
getAnswerGiven: () => selected !== null,
|
|
1464
1644
|
resetTask: () => {
|
|
1465
1645
|
completedRef.current = false;
|
|
1646
|
+
telemetryReplayedRef.current = false;
|
|
1466
1647
|
setQuizPassed(false);
|
|
1467
1648
|
setSelected(null);
|
|
1468
1649
|
setSelectionCorrect(null);
|
|
1650
|
+
setCompletedScore(null);
|
|
1651
|
+
setCompletedMaxScore(null);
|
|
1469
1652
|
},
|
|
1470
1653
|
showSolutions: () => {
|
|
1471
1654
|
},
|
|
1472
|
-
getXAPIData: () =>
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1655
|
+
getXAPIData: () => {
|
|
1656
|
+
const { score, maxScore } = resolveScores();
|
|
1657
|
+
return {
|
|
1658
|
+
checkId,
|
|
1659
|
+
interactionType: "mcq",
|
|
1660
|
+
response: selected ?? void 0,
|
|
1661
|
+
correct: selectionCorrect ?? void 0,
|
|
1662
|
+
score,
|
|
1663
|
+
maxScore
|
|
1664
|
+
};
|
|
1665
|
+
},
|
|
1666
|
+
getCurrentState: () => ({
|
|
1667
|
+
selected,
|
|
1668
|
+
selectionCorrect,
|
|
1669
|
+
quizPassed,
|
|
1670
|
+
completedScore,
|
|
1671
|
+
completedMaxScore
|
|
1479
1672
|
}),
|
|
1480
|
-
getCurrentState: () => ({ selected, selectionCorrect, quizPassed }),
|
|
1481
1673
|
resume: (state) => {
|
|
1482
1674
|
const nextSelected = readStringField(state, "selected");
|
|
1483
1675
|
if (typeof nextSelected === "string" || nextSelected === null) setSelected(nextSelected);
|
|
@@ -1485,21 +1677,47 @@ function QuizInner(props, ref) {
|
|
|
1485
1677
|
if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
|
|
1486
1678
|
setSelectionCorrect(nextCorrect);
|
|
1487
1679
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1680
|
+
const nextCompletedScore = readNumberField(state, "completedScore");
|
|
1681
|
+
if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
|
|
1682
|
+
const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
|
|
1683
|
+
if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
|
|
1684
|
+
const nextPassed = readBooleanField(state, "quizPassed");
|
|
1685
|
+
if (nextPassed === true || nextPassed === false) {
|
|
1686
|
+
setQuizPassed(nextPassed);
|
|
1687
|
+
completedRef.current = nextPassed;
|
|
1688
|
+
if (nextPassed) {
|
|
1689
|
+
const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
|
|
1690
|
+
const score = nextCompletedScore ?? completedScore ?? maxScore;
|
|
1691
|
+
replayTelemetry(
|
|
1692
|
+
nextSelected ?? null,
|
|
1693
|
+
nextCorrect ?? null,
|
|
1694
|
+
nextPassed,
|
|
1695
|
+
score,
|
|
1696
|
+
maxScore
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1492
1700
|
}
|
|
1493
1701
|
}),
|
|
1494
|
-
[
|
|
1702
|
+
[
|
|
1703
|
+
checkId,
|
|
1704
|
+
completedMaxScore,
|
|
1705
|
+
completedScore,
|
|
1706
|
+
props.passingScore,
|
|
1707
|
+
props.question,
|
|
1708
|
+
quiz,
|
|
1709
|
+
quizPassed,
|
|
1710
|
+
selected,
|
|
1711
|
+
selectionCorrect
|
|
1712
|
+
]
|
|
1495
1713
|
);
|
|
1496
1714
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1497
|
-
return /* @__PURE__ */ (0,
|
|
1498
|
-
/* @__PURE__ */ (0,
|
|
1499
|
-
/* @__PURE__ */ (0,
|
|
1500
|
-
/* @__PURE__ */ (0,
|
|
1501
|
-
props.choices.map((c, i) => /* @__PURE__ */ (0,
|
|
1502
|
-
/* @__PURE__ */ (0,
|
|
1715
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
1716
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { id: questionId, children: props.question }),
|
|
1717
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
|
|
1718
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("legend", { style: import_accessibility.visuallyHiddenStyle, children: "Quiz choices" }),
|
|
1719
|
+
props.choices.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { style: { display: "block" }, children: [
|
|
1720
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1503
1721
|
"input",
|
|
1504
1722
|
{
|
|
1505
1723
|
type: "radio",
|
|
@@ -1524,9 +1742,12 @@ function QuizInner(props, ref) {
|
|
|
1524
1742
|
completedRef.current = true;
|
|
1525
1743
|
setQuizPassed(true);
|
|
1526
1744
|
const maxScore = custom?.maxScore ?? 1;
|
|
1745
|
+
const score = custom?.score ?? maxScore;
|
|
1746
|
+
setCompletedScore(score);
|
|
1747
|
+
setCompletedMaxScore(maxScore);
|
|
1527
1748
|
quiz.complete({
|
|
1528
1749
|
checkId,
|
|
1529
|
-
score
|
|
1750
|
+
score,
|
|
1530
1751
|
maxScore,
|
|
1531
1752
|
passingScore: props.passingScore ?? maxScore
|
|
1532
1753
|
});
|
|
@@ -1537,15 +1758,15 @@ function QuizInner(props, ref) {
|
|
|
1537
1758
|
c
|
|
1538
1759
|
] }, `${questionId}-${i}`))
|
|
1539
1760
|
] }),
|
|
1540
|
-
selected && selectionCorrect !== null ? /* @__PURE__ */ (0,
|
|
1761
|
+
selected && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
1541
1762
|
] });
|
|
1542
1763
|
}
|
|
1543
|
-
var QuizInnerForwarded = (0,
|
|
1544
|
-
var Quiz = (0,
|
|
1545
|
-
return /* @__PURE__ */ (0,
|
|
1764
|
+
var QuizInnerForwarded = (0, import_react12.forwardRef)(QuizInner);
|
|
1765
|
+
var Quiz = (0, import_react12.forwardRef)(function Quiz2(props, ref) {
|
|
1766
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1546
1767
|
});
|
|
1547
1768
|
function KnowledgeCheck(props) {
|
|
1548
|
-
return /* @__PURE__ */ (0,
|
|
1769
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1549
1770
|
Quiz,
|
|
1550
1771
|
{
|
|
1551
1772
|
checkId: props.checkId,
|
|
@@ -1561,27 +1782,27 @@ function resetQuizWarningsForTests() {
|
|
|
1561
1782
|
}
|
|
1562
1783
|
|
|
1563
1784
|
// src/components.tsx
|
|
1564
|
-
var
|
|
1785
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
1565
1786
|
function Course(props) {
|
|
1566
|
-
const courseId = (0,
|
|
1567
|
-
const providerConfig = (0,
|
|
1787
|
+
const courseId = (0, import_react13.useMemo)(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
|
|
1788
|
+
const providerConfig = (0, import_react13.useMemo)(
|
|
1568
1789
|
() => ({ ...props.config, courseId }),
|
|
1569
1790
|
[props.config, courseId]
|
|
1570
1791
|
);
|
|
1571
|
-
return /* @__PURE__ */ (0,
|
|
1572
|
-
/* @__PURE__ */ (0,
|
|
1573
|
-
/* @__PURE__ */ (0,
|
|
1792
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "aria-label": props.title, children: [
|
|
1793
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h1", { children: props.title }),
|
|
1794
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: props.children })
|
|
1574
1795
|
] }) });
|
|
1575
1796
|
}
|
|
1576
1797
|
function Lesson(props) {
|
|
1577
|
-
const lessonId = (0,
|
|
1798
|
+
const lessonId = (0, import_react13.useMemo)(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
1578
1799
|
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
1579
1800
|
const { setActiveLesson, config } = useLessonkit();
|
|
1580
1801
|
const { completeLesson } = useCompletion();
|
|
1581
|
-
const lessonMountGenerationRef = (0,
|
|
1582
|
-
const liveCourseIdRef = (0,
|
|
1802
|
+
const lessonMountGenerationRef = (0, import_react13.useRef)(0);
|
|
1803
|
+
const liveCourseIdRef = (0, import_react13.useRef)(config.courseId);
|
|
1583
1804
|
liveCourseIdRef.current = config.courseId;
|
|
1584
|
-
(0,
|
|
1805
|
+
(0, import_react13.useEffect)(() => {
|
|
1585
1806
|
const unregister = registerLessonMount(lessonId);
|
|
1586
1807
|
const generation = ++lessonMountGenerationRef.current;
|
|
1587
1808
|
const mountedCourseId = config.courseId;
|
|
@@ -1606,37 +1827,37 @@ function Lesson(props) {
|
|
|
1606
1827
|
});
|
|
1607
1828
|
};
|
|
1608
1829
|
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
1609
|
-
return /* @__PURE__ */ (0,
|
|
1610
|
-
/* @__PURE__ */ (0,
|
|
1611
|
-
/* @__PURE__ */ (0,
|
|
1830
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("article", { "aria-label": props.title, children: [
|
|
1831
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h2", { children: props.title }),
|
|
1832
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: props.children })
|
|
1612
1833
|
] }) });
|
|
1613
1834
|
}
|
|
1614
1835
|
function Scenario(props) {
|
|
1615
|
-
const blockId = (0,
|
|
1836
|
+
const blockId = (0, import_react13.useMemo)(
|
|
1616
1837
|
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1617
1838
|
[props.blockId]
|
|
1618
1839
|
);
|
|
1619
|
-
return /* @__PURE__ */ (0,
|
|
1840
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
1620
1841
|
}
|
|
1621
1842
|
function Reflection(props) {
|
|
1622
|
-
const blockId = (0,
|
|
1843
|
+
const blockId = (0, import_react13.useMemo)(
|
|
1623
1844
|
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1624
1845
|
[props.blockId]
|
|
1625
1846
|
);
|
|
1626
|
-
const promptId = (0,
|
|
1627
|
-
const hintId = (0,
|
|
1628
|
-
const [internalValue, setInternalValue] = (0,
|
|
1847
|
+
const promptId = (0, import_react13.useId)();
|
|
1848
|
+
const hintId = (0, import_react13.useId)();
|
|
1849
|
+
const [internalValue, setInternalValue] = (0, import_react13.useState)("");
|
|
1629
1850
|
const isControlled = props.value !== void 0;
|
|
1630
1851
|
const value = isControlled ? props.value : internalValue;
|
|
1631
1852
|
const handleChange = (event) => {
|
|
1632
1853
|
if (!isControlled) setInternalValue(event.target.value);
|
|
1633
1854
|
props.onChange?.(event.target.value);
|
|
1634
1855
|
};
|
|
1635
|
-
return /* @__PURE__ */ (0,
|
|
1636
|
-
props.prompt ? /* @__PURE__ */ (0,
|
|
1637
|
-
props.hint ? /* @__PURE__ */ (0,
|
|
1856
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
|
|
1857
|
+
props.prompt ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { id: promptId, children: props.prompt }) : null,
|
|
1858
|
+
props.hint ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { id: hintId, style: import_accessibility2.visuallyHiddenStyle, children: props.hint }) : null,
|
|
1638
1859
|
props.children,
|
|
1639
|
-
/* @__PURE__ */ (0,
|
|
1860
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1640
1861
|
"textarea",
|
|
1641
1862
|
{
|
|
1642
1863
|
value,
|
|
@@ -1654,7 +1875,7 @@ function ProgressTracker(props) {
|
|
|
1654
1875
|
if (props.totalLessons != null) {
|
|
1655
1876
|
const total = props.totalLessons;
|
|
1656
1877
|
const displayed = Math.min(completed, total);
|
|
1657
|
-
return /* @__PURE__ */ (0,
|
|
1878
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("aside", { "aria-label": "Progress", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1658
1879
|
"div",
|
|
1659
1880
|
{
|
|
1660
1881
|
role: "progressbar",
|
|
@@ -1662,7 +1883,7 @@ function ProgressTracker(props) {
|
|
|
1662
1883
|
"aria-valuemax": total,
|
|
1663
1884
|
"aria-valuenow": displayed,
|
|
1664
1885
|
"aria-label": "Lessons completed",
|
|
1665
|
-
children: /* @__PURE__ */ (0,
|
|
1886
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { children: [
|
|
1666
1887
|
"Lessons completed: ",
|
|
1667
1888
|
displayed,
|
|
1668
1889
|
" of ",
|
|
@@ -1671,58 +1892,101 @@ function ProgressTracker(props) {
|
|
|
1671
1892
|
}
|
|
1672
1893
|
) });
|
|
1673
1894
|
}
|
|
1674
|
-
return /* @__PURE__ */ (0,
|
|
1895
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { children: [
|
|
1675
1896
|
"Lessons completed: ",
|
|
1676
1897
|
completed
|
|
1677
1898
|
] }) });
|
|
1678
1899
|
}
|
|
1679
1900
|
|
|
1680
1901
|
// src/blocks/TrueFalse.tsx
|
|
1681
|
-
var
|
|
1682
|
-
var
|
|
1902
|
+
var import_react14 = __toESM(require("react"), 1);
|
|
1903
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1683
1904
|
var INTERACTION = "trueFalse";
|
|
1684
1905
|
function TrueFalseInner(props, ref) {
|
|
1685
1906
|
const { enclosingLessonId } = props;
|
|
1686
|
-
const checkId = (0,
|
|
1907
|
+
const checkId = (0, import_react14.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1687
1908
|
const assessment = useAssessmentState(enclosingLessonId);
|
|
1688
1909
|
const { config } = useLessonkit();
|
|
1689
1910
|
const { scoreResponse } = usePluginScoring(checkId, enclosingLessonId);
|
|
1690
|
-
const [selected, setSelected] = (0,
|
|
1691
|
-
const [selectionCorrect, setSelectionCorrect] = (0,
|
|
1692
|
-
const [showSolutions, setShowSolutions] = (0,
|
|
1693
|
-
const [passed, setPassed] = (0,
|
|
1694
|
-
const
|
|
1695
|
-
const
|
|
1911
|
+
const [selected, setSelected] = (0, import_react14.useState)(null);
|
|
1912
|
+
const [selectionCorrect, setSelectionCorrect] = (0, import_react14.useState)(null);
|
|
1913
|
+
const [showSolutions, setShowSolutions] = (0, import_react14.useState)(false);
|
|
1914
|
+
const [passed, setPassed] = (0, import_react14.useState)(false);
|
|
1915
|
+
const [completedScore, setCompletedScore] = (0, import_react14.useState)(null);
|
|
1916
|
+
const [completedMaxScore, setCompletedMaxScore] = (0, import_react14.useState)(null);
|
|
1917
|
+
const completedRef = (0, import_react14.useRef)(false);
|
|
1918
|
+
const telemetryReplayedRef = (0, import_react14.useRef)(false);
|
|
1919
|
+
const questionId = import_react14.default.useId();
|
|
1696
1920
|
const reset = () => {
|
|
1697
1921
|
completedRef.current = false;
|
|
1922
|
+
telemetryReplayedRef.current = false;
|
|
1698
1923
|
setPassed(false);
|
|
1699
1924
|
setSelected(null);
|
|
1700
1925
|
setSelectionCorrect(null);
|
|
1701
1926
|
setShowSolutions(false);
|
|
1927
|
+
setCompletedScore(null);
|
|
1928
|
+
setCompletedMaxScore(null);
|
|
1702
1929
|
};
|
|
1703
|
-
(0,
|
|
1930
|
+
(0, import_react14.useEffect)(() => {
|
|
1704
1931
|
reset();
|
|
1705
1932
|
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
|
|
1706
|
-
const
|
|
1933
|
+
const resolveScores = () => {
|
|
1934
|
+
const maxScore = completedMaxScore ?? 1;
|
|
1935
|
+
if (passed) {
|
|
1936
|
+
return { score: completedScore ?? maxScore, maxScore };
|
|
1937
|
+
}
|
|
1938
|
+
if (selectionCorrect) {
|
|
1939
|
+
return { score: completedMaxScore ?? maxScore, maxScore };
|
|
1940
|
+
}
|
|
1941
|
+
return { score: 0, maxScore };
|
|
1942
|
+
};
|
|
1943
|
+
const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
|
|
1944
|
+
if (!nextPassed || telemetryReplayedRef.current) return;
|
|
1945
|
+
telemetryReplayedRef.current = true;
|
|
1946
|
+
if (nextSelected !== null) {
|
|
1947
|
+
assessment.answer({
|
|
1948
|
+
checkId,
|
|
1949
|
+
interactionType: INTERACTION,
|
|
1950
|
+
question: props.question,
|
|
1951
|
+
response: nextSelected,
|
|
1952
|
+
correct: nextCorrect ?? false
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
assessment.complete({
|
|
1956
|
+
checkId,
|
|
1957
|
+
interactionType: INTERACTION,
|
|
1958
|
+
score: nextScore,
|
|
1959
|
+
maxScore: nextMaxScore,
|
|
1960
|
+
passingScore: props.passingScore ?? nextMaxScore
|
|
1961
|
+
});
|
|
1962
|
+
};
|
|
1963
|
+
const handle = (0, import_react14.useMemo)(
|
|
1707
1964
|
() => buildAssessmentHandle({
|
|
1708
1965
|
checkId,
|
|
1709
|
-
getScore: () =>
|
|
1710
|
-
|
|
1711
|
-
return passed ? maxScore : selected === null ? 0 : selected === props.answer ? maxScore : 0;
|
|
1712
|
-
},
|
|
1713
|
-
getMaxScore: () => 1,
|
|
1966
|
+
getScore: () => resolveScores().score,
|
|
1967
|
+
getMaxScore: () => resolveScores().maxScore,
|
|
1714
1968
|
getAnswerGiven: () => selected !== null,
|
|
1715
1969
|
resetTask: reset,
|
|
1716
1970
|
showSolutions: () => setShowSolutions(true),
|
|
1717
|
-
getXAPIData: () =>
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1971
|
+
getXAPIData: () => {
|
|
1972
|
+
const { score, maxScore } = resolveScores();
|
|
1973
|
+
return {
|
|
1974
|
+
checkId,
|
|
1975
|
+
interactionType: INTERACTION,
|
|
1976
|
+
response: selected ?? void 0,
|
|
1977
|
+
correct: selectionCorrect ?? void 0,
|
|
1978
|
+
score,
|
|
1979
|
+
maxScore
|
|
1980
|
+
};
|
|
1981
|
+
},
|
|
1982
|
+
getCurrentState: () => ({
|
|
1983
|
+
selected,
|
|
1984
|
+
selectionCorrect,
|
|
1985
|
+
passed,
|
|
1986
|
+
showSolutions,
|
|
1987
|
+
completedScore,
|
|
1988
|
+
completedMaxScore
|
|
1724
1989
|
}),
|
|
1725
|
-
getCurrentState: () => ({ selected, selectionCorrect, passed, showSolutions }),
|
|
1726
1990
|
resume: (state) => {
|
|
1727
1991
|
const nextSelected = readBooleanField(state, "selected");
|
|
1728
1992
|
if (nextSelected === true || nextSelected === false || nextSelected === null) {
|
|
@@ -1732,14 +1996,35 @@ function TrueFalseInner(props, ref) {
|
|
|
1732
1996
|
if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
|
|
1733
1997
|
setSelectionCorrect(nextCorrect);
|
|
1734
1998
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1999
|
+
const nextCompletedScore = readNumberField(state, "completedScore");
|
|
2000
|
+
if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
|
|
2001
|
+
const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
|
|
2002
|
+
if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
|
|
2003
|
+
const nextPassed = readBooleanField(state, "passed");
|
|
2004
|
+
if (nextPassed === true || nextPassed === false) {
|
|
2005
|
+
setPassed(nextPassed);
|
|
2006
|
+
completedRef.current = nextPassed;
|
|
2007
|
+
if (nextPassed) {
|
|
2008
|
+
const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
|
|
2009
|
+
const score = nextCompletedScore ?? completedScore ?? maxScore;
|
|
2010
|
+
replayTelemetry(nextSelected ?? null, nextCorrect ?? null, nextPassed, score, maxScore);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
1739
2013
|
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
1740
2014
|
}
|
|
1741
2015
|
}),
|
|
1742
|
-
[
|
|
2016
|
+
[
|
|
2017
|
+
assessment,
|
|
2018
|
+
checkId,
|
|
2019
|
+
completedMaxScore,
|
|
2020
|
+
completedScore,
|
|
2021
|
+
passed,
|
|
2022
|
+
props.passingScore,
|
|
2023
|
+
props.question,
|
|
2024
|
+
selected,
|
|
2025
|
+
selectionCorrect,
|
|
2026
|
+
showSolutions
|
|
2027
|
+
]
|
|
1743
2028
|
);
|
|
1744
2029
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1745
2030
|
const submit = (value) => {
|
|
@@ -1758,6 +2043,8 @@ function TrueFalseInner(props, ref) {
|
|
|
1758
2043
|
if (scored.passed && !completedRef.current) {
|
|
1759
2044
|
completedRef.current = true;
|
|
1760
2045
|
setPassed(true);
|
|
2046
|
+
setCompletedScore(scored.score);
|
|
2047
|
+
setCompletedMaxScore(scored.maxScore);
|
|
1761
2048
|
assessment.complete({
|
|
1762
2049
|
checkId,
|
|
1763
2050
|
interactionType: INTERACTION,
|
|
@@ -1768,12 +2055,12 @@ function TrueFalseInner(props, ref) {
|
|
|
1768
2055
|
}
|
|
1769
2056
|
};
|
|
1770
2057
|
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
1771
|
-
return /* @__PURE__ */ (0,
|
|
1772
|
-
/* @__PURE__ */ (0,
|
|
1773
|
-
/* @__PURE__ */ (0,
|
|
1774
|
-
/* @__PURE__ */ (0,
|
|
1775
|
-
/* @__PURE__ */ (0,
|
|
1776
|
-
/* @__PURE__ */ (0,
|
|
2058
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
|
|
2059
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { id: questionId, children: props.question }),
|
|
2060
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
|
|
2061
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("legend", { className: "lk-visually-hidden", children: "True or False" }),
|
|
2062
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { display: "block", marginRight: "1rem" }, children: [
|
|
2063
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1777
2064
|
"input",
|
|
1778
2065
|
{
|
|
1779
2066
|
type: "radio",
|
|
@@ -1785,8 +2072,8 @@ function TrueFalseInner(props, ref) {
|
|
|
1785
2072
|
),
|
|
1786
2073
|
"True"
|
|
1787
2074
|
] }),
|
|
1788
|
-
/* @__PURE__ */ (0,
|
|
1789
|
-
/* @__PURE__ */ (0,
|
|
2075
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { display: "block" }, children: [
|
|
2076
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1790
2077
|
"input",
|
|
1791
2078
|
{
|
|
1792
2079
|
type: "radio",
|
|
@@ -1799,49 +2086,49 @@ function TrueFalseInner(props, ref) {
|
|
|
1799
2086
|
"False"
|
|
1800
2087
|
] })
|
|
1801
2088
|
] }),
|
|
1802
|
-
reveal ? /* @__PURE__ */ (0,
|
|
2089
|
+
reveal ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("p", { children: [
|
|
1803
2090
|
"Correct answer: ",
|
|
1804
|
-
/* @__PURE__ */ (0,
|
|
2091
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("strong", { children: props.answer ? "True" : "False" })
|
|
1805
2092
|
] }) : null,
|
|
1806
|
-
selected !== null && selectionCorrect !== null ? /* @__PURE__ */ (0,
|
|
1807
|
-
props.enableRetry && passed ? /* @__PURE__ */ (0,
|
|
1808
|
-
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0,
|
|
2093
|
+
selected !== null && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
|
|
2094
|
+
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2095
|
+
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1809
2096
|
] });
|
|
1810
2097
|
}
|
|
1811
|
-
var TrueFalseInnerForwarded = (0,
|
|
1812
|
-
var TrueFalse = (0,
|
|
1813
|
-
return /* @__PURE__ */ (0,
|
|
2098
|
+
var TrueFalseInnerForwarded = (0, import_react14.forwardRef)(TrueFalseInner);
|
|
2099
|
+
var TrueFalse = (0, import_react14.forwardRef)(function TrueFalse2(props, ref) {
|
|
2100
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1814
2101
|
});
|
|
1815
2102
|
|
|
1816
2103
|
// src/blocks/MarkTheWords.tsx
|
|
1817
|
-
var
|
|
1818
|
-
var
|
|
2104
|
+
var import_react15 = __toESM(require("react"), 1);
|
|
2105
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
1819
2106
|
var INTERACTION2 = "markTheWords";
|
|
1820
2107
|
function tokenize(text) {
|
|
1821
2108
|
return text.split(/(\s+)/).filter((t) => t.length > 0);
|
|
1822
2109
|
}
|
|
1823
2110
|
function MarkTheWordsInner(props, ref) {
|
|
1824
|
-
const checkId = (0,
|
|
2111
|
+
const checkId = (0, import_react15.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1825
2112
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1826
|
-
const tokens = (0,
|
|
1827
|
-
const correctSet = (0,
|
|
2113
|
+
const tokens = (0, import_react15.useMemo)(() => tokenize(props.text), [props.text]);
|
|
2114
|
+
const correctSet = (0, import_react15.useMemo)(
|
|
1828
2115
|
() => new Set(props.correctWords.map((w) => w.toLowerCase())),
|
|
1829
2116
|
[props.correctWords]
|
|
1830
2117
|
);
|
|
1831
|
-
const [marked, setMarked] = (0,
|
|
1832
|
-
const [passed, setPassed] = (0,
|
|
1833
|
-
const [showSolutions, setShowSolutions] = (0,
|
|
1834
|
-
const completedRef = (0,
|
|
2118
|
+
const [marked, setMarked] = (0, import_react15.useState)(() => /* @__PURE__ */ new Set());
|
|
2119
|
+
const [passed, setPassed] = (0, import_react15.useState)(false);
|
|
2120
|
+
const [showSolutions, setShowSolutions] = (0, import_react15.useState)(false);
|
|
2121
|
+
const completedRef = (0, import_react15.useRef)(false);
|
|
1835
2122
|
const reset = () => {
|
|
1836
2123
|
completedRef.current = false;
|
|
1837
2124
|
setPassed(false);
|
|
1838
2125
|
setMarked(/* @__PURE__ */ new Set());
|
|
1839
2126
|
setShowSolutions(false);
|
|
1840
2127
|
};
|
|
1841
|
-
(0,
|
|
2128
|
+
(0, import_react15.useEffect)(() => {
|
|
1842
2129
|
reset();
|
|
1843
2130
|
}, [checkId, props.text, props.correctWords.join("\0")]);
|
|
1844
|
-
const selectableIndices = (0,
|
|
2131
|
+
const selectableIndices = (0, import_react15.useMemo)(() => {
|
|
1845
2132
|
const indices = [];
|
|
1846
2133
|
tokens.forEach((t, i) => {
|
|
1847
2134
|
if (!/^\s+$/.test(t) && correctSet.has(t.toLowerCase())) indices.push(i);
|
|
@@ -1853,7 +2140,7 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1853
2140
|
const maxScore = selectableIndices.length;
|
|
1854
2141
|
const score = allMarked ? maxScore : marked.size;
|
|
1855
2142
|
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
1856
|
-
const handle = (0,
|
|
2143
|
+
const handle = (0, import_react15.useMemo)(
|
|
1857
2144
|
() => buildAssessmentHandle({
|
|
1858
2145
|
checkId,
|
|
1859
2146
|
getScore: () => score,
|
|
@@ -1892,7 +2179,7 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1892
2179
|
return next;
|
|
1893
2180
|
});
|
|
1894
2181
|
};
|
|
1895
|
-
(0,
|
|
2182
|
+
(0, import_react15.useEffect)(() => {
|
|
1896
2183
|
if (!hasTargets) {
|
|
1897
2184
|
if (isDevEnvironment4()) {
|
|
1898
2185
|
console.warn(
|
|
@@ -1910,7 +2197,7 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1910
2197
|
interactionType: INTERACTION2,
|
|
1911
2198
|
question: props.text,
|
|
1912
2199
|
response: [...marked].map((i) => tokens[i]),
|
|
1913
|
-
correct:
|
|
2200
|
+
correct: passedThreshold
|
|
1914
2201
|
});
|
|
1915
2202
|
assessment.complete({
|
|
1916
2203
|
checkId,
|
|
@@ -1932,20 +2219,20 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1932
2219
|
score,
|
|
1933
2220
|
tokens
|
|
1934
2221
|
]);
|
|
1935
|
-
return /* @__PURE__ */ (0,
|
|
1936
|
-
!hasTargets ? /* @__PURE__ */ (0,
|
|
2222
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
|
|
2223
|
+
!hasTargets ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("p", { role: "alert", children: [
|
|
1937
2224
|
"No words in this sentence match ",
|
|
1938
|
-
/* @__PURE__ */ (0,
|
|
2225
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { children: "correctWords" }),
|
|
1939
2226
|
". Check spelling and capitalization in the source text."
|
|
1940
2227
|
] }) : null,
|
|
1941
|
-
/* @__PURE__ */ (0,
|
|
1942
|
-
/* @__PURE__ */ (0,
|
|
2228
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
|
|
2229
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
|
|
1943
2230
|
const isWord = !/^\s+$/.test(token);
|
|
1944
2231
|
const isTarget = isWord && correctSet.has(token.toLowerCase());
|
|
1945
|
-
if (!isTarget) return /* @__PURE__ */ (0,
|
|
2232
|
+
if (!isTarget) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react15.default.Fragment, { children: token }, i);
|
|
1946
2233
|
const selected = marked.has(i);
|
|
1947
2234
|
const solution = showSolutions || passed && props.enableSolutionsButton;
|
|
1948
|
-
return /* @__PURE__ */ (0,
|
|
2235
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
1949
2236
|
"button",
|
|
1950
2237
|
{
|
|
1951
2238
|
type: "button",
|
|
@@ -1963,18 +2250,18 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1963
2250
|
i
|
|
1964
2251
|
);
|
|
1965
2252
|
}) }),
|
|
1966
|
-
allMarked ? /* @__PURE__ */ (0,
|
|
1967
|
-
props.enableRetry && passed ? /* @__PURE__ */ (0,
|
|
1968
|
-
props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ (0,
|
|
2253
|
+
allMarked ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
|
|
2254
|
+
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2255
|
+
props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1969
2256
|
] });
|
|
1970
2257
|
}
|
|
1971
|
-
var MarkTheWordsInnerForwarded = (0,
|
|
1972
|
-
var MarkTheWords = (0,
|
|
1973
|
-
return /* @__PURE__ */ (0,
|
|
2258
|
+
var MarkTheWordsInnerForwarded = (0, import_react15.forwardRef)(MarkTheWordsInner);
|
|
2259
|
+
var MarkTheWords = (0, import_react15.forwardRef)(function MarkTheWords2(props, ref) {
|
|
2260
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1974
2261
|
});
|
|
1975
2262
|
|
|
1976
2263
|
// src/blocks/FillInTheBlanks.tsx
|
|
1977
|
-
var
|
|
2264
|
+
var import_react16 = __toESM(require("react"), 1);
|
|
1978
2265
|
|
|
1979
2266
|
// src/assessment/internal/parseStarDelimitedTemplate.ts
|
|
1980
2267
|
function parseStarDelimitedTemplate(template, idPrefix) {
|
|
@@ -1995,7 +2282,7 @@ function parseStarDelimitedTemplate(template, idPrefix) {
|
|
|
1995
2282
|
}
|
|
1996
2283
|
|
|
1997
2284
|
// src/blocks/FillInTheBlanks.tsx
|
|
1998
|
-
var
|
|
2285
|
+
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
1999
2286
|
var INTERACTION3 = "fillInBlanks";
|
|
2000
2287
|
function parseTemplate(template) {
|
|
2001
2288
|
const { parts, values } = parseStarDelimitedTemplate(template, "blank");
|
|
@@ -2005,25 +2292,31 @@ function parseTemplate(template) {
|
|
|
2005
2292
|
};
|
|
2006
2293
|
}
|
|
2007
2294
|
function FillInTheBlanksInner(props, ref) {
|
|
2008
|
-
const checkId = (0,
|
|
2295
|
+
const checkId = (0, import_react16.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
2009
2296
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2010
|
-
const parsed = (0,
|
|
2297
|
+
const parsed = (0, import_react16.useMemo)(() => parseTemplate(props.template), [props.template]);
|
|
2011
2298
|
const blanks = props.blanks ?? parsed.blanks;
|
|
2012
|
-
const [values, setValues] = (0,
|
|
2299
|
+
const [values, setValues] = (0, import_react16.useState)(
|
|
2013
2300
|
() => Object.fromEntries(blanks.map((b) => [b.id, ""]))
|
|
2014
2301
|
);
|
|
2015
|
-
const [passed, setPassed] = (0,
|
|
2016
|
-
const [showSolutions, setShowSolutions] = (0,
|
|
2017
|
-
const
|
|
2018
|
-
const
|
|
2302
|
+
const [passed, setPassed] = (0, import_react16.useState)(false);
|
|
2303
|
+
const [showSolutions, setShowSolutions] = (0, import_react16.useState)(false);
|
|
2304
|
+
const [submitted, setSubmitted] = (0, import_react16.useState)(false);
|
|
2305
|
+
const completedRef = (0, import_react16.useRef)(false);
|
|
2306
|
+
const answeredRef = (0, import_react16.useRef)(false);
|
|
2307
|
+
const checkSnapshotRef = (0, import_react16.useRef)(null);
|
|
2308
|
+
const telemetryReplayedRef = (0, import_react16.useRef)(false);
|
|
2019
2309
|
const reset = () => {
|
|
2020
2310
|
completedRef.current = false;
|
|
2021
2311
|
answeredRef.current = false;
|
|
2312
|
+
checkSnapshotRef.current = null;
|
|
2313
|
+
telemetryReplayedRef.current = false;
|
|
2022
2314
|
setPassed(false);
|
|
2023
2315
|
setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
|
|
2024
2316
|
setShowSolutions(false);
|
|
2317
|
+
setSubmitted(false);
|
|
2025
2318
|
};
|
|
2026
|
-
(0,
|
|
2319
|
+
(0, import_react16.useEffect)(() => {
|
|
2027
2320
|
reset();
|
|
2028
2321
|
}, [checkId, props.template, blanks.map((b) => b.answer).join("\0")]);
|
|
2029
2322
|
const hasBlanks = blanks.length > 0;
|
|
@@ -2034,7 +2327,32 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2034
2327
|
});
|
|
2035
2328
|
const maxScore = blanks.length;
|
|
2036
2329
|
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
2037
|
-
const
|
|
2330
|
+
const replayTelemetry = (nextValues, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
|
|
2331
|
+
if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
|
|
2332
|
+
telemetryReplayedRef.current = true;
|
|
2333
|
+
const nextPassedThreshold = meetsPassingThreshold(
|
|
2334
|
+
nextScore,
|
|
2335
|
+
nextMaxScore || 1,
|
|
2336
|
+
props.passingScore
|
|
2337
|
+
);
|
|
2338
|
+
assessment.answer({
|
|
2339
|
+
checkId,
|
|
2340
|
+
interactionType: INTERACTION3,
|
|
2341
|
+
question: props.template,
|
|
2342
|
+
response: nextValues,
|
|
2343
|
+
correct: nextPassedThreshold
|
|
2344
|
+
});
|
|
2345
|
+
if (nextPassed || nextPassedThreshold) {
|
|
2346
|
+
assessment.complete({
|
|
2347
|
+
checkId,
|
|
2348
|
+
interactionType: INTERACTION3,
|
|
2349
|
+
score: nextScore,
|
|
2350
|
+
maxScore: nextMaxScore,
|
|
2351
|
+
passingScore: props.passingScore ?? nextMaxScore
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
};
|
|
2355
|
+
const handle = (0, import_react16.useMemo)(
|
|
2038
2356
|
() => buildAssessmentHandle({
|
|
2039
2357
|
checkId,
|
|
2040
2358
|
getScore: () => score,
|
|
@@ -2050,19 +2368,36 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2050
2368
|
score,
|
|
2051
2369
|
maxScore: maxScore || 1
|
|
2052
2370
|
}),
|
|
2053
|
-
getCurrentState: () => ({ values, passed, showSolutions }),
|
|
2371
|
+
getCurrentState: () => ({ values, passed, showSolutions, submitted }),
|
|
2054
2372
|
resume: (state) => {
|
|
2055
2373
|
const raw = state.values;
|
|
2056
|
-
|
|
2374
|
+
let nextValues = values;
|
|
2375
|
+
if (raw && typeof raw === "object") {
|
|
2376
|
+
nextValues = { ...raw };
|
|
2377
|
+
setValues(nextValues);
|
|
2378
|
+
}
|
|
2379
|
+
let nextPassed = passed;
|
|
2380
|
+
let nextSubmitted = submitted;
|
|
2057
2381
|
readBooleanStateField(state, "passed", (value) => {
|
|
2382
|
+
nextPassed = value;
|
|
2058
2383
|
setPassed(value);
|
|
2059
2384
|
completedRef.current = value;
|
|
2060
2385
|
answeredRef.current = value;
|
|
2061
2386
|
});
|
|
2062
2387
|
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
2388
|
+
readBooleanStateField(state, "submitted", (value) => {
|
|
2389
|
+
nextSubmitted = value;
|
|
2390
|
+
setSubmitted(value);
|
|
2391
|
+
if (value) answeredRef.current = true;
|
|
2392
|
+
});
|
|
2393
|
+
let nextScore = 0;
|
|
2394
|
+
blanks.forEach((b) => {
|
|
2395
|
+
if ((nextValues[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) nextScore += 1;
|
|
2396
|
+
});
|
|
2397
|
+
replayTelemetry(nextValues, nextPassed, nextSubmitted, nextScore, blanks.length);
|
|
2063
2398
|
}
|
|
2064
2399
|
}),
|
|
2065
|
-
[allFilled, checkId, maxScore, passed, passedThreshold, score, showSolutions, values]
|
|
2400
|
+
[allFilled, assessment, blanks, checkId, maxScore, passed, passedThreshold, props.passingScore, props.template, score, showSolutions, submitted, values]
|
|
2066
2401
|
);
|
|
2067
2402
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2068
2403
|
const check = () => {
|
|
@@ -2073,16 +2408,19 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2073
2408
|
return;
|
|
2074
2409
|
}
|
|
2075
2410
|
if (!allFilled) return;
|
|
2076
|
-
if (
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2411
|
+
if (passed) return;
|
|
2412
|
+
const snapshot = JSON.stringify(values);
|
|
2413
|
+
if (checkSnapshotRef.current === snapshot) return;
|
|
2414
|
+
checkSnapshotRef.current = snapshot;
|
|
2415
|
+
answeredRef.current = true;
|
|
2416
|
+
setSubmitted(true);
|
|
2417
|
+
assessment.answer({
|
|
2418
|
+
checkId,
|
|
2419
|
+
interactionType: INTERACTION3,
|
|
2420
|
+
question: props.template,
|
|
2421
|
+
response: values,
|
|
2422
|
+
correct: passedThreshold
|
|
2423
|
+
});
|
|
2086
2424
|
if (passedThreshold && !completedRef.current) {
|
|
2087
2425
|
completedRef.current = true;
|
|
2088
2426
|
setPassed(true);
|
|
@@ -2095,20 +2433,24 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2095
2433
|
});
|
|
2096
2434
|
}
|
|
2097
2435
|
};
|
|
2098
|
-
(0,
|
|
2099
|
-
if (!allFilled)
|
|
2436
|
+
(0, import_react16.useEffect)(() => {
|
|
2437
|
+
if (!allFilled) {
|
|
2438
|
+
answeredRef.current = false;
|
|
2439
|
+
checkSnapshotRef.current = null;
|
|
2440
|
+
setSubmitted(false);
|
|
2441
|
+
}
|
|
2100
2442
|
}, [allFilled]);
|
|
2101
|
-
(0,
|
|
2102
|
-
if (props.autoCheck && allFilled) check();
|
|
2103
|
-
}, [allFilled, props.autoCheck, values, passedThreshold]);
|
|
2443
|
+
(0, import_react16.useEffect)(() => {
|
|
2444
|
+
if (props.autoCheck && allFilled && !passed) check();
|
|
2445
|
+
}, [allFilled, props.autoCheck, values, passedThreshold, passed]);
|
|
2104
2446
|
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
2105
|
-
return /* @__PURE__ */ (0,
|
|
2106
|
-
/* @__PURE__ */ (0,
|
|
2447
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
|
|
2448
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { children: parsed.parts.map((part, i) => {
|
|
2107
2449
|
const blank = blanks.find((b) => b.id === part);
|
|
2108
|
-
if (!blank) return /* @__PURE__ */ (0,
|
|
2109
|
-
return /* @__PURE__ */ (0,
|
|
2110
|
-
/* @__PURE__ */ (0,
|
|
2111
|
-
/* @__PURE__ */ (0,
|
|
2450
|
+
if (!blank) return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react16.default.Fragment, { children: part }, i);
|
|
2451
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { style: { margin: "0 0.25em" }, children: [
|
|
2452
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "lk-visually-hidden", children: blank.answer }),
|
|
2453
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
2112
2454
|
"input",
|
|
2113
2455
|
{
|
|
2114
2456
|
type: "text",
|
|
@@ -2124,49 +2466,55 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2124
2466
|
)
|
|
2125
2467
|
] }, blank.id);
|
|
2126
2468
|
}) }),
|
|
2127
|
-
!props.autoCheck ? /* @__PURE__ */ (0,
|
|
2128
|
-
!hasBlanks ? /* @__PURE__ */ (0,
|
|
2129
|
-
|
|
2130
|
-
props.enableRetry && passed ? /* @__PURE__ */ (0,
|
|
2131
|
-
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0,
|
|
2469
|
+
!props.autoCheck ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
|
|
2470
|
+
!hasBlanks ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
|
|
2471
|
+
submitted ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
|
|
2472
|
+
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2473
|
+
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
2132
2474
|
] });
|
|
2133
2475
|
}
|
|
2134
|
-
var FillInTheBlanksInnerForwarded = (0,
|
|
2135
|
-
var FillInTheBlanks = (0,
|
|
2476
|
+
var FillInTheBlanksInnerForwarded = (0, import_react16.forwardRef)(FillInTheBlanksInner);
|
|
2477
|
+
var FillInTheBlanks = (0, import_react16.forwardRef)(
|
|
2136
2478
|
function FillInTheBlanks2(props, ref) {
|
|
2137
|
-
return /* @__PURE__ */ (0,
|
|
2479
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2138
2480
|
}
|
|
2139
2481
|
);
|
|
2140
2482
|
|
|
2141
2483
|
// src/blocks/DragTheWords.tsx
|
|
2142
|
-
var
|
|
2143
|
-
var
|
|
2484
|
+
var import_react17 = __toESM(require("react"), 1);
|
|
2485
|
+
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
2144
2486
|
var INTERACTION4 = "dragTheWords";
|
|
2145
2487
|
function parseZones(template) {
|
|
2146
2488
|
const { parts, values } = parseStarDelimitedTemplate(template, "zone");
|
|
2147
2489
|
return { parts, answers: values };
|
|
2148
2490
|
}
|
|
2149
2491
|
function DragTheWordsInner(props, ref) {
|
|
2150
|
-
const checkId = (0,
|
|
2492
|
+
const checkId = (0, import_react17.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
2151
2493
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2152
|
-
const { parts, answers } = (0,
|
|
2153
|
-
const [zones, setZones] = (0,
|
|
2494
|
+
const { parts, answers } = (0, import_react17.useMemo)(() => parseZones(props.template), [props.template]);
|
|
2495
|
+
const [zones, setZones] = (0, import_react17.useState)(
|
|
2154
2496
|
() => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
|
|
2155
2497
|
);
|
|
2156
|
-
const [pool, setPool] = (0,
|
|
2157
|
-
const [keyboardWord, setKeyboardWord] = (0,
|
|
2158
|
-
const [passed, setPassed] = (0,
|
|
2159
|
-
const
|
|
2160
|
-
const
|
|
2498
|
+
const [pool, setPool] = (0, import_react17.useState)(() => [...props.words]);
|
|
2499
|
+
const [keyboardWord, setKeyboardWord] = (0, import_react17.useState)(null);
|
|
2500
|
+
const [passed, setPassed] = (0, import_react17.useState)(false);
|
|
2501
|
+
const [submitted, setSubmitted] = (0, import_react17.useState)(false);
|
|
2502
|
+
const completedRef = (0, import_react17.useRef)(false);
|
|
2503
|
+
const answeredRef = (0, import_react17.useRef)(false);
|
|
2504
|
+
const checkSnapshotRef = (0, import_react17.useRef)(null);
|
|
2505
|
+
const telemetryReplayedRef = (0, import_react17.useRef)(false);
|
|
2161
2506
|
const reset = () => {
|
|
2162
2507
|
completedRef.current = false;
|
|
2163
2508
|
answeredRef.current = false;
|
|
2509
|
+
checkSnapshotRef.current = null;
|
|
2510
|
+
telemetryReplayedRef.current = false;
|
|
2164
2511
|
setPassed(false);
|
|
2512
|
+
setSubmitted(false);
|
|
2165
2513
|
setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
|
|
2166
2514
|
setPool([...props.words]);
|
|
2167
2515
|
setKeyboardWord(null);
|
|
2168
2516
|
};
|
|
2169
|
-
(0,
|
|
2517
|
+
(0, import_react17.useEffect)(() => {
|
|
2170
2518
|
reset();
|
|
2171
2519
|
}, [checkId, props.template, props.words.join("\0")]);
|
|
2172
2520
|
const hasZones = answers.length > 0;
|
|
@@ -2177,14 +2525,39 @@ function DragTheWordsInner(props, ref) {
|
|
|
2177
2525
|
});
|
|
2178
2526
|
const maxScore = answers.length;
|
|
2179
2527
|
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
2180
|
-
const
|
|
2181
|
-
()
|
|
2528
|
+
const replayTelemetry = (nextZones, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
|
|
2529
|
+
if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
|
|
2530
|
+
telemetryReplayedRef.current = true;
|
|
2531
|
+
const nextPassedThreshold = meetsPassingThreshold(
|
|
2532
|
+
nextScore,
|
|
2533
|
+
nextMaxScore || 1,
|
|
2534
|
+
props.passingScore
|
|
2535
|
+
);
|
|
2536
|
+
assessment.answer({
|
|
2182
2537
|
checkId,
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2538
|
+
interactionType: INTERACTION4,
|
|
2539
|
+
question: props.template,
|
|
2540
|
+
response: nextZones,
|
|
2541
|
+
correct: nextPassedThreshold
|
|
2542
|
+
});
|
|
2543
|
+
if (nextPassed || nextPassedThreshold) {
|
|
2544
|
+
assessment.complete({
|
|
2545
|
+
checkId,
|
|
2546
|
+
interactionType: INTERACTION4,
|
|
2547
|
+
score: nextScore,
|
|
2548
|
+
maxScore: nextMaxScore,
|
|
2549
|
+
passingScore: props.passingScore ?? nextMaxScore
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2553
|
+
const handle = (0, import_react17.useMemo)(
|
|
2554
|
+
() => buildAssessmentHandle({
|
|
2555
|
+
checkId,
|
|
2556
|
+
getScore: () => score,
|
|
2557
|
+
getMaxScore: () => maxScore || 1,
|
|
2558
|
+
getAnswerGiven: () => allFilled,
|
|
2559
|
+
resetTask: reset,
|
|
2560
|
+
showSolutions: () => {
|
|
2188
2561
|
},
|
|
2189
2562
|
getXAPIData: () => ({
|
|
2190
2563
|
checkId,
|
|
@@ -2194,21 +2567,38 @@ function DragTheWordsInner(props, ref) {
|
|
|
2194
2567
|
score,
|
|
2195
2568
|
maxScore: maxScore || 1
|
|
2196
2569
|
}),
|
|
2197
|
-
getCurrentState: () => ({ zones, pool, passed, keyboardWord }),
|
|
2570
|
+
getCurrentState: () => ({ zones, pool, passed, keyboardWord, submitted }),
|
|
2198
2571
|
resume: (state) => {
|
|
2199
2572
|
const rawZones = state.zones;
|
|
2200
|
-
|
|
2573
|
+
let nextZones = zones;
|
|
2574
|
+
if (rawZones && typeof rawZones === "object") {
|
|
2575
|
+
nextZones = { ...rawZones };
|
|
2576
|
+
setZones(nextZones);
|
|
2577
|
+
}
|
|
2201
2578
|
if (Array.isArray(state.pool)) setPool([...state.pool]);
|
|
2579
|
+
let nextPassed = passed;
|
|
2580
|
+
let nextSubmitted = submitted;
|
|
2202
2581
|
readBooleanStateField(state, "passed", (value) => {
|
|
2582
|
+
nextPassed = value;
|
|
2203
2583
|
setPassed(value);
|
|
2204
2584
|
completedRef.current = value;
|
|
2205
2585
|
answeredRef.current = value;
|
|
2206
2586
|
});
|
|
2587
|
+
readBooleanStateField(state, "submitted", (value) => {
|
|
2588
|
+
nextSubmitted = value;
|
|
2589
|
+
setSubmitted(value);
|
|
2590
|
+
if (value) answeredRef.current = true;
|
|
2591
|
+
});
|
|
2207
2592
|
const kw = state.keyboardWord;
|
|
2208
2593
|
if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
|
|
2594
|
+
let nextScore = 0;
|
|
2595
|
+
answers.forEach((ans, i) => {
|
|
2596
|
+
if ((nextZones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) nextScore += 1;
|
|
2597
|
+
});
|
|
2598
|
+
replayTelemetry(nextZones, nextPassed, nextSubmitted, nextScore, answers.length);
|
|
2209
2599
|
}
|
|
2210
2600
|
}),
|
|
2211
|
-
[allFilled, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, score, zones]
|
|
2601
|
+
[allFilled, answers, assessment, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, props.passingScore, props.template, score, submitted, zones]
|
|
2212
2602
|
);
|
|
2213
2603
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2214
2604
|
const placeInZone = (zoneId, word) => {
|
|
@@ -2238,16 +2628,19 @@ function DragTheWordsInner(props, ref) {
|
|
|
2238
2628
|
return;
|
|
2239
2629
|
}
|
|
2240
2630
|
if (!allFilled) return;
|
|
2241
|
-
if (
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2631
|
+
if (passed) return;
|
|
2632
|
+
const snapshot = JSON.stringify(zones);
|
|
2633
|
+
if (checkSnapshotRef.current === snapshot) return;
|
|
2634
|
+
checkSnapshotRef.current = snapshot;
|
|
2635
|
+
answeredRef.current = true;
|
|
2636
|
+
setSubmitted(true);
|
|
2637
|
+
assessment.answer({
|
|
2638
|
+
checkId,
|
|
2639
|
+
interactionType: INTERACTION4,
|
|
2640
|
+
question: props.template,
|
|
2641
|
+
response: zones,
|
|
2642
|
+
correct: passedThreshold
|
|
2643
|
+
});
|
|
2251
2644
|
if (passedThreshold && !completedRef.current) {
|
|
2252
2645
|
completedRef.current = true;
|
|
2253
2646
|
setPassed(true);
|
|
@@ -2260,15 +2653,19 @@ function DragTheWordsInner(props, ref) {
|
|
|
2260
2653
|
});
|
|
2261
2654
|
}
|
|
2262
2655
|
};
|
|
2263
|
-
(0,
|
|
2264
|
-
if (!allFilled)
|
|
2656
|
+
(0, import_react17.useEffect)(() => {
|
|
2657
|
+
if (!allFilled) {
|
|
2658
|
+
answeredRef.current = false;
|
|
2659
|
+
checkSnapshotRef.current = null;
|
|
2660
|
+
setSubmitted(false);
|
|
2661
|
+
}
|
|
2265
2662
|
}, [allFilled]);
|
|
2266
|
-
(0,
|
|
2267
|
-
if (props.autoCheck && allFilled) check();
|
|
2268
|
-
}, [allFilled, props.autoCheck, zones, passedThreshold]);
|
|
2269
|
-
return /* @__PURE__ */ (0,
|
|
2270
|
-
/* @__PURE__ */ (0,
|
|
2271
|
-
/* @__PURE__ */ (0,
|
|
2663
|
+
(0, import_react17.useEffect)(() => {
|
|
2664
|
+
if (props.autoCheck && allFilled && !passed) check();
|
|
2665
|
+
}, [allFilled, props.autoCheck, zones, passedThreshold, passed]);
|
|
2666
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
|
|
2667
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
|
|
2668
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2272
2669
|
"button",
|
|
2273
2670
|
{
|
|
2274
2671
|
type: "button",
|
|
@@ -2282,9 +2679,9 @@ function DragTheWordsInner(props, ref) {
|
|
|
2282
2679
|
},
|
|
2283
2680
|
word
|
|
2284
2681
|
)) }),
|
|
2285
|
-
/* @__PURE__ */ (0,
|
|
2286
|
-
if (!part.startsWith("zone-")) return /* @__PURE__ */ (0,
|
|
2287
|
-
return /* @__PURE__ */ (0,
|
|
2682
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { children: parts.map((part, i) => {
|
|
2683
|
+
if (!part.startsWith("zone-")) return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react17.default.Fragment, { children: part }, i);
|
|
2684
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2288
2685
|
"span",
|
|
2289
2686
|
{
|
|
2290
2687
|
role: "button",
|
|
@@ -2308,53 +2705,56 @@ function DragTheWordsInner(props, ref) {
|
|
|
2308
2705
|
part
|
|
2309
2706
|
);
|
|
2310
2707
|
}) }),
|
|
2311
|
-
/* @__PURE__ */ (0,
|
|
2312
|
-
!hasZones ? /* @__PURE__ */ (0,
|
|
2313
|
-
|
|
2708
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
|
|
2709
|
+
!hasZones ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
|
|
2710
|
+
submitted ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
|
|
2314
2711
|
] });
|
|
2315
2712
|
}
|
|
2316
|
-
var DragTheWordsInnerForwarded = (0,
|
|
2317
|
-
var DragTheWords = (0,
|
|
2318
|
-
return /* @__PURE__ */ (0,
|
|
2713
|
+
var DragTheWordsInnerForwarded = (0, import_react17.forwardRef)(DragTheWordsInner);
|
|
2714
|
+
var DragTheWords = (0, import_react17.forwardRef)(function DragTheWords2(props, ref) {
|
|
2715
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2319
2716
|
});
|
|
2320
2717
|
|
|
2321
2718
|
// src/blocks/DragAndDrop.tsx
|
|
2322
|
-
var
|
|
2323
|
-
var
|
|
2719
|
+
var import_react18 = require("react");
|
|
2720
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
2324
2721
|
var INTERACTION5 = "dragAndDrop";
|
|
2325
2722
|
function DragAndDropInner(props, ref) {
|
|
2326
|
-
const checkId = (0,
|
|
2723
|
+
const checkId = (0, import_react18.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
2327
2724
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2328
|
-
const [assignments, setAssignments] = (0,
|
|
2725
|
+
const [assignments, setAssignments] = (0, import_react18.useState)(
|
|
2329
2726
|
() => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
|
|
2330
2727
|
);
|
|
2331
|
-
const [pool, setPool] = (0,
|
|
2332
|
-
const [keyboardItem, setKeyboardItem] = (0,
|
|
2333
|
-
const [passed, setPassed] = (0,
|
|
2334
|
-
const
|
|
2728
|
+
const [pool, setPool] = (0, import_react18.useState)(() => props.items.map((i) => i.id));
|
|
2729
|
+
const [keyboardItem, setKeyboardItem] = (0, import_react18.useState)(null);
|
|
2730
|
+
const [passed, setPassed] = (0, import_react18.useState)(false);
|
|
2731
|
+
const [checked, setChecked] = (0, import_react18.useState)(false);
|
|
2732
|
+
const completedRef = (0, import_react18.useRef)(false);
|
|
2335
2733
|
const reset = () => {
|
|
2336
2734
|
completedRef.current = false;
|
|
2337
2735
|
setPassed(false);
|
|
2736
|
+
setChecked(false);
|
|
2338
2737
|
setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
|
|
2339
2738
|
setPool(props.items.map((i) => i.id));
|
|
2340
2739
|
setKeyboardItem(null);
|
|
2341
2740
|
};
|
|
2342
|
-
(0,
|
|
2741
|
+
(0, import_react18.useEffect)(() => {
|
|
2343
2742
|
reset();
|
|
2344
2743
|
}, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
|
|
2345
|
-
const
|
|
2346
|
-
const
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2744
|
+
const hasTargets = props.targets.length > 0;
|
|
2745
|
+
const allFilled = hasTargets && props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
|
|
2746
|
+
let score = 0;
|
|
2747
|
+
props.targets.forEach((t) => {
|
|
2748
|
+
if (assignments[t.id] === t.accepts) score += 1;
|
|
2749
|
+
});
|
|
2750
|
+
const maxScore = props.targets.length || 1;
|
|
2751
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
2752
|
+
const handle = (0, import_react18.useMemo)(() => {
|
|
2353
2753
|
return buildAssessmentHandle({
|
|
2354
2754
|
checkId,
|
|
2355
2755
|
getScore: () => score,
|
|
2356
2756
|
getMaxScore: () => maxScore,
|
|
2357
|
-
getAnswerGiven: () => allFilled,
|
|
2757
|
+
getAnswerGiven: () => hasTargets && allFilled,
|
|
2358
2758
|
resetTask: reset,
|
|
2359
2759
|
showSolutions: () => {
|
|
2360
2760
|
},
|
|
@@ -2362,11 +2762,11 @@ function DragAndDropInner(props, ref) {
|
|
|
2362
2762
|
checkId,
|
|
2363
2763
|
interactionType: INTERACTION5,
|
|
2364
2764
|
response: assignments,
|
|
2365
|
-
correct:
|
|
2765
|
+
correct: passedThreshold,
|
|
2366
2766
|
score,
|
|
2367
2767
|
maxScore
|
|
2368
2768
|
}),
|
|
2369
|
-
getCurrentState: () => ({ assignments, pool, passed, keyboardItem }),
|
|
2769
|
+
getCurrentState: () => ({ assignments, pool, passed, checked, keyboardItem }),
|
|
2370
2770
|
resume: (state) => {
|
|
2371
2771
|
const rawAssignments = state.assignments;
|
|
2372
2772
|
if (rawAssignments && typeof rawAssignments === "object") {
|
|
@@ -2377,14 +2777,16 @@ function DragAndDropInner(props, ref) {
|
|
|
2377
2777
|
setPassed(value);
|
|
2378
2778
|
completedRef.current = value;
|
|
2379
2779
|
});
|
|
2780
|
+
readBooleanStateField(state, "checked", setChecked);
|
|
2380
2781
|
const item = state.keyboardItem;
|
|
2381
2782
|
if (item === null || typeof item === "string") setKeyboardItem(item ?? null);
|
|
2382
2783
|
}
|
|
2383
2784
|
});
|
|
2384
|
-
}, [
|
|
2785
|
+
}, [allFilled, assignments, checkId, checked, hasTargets, keyboardItem, maxScore, passed, passedThreshold, pool, props.targets, score]);
|
|
2385
2786
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2386
2787
|
const place = (targetId, itemId) => {
|
|
2387
2788
|
if (passed && !props.enableRetry) return;
|
|
2789
|
+
setChecked(false);
|
|
2388
2790
|
const prev = assignments[targetId];
|
|
2389
2791
|
setAssignments((a) => ({ ...a, [targetId]: itemId }));
|
|
2390
2792
|
setPool((p) => {
|
|
@@ -2396,29 +2798,31 @@ function DragAndDropInner(props, ref) {
|
|
|
2396
2798
|
};
|
|
2397
2799
|
const check = () => {
|
|
2398
2800
|
if (!allFilled) return;
|
|
2801
|
+
setChecked(true);
|
|
2399
2802
|
assessment.answer({
|
|
2400
2803
|
checkId,
|
|
2401
2804
|
interactionType: INTERACTION5,
|
|
2402
2805
|
response: assignments,
|
|
2403
|
-
correct:
|
|
2806
|
+
correct: passedThreshold
|
|
2404
2807
|
});
|
|
2405
|
-
if (
|
|
2808
|
+
if (passedThreshold && !completedRef.current) {
|
|
2406
2809
|
completedRef.current = true;
|
|
2407
2810
|
setPassed(true);
|
|
2408
2811
|
assessment.complete({
|
|
2409
2812
|
checkId,
|
|
2410
2813
|
interactionType: INTERACTION5,
|
|
2411
|
-
score
|
|
2412
|
-
maxScore
|
|
2413
|
-
passingScore: props.passingScore ??
|
|
2814
|
+
score,
|
|
2815
|
+
maxScore,
|
|
2816
|
+
passingScore: props.passingScore ?? maxScore
|
|
2414
2817
|
});
|
|
2415
2818
|
}
|
|
2416
2819
|
};
|
|
2417
|
-
return /* @__PURE__ */ (0,
|
|
2418
|
-
/* @__PURE__ */ (0,
|
|
2419
|
-
/* @__PURE__ */ (0,
|
|
2820
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
|
|
2821
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
|
|
2822
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { role: "list", "aria-label": "Draggable items", children: pool.flatMap((id) => {
|
|
2420
2823
|
const item = props.items.find((i) => i.id === id);
|
|
2421
|
-
|
|
2824
|
+
if (!item) return [];
|
|
2825
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2422
2826
|
"button",
|
|
2423
2827
|
{
|
|
2424
2828
|
type: "button",
|
|
@@ -2433,13 +2837,13 @@ function DragAndDropInner(props, ref) {
|
|
|
2433
2837
|
id
|
|
2434
2838
|
);
|
|
2435
2839
|
}) }),
|
|
2436
|
-
/* @__PURE__ */ (0,
|
|
2840
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { children: props.targets.map((target) => {
|
|
2437
2841
|
const assigned = assignments[target.id];
|
|
2438
2842
|
const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
|
|
2439
|
-
return /* @__PURE__ */ (0,
|
|
2440
|
-
/* @__PURE__ */ (0,
|
|
2843
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("li", { children: [
|
|
2844
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("strong", { children: target.label }),
|
|
2441
2845
|
" ",
|
|
2442
|
-
/* @__PURE__ */ (0,
|
|
2846
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2443
2847
|
"span",
|
|
2444
2848
|
{
|
|
2445
2849
|
role: "button",
|
|
@@ -2466,30 +2870,31 @@ function DragAndDropInner(props, ref) {
|
|
|
2466
2870
|
)
|
|
2467
2871
|
] }, target.id);
|
|
2468
2872
|
}) }),
|
|
2469
|
-
/* @__PURE__ */ (0,
|
|
2470
|
-
|
|
2873
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", "data-testid": "check-drag-drop", disabled: !hasTargets || !allFilled || passed, onClick: check, children: "Check" }),
|
|
2874
|
+
checked ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { role: "status", "aria-live": "polite", children: passedThreshold ? "Correct" : "Try again" }) : null
|
|
2471
2875
|
] });
|
|
2472
2876
|
}
|
|
2473
|
-
var DragAndDropInnerForwarded = (0,
|
|
2474
|
-
var DragAndDrop = (0,
|
|
2475
|
-
return /* @__PURE__ */ (0,
|
|
2877
|
+
var DragAndDropInnerForwarded = (0, import_react18.forwardRef)(DragAndDropInner);
|
|
2878
|
+
var DragAndDrop = (0, import_react18.forwardRef)(function DragAndDrop2(props, ref) {
|
|
2879
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2476
2880
|
});
|
|
2477
2881
|
|
|
2478
2882
|
// src/blocks/AssessmentSequence.tsx
|
|
2479
|
-
var
|
|
2883
|
+
var import_react24 = __toESM(require("react"), 1);
|
|
2884
|
+
var import_core17 = require("@lessonkit/core");
|
|
2480
2885
|
|
|
2481
2886
|
// src/compound/useCompoundShell.ts
|
|
2482
|
-
var
|
|
2483
|
-
var
|
|
2887
|
+
var import_react22 = require("react");
|
|
2888
|
+
var import_core15 = require("@lessonkit/core");
|
|
2484
2889
|
|
|
2485
2890
|
// src/compound/useCompoundNavigation.ts
|
|
2486
|
-
var
|
|
2891
|
+
var import_react19 = require("react");
|
|
2487
2892
|
function useCompoundNavigation(pageCount, index, setIndex) {
|
|
2488
|
-
const goNext = (0,
|
|
2893
|
+
const goNext = (0, import_react19.useCallback)(() => {
|
|
2489
2894
|
if (pageCount < 1) return;
|
|
2490
2895
|
setIndex((i) => Math.min(i + 1, pageCount - 1));
|
|
2491
2896
|
}, [pageCount, setIndex]);
|
|
2492
|
-
const goPrev = (0,
|
|
2897
|
+
const goPrev = (0, import_react19.useCallback)(() => {
|
|
2493
2898
|
setIndex((i) => Math.max(i - 1, 0));
|
|
2494
2899
|
}, [setIndex]);
|
|
2495
2900
|
const clampedIndex = pageCount < 1 ? 0 : Math.min(index, pageCount - 1);
|
|
@@ -2503,102 +2908,269 @@ function useCompoundNavigation(pageCount, index, setIndex) {
|
|
|
2503
2908
|
}
|
|
2504
2909
|
|
|
2505
2910
|
// src/compound/useCompoundPersistence.ts
|
|
2506
|
-
var
|
|
2507
|
-
var
|
|
2911
|
+
var import_react21 = require("react");
|
|
2912
|
+
var import_core14 = require("@lessonkit/core");
|
|
2913
|
+
|
|
2914
|
+
// src/compound/resumeChildHandles.ts
|
|
2915
|
+
function filterRegisteredChildStates(handles, childStates) {
|
|
2916
|
+
const filtered = {};
|
|
2917
|
+
for (const [key, value] of Object.entries(childStates)) {
|
|
2918
|
+
if (handles.has(key)) {
|
|
2919
|
+
filtered[key] = value;
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
return filtered;
|
|
2923
|
+
}
|
|
2924
|
+
function resumeChildHandles(handles, childStates, opts) {
|
|
2925
|
+
const pendingKeys = Object.keys(childStates);
|
|
2926
|
+
const alreadyResumed = opts?.alreadyResumed;
|
|
2927
|
+
if (opts?.waitForHandles && pendingKeys.length > 0) {
|
|
2928
|
+
if (handles.size === 0) return false;
|
|
2929
|
+
const registeredPending = pendingKeys.filter((k) => handles.has(k));
|
|
2930
|
+
if (registeredPending.length === 0) {
|
|
2931
|
+
return false;
|
|
2932
|
+
}
|
|
2933
|
+
if (registeredPending.length < pendingKeys.length) {
|
|
2934
|
+
for (const key of registeredPending) {
|
|
2935
|
+
if (alreadyResumed?.has(key)) continue;
|
|
2936
|
+
const handle = handles.get(key);
|
|
2937
|
+
const child = childStates[key];
|
|
2938
|
+
if (handle?.resume && child) {
|
|
2939
|
+
handle.resume(child);
|
|
2940
|
+
alreadyResumed?.add(key);
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
return false;
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
for (const [checkId, handle] of handles) {
|
|
2947
|
+
if (alreadyResumed?.has(checkId)) continue;
|
|
2948
|
+
const child = childStates[checkId];
|
|
2949
|
+
if (child && handle.resume) {
|
|
2950
|
+
handle.resume(child);
|
|
2951
|
+
alreadyResumed?.add(checkId);
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
return true;
|
|
2955
|
+
}
|
|
2508
2956
|
|
|
2509
2957
|
// src/compound/useCompoundResume.ts
|
|
2510
|
-
var
|
|
2511
|
-
var import_core11 = require("@lessonkit/core");
|
|
2958
|
+
var import_react20 = require("react");
|
|
2512
2959
|
var import_core12 = require("@lessonkit/core");
|
|
2960
|
+
var import_core13 = require("@lessonkit/core");
|
|
2961
|
+
var warnedCompoundPersistFailure = false;
|
|
2962
|
+
function warnCompoundPersistFailure() {
|
|
2963
|
+
if (warnedCompoundPersistFailure || !isDevEnvironment4()) return;
|
|
2964
|
+
warnedCompoundPersistFailure = true;
|
|
2965
|
+
console.warn(
|
|
2966
|
+
"[lessonkit] compound resume state could not be saved to sessionStorage (quota or privacy mode); progress may be lost on reload."
|
|
2967
|
+
);
|
|
2968
|
+
}
|
|
2513
2969
|
function useCompoundResume(opts) {
|
|
2514
|
-
const
|
|
2515
|
-
const
|
|
2516
|
-
(0,
|
|
2970
|
+
const lessonkitCtx = (0, import_react20.useContext)(LessonkitContext);
|
|
2971
|
+
const storageRef = (0, import_react20.useRef)(opts.storage ?? lessonkitCtx?.storage ?? (0, import_core13.createSessionStoragePort)());
|
|
2972
|
+
const resumedRef = (0, import_react20.useRef)(false);
|
|
2973
|
+
const resumeKeyRef = (0, import_react20.useRef)("");
|
|
2974
|
+
const prevEnabledRef = (0, import_react20.useRef)(opts.enabled);
|
|
2975
|
+
(0, import_react20.useEffect)(() => {
|
|
2976
|
+
storageRef.current = opts.storage ?? lessonkitCtx?.storage ?? (0, import_core13.createSessionStoragePort)();
|
|
2977
|
+
}, [opts.storage, lessonkitCtx?.storage]);
|
|
2978
|
+
(0, import_react20.useEffect)(() => {
|
|
2979
|
+
if (!prevEnabledRef.current && opts.enabled) {
|
|
2980
|
+
resumedRef.current = false;
|
|
2981
|
+
}
|
|
2982
|
+
prevEnabledRef.current = opts.enabled;
|
|
2983
|
+
const key = `${opts.courseId ?? ""}:${opts.compoundId}`;
|
|
2984
|
+
if (resumeKeyRef.current !== key) {
|
|
2985
|
+
resumeKeyRef.current = key;
|
|
2986
|
+
resumedRef.current = false;
|
|
2987
|
+
}
|
|
2517
2988
|
if (!opts.enabled || !opts.courseId || resumedRef.current) return;
|
|
2518
|
-
const saved = (0,
|
|
2989
|
+
const saved = (0, import_core12.loadCompoundState)(storageRef.current, opts.courseId, opts.compoundId);
|
|
2519
2990
|
if (saved) {
|
|
2520
2991
|
resumedRef.current = true;
|
|
2521
2992
|
opts.onResume?.(saved);
|
|
2522
2993
|
}
|
|
2523
2994
|
}, [opts.enabled, opts.courseId, opts.compoundId, opts.onResume]);
|
|
2524
|
-
return (0,
|
|
2995
|
+
return (0, import_react20.useCallback)(
|
|
2525
2996
|
(state) => {
|
|
2526
2997
|
if (!opts.enabled || !opts.courseId) return;
|
|
2527
|
-
(0,
|
|
2998
|
+
const persisted = (0, import_core12.saveCompoundState)(storageRef.current, opts.courseId, opts.compoundId, state);
|
|
2999
|
+
if (!persisted) warnCompoundPersistFailure();
|
|
2528
3000
|
},
|
|
2529
3001
|
[opts.enabled, opts.courseId, opts.compoundId]
|
|
2530
3002
|
);
|
|
2531
3003
|
}
|
|
2532
3004
|
|
|
2533
3005
|
// src/compound/useCompoundPersistence.ts
|
|
2534
|
-
function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, storage = (0,
|
|
3006
|
+
function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, storage = (0, import_core14.createSessionStoragePort)()) {
|
|
2535
3007
|
if (!enabled || !courseId || pageCount < 1) return 0;
|
|
2536
|
-
const saved = (0,
|
|
3008
|
+
const saved = (0, import_core14.loadCompoundState)(storage, courseId, compoundId);
|
|
2537
3009
|
if (!saved) return 0;
|
|
2538
|
-
return (0,
|
|
3010
|
+
return (0, import_core14.clampCompoundPageIndex)(saved.activePageIndex, pageCount);
|
|
3011
|
+
}
|
|
3012
|
+
function stripOrphanChildStates(handles, childStates) {
|
|
3013
|
+
return filterRegisteredChildStates(handles, childStates);
|
|
2539
3014
|
}
|
|
2540
3015
|
function useCompoundPersistence(opts) {
|
|
2541
|
-
const
|
|
3016
|
+
const lessonkitCtx = (0, import_react21.useContext)(LessonkitContext);
|
|
3017
|
+
const storage = opts.storage ?? lessonkitCtx?.storage ?? (0, import_core14.createSessionStoragePort)();
|
|
2542
3018
|
const ctx = useCompoundRegistry();
|
|
2543
3019
|
const handlesVersion = useCompoundHandlesVersion();
|
|
2544
|
-
const
|
|
2545
|
-
const
|
|
2546
|
-
const
|
|
2547
|
-
const
|
|
3020
|
+
const bridgeRef = useCompoundHydrationBridgeRef();
|
|
3021
|
+
const pendingChildResumeRef = (0, import_react21.useRef)(null);
|
|
3022
|
+
const resumedChildKeysRef = (0, import_react21.useRef)(/* @__PURE__ */ new Set());
|
|
3023
|
+
const loadedChildStatesRef = (0, import_react21.useRef)({});
|
|
3024
|
+
const skipSaveUntilHydratedRef = (0, import_react21.useRef)(false);
|
|
3025
|
+
const hydrationKeyRef = (0, import_react21.useRef)("");
|
|
3026
|
+
const hydrationInitRef = (0, import_react21.useRef)(false);
|
|
3027
|
+
const hydrationKey = `${opts.courseId ?? ""}:${opts.compoundId}`;
|
|
3028
|
+
if (hydrationKeyRef.current !== hydrationKey) {
|
|
3029
|
+
hydrationKeyRef.current = hydrationKey;
|
|
3030
|
+
hydrationInitRef.current = false;
|
|
3031
|
+
loadedChildStatesRef.current = {};
|
|
3032
|
+
skipSaveUntilHydratedRef.current = false;
|
|
3033
|
+
pendingChildResumeRef.current = null;
|
|
3034
|
+
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
3035
|
+
}
|
|
3036
|
+
if (!hydrationInitRef.current && opts.enabled && opts.courseId) {
|
|
3037
|
+
hydrationInitRef.current = true;
|
|
3038
|
+
const saved = (0, import_core14.loadCompoundState)(storage, opts.courseId, opts.compoundId);
|
|
3039
|
+
if (saved && Object.keys(saved.childStates).length > 0) {
|
|
3040
|
+
loadedChildStatesRef.current = { ...saved.childStates };
|
|
3041
|
+
skipSaveUntilHydratedRef.current = true;
|
|
3042
|
+
pendingChildResumeRef.current = saved;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
const buildState = (0, import_react21.useCallback)(() => {
|
|
2548
3046
|
const childStates = {
|
|
2549
3047
|
...loadedChildStatesRef.current
|
|
2550
3048
|
};
|
|
2551
3049
|
if (ctx) {
|
|
2552
|
-
for (const [checkId,
|
|
3050
|
+
for (const [checkId, entry] of ctx.getRegisteredHandles()) {
|
|
3051
|
+
const handle = entry.handle;
|
|
2553
3052
|
if (handle.getCurrentState) {
|
|
2554
3053
|
childStates[checkId] = handle.getCurrentState();
|
|
2555
3054
|
delete loadedChildStatesRef.current[checkId];
|
|
2556
3055
|
}
|
|
2557
3056
|
}
|
|
2558
3057
|
}
|
|
2559
|
-
return (0,
|
|
2560
|
-
activePageIndex: (0,
|
|
3058
|
+
return (0, import_core14.createCompoundResumeState)({
|
|
3059
|
+
activePageIndex: (0, import_core14.clampCompoundPageIndex)(opts.index, opts.pageCount),
|
|
2561
3060
|
childStates
|
|
2562
3061
|
});
|
|
2563
3062
|
}, [ctx, opts.index, opts.pageCount]);
|
|
2564
|
-
const
|
|
3063
|
+
const buildStateRef = (0, import_react21.useRef)(buildState);
|
|
3064
|
+
buildStateRef.current = buildState;
|
|
3065
|
+
const persistNowRef = (0, import_react21.useRef)(() => {
|
|
3066
|
+
});
|
|
3067
|
+
const finalizeHydration = (0, import_react21.useCallback)(
|
|
3068
|
+
(childStates) => {
|
|
3069
|
+
loadedChildStatesRef.current = {
|
|
3070
|
+
...loadedChildStatesRef.current,
|
|
3071
|
+
...childStates
|
|
3072
|
+
};
|
|
3073
|
+
skipSaveUntilHydratedRef.current = false;
|
|
3074
|
+
pendingChildResumeRef.current = null;
|
|
3075
|
+
queueMicrotask(() => persistNowRef.current());
|
|
3076
|
+
},
|
|
3077
|
+
[]
|
|
3078
|
+
);
|
|
3079
|
+
const applyPendingChildResume = (0, import_react21.useCallback)(() => {
|
|
2565
3080
|
const pending = pendingChildResumeRef.current;
|
|
2566
3081
|
if (!pending || !ctx) return;
|
|
2567
|
-
const
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
3082
|
+
const handles = ctx.getHandles();
|
|
3083
|
+
const applied = resumeChildHandles(handles, pending.childStates, {
|
|
3084
|
+
waitForHandles: true,
|
|
3085
|
+
alreadyResumed: resumedChildKeysRef.current
|
|
3086
|
+
});
|
|
3087
|
+
if (!applied) {
|
|
3088
|
+
if (handles.size === 0) {
|
|
3089
|
+
const registeredOnly2 = stripOrphanChildStates(handles, pending.childStates);
|
|
3090
|
+
resumeChildHandles(handles, registeredOnly2, {
|
|
3091
|
+
alreadyResumed: resumedChildKeysRef.current
|
|
3092
|
+
});
|
|
3093
|
+
finalizeHydration(registeredOnly2);
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
3096
|
+
const handlesAtWait = handles.size;
|
|
3097
|
+
queueMicrotask(() => {
|
|
3098
|
+
if (pendingChildResumeRef.current !== pending) return;
|
|
3099
|
+
const handlesNow = ctx.getHandles();
|
|
3100
|
+
if (handlesNow.size !== handlesAtWait) return;
|
|
3101
|
+
const registeredOnly2 = stripOrphanChildStates(handlesNow, pending.childStates);
|
|
3102
|
+
resumeChildHandles(handlesNow, registeredOnly2, {
|
|
3103
|
+
alreadyResumed: resumedChildKeysRef.current
|
|
3104
|
+
});
|
|
3105
|
+
finalizeHydration(registeredOnly2);
|
|
3106
|
+
});
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
const registeredOnly = stripOrphanChildStates(handles, pending.childStates);
|
|
3110
|
+
finalizeHydration(registeredOnly);
|
|
3111
|
+
}, [ctx, finalizeHydration]);
|
|
2572
3112
|
const saveResume = useCompoundResume({
|
|
2573
3113
|
courseId: opts.courseId,
|
|
2574
3114
|
compoundId: opts.compoundId,
|
|
2575
3115
|
enabled: opts.enabled,
|
|
2576
3116
|
storage,
|
|
2577
3117
|
onResume: (state) => {
|
|
2578
|
-
const clamped = (0,
|
|
3118
|
+
const clamped = (0, import_core14.clampCompoundPageIndex)(state.activePageIndex, opts.pageCount);
|
|
2579
3119
|
loadedChildStatesRef.current = { ...state.childStates };
|
|
2580
3120
|
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
2581
3121
|
opts.setIndex(clamped);
|
|
2582
|
-
|
|
3122
|
+
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
3123
|
+
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
2583
3124
|
queueMicrotask(() => applyPendingChildResume());
|
|
2584
3125
|
}
|
|
2585
3126
|
});
|
|
2586
|
-
(0,
|
|
3127
|
+
const persistNow = (0, import_react21.useCallback)(() => {
|
|
2587
3128
|
if (!opts.enabled || !opts.courseId) return;
|
|
2588
3129
|
if (skipSaveUntilHydratedRef.current) return;
|
|
2589
|
-
saveResume(
|
|
2590
|
-
}, [
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
3130
|
+
saveResume(buildStateRef.current());
|
|
3131
|
+
}, [opts.enabled, opts.courseId, saveResume]);
|
|
3132
|
+
(0, import_react21.useEffect)(() => {
|
|
3133
|
+
persistNowRef.current = persistNow;
|
|
3134
|
+
}, [persistNow]);
|
|
3135
|
+
const notifyImperativeResume = (0, import_react21.useCallback)(
|
|
3136
|
+
(state) => {
|
|
3137
|
+
const clamped = (0, import_core14.clampCompoundPageIndex)(state.activePageIndex, opts.pageCount);
|
|
3138
|
+
loadedChildStatesRef.current = { ...state.childStates };
|
|
3139
|
+
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
3140
|
+
opts.setIndex(clamped);
|
|
3141
|
+
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
3142
|
+
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
3143
|
+
queueMicrotask(() => applyPendingChildResume());
|
|
3144
|
+
},
|
|
3145
|
+
[opts.pageCount, opts.setIndex, applyPendingChildResume]
|
|
3146
|
+
);
|
|
3147
|
+
(0, import_react21.useEffect)(() => {
|
|
3148
|
+
if (!bridgeRef) return;
|
|
3149
|
+
bridgeRef.current = { notifyImperativeResume };
|
|
3150
|
+
return () => {
|
|
3151
|
+
if (bridgeRef.current?.notifyImperativeResume === notifyImperativeResume) {
|
|
3152
|
+
bridgeRef.current = null;
|
|
3153
|
+
}
|
|
3154
|
+
};
|
|
3155
|
+
}, [bridgeRef, notifyImperativeResume]);
|
|
3156
|
+
(0, import_react21.useEffect)(() => {
|
|
2600
3157
|
applyPendingChildResume();
|
|
2601
3158
|
}, [opts.index, handlesVersion, applyPendingChildResume]);
|
|
3159
|
+
(0, import_react21.useEffect)(() => {
|
|
3160
|
+
persistNow();
|
|
3161
|
+
}, [persistNow, opts.index, opts.pageCount, handlesVersion]);
|
|
3162
|
+
(0, import_react21.useEffect)(() => {
|
|
3163
|
+
if (!opts.enabled || !opts.courseId || typeof document === "undefined") return;
|
|
3164
|
+
const flushOnExit = () => {
|
|
3165
|
+
if (document.visibilityState === "hidden") persistNow();
|
|
3166
|
+
};
|
|
3167
|
+
document.addEventListener("visibilitychange", flushOnExit);
|
|
3168
|
+
window.addEventListener("pagehide", flushOnExit);
|
|
3169
|
+
return () => {
|
|
3170
|
+
document.removeEventListener("visibilitychange", flushOnExit);
|
|
3171
|
+
window.removeEventListener("pagehide", flushOnExit);
|
|
3172
|
+
};
|
|
3173
|
+
}, [opts.enabled, opts.courseId, persistNow]);
|
|
2602
3174
|
}
|
|
2603
3175
|
|
|
2604
3176
|
// src/compound/useCompoundShell.ts
|
|
@@ -2614,18 +3186,19 @@ function useCompoundShell(opts) {
|
|
|
2614
3186
|
storage: opts.storage
|
|
2615
3187
|
});
|
|
2616
3188
|
const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
|
|
2617
|
-
const visibleIndex = (0,
|
|
3189
|
+
const visibleIndex = (0, import_core15.clampCompoundPageIndex)(opts.index, opts.pageCount);
|
|
2618
3190
|
useCompoundHandleRef(opts.ref, {
|
|
2619
3191
|
activePageIndex: visibleIndex,
|
|
2620
3192
|
setActivePageIndex: opts.setIndex,
|
|
2621
3193
|
getHandles: () => ctx?.getHandles() ?? /* @__PURE__ */ new Map(),
|
|
3194
|
+
getRegisteredHandles: () => ctx?.getRegisteredHandles() ?? /* @__PURE__ */ new Map(),
|
|
2622
3195
|
pageCount: opts.pageCount,
|
|
2623
3196
|
enableSolutionsButton: opts.enableSolutionsButton
|
|
2624
3197
|
});
|
|
2625
3198
|
return { visibleIndex, goNext, goPrev, progress, ctx };
|
|
2626
3199
|
}
|
|
2627
3200
|
function useCompoundInitialIndex(opts) {
|
|
2628
|
-
return (0,
|
|
3201
|
+
return (0, import_react22.useMemo)(
|
|
2629
3202
|
() => readCompoundInitialIndex(
|
|
2630
3203
|
opts.courseId,
|
|
2631
3204
|
opts.compoundId,
|
|
@@ -2638,8 +3211,8 @@ function useCompoundInitialIndex(opts) {
|
|
|
2638
3211
|
}
|
|
2639
3212
|
|
|
2640
3213
|
// src/compound/validateChildren.ts
|
|
2641
|
-
var
|
|
2642
|
-
var
|
|
3214
|
+
var import_react23 = __toESM(require("react"), 1);
|
|
3215
|
+
var import_core16 = require("@lessonkit/core");
|
|
2643
3216
|
|
|
2644
3217
|
// src/compound/blockType.ts
|
|
2645
3218
|
var LESSONKIT_BLOCK_TYPE = /* @__PURE__ */ Symbol.for("lessonkit.blockType");
|
|
@@ -2663,6 +3236,8 @@ var warnedPairs = /* @__PURE__ */ new Set();
|
|
|
2663
3236
|
var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
2664
3237
|
"Page",
|
|
2665
3238
|
"InteractiveBook",
|
|
3239
|
+
"Slide",
|
|
3240
|
+
"SlideDeck",
|
|
2666
3241
|
"AssessmentSequence"
|
|
2667
3242
|
]);
|
|
2668
3243
|
function warnOrThrow(msg, strict) {
|
|
@@ -2673,8 +3248,8 @@ function warnOrThrow(msg, strict) {
|
|
|
2673
3248
|
}
|
|
2674
3249
|
}
|
|
2675
3250
|
function validateNode(parent, node, depth, strict) {
|
|
2676
|
-
|
|
2677
|
-
if (!
|
|
3251
|
+
import_react23.default.Children.forEach(node, (child) => {
|
|
3252
|
+
if (!import_react23.default.isValidElement(child)) return;
|
|
2678
3253
|
const blockType = getLessonkitBlockType(child.type);
|
|
2679
3254
|
if (!blockType) {
|
|
2680
3255
|
if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
@@ -2682,7 +3257,7 @@ function validateNode(parent, node, depth, strict) {
|
|
|
2682
3257
|
}
|
|
2683
3258
|
return;
|
|
2684
3259
|
}
|
|
2685
|
-
if (!(0,
|
|
3260
|
+
if (!(0, import_core16.isChildTypeAllowed)(parent, blockType)) {
|
|
2686
3261
|
const key = `${parent}:${blockType}`;
|
|
2687
3262
|
if (!warnedPairs.has(key)) {
|
|
2688
3263
|
warnedPairs.add(key);
|
|
@@ -2692,7 +3267,7 @@ function validateNode(parent, node, depth, strict) {
|
|
|
2692
3267
|
}
|
|
2693
3268
|
}
|
|
2694
3269
|
if (COMPOUND_CONTAINER_TYPES.has(blockType)) {
|
|
2695
|
-
const maxDepth =
|
|
3270
|
+
const maxDepth = import_core16.COMPOUND_MAX_NESTING_DEPTH[parent];
|
|
2696
3271
|
if (depth >= maxDepth) {
|
|
2697
3272
|
warnOrThrow(
|
|
2698
3273
|
`[lessonkit] Block "${blockType}" exceeds max nesting depth (${maxDepth}) for "${parent}"`,
|
|
@@ -2707,15 +3282,15 @@ function validateNode(parent, node, depth, strict) {
|
|
|
2707
3282
|
} else if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
2708
3283
|
validateSubtreeForForbidden(
|
|
2709
3284
|
child.props.children,
|
|
2710
|
-
|
|
3285
|
+
import_core16.ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
2711
3286
|
strict
|
|
2712
3287
|
);
|
|
2713
3288
|
}
|
|
2714
3289
|
});
|
|
2715
3290
|
}
|
|
2716
3291
|
function validateSubtreeForForbidden(node, forbidden, strict) {
|
|
2717
|
-
|
|
2718
|
-
if (!
|
|
3292
|
+
import_react23.default.Children.forEach(node, (child) => {
|
|
3293
|
+
if (!import_react23.default.isValidElement(child)) return;
|
|
2719
3294
|
const blockType = getLessonkitBlockType(child.type);
|
|
2720
3295
|
if (blockType && forbidden.includes(blockType)) {
|
|
2721
3296
|
warnOrThrow(`[lessonkit] Block "${blockType}" must not nest inside Accordion`, strict);
|
|
@@ -2737,7 +3312,7 @@ function validateSubtreeForForbidden(node, forbidden, strict) {
|
|
|
2737
3312
|
function validateAccordionSections(sections, strict) {
|
|
2738
3313
|
if (!isDevEnvironment4() && !strict) return;
|
|
2739
3314
|
for (const section of sections) {
|
|
2740
|
-
validateSubtreeForForbidden(section.content,
|
|
3315
|
+
validateSubtreeForForbidden(section.content, import_core16.ACCORDION_FORBIDDEN_CHILD_TYPES, strict);
|
|
2741
3316
|
}
|
|
2742
3317
|
}
|
|
2743
3318
|
function validateCompoundChildren(parent, children, strict) {
|
|
@@ -2755,8 +3330,8 @@ function warnSharedCompoundStorageKey(opts) {
|
|
|
2755
3330
|
}
|
|
2756
3331
|
|
|
2757
3332
|
// src/blocks/AssessmentSequence.tsx
|
|
2758
|
-
var
|
|
2759
|
-
var AssessmentSequenceInner = (0,
|
|
3333
|
+
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
3334
|
+
var AssessmentSequenceInner = (0, import_react24.forwardRef)(
|
|
2760
3335
|
function AssessmentSequenceInner2(props, ref) {
|
|
2761
3336
|
const { compoundId, childArray, index, setIndex, persistEnabled } = props;
|
|
2762
3337
|
const sequential = props.sequential !== false;
|
|
@@ -2773,18 +3348,18 @@ var AssessmentSequenceInner = (0, import_react22.forwardRef)(
|
|
|
2773
3348
|
});
|
|
2774
3349
|
validateCompoundChildren("AssessmentSequence", props.children);
|
|
2775
3350
|
if (!sequential) {
|
|
2776
|
-
return /* @__PURE__ */ (0,
|
|
3351
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
|
|
2777
3352
|
}
|
|
2778
|
-
return /* @__PURE__ */ (0,
|
|
2779
|
-
/* @__PURE__ */ (0,
|
|
3353
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
|
|
3354
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("p", { children: [
|
|
2780
3355
|
"Question ",
|
|
2781
3356
|
progress.current,
|
|
2782
3357
|
" of ",
|
|
2783
3358
|
progress.total
|
|
2784
3359
|
] }),
|
|
2785
|
-
/* @__PURE__ */ (0,
|
|
2786
|
-
/* @__PURE__ */ (0,
|
|
2787
|
-
/* @__PURE__ */ (0,
|
|
3360
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { "data-testid": "assessment-sequence-step", children: childArray.map((child, i) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { hidden: i !== visibleIndex, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CompoundPageIndexProvider, { pageIndex: i, children: child }) }, child.key ?? i)) }),
|
|
3361
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("nav", { "aria-label": "Sequence navigation", children: [
|
|
3362
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2788
3363
|
"button",
|
|
2789
3364
|
{
|
|
2790
3365
|
type: "button",
|
|
@@ -2794,7 +3369,7 @@ var AssessmentSequenceInner = (0, import_react22.forwardRef)(
|
|
|
2794
3369
|
children: "Previous"
|
|
2795
3370
|
}
|
|
2796
3371
|
),
|
|
2797
|
-
/* @__PURE__ */ (0,
|
|
3372
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2798
3373
|
"button",
|
|
2799
3374
|
{
|
|
2800
3375
|
type: "button",
|
|
@@ -2808,18 +3383,23 @@ var AssessmentSequenceInner = (0, import_react22.forwardRef)(
|
|
|
2808
3383
|
] });
|
|
2809
3384
|
}
|
|
2810
3385
|
);
|
|
2811
|
-
var AssessmentSequence = (0,
|
|
3386
|
+
var AssessmentSequence = (0, import_react24.forwardRef)(
|
|
2812
3387
|
function AssessmentSequence2(props, ref) {
|
|
2813
|
-
const
|
|
2814
|
-
|
|
3388
|
+
const reactInstanceId = (0, import_react24.useId)();
|
|
3389
|
+
const autoCompoundIdRef = (0, import_react24.useRef)(null);
|
|
3390
|
+
if (!props.blockId && !autoCompoundIdRef.current) {
|
|
3391
|
+
autoCompoundIdRef.current = (0, import_core17.deriveId)(`assessment-sequence-${reactInstanceId}`);
|
|
3392
|
+
}
|
|
3393
|
+
const compoundId = (0, import_react24.useMemo)(
|
|
3394
|
+
() => props.blockId ? normalizeComponentId(props.blockId, "blockId") : autoCompoundIdRef.current ?? DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
|
|
2815
3395
|
[props.blockId]
|
|
2816
3396
|
);
|
|
2817
|
-
const childArray =
|
|
2818
|
-
|
|
3397
|
+
const childArray = import_react24.default.Children.toArray(props.children).filter(
|
|
3398
|
+
import_react24.default.isValidElement
|
|
2819
3399
|
);
|
|
2820
|
-
const { config } = useLessonkit();
|
|
3400
|
+
const { config, storage } = useLessonkit();
|
|
2821
3401
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
2822
|
-
(0,
|
|
3402
|
+
(0, import_react24.useEffect)(() => {
|
|
2823
3403
|
warnSharedCompoundStorageKey({
|
|
2824
3404
|
persistEnabled,
|
|
2825
3405
|
hasExplicitBlockId: Boolean(props.blockId),
|
|
@@ -2830,11 +3410,15 @@ var AssessmentSequence = (0, import_react22.forwardRef)(
|
|
|
2830
3410
|
courseId: config.courseId,
|
|
2831
3411
|
compoundId,
|
|
2832
3412
|
pageCount: childArray.length,
|
|
2833
|
-
persistEnabled
|
|
3413
|
+
persistEnabled,
|
|
3414
|
+
storage
|
|
2834
3415
|
});
|
|
2835
|
-
const [index, setIndex] = (0,
|
|
2836
|
-
const setIndexStable = (0,
|
|
2837
|
-
|
|
3416
|
+
const [index, setIndex] = (0, import_react24.useState)(initialIndex);
|
|
3417
|
+
const setIndexStable = (0, import_react24.useCallback)((i) => setIndex(i), []);
|
|
3418
|
+
(0, import_react24.useEffect)(() => {
|
|
3419
|
+
setIndex(initialIndex);
|
|
3420
|
+
}, [config.courseId, compoundId, initialIndex]);
|
|
3421
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2838
3422
|
AssessmentSequenceInner,
|
|
2839
3423
|
{
|
|
2840
3424
|
...props,
|
|
@@ -2851,25 +3435,25 @@ var AssessmentSequence = (0, import_react22.forwardRef)(
|
|
|
2851
3435
|
setLessonkitBlockType(AssessmentSequence, "AssessmentSequence");
|
|
2852
3436
|
|
|
2853
3437
|
// src/blocks/Text.tsx
|
|
2854
|
-
var
|
|
2855
|
-
var
|
|
3438
|
+
var import_react25 = require("react");
|
|
3439
|
+
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
2856
3440
|
function Text(props) {
|
|
2857
|
-
return /* @__PURE__ */ (0,
|
|
3441
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
|
|
2858
3442
|
}
|
|
2859
3443
|
setLessonkitBlockType(Text, "Text");
|
|
2860
3444
|
|
|
2861
3445
|
// src/blocks/Heading.tsx
|
|
2862
|
-
var
|
|
3446
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
2863
3447
|
function Heading(props) {
|
|
2864
3448
|
const Tag = `h${props.level}`;
|
|
2865
|
-
return /* @__PURE__ */ (0,
|
|
3449
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
|
|
2866
3450
|
}
|
|
2867
3451
|
setLessonkitBlockType(Heading, "Heading");
|
|
2868
3452
|
|
|
2869
3453
|
// src/blocks/Image.tsx
|
|
2870
|
-
var
|
|
3454
|
+
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
2871
3455
|
function Image(props) {
|
|
2872
|
-
return /* @__PURE__ */ (0,
|
|
3456
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
2873
3457
|
"img",
|
|
2874
3458
|
{
|
|
2875
3459
|
src: props.src,
|
|
@@ -2883,14 +3467,14 @@ function Image(props) {
|
|
|
2883
3467
|
setLessonkitBlockType(Image, "Image");
|
|
2884
3468
|
|
|
2885
3469
|
// src/blocks/Page.tsx
|
|
2886
|
-
var
|
|
2887
|
-
var
|
|
3470
|
+
var import_react26 = require("react");
|
|
3471
|
+
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
2888
3472
|
function Page(props) {
|
|
2889
3473
|
validateCompoundChildren("Page", props.children);
|
|
2890
3474
|
const { track } = useLessonkit();
|
|
2891
3475
|
const lessonId = useEnclosingLessonId();
|
|
2892
|
-
(0,
|
|
2893
|
-
if (props.hidden || !lessonId) return;
|
|
3476
|
+
(0, import_react26.useEffect)(() => {
|
|
3477
|
+
if (props.hidden || !lessonId || props.parentType) return;
|
|
2894
3478
|
track(
|
|
2895
3479
|
"compound_page_viewed",
|
|
2896
3480
|
{
|
|
@@ -2901,7 +3485,7 @@ function Page(props) {
|
|
|
2901
3485
|
{ lessonId }
|
|
2902
3486
|
);
|
|
2903
3487
|
}, [props.hidden, props.pageIndex, props.parentType, props.blockId, lessonId, track]);
|
|
2904
|
-
return /* @__PURE__ */ (0,
|
|
3488
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
|
|
2905
3489
|
"section",
|
|
2906
3490
|
{
|
|
2907
3491
|
"aria-label": props.title ?? "Page",
|
|
@@ -2909,8 +3493,8 @@ function Page(props) {
|
|
|
2909
3493
|
"data-testid": `page-${props.blockId}`,
|
|
2910
3494
|
hidden: props.hidden ? true : void 0,
|
|
2911
3495
|
children: [
|
|
2912
|
-
props.title ? /* @__PURE__ */ (0,
|
|
2913
|
-
/* @__PURE__ */ (0,
|
|
3496
|
+
props.title ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h3", { children: props.title }) : null,
|
|
3497
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CompoundPageIndexProvider, { pageIndex: props.pageIndex ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { children: props.children }) })
|
|
2914
3498
|
]
|
|
2915
3499
|
}
|
|
2916
3500
|
);
|
|
@@ -2918,9 +3502,9 @@ function Page(props) {
|
|
|
2918
3502
|
setLessonkitBlockType(Page, "Page");
|
|
2919
3503
|
|
|
2920
3504
|
// src/blocks/InteractiveBook.tsx
|
|
2921
|
-
var
|
|
2922
|
-
var
|
|
2923
|
-
var InteractiveBookInner = (0,
|
|
3505
|
+
var import_react27 = __toESM(require("react"), 1);
|
|
3506
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
3507
|
+
var InteractiveBookInner = (0, import_react27.forwardRef)(
|
|
2924
3508
|
function InteractiveBookInner2(props, ref) {
|
|
2925
3509
|
const { blockId, pages, index, setIndex, persistEnabled } = props;
|
|
2926
3510
|
validateCompoundChildren("InteractiveBook", pages);
|
|
@@ -2935,11 +3519,11 @@ var InteractiveBookInner = (0, import_react25.forwardRef)(
|
|
|
2935
3519
|
persistEnabled,
|
|
2936
3520
|
ref
|
|
2937
3521
|
});
|
|
2938
|
-
const pageTitles = (0,
|
|
3522
|
+
const pageTitles = (0, import_react27.useMemo)(
|
|
2939
3523
|
() => pages.map((page) => page.props.title),
|
|
2940
3524
|
[pages]
|
|
2941
3525
|
);
|
|
2942
|
-
(0,
|
|
3526
|
+
(0, import_react27.useEffect)(() => {
|
|
2943
3527
|
if (!lessonId || pages.length === 0) return;
|
|
2944
3528
|
track(
|
|
2945
3529
|
"book_page_viewed",
|
|
@@ -2951,31 +3535,31 @@ var InteractiveBookInner = (0, import_react25.forwardRef)(
|
|
|
2951
3535
|
{ lessonId }
|
|
2952
3536
|
);
|
|
2953
3537
|
}, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
|
|
2954
|
-
return /* @__PURE__ */ (0,
|
|
2955
|
-
/* @__PURE__ */ (0,
|
|
2956
|
-
/* @__PURE__ */ (0,
|
|
3538
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
|
|
3539
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { children: props.title }),
|
|
3540
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { children: [
|
|
2957
3541
|
"Page ",
|
|
2958
3542
|
progress.current,
|
|
2959
3543
|
" of ",
|
|
2960
3544
|
progress.total
|
|
2961
3545
|
] }),
|
|
2962
|
-
props.showBookScore && ctx ? /* @__PURE__ */ (0,
|
|
3546
|
+
props.showBookScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { "data-testid": "book-score", children: [
|
|
2963
3547
|
"Score: ",
|
|
2964
3548
|
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
2965
3549
|
" /",
|
|
2966
3550
|
" ",
|
|
2967
3551
|
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
2968
3552
|
] }) : null,
|
|
2969
|
-
/* @__PURE__ */ (0,
|
|
2970
|
-
(page, i) =>
|
|
3553
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { "data-testid": "interactive-book-page", children: pages.map(
|
|
3554
|
+
(page, i) => import_react27.default.cloneElement(page, {
|
|
2971
3555
|
key: page.key ?? page.props.blockId,
|
|
2972
3556
|
hidden: i !== visibleIndex,
|
|
2973
3557
|
pageIndex: i,
|
|
2974
3558
|
parentType: "InteractiveBook"
|
|
2975
3559
|
})
|
|
2976
3560
|
) }),
|
|
2977
|
-
/* @__PURE__ */ (0,
|
|
2978
|
-
/* @__PURE__ */ (0,
|
|
3561
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("nav", { "aria-label": "Book navigation", children: [
|
|
3562
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
2979
3563
|
"button",
|
|
2980
3564
|
{
|
|
2981
3565
|
type: "button",
|
|
@@ -2985,7 +3569,7 @@ var InteractiveBookInner = (0, import_react25.forwardRef)(
|
|
|
2985
3569
|
children: "Previous"
|
|
2986
3570
|
}
|
|
2987
3571
|
),
|
|
2988
|
-
/* @__PURE__ */ (0,
|
|
3572
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
2989
3573
|
"button",
|
|
2990
3574
|
{
|
|
2991
3575
|
type: "button",
|
|
@@ -2999,25 +3583,29 @@ var InteractiveBookInner = (0, import_react25.forwardRef)(
|
|
|
2999
3583
|
] });
|
|
3000
3584
|
}
|
|
3001
3585
|
);
|
|
3002
|
-
var InteractiveBook = (0,
|
|
3003
|
-
const blockId = (0,
|
|
3586
|
+
var InteractiveBook = (0, import_react27.forwardRef)(function InteractiveBook2(props, ref) {
|
|
3587
|
+
const blockId = (0, import_react27.useMemo)(
|
|
3004
3588
|
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3005
3589
|
[props.blockId]
|
|
3006
3590
|
);
|
|
3007
|
-
const pages =
|
|
3008
|
-
|
|
3591
|
+
const pages = import_react27.default.Children.toArray(props.children).filter(
|
|
3592
|
+
import_react27.default.isValidElement
|
|
3009
3593
|
);
|
|
3010
|
-
const { config } = useLessonkit();
|
|
3594
|
+
const { config, storage } = useLessonkit();
|
|
3011
3595
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
3012
3596
|
const initialIndex = useCompoundInitialIndex({
|
|
3013
3597
|
courseId: config.courseId,
|
|
3014
3598
|
compoundId: blockId,
|
|
3015
3599
|
pageCount: pages.length,
|
|
3016
|
-
persistEnabled
|
|
3600
|
+
persistEnabled,
|
|
3601
|
+
storage
|
|
3017
3602
|
});
|
|
3018
|
-
const [index, setIndex] = (0,
|
|
3019
|
-
const setIndexStable = (0,
|
|
3020
|
-
|
|
3603
|
+
const [index, setIndex] = (0, import_react27.useState)(initialIndex);
|
|
3604
|
+
const setIndexStable = (0, import_react27.useCallback)((i) => setIndex(i), []);
|
|
3605
|
+
(0, import_react27.useEffect)(() => {
|
|
3606
|
+
setIndex(initialIndex);
|
|
3607
|
+
}, [config.courseId, blockId, initialIndex]);
|
|
3608
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3021
3609
|
InteractiveBookInner,
|
|
3022
3610
|
{
|
|
3023
3611
|
...props,
|
|
@@ -3032,17 +3620,249 @@ var InteractiveBook = (0, import_react25.forwardRef)(function InteractiveBook2(p
|
|
|
3032
3620
|
});
|
|
3033
3621
|
setLessonkitBlockType(InteractiveBook, "InteractiveBook");
|
|
3034
3622
|
|
|
3623
|
+
// src/blocks/Slide.tsx
|
|
3624
|
+
var import_react28 = require("react");
|
|
3625
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
3626
|
+
function Slide(props) {
|
|
3627
|
+
validateCompoundChildren("Slide", props.children);
|
|
3628
|
+
const { track } = useLessonkit();
|
|
3629
|
+
const lessonId = useEnclosingLessonId();
|
|
3630
|
+
(0, import_react28.useEffect)(() => {
|
|
3631
|
+
if (props.hidden || !lessonId || props.parentType) return;
|
|
3632
|
+
track(
|
|
3633
|
+
"compound_page_viewed",
|
|
3634
|
+
{
|
|
3635
|
+
blockId: props.blockId,
|
|
3636
|
+
pageIndex: props.slideIndex ?? 0,
|
|
3637
|
+
parentType: props.parentType
|
|
3638
|
+
},
|
|
3639
|
+
{ lessonId }
|
|
3640
|
+
);
|
|
3641
|
+
}, [props.hidden, props.slideIndex, props.parentType, props.blockId, lessonId, track]);
|
|
3642
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
3643
|
+
"section",
|
|
3644
|
+
{
|
|
3645
|
+
"aria-label": props.title ?? "Slide",
|
|
3646
|
+
"data-lk-block-id": props.blockId,
|
|
3647
|
+
"data-testid": `slide-${props.blockId}`,
|
|
3648
|
+
hidden: props.hidden ? true : void 0,
|
|
3649
|
+
children: [
|
|
3650
|
+
props.title ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h3", { children: props.title }) : null,
|
|
3651
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(CompoundPageIndexProvider, { pageIndex: props.slideIndex ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { children: props.children }) })
|
|
3652
|
+
]
|
|
3653
|
+
}
|
|
3654
|
+
);
|
|
3655
|
+
}
|
|
3656
|
+
setLessonkitBlockType(Slide, "Slide");
|
|
3657
|
+
|
|
3658
|
+
// src/blocks/SlideDeck.tsx
|
|
3659
|
+
var import_react30 = __toESM(require("react"), 1);
|
|
3660
|
+
|
|
3661
|
+
// src/compound/useCompoundKeyboardNav.ts
|
|
3662
|
+
var import_react29 = require("react");
|
|
3663
|
+
var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT", "BUTTON"]);
|
|
3664
|
+
function isEditableTarget(target) {
|
|
3665
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
3666
|
+
if (INTERACTIVE_TAGS.has(target.tagName)) return true;
|
|
3667
|
+
if (target.isContentEditable) return true;
|
|
3668
|
+
if (target.closest("[role='slider'], [role='listbox'], [data-lk-assessment-interactive]")) {
|
|
3669
|
+
return true;
|
|
3670
|
+
}
|
|
3671
|
+
return false;
|
|
3672
|
+
}
|
|
3673
|
+
function useCompoundKeyboardNav(opts) {
|
|
3674
|
+
const { containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex } = opts;
|
|
3675
|
+
(0, import_react29.useEffect)(() => {
|
|
3676
|
+
const el = containerRef.current;
|
|
3677
|
+
if (!el || pageCount === 0) return;
|
|
3678
|
+
const onKeyDown = (event) => {
|
|
3679
|
+
if (!el.contains(document.activeElement) && document.activeElement !== document.body) {
|
|
3680
|
+
return;
|
|
3681
|
+
}
|
|
3682
|
+
if (isEditableTarget(event.target)) return;
|
|
3683
|
+
switch (event.key) {
|
|
3684
|
+
case "ArrowRight":
|
|
3685
|
+
case "ArrowDown":
|
|
3686
|
+
if (visibleIndex < pageCount - 1) {
|
|
3687
|
+
event.preventDefault();
|
|
3688
|
+
goNext();
|
|
3689
|
+
}
|
|
3690
|
+
break;
|
|
3691
|
+
case "ArrowLeft":
|
|
3692
|
+
case "ArrowUp":
|
|
3693
|
+
if (visibleIndex > 0) {
|
|
3694
|
+
event.preventDefault();
|
|
3695
|
+
goPrev();
|
|
3696
|
+
}
|
|
3697
|
+
break;
|
|
3698
|
+
case "Home":
|
|
3699
|
+
if (visibleIndex !== 0) {
|
|
3700
|
+
event.preventDefault();
|
|
3701
|
+
setIndex(0);
|
|
3702
|
+
}
|
|
3703
|
+
break;
|
|
3704
|
+
case "End":
|
|
3705
|
+
if (visibleIndex !== pageCount - 1) {
|
|
3706
|
+
event.preventDefault();
|
|
3707
|
+
setIndex(pageCount - 1);
|
|
3708
|
+
}
|
|
3709
|
+
break;
|
|
3710
|
+
default:
|
|
3711
|
+
break;
|
|
3712
|
+
}
|
|
3713
|
+
};
|
|
3714
|
+
el.addEventListener("keydown", onKeyDown);
|
|
3715
|
+
return () => el.removeEventListener("keydown", onKeyDown);
|
|
3716
|
+
}, [containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex]);
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
// src/blocks/SlideDeck.tsx
|
|
3720
|
+
var import_jsx_runtime20 = require("react/jsx-runtime");
|
|
3721
|
+
var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(props, ref) {
|
|
3722
|
+
const { blockId, slides, index, setIndex, persistEnabled } = props;
|
|
3723
|
+
validateCompoundChildren("SlideDeck", slides);
|
|
3724
|
+
const { config, track } = useLessonkit();
|
|
3725
|
+
const lessonId = useEnclosingLessonId();
|
|
3726
|
+
const containerRef = (0, import_react30.useRef)(null);
|
|
3727
|
+
const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
|
|
3728
|
+
courseId: config.courseId,
|
|
3729
|
+
compoundId: blockId,
|
|
3730
|
+
pageCount: slides.length,
|
|
3731
|
+
index,
|
|
3732
|
+
setIndex,
|
|
3733
|
+
persistEnabled,
|
|
3734
|
+
ref
|
|
3735
|
+
});
|
|
3736
|
+
const setIndexStable = (0, import_react30.useCallback)((i) => setIndex(i), [setIndex]);
|
|
3737
|
+
useCompoundKeyboardNav({
|
|
3738
|
+
containerRef,
|
|
3739
|
+
visibleIndex,
|
|
3740
|
+
pageCount: slides.length,
|
|
3741
|
+
goNext,
|
|
3742
|
+
goPrev,
|
|
3743
|
+
setIndex: setIndexStable
|
|
3744
|
+
});
|
|
3745
|
+
const slideTitles = (0, import_react30.useMemo)(
|
|
3746
|
+
() => slides.map((slide) => slide.props.title),
|
|
3747
|
+
[slides]
|
|
3748
|
+
);
|
|
3749
|
+
(0, import_react30.useEffect)(() => {
|
|
3750
|
+
if (!lessonId || slides.length === 0) return;
|
|
3751
|
+
track(
|
|
3752
|
+
"slide_viewed",
|
|
3753
|
+
{
|
|
3754
|
+
blockId,
|
|
3755
|
+
slideIndex: visibleIndex,
|
|
3756
|
+
slideTitle: slideTitles[visibleIndex]
|
|
3757
|
+
},
|
|
3758
|
+
{ lessonId }
|
|
3759
|
+
);
|
|
3760
|
+
}, [visibleIndex, blockId, lessonId, slides.length, slideTitles, track]);
|
|
3761
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
|
|
3762
|
+
"section",
|
|
3763
|
+
{
|
|
3764
|
+
ref: containerRef,
|
|
3765
|
+
tabIndex: -1,
|
|
3766
|
+
"aria-label": props.title,
|
|
3767
|
+
"data-testid": "slide-deck",
|
|
3768
|
+
"data-lk-block-id": blockId,
|
|
3769
|
+
children: [
|
|
3770
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h3", { children: props.title }),
|
|
3771
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("p", { children: [
|
|
3772
|
+
"Slide ",
|
|
3773
|
+
progress.current,
|
|
3774
|
+
" of ",
|
|
3775
|
+
progress.total
|
|
3776
|
+
] }),
|
|
3777
|
+
props.showDeckScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("p", { "data-testid": "deck-score", children: [
|
|
3778
|
+
"Score: ",
|
|
3779
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
3780
|
+
" /",
|
|
3781
|
+
" ",
|
|
3782
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
3783
|
+
] }) : null,
|
|
3784
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { "data-testid": "slide-deck-slide", children: slides.map(
|
|
3785
|
+
(slide, i) => import_react30.default.cloneElement(slide, {
|
|
3786
|
+
key: slide.key ?? slide.props.blockId,
|
|
3787
|
+
hidden: i !== visibleIndex,
|
|
3788
|
+
slideIndex: i,
|
|
3789
|
+
parentType: "SlideDeck"
|
|
3790
|
+
})
|
|
3791
|
+
) }),
|
|
3792
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("nav", { "aria-label": "Slide navigation", children: [
|
|
3793
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
3794
|
+
"button",
|
|
3795
|
+
{
|
|
3796
|
+
type: "button",
|
|
3797
|
+
"data-testid": "slide-prev",
|
|
3798
|
+
disabled: visibleIndex === 0 || slides.length === 0,
|
|
3799
|
+
onClick: goPrev,
|
|
3800
|
+
children: "Previous slide"
|
|
3801
|
+
}
|
|
3802
|
+
),
|
|
3803
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
3804
|
+
"button",
|
|
3805
|
+
{
|
|
3806
|
+
type: "button",
|
|
3807
|
+
"data-testid": "slide-next",
|
|
3808
|
+
disabled: visibleIndex >= slides.length - 1 || slides.length === 0,
|
|
3809
|
+
onClick: goNext,
|
|
3810
|
+
children: "Next slide"
|
|
3811
|
+
}
|
|
3812
|
+
)
|
|
3813
|
+
] })
|
|
3814
|
+
]
|
|
3815
|
+
}
|
|
3816
|
+
);
|
|
3817
|
+
});
|
|
3818
|
+
var SlideDeck = (0, import_react30.forwardRef)(function SlideDeck2(props, ref) {
|
|
3819
|
+
const blockId = (0, import_react30.useMemo)(
|
|
3820
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3821
|
+
[props.blockId]
|
|
3822
|
+
);
|
|
3823
|
+
const slides = import_react30.default.Children.toArray(props.children).filter(
|
|
3824
|
+
import_react30.default.isValidElement
|
|
3825
|
+
);
|
|
3826
|
+
const { config, storage } = useLessonkit();
|
|
3827
|
+
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
3828
|
+
const initialIndex = useCompoundInitialIndex({
|
|
3829
|
+
courseId: config.courseId,
|
|
3830
|
+
compoundId: blockId,
|
|
3831
|
+
pageCount: slides.length,
|
|
3832
|
+
persistEnabled,
|
|
3833
|
+
storage
|
|
3834
|
+
});
|
|
3835
|
+
const [index, setIndex] = (0, import_react30.useState)(initialIndex);
|
|
3836
|
+
const setIndexStable = (0, import_react30.useCallback)((i) => setIndex(i), []);
|
|
3837
|
+
(0, import_react30.useEffect)(() => {
|
|
3838
|
+
setIndex(initialIndex);
|
|
3839
|
+
}, [config.courseId, blockId, initialIndex]);
|
|
3840
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
3841
|
+
SlideDeckInner,
|
|
3842
|
+
{
|
|
3843
|
+
...props,
|
|
3844
|
+
ref,
|
|
3845
|
+
blockId,
|
|
3846
|
+
slides,
|
|
3847
|
+
index,
|
|
3848
|
+
setIndex,
|
|
3849
|
+
persistEnabled
|
|
3850
|
+
}
|
|
3851
|
+
) });
|
|
3852
|
+
});
|
|
3853
|
+
setLessonkitBlockType(SlideDeck, "SlideDeck");
|
|
3854
|
+
|
|
3035
3855
|
// src/blocks/Accordion.tsx
|
|
3036
|
-
var
|
|
3037
|
-
var
|
|
3856
|
+
var import_react31 = require("react");
|
|
3857
|
+
var import_jsx_runtime21 = require("react/jsx-runtime");
|
|
3038
3858
|
function Accordion(props) {
|
|
3039
3859
|
if (isDevEnvironment4()) {
|
|
3040
3860
|
validateAccordionSections(props.sections);
|
|
3041
3861
|
}
|
|
3042
|
-
const [open, setOpen] = (0,
|
|
3862
|
+
const [open, setOpen] = (0, import_react31.useState)(/* @__PURE__ */ new Set());
|
|
3043
3863
|
const { track } = useLessonkit();
|
|
3044
3864
|
const lessonId = useEnclosingLessonId();
|
|
3045
|
-
const baseId = (0,
|
|
3865
|
+
const baseId = (0, import_react31.useId)();
|
|
3046
3866
|
const toggle = (sectionId) => {
|
|
3047
3867
|
setOpen((prev) => {
|
|
3048
3868
|
const next = new Set(prev);
|
|
@@ -3057,12 +3877,12 @@ function Accordion(props) {
|
|
|
3057
3877
|
return next;
|
|
3058
3878
|
});
|
|
3059
3879
|
};
|
|
3060
|
-
return /* @__PURE__ */ (0,
|
|
3880
|
+
return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
|
|
3061
3881
|
const expanded = open.has(section.id);
|
|
3062
3882
|
const panelId = `${baseId}-${section.id}`;
|
|
3063
3883
|
const triggerId = `${baseId}-trigger-${section.id}`;
|
|
3064
|
-
return /* @__PURE__ */ (0,
|
|
3065
|
-
/* @__PURE__ */ (0,
|
|
3884
|
+
return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { "data-testid": `accordion-section-${section.id}`, children: [
|
|
3885
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h4", { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3066
3886
|
"button",
|
|
3067
3887
|
{
|
|
3068
3888
|
id: triggerId,
|
|
@@ -3074,28 +3894,28 @@ function Accordion(props) {
|
|
|
3074
3894
|
children: section.title
|
|
3075
3895
|
}
|
|
3076
3896
|
) }),
|
|
3077
|
-
expanded ? /* @__PURE__ */ (0,
|
|
3897
|
+
expanded ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
|
|
3078
3898
|
] }, section.id);
|
|
3079
3899
|
}) });
|
|
3080
3900
|
}
|
|
3081
3901
|
setLessonkitBlockType(Accordion, "Accordion");
|
|
3082
3902
|
|
|
3083
3903
|
// src/blocks/DialogCards.tsx
|
|
3084
|
-
var
|
|
3085
|
-
var
|
|
3904
|
+
var import_react32 = require("react");
|
|
3905
|
+
var import_jsx_runtime22 = require("react/jsx-runtime");
|
|
3086
3906
|
function DialogCards(props) {
|
|
3087
|
-
const [index, setIndex] = (0,
|
|
3088
|
-
const [flipped, setFlipped] = (0,
|
|
3907
|
+
const [index, setIndex] = (0, import_react32.useState)(0);
|
|
3908
|
+
const [flipped, setFlipped] = (0, import_react32.useState)(false);
|
|
3089
3909
|
const card = props.cards[index];
|
|
3090
3910
|
if (!card) return null;
|
|
3091
|
-
return /* @__PURE__ */ (0,
|
|
3092
|
-
/* @__PURE__ */ (0,
|
|
3911
|
+
return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
|
|
3912
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("p", { children: [
|
|
3093
3913
|
"Card ",
|
|
3094
3914
|
index + 1,
|
|
3095
3915
|
" of ",
|
|
3096
3916
|
props.cards.length
|
|
3097
3917
|
] }),
|
|
3098
|
-
/* @__PURE__ */ (0,
|
|
3918
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
3099
3919
|
"button",
|
|
3100
3920
|
{
|
|
3101
3921
|
type: "button",
|
|
@@ -3106,8 +3926,8 @@ function DialogCards(props) {
|
|
|
3106
3926
|
children: flipped ? card.back : card.front
|
|
3107
3927
|
}
|
|
3108
3928
|
),
|
|
3109
|
-
/* @__PURE__ */ (0,
|
|
3110
|
-
/* @__PURE__ */ (0,
|
|
3929
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("nav", { "aria-label": "Card navigation", children: [
|
|
3930
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
3111
3931
|
"button",
|
|
3112
3932
|
{
|
|
3113
3933
|
type: "button",
|
|
@@ -3120,7 +3940,7 @@ function DialogCards(props) {
|
|
|
3120
3940
|
children: "Previous"
|
|
3121
3941
|
}
|
|
3122
3942
|
),
|
|
3123
|
-
/* @__PURE__ */ (0,
|
|
3943
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
3124
3944
|
"button",
|
|
3125
3945
|
{
|
|
3126
3946
|
type: "button",
|
|
@@ -3139,11 +3959,11 @@ function DialogCards(props) {
|
|
|
3139
3959
|
setLessonkitBlockType(DialogCards, "DialogCards");
|
|
3140
3960
|
|
|
3141
3961
|
// src/blocks/Flashcards.tsx
|
|
3142
|
-
var
|
|
3143
|
-
var
|
|
3962
|
+
var import_react33 = require("react");
|
|
3963
|
+
var import_jsx_runtime23 = require("react/jsx-runtime");
|
|
3144
3964
|
function Flashcards(props) {
|
|
3145
|
-
const [index, setIndex] = (0,
|
|
3146
|
-
const [face, setFace] = (0,
|
|
3965
|
+
const [index, setIndex] = (0, import_react33.useState)(0);
|
|
3966
|
+
const [face, setFace] = (0, import_react33.useState)("front");
|
|
3147
3967
|
const { track } = useLessonkit();
|
|
3148
3968
|
const lessonId = useEnclosingLessonId();
|
|
3149
3969
|
const card = props.cards[index];
|
|
@@ -3157,10 +3977,10 @@ function Flashcards(props) {
|
|
|
3157
3977
|
lessonId ? { lessonId } : void 0
|
|
3158
3978
|
);
|
|
3159
3979
|
};
|
|
3160
|
-
return /* @__PURE__ */ (0,
|
|
3161
|
-
/* @__PURE__ */ (0,
|
|
3162
|
-
props.selfScore ? /* @__PURE__ */ (0,
|
|
3163
|
-
/* @__PURE__ */ (0,
|
|
3980
|
+
return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
|
|
3981
|
+
/* @__PURE__ */ (0, import_jsx_runtime23.jsx)("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
|
|
3982
|
+
props.selfScore ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
|
|
3983
|
+
/* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
3164
3984
|
"button",
|
|
3165
3985
|
{
|
|
3166
3986
|
type: "button",
|
|
@@ -3178,10 +3998,10 @@ function Flashcards(props) {
|
|
|
3178
3998
|
setLessonkitBlockType(Flashcards, "Flashcards");
|
|
3179
3999
|
|
|
3180
4000
|
// src/blocks/ImageHotspots.tsx
|
|
3181
|
-
var
|
|
3182
|
-
var
|
|
4001
|
+
var import_react34 = require("react");
|
|
4002
|
+
var import_jsx_runtime24 = require("react/jsx-runtime");
|
|
3183
4003
|
function ImageHotspots(props) {
|
|
3184
|
-
const [active, setActive] = (0,
|
|
4004
|
+
const [active, setActive] = (0, import_react34.useState)(null);
|
|
3185
4005
|
const { track } = useLessonkit();
|
|
3186
4006
|
const lessonId = useEnclosingLessonId();
|
|
3187
4007
|
const open = (hotspotId) => {
|
|
@@ -3192,10 +4012,10 @@ function ImageHotspots(props) {
|
|
|
3192
4012
|
lessonId ? { lessonId } : void 0
|
|
3193
4013
|
);
|
|
3194
4014
|
};
|
|
3195
|
-
return /* @__PURE__ */ (0,
|
|
3196
|
-
/* @__PURE__ */ (0,
|
|
3197
|
-
/* @__PURE__ */ (0,
|
|
3198
|
-
props.hotspots.map((h) => /* @__PURE__ */ (0,
|
|
4015
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
|
|
4016
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
4017
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
4018
|
+
props.hotspots.map((h) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
3199
4019
|
"button",
|
|
3200
4020
|
{
|
|
3201
4021
|
type: "button",
|
|
@@ -3214,19 +4034,19 @@ function ImageHotspots(props) {
|
|
|
3214
4034
|
h.id
|
|
3215
4035
|
))
|
|
3216
4036
|
] }),
|
|
3217
|
-
active ? /* @__PURE__ */ (0,
|
|
4037
|
+
active ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
|
|
3218
4038
|
props.hotspots.find((h) => h.id === active)?.content,
|
|
3219
|
-
/* @__PURE__ */ (0,
|
|
4039
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("button", { type: "button", onClick: () => setActive(null), children: "Close" })
|
|
3220
4040
|
] }) : null
|
|
3221
4041
|
] });
|
|
3222
4042
|
}
|
|
3223
4043
|
setLessonkitBlockType(ImageHotspots, "ImageHotspots");
|
|
3224
4044
|
|
|
3225
4045
|
// src/blocks/ImageSlider.tsx
|
|
3226
|
-
var
|
|
3227
|
-
var
|
|
4046
|
+
var import_react35 = require("react");
|
|
4047
|
+
var import_jsx_runtime25 = require("react/jsx-runtime");
|
|
3228
4048
|
function ImageSlider(props) {
|
|
3229
|
-
const [index, setIndex] = (0,
|
|
4049
|
+
const [index, setIndex] = (0, import_react35.useState)(0);
|
|
3230
4050
|
const { track } = useLessonkit();
|
|
3231
4051
|
const lessonId = useEnclosingLessonId();
|
|
3232
4052
|
const slide = props.slides[index];
|
|
@@ -3239,11 +4059,11 @@ function ImageSlider(props) {
|
|
|
3239
4059
|
lessonId ? { lessonId } : void 0
|
|
3240
4060
|
);
|
|
3241
4061
|
};
|
|
3242
|
-
return /* @__PURE__ */ (0,
|
|
3243
|
-
/* @__PURE__ */ (0,
|
|
3244
|
-
slide.caption ? /* @__PURE__ */ (0,
|
|
3245
|
-
/* @__PURE__ */ (0,
|
|
3246
|
-
/* @__PURE__ */ (0,
|
|
4062
|
+
return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
|
|
4063
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
|
|
4064
|
+
slide.caption ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { children: slide.caption }) : null,
|
|
4065
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("nav", { "aria-label": "Slide navigation", children: [
|
|
4066
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
3247
4067
|
"button",
|
|
3248
4068
|
{
|
|
3249
4069
|
type: "button",
|
|
@@ -3253,12 +4073,12 @@ function ImageSlider(props) {
|
|
|
3253
4073
|
children: "Previous"
|
|
3254
4074
|
}
|
|
3255
4075
|
),
|
|
3256
|
-
/* @__PURE__ */ (0,
|
|
4076
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { children: [
|
|
3257
4077
|
index + 1,
|
|
3258
4078
|
" / ",
|
|
3259
4079
|
props.slides.length
|
|
3260
4080
|
] }),
|
|
3261
|
-
/* @__PURE__ */ (0,
|
|
4081
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
3262
4082
|
"button",
|
|
3263
4083
|
{
|
|
3264
4084
|
type: "button",
|
|
@@ -3274,16 +4094,42 @@ function ImageSlider(props) {
|
|
|
3274
4094
|
setLessonkitBlockType(ImageSlider, "ImageSlider");
|
|
3275
4095
|
|
|
3276
4096
|
// src/blocks/FindHotspot.tsx
|
|
3277
|
-
var
|
|
3278
|
-
var
|
|
4097
|
+
var import_react36 = require("react");
|
|
4098
|
+
var import_jsx_runtime26 = require("react/jsx-runtime");
|
|
3279
4099
|
var INTERACTION6 = "findHotspot";
|
|
3280
4100
|
function FindHotspotInner(props, ref) {
|
|
3281
|
-
const checkId = (0,
|
|
3282
|
-
const [selected, setSelected] = (0,
|
|
3283
|
-
const [checked, setChecked] = (0,
|
|
4101
|
+
const checkId = (0, import_react36.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4102
|
+
const [selected, setSelected] = (0, import_react36.useState)(null);
|
|
4103
|
+
const [checked, setChecked] = (0, import_react36.useState)(false);
|
|
4104
|
+
const telemetryReplayedRef = (0, import_react36.useRef)(false);
|
|
3284
4105
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4106
|
+
const targetIdsKey = props.targets.map((t) => t.id).join("\0");
|
|
4107
|
+
(0, import_react36.useEffect)(() => {
|
|
4108
|
+
setSelected(null);
|
|
4109
|
+
setChecked(false);
|
|
4110
|
+
telemetryReplayedRef.current = false;
|
|
4111
|
+
}, [checkId, props.correctTargetId, targetIdsKey]);
|
|
3285
4112
|
const correct = selected === props.correctTargetId;
|
|
3286
|
-
const
|
|
4113
|
+
const replayTelemetry = (nextSelected, nextChecked, nextCorrect) => {
|
|
4114
|
+
if (telemetryReplayedRef.current || !nextChecked || nextSelected === null) return;
|
|
4115
|
+
telemetryReplayedRef.current = true;
|
|
4116
|
+
assessment.answer({
|
|
4117
|
+
checkId,
|
|
4118
|
+
interactionType: INTERACTION6,
|
|
4119
|
+
response: nextSelected,
|
|
4120
|
+
correct: nextCorrect
|
|
4121
|
+
});
|
|
4122
|
+
if (nextCorrect) {
|
|
4123
|
+
assessment.complete({
|
|
4124
|
+
checkId,
|
|
4125
|
+
interactionType: INTERACTION6,
|
|
4126
|
+
score: 1,
|
|
4127
|
+
maxScore: 1,
|
|
4128
|
+
passingScore: props.passingScore ?? 1
|
|
4129
|
+
});
|
|
4130
|
+
}
|
|
4131
|
+
};
|
|
4132
|
+
const handle = (0, import_react36.useMemo)(
|
|
3287
4133
|
() => buildAssessmentHandle({
|
|
3288
4134
|
checkId,
|
|
3289
4135
|
getScore: () => checked && correct ? 1 : 0,
|
|
@@ -3292,6 +4138,7 @@ function FindHotspotInner(props, ref) {
|
|
|
3292
4138
|
resetTask: () => {
|
|
3293
4139
|
setSelected(null);
|
|
3294
4140
|
setChecked(false);
|
|
4141
|
+
telemetryReplayedRef.current = false;
|
|
3295
4142
|
},
|
|
3296
4143
|
showSolutions: () => setSelected(props.correctTargetId),
|
|
3297
4144
|
getXAPIData: () => ({
|
|
@@ -3304,16 +4151,31 @@ function FindHotspotInner(props, ref) {
|
|
|
3304
4151
|
}),
|
|
3305
4152
|
getCurrentState: () => ({ selected, checked }),
|
|
3306
4153
|
resume: (state) => {
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
4154
|
+
let nextSelected = selected;
|
|
4155
|
+
const rawSelected = readStringField(state, "selected");
|
|
4156
|
+
if (typeof rawSelected === "string" || rawSelected === null) {
|
|
4157
|
+
const valid = rawSelected === null || props.targets.some((t) => t.id === rawSelected);
|
|
4158
|
+
nextSelected = valid ? rawSelected : null;
|
|
4159
|
+
setSelected(nextSelected);
|
|
4160
|
+
}
|
|
4161
|
+
let nextChecked = checked;
|
|
4162
|
+
readBooleanStateField(state, "checked", (value) => {
|
|
4163
|
+
nextChecked = value;
|
|
4164
|
+
setChecked(value);
|
|
4165
|
+
});
|
|
4166
|
+
const nextCorrect = nextSelected === props.correctTargetId;
|
|
4167
|
+
replayTelemetry(nextSelected, nextChecked, nextCorrect);
|
|
3310
4168
|
}
|
|
3311
4169
|
}),
|
|
3312
|
-
[
|
|
4170
|
+
[assessment, checkId, checked, correct, props.correctTargetId, props.passingScore, props.targets, selected]
|
|
3313
4171
|
);
|
|
3314
4172
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
4173
|
+
const selectTarget = (id) => {
|
|
4174
|
+
setSelected(id);
|
|
4175
|
+
setChecked(false);
|
|
4176
|
+
};
|
|
3315
4177
|
const submit = () => {
|
|
3316
|
-
if (!selected) return;
|
|
4178
|
+
if (!selected || checked) return;
|
|
3317
4179
|
setChecked(true);
|
|
3318
4180
|
assessment.answer({
|
|
3319
4181
|
checkId,
|
|
@@ -3327,14 +4189,14 @@ function FindHotspotInner(props, ref) {
|
|
|
3327
4189
|
interactionType: INTERACTION6,
|
|
3328
4190
|
score: 1,
|
|
3329
4191
|
maxScore: 1,
|
|
3330
|
-
passingScore: props.passingScore
|
|
4192
|
+
passingScore: props.passingScore ?? 1
|
|
3331
4193
|
});
|
|
3332
4194
|
}
|
|
3333
4195
|
};
|
|
3334
|
-
return /* @__PURE__ */ (0,
|
|
3335
|
-
/* @__PURE__ */ (0,
|
|
3336
|
-
/* @__PURE__ */ (0,
|
|
3337
|
-
props.targets.map((t) => /* @__PURE__ */ (0,
|
|
4196
|
+
return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
|
|
4197
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
4198
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
4199
|
+
props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
|
|
3338
4200
|
"button",
|
|
3339
4201
|
{
|
|
3340
4202
|
type: "button",
|
|
@@ -3347,30 +4209,30 @@ function FindHotspotInner(props, ref) {
|
|
|
3347
4209
|
top: `${t.y}%`,
|
|
3348
4210
|
transform: "translate(-50%, -50%)"
|
|
3349
4211
|
},
|
|
3350
|
-
onClick: () =>
|
|
4212
|
+
onClick: () => selectTarget(t.id),
|
|
3351
4213
|
children: t.label
|
|
3352
4214
|
},
|
|
3353
4215
|
t.id
|
|
3354
4216
|
))
|
|
3355
4217
|
] }),
|
|
3356
|
-
/* @__PURE__ */ (0,
|
|
3357
|
-
checked ? /* @__PURE__ */ (0,
|
|
4218
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
|
|
4219
|
+
checked ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
3358
4220
|
] });
|
|
3359
4221
|
}
|
|
3360
|
-
var FindHotspotInnerForwarded = (0,
|
|
3361
|
-
var FindHotspot = (0,
|
|
3362
|
-
return /* @__PURE__ */ (0,
|
|
4222
|
+
var FindHotspotInnerForwarded = (0, import_react36.forwardRef)(FindHotspotInner);
|
|
4223
|
+
var FindHotspot = (0, import_react36.forwardRef)(function FindHotspot2(props, ref) {
|
|
4224
|
+
return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
3363
4225
|
});
|
|
3364
4226
|
setLessonkitBlockType(FindHotspot, "FindHotspot");
|
|
3365
4227
|
|
|
3366
4228
|
// src/blocks/FindMultipleHotspots.tsx
|
|
3367
|
-
var
|
|
3368
|
-
var
|
|
4229
|
+
var import_react37 = require("react");
|
|
4230
|
+
var import_jsx_runtime27 = require("react/jsx-runtime");
|
|
3369
4231
|
var INTERACTION7 = "findMultipleHotspots";
|
|
3370
4232
|
function FindMultipleHotspotsInner(props, ref) {
|
|
3371
|
-
const checkId = (0,
|
|
3372
|
-
const [selected, setSelected] = (0,
|
|
3373
|
-
const [checked, setChecked] = (0,
|
|
4233
|
+
const checkId = (0, import_react37.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4234
|
+
const [selected, setSelected] = (0, import_react37.useState)(/* @__PURE__ */ new Set());
|
|
4235
|
+
const [checked, setChecked] = (0, import_react37.useState)(false);
|
|
3374
4236
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
3375
4237
|
const toggle = (id) => {
|
|
3376
4238
|
setSelected((prev) => {
|
|
@@ -3379,9 +4241,10 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
3379
4241
|
else next.add(id);
|
|
3380
4242
|
return next;
|
|
3381
4243
|
});
|
|
4244
|
+
setChecked(false);
|
|
3382
4245
|
};
|
|
3383
4246
|
const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
|
|
3384
|
-
const handle = (0,
|
|
4247
|
+
const handle = (0, import_react37.useMemo)(
|
|
3385
4248
|
() => buildAssessmentHandle({
|
|
3386
4249
|
checkId,
|
|
3387
4250
|
getScore: () => checked && correct ? 1 : 0,
|
|
@@ -3411,7 +4274,7 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
3411
4274
|
);
|
|
3412
4275
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3413
4276
|
const submit = () => {
|
|
3414
|
-
if (selected.size === 0) return;
|
|
4277
|
+
if (selected.size === 0 || checked) return;
|
|
3415
4278
|
setChecked(true);
|
|
3416
4279
|
assessment.answer({
|
|
3417
4280
|
checkId,
|
|
@@ -3425,14 +4288,14 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
3425
4288
|
interactionType: INTERACTION7,
|
|
3426
4289
|
score: 1,
|
|
3427
4290
|
maxScore: 1,
|
|
3428
|
-
passingScore: props.passingScore
|
|
4291
|
+
passingScore: props.passingScore ?? 1
|
|
3429
4292
|
});
|
|
3430
4293
|
}
|
|
3431
4294
|
};
|
|
3432
|
-
return /* @__PURE__ */ (0,
|
|
3433
|
-
/* @__PURE__ */ (0,
|
|
3434
|
-
/* @__PURE__ */ (0,
|
|
3435
|
-
props.targets.map((t) => /* @__PURE__ */ (0,
|
|
4295
|
+
return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
|
|
4296
|
+
/* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
4297
|
+
/* @__PURE__ */ (0, import_jsx_runtime27.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
4298
|
+
props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
|
|
3436
4299
|
"button",
|
|
3437
4300
|
{
|
|
3438
4301
|
type: "button",
|
|
@@ -3451,23 +4314,23 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
3451
4314
|
t.id
|
|
3452
4315
|
))
|
|
3453
4316
|
] }),
|
|
3454
|
-
/* @__PURE__ */ (0,
|
|
3455
|
-
checked ? /* @__PURE__ */ (0,
|
|
4317
|
+
/* @__PURE__ */ (0, import_jsx_runtime27.jsx)("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
|
|
4318
|
+
checked ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
3456
4319
|
] });
|
|
3457
4320
|
}
|
|
3458
|
-
var FindMultipleHotspotsInnerForwarded = (0,
|
|
3459
|
-
var FindMultipleHotspots = (0,
|
|
4321
|
+
var FindMultipleHotspotsInnerForwarded = (0, import_react37.forwardRef)(FindMultipleHotspotsInner);
|
|
4322
|
+
var FindMultipleHotspots = (0, import_react37.forwardRef)(
|
|
3460
4323
|
function FindMultipleHotspots2(props, ref) {
|
|
3461
|
-
return /* @__PURE__ */ (0,
|
|
4324
|
+
return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
3462
4325
|
}
|
|
3463
4326
|
);
|
|
3464
4327
|
setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
|
|
3465
4328
|
|
|
3466
4329
|
// src/index.tsx
|
|
3467
|
-
var
|
|
4330
|
+
var import_core19 = require("@lessonkit/core");
|
|
3468
4331
|
|
|
3469
4332
|
// src/theme/ThemeProvider.tsx
|
|
3470
|
-
var
|
|
4333
|
+
var import_react38 = __toESM(require("react"), 1);
|
|
3471
4334
|
var import_themes = require("@lessonkit/themes");
|
|
3472
4335
|
|
|
3473
4336
|
// src/theme/applyCssVariables.ts
|
|
@@ -3486,11 +4349,11 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
3486
4349
|
}
|
|
3487
4350
|
|
|
3488
4351
|
// src/theme/ThemeProvider.tsx
|
|
3489
|
-
var
|
|
3490
|
-
var ThemeContext = (0,
|
|
4352
|
+
var import_jsx_runtime28 = require("react/jsx-runtime");
|
|
4353
|
+
var ThemeContext = (0, import_react38.createContext)(null);
|
|
3491
4354
|
var useIsoLayoutEffect2 = (
|
|
3492
4355
|
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
3493
|
-
typeof window !== "undefined" ?
|
|
4356
|
+
typeof window !== "undefined" ? import_react38.useLayoutEffect : import_react38.default.useEffect
|
|
3494
4357
|
);
|
|
3495
4358
|
function getSystemMode() {
|
|
3496
4359
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
@@ -3509,7 +4372,7 @@ function ThemeProvider(props) {
|
|
|
3509
4372
|
const preset = props.preset ?? "default";
|
|
3510
4373
|
const mode = props.mode ?? "light";
|
|
3511
4374
|
const targetKind = props.target ?? "document";
|
|
3512
|
-
const [resolvedMode, setResolvedMode] = (0,
|
|
4375
|
+
const [resolvedMode, setResolvedMode] = (0, import_react38.useState)(
|
|
3513
4376
|
() => mode === "system" ? getSystemMode() : mode
|
|
3514
4377
|
);
|
|
3515
4378
|
useIsoLayoutEffect2(() => {
|
|
@@ -3525,20 +4388,20 @@ function ThemeProvider(props) {
|
|
|
3525
4388
|
return () => mq.removeEventListener("change", onChange);
|
|
3526
4389
|
}, [mode]);
|
|
3527
4390
|
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
3528
|
-
const effectiveTheme = (0,
|
|
4391
|
+
const effectiveTheme = (0, import_react38.useMemo)(() => {
|
|
3529
4392
|
const modeBase = resolveModeBase(mode, dataTheme);
|
|
3530
4393
|
const base = preset === "default" ? modeBase : preset === "brand" ? (0, import_themes.mergeThemes)(modeBase, import_themes.brandThemeOverrides) : (0, import_themes.mergeThemes)(modeBase, (0, import_themes.getPresetTheme)(preset));
|
|
3531
4394
|
return (0, import_themes.mergeThemes)(base, props.theme ?? {});
|
|
3532
4395
|
}, [preset, mode, dataTheme, props.theme]);
|
|
3533
|
-
const hostRef = (0,
|
|
3534
|
-
const appliedKeysRef = (0,
|
|
4396
|
+
const hostRef = (0, import_react38.useRef)(null);
|
|
4397
|
+
const appliedKeysRef = (0, import_react38.useRef)(/* @__PURE__ */ new Set());
|
|
3535
4398
|
useIsoLayoutEffect2(() => {
|
|
3536
4399
|
if (targetKind === "document" && typeof document !== "undefined") {
|
|
3537
4400
|
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
3538
4401
|
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
3539
4402
|
}
|
|
3540
4403
|
}, [targetKind, dataTheme]);
|
|
3541
|
-
const inject = (0,
|
|
4404
|
+
const inject = (0, import_react38.useCallback)(() => {
|
|
3542
4405
|
const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
|
|
3543
4406
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
3544
4407
|
if (!el) return;
|
|
@@ -3555,7 +4418,7 @@ function ThemeProvider(props) {
|
|
|
3555
4418
|
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
3556
4419
|
};
|
|
3557
4420
|
}, [inject, targetKind]);
|
|
3558
|
-
const value = (0,
|
|
4421
|
+
const value = (0, import_react38.useMemo)(
|
|
3559
4422
|
() => ({
|
|
3560
4423
|
theme: effectiveTheme,
|
|
3561
4424
|
preset,
|
|
@@ -3565,12 +4428,12 @@ function ThemeProvider(props) {
|
|
|
3565
4428
|
[effectiveTheme, preset, mode, dataTheme]
|
|
3566
4429
|
);
|
|
3567
4430
|
if (targetKind === "document") {
|
|
3568
|
-
return /* @__PURE__ */ (0,
|
|
4431
|
+
return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
|
|
3569
4432
|
}
|
|
3570
|
-
return /* @__PURE__ */ (0,
|
|
4433
|
+
return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
3571
4434
|
}
|
|
3572
4435
|
function useTheme() {
|
|
3573
|
-
const ctx = (0,
|
|
4436
|
+
const ctx = (0, import_react38.useContext)(ThemeContext);
|
|
3574
4437
|
if (!ctx) {
|
|
3575
4438
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
3576
4439
|
}
|
|
@@ -3578,8 +4441,15 @@ function useTheme() {
|
|
|
3578
4441
|
}
|
|
3579
4442
|
|
|
3580
4443
|
// src/catalogV3Entries.ts
|
|
3581
|
-
var
|
|
3582
|
-
var COMPOUND_PARENTS = [
|
|
4444
|
+
var import_core18 = require("@lessonkit/core");
|
|
4445
|
+
var COMPOUND_PARENTS = [
|
|
4446
|
+
"Lesson",
|
|
4447
|
+
"Page",
|
|
4448
|
+
"InteractiveBook",
|
|
4449
|
+
"Slide",
|
|
4450
|
+
"SlideDeck",
|
|
4451
|
+
"AssessmentSequence"
|
|
4452
|
+
];
|
|
3583
4453
|
function extendParents(entry) {
|
|
3584
4454
|
if (!entry.parentConstraints?.length) return entry;
|
|
3585
4455
|
const merged = /* @__PURE__ */ new Set([...entry.parentConstraints, ...COMPOUND_PARENTS]);
|
|
@@ -3643,8 +4513,8 @@ var v3CompoundAndContentEntries = [
|
|
|
3643
4513
|
h5pMachineName: "H5P.Column",
|
|
3644
4514
|
h5pAlias: "Column",
|
|
3645
4515
|
description: "Column layout container (H5P Column / Page).",
|
|
3646
|
-
allowedChildTypes: [...
|
|
3647
|
-
maxNestingDepth:
|
|
4516
|
+
allowedChildTypes: [...import_core18.PAGE_ALLOWED_CHILD_TYPES],
|
|
4517
|
+
maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.Page,
|
|
3648
4518
|
props: [
|
|
3649
4519
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
3650
4520
|
{ name: "title", type: "string", required: false, description: "Page title." },
|
|
@@ -3664,8 +4534,8 @@ var v3CompoundAndContentEntries = [
|
|
|
3664
4534
|
h5pMachineName: "H5P.InteractiveBook",
|
|
3665
4535
|
h5pAlias: "Interactive Book",
|
|
3666
4536
|
description: "Multi-page book with chapter navigation.",
|
|
3667
|
-
allowedChildTypes: [...
|
|
3668
|
-
maxNestingDepth:
|
|
4537
|
+
allowedChildTypes: [...import_core18.INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
|
|
4538
|
+
maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.InteractiveBook,
|
|
3669
4539
|
props: [
|
|
3670
4540
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
3671
4541
|
{ name: "title", type: "string", required: true, description: "Book title." },
|
|
@@ -3683,6 +4553,53 @@ var v3CompoundAndContentEntries = [
|
|
|
3683
4553
|
theming: { surface: "global-inherit", stylingNotes: "Book chrome." },
|
|
3684
4554
|
telemetry: { emits: ["book_page_viewed"], requiresActiveLesson: true }
|
|
3685
4555
|
},
|
|
4556
|
+
{
|
|
4557
|
+
type: "Slide",
|
|
4558
|
+
category: "container",
|
|
4559
|
+
compoundContract: true,
|
|
4560
|
+
h5pMachineName: "H5P.CoursePresentation",
|
|
4561
|
+
h5pAlias: "Course Presentation slide",
|
|
4562
|
+
description: "Single slide row in a SlideDeck. Planned allowlist expansion: Video, Summary.",
|
|
4563
|
+
allowedChildTypes: [...import_core18.SLIDE_ALLOWED_CHILD_TYPES],
|
|
4564
|
+
maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.Slide,
|
|
4565
|
+
props: [
|
|
4566
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4567
|
+
{ name: "title", type: "string", required: false, description: "Slide title." },
|
|
4568
|
+
{ name: "children", type: "ReactNode", required: true, description: "Slide content." }
|
|
4569
|
+
],
|
|
4570
|
+
requiredIds: [],
|
|
4571
|
+
optionalIds: ["blockId"],
|
|
4572
|
+
parentConstraints: ["SlideDeck"],
|
|
4573
|
+
a11y: { element: "section", ariaLabel: "Slide", keyboard: "N/A", notes: "H5P Course Presentation slide row." },
|
|
4574
|
+
theming: { surface: "global-inherit", stylingNotes: "Container." },
|
|
4575
|
+
telemetry: { emits: ["compound_page_viewed"], requiresActiveLesson: true }
|
|
4576
|
+
},
|
|
4577
|
+
{
|
|
4578
|
+
type: "SlideDeck",
|
|
4579
|
+
category: "container",
|
|
4580
|
+
compoundContract: true,
|
|
4581
|
+
h5pMachineName: "H5P.CoursePresentation",
|
|
4582
|
+
h5pAlias: "Course Presentation",
|
|
4583
|
+
description: "Multi-slide presentation with keyboard navigation.",
|
|
4584
|
+
allowedChildTypes: [...import_core18.SLIDE_DECK_ALLOWED_CHILD_TYPES],
|
|
4585
|
+
maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.SlideDeck,
|
|
4586
|
+
props: [
|
|
4587
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4588
|
+
{ name: "title", type: "string", required: true, description: "Deck title." },
|
|
4589
|
+
{ name: "showDeckScore", type: "boolean", required: false, description: "Show aggregate score." },
|
|
4590
|
+
{ name: "children", type: "Slide[]", required: true, description: "Slides." }
|
|
4591
|
+
],
|
|
4592
|
+
requiredIds: ["blockId"],
|
|
4593
|
+
parentConstraints: ["Lesson"],
|
|
4594
|
+
a11y: {
|
|
4595
|
+
element: "section",
|
|
4596
|
+
ariaLabel: "Slide deck",
|
|
4597
|
+
keyboard: "Arrow keys, Home, End, Previous/Next slide buttons.",
|
|
4598
|
+
notes: "H5P Course Presentation equivalent."
|
|
4599
|
+
},
|
|
4600
|
+
theming: { surface: "global-inherit", stylingNotes: "Deck chrome." },
|
|
4601
|
+
telemetry: { emits: ["slide_viewed"], requiresActiveLesson: true }
|
|
4602
|
+
},
|
|
3686
4603
|
{
|
|
3687
4604
|
type: "Accordion",
|
|
3688
4605
|
category: "content",
|
|
@@ -3816,8 +4733,8 @@ function buildV3CatalogFromV2(v2) {
|
|
|
3816
4733
|
return {
|
|
3817
4734
|
...base,
|
|
3818
4735
|
compoundContract: true,
|
|
3819
|
-
allowedChildTypes: [...
|
|
3820
|
-
maxNestingDepth:
|
|
4736
|
+
allowedChildTypes: [...import_core18.ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
|
|
4737
|
+
maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.AssessmentSequence
|
|
3821
4738
|
};
|
|
3822
4739
|
}
|
|
3823
4740
|
return base;
|
|
@@ -4229,6 +5146,8 @@ function getBlockCatalogEntry(type, opts) {
|
|
|
4229
5146
|
Quiz,
|
|
4230
5147
|
Reflection,
|
|
4231
5148
|
Scenario,
|
|
5149
|
+
Slide,
|
|
5150
|
+
SlideDeck,
|
|
4232
5151
|
Text,
|
|
4233
5152
|
ThemeProvider,
|
|
4234
5153
|
TrueFalse,
|