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