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