@lessonkit/react 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/block-catalog.v3.json +1005 -107
- package/dist/AssessmentLessonGuard-D2Plzybb.d.cts +21 -0
- package/dist/AssessmentLessonGuard-D2Plzybb.d.ts +21 -0
- package/dist/blocks-entry.cjs +4563 -0
- package/dist/blocks-entry.d.cts +411 -0
- package/dist/blocks-entry.d.ts +411 -0
- package/dist/blocks-entry.js +69 -0
- package/dist/chunk-4LQ4TTEE.js +4018 -0
- package/dist/chunk-TDM3ARE7.js +1775 -0
- package/dist/chunk-UUTXECVW.js +252 -0
- package/dist/index.cjs +2329 -318
- package/dist/index.d.cts +31 -282
- package/dist/index.d.ts +31 -282
- package/dist/index.js +433 -4295
- package/dist/testing.cjs +540 -0
- package/dist/testing.d.cts +16 -0
- package/dist/testing.d.ts +16 -0
- package/dist/testing.js +18 -0
- package/package.json +33 -16
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
Accordion: () => Accordion,
|
|
34
|
+
ArithmeticQuiz: () => ArithmeticQuiz,
|
|
34
35
|
AssessmentSequence: () => AssessmentSequence,
|
|
35
36
|
BLOCK_CATALOG: () => BLOCK_CATALOG,
|
|
36
37
|
BLOCK_CATALOG_V2: () => BLOCK_CATALOG_V2,
|
|
@@ -39,6 +40,7 @@ __export(index_exports, {
|
|
|
39
40
|
DialogCards: () => DialogCards,
|
|
40
41
|
DragAndDrop: () => DragAndDrop,
|
|
41
42
|
DragTheWords: () => DragTheWords,
|
|
43
|
+
Essay: () => Essay,
|
|
42
44
|
FillInTheBlanks: () => FillInTheBlanks,
|
|
43
45
|
FindHotspot: () => FindHotspot,
|
|
44
46
|
FindMultipleHotspots: () => FindMultipleHotspots,
|
|
@@ -46,36 +48,48 @@ __export(index_exports, {
|
|
|
46
48
|
Heading: () => Heading,
|
|
47
49
|
Image: () => Image,
|
|
48
50
|
ImageHotspots: () => ImageHotspots,
|
|
51
|
+
ImagePairing: () => ImagePairing,
|
|
52
|
+
ImageSequencing: () => ImageSequencing,
|
|
49
53
|
ImageSlider: () => ImageSlider,
|
|
54
|
+
InformationWall: () => InformationWall,
|
|
50
55
|
InteractiveBook: () => InteractiveBook,
|
|
56
|
+
InteractiveVideo: () => InteractiveVideo,
|
|
51
57
|
KnowledgeCheck: () => KnowledgeCheck,
|
|
52
58
|
Lesson: () => Lesson,
|
|
53
59
|
LessonkitProvider: () => LessonkitProvider,
|
|
54
60
|
MarkTheWords: () => MarkTheWords,
|
|
61
|
+
MemoryGame: () => MemoryGame,
|
|
55
62
|
Page: () => Page,
|
|
63
|
+
ParallaxSlideshow: () => ParallaxSlideshow,
|
|
56
64
|
ProgressTracker: () => ProgressTracker,
|
|
65
|
+
Questionnaire: () => Questionnaire,
|
|
57
66
|
Quiz: () => Quiz,
|
|
58
67
|
Reflection: () => Reflection,
|
|
59
68
|
Scenario: () => Scenario,
|
|
60
69
|
Slide: () => Slide,
|
|
61
70
|
SlideDeck: () => SlideDeck,
|
|
71
|
+
Summary: () => Summary,
|
|
62
72
|
Text: () => Text,
|
|
63
73
|
ThemeProvider: () => ThemeProvider,
|
|
74
|
+
TimedCue: () => TimedCue,
|
|
64
75
|
TrueFalse: () => TrueFalse,
|
|
76
|
+
Video: () => Video,
|
|
77
|
+
assertProductionCourseConfig: () => assertProductionCourseConfig,
|
|
65
78
|
blockCatalogV2Version: () => blockCatalogV2Version,
|
|
66
79
|
blockCatalogV3Version: () => blockCatalogV3Version,
|
|
67
80
|
blockCatalogVersion: () => blockCatalogVersion,
|
|
68
81
|
buildBlockCatalog: () => buildBlockCatalog,
|
|
69
|
-
buildTelemetryEvent: () =>
|
|
70
|
-
createLessonkitRuntime: () =>
|
|
71
|
-
createPluginRegistry: () =>
|
|
72
|
-
createTelemetryPipeline: () =>
|
|
73
|
-
defineAssessmentPlugin: () =>
|
|
74
|
-
defineLifecyclePlugin: () =>
|
|
75
|
-
defineTelemetryPlugin: () =>
|
|
82
|
+
buildTelemetryEvent: () => import_core21.buildTelemetryEvent,
|
|
83
|
+
createLessonkitRuntime: () => import_core21.createLessonkitRuntime,
|
|
84
|
+
createPluginRegistry: () => import_core21.createPluginRegistry,
|
|
85
|
+
createTelemetryPipeline: () => import_core21.createTelemetryPipeline,
|
|
86
|
+
defineAssessmentPlugin: () => import_core21.defineAssessmentPlugin,
|
|
87
|
+
defineLifecyclePlugin: () => import_core21.defineLifecyclePlugin,
|
|
88
|
+
defineTelemetryPlugin: () => import_core21.defineTelemetryPlugin,
|
|
76
89
|
getBlockCatalogEntry: () => getBlockCatalogEntry,
|
|
77
90
|
resetAssessmentWarningsForTests: () => resetAssessmentWarningsForTests,
|
|
78
91
|
resetQuizWarningsForTests: () => resetQuizWarningsForTests,
|
|
92
|
+
shouldEnforceProductionGuard: () => shouldEnforceProductionGuard,
|
|
79
93
|
useAssessmentState: () => useAssessmentState,
|
|
80
94
|
useCompletion: () => useCompletion,
|
|
81
95
|
useLessonkit: () => useLessonkit,
|
|
@@ -99,15 +113,11 @@ var import_core8 = require("@lessonkit/core");
|
|
|
99
113
|
|
|
100
114
|
// src/runtime/observability.ts
|
|
101
115
|
var import_xapi = require("@lessonkit/xapi");
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
if (observability?.onXapiQueueCap) {
|
|
109
|
-
opts.onCap = observability.onXapiQueueCap;
|
|
110
|
-
}
|
|
116
|
+
function createXapiQueueFromObservability(getObservability) {
|
|
117
|
+
const opts = {
|
|
118
|
+
onDepth: (size) => getObservability?.()?.onXapiQueueDepth?.(size),
|
|
119
|
+
onCap: () => getObservability?.()?.onXapiQueueCap?.()
|
|
120
|
+
};
|
|
111
121
|
return (0, import_xapi.createInMemoryXAPIQueue)(opts);
|
|
112
122
|
}
|
|
113
123
|
function wrapBatchSink(batchSink, observability) {
|
|
@@ -122,32 +132,6 @@ function wrapBatchSink(batchSink, observability) {
|
|
|
122
132
|
}
|
|
123
133
|
};
|
|
124
134
|
}
|
|
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
|
-
}
|
|
151
135
|
function wrapTrackingSink(sink, observability) {
|
|
152
136
|
if (!sink || !observability?.onTelemetrySinkError) return sink;
|
|
153
137
|
const onError = observability.onTelemetrySinkError;
|
|
@@ -157,16 +141,108 @@ function wrapTrackingSink(sink, observability) {
|
|
|
157
141
|
if (result != null && typeof result.catch === "function") {
|
|
158
142
|
return result.catch((err) => {
|
|
159
143
|
onError(err, { sinkId: "tracking" });
|
|
144
|
+
throw err;
|
|
160
145
|
});
|
|
161
146
|
}
|
|
162
147
|
return result;
|
|
163
148
|
} catch (err) {
|
|
164
149
|
onError(err, { sinkId: "tracking" });
|
|
165
|
-
|
|
150
|
+
throw err;
|
|
166
151
|
}
|
|
167
152
|
});
|
|
168
153
|
}
|
|
169
154
|
|
|
155
|
+
// src/runtime/productionGuard.ts
|
|
156
|
+
var import_meta = {};
|
|
157
|
+
function isProductionEnvironment() {
|
|
158
|
+
try {
|
|
159
|
+
if (import_meta.env?.PROD === true) return true;
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
const g = globalThis;
|
|
163
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
|
|
164
|
+
}
|
|
165
|
+
function shouldEnforceProductionGuard() {
|
|
166
|
+
try {
|
|
167
|
+
if (import_meta.env?.MODE === "test") return false;
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
return isProductionEnvironment();
|
|
171
|
+
}
|
|
172
|
+
function looksLikeConsoleSink(fn) {
|
|
173
|
+
if (typeof fn !== "function") return false;
|
|
174
|
+
const src = Function.prototype.toString.call(fn);
|
|
175
|
+
return /console\.(log|debug|info)\s*\(/.test(src);
|
|
176
|
+
}
|
|
177
|
+
function isTrackingDeliveryConfigured(tracking) {
|
|
178
|
+
if (!tracking || tracking.enabled === false) return false;
|
|
179
|
+
return Boolean(tracking.sink || tracking.batchSink);
|
|
180
|
+
}
|
|
181
|
+
function isXapiDeliveryConfigured(xapi) {
|
|
182
|
+
if (!xapi || xapi.enabled === false) return false;
|
|
183
|
+
if (xapi.client) return true;
|
|
184
|
+
return typeof xapi.transport === "function";
|
|
185
|
+
}
|
|
186
|
+
function trackingUsesConsole(config) {
|
|
187
|
+
const tracking = config.tracking;
|
|
188
|
+
if (!tracking || tracking.enabled === false) return false;
|
|
189
|
+
if (tracking.batchSink && looksLikeConsoleSink(tracking.batchSink)) return true;
|
|
190
|
+
if (tracking.sink && looksLikeConsoleSink(tracking.sink)) return true;
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
function xapiUsesConsole(config) {
|
|
194
|
+
const xapi = config.xapi;
|
|
195
|
+
if (!xapi || xapi.enabled === false || xapi.client) return false;
|
|
196
|
+
return typeof xapi.transport === "function" && looksLikeConsoleSink(xapi.transport);
|
|
197
|
+
}
|
|
198
|
+
function observabilityIncomplete(observability, opts) {
|
|
199
|
+
if (!opts.trackingEnabled && !opts.xapiEnabled) return false;
|
|
200
|
+
const required = [observability?.onLxpackBridgeMiss];
|
|
201
|
+
if (opts.trackingEnabled) {
|
|
202
|
+
required.push(observability?.onTelemetrySinkError, observability?.onTelemetryBufferDrop);
|
|
203
|
+
}
|
|
204
|
+
if (opts.xapiEnabled) {
|
|
205
|
+
required.push(
|
|
206
|
+
observability?.onXapiQueueDepth,
|
|
207
|
+
observability?.onXapiQueueCap,
|
|
208
|
+
observability?.onXapiTransportError
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return required.some((hook) => !hook);
|
|
212
|
+
}
|
|
213
|
+
function requiredObservabilityHookCount(opts) {
|
|
214
|
+
let count = 1;
|
|
215
|
+
if (opts.trackingEnabled) count += 2;
|
|
216
|
+
if (opts.xapiEnabled) count += 3;
|
|
217
|
+
return count;
|
|
218
|
+
}
|
|
219
|
+
function assertProductionCourseConfig(config) {
|
|
220
|
+
if (!isProductionEnvironment()) return;
|
|
221
|
+
if (config.tracking && config.tracking.enabled !== false && !isTrackingDeliveryConfigured(config.tracking)) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
"[lessonkit] Production build has tracking enabled but no sink or batchSink configured."
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const trackingEnabled = isTrackingDeliveryConfigured(config.tracking);
|
|
227
|
+
const xapiEnabled = isXapiDeliveryConfigured(config.xapi);
|
|
228
|
+
if (trackingUsesConsole(config)) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
"[lessonkit] Production build uses console telemetry sinks. Wire createFetchBatchSink or a real sink. See production checklist."
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (xapiUsesConsole(config)) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
"[lessonkit] Production build uses console xAPI transport. Wire createFetchTransport to your LRS proxy. See production checklist."
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (observabilityIncomplete(config.observability, { trackingEnabled, xapiEnabled })) {
|
|
239
|
+
const hookCount = requiredObservabilityHookCount({ trackingEnabled, xapiEnabled });
|
|
240
|
+
throw new Error(
|
|
241
|
+
`[lessonkit] Production build missing observability hooks. Wire all ${hookCount} config.observability callbacks before go-live.`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
170
246
|
// src/provider/useLessonkitProviderRuntime.ts
|
|
171
247
|
var import_xapi5 = require("@lessonkit/xapi");
|
|
172
248
|
|
|
@@ -265,7 +341,7 @@ var import_core4 = require("@lessonkit/core");
|
|
|
265
341
|
|
|
266
342
|
// src/runtime/xapi.ts
|
|
267
343
|
var import_xapi3 = require("@lessonkit/xapi");
|
|
268
|
-
function createXapiClientFromConfig(config, queue) {
|
|
344
|
+
function createXapiClientFromConfig(config, queue, observability) {
|
|
269
345
|
if (config.xapi?.enabled === false) return null;
|
|
270
346
|
if (config.xapi?.client) return config.xapi.client;
|
|
271
347
|
if (!config.courseId) return null;
|
|
@@ -275,7 +351,9 @@ function createXapiClientFromConfig(config, queue) {
|
|
|
275
351
|
courseId: config.courseId,
|
|
276
352
|
transport: config.xapi?.transport,
|
|
277
353
|
exitTransport: config.xapi?.exitTransport,
|
|
278
|
-
|
|
354
|
+
abortInFlight: config.xapi?.abortInFlight,
|
|
355
|
+
queue,
|
|
356
|
+
onTransportError: observability?.onXapiTransportError
|
|
279
357
|
});
|
|
280
358
|
}
|
|
281
359
|
|
|
@@ -359,12 +437,26 @@ function emitTelemetryWithPlugins(opts) {
|
|
|
359
437
|
}
|
|
360
438
|
|
|
361
439
|
// src/provider/courseStarted/emit.ts
|
|
440
|
+
function resolveTrackingClient(source) {
|
|
441
|
+
return typeof source === "function" ? source() : source;
|
|
442
|
+
}
|
|
362
443
|
var courseStartedTrackingFlights = /* @__PURE__ */ new Map();
|
|
444
|
+
var courseStartedEmitFlights = /* @__PURE__ */ new Map();
|
|
363
445
|
function isTrackingActive(tracking) {
|
|
364
446
|
return tracking?.enabled !== false;
|
|
365
447
|
}
|
|
366
448
|
function isCourseStartedSinkSettled(result) {
|
|
367
|
-
return result === "emitted";
|
|
449
|
+
return result === "emitted" || result === "filtered";
|
|
450
|
+
}
|
|
451
|
+
async function deliverToTracking(client, event) {
|
|
452
|
+
if (client.deliver) {
|
|
453
|
+
return client.deliver(event);
|
|
454
|
+
}
|
|
455
|
+
client.track(event);
|
|
456
|
+
const flushed = await client.flush?.();
|
|
457
|
+
if (flushed === false) return false;
|
|
458
|
+
if (flushed === true) return true;
|
|
459
|
+
return false;
|
|
368
460
|
}
|
|
369
461
|
function buildCourseStartedEvent(opts) {
|
|
370
462
|
const pluginCtx = buildPluginContext({
|
|
@@ -389,8 +481,7 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
|
|
|
389
481
|
}
|
|
390
482
|
const existing = courseStartedTrackingFlights.get(flightKey);
|
|
391
483
|
if (existing) {
|
|
392
|
-
|
|
393
|
-
if (settled) return true;
|
|
484
|
+
return existing;
|
|
394
485
|
}
|
|
395
486
|
let resolveFlight;
|
|
396
487
|
const flight = new Promise((resolve) => {
|
|
@@ -403,9 +494,13 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
|
|
|
403
494
|
resolveFlight(false);
|
|
404
495
|
return;
|
|
405
496
|
}
|
|
406
|
-
tracking
|
|
407
|
-
const delivered = await
|
|
408
|
-
if (
|
|
497
|
+
const client = resolveTrackingClient(tracking);
|
|
498
|
+
const delivered = await deliverToTracking(client, event);
|
|
499
|
+
if (shouldCommit && !shouldCommit()) {
|
|
500
|
+
resolveFlight(false);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (!delivered) {
|
|
409
504
|
resolveFlight(false);
|
|
410
505
|
return;
|
|
411
506
|
}
|
|
@@ -417,7 +512,9 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
|
|
|
417
512
|
} catch {
|
|
418
513
|
resolveFlight(false);
|
|
419
514
|
} finally {
|
|
420
|
-
courseStartedTrackingFlights.
|
|
515
|
+
if (courseStartedTrackingFlights.get(flightKey) === flight) {
|
|
516
|
+
courseStartedTrackingFlights.delete(flightKey);
|
|
517
|
+
}
|
|
421
518
|
}
|
|
422
519
|
})();
|
|
423
520
|
return flight;
|
|
@@ -497,12 +594,55 @@ async function emitCourseStartedToTrackingOnly(opts) {
|
|
|
497
594
|
}
|
|
498
595
|
}
|
|
499
596
|
async function emitPendingCourseStarted(opts) {
|
|
597
|
+
const flightKey = `${opts.sessionId}:${opts.courseId}`;
|
|
598
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
599
|
+
const existing = courseStartedEmitFlights.get(flightKey);
|
|
600
|
+
const flight = existing ?? startPendingCourseStartedFlight(opts, flightKey);
|
|
601
|
+
const result = await flight;
|
|
602
|
+
if (result !== "failed") return result;
|
|
603
|
+
const sessionStarted = (0, import_core5.hasCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
|
|
604
|
+
const trackingEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
|
|
605
|
+
opts.storage,
|
|
606
|
+
opts.sessionId,
|
|
607
|
+
opts.courseId
|
|
608
|
+
);
|
|
609
|
+
const pipelineDelivered = (0, import_core5.hasCourseStartedPipelineDelivered)(
|
|
610
|
+
opts.storage,
|
|
611
|
+
opts.sessionId,
|
|
612
|
+
opts.courseId
|
|
613
|
+
);
|
|
614
|
+
if (sessionStarted && trackingEmitted && pipelineDelivered) {
|
|
615
|
+
return "emitted";
|
|
616
|
+
}
|
|
617
|
+
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
618
|
+
}
|
|
619
|
+
return "failed";
|
|
620
|
+
}
|
|
621
|
+
function startPendingCourseStartedFlight(opts, flightKey) {
|
|
622
|
+
const flight = emitPendingCourseStartedInner(opts);
|
|
623
|
+
courseStartedEmitFlights.set(flightKey, flight);
|
|
624
|
+
void flight.finally(() => {
|
|
625
|
+
if (courseStartedEmitFlights.get(flightKey) === flight) {
|
|
626
|
+
courseStartedEmitFlights.delete(flightKey);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
return flight;
|
|
630
|
+
}
|
|
631
|
+
async function emitPendingCourseStartedInner(opts) {
|
|
500
632
|
const trackingEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
|
|
501
633
|
opts.storage,
|
|
502
634
|
opts.sessionId,
|
|
503
635
|
opts.courseId
|
|
504
636
|
);
|
|
505
637
|
const sessionStarted = (0, import_core5.hasCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
|
|
638
|
+
const pipelineDelivered = (0, import_core5.hasCourseStartedPipelineDelivered)(
|
|
639
|
+
opts.storage,
|
|
640
|
+
opts.sessionId,
|
|
641
|
+
opts.courseId
|
|
642
|
+
);
|
|
643
|
+
if (sessionStarted && trackingEmitted && pipelineDelivered) {
|
|
644
|
+
return "emitted";
|
|
645
|
+
}
|
|
506
646
|
if (sessionStarted && !trackingEmitted) {
|
|
507
647
|
return emitCourseStartedToTrackingOnly(opts);
|
|
508
648
|
}
|
|
@@ -514,14 +654,6 @@ async function emitPendingCourseStarted(opts) {
|
|
|
514
654
|
if (!trackingEmitted && !sessionStarted) {
|
|
515
655
|
return emitCourseStarted(opts);
|
|
516
656
|
}
|
|
517
|
-
const pipelineDelivered = (0, import_core5.hasCourseStartedPipelineDelivered)(
|
|
518
|
-
opts.storage,
|
|
519
|
-
opts.sessionId,
|
|
520
|
-
opts.courseId
|
|
521
|
-
);
|
|
522
|
-
if (sessionStarted && trackingEmitted && pipelineDelivered) {
|
|
523
|
-
return "emitted";
|
|
524
|
-
}
|
|
525
657
|
if (sessionStarted && trackingEmitted && !pipelineDelivered) {
|
|
526
658
|
const event = buildCourseStartedEvent(opts);
|
|
527
659
|
if (event === null) return "filtered";
|
|
@@ -532,7 +664,7 @@ async function emitPendingCourseStarted(opts) {
|
|
|
532
664
|
onXapiStatementSent: opts.onXapiStatementSent
|
|
533
665
|
});
|
|
534
666
|
}
|
|
535
|
-
return "
|
|
667
|
+
return "failed";
|
|
536
668
|
}
|
|
537
669
|
function assertTrackingSinkConfig(tracking) {
|
|
538
670
|
if (!tracking?.sink || !tracking?.batchSink) return;
|
|
@@ -580,6 +712,9 @@ function useLessonkitProviderRuntime(config) {
|
|
|
580
712
|
() => ({ ...config, courseId: normalizedCourseId }),
|
|
581
713
|
[config, normalizedCourseId]
|
|
582
714
|
);
|
|
715
|
+
if (shouldEnforceProductionGuard()) {
|
|
716
|
+
assertProductionCourseConfig(normalizedConfig);
|
|
717
|
+
}
|
|
583
718
|
const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
|
|
584
719
|
(0, import_react.useEffect)(() => {
|
|
585
720
|
if (useV2Runtime) return;
|
|
@@ -675,7 +810,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
675
810
|
}, []);
|
|
676
811
|
const activeLessonIdRef = (0, import_react.useRef)(progress.activeLessonId);
|
|
677
812
|
activeLessonIdRef.current = progress.activeLessonId;
|
|
678
|
-
const xapiQueueRef = (0, import_react.useRef)(createXapiQueueFromObservability(
|
|
813
|
+
const xapiQueueRef = (0, import_react.useRef)(createXapiQueueFromObservability(() => observabilityRef.current));
|
|
679
814
|
const xapiRef = (0, import_react.useRef)(null);
|
|
680
815
|
const [xapi, setXapi] = (0, import_react.useState)(null);
|
|
681
816
|
const prevXapiCourseIdRef = (0, import_react.useRef)(normalizedCourseId);
|
|
@@ -696,13 +831,17 @@ function useLessonkitProviderRuntime(config) {
|
|
|
696
831
|
}
|
|
697
832
|
void xapiRef.current?.flush();
|
|
698
833
|
}
|
|
699
|
-
xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
|
|
834
|
+
xapiQueueRef.current = createXapiQueueFromObservability(() => observabilityRef.current);
|
|
700
835
|
prevXapiCourseIdRef.current = courseId;
|
|
701
836
|
xapiCourseStartedSentOnClientRef.current = false;
|
|
702
837
|
xapiBootstrapSendRef.current = false;
|
|
703
838
|
}
|
|
704
839
|
const prev = xapiRef.current;
|
|
705
|
-
const next = createXapiClientFromConfig(
|
|
840
|
+
const next = createXapiClientFromConfig(
|
|
841
|
+
normalizedConfig,
|
|
842
|
+
xapiQueueRef.current,
|
|
843
|
+
observabilityRef.current
|
|
844
|
+
);
|
|
706
845
|
xapiRef.current = next;
|
|
707
846
|
setXapi(next);
|
|
708
847
|
let bootstrapSent = false;
|
|
@@ -759,7 +898,12 @@ function useLessonkitProviderRuntime(config) {
|
|
|
759
898
|
})();
|
|
760
899
|
return () => {
|
|
761
900
|
cancelled = true;
|
|
762
|
-
void
|
|
901
|
+
void (async () => {
|
|
902
|
+
try {
|
|
903
|
+
await prev?.flush();
|
|
904
|
+
} catch {
|
|
905
|
+
}
|
|
906
|
+
})();
|
|
763
907
|
};
|
|
764
908
|
}, [xapiEnabled, xapiClient, xapiTransport, courseId, trackingEnabled]);
|
|
765
909
|
const trackingRef = (0, import_react.useRef)((0, import_core8.createTrackingClient)());
|
|
@@ -823,13 +967,13 @@ function useLessonkitProviderRuntime(config) {
|
|
|
823
967
|
} else if (courseStartedFullySettled) {
|
|
824
968
|
courseStartedEmittedToSinkRef.current = true;
|
|
825
969
|
} else if (!courseStartedEmittedToSinkRef.current) {
|
|
826
|
-
const generation =
|
|
970
|
+
const generation = courseStartedEmitGenerationRef.current;
|
|
827
971
|
const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
|
|
828
972
|
void (async () => {
|
|
829
973
|
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
830
974
|
const result = await emitPendingCourseStarted({
|
|
831
975
|
pluginHost: pluginHostRef.current,
|
|
832
|
-
tracking:
|
|
976
|
+
tracking: () => trackingRef.current,
|
|
833
977
|
xapi: xapiRef.current,
|
|
834
978
|
storage: defaultStorage,
|
|
835
979
|
sessionId,
|
|
@@ -839,7 +983,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
839
983
|
lxpackBridge: lxpackBridgeModeRef.current,
|
|
840
984
|
onLxpackBridgeMiss,
|
|
841
985
|
extraSinks: extraSinksRef.current,
|
|
842
|
-
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
986
|
+
skipXapi: xapiCourseStartedSentOnClientRef.current || xapiBootstrapSendRef.current,
|
|
843
987
|
onXapiStatementSent: () => {
|
|
844
988
|
xapiCourseStartedSentOnClientRef.current = true;
|
|
845
989
|
},
|
|
@@ -850,7 +994,6 @@ function useLessonkitProviderRuntime(config) {
|
|
|
850
994
|
})();
|
|
851
995
|
}
|
|
852
996
|
return () => {
|
|
853
|
-
courseStartedEmitGenerationRef.current += 1;
|
|
854
997
|
if (prev !== trackingRef.current) {
|
|
855
998
|
void disposeTrackingClient(prev);
|
|
856
999
|
}
|
|
@@ -928,9 +1071,11 @@ function useLessonkitProviderRuntime(config) {
|
|
|
928
1071
|
} catch {
|
|
929
1072
|
}
|
|
930
1073
|
if (!courseStartedEmittedToSinkRef.current) {
|
|
1074
|
+
const generation = courseStartedEmitGenerationRef.current;
|
|
1075
|
+
const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
|
|
931
1076
|
const result = await emitPendingCourseStarted({
|
|
932
1077
|
pluginHost: pluginHostRef.current,
|
|
933
|
-
tracking: trackingRef.current,
|
|
1078
|
+
tracking: () => trackingRef.current,
|
|
934
1079
|
xapi: xapiRef.current,
|
|
935
1080
|
storage: defaultStorage,
|
|
936
1081
|
sessionId,
|
|
@@ -940,11 +1085,13 @@ function useLessonkitProviderRuntime(config) {
|
|
|
940
1085
|
lxpackBridge: lxpackBridgeModeRef.current,
|
|
941
1086
|
onLxpackBridgeMiss,
|
|
942
1087
|
extraSinks: extraSinksRef.current,
|
|
943
|
-
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
1088
|
+
skipXapi: xapiCourseStartedSentOnClientRef.current || xapiBootstrapSendRef.current,
|
|
944
1089
|
onXapiStatementSent: () => {
|
|
945
1090
|
xapiCourseStartedSentOnClientRef.current = true;
|
|
946
|
-
}
|
|
1091
|
+
},
|
|
1092
|
+
shouldCommit
|
|
947
1093
|
});
|
|
1094
|
+
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
948
1095
|
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
949
1096
|
}
|
|
950
1097
|
})();
|
|
@@ -1018,20 +1165,6 @@ function useLessonkitProviderRuntime(config) {
|
|
|
1018
1165
|
window.removeEventListener("pagehide", flushOnPageExit);
|
|
1019
1166
|
};
|
|
1020
1167
|
}, []);
|
|
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
|
-
]);
|
|
1035
1168
|
const setActiveLesson = (0, import_react.useCallback)(
|
|
1036
1169
|
(lessonId) => {
|
|
1037
1170
|
if (useV2Runtime && headlessRef.current) {
|
|
@@ -3062,6 +3195,8 @@ function useCompoundPersistence(opts) {
|
|
|
3062
3195
|
}, [ctx, opts.index, opts.pageCount]);
|
|
3063
3196
|
const buildStateRef = (0, import_react21.useRef)(buildState);
|
|
3064
3197
|
buildStateRef.current = buildState;
|
|
3198
|
+
const transformStateRef = (0, import_react21.useRef)(opts.transformState);
|
|
3199
|
+
transformStateRef.current = opts.transformState;
|
|
3065
3200
|
const persistNowRef = (0, import_react21.useRef)(() => {
|
|
3066
3201
|
});
|
|
3067
3202
|
const finalizeHydration = (0, import_react21.useCallback)(
|
|
@@ -3127,7 +3262,9 @@ function useCompoundPersistence(opts) {
|
|
|
3127
3262
|
const persistNow = (0, import_react21.useCallback)(() => {
|
|
3128
3263
|
if (!opts.enabled || !opts.courseId) return;
|
|
3129
3264
|
if (skipSaveUntilHydratedRef.current) return;
|
|
3130
|
-
|
|
3265
|
+
const built = buildStateRef.current();
|
|
3266
|
+
const state = transformStateRef.current ? transformStateRef.current(built) : built;
|
|
3267
|
+
saveResume(state);
|
|
3131
3268
|
}, [opts.enabled, opts.courseId, saveResume]);
|
|
3132
3269
|
(0, import_react21.useEffect)(() => {
|
|
3133
3270
|
persistNowRef.current = persistNow;
|
|
@@ -3183,7 +3320,8 @@ function useCompoundShell(opts) {
|
|
|
3183
3320
|
index: opts.index,
|
|
3184
3321
|
setIndex: opts.setIndex,
|
|
3185
3322
|
enabled: opts.persistEnabled,
|
|
3186
|
-
storage: opts.storage
|
|
3323
|
+
storage: opts.storage,
|
|
3324
|
+
transformState: opts.transformState
|
|
3187
3325
|
});
|
|
3188
3326
|
const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
|
|
3189
3327
|
const visibleIndex = (0, import_core15.clampCompoundPageIndex)(opts.index, opts.pageCount);
|
|
@@ -3238,6 +3376,8 @@ var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
|
3238
3376
|
"InteractiveBook",
|
|
3239
3377
|
"Slide",
|
|
3240
3378
|
"SlideDeck",
|
|
3379
|
+
"TimedCue",
|
|
3380
|
+
"InteractiveVideo",
|
|
3241
3381
|
"AssessmentSequence"
|
|
3242
3382
|
]);
|
|
3243
3383
|
function warnOrThrow(msg, strict) {
|
|
@@ -3466,14 +3606,40 @@ function Image(props) {
|
|
|
3466
3606
|
}
|
|
3467
3607
|
setLessonkitBlockType(Image, "Image");
|
|
3468
3608
|
|
|
3469
|
-
// src/blocks/
|
|
3609
|
+
// src/blocks/Video.tsx
|
|
3470
3610
|
var import_react26 = require("react");
|
|
3471
3611
|
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
3612
|
+
function Video(props) {
|
|
3613
|
+
const blockId = (0, import_react26.useMemo)(
|
|
3614
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3615
|
+
[props.blockId]
|
|
3616
|
+
);
|
|
3617
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("section", { "aria-label": props.title ?? "Video", "data-lk-block-id": blockId, "data-testid": "video", children: [
|
|
3618
|
+
props.title ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h3", { "data-testid": "video-title", children: props.title }) : null,
|
|
3619
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3620
|
+
"video",
|
|
3621
|
+
{
|
|
3622
|
+
controls: true,
|
|
3623
|
+
preload: "metadata",
|
|
3624
|
+
poster: props.poster,
|
|
3625
|
+
src: props.src,
|
|
3626
|
+
"data-testid": "video-player",
|
|
3627
|
+
style: { maxWidth: "100%" },
|
|
3628
|
+
children: props.captions ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("track", { kind: "captions", src: props.captions, srcLang: "en", label: "Captions", default: true }) : null
|
|
3629
|
+
}
|
|
3630
|
+
)
|
|
3631
|
+
] });
|
|
3632
|
+
}
|
|
3633
|
+
setLessonkitBlockType(Video, "Video");
|
|
3634
|
+
|
|
3635
|
+
// src/blocks/Page.tsx
|
|
3636
|
+
var import_react27 = require("react");
|
|
3637
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
3472
3638
|
function Page(props) {
|
|
3473
3639
|
validateCompoundChildren("Page", props.children);
|
|
3474
3640
|
const { track } = useLessonkit();
|
|
3475
3641
|
const lessonId = useEnclosingLessonId();
|
|
3476
|
-
(0,
|
|
3642
|
+
(0, import_react27.useEffect)(() => {
|
|
3477
3643
|
if (props.hidden || !lessonId || props.parentType) return;
|
|
3478
3644
|
track(
|
|
3479
3645
|
"compound_page_viewed",
|
|
@@ -3485,7 +3651,7 @@ function Page(props) {
|
|
|
3485
3651
|
{ lessonId }
|
|
3486
3652
|
);
|
|
3487
3653
|
}, [props.hidden, props.pageIndex, props.parentType, props.blockId, lessonId, track]);
|
|
3488
|
-
return /* @__PURE__ */ (0,
|
|
3654
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
|
|
3489
3655
|
"section",
|
|
3490
3656
|
{
|
|
3491
3657
|
"aria-label": props.title ?? "Page",
|
|
@@ -3493,8 +3659,8 @@ function Page(props) {
|
|
|
3493
3659
|
"data-testid": `page-${props.blockId}`,
|
|
3494
3660
|
hidden: props.hidden ? true : void 0,
|
|
3495
3661
|
children: [
|
|
3496
|
-
props.title ? /* @__PURE__ */ (0,
|
|
3497
|
-
/* @__PURE__ */ (0,
|
|
3662
|
+
props.title ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { children: props.title }) : null,
|
|
3663
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(CompoundPageIndexProvider, { pageIndex: props.pageIndex ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { children: props.children }) })
|
|
3498
3664
|
]
|
|
3499
3665
|
}
|
|
3500
3666
|
);
|
|
@@ -3502,9 +3668,9 @@ function Page(props) {
|
|
|
3502
3668
|
setLessonkitBlockType(Page, "Page");
|
|
3503
3669
|
|
|
3504
3670
|
// src/blocks/InteractiveBook.tsx
|
|
3505
|
-
var
|
|
3506
|
-
var
|
|
3507
|
-
var InteractiveBookInner = (0,
|
|
3671
|
+
var import_react28 = __toESM(require("react"), 1);
|
|
3672
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
3673
|
+
var InteractiveBookInner = (0, import_react28.forwardRef)(
|
|
3508
3674
|
function InteractiveBookInner2(props, ref) {
|
|
3509
3675
|
const { blockId, pages, index, setIndex, persistEnabled } = props;
|
|
3510
3676
|
validateCompoundChildren("InteractiveBook", pages);
|
|
@@ -3519,11 +3685,11 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
|
|
|
3519
3685
|
persistEnabled,
|
|
3520
3686
|
ref
|
|
3521
3687
|
});
|
|
3522
|
-
const pageTitles = (0,
|
|
3688
|
+
const pageTitles = (0, import_react28.useMemo)(
|
|
3523
3689
|
() => pages.map((page) => page.props.title),
|
|
3524
3690
|
[pages]
|
|
3525
3691
|
);
|
|
3526
|
-
(0,
|
|
3692
|
+
(0, import_react28.useEffect)(() => {
|
|
3527
3693
|
if (!lessonId || pages.length === 0) return;
|
|
3528
3694
|
track(
|
|
3529
3695
|
"book_page_viewed",
|
|
@@ -3535,31 +3701,31 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
|
|
|
3535
3701
|
{ lessonId }
|
|
3536
3702
|
);
|
|
3537
3703
|
}, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
|
|
3538
|
-
return /* @__PURE__ */ (0,
|
|
3539
|
-
/* @__PURE__ */ (0,
|
|
3540
|
-
/* @__PURE__ */ (0,
|
|
3704
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
|
|
3705
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h3", { children: props.title }),
|
|
3706
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("p", { children: [
|
|
3541
3707
|
"Page ",
|
|
3542
3708
|
progress.current,
|
|
3543
3709
|
" of ",
|
|
3544
3710
|
progress.total
|
|
3545
3711
|
] }),
|
|
3546
|
-
props.showBookScore && ctx ? /* @__PURE__ */ (0,
|
|
3712
|
+
props.showBookScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("p", { "data-testid": "book-score", children: [
|
|
3547
3713
|
"Score: ",
|
|
3548
3714
|
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
3549
3715
|
" /",
|
|
3550
3716
|
" ",
|
|
3551
3717
|
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
3552
3718
|
] }) : null,
|
|
3553
|
-
/* @__PURE__ */ (0,
|
|
3554
|
-
(page, i) =>
|
|
3719
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { "data-testid": "interactive-book-page", children: pages.map(
|
|
3720
|
+
(page, i) => import_react28.default.cloneElement(page, {
|
|
3555
3721
|
key: page.key ?? page.props.blockId,
|
|
3556
3722
|
hidden: i !== visibleIndex,
|
|
3557
3723
|
pageIndex: i,
|
|
3558
3724
|
parentType: "InteractiveBook"
|
|
3559
3725
|
})
|
|
3560
3726
|
) }),
|
|
3561
|
-
/* @__PURE__ */ (0,
|
|
3562
|
-
/* @__PURE__ */ (0,
|
|
3727
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("nav", { "aria-label": "Book navigation", children: [
|
|
3728
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3563
3729
|
"button",
|
|
3564
3730
|
{
|
|
3565
3731
|
type: "button",
|
|
@@ -3569,7 +3735,7 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
|
|
|
3569
3735
|
children: "Previous"
|
|
3570
3736
|
}
|
|
3571
3737
|
),
|
|
3572
|
-
/* @__PURE__ */ (0,
|
|
3738
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3573
3739
|
"button",
|
|
3574
3740
|
{
|
|
3575
3741
|
type: "button",
|
|
@@ -3583,13 +3749,13 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
|
|
|
3583
3749
|
] });
|
|
3584
3750
|
}
|
|
3585
3751
|
);
|
|
3586
|
-
var InteractiveBook = (0,
|
|
3587
|
-
const blockId = (0,
|
|
3752
|
+
var InteractiveBook = (0, import_react28.forwardRef)(function InteractiveBook2(props, ref) {
|
|
3753
|
+
const blockId = (0, import_react28.useMemo)(
|
|
3588
3754
|
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3589
3755
|
[props.blockId]
|
|
3590
3756
|
);
|
|
3591
|
-
const pages =
|
|
3592
|
-
|
|
3757
|
+
const pages = import_react28.default.Children.toArray(props.children).filter(
|
|
3758
|
+
import_react28.default.isValidElement
|
|
3593
3759
|
);
|
|
3594
3760
|
const { config, storage } = useLessonkit();
|
|
3595
3761
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
@@ -3600,12 +3766,12 @@ var InteractiveBook = (0, import_react27.forwardRef)(function InteractiveBook2(p
|
|
|
3600
3766
|
persistEnabled,
|
|
3601
3767
|
storage
|
|
3602
3768
|
});
|
|
3603
|
-
const [index, setIndex] = (0,
|
|
3604
|
-
const setIndexStable = (0,
|
|
3605
|
-
(0,
|
|
3769
|
+
const [index, setIndex] = (0, import_react28.useState)(initialIndex);
|
|
3770
|
+
const setIndexStable = (0, import_react28.useCallback)((i) => setIndex(i), []);
|
|
3771
|
+
(0, import_react28.useEffect)(() => {
|
|
3606
3772
|
setIndex(initialIndex);
|
|
3607
3773
|
}, [config.courseId, blockId, initialIndex]);
|
|
3608
|
-
return /* @__PURE__ */ (0,
|
|
3774
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3609
3775
|
InteractiveBookInner,
|
|
3610
3776
|
{
|
|
3611
3777
|
...props,
|
|
@@ -3621,13 +3787,13 @@ var InteractiveBook = (0, import_react27.forwardRef)(function InteractiveBook2(p
|
|
|
3621
3787
|
setLessonkitBlockType(InteractiveBook, "InteractiveBook");
|
|
3622
3788
|
|
|
3623
3789
|
// src/blocks/Slide.tsx
|
|
3624
|
-
var
|
|
3625
|
-
var
|
|
3790
|
+
var import_react29 = require("react");
|
|
3791
|
+
var import_jsx_runtime20 = require("react/jsx-runtime");
|
|
3626
3792
|
function Slide(props) {
|
|
3627
3793
|
validateCompoundChildren("Slide", props.children);
|
|
3628
3794
|
const { track } = useLessonkit();
|
|
3629
3795
|
const lessonId = useEnclosingLessonId();
|
|
3630
|
-
(0,
|
|
3796
|
+
(0, import_react29.useEffect)(() => {
|
|
3631
3797
|
if (props.hidden || !lessonId || props.parentType) return;
|
|
3632
3798
|
track(
|
|
3633
3799
|
"compound_page_viewed",
|
|
@@ -3639,7 +3805,7 @@ function Slide(props) {
|
|
|
3639
3805
|
{ lessonId }
|
|
3640
3806
|
);
|
|
3641
3807
|
}, [props.hidden, props.slideIndex, props.parentType, props.blockId, lessonId, track]);
|
|
3642
|
-
return /* @__PURE__ */ (0,
|
|
3808
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
|
|
3643
3809
|
"section",
|
|
3644
3810
|
{
|
|
3645
3811
|
"aria-label": props.title ?? "Slide",
|
|
@@ -3647,8 +3813,8 @@ function Slide(props) {
|
|
|
3647
3813
|
"data-testid": `slide-${props.blockId}`,
|
|
3648
3814
|
hidden: props.hidden ? true : void 0,
|
|
3649
3815
|
children: [
|
|
3650
|
-
props.title ? /* @__PURE__ */ (0,
|
|
3651
|
-
/* @__PURE__ */ (0,
|
|
3816
|
+
props.title ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h3", { children: props.title }) : null,
|
|
3817
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CompoundPageIndexProvider, { pageIndex: props.slideIndex ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { children: props.children }) })
|
|
3652
3818
|
]
|
|
3653
3819
|
}
|
|
3654
3820
|
);
|
|
@@ -3656,10 +3822,10 @@ function Slide(props) {
|
|
|
3656
3822
|
setLessonkitBlockType(Slide, "Slide");
|
|
3657
3823
|
|
|
3658
3824
|
// src/blocks/SlideDeck.tsx
|
|
3659
|
-
var
|
|
3825
|
+
var import_react31 = __toESM(require("react"), 1);
|
|
3660
3826
|
|
|
3661
3827
|
// src/compound/useCompoundKeyboardNav.ts
|
|
3662
|
-
var
|
|
3828
|
+
var import_react30 = require("react");
|
|
3663
3829
|
var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT", "BUTTON"]);
|
|
3664
3830
|
function isEditableTarget(target) {
|
|
3665
3831
|
if (!(target instanceof HTMLElement)) return false;
|
|
@@ -3672,7 +3838,7 @@ function isEditableTarget(target) {
|
|
|
3672
3838
|
}
|
|
3673
3839
|
function useCompoundKeyboardNav(opts) {
|
|
3674
3840
|
const { containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex } = opts;
|
|
3675
|
-
(0,
|
|
3841
|
+
(0, import_react30.useEffect)(() => {
|
|
3676
3842
|
const el = containerRef.current;
|
|
3677
3843
|
if (!el || pageCount === 0) return;
|
|
3678
3844
|
const onKeyDown = (event) => {
|
|
@@ -3717,13 +3883,13 @@ function useCompoundKeyboardNav(opts) {
|
|
|
3717
3883
|
}
|
|
3718
3884
|
|
|
3719
3885
|
// src/blocks/SlideDeck.tsx
|
|
3720
|
-
var
|
|
3721
|
-
var SlideDeckInner = (0,
|
|
3886
|
+
var import_jsx_runtime21 = require("react/jsx-runtime");
|
|
3887
|
+
var SlideDeckInner = (0, import_react31.forwardRef)(function SlideDeckInner2(props, ref) {
|
|
3722
3888
|
const { blockId, slides, index, setIndex, persistEnabled } = props;
|
|
3723
3889
|
validateCompoundChildren("SlideDeck", slides);
|
|
3724
3890
|
const { config, track } = useLessonkit();
|
|
3725
3891
|
const lessonId = useEnclosingLessonId();
|
|
3726
|
-
const containerRef = (0,
|
|
3892
|
+
const containerRef = (0, import_react31.useRef)(null);
|
|
3727
3893
|
const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
|
|
3728
3894
|
courseId: config.courseId,
|
|
3729
3895
|
compoundId: blockId,
|
|
@@ -3733,7 +3899,7 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
|
|
|
3733
3899
|
persistEnabled,
|
|
3734
3900
|
ref
|
|
3735
3901
|
});
|
|
3736
|
-
const setIndexStable = (0,
|
|
3902
|
+
const setIndexStable = (0, import_react31.useCallback)((i) => setIndex(i), [setIndex]);
|
|
3737
3903
|
useCompoundKeyboardNav({
|
|
3738
3904
|
containerRef,
|
|
3739
3905
|
visibleIndex,
|
|
@@ -3742,11 +3908,11 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
|
|
|
3742
3908
|
goPrev,
|
|
3743
3909
|
setIndex: setIndexStable
|
|
3744
3910
|
});
|
|
3745
|
-
const slideTitles = (0,
|
|
3911
|
+
const slideTitles = (0, import_react31.useMemo)(
|
|
3746
3912
|
() => slides.map((slide) => slide.props.title),
|
|
3747
3913
|
[slides]
|
|
3748
3914
|
);
|
|
3749
|
-
(0,
|
|
3915
|
+
(0, import_react31.useEffect)(() => {
|
|
3750
3916
|
if (!lessonId || slides.length === 0) return;
|
|
3751
3917
|
track(
|
|
3752
3918
|
"slide_viewed",
|
|
@@ -3758,7 +3924,7 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
|
|
|
3758
3924
|
{ lessonId }
|
|
3759
3925
|
);
|
|
3760
3926
|
}, [visibleIndex, blockId, lessonId, slides.length, slideTitles, track]);
|
|
3761
|
-
return /* @__PURE__ */ (0,
|
|
3927
|
+
return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
|
|
3762
3928
|
"section",
|
|
3763
3929
|
{
|
|
3764
3930
|
ref: containerRef,
|
|
@@ -3767,30 +3933,30 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
|
|
|
3767
3933
|
"data-testid": "slide-deck",
|
|
3768
3934
|
"data-lk-block-id": blockId,
|
|
3769
3935
|
children: [
|
|
3770
|
-
/* @__PURE__ */ (0,
|
|
3771
|
-
/* @__PURE__ */ (0,
|
|
3936
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h3", { children: props.title }),
|
|
3937
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("p", { children: [
|
|
3772
3938
|
"Slide ",
|
|
3773
3939
|
progress.current,
|
|
3774
3940
|
" of ",
|
|
3775
3941
|
progress.total
|
|
3776
3942
|
] }),
|
|
3777
|
-
props.showDeckScore && ctx ? /* @__PURE__ */ (0,
|
|
3943
|
+
props.showDeckScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("p", { "data-testid": "deck-score", children: [
|
|
3778
3944
|
"Score: ",
|
|
3779
3945
|
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
3780
3946
|
" /",
|
|
3781
3947
|
" ",
|
|
3782
3948
|
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
3783
3949
|
] }) : null,
|
|
3784
|
-
/* @__PURE__ */ (0,
|
|
3785
|
-
(slide, i) =>
|
|
3950
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { "data-testid": "slide-deck-slide", children: slides.map(
|
|
3951
|
+
(slide, i) => import_react31.default.cloneElement(slide, {
|
|
3786
3952
|
key: slide.key ?? slide.props.blockId,
|
|
3787
3953
|
hidden: i !== visibleIndex,
|
|
3788
3954
|
slideIndex: i,
|
|
3789
3955
|
parentType: "SlideDeck"
|
|
3790
3956
|
})
|
|
3791
3957
|
) }),
|
|
3792
|
-
/* @__PURE__ */ (0,
|
|
3793
|
-
/* @__PURE__ */ (0,
|
|
3958
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("nav", { "aria-label": "Slide navigation", children: [
|
|
3959
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3794
3960
|
"button",
|
|
3795
3961
|
{
|
|
3796
3962
|
type: "button",
|
|
@@ -3800,7 +3966,7 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
|
|
|
3800
3966
|
children: "Previous slide"
|
|
3801
3967
|
}
|
|
3802
3968
|
),
|
|
3803
|
-
/* @__PURE__ */ (0,
|
|
3969
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3804
3970
|
"button",
|
|
3805
3971
|
{
|
|
3806
3972
|
type: "button",
|
|
@@ -3815,13 +3981,13 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
|
|
|
3815
3981
|
}
|
|
3816
3982
|
);
|
|
3817
3983
|
});
|
|
3818
|
-
var SlideDeck = (0,
|
|
3819
|
-
const blockId = (0,
|
|
3984
|
+
var SlideDeck = (0, import_react31.forwardRef)(function SlideDeck2(props, ref) {
|
|
3985
|
+
const blockId = (0, import_react31.useMemo)(
|
|
3820
3986
|
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3821
3987
|
[props.blockId]
|
|
3822
3988
|
);
|
|
3823
|
-
const slides =
|
|
3824
|
-
|
|
3989
|
+
const slides = import_react31.default.Children.toArray(props.children).filter(
|
|
3990
|
+
import_react31.default.isValidElement
|
|
3825
3991
|
);
|
|
3826
3992
|
const { config, storage } = useLessonkit();
|
|
3827
3993
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
@@ -3832,12 +3998,12 @@ var SlideDeck = (0, import_react30.forwardRef)(function SlideDeck2(props, ref) {
|
|
|
3832
3998
|
persistEnabled,
|
|
3833
3999
|
storage
|
|
3834
4000
|
});
|
|
3835
|
-
const [index, setIndex] = (0,
|
|
3836
|
-
const setIndexStable = (0,
|
|
3837
|
-
(0,
|
|
4001
|
+
const [index, setIndex] = (0, import_react31.useState)(initialIndex);
|
|
4002
|
+
const setIndexStable = (0, import_react31.useCallback)((i) => setIndex(i), []);
|
|
4003
|
+
(0, import_react31.useEffect)(() => {
|
|
3838
4004
|
setIndex(initialIndex);
|
|
3839
4005
|
}, [config.courseId, blockId, initialIndex]);
|
|
3840
|
-
return /* @__PURE__ */ (0,
|
|
4006
|
+
return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3841
4007
|
SlideDeckInner,
|
|
3842
4008
|
{
|
|
3843
4009
|
...props,
|
|
@@ -3852,95 +4018,1698 @@ var SlideDeck = (0, import_react30.forwardRef)(function SlideDeck2(props, ref) {
|
|
|
3852
4018
|
});
|
|
3853
4019
|
setLessonkitBlockType(SlideDeck, "SlideDeck");
|
|
3854
4020
|
|
|
3855
|
-
// src/blocks/
|
|
3856
|
-
var
|
|
3857
|
-
var
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
const
|
|
3863
|
-
|
|
4021
|
+
// src/blocks/TimedCue.tsx
|
|
4022
|
+
var import_react32 = __toESM(require("react"), 1);
|
|
4023
|
+
var import_accessibility3 = require("@lessonkit/accessibility");
|
|
4024
|
+
var import_jsx_runtime22 = require("react/jsx-runtime");
|
|
4025
|
+
function TimedCue(props) {
|
|
4026
|
+
validateCompoundChildren("TimedCue", props.children, true);
|
|
4027
|
+
const child = import_react32.default.Children.only(props.children);
|
|
4028
|
+
const overlayRef = (0, import_react32.useRef)(null);
|
|
4029
|
+
(0, import_react32.useEffect)(() => {
|
|
4030
|
+
if (props.hidden || !overlayRef.current) return;
|
|
4031
|
+
const trap = (0, import_accessibility3.trapFocus)(overlayRef.current, { restoreFocus: false });
|
|
4032
|
+
trap.activate();
|
|
4033
|
+
const firstFocusable = overlayRef.current.querySelector(
|
|
4034
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
4035
|
+
);
|
|
4036
|
+
firstFocusable?.focus();
|
|
4037
|
+
return () => trap.deactivate();
|
|
4038
|
+
}, [props.hidden, props.cueIndex]);
|
|
4039
|
+
return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
|
|
4040
|
+
"div",
|
|
4041
|
+
{
|
|
4042
|
+
ref: overlayRef,
|
|
4043
|
+
role: "dialog",
|
|
4044
|
+
"aria-modal": props.hidden ? void 0 : true,
|
|
4045
|
+
"aria-hidden": props.hidden ? true : void 0,
|
|
4046
|
+
hidden: props.hidden ? true : void 0,
|
|
4047
|
+
"aria-label": props.label ?? `Interaction at ${props.atSeconds} seconds`,
|
|
4048
|
+
"data-testid": `timed-cue-${props.cueIndex ?? 0}`,
|
|
4049
|
+
"data-lk-cue-at": props.atSeconds,
|
|
4050
|
+
className: "lk-timed-cue-overlay",
|
|
4051
|
+
style: {
|
|
4052
|
+
position: "relative",
|
|
4053
|
+
zIndex: 2,
|
|
4054
|
+
background: "var(--lk-surface, #fff)",
|
|
4055
|
+
padding: "1rem",
|
|
4056
|
+
border: "1px solid var(--lk-border, #ccc)",
|
|
4057
|
+
marginTop: "0.5rem"
|
|
4058
|
+
},
|
|
4059
|
+
children: [
|
|
4060
|
+
props.hidden ? null : props.label ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { "data-testid": "timed-cue-label", children: props.label }) : null,
|
|
4061
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(CompoundPageIndexProvider, { pageIndex: props.cueIndex ?? 0, children: child })
|
|
4062
|
+
]
|
|
4063
|
+
}
|
|
4064
|
+
);
|
|
4065
|
+
}
|
|
4066
|
+
setLessonkitBlockType(TimedCue, "TimedCue");
|
|
4067
|
+
|
|
4068
|
+
// src/blocks/InteractiveVideo.tsx
|
|
4069
|
+
var import_react33 = __toESM(require("react"), 1);
|
|
4070
|
+
var import_core19 = require("@lessonkit/core");
|
|
4071
|
+
|
|
4072
|
+
// src/compound/useCompoundVideoShell.ts
|
|
4073
|
+
var import_core18 = require("@lessonkit/core");
|
|
4074
|
+
var IV_META_KEY = "__lk_iv__";
|
|
4075
|
+
function readInteractiveVideoMeta(childStates) {
|
|
4076
|
+
const raw = childStates[IV_META_KEY];
|
|
4077
|
+
if (!raw || typeof raw !== "object") return null;
|
|
4078
|
+
const currentTime = typeof raw.currentTime === "number" ? raw.currentTime : 0;
|
|
4079
|
+
const completedCueIndices = Array.isArray(raw.completedCueIndices) ? raw.completedCueIndices.filter((n) => typeof n === "number") : [];
|
|
4080
|
+
return { currentTime, completedCueIndices };
|
|
4081
|
+
}
|
|
4082
|
+
function mergeVideoMetaIntoState(state, meta) {
|
|
4083
|
+
return {
|
|
4084
|
+
...state,
|
|
4085
|
+
childStates: {
|
|
4086
|
+
...state.childStates,
|
|
4087
|
+
[IV_META_KEY]: meta
|
|
4088
|
+
}
|
|
4089
|
+
};
|
|
4090
|
+
}
|
|
4091
|
+
|
|
4092
|
+
// src/blocks/InteractiveVideo.tsx
|
|
4093
|
+
var import_jsx_runtime23 = require("react/jsx-runtime");
|
|
4094
|
+
function loadVideoMeta(storage, courseId, blockId, enabled) {
|
|
4095
|
+
if (!enabled || !courseId) return { currentTime: 0, completedCueIndices: [] };
|
|
4096
|
+
const saved = (0, import_core19.loadCompoundState)(storage, courseId, blockId);
|
|
4097
|
+
if (!saved) return { currentTime: 0, completedCueIndices: [] };
|
|
4098
|
+
const meta = readInteractiveVideoMeta(saved.childStates);
|
|
4099
|
+
return meta ?? { currentTime: 0, completedCueIndices: [] };
|
|
4100
|
+
}
|
|
4101
|
+
function getCueChildCheckId(cue) {
|
|
4102
|
+
const child = import_react33.default.Children.only(cue.props.children);
|
|
4103
|
+
if (!import_react33.default.isValidElement(child)) return null;
|
|
4104
|
+
const props = child.props;
|
|
4105
|
+
if (typeof props.checkId !== "string") return null;
|
|
4106
|
+
return normalizeComponentId(props.checkId, "checkId");
|
|
4107
|
+
}
|
|
4108
|
+
function cueRequiresAnswer(cue) {
|
|
4109
|
+
return Boolean(cue.props.mustComplete && getCueChildCheckId(cue));
|
|
4110
|
+
}
|
|
4111
|
+
var InteractiveVideoInner = (0, import_react33.forwardRef)(function InteractiveVideoInner2(props, ref) {
|
|
4112
|
+
const { blockId, cues, index, setIndex, persistEnabled, initialMeta } = props;
|
|
4113
|
+
validateCompoundChildren("InteractiveVideo", cues);
|
|
4114
|
+
const { config, track, storage } = useLessonkit();
|
|
3864
4115
|
const lessonId = useEnclosingLessonId();
|
|
3865
|
-
const
|
|
3866
|
-
const
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
4116
|
+
const videoRef = (0, import_react33.useRef)(null);
|
|
4117
|
+
const completedCuesRef = (0, import_react33.useRef)(new Set(initialMeta.completedCueIndices));
|
|
4118
|
+
const [completedCues, setCompletedCues] = (0, import_react33.useState)(
|
|
4119
|
+
() => new Set(initialMeta.completedCueIndices)
|
|
4120
|
+
);
|
|
4121
|
+
const [overlayActive, setOverlayActive] = (0, import_react33.useState)(false);
|
|
4122
|
+
const firedCuesRef = (0, import_react33.useRef)(new Set(initialMeta.completedCueIndices));
|
|
4123
|
+
const resumeOverlayCheckedRef = (0, import_react33.useRef)(false);
|
|
4124
|
+
const sortedCues = (0, import_react33.useMemo)(
|
|
4125
|
+
() => [...cues].sort((a, b) => (a.props.atSeconds ?? 0) - (b.props.atSeconds ?? 0)),
|
|
4126
|
+
[cues]
|
|
4127
|
+
);
|
|
4128
|
+
(0, import_react33.useEffect)(() => {
|
|
4129
|
+
completedCuesRef.current = completedCues;
|
|
4130
|
+
}, [completedCues]);
|
|
4131
|
+
const transformState = (0, import_react33.useCallback)(
|
|
4132
|
+
(state) => mergeVideoMetaIntoState(state, {
|
|
4133
|
+
currentTime: videoRef.current?.currentTime ?? initialMeta.currentTime,
|
|
4134
|
+
completedCueIndices: [...completedCuesRef.current]
|
|
4135
|
+
}),
|
|
4136
|
+
[initialMeta.currentTime]
|
|
4137
|
+
);
|
|
4138
|
+
const { ctx } = useCompoundShell({
|
|
4139
|
+
courseId: config.courseId,
|
|
4140
|
+
compoundId: blockId,
|
|
4141
|
+
pageCount: sortedCues.length,
|
|
4142
|
+
index,
|
|
4143
|
+
setIndex,
|
|
4144
|
+
persistEnabled,
|
|
4145
|
+
ref,
|
|
4146
|
+
storage,
|
|
4147
|
+
transformState
|
|
4148
|
+
});
|
|
4149
|
+
const activeCue = sortedCues[index];
|
|
4150
|
+
const cueCanContinue = (0, import_react33.useCallback)(
|
|
4151
|
+
(cue) => {
|
|
4152
|
+
if (!cue || !cueRequiresAnswer(cue)) return true;
|
|
4153
|
+
const checkId = getCueChildCheckId(cue);
|
|
4154
|
+
if (!checkId) return true;
|
|
4155
|
+
const entry = ctx?.getRegisteredHandles().get(checkId);
|
|
4156
|
+
if (!entry) return false;
|
|
4157
|
+
return entry.handle.getAnswerGiven();
|
|
4158
|
+
},
|
|
4159
|
+
[ctx]
|
|
4160
|
+
);
|
|
4161
|
+
const canContinueActiveCue = cueCanContinue(activeCue);
|
|
4162
|
+
(0, import_react33.useEffect)(() => {
|
|
4163
|
+
const video = videoRef.current;
|
|
4164
|
+
if (!video || initialMeta.currentTime <= 0) return;
|
|
4165
|
+
video.currentTime = initialMeta.currentTime;
|
|
4166
|
+
}, [initialMeta.currentTime]);
|
|
4167
|
+
(0, import_react33.useEffect)(() => {
|
|
4168
|
+
if (resumeOverlayCheckedRef.current || sortedCues.length === 0) return;
|
|
4169
|
+
resumeOverlayCheckedRef.current = true;
|
|
4170
|
+
const hasSavedProgress = initialMeta.currentTime > 0 || initialMeta.completedCueIndices.length > 0 || persistEnabled && config.courseId && (0, import_core19.loadCompoundState)(storage, config.courseId, blockId) !== null;
|
|
4171
|
+
if (!hasSavedProgress) return;
|
|
4172
|
+
const video = videoRef.current;
|
|
4173
|
+
if (!video) return;
|
|
4174
|
+
const cue = sortedCues[index];
|
|
4175
|
+
if (!cue || completedCues.has(index)) return;
|
|
4176
|
+
setOverlayActive(true);
|
|
4177
|
+
video.pause();
|
|
4178
|
+
const at = cue.props.atSeconds ?? 0;
|
|
4179
|
+
if (video.currentTime < at) {
|
|
4180
|
+
video.currentTime = at;
|
|
4181
|
+
}
|
|
4182
|
+
}, [
|
|
4183
|
+
blockId,
|
|
4184
|
+
completedCues,
|
|
4185
|
+
config.courseId,
|
|
4186
|
+
index,
|
|
4187
|
+
initialMeta.completedCueIndices.length,
|
|
4188
|
+
initialMeta.currentTime,
|
|
4189
|
+
persistEnabled,
|
|
4190
|
+
sortedCues,
|
|
4191
|
+
storage
|
|
4192
|
+
]);
|
|
4193
|
+
const mandatoryIncompleteBefore = (0, import_react33.useCallback)(
|
|
4194
|
+
(time) => {
|
|
4195
|
+
for (let i = 0; i < sortedCues.length; i++) {
|
|
4196
|
+
const cue = sortedCues[i];
|
|
4197
|
+
if ((cue.props.atSeconds ?? 0) >= time) break;
|
|
4198
|
+
if (cue.props.mustComplete && !completedCues.has(i)) return cue.props.atSeconds ?? 0;
|
|
4199
|
+
}
|
|
4200
|
+
return null;
|
|
4201
|
+
},
|
|
4202
|
+
[sortedCues, completedCues]
|
|
4203
|
+
);
|
|
4204
|
+
const activateCue = (0, import_react33.useCallback)(
|
|
4205
|
+
(i) => {
|
|
4206
|
+
const cue = sortedCues[i];
|
|
4207
|
+
if (!cue || firedCuesRef.current.has(i)) return;
|
|
4208
|
+
firedCuesRef.current.add(i);
|
|
4209
|
+
videoRef.current?.pause();
|
|
4210
|
+
setIndex(i);
|
|
4211
|
+
setOverlayActive(true);
|
|
4212
|
+
if (lessonId) {
|
|
4213
|
+
track(
|
|
4214
|
+
"video_cue_reached",
|
|
4215
|
+
{ blockId, cueIndex: i, atSeconds: cue.props.atSeconds ?? 0, cueLabel: cue.props.label },
|
|
4216
|
+
{ lessonId }
|
|
4217
|
+
);
|
|
4218
|
+
}
|
|
4219
|
+
},
|
|
4220
|
+
[blockId, lessonId, setIndex, sortedCues, track]
|
|
4221
|
+
);
|
|
4222
|
+
const onTimeUpdate = () => {
|
|
4223
|
+
const video = videoRef.current;
|
|
4224
|
+
if (!video || overlayActive) return;
|
|
4225
|
+
const t = video.currentTime;
|
|
4226
|
+
const blockSeek = mandatoryIncompleteBefore(t);
|
|
4227
|
+
if (blockSeek !== null && t > blockSeek + 0.5) {
|
|
4228
|
+
video.currentTime = blockSeek;
|
|
4229
|
+
return;
|
|
4230
|
+
}
|
|
4231
|
+
for (let i = 0; i < sortedCues.length; i++) {
|
|
4232
|
+
if (firedCuesRef.current.has(i)) continue;
|
|
4233
|
+
const at = sortedCues[i]?.props.atSeconds ?? 0;
|
|
4234
|
+
if (t >= at) {
|
|
4235
|
+
activateCue(i);
|
|
4236
|
+
break;
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
};
|
|
4240
|
+
const completeCue = () => {
|
|
4241
|
+
const cue = sortedCues[index];
|
|
4242
|
+
if (!cue || !cueCanContinue(cue)) return;
|
|
4243
|
+
setCompletedCues((prev) => {
|
|
4244
|
+
const next = /* @__PURE__ */ new Set([...prev, index]);
|
|
4245
|
+
completedCuesRef.current = next;
|
|
4246
|
+
return next;
|
|
4247
|
+
});
|
|
4248
|
+
setOverlayActive(false);
|
|
4249
|
+
if (lessonId) {
|
|
3872
4250
|
track(
|
|
3873
|
-
"
|
|
3874
|
-
{
|
|
3875
|
-
|
|
4251
|
+
"video_segment_completed",
|
|
4252
|
+
{
|
|
4253
|
+
blockId,
|
|
4254
|
+
segmentIndex: index,
|
|
4255
|
+
atSeconds: cue.props.atSeconds ?? 0,
|
|
4256
|
+
segmentLabel: cue.props.label
|
|
4257
|
+
},
|
|
4258
|
+
{ lessonId }
|
|
3876
4259
|
);
|
|
3877
|
-
|
|
4260
|
+
}
|
|
4261
|
+
videoRef.current?.play().catch(() => {
|
|
3878
4262
|
});
|
|
3879
4263
|
};
|
|
3880
|
-
return /* @__PURE__ */ (0,
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
"aria-controls": panelId,
|
|
3892
|
-
"data-testid": `accordion-trigger-${section.id}`,
|
|
3893
|
-
onClick: () => toggle(section.id),
|
|
3894
|
-
children: section.title
|
|
3895
|
-
}
|
|
3896
|
-
) }),
|
|
3897
|
-
expanded ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
|
|
3898
|
-
] }, section.id);
|
|
3899
|
-
}) });
|
|
3900
|
-
}
|
|
3901
|
-
setLessonkitBlockType(Accordion, "Accordion");
|
|
3902
|
-
|
|
3903
|
-
// src/blocks/DialogCards.tsx
|
|
3904
|
-
var import_react32 = require("react");
|
|
3905
|
-
var import_jsx_runtime22 = require("react/jsx-runtime");
|
|
3906
|
-
function DialogCards(props) {
|
|
3907
|
-
const [index, setIndex] = (0, import_react32.useState)(0);
|
|
3908
|
-
const [flipped, setFlipped] = (0, import_react32.useState)(false);
|
|
3909
|
-
const card = props.cards[index];
|
|
3910
|
-
if (!card) return null;
|
|
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: [
|
|
3913
|
-
"Card ",
|
|
3914
|
-
index + 1,
|
|
3915
|
-
" of ",
|
|
3916
|
-
props.cards.length
|
|
3917
|
-
] }),
|
|
3918
|
-
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
3919
|
-
"button",
|
|
4264
|
+
return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("section", { "aria-label": props.title, "data-testid": "interactive-video", "data-lk-block-id": blockId, children: [
|
|
4265
|
+
/* @__PURE__ */ (0, import_jsx_runtime23.jsx)("h3", { children: props.title }),
|
|
4266
|
+
props.showVideoScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("p", { "data-testid": "video-score", children: [
|
|
4267
|
+
"Score: ",
|
|
4268
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
4269
|
+
" /",
|
|
4270
|
+
" ",
|
|
4271
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
4272
|
+
] }) : null,
|
|
4273
|
+
/* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { style: { position: "relative" }, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
4274
|
+
"video",
|
|
3920
4275
|
{
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
4276
|
+
ref: videoRef,
|
|
4277
|
+
src: props.src,
|
|
4278
|
+
poster: props.poster,
|
|
4279
|
+
controls: true,
|
|
4280
|
+
"data-testid": "interactive-video-player",
|
|
4281
|
+
onTimeUpdate,
|
|
4282
|
+
onSeeking: () => {
|
|
4283
|
+
const video = videoRef.current;
|
|
4284
|
+
if (!video) return;
|
|
4285
|
+
const blockSeek = mandatoryIncompleteBefore(video.currentTime);
|
|
4286
|
+
if (blockSeek !== null && video.currentTime > blockSeek + 0.5) {
|
|
4287
|
+
video.currentTime = blockSeek;
|
|
4288
|
+
}
|
|
4289
|
+
},
|
|
4290
|
+
children: props.captions ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("track", { kind: "captions", src: props.captions, srcLang: "en", label: "Captions", default: true }) : null
|
|
3927
4291
|
}
|
|
3928
|
-
),
|
|
3929
|
-
/* @__PURE__ */ (0,
|
|
3930
|
-
|
|
4292
|
+
) }),
|
|
4293
|
+
/* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { "data-testid": "interactive-video-cues", children: sortedCues.map(
|
|
4294
|
+
(cue, i) => import_react33.default.cloneElement(cue, {
|
|
4295
|
+
key: cue.key ?? i,
|
|
4296
|
+
hidden: !overlayActive || i !== index,
|
|
4297
|
+
cueIndex: i,
|
|
4298
|
+
parentType: "InteractiveVideo"
|
|
4299
|
+
})
|
|
4300
|
+
) }),
|
|
4301
|
+
overlayActive ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
|
|
4302
|
+
activeCue?.props.mustComplete && !canContinueActiveCue ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { role: "status", "data-testid": "cue-must-complete-hint", children: "Complete the interaction to continue." }) : null,
|
|
4303
|
+
/* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
3931
4304
|
"button",
|
|
3932
4305
|
{
|
|
3933
4306
|
type: "button",
|
|
3934
|
-
"data-testid": "
|
|
3935
|
-
disabled:
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
4307
|
+
"data-testid": "cue-continue",
|
|
4308
|
+
disabled: !canContinueActiveCue,
|
|
4309
|
+
"aria-disabled": !canContinueActiveCue,
|
|
4310
|
+
onClick: completeCue,
|
|
4311
|
+
children: "Continue video"
|
|
4312
|
+
}
|
|
4313
|
+
)
|
|
4314
|
+
] }) : null
|
|
4315
|
+
] });
|
|
4316
|
+
});
|
|
4317
|
+
var InteractiveVideo = (0, import_react33.forwardRef)(
|
|
4318
|
+
function InteractiveVideo2(props, ref) {
|
|
4319
|
+
const blockId = (0, import_react33.useMemo)(
|
|
4320
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
4321
|
+
[props.blockId]
|
|
4322
|
+
);
|
|
4323
|
+
const cues = import_react33.default.Children.toArray(props.children).filter(
|
|
4324
|
+
import_react33.default.isValidElement
|
|
4325
|
+
);
|
|
4326
|
+
const { config, storage } = useLessonkit();
|
|
4327
|
+
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
4328
|
+
const initialMeta = (0, import_react33.useMemo)(
|
|
4329
|
+
() => loadVideoMeta(storage, config.courseId, blockId, persistEnabled),
|
|
4330
|
+
[storage, config.courseId, blockId, persistEnabled]
|
|
4331
|
+
);
|
|
4332
|
+
const initialIndex = useCompoundInitialIndex({
|
|
4333
|
+
courseId: config.courseId,
|
|
4334
|
+
compoundId: blockId,
|
|
4335
|
+
pageCount: cues.length,
|
|
4336
|
+
persistEnabled,
|
|
4337
|
+
storage
|
|
4338
|
+
});
|
|
4339
|
+
const [index, setIndex] = (0, import_react33.useState)(initialIndex);
|
|
4340
|
+
const setIndexStable = (0, import_react33.useCallback)((i) => setIndex(i), []);
|
|
4341
|
+
(0, import_react33.useEffect)(() => {
|
|
4342
|
+
setIndex(initialIndex);
|
|
4343
|
+
}, [config.courseId, blockId, initialIndex]);
|
|
4344
|
+
return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
4345
|
+
InteractiveVideoInner,
|
|
4346
|
+
{
|
|
4347
|
+
...props,
|
|
4348
|
+
ref,
|
|
4349
|
+
blockId,
|
|
4350
|
+
cues,
|
|
4351
|
+
index,
|
|
4352
|
+
setIndex,
|
|
4353
|
+
persistEnabled,
|
|
4354
|
+
initialMeta
|
|
4355
|
+
}
|
|
4356
|
+
) });
|
|
4357
|
+
}
|
|
4358
|
+
);
|
|
4359
|
+
setLessonkitBlockType(InteractiveVideo, "InteractiveVideo");
|
|
4360
|
+
|
|
4361
|
+
// src/blocks/Summary.tsx
|
|
4362
|
+
var import_react34 = require("react");
|
|
4363
|
+
var import_jsx_runtime24 = require("react/jsx-runtime");
|
|
4364
|
+
var INTERACTION6 = "summary";
|
|
4365
|
+
function SummaryInner(props, ref) {
|
|
4366
|
+
const checkId = (0, import_react34.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4367
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4368
|
+
const [selectedIndices, setSelectedIndices] = (0, import_react34.useState)([]);
|
|
4369
|
+
const [passed, setPassed] = (0, import_react34.useState)(false);
|
|
4370
|
+
const [checked, setChecked] = (0, import_react34.useState)(false);
|
|
4371
|
+
const completedRef = (0, import_react34.useRef)(false);
|
|
4372
|
+
const telemetryReplayedRef = (0, import_react34.useRef)(false);
|
|
4373
|
+
const correctKey = props.correct.join("\0");
|
|
4374
|
+
const statementsKey = props.statements.join("\0");
|
|
4375
|
+
const selected = selectedIndices.map((i) => props.statements[i] ?? "");
|
|
4376
|
+
const reset = () => {
|
|
4377
|
+
completedRef.current = false;
|
|
4378
|
+
telemetryReplayedRef.current = false;
|
|
4379
|
+
setSelectedIndices([]);
|
|
4380
|
+
setPassed(false);
|
|
4381
|
+
setChecked(false);
|
|
4382
|
+
};
|
|
4383
|
+
(0, import_react34.useEffect)(() => {
|
|
4384
|
+
reset();
|
|
4385
|
+
}, [checkId, correctKey, statementsKey]);
|
|
4386
|
+
const isCorrect = selected.length === props.correct.length && selected.every((s, i) => s === props.correct[i]);
|
|
4387
|
+
const maxScore = props.correct.length || 1;
|
|
4388
|
+
const score = isCorrect ? maxScore : 0;
|
|
4389
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
4390
|
+
const availableIndices = props.statements.map((_, i) => i).filter((i) => !selectedIndices.includes(i));
|
|
4391
|
+
const handle = (0, import_react34.useMemo)(
|
|
4392
|
+
() => buildAssessmentHandle({
|
|
4393
|
+
checkId,
|
|
4394
|
+
getScore: () => passed ? score : 0,
|
|
4395
|
+
getMaxScore: () => maxScore,
|
|
4396
|
+
getAnswerGiven: () => selectedIndices.length > 0,
|
|
4397
|
+
resetTask: reset,
|
|
4398
|
+
showSolutions: () => {
|
|
4399
|
+
},
|
|
4400
|
+
getXAPIData: () => ({
|
|
4401
|
+
checkId,
|
|
4402
|
+
interactionType: INTERACTION6,
|
|
4403
|
+
response: selected,
|
|
4404
|
+
correct: passedThreshold,
|
|
4405
|
+
score: passed ? score : 0,
|
|
4406
|
+
maxScore
|
|
4407
|
+
}),
|
|
4408
|
+
getCurrentState: () => ({ selectedIndices, passed, checked }),
|
|
4409
|
+
resume: (state) => {
|
|
4410
|
+
let nextIndices = [];
|
|
4411
|
+
if (Array.isArray(state.selectedIndices)) {
|
|
4412
|
+
nextIndices = [...state.selectedIndices];
|
|
4413
|
+
} else if (Array.isArray(state.selected)) {
|
|
4414
|
+
const legacy = state.selected;
|
|
4415
|
+
nextIndices = legacy.map((text) => props.statements.indexOf(text)).filter((i) => i >= 0);
|
|
4416
|
+
}
|
|
4417
|
+
setSelectedIndices(nextIndices);
|
|
4418
|
+
const nextSelected = nextIndices.map((i) => props.statements[i] ?? "");
|
|
4419
|
+
const nextIsCorrect = nextSelected.length === props.correct.length && nextSelected.every((s, i) => s === props.correct[i]);
|
|
4420
|
+
const nextScore = nextIsCorrect ? maxScore : 0;
|
|
4421
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
4422
|
+
setPassed(value);
|
|
4423
|
+
completedRef.current = value;
|
|
4424
|
+
if (value) {
|
|
4425
|
+
if (!telemetryReplayedRef.current) {
|
|
4426
|
+
telemetryReplayedRef.current = true;
|
|
4427
|
+
assessment.answer({
|
|
4428
|
+
checkId,
|
|
4429
|
+
interactionType: INTERACTION6,
|
|
4430
|
+
response: nextSelected,
|
|
4431
|
+
correct: true
|
|
4432
|
+
});
|
|
4433
|
+
assessment.complete({
|
|
4434
|
+
checkId,
|
|
4435
|
+
interactionType: INTERACTION6,
|
|
4436
|
+
score: nextScore,
|
|
4437
|
+
maxScore,
|
|
4438
|
+
passingScore: props.passingScore ?? maxScore
|
|
4439
|
+
});
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
});
|
|
4443
|
+
readBooleanStateField(state, "checked", setChecked);
|
|
4444
|
+
}
|
|
4445
|
+
}),
|
|
4446
|
+
[
|
|
4447
|
+
assessment,
|
|
4448
|
+
checkId,
|
|
4449
|
+
checked,
|
|
4450
|
+
maxScore,
|
|
4451
|
+
passed,
|
|
4452
|
+
passedThreshold,
|
|
4453
|
+
props.passingScore,
|
|
4454
|
+
props.statements,
|
|
4455
|
+
score,
|
|
4456
|
+
selected,
|
|
4457
|
+
selectedIndices.length
|
|
4458
|
+
]
|
|
4459
|
+
);
|
|
4460
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
4461
|
+
const addStatement = (statementIndex) => {
|
|
4462
|
+
if (passed && !props.enableRetry) return;
|
|
4463
|
+
setChecked(false);
|
|
4464
|
+
setSelectedIndices((prev) => [...prev, statementIndex]);
|
|
4465
|
+
};
|
|
4466
|
+
const removeLast = () => {
|
|
4467
|
+
if (passed && !props.enableRetry) return;
|
|
4468
|
+
setChecked(false);
|
|
4469
|
+
setSelectedIndices((prev) => prev.slice(0, -1));
|
|
4470
|
+
};
|
|
4471
|
+
const check = () => {
|
|
4472
|
+
if (selectedIndices.length === 0) return;
|
|
4473
|
+
setChecked(true);
|
|
4474
|
+
assessment.answer({
|
|
4475
|
+
checkId,
|
|
4476
|
+
interactionType: INTERACTION6,
|
|
4477
|
+
response: selected,
|
|
4478
|
+
correct: passedThreshold
|
|
4479
|
+
});
|
|
4480
|
+
if (passedThreshold && !completedRef.current) {
|
|
4481
|
+
completedRef.current = true;
|
|
4482
|
+
setPassed(true);
|
|
4483
|
+
assessment.complete({
|
|
4484
|
+
checkId,
|
|
4485
|
+
interactionType: INTERACTION6,
|
|
4486
|
+
score,
|
|
4487
|
+
maxScore,
|
|
4488
|
+
passingScore: props.passingScore ?? maxScore
|
|
4489
|
+
});
|
|
4490
|
+
}
|
|
4491
|
+
};
|
|
4492
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("section", { "aria-label": "Summary", "data-lk-check-id": checkId, "data-testid": "summary", children: [
|
|
4493
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { children: "Select statements in order to build the summary." }),
|
|
4494
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("ol", { "data-testid": "summary-selected", children: selected.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("li", { children: s }, `${i}-${selectedIndices[i]}`)) }),
|
|
4495
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { role: "group", "aria-label": "Available statements", children: availableIndices.map((statementIndex) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
4496
|
+
"button",
|
|
4497
|
+
{
|
|
4498
|
+
type: "button",
|
|
4499
|
+
"data-testid": `summary-statement-${statementIndex}`,
|
|
4500
|
+
disabled: passed && !props.enableRetry,
|
|
4501
|
+
onClick: () => addStatement(statementIndex),
|
|
4502
|
+
style: { display: "block", margin: "0.25rem 0" },
|
|
4503
|
+
children: props.statements[statementIndex]
|
|
4504
|
+
},
|
|
4505
|
+
statementIndex
|
|
4506
|
+
)) }),
|
|
4507
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
4508
|
+
"button",
|
|
4509
|
+
{
|
|
4510
|
+
type: "button",
|
|
4511
|
+
"data-testid": "summary-undo",
|
|
4512
|
+
disabled: passed && !props.enableRetry || selectedIndices.length === 0,
|
|
4513
|
+
onClick: removeLast,
|
|
4514
|
+
children: "Remove last"
|
|
4515
|
+
}
|
|
4516
|
+
),
|
|
4517
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
4518
|
+
"button",
|
|
4519
|
+
{
|
|
4520
|
+
type: "button",
|
|
4521
|
+
"data-testid": "summary-check",
|
|
4522
|
+
disabled: selectedIndices.length === 0 || passed && !props.enableRetry,
|
|
4523
|
+
onClick: check,
|
|
4524
|
+
children: "Check"
|
|
4525
|
+
}
|
|
4526
|
+
),
|
|
4527
|
+
checked ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { role: "status", "aria-live": "polite", "data-testid": "summary-feedback", children: passedThreshold ? "Correct" : "Try again" }) : null,
|
|
4528
|
+
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("button", { type: "button", "data-testid": "summary-retry", onClick: reset, children: "Try again" }) : null
|
|
4529
|
+
] });
|
|
4530
|
+
}
|
|
4531
|
+
var SummaryInnerForwarded = (0, import_react34.forwardRef)(SummaryInner);
|
|
4532
|
+
var Summary = (0, import_react34.forwardRef)(function Summary2(props, ref) {
|
|
4533
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(AssessmentLessonGuard, { blockLabel: "Summary", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(SummaryInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
4534
|
+
});
|
|
4535
|
+
setLessonkitBlockType(Summary, "Summary");
|
|
4536
|
+
|
|
4537
|
+
// src/blocks/ImagePairing.tsx
|
|
4538
|
+
var import_react35 = require("react");
|
|
4539
|
+
var import_jsx_runtime25 = require("react/jsx-runtime");
|
|
4540
|
+
var INTERACTION7 = "imagePairing";
|
|
4541
|
+
function shuffleCards(cards) {
|
|
4542
|
+
const next = [...cards];
|
|
4543
|
+
for (let i = next.length - 1; i > 0; i -= 1) {
|
|
4544
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
4545
|
+
[next[i], next[j]] = [next[j], next[i]];
|
|
4546
|
+
}
|
|
4547
|
+
return next;
|
|
4548
|
+
}
|
|
4549
|
+
function buildDeck(pairs) {
|
|
4550
|
+
const cards = pairs.flatMap(
|
|
4551
|
+
(pair) => [0, 1].map((copy) => ({
|
|
4552
|
+
cardKey: `${pair.id}-${copy}`,
|
|
4553
|
+
pairId: pair.id,
|
|
4554
|
+
label: pair.label,
|
|
4555
|
+
imageSrc: pair.imageSrc
|
|
4556
|
+
}))
|
|
4557
|
+
);
|
|
4558
|
+
return shuffleCards(cards);
|
|
4559
|
+
}
|
|
4560
|
+
function ImagePairingInner(props, ref) {
|
|
4561
|
+
const checkId = (0, import_react35.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4562
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4563
|
+
const pairsKey = props.pairs.map((p) => p.id).join("\0");
|
|
4564
|
+
const [cards, setCards] = (0, import_react35.useState)(() => buildDeck(props.pairs));
|
|
4565
|
+
const [matched, setMatched] = (0, import_react35.useState)(() => /* @__PURE__ */ new Set());
|
|
4566
|
+
const [revealed, setRevealed] = (0, import_react35.useState)(() => /* @__PURE__ */ new Set());
|
|
4567
|
+
const [keyboardSelection, setKeyboardSelection] = (0, import_react35.useState)(null);
|
|
4568
|
+
const [passed, setPassed] = (0, import_react35.useState)(false);
|
|
4569
|
+
const completedRef = (0, import_react35.useRef)(false);
|
|
4570
|
+
const telemetryReplayedRef = (0, import_react35.useRef)(false);
|
|
4571
|
+
const reset = () => {
|
|
4572
|
+
completedRef.current = false;
|
|
4573
|
+
telemetryReplayedRef.current = false;
|
|
4574
|
+
setCards(buildDeck(props.pairs));
|
|
4575
|
+
setMatched(/* @__PURE__ */ new Set());
|
|
4576
|
+
setRevealed(/* @__PURE__ */ new Set());
|
|
4577
|
+
setKeyboardSelection(null);
|
|
4578
|
+
setPassed(false);
|
|
4579
|
+
};
|
|
4580
|
+
(0, import_react35.useEffect)(() => {
|
|
4581
|
+
reset();
|
|
4582
|
+
}, [checkId, pairsKey]);
|
|
4583
|
+
const totalPairs = props.pairs.length;
|
|
4584
|
+
const matchedCount = matched.size;
|
|
4585
|
+
const maxScore = totalPairs || 1;
|
|
4586
|
+
const score = matchedCount;
|
|
4587
|
+
const allMatched = totalPairs > 0 && matchedCount === totalPairs;
|
|
4588
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
4589
|
+
const completeIfReady = (nextMatched) => {
|
|
4590
|
+
if (nextMatched.size === totalPairs && totalPairs > 0 && !completedRef.current) {
|
|
4591
|
+
const finalScore = nextMatched.size;
|
|
4592
|
+
const finalPassed = meetsPassingThreshold(finalScore, maxScore, props.passingScore);
|
|
4593
|
+
completedRef.current = true;
|
|
4594
|
+
setPassed(true);
|
|
4595
|
+
assessment.answer({
|
|
4596
|
+
checkId,
|
|
4597
|
+
interactionType: INTERACTION7,
|
|
4598
|
+
response: { matchedPairIds: [...nextMatched] },
|
|
4599
|
+
correct: finalPassed
|
|
4600
|
+
});
|
|
4601
|
+
assessment.complete({
|
|
4602
|
+
checkId,
|
|
4603
|
+
interactionType: INTERACTION7,
|
|
4604
|
+
score: finalScore,
|
|
4605
|
+
maxScore,
|
|
4606
|
+
passingScore: props.passingScore ?? maxScore
|
|
4607
|
+
});
|
|
4608
|
+
}
|
|
4609
|
+
};
|
|
4610
|
+
const tryMatch = (firstKey, secondKey) => {
|
|
4611
|
+
if (firstKey === secondKey) return;
|
|
4612
|
+
const first = cards.find((c) => c.cardKey === firstKey);
|
|
4613
|
+
const second = cards.find((c) => c.cardKey === secondKey);
|
|
4614
|
+
if (!first || !second) return;
|
|
4615
|
+
setRevealed((prev) => /* @__PURE__ */ new Set([...prev, firstKey, secondKey]));
|
|
4616
|
+
if (first.pairId === second.pairId) {
|
|
4617
|
+
setMatched((prev) => {
|
|
4618
|
+
const next = /* @__PURE__ */ new Set([...prev, first.pairId]);
|
|
4619
|
+
completeIfReady(next);
|
|
4620
|
+
return next;
|
|
4621
|
+
});
|
|
4622
|
+
setRevealed(/* @__PURE__ */ new Set());
|
|
4623
|
+
setKeyboardSelection(null);
|
|
4624
|
+
} else {
|
|
4625
|
+
window.setTimeout(() => {
|
|
4626
|
+
setRevealed((prev) => {
|
|
4627
|
+
const next = new Set(prev);
|
|
4628
|
+
next.delete(firstKey);
|
|
4629
|
+
next.delete(secondKey);
|
|
4630
|
+
return next;
|
|
4631
|
+
});
|
|
4632
|
+
setKeyboardSelection(null);
|
|
4633
|
+
}, 800);
|
|
4634
|
+
}
|
|
4635
|
+
};
|
|
4636
|
+
const selectCard = (cardKey) => {
|
|
4637
|
+
if (passed && !props.enableRetry) return;
|
|
4638
|
+
if (matched.has(cards.find((c) => c.cardKey === cardKey)?.pairId ?? "")) return;
|
|
4639
|
+
if (keyboardSelection === null) {
|
|
4640
|
+
setKeyboardSelection(cardKey);
|
|
4641
|
+
setRevealed((prev) => /* @__PURE__ */ new Set([...prev, cardKey]));
|
|
4642
|
+
return;
|
|
4643
|
+
}
|
|
4644
|
+
if (keyboardSelection === cardKey) {
|
|
4645
|
+
setKeyboardSelection(null);
|
|
4646
|
+
setRevealed((prev) => {
|
|
4647
|
+
const next = new Set(prev);
|
|
4648
|
+
next.delete(cardKey);
|
|
4649
|
+
return next;
|
|
4650
|
+
});
|
|
4651
|
+
return;
|
|
4652
|
+
}
|
|
4653
|
+
tryMatch(keyboardSelection, cardKey);
|
|
4654
|
+
};
|
|
4655
|
+
const handle = (0, import_react35.useMemo)(
|
|
4656
|
+
() => buildAssessmentHandle({
|
|
4657
|
+
checkId,
|
|
4658
|
+
getScore: () => score,
|
|
4659
|
+
getMaxScore: () => maxScore,
|
|
4660
|
+
getAnswerGiven: () => matchedCount > 0,
|
|
4661
|
+
resetTask: reset,
|
|
4662
|
+
showSolutions: () => {
|
|
4663
|
+
},
|
|
4664
|
+
getXAPIData: () => ({
|
|
4665
|
+
checkId,
|
|
4666
|
+
interactionType: INTERACTION7,
|
|
4667
|
+
response: { matchedPairIds: [...matched] },
|
|
4668
|
+
correct: allMatched && passedThreshold,
|
|
4669
|
+
score,
|
|
4670
|
+
maxScore
|
|
4671
|
+
}),
|
|
4672
|
+
getCurrentState: () => ({
|
|
4673
|
+
matched: [...matched],
|
|
4674
|
+
revealed: [...revealed],
|
|
4675
|
+
keyboardSelection,
|
|
4676
|
+
passed
|
|
4677
|
+
}),
|
|
4678
|
+
resume: (state) => {
|
|
4679
|
+
if (Array.isArray(state.matched)) setMatched(new Set(state.matched));
|
|
4680
|
+
if (Array.isArray(state.revealed)) setRevealed(new Set(state.revealed));
|
|
4681
|
+
const sel = state.keyboardSelection;
|
|
4682
|
+
if (sel === null || typeof sel === "string") setKeyboardSelection(sel ?? null);
|
|
4683
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
4684
|
+
setPassed(value);
|
|
4685
|
+
completedRef.current = value;
|
|
4686
|
+
if (value && !telemetryReplayedRef.current) {
|
|
4687
|
+
telemetryReplayedRef.current = true;
|
|
4688
|
+
const matchedIds = Array.isArray(state.matched) ? state.matched : [...matched];
|
|
4689
|
+
const finalScore = matchedIds.length;
|
|
4690
|
+
assessment.answer({
|
|
4691
|
+
checkId,
|
|
4692
|
+
interactionType: INTERACTION7,
|
|
4693
|
+
response: { matchedPairIds: matchedIds },
|
|
4694
|
+
correct: true
|
|
4695
|
+
});
|
|
4696
|
+
assessment.complete({
|
|
4697
|
+
checkId,
|
|
4698
|
+
interactionType: INTERACTION7,
|
|
4699
|
+
score: finalScore,
|
|
4700
|
+
maxScore,
|
|
4701
|
+
passingScore: props.passingScore ?? maxScore
|
|
4702
|
+
});
|
|
4703
|
+
}
|
|
4704
|
+
});
|
|
4705
|
+
}
|
|
4706
|
+
}),
|
|
4707
|
+
[allMatched, checkId, keyboardSelection, matched, matchedCount, maxScore, passed, passedThreshold, revealed, score]
|
|
4708
|
+
);
|
|
4709
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
4710
|
+
return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("section", { "aria-label": "Image Pairing", "data-lk-check-id": checkId, "data-testid": "image-pairing", children: [
|
|
4711
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { children: "Match the image pairs (select two cards with keyboard or click)." }),
|
|
4712
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { role: "list", "aria-label": "Image cards", "data-testid": "image-pairing-grid", children: cards.map((card) => {
|
|
4713
|
+
const isMatched = matched.has(card.pairId);
|
|
4714
|
+
const isRevealed = isMatched || revealed.has(card.cardKey);
|
|
4715
|
+
const isSelected = keyboardSelection === card.cardKey;
|
|
4716
|
+
return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4717
|
+
"button",
|
|
4718
|
+
{
|
|
4719
|
+
type: "button",
|
|
4720
|
+
role: "listitem",
|
|
4721
|
+
"data-testid": `pairing-card-${card.cardKey}`,
|
|
4722
|
+
"aria-pressed": isSelected,
|
|
4723
|
+
disabled: isMatched || passed && !props.enableRetry,
|
|
4724
|
+
onClick: () => selectCard(card.cardKey),
|
|
4725
|
+
style: {
|
|
4726
|
+
margin: "0.25rem",
|
|
4727
|
+
minWidth: "6rem",
|
|
4728
|
+
minHeight: "6rem",
|
|
4729
|
+
border: isSelected ? "2px solid currentColor" : "1px solid currentColor"
|
|
4730
|
+
},
|
|
4731
|
+
children: isRevealed ? /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
|
|
4732
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)("img", { src: card.imageSrc, alt: card.label, style: { maxWidth: "5rem", maxHeight: "5rem" } }),
|
|
4733
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "lk-visually-hidden", children: card.label })
|
|
4734
|
+
] }) : "?"
|
|
4735
|
+
},
|
|
4736
|
+
card.cardKey
|
|
4737
|
+
);
|
|
4738
|
+
}) }),
|
|
4739
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("p", { role: "status", "aria-live": "polite", "data-testid": "image-pairing-progress", children: [
|
|
4740
|
+
matchedCount,
|
|
4741
|
+
" / ",
|
|
4742
|
+
totalPairs,
|
|
4743
|
+
" pairs matched"
|
|
4744
|
+
] }),
|
|
4745
|
+
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("button", { type: "button", "data-testid": "image-pairing-retry", onClick: reset, children: "Try again" }) : null
|
|
4746
|
+
] });
|
|
4747
|
+
}
|
|
4748
|
+
var ImagePairingInnerForwarded = (0, import_react35.forwardRef)(ImagePairingInner);
|
|
4749
|
+
var ImagePairing = (0, import_react35.forwardRef)(function ImagePairing2(props, ref) {
|
|
4750
|
+
return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(AssessmentLessonGuard, { blockLabel: "ImagePairing", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ImagePairingInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
4751
|
+
});
|
|
4752
|
+
setLessonkitBlockType(ImagePairing, "ImagePairing");
|
|
4753
|
+
|
|
4754
|
+
// src/blocks/ImageSequencing.tsx
|
|
4755
|
+
var import_react36 = require("react");
|
|
4756
|
+
var import_jsx_runtime26 = require("react/jsx-runtime");
|
|
4757
|
+
var INTERACTION8 = "imageSequencing";
|
|
4758
|
+
function ImageSequencingInner(props, ref) {
|
|
4759
|
+
const checkId = (0, import_react36.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4760
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4761
|
+
const imagesKey = props.images.map((i) => i.id).join("\0");
|
|
4762
|
+
const orderKey = props.correctOrder.join("\0");
|
|
4763
|
+
const [order, setOrder] = (0, import_react36.useState)(() => props.images.map((i) => i.id));
|
|
4764
|
+
const [passed, setPassed] = (0, import_react36.useState)(false);
|
|
4765
|
+
const [checked, setChecked] = (0, import_react36.useState)(false);
|
|
4766
|
+
const completedRef = (0, import_react36.useRef)(false);
|
|
4767
|
+
const telemetryReplayedRef = (0, import_react36.useRef)(false);
|
|
4768
|
+
const reset = () => {
|
|
4769
|
+
completedRef.current = false;
|
|
4770
|
+
telemetryReplayedRef.current = false;
|
|
4771
|
+
setOrder(props.images.map((i) => i.id));
|
|
4772
|
+
setPassed(false);
|
|
4773
|
+
setChecked(false);
|
|
4774
|
+
};
|
|
4775
|
+
(0, import_react36.useEffect)(() => {
|
|
4776
|
+
reset();
|
|
4777
|
+
}, [checkId, imagesKey, orderKey]);
|
|
4778
|
+
const isCorrect = order.every((id, i) => id === props.correctOrder[i]);
|
|
4779
|
+
const maxScore = props.correctOrder.length || 1;
|
|
4780
|
+
const score = isCorrect ? maxScore : 0;
|
|
4781
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
4782
|
+
const move = (index, direction) => {
|
|
4783
|
+
if (passed && !props.enableRetry) return;
|
|
4784
|
+
setChecked(false);
|
|
4785
|
+
const nextIndex = index + direction;
|
|
4786
|
+
if (nextIndex < 0 || nextIndex >= order.length) return;
|
|
4787
|
+
setOrder((prev) => {
|
|
4788
|
+
const next = [...prev];
|
|
4789
|
+
[next[index], next[nextIndex]] = [next[nextIndex], next[index]];
|
|
4790
|
+
return next;
|
|
4791
|
+
});
|
|
4792
|
+
};
|
|
4793
|
+
const handle = (0, import_react36.useMemo)(
|
|
4794
|
+
() => buildAssessmentHandle({
|
|
4795
|
+
checkId,
|
|
4796
|
+
getScore: () => passed ? score : 0,
|
|
4797
|
+
getMaxScore: () => maxScore,
|
|
4798
|
+
getAnswerGiven: () => order.length > 0,
|
|
4799
|
+
resetTask: reset,
|
|
4800
|
+
showSolutions: () => {
|
|
4801
|
+
},
|
|
4802
|
+
getXAPIData: () => ({
|
|
4803
|
+
checkId,
|
|
4804
|
+
interactionType: INTERACTION8,
|
|
4805
|
+
response: order,
|
|
4806
|
+
correct: passedThreshold,
|
|
4807
|
+
score: passed ? score : 0,
|
|
4808
|
+
maxScore
|
|
4809
|
+
}),
|
|
4810
|
+
getCurrentState: () => ({ order, passed, checked }),
|
|
4811
|
+
resume: (state) => {
|
|
4812
|
+
let nextOrder = order;
|
|
4813
|
+
if (Array.isArray(state.order)) {
|
|
4814
|
+
nextOrder = [...state.order];
|
|
4815
|
+
setOrder(nextOrder);
|
|
4816
|
+
}
|
|
4817
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
4818
|
+
setPassed(value);
|
|
4819
|
+
completedRef.current = value;
|
|
4820
|
+
if (value && !telemetryReplayedRef.current) {
|
|
4821
|
+
telemetryReplayedRef.current = true;
|
|
4822
|
+
const nextIsCorrect = nextOrder.every((id, i) => id === props.correctOrder[i]);
|
|
4823
|
+
const nextScore = nextIsCorrect ? maxScore : 0;
|
|
4824
|
+
assessment.answer({
|
|
4825
|
+
checkId,
|
|
4826
|
+
interactionType: INTERACTION8,
|
|
4827
|
+
response: nextOrder,
|
|
4828
|
+
correct: nextIsCorrect
|
|
4829
|
+
});
|
|
4830
|
+
assessment.complete({
|
|
4831
|
+
checkId,
|
|
4832
|
+
interactionType: INTERACTION8,
|
|
4833
|
+
score: nextScore,
|
|
4834
|
+
maxScore,
|
|
4835
|
+
passingScore: props.passingScore ?? maxScore
|
|
4836
|
+
});
|
|
4837
|
+
}
|
|
4838
|
+
});
|
|
4839
|
+
readBooleanStateField(state, "checked", setChecked);
|
|
4840
|
+
}
|
|
4841
|
+
}),
|
|
4842
|
+
[checkId, checked, maxScore, order, passed, passedThreshold, score]
|
|
4843
|
+
);
|
|
4844
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
4845
|
+
const check = () => {
|
|
4846
|
+
setChecked(true);
|
|
4847
|
+
assessment.answer({
|
|
4848
|
+
checkId,
|
|
4849
|
+
interactionType: INTERACTION8,
|
|
4850
|
+
response: order,
|
|
4851
|
+
correct: passedThreshold
|
|
4852
|
+
});
|
|
4853
|
+
if (passedThreshold && !completedRef.current) {
|
|
4854
|
+
completedRef.current = true;
|
|
4855
|
+
setPassed(true);
|
|
4856
|
+
assessment.complete({
|
|
4857
|
+
checkId,
|
|
4858
|
+
interactionType: INTERACTION8,
|
|
4859
|
+
score,
|
|
4860
|
+
maxScore,
|
|
4861
|
+
passingScore: props.passingScore ?? maxScore
|
|
4862
|
+
});
|
|
4863
|
+
}
|
|
4864
|
+
};
|
|
4865
|
+
return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("section", { "aria-label": "Image Sequencing", "data-lk-check-id": checkId, "data-testid": "image-sequencing", children: [
|
|
4866
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { children: "Reorder the images into the correct sequence." }),
|
|
4867
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)("ol", { "data-testid": "image-sequencing-list", children: order.map((id, index) => {
|
|
4868
|
+
const image = props.images.find((i) => i.id === id);
|
|
4869
|
+
if (!image) return null;
|
|
4870
|
+
return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("li", { "data-testid": `sequencing-item-${id}`, children: [
|
|
4871
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)("img", { src: image.src, alt: image.alt, style: { maxWidth: "8rem", verticalAlign: "middle" } }),
|
|
4872
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
|
|
4873
|
+
"button",
|
|
4874
|
+
{
|
|
4875
|
+
type: "button",
|
|
4876
|
+
"data-testid": `sequencing-up-${id}`,
|
|
4877
|
+
"aria-label": `Move ${image.alt} up`,
|
|
4878
|
+
disabled: index === 0 || passed && !props.enableRetry,
|
|
4879
|
+
onClick: () => move(index, -1),
|
|
4880
|
+
children: "Up"
|
|
4881
|
+
}
|
|
4882
|
+
),
|
|
4883
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
|
|
4884
|
+
"button",
|
|
4885
|
+
{
|
|
4886
|
+
type: "button",
|
|
4887
|
+
"data-testid": `sequencing-down-${id}`,
|
|
4888
|
+
"aria-label": `Move ${image.alt} down`,
|
|
4889
|
+
disabled: index >= order.length - 1 || passed && !props.enableRetry,
|
|
4890
|
+
onClick: () => move(index, 1),
|
|
4891
|
+
children: "Down"
|
|
4892
|
+
}
|
|
4893
|
+
)
|
|
4894
|
+
] }, id);
|
|
4895
|
+
}) }),
|
|
4896
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
|
|
4897
|
+
"button",
|
|
4898
|
+
{
|
|
4899
|
+
type: "button",
|
|
4900
|
+
"data-testid": "image-sequencing-check",
|
|
4901
|
+
disabled: passed && !props.enableRetry,
|
|
4902
|
+
onClick: check,
|
|
4903
|
+
children: "Check"
|
|
4904
|
+
}
|
|
4905
|
+
),
|
|
4906
|
+
checked ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { role: "status", "aria-live": "polite", "data-testid": "image-sequencing-feedback", children: passedThreshold ? "Correct" : "Try again" }) : null,
|
|
4907
|
+
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("button", { type: "button", "data-testid": "image-sequencing-retry", onClick: reset, children: "Try again" }) : null
|
|
4908
|
+
] });
|
|
4909
|
+
}
|
|
4910
|
+
var ImageSequencingInnerForwarded = (0, import_react36.forwardRef)(ImageSequencingInner);
|
|
4911
|
+
var ImageSequencing = (0, import_react36.forwardRef)(
|
|
4912
|
+
function ImageSequencing2(props, ref) {
|
|
4913
|
+
return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(AssessmentLessonGuard, { blockLabel: "ImageSequencing", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ImageSequencingInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
4914
|
+
}
|
|
4915
|
+
);
|
|
4916
|
+
setLessonkitBlockType(ImageSequencing, "ImageSequencing");
|
|
4917
|
+
|
|
4918
|
+
// src/blocks/ArithmeticQuiz.tsx
|
|
4919
|
+
var import_react37 = require("react");
|
|
4920
|
+
var import_jsx_runtime27 = require("react/jsx-runtime");
|
|
4921
|
+
var INTERACTION9 = "arithmeticQuiz";
|
|
4922
|
+
function ArithmeticQuizInner(props, ref) {
|
|
4923
|
+
const checkId = (0, import_react37.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4924
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4925
|
+
const problemsKey = props.problems.map((p) => `${p.question}\0${p.answer}`).join("|");
|
|
4926
|
+
const [answers, setAnswers] = (0, import_react37.useState)(
|
|
4927
|
+
() => Object.fromEntries(props.problems.map((_, i) => [i, ""]))
|
|
4928
|
+
);
|
|
4929
|
+
const [passed, setPassed] = (0, import_react37.useState)(false);
|
|
4930
|
+
const [checked, setChecked] = (0, import_react37.useState)(false);
|
|
4931
|
+
const [timeLeft, setTimeLeft] = (0, import_react37.useState)(
|
|
4932
|
+
props.timeLimitSeconds ?? null
|
|
4933
|
+
);
|
|
4934
|
+
const completedRef = (0, import_react37.useRef)(false);
|
|
4935
|
+
const telemetryReplayedRef = (0, import_react37.useRef)(false);
|
|
4936
|
+
const reset = () => {
|
|
4937
|
+
completedRef.current = false;
|
|
4938
|
+
telemetryReplayedRef.current = false;
|
|
4939
|
+
setAnswers(Object.fromEntries(props.problems.map((_, i) => [i, ""])));
|
|
4940
|
+
setPassed(false);
|
|
4941
|
+
setChecked(false);
|
|
4942
|
+
setTimeLeft(props.timeLimitSeconds ?? null);
|
|
4943
|
+
};
|
|
4944
|
+
(0, import_react37.useEffect)(() => {
|
|
4945
|
+
reset();
|
|
4946
|
+
}, [checkId, problemsKey, props.timeLimitSeconds]);
|
|
4947
|
+
let score = 0;
|
|
4948
|
+
props.problems.forEach((p, i) => {
|
|
4949
|
+
if ((answers[i] ?? "").trim() === p.answer.trim()) score += 1;
|
|
4950
|
+
});
|
|
4951
|
+
const maxScore = props.problems.length || 1;
|
|
4952
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
4953
|
+
const allFilled = props.problems.every((_, i) => (answers[i] ?? "").trim().length > 0);
|
|
4954
|
+
const runCheck = (0, import_react37.useCallback)(
|
|
4955
|
+
(force = false) => {
|
|
4956
|
+
if (!force && !allFilled) return;
|
|
4957
|
+
setChecked(true);
|
|
4958
|
+
assessment.answer({
|
|
4959
|
+
checkId,
|
|
4960
|
+
interactionType: INTERACTION9,
|
|
4961
|
+
response: answers,
|
|
4962
|
+
correct: passedThreshold
|
|
4963
|
+
});
|
|
4964
|
+
if (passedThreshold && !completedRef.current) {
|
|
4965
|
+
completedRef.current = true;
|
|
4966
|
+
setPassed(true);
|
|
4967
|
+
assessment.complete({
|
|
4968
|
+
checkId,
|
|
4969
|
+
interactionType: INTERACTION9,
|
|
4970
|
+
score,
|
|
4971
|
+
maxScore,
|
|
4972
|
+
passingScore: props.passingScore ?? maxScore
|
|
4973
|
+
});
|
|
4974
|
+
}
|
|
4975
|
+
},
|
|
4976
|
+
[allFilled, answers, assessment, checkId, maxScore, passedThreshold, props.passingScore, score]
|
|
4977
|
+
);
|
|
4978
|
+
(0, import_react37.useEffect)(() => {
|
|
4979
|
+
if (timeLeft === null || passed || checked) return;
|
|
4980
|
+
if (timeLeft <= 0) {
|
|
4981
|
+
runCheck(true);
|
|
4982
|
+
return;
|
|
4983
|
+
}
|
|
4984
|
+
const id = window.setTimeout(() => setTimeLeft((t) => t !== null ? t - 1 : t), 1e3);
|
|
4985
|
+
return () => window.clearTimeout(id);
|
|
4986
|
+
}, [checked, passed, runCheck, timeLeft]);
|
|
4987
|
+
const handle = (0, import_react37.useMemo)(
|
|
4988
|
+
() => buildAssessmentHandle({
|
|
4989
|
+
checkId,
|
|
4990
|
+
getScore: () => passed ? score : 0,
|
|
4991
|
+
getMaxScore: () => maxScore,
|
|
4992
|
+
getAnswerGiven: () => allFilled,
|
|
4993
|
+
resetTask: reset,
|
|
4994
|
+
showSolutions: () => {
|
|
4995
|
+
},
|
|
4996
|
+
getXAPIData: () => ({
|
|
4997
|
+
checkId,
|
|
4998
|
+
interactionType: INTERACTION9,
|
|
4999
|
+
response: answers,
|
|
5000
|
+
correct: passedThreshold,
|
|
5001
|
+
score: passed ? score : 0,
|
|
5002
|
+
maxScore
|
|
5003
|
+
}),
|
|
5004
|
+
getCurrentState: () => ({ answers, passed, checked, timeLeft }),
|
|
5005
|
+
resume: (state) => {
|
|
5006
|
+
const raw = state.answers;
|
|
5007
|
+
let nextAnswers = answers;
|
|
5008
|
+
if (raw && typeof raw === "object") {
|
|
5009
|
+
nextAnswers = { ...raw };
|
|
5010
|
+
setAnswers(nextAnswers);
|
|
5011
|
+
}
|
|
5012
|
+
readBooleanStateField(state, "passed", (value) => {
|
|
5013
|
+
setPassed(value);
|
|
5014
|
+
completedRef.current = value;
|
|
5015
|
+
if (value && !telemetryReplayedRef.current) {
|
|
5016
|
+
telemetryReplayedRef.current = true;
|
|
5017
|
+
let nextScore = 0;
|
|
5018
|
+
props.problems.forEach((p, i) => {
|
|
5019
|
+
if ((nextAnswers[i] ?? "").trim() === p.answer.trim()) nextScore += 1;
|
|
5020
|
+
});
|
|
5021
|
+
assessment.answer({
|
|
5022
|
+
checkId,
|
|
5023
|
+
interactionType: INTERACTION9,
|
|
5024
|
+
response: nextAnswers,
|
|
5025
|
+
correct: true
|
|
5026
|
+
});
|
|
5027
|
+
assessment.complete({
|
|
5028
|
+
checkId,
|
|
5029
|
+
interactionType: INTERACTION9,
|
|
5030
|
+
score: nextScore,
|
|
5031
|
+
maxScore,
|
|
5032
|
+
passingScore: props.passingScore ?? maxScore
|
|
5033
|
+
});
|
|
5034
|
+
}
|
|
5035
|
+
});
|
|
5036
|
+
readBooleanStateField(state, "checked", setChecked);
|
|
5037
|
+
if (typeof state.timeLeft === "number") setTimeLeft(state.timeLeft);
|
|
5038
|
+
}
|
|
5039
|
+
}),
|
|
5040
|
+
[allFilled, answers, checkId, checked, maxScore, passed, passedThreshold, score, timeLeft]
|
|
5041
|
+
);
|
|
5042
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
5043
|
+
const onInput = (index, value) => {
|
|
5044
|
+
if (passed && !props.enableRetry) return;
|
|
5045
|
+
setChecked(false);
|
|
5046
|
+
setAnswers((prev) => ({ ...prev, [index]: value }));
|
|
5047
|
+
};
|
|
5048
|
+
return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("section", { "aria-label": "Arithmetic Quiz", "data-lk-check-id": checkId, "data-testid": "arithmetic-quiz", children: [
|
|
5049
|
+
props.timeLimitSeconds ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { "data-testid": "arithmetic-timer", role: "timer", "aria-live": "polite", children: [
|
|
5050
|
+
"Time left: ",
|
|
5051
|
+
timeLeft ?? 0,
|
|
5052
|
+
"s"
|
|
5053
|
+
] }) : null,
|
|
5054
|
+
/* @__PURE__ */ (0, import_jsx_runtime27.jsx)("ol", { "data-testid": "arithmetic-problems", children: props.problems.map((problem, index) => /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("li", { children: [
|
|
5055
|
+
/* @__PURE__ */ (0, import_jsx_runtime27.jsx)("label", { htmlFor: `${checkId}-problem-${index}`, children: problem.question }),
|
|
5056
|
+
/* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
|
|
5057
|
+
"input",
|
|
5058
|
+
{
|
|
5059
|
+
id: `${checkId}-problem-${index}`,
|
|
5060
|
+
type: "text",
|
|
5061
|
+
inputMode: "numeric",
|
|
5062
|
+
"data-testid": `arithmetic-answer-${index}`,
|
|
5063
|
+
value: answers[index] ?? "",
|
|
5064
|
+
disabled: passed && !props.enableRetry,
|
|
5065
|
+
onChange: (e) => onInput(index, e.target.value)
|
|
5066
|
+
}
|
|
5067
|
+
)
|
|
5068
|
+
] }, index)) }),
|
|
5069
|
+
/* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
|
|
5070
|
+
"button",
|
|
5071
|
+
{
|
|
5072
|
+
type: "button",
|
|
5073
|
+
"data-testid": "arithmetic-check",
|
|
5074
|
+
disabled: !allFilled && timeLeft !== 0 || passed && !props.enableRetry,
|
|
5075
|
+
onClick: () => runCheck(),
|
|
5076
|
+
children: "Check"
|
|
5077
|
+
}
|
|
5078
|
+
),
|
|
5079
|
+
checked ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { role: "status", "aria-live": "polite", "data-testid": "arithmetic-feedback", children: [
|
|
5080
|
+
passedThreshold ? "Correct" : "Try again",
|
|
5081
|
+
" (",
|
|
5082
|
+
score,
|
|
5083
|
+
"/",
|
|
5084
|
+
maxScore,
|
|
5085
|
+
")"
|
|
5086
|
+
] }) : null,
|
|
5087
|
+
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("button", { type: "button", "data-testid": "arithmetic-retry", onClick: reset, children: "Try again" }) : null
|
|
5088
|
+
] });
|
|
5089
|
+
}
|
|
5090
|
+
var ArithmeticQuizInnerForwarded = (0, import_react37.forwardRef)(ArithmeticQuizInner);
|
|
5091
|
+
var ArithmeticQuiz = (0, import_react37.forwardRef)(
|
|
5092
|
+
function ArithmeticQuiz2(props, ref) {
|
|
5093
|
+
return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(AssessmentLessonGuard, { blockLabel: "ArithmeticQuiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ArithmeticQuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
5094
|
+
}
|
|
5095
|
+
);
|
|
5096
|
+
setLessonkitBlockType(ArithmeticQuiz, "ArithmeticQuiz");
|
|
5097
|
+
|
|
5098
|
+
// src/blocks/Essay.tsx
|
|
5099
|
+
var import_react38 = __toESM(require("react"), 1);
|
|
5100
|
+
var import_jsx_runtime28 = require("react/jsx-runtime");
|
|
5101
|
+
var INTERACTION10 = "essay";
|
|
5102
|
+
function EssayInner(props, ref) {
|
|
5103
|
+
const checkId = (0, import_react38.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
5104
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
5105
|
+
const [text, setText] = (0, import_react38.useState)("");
|
|
5106
|
+
const [submitted, setSubmitted] = (0, import_react38.useState)(false);
|
|
5107
|
+
const completedRef = (0, import_react38.useRef)(false);
|
|
5108
|
+
const telemetryReplayedRef = (0, import_react38.useRef)(false);
|
|
5109
|
+
const questionId = import_react38.default.useId();
|
|
5110
|
+
const minLength = props.minLength ?? 0;
|
|
5111
|
+
const meetsMinLength = text.trim().length >= minLength;
|
|
5112
|
+
const reset = () => {
|
|
5113
|
+
completedRef.current = false;
|
|
5114
|
+
telemetryReplayedRef.current = false;
|
|
5115
|
+
setText("");
|
|
5116
|
+
setSubmitted(false);
|
|
5117
|
+
};
|
|
5118
|
+
(0, import_react38.useEffect)(() => {
|
|
5119
|
+
reset();
|
|
5120
|
+
}, [checkId, props.question, props.minLength]);
|
|
5121
|
+
const handle = (0, import_react38.useMemo)(
|
|
5122
|
+
() => buildAssessmentHandle({
|
|
5123
|
+
checkId,
|
|
5124
|
+
getScore: () => 0,
|
|
5125
|
+
getMaxScore: () => 1,
|
|
5126
|
+
getAnswerGiven: () => submitted && meetsMinLength,
|
|
5127
|
+
resetTask: reset,
|
|
5128
|
+
showSolutions: () => {
|
|
5129
|
+
},
|
|
5130
|
+
getXAPIData: () => ({
|
|
5131
|
+
checkId,
|
|
5132
|
+
interactionType: INTERACTION10,
|
|
5133
|
+
question: props.question,
|
|
5134
|
+
response: text,
|
|
5135
|
+
score: 0,
|
|
5136
|
+
maxScore: 1
|
|
5137
|
+
}),
|
|
5138
|
+
getCurrentState: () => ({ text, submitted }),
|
|
5139
|
+
resume: (state) => {
|
|
5140
|
+
const nextText = readStringField(state, "text");
|
|
5141
|
+
if (typeof nextText === "string") setText(nextText);
|
|
5142
|
+
readBooleanStateField(state, "submitted", (value) => {
|
|
5143
|
+
setSubmitted(value);
|
|
5144
|
+
completedRef.current = value;
|
|
5145
|
+
if (value && !telemetryReplayedRef.current) {
|
|
5146
|
+
telemetryReplayedRef.current = true;
|
|
5147
|
+
const response = typeof nextText === "string" ? nextText : text;
|
|
5148
|
+
assessment.answer({
|
|
5149
|
+
checkId,
|
|
5150
|
+
interactionType: INTERACTION10,
|
|
5151
|
+
question: props.question,
|
|
5152
|
+
response,
|
|
5153
|
+
correct: false
|
|
5154
|
+
});
|
|
5155
|
+
assessment.complete({
|
|
5156
|
+
checkId,
|
|
5157
|
+
interactionType: INTERACTION10,
|
|
5158
|
+
score: 0,
|
|
5159
|
+
maxScore: 1,
|
|
5160
|
+
passingScore: props.passingScore ?? 1
|
|
5161
|
+
});
|
|
5162
|
+
}
|
|
5163
|
+
});
|
|
5164
|
+
}
|
|
5165
|
+
}),
|
|
5166
|
+
[checkId, meetsMinLength, props.question, submitted, text]
|
|
5167
|
+
);
|
|
5168
|
+
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
5169
|
+
const submit = () => {
|
|
5170
|
+
if (!meetsMinLength || submitted && !props.enableRetry) return;
|
|
5171
|
+
setSubmitted(true);
|
|
5172
|
+
if (!completedRef.current) {
|
|
5173
|
+
completedRef.current = true;
|
|
5174
|
+
assessment.answer({
|
|
5175
|
+
checkId,
|
|
5176
|
+
interactionType: INTERACTION10,
|
|
5177
|
+
question: props.question,
|
|
5178
|
+
response: text,
|
|
5179
|
+
correct: false
|
|
5180
|
+
});
|
|
5181
|
+
assessment.complete({
|
|
5182
|
+
checkId,
|
|
5183
|
+
interactionType: INTERACTION10,
|
|
5184
|
+
score: 0,
|
|
5185
|
+
maxScore: 1,
|
|
5186
|
+
passingScore: props.passingScore ?? 1
|
|
5187
|
+
});
|
|
5188
|
+
}
|
|
5189
|
+
};
|
|
5190
|
+
return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("section", { "aria-label": "Essay", "data-lk-check-id": checkId, "data-testid": "essay", children: [
|
|
5191
|
+
/* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { id: questionId, children: props.question }),
|
|
5192
|
+
/* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
|
|
5193
|
+
"textarea",
|
|
5194
|
+
{
|
|
5195
|
+
"aria-labelledby": questionId,
|
|
5196
|
+
"data-testid": "essay-textarea",
|
|
5197
|
+
value: text,
|
|
5198
|
+
disabled: submitted && !props.enableRetry,
|
|
5199
|
+
onChange: (e) => {
|
|
5200
|
+
if (submitted && !props.enableRetry) return;
|
|
5201
|
+
setSubmitted(false);
|
|
5202
|
+
completedRef.current = false;
|
|
5203
|
+
setText(e.target.value);
|
|
5204
|
+
},
|
|
5205
|
+
rows: 6,
|
|
5206
|
+
style: { width: "100%" }
|
|
5207
|
+
}
|
|
5208
|
+
),
|
|
5209
|
+
minLength > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("p", { "data-testid": "essay-min-length", children: [
|
|
5210
|
+
"Minimum length: ",
|
|
5211
|
+
minLength,
|
|
5212
|
+
" characters (",
|
|
5213
|
+
text.trim().length,
|
|
5214
|
+
"/",
|
|
5215
|
+
minLength,
|
|
5216
|
+
")"
|
|
5217
|
+
] }) : null,
|
|
5218
|
+
/* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
|
|
5219
|
+
"button",
|
|
5220
|
+
{
|
|
5221
|
+
type: "button",
|
|
5222
|
+
"data-testid": "essay-submit",
|
|
5223
|
+
disabled: !meetsMinLength || submitted && !props.enableRetry,
|
|
5224
|
+
onClick: submit,
|
|
5225
|
+
children: "Submit"
|
|
5226
|
+
}
|
|
5227
|
+
),
|
|
5228
|
+
submitted ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { role: "status", "aria-live": "polite", "data-testid": "essay-submitted", children: "Response submitted for review." }) : null,
|
|
5229
|
+
props.enableRetry && submitted ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("button", { type: "button", "data-testid": "essay-retry", onClick: reset, children: "Try again" }) : null
|
|
5230
|
+
] });
|
|
5231
|
+
}
|
|
5232
|
+
var EssayInnerForwarded = (0, import_react38.forwardRef)(EssayInner);
|
|
5233
|
+
var Essay = (0, import_react38.forwardRef)(function Essay2(props, ref) {
|
|
5234
|
+
return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(AssessmentLessonGuard, { blockLabel: "Essay", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(EssayInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
5235
|
+
});
|
|
5236
|
+
setLessonkitBlockType(Essay, "Essay");
|
|
5237
|
+
|
|
5238
|
+
// src/blocks/Questionnaire.tsx
|
|
5239
|
+
var import_react39 = require("react");
|
|
5240
|
+
var import_jsx_runtime29 = require("react/jsx-runtime");
|
|
5241
|
+
function Questionnaire(props) {
|
|
5242
|
+
const blockId = (0, import_react39.useMemo)(
|
|
5243
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
5244
|
+
[props.blockId]
|
|
5245
|
+
);
|
|
5246
|
+
const fieldsKey = props.fields.map((f) => `${f.id}:${f.type}:${f.label}`).join("|");
|
|
5247
|
+
const [values, setValues] = (0, import_react39.useState)(
|
|
5248
|
+
() => Object.fromEntries(props.fields.map((f) => [f.id, ""]))
|
|
5249
|
+
);
|
|
5250
|
+
const [submitted, setSubmitted] = (0, import_react39.useState)(false);
|
|
5251
|
+
const { track } = useLessonkit();
|
|
5252
|
+
const lessonId = useEnclosingLessonId();
|
|
5253
|
+
const baseId = (0, import_react39.useId)();
|
|
5254
|
+
(0, import_react39.useEffect)(() => {
|
|
5255
|
+
setValues(Object.fromEntries(props.fields.map((f) => [f.id, ""])));
|
|
5256
|
+
setSubmitted(false);
|
|
5257
|
+
}, [blockId, fieldsKey, props.fields]);
|
|
5258
|
+
const submit = () => {
|
|
5259
|
+
if (submitted) return;
|
|
5260
|
+
setSubmitted(true);
|
|
5261
|
+
if (lessonId) {
|
|
5262
|
+
track(
|
|
5263
|
+
"questionnaire_submitted",
|
|
5264
|
+
{ blockId, fieldCount: props.fields.length },
|
|
5265
|
+
{ lessonId }
|
|
5266
|
+
);
|
|
5267
|
+
}
|
|
5268
|
+
};
|
|
5269
|
+
return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("section", { "aria-label": "Questionnaire", "data-lk-block-id": blockId, "data-testid": "questionnaire", children: [
|
|
5270
|
+
/* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
|
|
5271
|
+
"form",
|
|
5272
|
+
{
|
|
5273
|
+
onSubmit: (e) => {
|
|
5274
|
+
e.preventDefault();
|
|
5275
|
+
submit();
|
|
5276
|
+
},
|
|
5277
|
+
children: [
|
|
5278
|
+
props.fields.map((field) => {
|
|
5279
|
+
const fieldId = `${baseId}-${field.id}`;
|
|
5280
|
+
return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { "data-testid": `questionnaire-field-${field.id}`, children: [
|
|
5281
|
+
/* @__PURE__ */ (0, import_jsx_runtime29.jsx)("label", { htmlFor: fieldId, children: field.label }),
|
|
5282
|
+
field.type === "textarea" ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
|
|
5283
|
+
"textarea",
|
|
5284
|
+
{
|
|
5285
|
+
id: fieldId,
|
|
5286
|
+
"data-testid": `questionnaire-input-${field.id}`,
|
|
5287
|
+
value: values[field.id] ?? "",
|
|
5288
|
+
disabled: submitted,
|
|
5289
|
+
rows: 4,
|
|
5290
|
+
style: { display: "block", width: "100%" },
|
|
5291
|
+
onChange: (e) => setValues((prev) => ({ ...prev, [field.id]: e.target.value }))
|
|
5292
|
+
}
|
|
5293
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
|
|
5294
|
+
"input",
|
|
5295
|
+
{
|
|
5296
|
+
id: fieldId,
|
|
5297
|
+
type: "text",
|
|
5298
|
+
"data-testid": `questionnaire-input-${field.id}`,
|
|
5299
|
+
value: values[field.id] ?? "",
|
|
5300
|
+
disabled: submitted,
|
|
5301
|
+
style: { display: "block", width: "100%" },
|
|
5302
|
+
onChange: (e) => setValues((prev) => ({ ...prev, [field.id]: e.target.value }))
|
|
5303
|
+
}
|
|
5304
|
+
)
|
|
5305
|
+
] }, field.id);
|
|
5306
|
+
}),
|
|
5307
|
+
/* @__PURE__ */ (0, import_jsx_runtime29.jsx)("button", { type: "submit", "data-testid": "questionnaire-submit", disabled: submitted, children: "Submit" })
|
|
5308
|
+
]
|
|
5309
|
+
}
|
|
5310
|
+
),
|
|
5311
|
+
submitted ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { role: "status", "aria-live": "polite", "data-testid": "questionnaire-submitted", children: "Thank you for your responses." }) : null
|
|
5312
|
+
] });
|
|
5313
|
+
}
|
|
5314
|
+
setLessonkitBlockType(Questionnaire, "Questionnaire");
|
|
5315
|
+
|
|
5316
|
+
// src/blocks/MemoryGame.tsx
|
|
5317
|
+
var import_react40 = require("react");
|
|
5318
|
+
var import_jsx_runtime30 = require("react/jsx-runtime");
|
|
5319
|
+
function shuffleCards2(cards) {
|
|
5320
|
+
const next = [...cards];
|
|
5321
|
+
for (let i = next.length - 1; i > 0; i -= 1) {
|
|
5322
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
5323
|
+
[next[i], next[j]] = [next[j], next[i]];
|
|
5324
|
+
}
|
|
5325
|
+
return next;
|
|
5326
|
+
}
|
|
5327
|
+
function buildDeck2(pairs) {
|
|
5328
|
+
const cards = pairs.flatMap(
|
|
5329
|
+
(pair) => [0, 1].map((copy) => ({
|
|
5330
|
+
cardKey: `${pair.id}-${copy}`,
|
|
5331
|
+
pairId: pair.id,
|
|
5332
|
+
label: pair.label
|
|
5333
|
+
}))
|
|
5334
|
+
);
|
|
5335
|
+
return shuffleCards2(cards);
|
|
5336
|
+
}
|
|
5337
|
+
function MemoryGame(props) {
|
|
5338
|
+
const pairsKey = props.pairs.map((p) => p.id).join("\0");
|
|
5339
|
+
const [cards, setCards] = (0, import_react40.useState)(() => buildDeck2(props.pairs));
|
|
5340
|
+
const [matched, setMatched] = (0, import_react40.useState)(() => /* @__PURE__ */ new Set());
|
|
5341
|
+
const [revealed, setRevealed] = (0, import_react40.useState)(() => /* @__PURE__ */ new Set());
|
|
5342
|
+
const [selection, setSelection] = (0, import_react40.useState)(null);
|
|
5343
|
+
const [complete, setComplete] = (0, import_react40.useState)(false);
|
|
5344
|
+
const { track } = useLessonkit();
|
|
5345
|
+
const lessonId = useEnclosingLessonId();
|
|
5346
|
+
const trackOpts = lessonId ? { lessonId } : void 0;
|
|
5347
|
+
(0, import_react40.useEffect)(() => {
|
|
5348
|
+
setCards(buildDeck2(props.pairs));
|
|
5349
|
+
setMatched(/* @__PURE__ */ new Set());
|
|
5350
|
+
setRevealed(/* @__PURE__ */ new Set());
|
|
5351
|
+
setSelection(null);
|
|
5352
|
+
setComplete(false);
|
|
5353
|
+
}, [props.blockId, pairsKey]);
|
|
5354
|
+
const cardIndexByKey = (0, import_react40.useMemo)(
|
|
5355
|
+
() => Object.fromEntries(cards.map((c, i) => [c.cardKey, i])),
|
|
5356
|
+
[cards]
|
|
5357
|
+
);
|
|
5358
|
+
const flipCard = (cardKey, face) => {
|
|
5359
|
+
const cardIndex = cardIndexByKey[cardKey];
|
|
5360
|
+
if (typeof cardIndex === "number") {
|
|
5361
|
+
track(
|
|
5362
|
+
"memory_card_flipped",
|
|
5363
|
+
{ blockId: props.blockId, cardIndex, face },
|
|
5364
|
+
trackOpts
|
|
5365
|
+
);
|
|
5366
|
+
}
|
|
5367
|
+
};
|
|
5368
|
+
const tryMatch = (firstKey, secondKey) => {
|
|
5369
|
+
const first = cards.find((c) => c.cardKey === firstKey);
|
|
5370
|
+
const second = cards.find((c) => c.cardKey === secondKey);
|
|
5371
|
+
if (!first || !second) return;
|
|
5372
|
+
setRevealed((prev) => /* @__PURE__ */ new Set([...prev, firstKey, secondKey]));
|
|
5373
|
+
flipCard(secondKey, "back");
|
|
5374
|
+
if (first.pairId === second.pairId) {
|
|
5375
|
+
setMatched((prev) => {
|
|
5376
|
+
const next = /* @__PURE__ */ new Set([...prev, first.pairId]);
|
|
5377
|
+
if (next.size === props.pairs.length) setComplete(true);
|
|
5378
|
+
return next;
|
|
5379
|
+
});
|
|
5380
|
+
setRevealed(/* @__PURE__ */ new Set());
|
|
5381
|
+
setSelection(null);
|
|
5382
|
+
} else {
|
|
5383
|
+
window.setTimeout(() => {
|
|
5384
|
+
setRevealed((prev) => {
|
|
5385
|
+
const next = new Set(prev);
|
|
5386
|
+
next.delete(firstKey);
|
|
5387
|
+
next.delete(secondKey);
|
|
5388
|
+
return next;
|
|
5389
|
+
});
|
|
5390
|
+
flipCard(firstKey, "front");
|
|
5391
|
+
flipCard(secondKey, "front");
|
|
5392
|
+
setSelection(null);
|
|
5393
|
+
}, 800);
|
|
5394
|
+
}
|
|
5395
|
+
};
|
|
5396
|
+
const selectCard = (cardKey) => {
|
|
5397
|
+
if (complete) return;
|
|
5398
|
+
if (matched.has(cards.find((c) => c.cardKey === cardKey)?.pairId ?? "")) return;
|
|
5399
|
+
if (selection === null) {
|
|
5400
|
+
setSelection(cardKey);
|
|
5401
|
+
setRevealed((prev) => /* @__PURE__ */ new Set([...prev, cardKey]));
|
|
5402
|
+
flipCard(cardKey, "back");
|
|
5403
|
+
return;
|
|
5404
|
+
}
|
|
5405
|
+
if (selection === cardKey) {
|
|
5406
|
+
setSelection(null);
|
|
5407
|
+
setRevealed((prev) => {
|
|
5408
|
+
const next = new Set(prev);
|
|
5409
|
+
next.delete(cardKey);
|
|
5410
|
+
return next;
|
|
5411
|
+
});
|
|
5412
|
+
flipCard(cardKey, "front");
|
|
5413
|
+
return;
|
|
5414
|
+
}
|
|
5415
|
+
tryMatch(selection, cardKey);
|
|
5416
|
+
};
|
|
5417
|
+
const restart = () => {
|
|
5418
|
+
setCards(buildDeck2(props.pairs));
|
|
5419
|
+
setMatched(/* @__PURE__ */ new Set());
|
|
5420
|
+
setRevealed(/* @__PURE__ */ new Set());
|
|
5421
|
+
setSelection(null);
|
|
5422
|
+
setComplete(false);
|
|
5423
|
+
};
|
|
5424
|
+
return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("section", { "aria-label": "Memory Game", "data-lk-block-id": props.blockId, "data-testid": "memory-game", children: [
|
|
5425
|
+
/* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { role: "list", "aria-label": "Memory cards", "data-testid": "memory-game-grid", children: cards.map((card) => {
|
|
5426
|
+
const isMatched = matched.has(card.pairId);
|
|
5427
|
+
const isRevealed = isMatched || revealed.has(card.cardKey);
|
|
5428
|
+
const isSelected = selection === card.cardKey;
|
|
5429
|
+
return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
|
|
5430
|
+
"button",
|
|
5431
|
+
{
|
|
5432
|
+
type: "button",
|
|
5433
|
+
role: "listitem",
|
|
5434
|
+
"data-testid": `memory-card-${card.cardKey}`,
|
|
5435
|
+
"aria-pressed": isSelected,
|
|
5436
|
+
disabled: isMatched || complete,
|
|
5437
|
+
onClick: () => selectCard(card.cardKey),
|
|
5438
|
+
style: {
|
|
5439
|
+
margin: "0.25rem",
|
|
5440
|
+
minWidth: "5rem",
|
|
5441
|
+
minHeight: "5rem",
|
|
5442
|
+
border: isSelected ? "2px solid currentColor" : "1px solid currentColor"
|
|
5443
|
+
},
|
|
5444
|
+
children: isRevealed ? card.label : "?"
|
|
5445
|
+
},
|
|
5446
|
+
card.cardKey
|
|
5447
|
+
);
|
|
5448
|
+
}) }),
|
|
5449
|
+
complete ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { role: "status", "aria-live": "polite", "data-testid": "memory-game-complete", children: "All pairs matched!" }) : null,
|
|
5450
|
+
props.selfScore ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { "data-testid": "memory-game-self-score", children: "Self-score mode enabled" }) : null,
|
|
5451
|
+
/* @__PURE__ */ (0, import_jsx_runtime30.jsx)("button", { type: "button", "data-testid": "memory-game-restart", onClick: restart, children: "Restart" })
|
|
5452
|
+
] });
|
|
5453
|
+
}
|
|
5454
|
+
setLessonkitBlockType(MemoryGame, "MemoryGame");
|
|
5455
|
+
|
|
5456
|
+
// src/blocks/InformationWall.tsx
|
|
5457
|
+
var import_react41 = require("react");
|
|
5458
|
+
var import_jsx_runtime31 = require("react/jsx-runtime");
|
|
5459
|
+
function InformationWall(props) {
|
|
5460
|
+
const blockId = (0, import_react41.useMemo)(
|
|
5461
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
5462
|
+
[props.blockId]
|
|
5463
|
+
);
|
|
5464
|
+
const [query, setQuery] = (0, import_react41.useState)("");
|
|
5465
|
+
const { track } = useLessonkit();
|
|
5466
|
+
const lessonId = useEnclosingLessonId();
|
|
5467
|
+
const trackOpts = lessonId ? { lessonId } : void 0;
|
|
5468
|
+
const debounceRef = (0, import_react41.useRef)(null);
|
|
5469
|
+
const filtered = (0, import_react41.useMemo)(() => {
|
|
5470
|
+
const q = query.trim().toLowerCase();
|
|
5471
|
+
if (!q) return props.panels;
|
|
5472
|
+
return props.panels.filter(
|
|
5473
|
+
(panel) => panel.title.toLowerCase().includes(q) || panel.body.toLowerCase().includes(q)
|
|
5474
|
+
);
|
|
5475
|
+
}, [props.panels, query]);
|
|
5476
|
+
(0, import_react41.useEffect)(
|
|
5477
|
+
() => () => {
|
|
5478
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
5479
|
+
},
|
|
5480
|
+
[]
|
|
5481
|
+
);
|
|
5482
|
+
const onSearch = (value) => {
|
|
5483
|
+
setQuery(value);
|
|
5484
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
5485
|
+
debounceRef.current = setTimeout(() => {
|
|
5486
|
+
const q = value.trim().toLowerCase();
|
|
5487
|
+
const resultCount = q ? props.panels.filter(
|
|
5488
|
+
(panel) => panel.title.toLowerCase().includes(q) || panel.body.toLowerCase().includes(q)
|
|
5489
|
+
).length : props.panels.length;
|
|
5490
|
+
track(
|
|
5491
|
+
"information_wall_search",
|
|
5492
|
+
{ blockId, query: value, resultCount },
|
|
5493
|
+
trackOpts
|
|
5494
|
+
);
|
|
5495
|
+
}, 300);
|
|
5496
|
+
};
|
|
5497
|
+
return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("section", { "aria-label": "Information Wall", "data-lk-block-id": blockId, "data-testid": "information-wall", children: [
|
|
5498
|
+
/* @__PURE__ */ (0, import_jsx_runtime31.jsx)("label", { htmlFor: `${blockId}-search`, children: "Search panels" }),
|
|
5499
|
+
/* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
|
|
5500
|
+
"input",
|
|
5501
|
+
{
|
|
5502
|
+
id: `${blockId}-search`,
|
|
5503
|
+
type: "search",
|
|
5504
|
+
"data-testid": "information-wall-search",
|
|
5505
|
+
value: query,
|
|
5506
|
+
placeholder: "Search\u2026",
|
|
5507
|
+
onChange: (e) => onSearch(e.target.value)
|
|
5508
|
+
}
|
|
5509
|
+
),
|
|
5510
|
+
/* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("p", { "data-testid": "information-wall-result-count", children: [
|
|
5511
|
+
filtered.length,
|
|
5512
|
+
" panel",
|
|
5513
|
+
filtered.length === 1 ? "" : "s"
|
|
5514
|
+
] }),
|
|
5515
|
+
/* @__PURE__ */ (0, import_jsx_runtime31.jsx)("ul", { "data-testid": "information-wall-panels", children: filtered.map((panel) => /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("li", { "data-testid": `information-panel-${panel.id}`, children: [
|
|
5516
|
+
/* @__PURE__ */ (0, import_jsx_runtime31.jsx)("h4", { children: panel.title }),
|
|
5517
|
+
/* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { children: panel.body })
|
|
5518
|
+
] }, panel.id)) })
|
|
5519
|
+
] });
|
|
5520
|
+
}
|
|
5521
|
+
setLessonkitBlockType(InformationWall, "InformationWall");
|
|
5522
|
+
|
|
5523
|
+
// src/blocks/ParallaxSlideshow.tsx
|
|
5524
|
+
var import_react42 = require("react");
|
|
5525
|
+
var import_jsx_runtime32 = require("react/jsx-runtime");
|
|
5526
|
+
function usePrefersReducedMotion() {
|
|
5527
|
+
const [reduced, setReduced] = (0, import_react42.useState)(false);
|
|
5528
|
+
(0, import_react42.useEffect)(() => {
|
|
5529
|
+
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
5530
|
+
setReduced(mq.matches);
|
|
5531
|
+
const onChange = (e) => setReduced(e.matches);
|
|
5532
|
+
mq.addEventListener("change", onChange);
|
|
5533
|
+
return () => mq.removeEventListener("change", onChange);
|
|
5534
|
+
}, []);
|
|
5535
|
+
return reduced;
|
|
5536
|
+
}
|
|
5537
|
+
function ParallaxSlideshow(props) {
|
|
5538
|
+
const [index, setIndex] = (0, import_react42.useState)(0);
|
|
5539
|
+
const reducedMotion = usePrefersReducedMotion();
|
|
5540
|
+
const { track } = useLessonkit();
|
|
5541
|
+
const lessonId = useEnclosingLessonId();
|
|
5542
|
+
const trackOpts = lessonId ? { lessonId } : void 0;
|
|
5543
|
+
const slide = props.slides[index];
|
|
5544
|
+
(0, import_react42.useEffect)(() => {
|
|
5545
|
+
track(
|
|
5546
|
+
"parallax_slide_viewed",
|
|
5547
|
+
{ blockId: props.blockId, slideIndex: index },
|
|
5548
|
+
trackOpts
|
|
5549
|
+
);
|
|
5550
|
+
}, [index, props.blockId, track, trackOpts]);
|
|
5551
|
+
if (!slide) return null;
|
|
5552
|
+
const goTo = (next) => {
|
|
5553
|
+
if (next < 0 || next >= props.slides.length) return;
|
|
5554
|
+
setIndex(next);
|
|
5555
|
+
};
|
|
5556
|
+
return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
|
|
5557
|
+
"section",
|
|
5558
|
+
{
|
|
5559
|
+
"aria-label": "Parallax slideshow",
|
|
5560
|
+
"data-lk-block-id": props.blockId,
|
|
5561
|
+
"data-testid": "parallax-slideshow",
|
|
5562
|
+
"data-reduced-motion": reducedMotion ? "true" : "false",
|
|
5563
|
+
children: [
|
|
5564
|
+
/* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
|
|
5565
|
+
"article",
|
|
5566
|
+
{
|
|
5567
|
+
"data-testid": `parallax-slide-${index}`,
|
|
5568
|
+
style: reducedMotion ? void 0 : {
|
|
5569
|
+
backgroundAttachment: "fixed",
|
|
5570
|
+
backgroundImage: slide.imageSrc ? `url(${slide.imageSrc})` : void 0,
|
|
5571
|
+
backgroundPosition: "center",
|
|
5572
|
+
backgroundSize: "cover",
|
|
5573
|
+
minHeight: "12rem",
|
|
5574
|
+
padding: "1rem"
|
|
5575
|
+
},
|
|
5576
|
+
children: [
|
|
5577
|
+
reducedMotion && slide.imageSrc ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
|
|
5578
|
+
"img",
|
|
5579
|
+
{
|
|
5580
|
+
src: slide.imageSrc,
|
|
5581
|
+
alt: "",
|
|
5582
|
+
"data-testid": "parallax-slide-image",
|
|
5583
|
+
style: { maxWidth: "100%" }
|
|
5584
|
+
}
|
|
5585
|
+
) : null,
|
|
5586
|
+
/* @__PURE__ */ (0, import_jsx_runtime32.jsx)("h3", { "data-testid": "parallax-slide-title", children: slide.title }),
|
|
5587
|
+
/* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { "data-testid": "parallax-slide-body", children: slide.body })
|
|
5588
|
+
]
|
|
5589
|
+
}
|
|
5590
|
+
),
|
|
5591
|
+
/* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("nav", { "aria-label": "Slide navigation", children: [
|
|
5592
|
+
/* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
|
|
5593
|
+
"button",
|
|
5594
|
+
{
|
|
5595
|
+
type: "button",
|
|
5596
|
+
"data-testid": "parallax-prev",
|
|
5597
|
+
disabled: index === 0,
|
|
5598
|
+
onClick: () => goTo(index - 1),
|
|
5599
|
+
children: "Previous"
|
|
5600
|
+
}
|
|
5601
|
+
),
|
|
5602
|
+
/* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { "data-testid": "parallax-progress", children: [
|
|
5603
|
+
index + 1,
|
|
5604
|
+
" / ",
|
|
5605
|
+
props.slides.length
|
|
5606
|
+
] }),
|
|
5607
|
+
/* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
|
|
5608
|
+
"button",
|
|
5609
|
+
{
|
|
5610
|
+
type: "button",
|
|
5611
|
+
"data-testid": "parallax-next",
|
|
5612
|
+
disabled: index >= props.slides.length - 1,
|
|
5613
|
+
onClick: () => goTo(index + 1),
|
|
5614
|
+
children: "Next"
|
|
5615
|
+
}
|
|
5616
|
+
)
|
|
5617
|
+
] })
|
|
5618
|
+
]
|
|
5619
|
+
}
|
|
5620
|
+
);
|
|
5621
|
+
}
|
|
5622
|
+
setLessonkitBlockType(ParallaxSlideshow, "ParallaxSlideshow");
|
|
5623
|
+
|
|
5624
|
+
// src/blocks/Accordion.tsx
|
|
5625
|
+
var import_react43 = require("react");
|
|
5626
|
+
var import_jsx_runtime33 = require("react/jsx-runtime");
|
|
5627
|
+
function Accordion(props) {
|
|
5628
|
+
if (isDevEnvironment4()) {
|
|
5629
|
+
validateAccordionSections(props.sections);
|
|
5630
|
+
}
|
|
5631
|
+
const [open, setOpen] = (0, import_react43.useState)(/* @__PURE__ */ new Set());
|
|
5632
|
+
const { track } = useLessonkit();
|
|
5633
|
+
const lessonId = useEnclosingLessonId();
|
|
5634
|
+
const baseId = (0, import_react43.useId)();
|
|
5635
|
+
const toggle = (sectionId) => {
|
|
5636
|
+
setOpen((prev) => {
|
|
5637
|
+
const next = new Set(prev);
|
|
5638
|
+
const expanded = !next.has(sectionId);
|
|
5639
|
+
if (expanded) next.add(sectionId);
|
|
5640
|
+
else next.delete(sectionId);
|
|
5641
|
+
track(
|
|
5642
|
+
"accordion_section_toggled",
|
|
5643
|
+
{ blockId: props.blockId, sectionId, expanded },
|
|
5644
|
+
lessonId ? { lessonId } : void 0
|
|
5645
|
+
);
|
|
5646
|
+
return next;
|
|
5647
|
+
});
|
|
5648
|
+
};
|
|
5649
|
+
return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
|
|
5650
|
+
const expanded = open.has(section.id);
|
|
5651
|
+
const panelId = `${baseId}-${section.id}`;
|
|
5652
|
+
const triggerId = `${baseId}-trigger-${section.id}`;
|
|
5653
|
+
return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { "data-testid": `accordion-section-${section.id}`, children: [
|
|
5654
|
+
/* @__PURE__ */ (0, import_jsx_runtime33.jsx)("h4", { children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
|
|
5655
|
+
"button",
|
|
5656
|
+
{
|
|
5657
|
+
id: triggerId,
|
|
5658
|
+
type: "button",
|
|
5659
|
+
"aria-expanded": expanded,
|
|
5660
|
+
"aria-controls": panelId,
|
|
5661
|
+
"data-testid": `accordion-trigger-${section.id}`,
|
|
5662
|
+
onClick: () => toggle(section.id),
|
|
5663
|
+
children: section.title
|
|
5664
|
+
}
|
|
5665
|
+
) }),
|
|
5666
|
+
expanded ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
|
|
5667
|
+
] }, section.id);
|
|
5668
|
+
}) });
|
|
5669
|
+
}
|
|
5670
|
+
setLessonkitBlockType(Accordion, "Accordion");
|
|
5671
|
+
|
|
5672
|
+
// src/blocks/DialogCards.tsx
|
|
5673
|
+
var import_react44 = require("react");
|
|
5674
|
+
var import_jsx_runtime34 = require("react/jsx-runtime");
|
|
5675
|
+
function DialogCards(props) {
|
|
5676
|
+
const [index, setIndex] = (0, import_react44.useState)(0);
|
|
5677
|
+
const [flipped, setFlipped] = (0, import_react44.useState)(false);
|
|
5678
|
+
const card = props.cards[index];
|
|
5679
|
+
if (!card) return null;
|
|
5680
|
+
return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
|
|
5681
|
+
/* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("p", { children: [
|
|
5682
|
+
"Card ",
|
|
5683
|
+
index + 1,
|
|
5684
|
+
" of ",
|
|
5685
|
+
props.cards.length
|
|
5686
|
+
] }),
|
|
5687
|
+
/* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
|
|
5688
|
+
"button",
|
|
5689
|
+
{
|
|
5690
|
+
type: "button",
|
|
5691
|
+
"data-testid": "dialog-card-flip",
|
|
5692
|
+
"aria-pressed": flipped,
|
|
5693
|
+
onClick: () => setFlipped((f) => !f),
|
|
5694
|
+
style: { minHeight: "6rem", width: "100%" },
|
|
5695
|
+
children: flipped ? card.back : card.front
|
|
5696
|
+
}
|
|
5697
|
+
),
|
|
5698
|
+
/* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("nav", { "aria-label": "Card navigation", children: [
|
|
5699
|
+
/* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
|
|
5700
|
+
"button",
|
|
5701
|
+
{
|
|
5702
|
+
type: "button",
|
|
5703
|
+
"data-testid": "dialog-prev",
|
|
5704
|
+
disabled: index === 0,
|
|
5705
|
+
onClick: () => {
|
|
5706
|
+
setIndex((i) => i - 1);
|
|
5707
|
+
setFlipped(false);
|
|
5708
|
+
},
|
|
3940
5709
|
children: "Previous"
|
|
3941
5710
|
}
|
|
3942
5711
|
),
|
|
3943
|
-
/* @__PURE__ */ (0,
|
|
5712
|
+
/* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
|
|
3944
5713
|
"button",
|
|
3945
5714
|
{
|
|
3946
5715
|
type: "button",
|
|
@@ -3959,11 +5728,11 @@ function DialogCards(props) {
|
|
|
3959
5728
|
setLessonkitBlockType(DialogCards, "DialogCards");
|
|
3960
5729
|
|
|
3961
5730
|
// src/blocks/Flashcards.tsx
|
|
3962
|
-
var
|
|
3963
|
-
var
|
|
5731
|
+
var import_react45 = require("react");
|
|
5732
|
+
var import_jsx_runtime35 = require("react/jsx-runtime");
|
|
3964
5733
|
function Flashcards(props) {
|
|
3965
|
-
const [index, setIndex] = (0,
|
|
3966
|
-
const [face, setFace] = (0,
|
|
5734
|
+
const [index, setIndex] = (0, import_react45.useState)(0);
|
|
5735
|
+
const [face, setFace] = (0, import_react45.useState)("front");
|
|
3967
5736
|
const { track } = useLessonkit();
|
|
3968
5737
|
const lessonId = useEnclosingLessonId();
|
|
3969
5738
|
const card = props.cards[index];
|
|
@@ -3977,10 +5746,10 @@ function Flashcards(props) {
|
|
|
3977
5746
|
lessonId ? { lessonId } : void 0
|
|
3978
5747
|
);
|
|
3979
5748
|
};
|
|
3980
|
-
return /* @__PURE__ */ (0,
|
|
3981
|
-
/* @__PURE__ */ (0,
|
|
3982
|
-
props.selfScore ? /* @__PURE__ */ (0,
|
|
3983
|
-
/* @__PURE__ */ (0,
|
|
5749
|
+
return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
|
|
5750
|
+
/* @__PURE__ */ (0, import_jsx_runtime35.jsx)("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
|
|
5751
|
+
props.selfScore ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
|
|
5752
|
+
/* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
|
|
3984
5753
|
"button",
|
|
3985
5754
|
{
|
|
3986
5755
|
type: "button",
|
|
@@ -3998,10 +5767,10 @@ function Flashcards(props) {
|
|
|
3998
5767
|
setLessonkitBlockType(Flashcards, "Flashcards");
|
|
3999
5768
|
|
|
4000
5769
|
// src/blocks/ImageHotspots.tsx
|
|
4001
|
-
var
|
|
4002
|
-
var
|
|
5770
|
+
var import_react46 = require("react");
|
|
5771
|
+
var import_jsx_runtime36 = require("react/jsx-runtime");
|
|
4003
5772
|
function ImageHotspots(props) {
|
|
4004
|
-
const [active, setActive] = (0,
|
|
5773
|
+
const [active, setActive] = (0, import_react46.useState)(null);
|
|
4005
5774
|
const { track } = useLessonkit();
|
|
4006
5775
|
const lessonId = useEnclosingLessonId();
|
|
4007
5776
|
const open = (hotspotId) => {
|
|
@@ -4012,10 +5781,10 @@ function ImageHotspots(props) {
|
|
|
4012
5781
|
lessonId ? { lessonId } : void 0
|
|
4013
5782
|
);
|
|
4014
5783
|
};
|
|
4015
|
-
return /* @__PURE__ */ (0,
|
|
4016
|
-
/* @__PURE__ */ (0,
|
|
4017
|
-
/* @__PURE__ */ (0,
|
|
4018
|
-
props.hotspots.map((h) => /* @__PURE__ */ (0,
|
|
5784
|
+
return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
|
|
5785
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
5786
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
5787
|
+
props.hotspots.map((h) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
|
|
4019
5788
|
"button",
|
|
4020
5789
|
{
|
|
4021
5790
|
type: "button",
|
|
@@ -4034,19 +5803,19 @@ function ImageHotspots(props) {
|
|
|
4034
5803
|
h.id
|
|
4035
5804
|
))
|
|
4036
5805
|
] }),
|
|
4037
|
-
active ? /* @__PURE__ */ (0,
|
|
5806
|
+
active ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
|
|
4038
5807
|
props.hotspots.find((h) => h.id === active)?.content,
|
|
4039
|
-
/* @__PURE__ */ (0,
|
|
5808
|
+
/* @__PURE__ */ (0, import_jsx_runtime36.jsx)("button", { type: "button", onClick: () => setActive(null), children: "Close" })
|
|
4040
5809
|
] }) : null
|
|
4041
5810
|
] });
|
|
4042
5811
|
}
|
|
4043
5812
|
setLessonkitBlockType(ImageHotspots, "ImageHotspots");
|
|
4044
5813
|
|
|
4045
5814
|
// src/blocks/ImageSlider.tsx
|
|
4046
|
-
var
|
|
4047
|
-
var
|
|
5815
|
+
var import_react47 = require("react");
|
|
5816
|
+
var import_jsx_runtime37 = require("react/jsx-runtime");
|
|
4048
5817
|
function ImageSlider(props) {
|
|
4049
|
-
const [index, setIndex] = (0,
|
|
5818
|
+
const [index, setIndex] = (0, import_react47.useState)(0);
|
|
4050
5819
|
const { track } = useLessonkit();
|
|
4051
5820
|
const lessonId = useEnclosingLessonId();
|
|
4052
5821
|
const slide = props.slides[index];
|
|
@@ -4059,11 +5828,11 @@ function ImageSlider(props) {
|
|
|
4059
5828
|
lessonId ? { lessonId } : void 0
|
|
4060
5829
|
);
|
|
4061
5830
|
};
|
|
4062
|
-
return /* @__PURE__ */ (0,
|
|
4063
|
-
/* @__PURE__ */ (0,
|
|
4064
|
-
slide.caption ? /* @__PURE__ */ (0,
|
|
4065
|
-
/* @__PURE__ */ (0,
|
|
4066
|
-
/* @__PURE__ */ (0,
|
|
5831
|
+
return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
|
|
5832
|
+
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
|
|
5833
|
+
slide.caption ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { children: slide.caption }) : null,
|
|
5834
|
+
/* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("nav", { "aria-label": "Slide navigation", children: [
|
|
5835
|
+
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
|
|
4067
5836
|
"button",
|
|
4068
5837
|
{
|
|
4069
5838
|
type: "button",
|
|
@@ -4073,12 +5842,12 @@ function ImageSlider(props) {
|
|
|
4073
5842
|
children: "Previous"
|
|
4074
5843
|
}
|
|
4075
5844
|
),
|
|
4076
|
-
/* @__PURE__ */ (0,
|
|
5845
|
+
/* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { children: [
|
|
4077
5846
|
index + 1,
|
|
4078
5847
|
" / ",
|
|
4079
5848
|
props.slides.length
|
|
4080
5849
|
] }),
|
|
4081
|
-
/* @__PURE__ */ (0,
|
|
5850
|
+
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
|
|
4082
5851
|
"button",
|
|
4083
5852
|
{
|
|
4084
5853
|
type: "button",
|
|
@@ -4094,17 +5863,17 @@ function ImageSlider(props) {
|
|
|
4094
5863
|
setLessonkitBlockType(ImageSlider, "ImageSlider");
|
|
4095
5864
|
|
|
4096
5865
|
// src/blocks/FindHotspot.tsx
|
|
4097
|
-
var
|
|
4098
|
-
var
|
|
4099
|
-
var
|
|
5866
|
+
var import_react48 = require("react");
|
|
5867
|
+
var import_jsx_runtime38 = require("react/jsx-runtime");
|
|
5868
|
+
var INTERACTION11 = "findHotspot";
|
|
4100
5869
|
function FindHotspotInner(props, ref) {
|
|
4101
|
-
const checkId = (0,
|
|
4102
|
-
const [selected, setSelected] = (0,
|
|
4103
|
-
const [checked, setChecked] = (0,
|
|
4104
|
-
const telemetryReplayedRef = (0,
|
|
5870
|
+
const checkId = (0, import_react48.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
5871
|
+
const [selected, setSelected] = (0, import_react48.useState)(null);
|
|
5872
|
+
const [checked, setChecked] = (0, import_react48.useState)(false);
|
|
5873
|
+
const telemetryReplayedRef = (0, import_react48.useRef)(false);
|
|
4105
5874
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4106
5875
|
const targetIdsKey = props.targets.map((t) => t.id).join("\0");
|
|
4107
|
-
(0,
|
|
5876
|
+
(0, import_react48.useEffect)(() => {
|
|
4108
5877
|
setSelected(null);
|
|
4109
5878
|
setChecked(false);
|
|
4110
5879
|
telemetryReplayedRef.current = false;
|
|
@@ -4115,21 +5884,21 @@ function FindHotspotInner(props, ref) {
|
|
|
4115
5884
|
telemetryReplayedRef.current = true;
|
|
4116
5885
|
assessment.answer({
|
|
4117
5886
|
checkId,
|
|
4118
|
-
interactionType:
|
|
5887
|
+
interactionType: INTERACTION11,
|
|
4119
5888
|
response: nextSelected,
|
|
4120
5889
|
correct: nextCorrect
|
|
4121
5890
|
});
|
|
4122
5891
|
if (nextCorrect) {
|
|
4123
5892
|
assessment.complete({
|
|
4124
5893
|
checkId,
|
|
4125
|
-
interactionType:
|
|
5894
|
+
interactionType: INTERACTION11,
|
|
4126
5895
|
score: 1,
|
|
4127
5896
|
maxScore: 1,
|
|
4128
5897
|
passingScore: props.passingScore ?? 1
|
|
4129
5898
|
});
|
|
4130
5899
|
}
|
|
4131
5900
|
};
|
|
4132
|
-
const handle = (0,
|
|
5901
|
+
const handle = (0, import_react48.useMemo)(
|
|
4133
5902
|
() => buildAssessmentHandle({
|
|
4134
5903
|
checkId,
|
|
4135
5904
|
getScore: () => checked && correct ? 1 : 0,
|
|
@@ -4143,7 +5912,7 @@ function FindHotspotInner(props, ref) {
|
|
|
4143
5912
|
showSolutions: () => setSelected(props.correctTargetId),
|
|
4144
5913
|
getXAPIData: () => ({
|
|
4145
5914
|
checkId,
|
|
4146
|
-
interactionType:
|
|
5915
|
+
interactionType: INTERACTION11,
|
|
4147
5916
|
response: selected ?? void 0,
|
|
4148
5917
|
correct: checked ? correct : void 0,
|
|
4149
5918
|
score: checked && correct ? 1 : 0,
|
|
@@ -4179,24 +5948,24 @@ function FindHotspotInner(props, ref) {
|
|
|
4179
5948
|
setChecked(true);
|
|
4180
5949
|
assessment.answer({
|
|
4181
5950
|
checkId,
|
|
4182
|
-
interactionType:
|
|
5951
|
+
interactionType: INTERACTION11,
|
|
4183
5952
|
response: selected,
|
|
4184
5953
|
correct
|
|
4185
5954
|
});
|
|
4186
5955
|
if (correct) {
|
|
4187
5956
|
assessment.complete({
|
|
4188
5957
|
checkId,
|
|
4189
|
-
interactionType:
|
|
5958
|
+
interactionType: INTERACTION11,
|
|
4190
5959
|
score: 1,
|
|
4191
5960
|
maxScore: 1,
|
|
4192
5961
|
passingScore: props.passingScore ?? 1
|
|
4193
5962
|
});
|
|
4194
5963
|
}
|
|
4195
5964
|
};
|
|
4196
|
-
return /* @__PURE__ */ (0,
|
|
4197
|
-
/* @__PURE__ */ (0,
|
|
4198
|
-
/* @__PURE__ */ (0,
|
|
4199
|
-
props.targets.map((t) => /* @__PURE__ */ (0,
|
|
5965
|
+
return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
|
|
5966
|
+
/* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
5967
|
+
/* @__PURE__ */ (0, import_jsx_runtime38.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
5968
|
+
props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
|
|
4200
5969
|
"button",
|
|
4201
5970
|
{
|
|
4202
5971
|
type: "button",
|
|
@@ -4215,24 +5984,24 @@ function FindHotspotInner(props, ref) {
|
|
|
4215
5984
|
t.id
|
|
4216
5985
|
))
|
|
4217
5986
|
] }),
|
|
4218
|
-
/* @__PURE__ */ (0,
|
|
4219
|
-
checked ? /* @__PURE__ */ (0,
|
|
5987
|
+
/* @__PURE__ */ (0, import_jsx_runtime38.jsx)("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
|
|
5988
|
+
checked ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
4220
5989
|
] });
|
|
4221
5990
|
}
|
|
4222
|
-
var FindHotspotInnerForwarded = (0,
|
|
4223
|
-
var FindHotspot = (0,
|
|
4224
|
-
return /* @__PURE__ */ (0,
|
|
5991
|
+
var FindHotspotInnerForwarded = (0, import_react48.forwardRef)(FindHotspotInner);
|
|
5992
|
+
var FindHotspot = (0, import_react48.forwardRef)(function FindHotspot2(props, ref) {
|
|
5993
|
+
return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
4225
5994
|
});
|
|
4226
5995
|
setLessonkitBlockType(FindHotspot, "FindHotspot");
|
|
4227
5996
|
|
|
4228
5997
|
// src/blocks/FindMultipleHotspots.tsx
|
|
4229
|
-
var
|
|
4230
|
-
var
|
|
4231
|
-
var
|
|
5998
|
+
var import_react49 = require("react");
|
|
5999
|
+
var import_jsx_runtime39 = require("react/jsx-runtime");
|
|
6000
|
+
var INTERACTION12 = "findMultipleHotspots";
|
|
4232
6001
|
function FindMultipleHotspotsInner(props, ref) {
|
|
4233
|
-
const checkId = (0,
|
|
4234
|
-
const [selected, setSelected] = (0,
|
|
4235
|
-
const [checked, setChecked] = (0,
|
|
6002
|
+
const checkId = (0, import_react49.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
6003
|
+
const [selected, setSelected] = (0, import_react49.useState)(/* @__PURE__ */ new Set());
|
|
6004
|
+
const [checked, setChecked] = (0, import_react49.useState)(false);
|
|
4236
6005
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4237
6006
|
const toggle = (id) => {
|
|
4238
6007
|
setSelected((prev) => {
|
|
@@ -4244,7 +6013,7 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
4244
6013
|
setChecked(false);
|
|
4245
6014
|
};
|
|
4246
6015
|
const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
|
|
4247
|
-
const handle = (0,
|
|
6016
|
+
const handle = (0, import_react49.useMemo)(
|
|
4248
6017
|
() => buildAssessmentHandle({
|
|
4249
6018
|
checkId,
|
|
4250
6019
|
getScore: () => checked && correct ? 1 : 0,
|
|
@@ -4257,7 +6026,7 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
4257
6026
|
showSolutions: () => setSelected(new Set(props.correctTargetIds)),
|
|
4258
6027
|
getXAPIData: () => ({
|
|
4259
6028
|
checkId,
|
|
4260
|
-
interactionType:
|
|
6029
|
+
interactionType: INTERACTION12,
|
|
4261
6030
|
response: [...selected],
|
|
4262
6031
|
correct: checked ? correct : void 0,
|
|
4263
6032
|
score: checked && correct ? 1 : 0,
|
|
@@ -4278,24 +6047,24 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
4278
6047
|
setChecked(true);
|
|
4279
6048
|
assessment.answer({
|
|
4280
6049
|
checkId,
|
|
4281
|
-
interactionType:
|
|
6050
|
+
interactionType: INTERACTION12,
|
|
4282
6051
|
response: [...selected],
|
|
4283
6052
|
correct
|
|
4284
6053
|
});
|
|
4285
6054
|
if (correct) {
|
|
4286
6055
|
assessment.complete({
|
|
4287
6056
|
checkId,
|
|
4288
|
-
interactionType:
|
|
6057
|
+
interactionType: INTERACTION12,
|
|
4289
6058
|
score: 1,
|
|
4290
6059
|
maxScore: 1,
|
|
4291
6060
|
passingScore: props.passingScore ?? 1
|
|
4292
6061
|
});
|
|
4293
6062
|
}
|
|
4294
6063
|
};
|
|
4295
|
-
return /* @__PURE__ */ (0,
|
|
4296
|
-
/* @__PURE__ */ (0,
|
|
4297
|
-
/* @__PURE__ */ (0,
|
|
4298
|
-
props.targets.map((t) => /* @__PURE__ */ (0,
|
|
6064
|
+
return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
|
|
6065
|
+
/* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
6066
|
+
/* @__PURE__ */ (0, import_jsx_runtime39.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
6067
|
+
props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
|
|
4299
6068
|
"button",
|
|
4300
6069
|
{
|
|
4301
6070
|
type: "button",
|
|
@@ -4314,23 +6083,23 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
4314
6083
|
t.id
|
|
4315
6084
|
))
|
|
4316
6085
|
] }),
|
|
4317
|
-
/* @__PURE__ */ (0,
|
|
4318
|
-
checked ? /* @__PURE__ */ (0,
|
|
6086
|
+
/* @__PURE__ */ (0, import_jsx_runtime39.jsx)("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
|
|
6087
|
+
checked ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
4319
6088
|
] });
|
|
4320
6089
|
}
|
|
4321
|
-
var FindMultipleHotspotsInnerForwarded = (0,
|
|
4322
|
-
var FindMultipleHotspots = (0,
|
|
6090
|
+
var FindMultipleHotspotsInnerForwarded = (0, import_react49.forwardRef)(FindMultipleHotspotsInner);
|
|
6091
|
+
var FindMultipleHotspots = (0, import_react49.forwardRef)(
|
|
4323
6092
|
function FindMultipleHotspots2(props, ref) {
|
|
4324
|
-
return /* @__PURE__ */ (0,
|
|
6093
|
+
return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
4325
6094
|
}
|
|
4326
6095
|
);
|
|
4327
6096
|
setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
|
|
4328
6097
|
|
|
4329
6098
|
// src/index.tsx
|
|
4330
|
-
var
|
|
6099
|
+
var import_core21 = require("@lessonkit/core");
|
|
4331
6100
|
|
|
4332
6101
|
// src/theme/ThemeProvider.tsx
|
|
4333
|
-
var
|
|
6102
|
+
var import_react50 = __toESM(require("react"), 1);
|
|
4334
6103
|
var import_themes = require("@lessonkit/themes");
|
|
4335
6104
|
|
|
4336
6105
|
// src/theme/applyCssVariables.ts
|
|
@@ -4349,11 +6118,11 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
4349
6118
|
}
|
|
4350
6119
|
|
|
4351
6120
|
// src/theme/ThemeProvider.tsx
|
|
4352
|
-
var
|
|
4353
|
-
var ThemeContext = (0,
|
|
6121
|
+
var import_jsx_runtime40 = require("react/jsx-runtime");
|
|
6122
|
+
var ThemeContext = (0, import_react50.createContext)(null);
|
|
4354
6123
|
var useIsoLayoutEffect2 = (
|
|
4355
6124
|
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
4356
|
-
typeof window !== "undefined" ?
|
|
6125
|
+
typeof window !== "undefined" ? import_react50.useLayoutEffect : import_react50.default.useEffect
|
|
4357
6126
|
);
|
|
4358
6127
|
function getSystemMode() {
|
|
4359
6128
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
@@ -4372,7 +6141,7 @@ function ThemeProvider(props) {
|
|
|
4372
6141
|
const preset = props.preset ?? "default";
|
|
4373
6142
|
const mode = props.mode ?? "light";
|
|
4374
6143
|
const targetKind = props.target ?? "document";
|
|
4375
|
-
const [resolvedMode, setResolvedMode] = (0,
|
|
6144
|
+
const [resolvedMode, setResolvedMode] = (0, import_react50.useState)(
|
|
4376
6145
|
() => mode === "system" ? getSystemMode() : mode
|
|
4377
6146
|
);
|
|
4378
6147
|
useIsoLayoutEffect2(() => {
|
|
@@ -4388,20 +6157,20 @@ function ThemeProvider(props) {
|
|
|
4388
6157
|
return () => mq.removeEventListener("change", onChange);
|
|
4389
6158
|
}, [mode]);
|
|
4390
6159
|
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
4391
|
-
const effectiveTheme = (0,
|
|
6160
|
+
const effectiveTheme = (0, import_react50.useMemo)(() => {
|
|
4392
6161
|
const modeBase = resolveModeBase(mode, dataTheme);
|
|
4393
6162
|
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));
|
|
4394
6163
|
return (0, import_themes.mergeThemes)(base, props.theme ?? {});
|
|
4395
6164
|
}, [preset, mode, dataTheme, props.theme]);
|
|
4396
|
-
const hostRef = (0,
|
|
4397
|
-
const appliedKeysRef = (0,
|
|
6165
|
+
const hostRef = (0, import_react50.useRef)(null);
|
|
6166
|
+
const appliedKeysRef = (0, import_react50.useRef)(/* @__PURE__ */ new Set());
|
|
4398
6167
|
useIsoLayoutEffect2(() => {
|
|
4399
6168
|
if (targetKind === "document" && typeof document !== "undefined") {
|
|
4400
6169
|
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
4401
6170
|
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
4402
6171
|
}
|
|
4403
6172
|
}, [targetKind, dataTheme]);
|
|
4404
|
-
const inject = (0,
|
|
6173
|
+
const inject = (0, import_react50.useCallback)(() => {
|
|
4405
6174
|
const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
|
|
4406
6175
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
4407
6176
|
if (!el) return;
|
|
@@ -4418,7 +6187,7 @@ function ThemeProvider(props) {
|
|
|
4418
6187
|
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
4419
6188
|
};
|
|
4420
6189
|
}, [inject, targetKind]);
|
|
4421
|
-
const value = (0,
|
|
6190
|
+
const value = (0, import_react50.useMemo)(
|
|
4422
6191
|
() => ({
|
|
4423
6192
|
theme: effectiveTheme,
|
|
4424
6193
|
preset,
|
|
@@ -4428,12 +6197,12 @@ function ThemeProvider(props) {
|
|
|
4428
6197
|
[effectiveTheme, preset, mode, dataTheme]
|
|
4429
6198
|
);
|
|
4430
6199
|
if (targetKind === "document") {
|
|
4431
|
-
return /* @__PURE__ */ (0,
|
|
6200
|
+
return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
|
|
4432
6201
|
}
|
|
4433
|
-
return /* @__PURE__ */ (0,
|
|
6202
|
+
return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
4434
6203
|
}
|
|
4435
6204
|
function useTheme() {
|
|
4436
|
-
const ctx = (0,
|
|
6205
|
+
const ctx = (0, import_react50.useContext)(ThemeContext);
|
|
4437
6206
|
if (!ctx) {
|
|
4438
6207
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
4439
6208
|
}
|
|
@@ -4441,13 +6210,15 @@ function useTheme() {
|
|
|
4441
6210
|
}
|
|
4442
6211
|
|
|
4443
6212
|
// src/catalogV3Entries.ts
|
|
4444
|
-
var
|
|
6213
|
+
var import_core20 = require("@lessonkit/core");
|
|
4445
6214
|
var COMPOUND_PARENTS = [
|
|
4446
6215
|
"Lesson",
|
|
4447
6216
|
"Page",
|
|
4448
6217
|
"InteractiveBook",
|
|
4449
6218
|
"Slide",
|
|
4450
6219
|
"SlideDeck",
|
|
6220
|
+
"TimedCue",
|
|
6221
|
+
"InteractiveVideo",
|
|
4451
6222
|
"AssessmentSequence"
|
|
4452
6223
|
];
|
|
4453
6224
|
function extendParents(entry) {
|
|
@@ -4506,6 +6277,23 @@ var v3CompoundAndContentEntries = [
|
|
|
4506
6277
|
theming: { surface: "global-inherit", stylingNotes: "Responsive max-width." },
|
|
4507
6278
|
telemetry: { emits: [] }
|
|
4508
6279
|
},
|
|
6280
|
+
{
|
|
6281
|
+
type: "Video",
|
|
6282
|
+
category: "content",
|
|
6283
|
+
description: "Self-hosted video with native controls and optional captions.",
|
|
6284
|
+
props: [
|
|
6285
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
6286
|
+
{ name: "src", type: "string", required: true, description: "Video URL." },
|
|
6287
|
+
{ name: "poster", type: "string", required: false, description: "Poster image URL." },
|
|
6288
|
+
{ name: "captions", type: "string", required: false, description: "WebVTT captions URL." },
|
|
6289
|
+
{ name: "title", type: "string", required: false, description: "Accessible title." }
|
|
6290
|
+
],
|
|
6291
|
+
requiredIds: ["blockId"],
|
|
6292
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
6293
|
+
a11y: { element: "video", ariaLabel: "Video", keyboard: "Native video controls.", notes: "No autoplay with sound." },
|
|
6294
|
+
theming: { surface: "global-inherit", stylingNotes: "Responsive video." },
|
|
6295
|
+
telemetry: { emits: [] }
|
|
6296
|
+
},
|
|
4509
6297
|
{
|
|
4510
6298
|
type: "Page",
|
|
4511
6299
|
category: "container",
|
|
@@ -4513,8 +6301,8 @@ var v3CompoundAndContentEntries = [
|
|
|
4513
6301
|
h5pMachineName: "H5P.Column",
|
|
4514
6302
|
h5pAlias: "Column",
|
|
4515
6303
|
description: "Column layout container (H5P Column / Page).",
|
|
4516
|
-
allowedChildTypes: [...
|
|
4517
|
-
maxNestingDepth:
|
|
6304
|
+
allowedChildTypes: [...import_core20.PAGE_ALLOWED_CHILD_TYPES],
|
|
6305
|
+
maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.Page,
|
|
4518
6306
|
props: [
|
|
4519
6307
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4520
6308
|
{ name: "title", type: "string", required: false, description: "Page title." },
|
|
@@ -4534,8 +6322,8 @@ var v3CompoundAndContentEntries = [
|
|
|
4534
6322
|
h5pMachineName: "H5P.InteractiveBook",
|
|
4535
6323
|
h5pAlias: "Interactive Book",
|
|
4536
6324
|
description: "Multi-page book with chapter navigation.",
|
|
4537
|
-
allowedChildTypes: [...
|
|
4538
|
-
maxNestingDepth:
|
|
6325
|
+
allowedChildTypes: [...import_core20.INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
|
|
6326
|
+
maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.InteractiveBook,
|
|
4539
6327
|
props: [
|
|
4540
6328
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4541
6329
|
{ name: "title", type: "string", required: true, description: "Book title." },
|
|
@@ -4559,9 +6347,9 @@ var v3CompoundAndContentEntries = [
|
|
|
4559
6347
|
compoundContract: true,
|
|
4560
6348
|
h5pMachineName: "H5P.CoursePresentation",
|
|
4561
6349
|
h5pAlias: "Course Presentation slide",
|
|
4562
|
-
description: "Single slide row in a SlideDeck.
|
|
4563
|
-
allowedChildTypes: [...
|
|
4564
|
-
maxNestingDepth:
|
|
6350
|
+
description: "Single slide row in a SlideDeck. Supports Video, Summary, and 1.4 blocks.",
|
|
6351
|
+
allowedChildTypes: [...import_core20.SLIDE_ALLOWED_CHILD_TYPES],
|
|
6352
|
+
maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.Slide,
|
|
4565
6353
|
props: [
|
|
4566
6354
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4567
6355
|
{ name: "title", type: "string", required: false, description: "Slide title." },
|
|
@@ -4581,8 +6369,8 @@ var v3CompoundAndContentEntries = [
|
|
|
4581
6369
|
h5pMachineName: "H5P.CoursePresentation",
|
|
4582
6370
|
h5pAlias: "Course Presentation",
|
|
4583
6371
|
description: "Multi-slide presentation with keyboard navigation.",
|
|
4584
|
-
allowedChildTypes: [...
|
|
4585
|
-
maxNestingDepth:
|
|
6372
|
+
allowedChildTypes: [...import_core20.SLIDE_DECK_ALLOWED_CHILD_TYPES],
|
|
6373
|
+
maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.SlideDeck,
|
|
4586
6374
|
props: [
|
|
4587
6375
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4588
6376
|
{ name: "title", type: "string", required: true, description: "Deck title." },
|
|
@@ -4600,6 +6388,121 @@ var v3CompoundAndContentEntries = [
|
|
|
4600
6388
|
theming: { surface: "global-inherit", stylingNotes: "Deck chrome." },
|
|
4601
6389
|
telemetry: { emits: ["slide_viewed"], requiresActiveLesson: true }
|
|
4602
6390
|
},
|
|
6391
|
+
{
|
|
6392
|
+
type: "TimedCue",
|
|
6393
|
+
category: "container",
|
|
6394
|
+
compoundContract: true,
|
|
6395
|
+
h5pMachineName: "H5P.InteractiveVideo",
|
|
6396
|
+
h5pAlias: "Interactive Video timed cue",
|
|
6397
|
+
description: "Timed overlay cue within InteractiveVideo.",
|
|
6398
|
+
allowedChildTypes: [...import_core20.TIMED_CUE_ALLOWED_CHILD_TYPES],
|
|
6399
|
+
maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.TimedCue,
|
|
6400
|
+
props: [
|
|
6401
|
+
{ name: "atSeconds", type: "number", required: true, description: "Cue time in seconds." },
|
|
6402
|
+
{ name: "label", type: "string", required: false, description: "Cue label." },
|
|
6403
|
+
{ name: "mustComplete", type: "boolean", required: false, description: "Block seek until completed." },
|
|
6404
|
+
{ name: "children", type: "ReactNode", required: true, description: "Single allowed child block." }
|
|
6405
|
+
],
|
|
6406
|
+
requiredIds: [],
|
|
6407
|
+
parentConstraints: ["InteractiveVideo"],
|
|
6408
|
+
a11y: { element: "dialog", ariaLabel: "Timed cue", keyboard: "Focus moves to overlay content.", notes: "Pauses parent video." },
|
|
6409
|
+
theming: { surface: "global-inherit", stylingNotes: "Overlay panel." },
|
|
6410
|
+
telemetry: { emits: [] }
|
|
6411
|
+
},
|
|
6412
|
+
{
|
|
6413
|
+
type: "InteractiveVideo",
|
|
6414
|
+
category: "container",
|
|
6415
|
+
compoundContract: true,
|
|
6416
|
+
h5pMachineName: "H5P.InteractiveVideo",
|
|
6417
|
+
h5pAlias: "Interactive Video",
|
|
6418
|
+
description: "Video with timed interaction overlays.",
|
|
6419
|
+
allowedChildTypes: [...import_core20.INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES],
|
|
6420
|
+
maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.InteractiveVideo,
|
|
6421
|
+
props: [
|
|
6422
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
6423
|
+
{ name: "title", type: "string", required: true, description: "Video title." },
|
|
6424
|
+
{ name: "src", type: "string", required: true, description: "Video URL." },
|
|
6425
|
+
{ name: "poster", type: "string", required: false, description: "Poster image." },
|
|
6426
|
+
{ name: "captions", type: "string", required: false, description: "WebVTT captions." },
|
|
6427
|
+
{ name: "showVideoScore", type: "boolean", required: false, description: "Show aggregate score." },
|
|
6428
|
+
{ name: "children", type: "TimedCue[]", required: true, description: "Timed cues." }
|
|
6429
|
+
],
|
|
6430
|
+
requiredIds: ["blockId"],
|
|
6431
|
+
parentConstraints: ["Lesson"],
|
|
6432
|
+
a11y: {
|
|
6433
|
+
element: "section",
|
|
6434
|
+
ariaLabel: "Interactive video",
|
|
6435
|
+
keyboard: "Native video controls; overlay interactions when paused.",
|
|
6436
|
+
notes: "H5P Interactive Video equivalent."
|
|
6437
|
+
},
|
|
6438
|
+
theming: { surface: "global-inherit", stylingNotes: "Video + overlay." },
|
|
6439
|
+
telemetry: { emits: ["video_cue_reached", "video_segment_completed"], requiresActiveLesson: true }
|
|
6440
|
+
},
|
|
6441
|
+
{
|
|
6442
|
+
type: "Questionnaire",
|
|
6443
|
+
category: "content",
|
|
6444
|
+
h5pMachineName: "H5P.Questionnaire",
|
|
6445
|
+
h5pAlias: "Questionnaire",
|
|
6446
|
+
description: "Unscored multi-field survey.",
|
|
6447
|
+
props: [
|
|
6448
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
6449
|
+
{ name: "fields", type: "QuestionnaireField[]", required: true, description: "Form fields." }
|
|
6450
|
+
],
|
|
6451
|
+
requiredIds: ["blockId"],
|
|
6452
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
6453
|
+
a11y: { element: "form", ariaLabel: "Questionnaire", keyboard: "Tab through fields.", notes: "Unscored survey." },
|
|
6454
|
+
theming: { surface: "global-inherit", stylingNotes: "Form layout." },
|
|
6455
|
+
telemetry: { emits: ["questionnaire_submitted"], requiresActiveLesson: true }
|
|
6456
|
+
},
|
|
6457
|
+
{
|
|
6458
|
+
type: "MemoryGame",
|
|
6459
|
+
category: "content",
|
|
6460
|
+
h5pMachineName: "H5P.MemoryGame",
|
|
6461
|
+
h5pAlias: "Memory Game",
|
|
6462
|
+
description: "Card flip memory matching game.",
|
|
6463
|
+
props: [
|
|
6464
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
6465
|
+
{ name: "pairs", type: "MemoryPair[]", required: true, description: "Card pairs." },
|
|
6466
|
+
{ name: "selfScore", type: "boolean", required: false, description: "Optional self-score mode." }
|
|
6467
|
+
],
|
|
6468
|
+
requiredIds: ["blockId"],
|
|
6469
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
6470
|
+
a11y: { element: "section", ariaLabel: "Memory game", keyboard: "Flip cards with buttons.", notes: "Reduced motion safe." },
|
|
6471
|
+
theming: { surface: "global-inherit", stylingNotes: "Card grid." },
|
|
6472
|
+
telemetry: { emits: ["memory_card_flipped"] }
|
|
6473
|
+
},
|
|
6474
|
+
{
|
|
6475
|
+
type: "InformationWall",
|
|
6476
|
+
category: "content",
|
|
6477
|
+
h5pMachineName: "H5P.InformationWall",
|
|
6478
|
+
h5pAlias: "Information Wall",
|
|
6479
|
+
description: "Searchable information panels.",
|
|
6480
|
+
props: [
|
|
6481
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
6482
|
+
{ name: "panels", type: "InformationPanel[]", required: true, description: "Content panels." }
|
|
6483
|
+
],
|
|
6484
|
+
requiredIds: ["blockId"],
|
|
6485
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
6486
|
+
a11y: { element: "section", ariaLabel: "Information wall", keyboard: "Search and browse panels.", notes: "Filterable grid." },
|
|
6487
|
+
theming: { surface: "global-inherit", stylingNotes: "Panel grid." },
|
|
6488
|
+
telemetry: { emits: ["information_wall_search"] }
|
|
6489
|
+
},
|
|
6490
|
+
{
|
|
6491
|
+
type: "ParallaxSlideshow",
|
|
6492
|
+
category: "content",
|
|
6493
|
+
h5pMachineName: "H5P.ImpressivePresentation",
|
|
6494
|
+
h5pAlias: "Slideshow (parallax)",
|
|
6495
|
+
description: "Slideshow with parallax; static fallback when reduced motion.",
|
|
6496
|
+
props: [
|
|
6497
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
6498
|
+
{ name: "slides", type: "ParallaxSlide[]", required: true, description: "Slides." }
|
|
6499
|
+
],
|
|
6500
|
+
requiredIds: ["blockId"],
|
|
6501
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
6502
|
+
a11y: { element: "section", ariaLabel: "Slideshow", keyboard: "Previous/next slide.", notes: "prefers-reduced-motion fallback." },
|
|
6503
|
+
theming: { surface: "global-inherit", stylingNotes: "Slide deck." },
|
|
6504
|
+
telemetry: { emits: ["parallax_slide_viewed"] }
|
|
6505
|
+
},
|
|
4603
6506
|
{
|
|
4604
6507
|
type: "Accordion",
|
|
4605
6508
|
category: "content",
|
|
@@ -4733,8 +6636,8 @@ function buildV3CatalogFromV2(v2) {
|
|
|
4733
6636
|
return {
|
|
4734
6637
|
...base,
|
|
4735
6638
|
compoundContract: true,
|
|
4736
|
-
allowedChildTypes: [...
|
|
4737
|
-
maxNestingDepth:
|
|
6639
|
+
allowedChildTypes: [...import_core20.ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
|
|
6640
|
+
maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.AssessmentSequence
|
|
4738
6641
|
};
|
|
4739
6642
|
}
|
|
4740
6643
|
return base;
|
|
@@ -5081,6 +6984,100 @@ var v2AssessmentEntries = [
|
|
|
5081
6984
|
},
|
|
5082
6985
|
theming: { surface: "global-inherit", stylingNotes: "Container for assessments." },
|
|
5083
6986
|
telemetry: { emits: [], manualTracking: "Child assessments emit assessment_* events." }
|
|
6987
|
+
},
|
|
6988
|
+
{
|
|
6989
|
+
type: "Summary",
|
|
6990
|
+
category: "assessment",
|
|
6991
|
+
assessmentContract: true,
|
|
6992
|
+
h5pMachineName: "H5P.Summary",
|
|
6993
|
+
h5pAlias: "Summary",
|
|
6994
|
+
description: "Construct a summary from a statement bank in correct order.",
|
|
6995
|
+
props: [
|
|
6996
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
6997
|
+
{ name: "statements", type: "string[]", required: true, description: "Available statements." },
|
|
6998
|
+
{ name: "correct", type: "string[]", required: true, description: "Correct ordered summary." },
|
|
6999
|
+
...assessmentBehaviourProps2
|
|
7000
|
+
],
|
|
7001
|
+
requiredIds: ["checkId"],
|
|
7002
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
7003
|
+
a11y: { element: "section", ariaLabel: "Summary", keyboard: "Select statements in order.", notes: "H5P Summary equivalent." },
|
|
7004
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
7005
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
7006
|
+
},
|
|
7007
|
+
{
|
|
7008
|
+
type: "ImagePairing",
|
|
7009
|
+
category: "assessment",
|
|
7010
|
+
assessmentContract: true,
|
|
7011
|
+
h5pMachineName: "H5P.ImagePair",
|
|
7012
|
+
h5pAlias: "Image Pairing",
|
|
7013
|
+
description: "Match image pairs in a memory-style task.",
|
|
7014
|
+
props: [
|
|
7015
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
7016
|
+
{ name: "pairs", type: "ImagePair[]", required: true, description: "Image pairs to match." },
|
|
7017
|
+
...assessmentBehaviourProps2
|
|
7018
|
+
],
|
|
7019
|
+
requiredIds: ["checkId"],
|
|
7020
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
7021
|
+
a11y: { element: "section", ariaLabel: "Image Pairing", keyboard: "Select two cards to match.", notes: "H5P Image Pairing equivalent." },
|
|
7022
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
7023
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
7024
|
+
},
|
|
7025
|
+
{
|
|
7026
|
+
type: "ImageSequencing",
|
|
7027
|
+
category: "assessment",
|
|
7028
|
+
assessmentContract: true,
|
|
7029
|
+
h5pMachineName: "H5P.ImageSequencing",
|
|
7030
|
+
h5pAlias: "Image Sequencing",
|
|
7031
|
+
description: "Order images in the correct sequence.",
|
|
7032
|
+
props: [
|
|
7033
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
7034
|
+
{ name: "images", type: "SequencingImage[]", required: true, description: "Images to order." },
|
|
7035
|
+
{ name: "correctOrder", type: "string[]", required: true, description: "Correct id order." },
|
|
7036
|
+
...assessmentBehaviourProps2
|
|
7037
|
+
],
|
|
7038
|
+
requiredIds: ["checkId"],
|
|
7039
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
7040
|
+
a11y: { element: "section", ariaLabel: "Image Sequencing", keyboard: "Reorder with up/down.", notes: "H5P Image Sequencing equivalent." },
|
|
7041
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
7042
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
7043
|
+
},
|
|
7044
|
+
{
|
|
7045
|
+
type: "ArithmeticQuiz",
|
|
7046
|
+
category: "assessment",
|
|
7047
|
+
assessmentContract: true,
|
|
7048
|
+
h5pMachineName: "H5P.ArithmeticQuiz",
|
|
7049
|
+
h5pAlias: "Arithmetic Quiz",
|
|
7050
|
+
description: "Timed arithmetic problems with optional timer.",
|
|
7051
|
+
props: [
|
|
7052
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
7053
|
+
{ name: "problems", type: "ArithmeticProblem[]", required: true, description: "Math problems." },
|
|
7054
|
+
{ name: "timeLimitSeconds", type: "number", required: false, description: "Optional time limit." },
|
|
7055
|
+
...assessmentBehaviourProps2
|
|
7056
|
+
],
|
|
7057
|
+
requiredIds: ["checkId"],
|
|
7058
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
7059
|
+
a11y: { element: "section", ariaLabel: "Arithmetic Quiz", keyboard: "Text input per problem.", notes: "H5P Arithmetic Quiz equivalent." },
|
|
7060
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
7061
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
7062
|
+
},
|
|
7063
|
+
{
|
|
7064
|
+
type: "Essay",
|
|
7065
|
+
category: "assessment",
|
|
7066
|
+
assessmentContract: true,
|
|
7067
|
+
h5pMachineName: "H5P.Essay",
|
|
7068
|
+
h5pAlias: "Essay",
|
|
7069
|
+
description: "Open text response; manual or plugin grading.",
|
|
7070
|
+
props: [
|
|
7071
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
7072
|
+
{ name: "question", type: "string", required: true, description: "Essay prompt." },
|
|
7073
|
+
{ name: "minLength", type: "number", required: false, description: "Minimum character length." },
|
|
7074
|
+
...assessmentBehaviourProps2
|
|
7075
|
+
],
|
|
7076
|
+
requiredIds: ["checkId"],
|
|
7077
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
7078
|
+
a11y: { element: "section", ariaLabel: "Essay", keyboard: "Textarea input.", notes: "H5P Essay equivalent." },
|
|
7079
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
7080
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
5084
7081
|
}
|
|
5085
7082
|
];
|
|
5086
7083
|
var BLOCK_CATALOG_V2 = [
|
|
@@ -5120,6 +7117,7 @@ function getBlockCatalogEntry(type, opts) {
|
|
|
5120
7117
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5121
7118
|
0 && (module.exports = {
|
|
5122
7119
|
Accordion,
|
|
7120
|
+
ArithmeticQuiz,
|
|
5123
7121
|
AssessmentSequence,
|
|
5124
7122
|
BLOCK_CATALOG,
|
|
5125
7123
|
BLOCK_CATALOG_V2,
|
|
@@ -5128,6 +7126,7 @@ function getBlockCatalogEntry(type, opts) {
|
|
|
5128
7126
|
DialogCards,
|
|
5129
7127
|
DragAndDrop,
|
|
5130
7128
|
DragTheWords,
|
|
7129
|
+
Essay,
|
|
5131
7130
|
FillInTheBlanks,
|
|
5132
7131
|
FindHotspot,
|
|
5133
7132
|
FindMultipleHotspots,
|
|
@@ -5135,22 +7134,33 @@ function getBlockCatalogEntry(type, opts) {
|
|
|
5135
7134
|
Heading,
|
|
5136
7135
|
Image,
|
|
5137
7136
|
ImageHotspots,
|
|
7137
|
+
ImagePairing,
|
|
7138
|
+
ImageSequencing,
|
|
5138
7139
|
ImageSlider,
|
|
7140
|
+
InformationWall,
|
|
5139
7141
|
InteractiveBook,
|
|
7142
|
+
InteractiveVideo,
|
|
5140
7143
|
KnowledgeCheck,
|
|
5141
7144
|
Lesson,
|
|
5142
7145
|
LessonkitProvider,
|
|
5143
7146
|
MarkTheWords,
|
|
7147
|
+
MemoryGame,
|
|
5144
7148
|
Page,
|
|
7149
|
+
ParallaxSlideshow,
|
|
5145
7150
|
ProgressTracker,
|
|
7151
|
+
Questionnaire,
|
|
5146
7152
|
Quiz,
|
|
5147
7153
|
Reflection,
|
|
5148
7154
|
Scenario,
|
|
5149
7155
|
Slide,
|
|
5150
7156
|
SlideDeck,
|
|
7157
|
+
Summary,
|
|
5151
7158
|
Text,
|
|
5152
7159
|
ThemeProvider,
|
|
7160
|
+
TimedCue,
|
|
5153
7161
|
TrueFalse,
|
|
7162
|
+
Video,
|
|
7163
|
+
assertProductionCourseConfig,
|
|
5154
7164
|
blockCatalogV2Version,
|
|
5155
7165
|
blockCatalogV3Version,
|
|
5156
7166
|
blockCatalogVersion,
|
|
@@ -5165,6 +7175,7 @@ function getBlockCatalogEntry(type, opts) {
|
|
|
5165
7175
|
getBlockCatalogEntry,
|
|
5166
7176
|
resetAssessmentWarningsForTests,
|
|
5167
7177
|
resetQuizWarningsForTests,
|
|
7178
|
+
shouldEnforceProductionGuard,
|
|
5168
7179
|
useAssessmentState,
|
|
5169
7180
|
useCompletion,
|
|
5170
7181
|
useLessonkit,
|