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