@lessonkit/react 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/block-catalog.v3.json +1005 -107
- package/dist/AssessmentLessonGuard-D2Plzybb.d.cts +21 -0
- package/dist/AssessmentLessonGuard-D2Plzybb.d.ts +21 -0
- package/dist/blocks-entry.cjs +4563 -0
- package/dist/blocks-entry.d.cts +411 -0
- package/dist/blocks-entry.d.ts +411 -0
- package/dist/blocks-entry.js +69 -0
- package/dist/chunk-4LQ4TTEE.js +4018 -0
- package/dist/chunk-TDM3ARE7.js +1775 -0
- package/dist/chunk-UUTXECVW.js +252 -0
- package/dist/index.cjs +2329 -318
- package/dist/index.d.cts +31 -282
- package/dist/index.d.ts +31 -282
- package/dist/index.js +433 -4295
- package/dist/testing.cjs +540 -0
- package/dist/testing.d.cts +16 -0
- package/dist/testing.d.ts +16 -0
- package/dist/testing.js +18 -0
- package/package.json +33 -16
package/dist/index.js
CHANGED
|
@@ -1,4304 +1,198 @@
|
|
|
1
|
-
// src/components.tsx
|
|
2
|
-
import { useEffect as useEffect4, useId as useId2, useMemo as useMemo6, useRef as useRef5, useState as useState4 } from "react";
|
|
3
|
-
import { visuallyHiddenStyle as visuallyHiddenStyle2 } from "@lessonkit/accessibility";
|
|
4
|
-
|
|
5
|
-
// src/context.tsx
|
|
6
|
-
import { createContext } from "react";
|
|
7
|
-
|
|
8
|
-
// src/provider/useLessonkitProviderRuntime.ts
|
|
9
|
-
import {
|
|
10
|
-
useCallback,
|
|
11
|
-
useEffect,
|
|
12
|
-
useLayoutEffect,
|
|
13
|
-
useMemo,
|
|
14
|
-
useRef,
|
|
15
|
-
useState
|
|
16
|
-
} from "react";
|
|
17
|
-
import { createLessonkitRuntime, createTrackingClient as createTrackingClient2, assertValidId } from "@lessonkit/core";
|
|
18
|
-
|
|
19
|
-
// src/runtime/observability.ts
|
|
20
|
-
import { createInMemoryXAPIQueue } from "@lessonkit/xapi";
|
|
21
|
-
function createXapiQueueFromObservability(observability) {
|
|
22
|
-
const opts = {};
|
|
23
|
-
if (observability?.onXapiQueueDepth) {
|
|
24
|
-
opts.onDepth = observability.onXapiQueueDepth;
|
|
25
|
-
}
|
|
26
|
-
if (observability?.onXapiQueueCap) {
|
|
27
|
-
opts.onCap = observability.onXapiQueueCap;
|
|
28
|
-
}
|
|
29
|
-
return createInMemoryXAPIQueue(opts);
|
|
30
|
-
}
|
|
31
|
-
function wrapBatchSink(batchSink, observability) {
|
|
32
|
-
if (!batchSink || !observability?.onTelemetrySinkError) return batchSink;
|
|
33
|
-
const onError = observability.onTelemetrySinkError;
|
|
34
|
-
return async (events) => {
|
|
35
|
-
try {
|
|
36
|
-
await batchSink(events);
|
|
37
|
-
} catch (err) {
|
|
38
|
-
onError(err, { sinkId: "tracking-batch" });
|
|
39
|
-
throw err;
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
function warnMissingProductionObservability(observability, opts) {
|
|
44
|
-
let isProduction = false;
|
|
45
|
-
try {
|
|
46
|
-
isProduction = import.meta.env?.PROD === true;
|
|
47
|
-
} catch {
|
|
48
|
-
}
|
|
49
|
-
if (!isProduction) {
|
|
50
|
-
const g = globalThis;
|
|
51
|
-
isProduction = typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
|
|
52
|
-
}
|
|
53
|
-
if (!isProduction) return;
|
|
54
|
-
if (!opts.trackingEnabled && !opts.xapiEnabled) return;
|
|
55
|
-
const hooks = [
|
|
56
|
-
observability?.onTelemetrySinkError,
|
|
57
|
-
observability?.onTelemetryBufferDrop,
|
|
58
|
-
observability?.onXapiQueueDepth,
|
|
59
|
-
observability?.onXapiQueueCap,
|
|
60
|
-
observability?.onLxpackBridgeMiss
|
|
61
|
-
];
|
|
62
|
-
if (hooks.some(Boolean)) return;
|
|
63
|
-
if (typeof console !== "undefined") {
|
|
64
|
-
console.warn(
|
|
65
|
-
"[lessonkit] Production deployment without observability hooks \u2014 telemetry/xAPI failures and buffer drops will be silent. See https://lessonkit.readthedocs.io/en/latest/guides/react-developers/production-checklist.html"
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function wrapTrackingSink(sink, observability) {
|
|
70
|
-
if (!sink || !observability?.onTelemetrySinkError) return sink;
|
|
71
|
-
const onError = observability.onTelemetrySinkError;
|
|
72
|
-
return ((event) => {
|
|
73
|
-
try {
|
|
74
|
-
const result = sink(event);
|
|
75
|
-
if (result != null && typeof result.catch === "function") {
|
|
76
|
-
return result.catch((err) => {
|
|
77
|
-
onError(err, { sinkId: "tracking" });
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
return result;
|
|
81
|
-
} catch (err) {
|
|
82
|
-
onError(err, { sinkId: "tracking" });
|
|
83
|
-
return void 0;
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// src/provider/useLessonkitProviderRuntime.ts
|
|
89
|
-
import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement3 } from "@lessonkit/xapi";
|
|
90
|
-
|
|
91
|
-
// src/runtime/emitTelemetry.ts
|
|
92
|
-
import { buildTelemetryEvent, tryBuildTelemetryEvent } from "@lessonkit/core";
|
|
93
|
-
|
|
94
|
-
// src/runtime/telemetryPipeline.ts
|
|
95
|
-
import {
|
|
96
|
-
createTelemetryPipeline,
|
|
97
|
-
createTrackingPipelineSink
|
|
98
|
-
} from "@lessonkit/core";
|
|
99
|
-
import { telemetryEventToXAPIStatement } from "@lessonkit/xapi";
|
|
100
|
-
|
|
101
|
-
// src/runtime/lxpackBridge.ts
|
|
102
1
|
import {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
} catch (err) {
|
|
137
|
-
if (isDevEnvironment()) {
|
|
138
|
-
console.warn(
|
|
139
|
-
"[lessonkit] xAPI mapping skipped:",
|
|
140
|
-
err instanceof Error ? err.message : err
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
id: "lxpack-bridge",
|
|
148
|
-
emit(event) {
|
|
149
|
-
forwardTelemetryToLxpack(event, opts.lxpackBridge, {
|
|
150
|
-
onBridgeMiss: opts.onLxpackBridgeMiss
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
...extraSinks
|
|
155
|
-
]);
|
|
156
|
-
}
|
|
157
|
-
function emitThroughPipeline(event, opts, extraSinks) {
|
|
158
|
-
createLegacyPipeline(opts, extraSinks).emit(event);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// src/runtime/emitTelemetry.ts
|
|
162
|
-
var warnedMissingCourseId = false;
|
|
163
|
-
function isDevEnvironment2() {
|
|
164
|
-
const g = globalThis;
|
|
165
|
-
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
166
|
-
}
|
|
167
|
-
function emitTelemetry(tracking, xapi, event, opts) {
|
|
168
|
-
if (!event.courseId) {
|
|
169
|
-
if (isDevEnvironment2() && !warnedMissingCourseId) {
|
|
170
|
-
warnedMissingCourseId = true;
|
|
171
|
-
console.warn("[lessonkit] telemetry event missing courseId");
|
|
172
|
-
}
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const legacy = {
|
|
176
|
-
tracking,
|
|
177
|
-
xapi,
|
|
178
|
-
lxpackBridge: opts?.lxpackBridge ?? "auto",
|
|
179
|
-
onLxpackBridgeMiss: opts?.onLxpackBridgeMiss
|
|
180
|
-
};
|
|
181
|
-
emitThroughPipeline(event, legacy, opts?.extraSinks);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// src/runtime/ports.ts
|
|
2
|
+
Accordion,
|
|
3
|
+
ArithmeticQuiz,
|
|
4
|
+
AssessmentSequence,
|
|
5
|
+
DialogCards,
|
|
6
|
+
DragAndDrop,
|
|
7
|
+
DragTheWords,
|
|
8
|
+
Essay,
|
|
9
|
+
FillInTheBlanks,
|
|
10
|
+
FindHotspot,
|
|
11
|
+
FindMultipleHotspots,
|
|
12
|
+
Flashcards,
|
|
13
|
+
Heading,
|
|
14
|
+
Image,
|
|
15
|
+
ImageHotspots,
|
|
16
|
+
ImagePairing,
|
|
17
|
+
ImageSequencing,
|
|
18
|
+
ImageSlider,
|
|
19
|
+
InformationWall,
|
|
20
|
+
InteractiveBook,
|
|
21
|
+
InteractiveVideo,
|
|
22
|
+
MarkTheWords,
|
|
23
|
+
MemoryGame,
|
|
24
|
+
Page,
|
|
25
|
+
ParallaxSlideshow,
|
|
26
|
+
Questionnaire,
|
|
27
|
+
Slide,
|
|
28
|
+
SlideDeck,
|
|
29
|
+
Summary,
|
|
30
|
+
Text,
|
|
31
|
+
TimedCue,
|
|
32
|
+
TrueFalse,
|
|
33
|
+
Video
|
|
34
|
+
} from "./chunk-4LQ4TTEE.js";
|
|
185
35
|
import {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
} from "
|
|
192
|
-
|
|
193
|
-
// src/provider/useLessonkitProviderRuntime.ts
|
|
194
|
-
import { resetSharedVolatileSessionIdForTests } from "@lessonkit/core";
|
|
195
|
-
|
|
196
|
-
// src/runtime/progress.ts
|
|
197
|
-
import { createProgressController } from "@lessonkit/core";
|
|
198
|
-
|
|
199
|
-
// src/runtime/xapi.ts
|
|
200
|
-
import { createXAPIClient } from "@lessonkit/xapi";
|
|
201
|
-
function createXapiClientFromConfig(config, queue) {
|
|
202
|
-
if (config.xapi?.enabled === false) return null;
|
|
203
|
-
if (config.xapi?.client) return config.xapi.client;
|
|
204
|
-
if (!config.courseId) return null;
|
|
205
|
-
const hasTransport = typeof config.xapi?.transport === "function";
|
|
206
|
-
if (!hasTransport && config.xapi?.enabled !== true) return null;
|
|
207
|
-
return createXAPIClient({
|
|
208
|
-
courseId: config.courseId,
|
|
209
|
-
transport: config.xapi?.transport,
|
|
210
|
-
exitTransport: config.xapi?.exitTransport,
|
|
211
|
-
queue
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// src/runtime/session.ts
|
|
36
|
+
KnowledgeCheck,
|
|
37
|
+
Quiz,
|
|
38
|
+
getLessonMountCount,
|
|
39
|
+
registerLessonMount,
|
|
40
|
+
resetQuizWarningsForTests
|
|
41
|
+
} from "./chunk-UUTXECVW.js";
|
|
216
42
|
import {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement2 } from "@lessonkit/xapi";
|
|
231
|
-
function isDevEnvironment3() {
|
|
232
|
-
const g = globalThis;
|
|
233
|
-
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
234
|
-
}
|
|
235
|
-
function warnExtraSinkFailure(sinkId, err) {
|
|
236
|
-
if (isDevEnvironment3()) {
|
|
237
|
-
console.warn(
|
|
238
|
-
`[lessonkit] course_started extra sink "${sinkId}" failed:`,
|
|
239
|
-
err instanceof Error ? err.message : err
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
async function emitExtraSinks(sinks, event, emitCtx) {
|
|
244
|
-
await Promise.all(
|
|
245
|
-
sinks.map(async (sink) => {
|
|
246
|
-
let result;
|
|
247
|
-
try {
|
|
248
|
-
result = sink.emit(event, emitCtx);
|
|
249
|
-
} catch (err) {
|
|
250
|
-
warnExtraSinkFailure(sink.id, err);
|
|
251
|
-
throw err;
|
|
252
|
-
}
|
|
253
|
-
if (result != null && typeof result.then === "function") {
|
|
254
|
-
try {
|
|
255
|
-
await result;
|
|
256
|
-
} catch (err) {
|
|
257
|
-
warnExtraSinkFailure(sink.id, err);
|
|
258
|
-
throw err;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
})
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
async function emitCourseStartedNonTrackingPipeline(opts) {
|
|
265
|
-
let xapiStatementSent = false;
|
|
266
|
-
if (!opts.skipXapi && opts.xapi) {
|
|
267
|
-
const statement = telemetryEventToXAPIStatement2(opts.event);
|
|
268
|
-
if (statement) {
|
|
269
|
-
opts.xapi.send(statement);
|
|
270
|
-
await opts.xapi.flush();
|
|
271
|
-
xapiStatementSent = true;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
forwardTelemetryToLxpack(opts.event, opts.lxpackBridge, {
|
|
275
|
-
onBridgeMiss: opts.onLxpackBridgeMiss
|
|
276
|
-
});
|
|
277
|
-
const emitCtx = {
|
|
278
|
-
courseId: opts.event.courseId,
|
|
279
|
-
sessionId: opts.event.sessionId,
|
|
280
|
-
attemptId: opts.event.attemptId
|
|
281
|
-
};
|
|
282
|
-
await emitExtraSinks(opts.extraSinks ?? [], opts.event, emitCtx);
|
|
283
|
-
return { xapiStatementSent };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// src/runtime/plugins.ts
|
|
287
|
-
import { buildPluginContext as buildPluginContextFromCore, createPluginRegistry } from "@lessonkit/core";
|
|
288
|
-
function createReactPluginHost(plugins) {
|
|
289
|
-
if (!plugins?.length) return null;
|
|
290
|
-
return createPluginRegistry(plugins);
|
|
291
|
-
}
|
|
292
|
-
function buildPluginContext(opts) {
|
|
293
|
-
return buildPluginContextFromCore(opts);
|
|
294
|
-
}
|
|
295
|
-
function emitTelemetryWithPlugins(opts) {
|
|
296
|
-
const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
|
|
297
|
-
if (next === null) return;
|
|
298
|
-
emitTelemetry(opts.tracking, opts.xapi, next, {
|
|
299
|
-
lxpackBridge: opts.lxpackBridge ?? "auto",
|
|
300
|
-
extraSinks: opts.extraSinks,
|
|
301
|
-
onLxpackBridgeMiss: opts.onLxpackBridgeMiss
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// src/provider/courseStarted/emit.ts
|
|
306
|
-
var courseStartedTrackingFlights = /* @__PURE__ */ new Map();
|
|
307
|
-
function isTrackingActive(tracking) {
|
|
308
|
-
return tracking?.enabled !== false;
|
|
309
|
-
}
|
|
310
|
-
function isCourseStartedSinkSettled(result) {
|
|
311
|
-
return result === "emitted";
|
|
312
|
-
}
|
|
313
|
-
function buildCourseStartedEvent(opts) {
|
|
314
|
-
const pluginCtx = buildPluginContext({
|
|
315
|
-
courseId: opts.courseId,
|
|
316
|
-
sessionId: opts.sessionId,
|
|
317
|
-
attemptId: opts.attemptId,
|
|
318
|
-
user: opts.user
|
|
319
|
-
});
|
|
320
|
-
const built = buildTelemetryEvent({
|
|
321
|
-
name: "course_started",
|
|
322
|
-
courseId: opts.courseId,
|
|
323
|
-
sessionId: opts.sessionId,
|
|
324
|
-
attemptId: opts.attemptId,
|
|
325
|
-
user: opts.user
|
|
326
|
-
});
|
|
327
|
-
return opts.pluginHost ? opts.pluginHost.runTelemetry(built, pluginCtx) : built;
|
|
328
|
-
}
|
|
329
|
-
async function emitCourseStartedToTracking(tracking, storage, sessionId, courseId, event, shouldCommit) {
|
|
330
|
-
const flightKey = `${sessionId}:${courseId}`;
|
|
331
|
-
if (hasCourseStartedEmittedToTracking(storage, sessionId, courseId)) {
|
|
332
|
-
return true;
|
|
333
|
-
}
|
|
334
|
-
const existing = courseStartedTrackingFlights.get(flightKey);
|
|
335
|
-
if (existing) {
|
|
336
|
-
const settled = await existing;
|
|
337
|
-
if (settled) return true;
|
|
338
|
-
}
|
|
339
|
-
let resolveFlight;
|
|
340
|
-
const flight = new Promise((resolve) => {
|
|
341
|
-
resolveFlight = resolve;
|
|
342
|
-
});
|
|
343
|
-
courseStartedTrackingFlights.set(flightKey, flight);
|
|
344
|
-
void (async () => {
|
|
345
|
-
try {
|
|
346
|
-
if (shouldCommit && !shouldCommit()) {
|
|
347
|
-
resolveFlight(false);
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
tracking.track(event);
|
|
351
|
-
const delivered = await tracking.flush?.();
|
|
352
|
-
if (delivered === false) {
|
|
353
|
-
resolveFlight(false);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
if (markCourseStartedEmittedToTracking(storage, sessionId, courseId) === false) {
|
|
357
|
-
resolveFlight(false);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
resolveFlight(true);
|
|
361
|
-
} catch {
|
|
362
|
-
resolveFlight(false);
|
|
363
|
-
} finally {
|
|
364
|
-
courseStartedTrackingFlights.delete(flightKey);
|
|
365
|
-
}
|
|
366
|
-
})();
|
|
367
|
-
return flight;
|
|
368
|
-
}
|
|
369
|
-
async function emitCourseStartedPipelineOnly(opts) {
|
|
370
|
-
try {
|
|
371
|
-
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
372
|
-
const { xapiStatementSent } = await emitCourseStartedNonTrackingPipeline({
|
|
373
|
-
event: opts.event,
|
|
374
|
-
xapi: opts.xapi,
|
|
375
|
-
lxpackBridge: opts.lxpackBridge,
|
|
376
|
-
onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
|
|
377
|
-
extraSinks: opts.extraSinks,
|
|
378
|
-
skipXapi: opts.skipXapi
|
|
379
|
-
});
|
|
380
|
-
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
381
|
-
if (markCourseStarted(opts.storage, opts.sessionId, opts.courseId) === false) return "failed";
|
|
382
|
-
if (markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId) === false) {
|
|
383
|
-
return "failed";
|
|
384
|
-
}
|
|
385
|
-
if (xapiStatementSent) {
|
|
386
|
-
opts.onXapiStatementSent?.();
|
|
387
|
-
}
|
|
388
|
-
return "emitted";
|
|
389
|
-
} catch {
|
|
390
|
-
return "failed";
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
async function emitCourseStarted(opts) {
|
|
394
|
-
const event = buildCourseStartedEvent(opts);
|
|
395
|
-
if (event === null) return "filtered";
|
|
396
|
-
const tracked = await emitCourseStartedToTracking(
|
|
397
|
-
opts.tracking,
|
|
398
|
-
opts.storage,
|
|
399
|
-
opts.sessionId,
|
|
400
|
-
opts.courseId,
|
|
401
|
-
event,
|
|
402
|
-
opts.shouldCommit
|
|
403
|
-
);
|
|
404
|
-
if (!tracked) return "failed";
|
|
405
|
-
return emitCourseStartedPipelineOnly({
|
|
406
|
-
...opts,
|
|
407
|
-
event,
|
|
408
|
-
skipXapi: opts.skipXapi,
|
|
409
|
-
onXapiStatementSent: opts.onXapiStatementSent,
|
|
410
|
-
shouldCommit: opts.shouldCommit
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
async function emitCourseStartedToTrackingOnly(opts) {
|
|
414
|
-
const event = buildCourseStartedEvent(opts);
|
|
415
|
-
if (event === null) return "filtered";
|
|
416
|
-
const tracked = await emitCourseStartedToTracking(
|
|
417
|
-
opts.tracking,
|
|
418
|
-
opts.storage,
|
|
419
|
-
opts.sessionId,
|
|
420
|
-
opts.courseId,
|
|
421
|
-
event,
|
|
422
|
-
opts.shouldCommit
|
|
423
|
-
);
|
|
424
|
-
if (!tracked) return "failed";
|
|
425
|
-
try {
|
|
426
|
-
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
427
|
-
await emitCourseStartedNonTrackingPipeline({
|
|
428
|
-
event,
|
|
429
|
-
xapi: null,
|
|
430
|
-
lxpackBridge: opts.lxpackBridge,
|
|
431
|
-
onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
|
|
432
|
-
extraSinks: opts.extraSinks,
|
|
433
|
-
skipXapi: true
|
|
434
|
-
});
|
|
435
|
-
if (markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId) === false) {
|
|
436
|
-
return "failed";
|
|
437
|
-
}
|
|
438
|
-
return "emitted";
|
|
439
|
-
} catch {
|
|
440
|
-
return "failed";
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
async function emitPendingCourseStarted(opts) {
|
|
444
|
-
const trackingEmitted = hasCourseStartedEmittedToTracking(
|
|
445
|
-
opts.storage,
|
|
446
|
-
opts.sessionId,
|
|
447
|
-
opts.courseId
|
|
448
|
-
);
|
|
449
|
-
const sessionStarted = hasCourseStarted(opts.storage, opts.sessionId, opts.courseId);
|
|
450
|
-
if (sessionStarted && !trackingEmitted) {
|
|
451
|
-
return emitCourseStartedToTrackingOnly(opts);
|
|
452
|
-
}
|
|
453
|
-
if (trackingEmitted && !sessionStarted) {
|
|
454
|
-
const event = buildCourseStartedEvent(opts);
|
|
455
|
-
if (event === null) return "filtered";
|
|
456
|
-
return emitCourseStartedPipelineOnly({ ...opts, event });
|
|
457
|
-
}
|
|
458
|
-
if (!trackingEmitted && !sessionStarted) {
|
|
459
|
-
return emitCourseStarted(opts);
|
|
460
|
-
}
|
|
461
|
-
const pipelineDelivered = hasCourseStartedPipelineDelivered(
|
|
462
|
-
opts.storage,
|
|
463
|
-
opts.sessionId,
|
|
464
|
-
opts.courseId
|
|
465
|
-
);
|
|
466
|
-
if (sessionStarted && trackingEmitted && pipelineDelivered) {
|
|
467
|
-
return "emitted";
|
|
468
|
-
}
|
|
469
|
-
if (sessionStarted && trackingEmitted && !pipelineDelivered) {
|
|
470
|
-
const event = buildCourseStartedEvent(opts);
|
|
471
|
-
if (event === null) return "filtered";
|
|
472
|
-
return emitCourseStartedPipelineOnly({
|
|
473
|
-
...opts,
|
|
474
|
-
event,
|
|
475
|
-
skipXapi: opts.skipXapi,
|
|
476
|
-
onXapiStatementSent: opts.onXapiStatementSent
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
return "emitted";
|
|
480
|
-
}
|
|
481
|
-
function assertTrackingSinkConfig(tracking) {
|
|
482
|
-
if (!tracking?.sink || !tracking?.batchSink) return;
|
|
483
|
-
throw new Error(
|
|
484
|
-
"[lessonkit] tracking.sink and tracking.batchSink cannot both be set; use batchSink alone for batched delivery"
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// src/runtime/telemetry.ts
|
|
489
|
-
import { createTrackingClient } from "@lessonkit/core";
|
|
490
|
-
function createTrackingClientFromConfig(config, observability) {
|
|
491
|
-
if (config.tracking?.enabled === false) return createTrackingClient();
|
|
492
|
-
if (config.tracking?.createClient) return config.tracking.createClient();
|
|
493
|
-
return createTrackingClient({
|
|
494
|
-
sink: config.tracking?.sink,
|
|
495
|
-
batchSink: config.tracking?.batchSink,
|
|
496
|
-
batch: config.tracking?.batch,
|
|
497
|
-
exitBatchSink: config.tracking?.exitBatchSink,
|
|
498
|
-
onBufferDrop: observability?.onTelemetryBufferDrop
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
async function disposeTrackingClient(client) {
|
|
502
|
-
try {
|
|
503
|
-
await client?.flush?.();
|
|
504
|
-
} catch {
|
|
505
|
-
}
|
|
506
|
-
try {
|
|
507
|
-
await client?.dispose?.();
|
|
508
|
-
} catch {
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// src/provider/useLessonkitProviderRuntime.ts
|
|
513
|
-
var useIsoLayoutEffect = (
|
|
514
|
-
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
515
|
-
typeof window !== "undefined" ? useLayoutEffect : useEffect
|
|
516
|
-
);
|
|
517
|
-
var defaultStorage = createSessionStoragePort();
|
|
518
|
-
function useLessonkitProviderRuntime(config) {
|
|
519
|
-
const normalizedCourseId = useMemo(
|
|
520
|
-
() => assertValidId(config.courseId, "courseId"),
|
|
521
|
-
[config.courseId]
|
|
522
|
-
);
|
|
523
|
-
const normalizedConfig = useMemo(
|
|
524
|
-
() => ({ ...config, courseId: normalizedCourseId }),
|
|
525
|
-
[config, normalizedCourseId]
|
|
526
|
-
);
|
|
527
|
-
const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
|
|
528
|
-
useEffect(() => {
|
|
529
|
-
if (useV2Runtime) return;
|
|
530
|
-
const g = globalThis;
|
|
531
|
-
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
|
|
532
|
-
console.warn(
|
|
533
|
-
'[lessonkit] LessonkitProvider runtimeVersion "v1" is deprecated; omit or use "v2" (default). v1 will be removed in LessonKit 2.0.'
|
|
534
|
-
);
|
|
535
|
-
}, [useV2Runtime]);
|
|
536
|
-
const extraSinksRef = useRef(normalizedConfig.sinks);
|
|
537
|
-
extraSinksRef.current = normalizedConfig.sinks;
|
|
538
|
-
const headlessRef = useRef(null);
|
|
539
|
-
const sessionIdRef = useRef(resolveSessionId(defaultStorage, normalizedConfig.session?.sessionId));
|
|
540
|
-
const prevConfiguredSessionIdRef = useRef(normalizedConfig.session?.sessionId);
|
|
541
|
-
if (normalizedConfig.session?.sessionId) {
|
|
542
|
-
sessionIdRef.current = normalizedConfig.session.sessionId;
|
|
543
|
-
} else if (prevConfiguredSessionIdRef.current) {
|
|
544
|
-
sessionIdRef.current = resolveSessionId(defaultStorage, void 0);
|
|
545
|
-
}
|
|
546
|
-
const attemptIdRef = useRef(normalizedConfig.session?.attemptId);
|
|
547
|
-
const userRef = useRef(normalizedConfig.session?.user);
|
|
548
|
-
attemptIdRef.current = normalizedConfig.session?.attemptId;
|
|
549
|
-
userRef.current = normalizedConfig.session?.user;
|
|
550
|
-
const courseIdRef = useRef(normalizedCourseId);
|
|
551
|
-
courseIdRef.current = normalizedCourseId;
|
|
552
|
-
const lxpackBridgeModeRef = useRef(normalizedConfig.lxpack?.bridge ?? "auto");
|
|
553
|
-
lxpackBridgeModeRef.current = normalizedConfig.lxpack?.bridge ?? "auto";
|
|
554
|
-
const observabilityRef = useRef(normalizedConfig.observability);
|
|
555
|
-
observabilityRef.current = normalizedConfig.observability;
|
|
556
|
-
const onLxpackBridgeMiss = useCallback((event) => {
|
|
557
|
-
observabilityRef.current?.onLxpackBridgeMiss?.(event);
|
|
558
|
-
}, []);
|
|
559
|
-
const pluginsFingerprint = normalizedConfig.plugins?.map((p) => `${p.id}\0${p.version}`).join("|") ?? "";
|
|
560
|
-
const pluginHost = useMemo(
|
|
561
|
-
() => createReactPluginHost(normalizedConfig.plugins),
|
|
562
|
-
[pluginsFingerprint]
|
|
563
|
-
);
|
|
564
|
-
const pluginHostRef = useRef(pluginHost);
|
|
565
|
-
pluginHostRef.current = pluginHost;
|
|
566
|
-
const progressRef = useRef(createProgressController());
|
|
567
|
-
const courseStartedEmittedToSinkRef = useRef(false);
|
|
568
|
-
const courseStartedEmitGenerationRef = useRef(0);
|
|
569
|
-
const prevCourseIdForProgressRef = useRef(normalizedCourseId);
|
|
570
|
-
const pendingCourseIdResetRef = useRef(false);
|
|
571
|
-
const prevUseV2RuntimeRef = useRef(useV2Runtime);
|
|
572
|
-
const xapiCourseStartedSentOnClientRef = useRef(false);
|
|
573
|
-
const xapiBootstrapSendRef = useRef(false);
|
|
574
|
-
if (prevUseV2RuntimeRef.current !== useV2Runtime) {
|
|
575
|
-
prevUseV2RuntimeRef.current = useV2Runtime;
|
|
576
|
-
if (useV2Runtime) {
|
|
577
|
-
headlessRef.current = createLessonkitRuntime({
|
|
578
|
-
courseId: normalizedCourseId,
|
|
579
|
-
runtimeVersion: "v2",
|
|
580
|
-
session: normalizedConfig.session,
|
|
581
|
-
plugins: pluginHostRef.current ?? normalizedConfig.plugins,
|
|
582
|
-
deferPluginSetup: true
|
|
583
|
-
});
|
|
584
|
-
progressRef.current = headlessRef.current.progress;
|
|
585
|
-
} else {
|
|
586
|
-
headlessRef.current = null;
|
|
587
|
-
progressRef.current = createProgressController();
|
|
588
|
-
}
|
|
589
|
-
pendingCourseIdResetRef.current = true;
|
|
590
|
-
courseStartedEmittedToSinkRef.current = false;
|
|
591
|
-
courseStartedEmitGenerationRef.current += 1;
|
|
592
|
-
} else if (useV2Runtime && !headlessRef.current) {
|
|
593
|
-
headlessRef.current = createLessonkitRuntime({
|
|
594
|
-
courseId: normalizedCourseId,
|
|
595
|
-
runtimeVersion: "v2",
|
|
596
|
-
session: normalizedConfig.session,
|
|
597
|
-
plugins: pluginHostRef.current ?? normalizedConfig.plugins,
|
|
598
|
-
deferPluginSetup: true
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
|
|
602
|
-
prevCourseIdForProgressRef.current = normalizedCourseId;
|
|
603
|
-
if (useV2Runtime && headlessRef.current) {
|
|
604
|
-
headlessRef.current.resetForCourseChange(normalizedCourseId);
|
|
605
|
-
progressRef.current = headlessRef.current.progress;
|
|
606
|
-
} else {
|
|
607
|
-
progressRef.current = createProgressController();
|
|
608
|
-
}
|
|
609
|
-
pendingCourseIdResetRef.current = true;
|
|
610
|
-
courseStartedEmittedToSinkRef.current = false;
|
|
611
|
-
courseStartedEmitGenerationRef.current += 1;
|
|
612
|
-
}
|
|
613
|
-
if (useV2Runtime && headlessRef.current) {
|
|
614
|
-
progressRef.current = headlessRef.current.progress;
|
|
615
|
-
}
|
|
616
|
-
const [progress, setProgress] = useState(() => progressRef.current.getState());
|
|
617
|
-
const syncProgress = useCallback(() => {
|
|
618
|
-
setProgress(progressRef.current.getState());
|
|
619
|
-
}, []);
|
|
620
|
-
const activeLessonIdRef = useRef(progress.activeLessonId);
|
|
621
|
-
activeLessonIdRef.current = progress.activeLessonId;
|
|
622
|
-
const xapiQueueRef = useRef(createXapiQueueFromObservability(normalizedConfig.observability));
|
|
623
|
-
const xapiRef = useRef(null);
|
|
624
|
-
const [xapi, setXapi] = useState(null);
|
|
625
|
-
const prevXapiCourseIdRef = useRef(normalizedCourseId);
|
|
626
|
-
const xapiEnabled = normalizedConfig.xapi?.enabled;
|
|
627
|
-
const xapiClient = normalizedConfig.xapi?.client;
|
|
628
|
-
const xapiTransport = normalizedConfig.xapi?.transport;
|
|
629
|
-
const courseId = normalizedCourseId;
|
|
630
|
-
const trackingEnabled = normalizedConfig.tracking?.enabled;
|
|
631
|
-
useIsoLayoutEffect(() => {
|
|
632
|
-
const courseChanged = prevXapiCourseIdRef.current !== courseId;
|
|
633
|
-
if (courseChanged) {
|
|
634
|
-
if (normalizedConfig.xapi?.client) {
|
|
635
|
-
const g = globalThis;
|
|
636
|
-
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production") {
|
|
637
|
-
console.warn(
|
|
638
|
-
"[lessonkit] courseId changed while using config.xapi.client; flush the client between courses or use config.xapi.transport so the provider can manage the queue."
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
void xapiRef.current?.flush();
|
|
642
|
-
}
|
|
643
|
-
xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
|
|
644
|
-
prevXapiCourseIdRef.current = courseId;
|
|
645
|
-
xapiCourseStartedSentOnClientRef.current = false;
|
|
646
|
-
xapiBootstrapSendRef.current = false;
|
|
647
|
-
}
|
|
648
|
-
const prev = xapiRef.current;
|
|
649
|
-
const next = createXapiClientFromConfig(normalizedConfig, xapiQueueRef.current);
|
|
650
|
-
xapiRef.current = next;
|
|
651
|
-
setXapi(next);
|
|
652
|
-
let bootstrapSent = false;
|
|
653
|
-
let bootstrapAlreadyStarted = false;
|
|
654
|
-
if (next) {
|
|
655
|
-
const sessionId = sessionIdRef.current;
|
|
656
|
-
const cid = courseIdRef.current;
|
|
657
|
-
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
658
|
-
bootstrapAlreadyStarted = hasCourseStarted(defaultStorage, sessionId, cid);
|
|
659
|
-
const clientChanged = !prev || prev !== next;
|
|
660
|
-
const skipBootstrap = trackingActive && !bootstrapAlreadyStarted;
|
|
661
|
-
const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && !xapiBootstrapSendRef.current && (!bootstrapAlreadyStarted || clientChanged);
|
|
662
|
-
if (needsBootstrap) {
|
|
663
|
-
try {
|
|
664
|
-
const event = buildCourseStartedEvent({
|
|
665
|
-
pluginHost: pluginHostRef.current,
|
|
666
|
-
courseId: cid,
|
|
667
|
-
sessionId,
|
|
668
|
-
attemptId: attemptIdRef.current,
|
|
669
|
-
user: userRef.current,
|
|
670
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
671
|
-
});
|
|
672
|
-
if (event !== null) {
|
|
673
|
-
const statement = telemetryEventToXAPIStatement3(event);
|
|
674
|
-
if (statement) {
|
|
675
|
-
next.send(statement);
|
|
676
|
-
xapiBootstrapSendRef.current = true;
|
|
677
|
-
bootstrapSent = true;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
} catch {
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
let cancelled = false;
|
|
685
|
-
void (async () => {
|
|
686
|
-
if (prev) {
|
|
687
|
-
try {
|
|
688
|
-
await prev.flush();
|
|
689
|
-
} catch {
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
if (cancelled) return;
|
|
693
|
-
try {
|
|
694
|
-
await next?.flush();
|
|
695
|
-
if (bootstrapSent && !cancelled) {
|
|
696
|
-
if (!bootstrapAlreadyStarted) {
|
|
697
|
-
markCourseStarted(defaultStorage, sessionIdRef.current, courseIdRef.current);
|
|
698
|
-
}
|
|
699
|
-
xapiCourseStartedSentOnClientRef.current = true;
|
|
700
|
-
}
|
|
701
|
-
} catch {
|
|
702
|
-
}
|
|
703
|
-
})();
|
|
704
|
-
return () => {
|
|
705
|
-
cancelled = true;
|
|
706
|
-
void prev?.flush();
|
|
707
|
-
};
|
|
708
|
-
}, [xapiEnabled, xapiClient, xapiTransport, courseId, trackingEnabled]);
|
|
709
|
-
const trackingRef = useRef(createTrackingClient2());
|
|
710
|
-
const trackingClientForUnmountRef = useRef(trackingRef.current);
|
|
711
|
-
const [tracking, setTracking] = useState(() => trackingRef.current);
|
|
712
|
-
const trackingSink = normalizedConfig.tracking?.sink;
|
|
713
|
-
const trackingBatchSink = normalizedConfig.tracking?.batchSink;
|
|
714
|
-
const batchEnabled = normalizedConfig.tracking?.batch?.enabled;
|
|
715
|
-
const batchFlushIntervalMs = normalizedConfig.tracking?.batch?.flushIntervalMs;
|
|
716
|
-
const batchMaxBatchSize = normalizedConfig.tracking?.batch?.maxBatchSize;
|
|
717
|
-
const buildCurrentPluginCtx = useCallback(
|
|
718
|
-
() => buildPluginContext({
|
|
719
|
-
courseId: courseIdRef.current,
|
|
720
|
-
sessionId: sessionIdRef.current,
|
|
721
|
-
attemptId: attemptIdRef.current,
|
|
722
|
-
user: userRef.current
|
|
723
|
-
}),
|
|
724
|
-
[]
|
|
725
|
-
);
|
|
726
|
-
useIsoLayoutEffect(() => {
|
|
727
|
-
const prev = trackingRef.current;
|
|
728
|
-
const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
|
|
729
|
-
const userBatchSink = wrapBatchSink(
|
|
730
|
-
normalizedConfig.tracking?.batchSink,
|
|
731
|
-
observabilityRef.current
|
|
732
|
-
);
|
|
733
|
-
assertTrackingSinkConfig(normalizedConfig.tracking);
|
|
734
|
-
const sink = pluginHostRef.current && baseSink ? (
|
|
735
|
-
/* v8 ignore next -- composeTrackingSink may return null; fall back to base sink */
|
|
736
|
-
pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink
|
|
737
|
-
) : baseSink;
|
|
738
|
-
const batchSink = pluginHostRef.current && userBatchSink ? async (events) => {
|
|
739
|
-
const host = pluginHostRef.current;
|
|
740
|
-
const ctx = buildCurrentPluginCtx();
|
|
741
|
-
const delivered = host.deliverTelemetryBatch(events, ctx);
|
|
742
|
-
const perEventForBatch = [];
|
|
743
|
-
const collector = (event) => {
|
|
744
|
-
perEventForBatch.push(event);
|
|
745
|
-
};
|
|
746
|
-
const composedPerEvent = host.composeTrackingSink(collector, buildCurrentPluginCtx) ?? collector;
|
|
747
|
-
for (const event of delivered) {
|
|
748
|
-
await Promise.resolve(composedPerEvent(event));
|
|
749
|
-
}
|
|
750
|
-
return userBatchSink(perEventForBatch);
|
|
751
|
-
} : userBatchSink;
|
|
752
|
-
const next = createTrackingClientFromConfig(
|
|
753
|
-
{
|
|
754
|
-
tracking: { ...normalizedConfig.tracking, sink, batchSink }
|
|
755
|
-
},
|
|
756
|
-
observabilityRef.current
|
|
757
|
-
);
|
|
758
|
-
trackingRef.current = next;
|
|
759
|
-
trackingClientForUnmountRef.current = next;
|
|
760
|
-
setTracking(next);
|
|
761
|
-
const sessionId = sessionIdRef.current;
|
|
762
|
-
const cid = courseIdRef.current;
|
|
763
|
-
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
764
|
-
const courseStartedFullySettled = hasCourseStartedEmittedToTracking(defaultStorage, sessionId, cid) && hasCourseStarted(defaultStorage, sessionId, cid) && hasCourseStartedPipelineDelivered(defaultStorage, sessionId, cid);
|
|
765
|
-
if (!trackingActive) {
|
|
766
|
-
courseStartedEmittedToSinkRef.current = false;
|
|
767
|
-
} else if (courseStartedFullySettled) {
|
|
768
|
-
courseStartedEmittedToSinkRef.current = true;
|
|
769
|
-
} else if (!courseStartedEmittedToSinkRef.current) {
|
|
770
|
-
const generation = ++courseStartedEmitGenerationRef.current;
|
|
771
|
-
const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
|
|
772
|
-
void (async () => {
|
|
773
|
-
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
774
|
-
const result = await emitPendingCourseStarted({
|
|
775
|
-
pluginHost: pluginHostRef.current,
|
|
776
|
-
tracking: next,
|
|
777
|
-
xapi: xapiRef.current,
|
|
778
|
-
storage: defaultStorage,
|
|
779
|
-
sessionId,
|
|
780
|
-
courseId: cid,
|
|
781
|
-
attemptId: attemptIdRef.current,
|
|
782
|
-
user: userRef.current,
|
|
783
|
-
lxpackBridge: lxpackBridgeModeRef.current,
|
|
784
|
-
onLxpackBridgeMiss,
|
|
785
|
-
extraSinks: extraSinksRef.current,
|
|
786
|
-
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
787
|
-
onXapiStatementSent: () => {
|
|
788
|
-
xapiCourseStartedSentOnClientRef.current = true;
|
|
789
|
-
},
|
|
790
|
-
shouldCommit
|
|
791
|
-
});
|
|
792
|
-
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
793
|
-
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
794
|
-
})();
|
|
795
|
-
}
|
|
796
|
-
return () => {
|
|
797
|
-
courseStartedEmitGenerationRef.current += 1;
|
|
798
|
-
if (prev !== trackingRef.current) {
|
|
799
|
-
void disposeTrackingClient(prev);
|
|
800
|
-
}
|
|
801
|
-
};
|
|
802
|
-
}, [
|
|
803
|
-
trackingEnabled,
|
|
804
|
-
trackingSink,
|
|
805
|
-
trackingBatchSink,
|
|
806
|
-
batchEnabled,
|
|
807
|
-
batchFlushIntervalMs,
|
|
808
|
-
batchMaxBatchSize,
|
|
809
|
-
normalizedConfig.plugins,
|
|
810
|
-
normalizedCourseId,
|
|
811
|
-
buildCurrentPluginCtx
|
|
812
|
-
]);
|
|
813
|
-
const emitWithBridge = useCallback((trackingClient, event) => {
|
|
814
|
-
emitTelemetryWithPlugins({
|
|
815
|
-
pluginHost: pluginHostRef.current,
|
|
816
|
-
tracking: trackingClient,
|
|
817
|
-
xapi: xapiRef.current,
|
|
818
|
-
event,
|
|
819
|
-
pluginCtx: buildPluginContext({
|
|
820
|
-
courseId: courseIdRef.current,
|
|
821
|
-
sessionId: sessionIdRef.current,
|
|
822
|
-
attemptId: attemptIdRef.current,
|
|
823
|
-
user: userRef.current
|
|
824
|
-
}),
|
|
825
|
-
lxpackBridge: lxpackBridgeModeRef.current,
|
|
826
|
-
onLxpackBridgeMiss,
|
|
827
|
-
extraSinks: extraSinksRef.current
|
|
828
|
-
});
|
|
829
|
-
}, [onLxpackBridgeMiss]);
|
|
830
|
-
const emitLifecycleEvent = useCallback(
|
|
831
|
-
(name, data, lessonId) => {
|
|
832
|
-
const event = tryBuildTelemetryEvent({
|
|
833
|
-
name,
|
|
834
|
-
courseId: courseIdRef.current,
|
|
835
|
-
lessonId: lessonId ?? activeLessonIdRef.current,
|
|
836
|
-
sessionId: sessionIdRef.current,
|
|
837
|
-
attemptId: attemptIdRef.current,
|
|
838
|
-
user: userRef.current,
|
|
839
|
-
data
|
|
840
|
-
});
|
|
841
|
-
if (!event) return;
|
|
842
|
-
emitWithBridge(trackingRef.current, event);
|
|
843
|
-
},
|
|
844
|
-
[emitWithBridge]
|
|
845
|
-
);
|
|
846
|
-
const track = useCallback(
|
|
847
|
-
(name, data, opts) => {
|
|
848
|
-
const event = tryBuildTelemetryEvent({
|
|
849
|
-
name,
|
|
850
|
-
courseId: courseIdRef.current,
|
|
851
|
-
lessonId: opts?.lessonId ?? activeLessonIdRef.current,
|
|
852
|
-
sessionId: sessionIdRef.current,
|
|
853
|
-
attemptId: attemptIdRef.current,
|
|
854
|
-
user: userRef.current,
|
|
855
|
-
data
|
|
856
|
-
});
|
|
857
|
-
if (!event) return;
|
|
858
|
-
emitWithBridge(trackingRef.current, event);
|
|
859
|
-
},
|
|
860
|
-
[emitWithBridge]
|
|
861
|
-
);
|
|
862
|
-
useLayoutEffect(() => {
|
|
863
|
-
if (!pendingCourseIdResetRef.current) return;
|
|
864
|
-
pendingCourseIdResetRef.current = false;
|
|
865
|
-
syncProgress();
|
|
866
|
-
if (!isTrackingActive(normalizedConfig.tracking)) return;
|
|
867
|
-
const sessionId = sessionIdRef.current;
|
|
868
|
-
const cid = courseIdRef.current;
|
|
869
|
-
void (async () => {
|
|
870
|
-
try {
|
|
871
|
-
await trackingRef.current?.flush?.();
|
|
872
|
-
} catch {
|
|
873
|
-
}
|
|
874
|
-
if (!courseStartedEmittedToSinkRef.current) {
|
|
875
|
-
const result = await emitPendingCourseStarted({
|
|
876
|
-
pluginHost: pluginHostRef.current,
|
|
877
|
-
tracking: trackingRef.current,
|
|
878
|
-
xapi: xapiRef.current,
|
|
879
|
-
storage: defaultStorage,
|
|
880
|
-
sessionId,
|
|
881
|
-
courseId: cid,
|
|
882
|
-
attemptId: attemptIdRef.current,
|
|
883
|
-
user: userRef.current,
|
|
884
|
-
lxpackBridge: lxpackBridgeModeRef.current,
|
|
885
|
-
onLxpackBridgeMiss,
|
|
886
|
-
extraSinks: extraSinksRef.current,
|
|
887
|
-
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
888
|
-
onXapiStatementSent: () => {
|
|
889
|
-
xapiCourseStartedSentOnClientRef.current = true;
|
|
890
|
-
}
|
|
891
|
-
});
|
|
892
|
-
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
893
|
-
}
|
|
894
|
-
})();
|
|
895
|
-
}, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress, onLxpackBridgeMiss]);
|
|
896
|
-
const emitLessonCompleted = useCallback(
|
|
897
|
-
(lessonId, durationMs) => {
|
|
898
|
-
track("lesson_completed", { lessonId, durationMs }, { lessonId });
|
|
899
|
-
if (durationMs !== void 0) {
|
|
900
|
-
track("lesson_time_on_task", { lessonId, durationMs }, { lessonId });
|
|
901
|
-
}
|
|
902
|
-
},
|
|
903
|
-
[track]
|
|
904
|
-
);
|
|
905
|
-
const completeLesson = useCallback(
|
|
906
|
-
(lessonId, opts) => {
|
|
907
|
-
if (opts?.courseId !== void 0 && opts.courseId !== courseIdRef.current) {
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
if (useV2Runtime && headlessRef.current) {
|
|
911
|
-
headlessRef.current.completeLesson(lessonId, emitLifecycleEvent);
|
|
912
|
-
syncProgress();
|
|
913
|
-
void Promise.resolve(trackingRef.current?.flush?.());
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
const result = progressRef.current.completeLesson(lessonId, Date.now());
|
|
917
|
-
if (!result.didComplete) return;
|
|
918
|
-
syncProgress();
|
|
919
|
-
emitLessonCompleted(lessonId, result.durationMs);
|
|
920
|
-
void Promise.resolve(trackingRef.current?.flush?.());
|
|
921
|
-
},
|
|
922
|
-
[syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
|
|
923
|
-
);
|
|
924
|
-
useEffect(() => {
|
|
925
|
-
return () => {
|
|
926
|
-
const client = trackingClientForUnmountRef.current;
|
|
927
|
-
const xapi2 = xapiRef.current;
|
|
928
|
-
void (async () => {
|
|
929
|
-
try {
|
|
930
|
-
await xapi2?.flush();
|
|
931
|
-
} catch {
|
|
932
|
-
}
|
|
933
|
-
try {
|
|
934
|
-
await client?.flush?.();
|
|
935
|
-
} catch {
|
|
936
|
-
}
|
|
937
|
-
try {
|
|
938
|
-
await client?.dispose?.();
|
|
939
|
-
} catch {
|
|
940
|
-
}
|
|
941
|
-
})();
|
|
942
|
-
};
|
|
943
|
-
}, []);
|
|
944
|
-
useEffect(() => {
|
|
945
|
-
if (typeof document === "undefined") return;
|
|
946
|
-
const flushOnPageExit = () => {
|
|
947
|
-
try {
|
|
948
|
-
xapiRef.current?.flushOnExit?.();
|
|
949
|
-
trackingRef.current?.flushOnExit?.();
|
|
950
|
-
} finally {
|
|
951
|
-
void xapiRef.current?.flush();
|
|
952
|
-
void trackingRef.current?.flush?.();
|
|
953
|
-
}
|
|
954
|
-
};
|
|
955
|
-
const onVisibilityChange = () => {
|
|
956
|
-
if (document.visibilityState === "hidden") flushOnPageExit();
|
|
957
|
-
};
|
|
958
|
-
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
959
|
-
window.addEventListener("pagehide", flushOnPageExit);
|
|
960
|
-
return () => {
|
|
961
|
-
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
962
|
-
window.removeEventListener("pagehide", flushOnPageExit);
|
|
963
|
-
};
|
|
964
|
-
}, []);
|
|
965
|
-
useEffect(() => {
|
|
966
|
-
warnMissingProductionObservability(observabilityRef.current, {
|
|
967
|
-
trackingEnabled: isTrackingActive(normalizedConfig.tracking),
|
|
968
|
-
xapiEnabled: normalizedConfig.xapi?.enabled !== false && Boolean(
|
|
969
|
-
normalizedConfig.xapi?.client || normalizedConfig.xapi?.transport || normalizedConfig.xapi?.enabled === true
|
|
970
|
-
)
|
|
971
|
-
});
|
|
972
|
-
}, [
|
|
973
|
-
normalizedConfig.tracking,
|
|
974
|
-
normalizedConfig.xapi?.enabled,
|
|
975
|
-
normalizedConfig.xapi?.client,
|
|
976
|
-
normalizedConfig.xapi?.transport,
|
|
977
|
-
normalizedConfig.observability
|
|
978
|
-
]);
|
|
979
|
-
const setActiveLesson = useCallback(
|
|
980
|
-
(lessonId) => {
|
|
981
|
-
if (useV2Runtime && headlessRef.current) {
|
|
982
|
-
headlessRef.current.setActiveLesson(lessonId, emitLifecycleEvent);
|
|
983
|
-
syncProgress();
|
|
984
|
-
void Promise.resolve(trackingRef.current?.flush?.());
|
|
985
|
-
return;
|
|
986
|
-
}
|
|
987
|
-
const current = progressRef.current.getState();
|
|
988
|
-
if (current.activeLessonId === lessonId) return;
|
|
989
|
-
if (current.completedLessonIds.has(lessonId)) {
|
|
990
|
-
progressRef.current.setActiveLesson(lessonId, Date.now());
|
|
991
|
-
syncProgress();
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
const previous = current.activeLessonId;
|
|
995
|
-
if (previous && previous !== lessonId) {
|
|
996
|
-
const completed = progressRef.current.completeLesson(previous, Date.now());
|
|
997
|
-
if (completed.didComplete) {
|
|
998
|
-
emitLessonCompleted(previous, completed.durationMs);
|
|
999
|
-
void Promise.resolve(trackingRef.current?.flush?.());
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
progressRef.current.setActiveLesson(lessonId, Date.now());
|
|
1003
|
-
syncProgress();
|
|
1004
|
-
track("lesson_started", { lessonId }, { lessonId });
|
|
1005
|
-
},
|
|
1006
|
-
[track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
|
|
1007
|
-
);
|
|
1008
|
-
const completeCourse = useCallback(() => {
|
|
1009
|
-
if (useV2Runtime && headlessRef.current) {
|
|
1010
|
-
headlessRef.current.completeCourse(emitLifecycleEvent);
|
|
1011
|
-
syncProgress();
|
|
1012
|
-
void trackingRef.current?.flush?.();
|
|
1013
|
-
return;
|
|
1014
|
-
}
|
|
1015
|
-
const current = progressRef.current.getState();
|
|
1016
|
-
if (current.activeLessonId) {
|
|
1017
|
-
const lessonResult = progressRef.current.completeLesson(current.activeLessonId, Date.now());
|
|
1018
|
-
if (lessonResult.didComplete) {
|
|
1019
|
-
emitLessonCompleted(current.activeLessonId, lessonResult.durationMs);
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
const result = progressRef.current.completeCourse();
|
|
1023
|
-
if (!result.didComplete) return;
|
|
1024
|
-
syncProgress();
|
|
1025
|
-
track("course_completed");
|
|
1026
|
-
void trackingRef.current?.flush?.();
|
|
1027
|
-
}, [track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]);
|
|
1028
|
-
const sessionUser = normalizedConfig.session?.user;
|
|
1029
|
-
const sessionUserKey = useMemo(
|
|
1030
|
-
() => sessionUser ? JSON.stringify(sessionUser) : "",
|
|
1031
|
-
[sessionUser]
|
|
1032
|
-
);
|
|
1033
|
-
const sessionAttemptId = normalizedConfig.session?.attemptId;
|
|
1034
|
-
const sessionConfiguredId = normalizedConfig.session?.sessionId;
|
|
1035
|
-
useEffect(() => {
|
|
1036
|
-
if (useV2Runtime && headlessRef.current) {
|
|
1037
|
-
headlessRef.current.updateConfig({
|
|
1038
|
-
courseId: normalizedCourseId,
|
|
1039
|
-
session: normalizedConfig.session
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
}, [
|
|
1043
|
-
useV2Runtime,
|
|
1044
|
-
normalizedCourseId,
|
|
1045
|
-
sessionAttemptId,
|
|
1046
|
-
sessionConfiguredId,
|
|
1047
|
-
sessionUserKey,
|
|
1048
|
-
normalizedConfig.session
|
|
1049
|
-
]);
|
|
1050
|
-
useEffect(() => {
|
|
1051
|
-
if (!useV2Runtime || !headlessRef.current) return;
|
|
1052
|
-
headlessRef.current.updateConfig({
|
|
1053
|
-
plugins: pluginHostRef.current ?? normalizedConfig.plugins
|
|
1054
|
-
});
|
|
1055
|
-
}, [useV2Runtime, pluginHost]);
|
|
1056
|
-
useEffect(() => {
|
|
1057
|
-
const host = useV2Runtime ? headlessRef.current?.pluginHost ?? null : pluginHost;
|
|
1058
|
-
if (!host) return;
|
|
1059
|
-
const ctx = buildPluginContext({
|
|
1060
|
-
courseId: courseIdRef.current,
|
|
1061
|
-
sessionId: sessionIdRef.current,
|
|
1062
|
-
attemptId: attemptIdRef.current,
|
|
1063
|
-
user: userRef.current
|
|
1064
|
-
});
|
|
1065
|
-
host.setupAll(ctx);
|
|
1066
|
-
return () => {
|
|
1067
|
-
host.disposeAll();
|
|
1068
|
-
};
|
|
1069
|
-
}, [pluginHost, useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
|
|
1070
|
-
useEffect(() => {
|
|
1071
|
-
const nextConfigured = normalizedConfig.session?.sessionId;
|
|
1072
|
-
const prevConfigured = prevConfiguredSessionIdRef.current;
|
|
1073
|
-
if (nextConfigured === prevConfigured) return;
|
|
1074
|
-
prevConfiguredSessionIdRef.current = nextConfigured;
|
|
1075
|
-
const cid = courseIdRef.current;
|
|
1076
|
-
if (nextConfigured) {
|
|
1077
|
-
const fromIds = /* @__PURE__ */ new Set();
|
|
1078
|
-
if (prevConfigured) fromIds.add(prevConfigured);
|
|
1079
|
-
const tabId = getTabSessionId(defaultStorage);
|
|
1080
|
-
if (tabId) fromIds.add(tabId);
|
|
1081
|
-
for (const fromId of fromIds) {
|
|
1082
|
-
if (fromId !== nextConfigured) {
|
|
1083
|
-
migrateCourseStartedMark(defaultStorage, fromId, nextConfigured, cid);
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
sessionIdRef.current = nextConfigured;
|
|
1087
|
-
} else if (prevConfigured) {
|
|
1088
|
-
const nextAuto = resolveSessionId(defaultStorage, void 0);
|
|
1089
|
-
migrateCourseStartedMark(defaultStorage, prevConfigured, nextAuto, cid);
|
|
1090
|
-
sessionIdRef.current = nextAuto;
|
|
1091
|
-
}
|
|
1092
|
-
}, [sessionConfiguredId, normalizedCourseId]);
|
|
1093
|
-
const runtime = useMemo(
|
|
1094
|
-
() => ({
|
|
1095
|
-
config: normalizedConfig,
|
|
1096
|
-
tracking,
|
|
1097
|
-
xapi,
|
|
1098
|
-
storage: defaultStorage,
|
|
1099
|
-
session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
|
|
1100
|
-
progress,
|
|
1101
|
-
setActiveLesson,
|
|
1102
|
-
completeLesson,
|
|
1103
|
-
completeCourse,
|
|
1104
|
-
track,
|
|
1105
|
-
plugins: pluginHost
|
|
1106
|
-
}),
|
|
1107
|
-
[
|
|
1108
|
-
normalizedConfig,
|
|
1109
|
-
tracking,
|
|
1110
|
-
xapi,
|
|
1111
|
-
progress,
|
|
1112
|
-
setActiveLesson,
|
|
1113
|
-
completeLesson,
|
|
1114
|
-
completeCourse,
|
|
1115
|
-
track,
|
|
1116
|
-
pluginHost,
|
|
1117
|
-
sessionUser,
|
|
1118
|
-
sessionAttemptId,
|
|
1119
|
-
sessionConfiguredId
|
|
1120
|
-
]
|
|
1121
|
-
);
|
|
1122
|
-
return runtime;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
// src/context.tsx
|
|
1126
|
-
import { jsx } from "react/jsx-runtime";
|
|
1127
|
-
var LessonkitContext = createContext(null);
|
|
1128
|
-
function LessonkitProvider(props) {
|
|
1129
|
-
const runtime = useLessonkitProviderRuntime(props.config);
|
|
1130
|
-
return /* @__PURE__ */ jsx(LessonkitContext.Provider, { value: runtime, children: props.children });
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
// src/hooks.ts
|
|
1134
|
-
import { useContext, useMemo as useMemo3 } from "react";
|
|
1135
|
-
|
|
1136
|
-
// src/assessment/useAssessmentState.ts
|
|
1137
|
-
import { useMemo as useMemo2 } from "react";
|
|
1138
|
-
function useAssessmentState(enclosingLessonId) {
|
|
1139
|
-
const { track } = useLessonkit();
|
|
1140
|
-
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
1141
|
-
return useMemo2(
|
|
1142
|
-
() => ({
|
|
1143
|
-
answer: (data) => {
|
|
1144
|
-
track("assessment_answered", data, trackOpts);
|
|
1145
|
-
},
|
|
1146
|
-
complete: (data) => {
|
|
1147
|
-
track("assessment_completed", data, trackOpts);
|
|
1148
|
-
}
|
|
1149
|
-
}),
|
|
1150
|
-
[track, enclosingLessonId]
|
|
1151
|
-
);
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
// src/hooks.ts
|
|
1155
|
-
function useLessonkit() {
|
|
1156
|
-
const ctx = useContext(LessonkitContext);
|
|
1157
|
-
if (!ctx) throw new Error("LessonKit: missing LessonkitProvider");
|
|
1158
|
-
return ctx;
|
|
1159
|
-
}
|
|
1160
|
-
function useProgress() {
|
|
1161
|
-
const { progress } = useLessonkit();
|
|
1162
|
-
return progress;
|
|
1163
|
-
}
|
|
1164
|
-
function useTracking() {
|
|
1165
|
-
const { track } = useLessonkit();
|
|
1166
|
-
return useMemo3(() => ({ track }), [track]);
|
|
1167
|
-
}
|
|
1168
|
-
function useCompletion() {
|
|
1169
|
-
const { completeLesson, completeCourse } = useLessonkit();
|
|
1170
|
-
return useMemo3(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
|
|
1171
|
-
}
|
|
1172
|
-
function useQuizState(enclosingLessonId) {
|
|
1173
|
-
const { track } = useLessonkit();
|
|
1174
|
-
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
1175
|
-
return useMemo3(
|
|
1176
|
-
() => ({
|
|
1177
|
-
answer: (opts) => {
|
|
1178
|
-
track("quiz_answered", opts, trackOpts);
|
|
1179
|
-
},
|
|
1180
|
-
complete: (opts) => {
|
|
1181
|
-
track("quiz_completed", opts, trackOpts);
|
|
1182
|
-
}
|
|
1183
|
-
}),
|
|
1184
|
-
[track, enclosingLessonId]
|
|
1185
|
-
);
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// src/lessonContext.tsx
|
|
1189
|
-
import { createContext as createContext2, useContext as useContext2 } from "react";
|
|
1190
|
-
var LessonContext = createContext2(void 0);
|
|
1191
|
-
function useEnclosingLessonId() {
|
|
1192
|
-
return useContext2(LessonContext);
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
// src/runtime/validateComponentId.ts
|
|
1196
|
-
import { assertValidId as assertValidId2 } from "@lessonkit/core";
|
|
1197
|
-
function isDevEnvironment4() {
|
|
1198
|
-
const g = globalThis;
|
|
1199
|
-
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
1200
|
-
}
|
|
1201
|
-
function normalizeComponentId(id, path) {
|
|
1202
|
-
if (path === "courseId") return assertValidId2(id, "courseId");
|
|
1203
|
-
if (path === "lessonId") return assertValidId2(id, "lessonId");
|
|
1204
|
-
if (path === "checkId") return assertValidId2(id, "checkId");
|
|
1205
|
-
if (path === "blockId") return assertValidId2(id, "blockId");
|
|
1206
|
-
return assertValidId2(id, path);
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
// src/runtime/lessonMountRegistry.ts
|
|
1210
|
-
var mountCounts = /* @__PURE__ */ new Map();
|
|
1211
|
-
var warnedConcurrentLessons = false;
|
|
1212
|
-
function registerLessonMount(lessonId) {
|
|
1213
|
-
if (isDevEnvironment4() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
|
|
1214
|
-
warnedConcurrentLessons = true;
|
|
1215
|
-
console.warn(
|
|
1216
|
-
"[lessonkit] Multiple <Lesson> components are mounted; only one should be active at a time. Set autoCompleteOnUnmount={false} on routed lessons or unmount the previous lesson before showing the next."
|
|
1217
|
-
);
|
|
1218
|
-
}
|
|
1219
|
-
mountCounts.set(lessonId, (mountCounts.get(lessonId) ?? 0) + 1);
|
|
1220
|
-
return () => {
|
|
1221
|
-
const next = (mountCounts.get(lessonId) ?? 1) - 1;
|
|
1222
|
-
if (next <= 0) {
|
|
1223
|
-
mountCounts.delete(lessonId);
|
|
1224
|
-
} else {
|
|
1225
|
-
mountCounts.set(lessonId, next);
|
|
1226
|
-
}
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
function getLessonMountCount(lessonId) {
|
|
1230
|
-
return mountCounts.get(lessonId) ?? 0;
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
// src/components/Quiz.tsx
|
|
1234
|
-
import { forwardRef, useEffect as useEffect3, useId, useMemo as useMemo5, useRef as useRef4, useState as useState3 } from "react";
|
|
1235
|
-
import { visuallyHiddenStyle } from "@lessonkit/accessibility";
|
|
1236
|
-
|
|
1237
|
-
// src/assessment/AssessmentLessonGuard.tsx
|
|
1238
|
-
import { useEffect as useEffect2 } from "react";
|
|
1239
|
-
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
1240
|
-
var warnedAssessmentOutsideLesson = false;
|
|
1241
|
-
function resetAssessmentWarningsForTests() {
|
|
1242
|
-
warnedAssessmentOutsideLesson = false;
|
|
1243
|
-
}
|
|
1244
|
-
function AssessmentLessonGuard(props) {
|
|
1245
|
-
const enclosingLessonId = useEnclosingLessonId();
|
|
1246
|
-
const missingLesson = enclosingLessonId === void 0;
|
|
1247
|
-
useEffect2(() => {
|
|
1248
|
-
if (!missingLesson || isDevEnvironment4()) return;
|
|
1249
|
-
if (!warnedAssessmentOutsideLesson) {
|
|
1250
|
-
warnedAssessmentOutsideLesson = true;
|
|
1251
|
-
console.error(
|
|
1252
|
-
`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>; assessment telemetry will not be emitted.`
|
|
1253
|
-
);
|
|
1254
|
-
}
|
|
1255
|
-
}, [missingLesson, props.blockLabel]);
|
|
1256
|
-
if (missingLesson && isDevEnvironment4()) {
|
|
1257
|
-
throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
|
|
1258
|
-
}
|
|
1259
|
-
if (missingLesson) {
|
|
1260
|
-
return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label": `${props.blockLabel} configuration error`, "data-lk-check-id": props.checkId, children: /* @__PURE__ */ jsxs("p", { children: [
|
|
1261
|
-
props.blockLabel,
|
|
1262
|
-
" must be placed inside a Lesson."
|
|
1263
|
-
] }) });
|
|
1264
|
-
}
|
|
1265
|
-
return /* @__PURE__ */ jsx2(Fragment, { children: props.children(enclosingLessonId) });
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
// src/assessment/internal/buildAssessmentHandle.ts
|
|
1269
|
-
function buildAssessmentHandle(opts) {
|
|
1270
|
-
return {
|
|
1271
|
-
getScore: opts.getScore,
|
|
1272
|
-
getMaxScore: opts.getMaxScore,
|
|
1273
|
-
getAnswerGiven: opts.getAnswerGiven,
|
|
1274
|
-
resetTask: opts.resetTask,
|
|
1275
|
-
showSolutions: opts.showSolutions,
|
|
1276
|
-
getXAPIData: opts.getXAPIData,
|
|
1277
|
-
...opts.getCurrentState ? { getCurrentState: opts.getCurrentState } : {},
|
|
1278
|
-
...opts.resume ? { resume: opts.resume } : {}
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
// src/assessment/internal/resumeState.ts
|
|
1283
|
-
function readBooleanField(state, key) {
|
|
1284
|
-
const value = state[key];
|
|
1285
|
-
if (value === true || value === false || value === null) return value;
|
|
1286
|
-
return void 0;
|
|
1287
|
-
}
|
|
1288
|
-
function readStringField(state, key) {
|
|
1289
|
-
const value = state[key];
|
|
1290
|
-
if (typeof value === "string" || value === null) return value;
|
|
1291
|
-
return void 0;
|
|
1292
|
-
}
|
|
1293
|
-
function readNumberField(state, key) {
|
|
1294
|
-
const value = state[key];
|
|
1295
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1296
|
-
if (value === null) return null;
|
|
1297
|
-
return void 0;
|
|
1298
|
-
}
|
|
1299
|
-
function readBooleanStateField(state, key, apply) {
|
|
1300
|
-
const value = state[key];
|
|
1301
|
-
if (typeof value === "boolean") apply(value);
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
// src/assessment/internal/useAssessmentHandleRegistration.ts
|
|
1305
|
-
import { useImperativeHandle as useImperativeHandle2 } from "react";
|
|
1306
|
-
|
|
1307
|
-
// src/compound/CompoundProvider.tsx
|
|
1308
|
-
import React5, { createContext as createContext5, useCallback as useCallback2, useContext as useContext5, useImperativeHandle, useMemo as useMemo4, useRef as useRef3, useState as useState2 } from "react";
|
|
1309
|
-
import { clampCompoundPageIndex, createCompoundResumeState } from "@lessonkit/core";
|
|
1310
|
-
|
|
1311
|
-
// src/compound/aggregateScores.ts
|
|
1312
|
-
function aggregateAssessmentScores(handles, opts) {
|
|
1313
|
-
let score = 0;
|
|
1314
|
-
let maxScore = 0;
|
|
1315
|
-
let allAnswered = true;
|
|
1316
|
-
for (const entry of handles) {
|
|
1317
|
-
const handle = "handle" in entry ? entry.handle : entry;
|
|
1318
|
-
const pageIndex = "handle" in entry ? entry.pageIndex : void 0;
|
|
1319
|
-
score += handle.getScore();
|
|
1320
|
-
maxScore += handle.getMaxScore();
|
|
1321
|
-
const countsForAnswerGiven = opts?.answerPageIndex === void 0 || pageIndex === void 0 || pageIndex === opts.answerPageIndex;
|
|
1322
|
-
if (countsForAnswerGiven && !handle.getAnswerGiven()) allAnswered = false;
|
|
1323
|
-
}
|
|
1324
|
-
return { score, maxScore, allAnswered };
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
// src/compound/CompoundHydrationBridge.tsx
|
|
1328
|
-
import { createContext as createContext3, useContext as useContext3, useRef as useRef2 } from "react";
|
|
1329
|
-
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
1330
|
-
var CompoundHydrationBridgeContext = createContext3(
|
|
1331
|
-
null
|
|
1332
|
-
);
|
|
1333
|
-
function CompoundHydrationBridgeProvider({ children }) {
|
|
1334
|
-
const bridgeRef = useRef2(null);
|
|
1335
|
-
return /* @__PURE__ */ jsx3(CompoundHydrationBridgeContext.Provider, { value: bridgeRef, children });
|
|
1336
|
-
}
|
|
1337
|
-
function useCompoundHydrationBridgeRef() {
|
|
1338
|
-
return useContext3(CompoundHydrationBridgeContext);
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
// src/compound/CompoundPageIndexContext.tsx
|
|
1342
|
-
import { createContext as createContext4, useContext as useContext4 } from "react";
|
|
1343
|
-
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1344
|
-
var CompoundPageIndexContext = createContext4(void 0);
|
|
1345
|
-
function CompoundPageIndexProvider({
|
|
1346
|
-
pageIndex,
|
|
1347
|
-
children
|
|
1348
|
-
}) {
|
|
1349
|
-
return /* @__PURE__ */ jsx4(CompoundPageIndexContext.Provider, { value: pageIndex, children });
|
|
1350
|
-
}
|
|
1351
|
-
function useCompoundPageIndex() {
|
|
1352
|
-
return useContext4(CompoundPageIndexContext);
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
// src/compound/CompoundProvider.tsx
|
|
1356
|
-
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1357
|
-
var CompoundRegistryContext = createContext5(null);
|
|
1358
|
-
var CompoundHandlesVersionContext = createContext5(0);
|
|
1359
|
-
function CompoundProvider({
|
|
1360
|
-
children,
|
|
1361
|
-
activePageIndex: _activePageIndex,
|
|
1362
|
-
onActivePageIndexChange: _onActivePageIndexChange
|
|
1363
|
-
}) {
|
|
1364
|
-
const registryRef = useRef3(/* @__PURE__ */ new Map());
|
|
1365
|
-
const [handlesVersion, setHandlesVersion] = useState2(0);
|
|
1366
|
-
const register = useCallback2((checkId, handle, pageIndex) => {
|
|
1367
|
-
const prev = registryRef.current.get(checkId);
|
|
1368
|
-
if (prev && prev.handle !== handle) {
|
|
1369
|
-
const message = `[lessonkit] duplicate checkId "${checkId}" registered in the same compound container; the previous handle was replaced.`;
|
|
1370
|
-
if (isDevEnvironment4()) {
|
|
1371
|
-
console.error(message);
|
|
1372
|
-
} else {
|
|
1373
|
-
console.warn(message);
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
registryRef.current.set(checkId, { handle, pageIndex });
|
|
1377
|
-
if (prev?.handle !== handle || prev?.pageIndex !== pageIndex) {
|
|
1378
|
-
setHandlesVersion((v) => v + 1);
|
|
1379
|
-
}
|
|
1380
|
-
return () => {
|
|
1381
|
-
const current = registryRef.current.get(checkId);
|
|
1382
|
-
if (current?.handle === handle) {
|
|
1383
|
-
registryRef.current.delete(checkId);
|
|
1384
|
-
setHandlesVersion((v) => v + 1);
|
|
1385
|
-
}
|
|
1386
|
-
};
|
|
1387
|
-
}, []);
|
|
1388
|
-
const registryValue = useMemo4(
|
|
1389
|
-
() => ({
|
|
1390
|
-
register,
|
|
1391
|
-
getHandles: () => {
|
|
1392
|
-
const handles = /* @__PURE__ */ new Map();
|
|
1393
|
-
for (const [checkId, entry] of registryRef.current) {
|
|
1394
|
-
handles.set(checkId, entry.handle);
|
|
1395
|
-
}
|
|
1396
|
-
return handles;
|
|
1397
|
-
},
|
|
1398
|
-
getRegisteredHandles: () => registryRef.current
|
|
1399
|
-
}),
|
|
1400
|
-
[register]
|
|
1401
|
-
);
|
|
1402
|
-
return /* @__PURE__ */ jsx5(CompoundHydrationBridgeProvider, { children: /* @__PURE__ */ jsx5(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ jsx5(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) }) });
|
|
1403
|
-
}
|
|
1404
|
-
function useCompoundRegistry() {
|
|
1405
|
-
const registry = useContext5(CompoundRegistryContext);
|
|
1406
|
-
const handlesVersion = useContext5(CompoundHandlesVersionContext);
|
|
1407
|
-
if (!registry) return null;
|
|
1408
|
-
return { ...registry, handlesVersion };
|
|
1409
|
-
}
|
|
1410
|
-
function useCompoundHandlesVersion() {
|
|
1411
|
-
return useContext5(CompoundHandlesVersionContext);
|
|
1412
|
-
}
|
|
1413
|
-
function useRegisterAssessmentHandle(checkId, handle) {
|
|
1414
|
-
const registry = useContext5(CompoundRegistryContext);
|
|
1415
|
-
const pageIndex = useCompoundPageIndex();
|
|
1416
|
-
React5.useLayoutEffect(() => {
|
|
1417
|
-
if (!registry || !handle) return;
|
|
1418
|
-
return registry.register(checkId, handle, pageIndex);
|
|
1419
|
-
}, [registry, checkId, handle, pageIndex]);
|
|
1420
|
-
}
|
|
1421
|
-
function useCompoundHandleRef(ref, opts) {
|
|
1422
|
-
const { activePageIndex, setActivePageIndex, getHandles, getRegisteredHandles, pageCount } = opts;
|
|
1423
|
-
const bridgeRef = useCompoundHydrationBridgeRef();
|
|
1424
|
-
const setIndexClamped = useCallback2(
|
|
1425
|
-
(index) => {
|
|
1426
|
-
const next = pageCount !== void 0 ? clampCompoundPageIndex(index, pageCount) : Math.max(0, Math.floor(index));
|
|
1427
|
-
setActivePageIndex(next);
|
|
1428
|
-
},
|
|
1429
|
-
[pageCount, setActivePageIndex]
|
|
1430
|
-
);
|
|
1431
|
-
useImperativeHandle(
|
|
1432
|
-
ref,
|
|
1433
|
-
() => ({
|
|
1434
|
-
getScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).score,
|
|
1435
|
-
getMaxScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).maxScore,
|
|
1436
|
-
getAnswerGiven: () => aggregateAssessmentScores(getRegisteredHandles().values(), {
|
|
1437
|
-
answerPageIndex: activePageIndex
|
|
1438
|
-
}).allAnswered,
|
|
1439
|
-
resetTask: () => {
|
|
1440
|
-
for (const entry of getRegisteredHandles().values()) entry.handle.resetTask();
|
|
1441
|
-
},
|
|
1442
|
-
showSolutions: () => {
|
|
1443
|
-
if (!opts.enableSolutionsButton) return;
|
|
1444
|
-
for (const entry of getRegisteredHandles().values()) entry.handle.showSolutions();
|
|
1445
|
-
},
|
|
1446
|
-
getCurrentState: () => {
|
|
1447
|
-
const childStates = {};
|
|
1448
|
-
for (const [checkId, entry] of getRegisteredHandles()) {
|
|
1449
|
-
if (entry.handle.getCurrentState) {
|
|
1450
|
-
childStates[checkId] = entry.handle.getCurrentState();
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
return createCompoundResumeState({ activePageIndex, childStates });
|
|
1454
|
-
},
|
|
1455
|
-
resume: (state) => {
|
|
1456
|
-
bridgeRef?.current?.notifyImperativeResume(state);
|
|
1457
|
-
}
|
|
1458
|
-
}),
|
|
1459
|
-
[activePageIndex, setIndexClamped, getHandles, getRegisteredHandles, opts.enableSolutionsButton, bridgeRef]
|
|
1460
|
-
);
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
// src/assessment/internal/useAssessmentHandleRegistration.ts
|
|
1464
|
-
function useAssessmentHandleRegistration(checkId, handle, ref) {
|
|
1465
|
-
useImperativeHandle2(ref, () => handle, [handle]);
|
|
1466
|
-
useRegisterAssessmentHandle(checkId, handle);
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
// src/assessment/internal/usePluginScoring.ts
|
|
1470
|
-
import { useCallback as useCallback3 } from "react";
|
|
1471
|
-
|
|
1472
|
-
// src/assessment/scoring.ts
|
|
1473
|
-
function resolvePassingThreshold(passingScore, maxScore) {
|
|
1474
|
-
return passingScore ?? maxScore;
|
|
1475
|
-
}
|
|
1476
|
-
function meetsPassingThreshold(score, maxScore, passingScore) {
|
|
1477
|
-
const threshold = resolvePassingThreshold(passingScore, maxScore);
|
|
1478
|
-
return score >= threshold;
|
|
1479
|
-
}
|
|
1480
|
-
function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
|
|
1481
|
-
const maxScore = custom?.maxScore ?? fallbackMax;
|
|
1482
|
-
if (custom?.passed !== void 0) {
|
|
1483
|
-
const score2 = custom.passed ? custom.score ?? maxScore : custom.score ?? 0;
|
|
1484
|
-
return { score: score2, maxScore, passed: custom.passed };
|
|
1485
|
-
}
|
|
1486
|
-
if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
1487
|
-
const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
|
|
1488
|
-
return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
|
|
1489
|
-
}
|
|
1490
|
-
const score = fallbackCorrect ? maxScore : 0;
|
|
1491
|
-
const passed = meetsPassingThreshold(score, maxScore, passingScore);
|
|
1492
|
-
return { score, maxScore, passed };
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
// src/assessment/internal/usePluginScoring.ts
|
|
1496
|
-
function usePluginScoring(checkId, lessonId) {
|
|
1497
|
-
const { plugins, config, session } = useLessonkit();
|
|
1498
|
-
const getPluginScore = useCallback3(
|
|
1499
|
-
(response) => {
|
|
1500
|
-
const pluginCtx = buildPluginContext({
|
|
1501
|
-
courseId: config.courseId,
|
|
1502
|
-
sessionId: session.sessionId,
|
|
1503
|
-
attemptId: session.attemptId,
|
|
1504
|
-
user: session.user
|
|
1505
|
-
});
|
|
1506
|
-
return plugins?.scoreAssessment({ checkId, lessonId, response }, pluginCtx) ?? null;
|
|
1507
|
-
},
|
|
1508
|
-
[checkId, config.courseId, lessonId, plugins, session.attemptId, session.sessionId, session.user]
|
|
1509
|
-
);
|
|
1510
|
-
const scoreResponse = useCallback3(
|
|
1511
|
-
(response, defaultCorrect, maxScore = 1, passingScore) => scoreFromCustom(getPluginScore(response), defaultCorrect, maxScore, passingScore),
|
|
1512
|
-
[getPluginScore]
|
|
1513
|
-
);
|
|
1514
|
-
const isChoiceCorrect = useCallback3(
|
|
1515
|
-
(choice, answer, custom, passingScore) => {
|
|
1516
|
-
if (!custom) return choice === answer;
|
|
1517
|
-
if (custom.passed !== void 0) return custom.passed;
|
|
1518
|
-
if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
1519
|
-
return meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
|
|
1520
|
-
}
|
|
1521
|
-
return choice === answer;
|
|
1522
|
-
},
|
|
1523
|
-
[]
|
|
1524
|
-
);
|
|
1525
|
-
return { getPluginScore, scoreResponse, isChoiceCorrect };
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
// src/components/Quiz.tsx
|
|
1529
|
-
import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1530
|
-
function QuizInner(props, ref) {
|
|
1531
|
-
const { enclosingLessonId } = props;
|
|
1532
|
-
const checkId = useMemo5(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1533
|
-
const quiz = useQuizState(enclosingLessonId);
|
|
1534
|
-
const { getPluginScore, isChoiceCorrect } = usePluginScoring(checkId, enclosingLessonId);
|
|
1535
|
-
const [selected, setSelected] = useState3(null);
|
|
1536
|
-
const [selectionCorrect, setSelectionCorrect] = useState3(null);
|
|
1537
|
-
const [quizPassed, setQuizPassed] = useState3(false);
|
|
1538
|
-
const [completedScore, setCompletedScore] = useState3(null);
|
|
1539
|
-
const [completedMaxScore, setCompletedMaxScore] = useState3(null);
|
|
1540
|
-
const completedRef = useRef4(false);
|
|
1541
|
-
const telemetryReplayedRef = useRef4(false);
|
|
1542
|
-
const questionId = useId();
|
|
1543
|
-
const choicesKey = props.choices.join("\0");
|
|
1544
|
-
useEffect3(() => {
|
|
1545
|
-
completedRef.current = false;
|
|
1546
|
-
telemetryReplayedRef.current = false;
|
|
1547
|
-
setQuizPassed(false);
|
|
1548
|
-
setSelected(null);
|
|
1549
|
-
setSelectionCorrect(null);
|
|
1550
|
-
setCompletedScore(null);
|
|
1551
|
-
setCompletedMaxScore(null);
|
|
1552
|
-
}, [checkId, props.answer, props.question, choicesKey]);
|
|
1553
|
-
const passed = quizPassed;
|
|
1554
|
-
const resolveScores = () => {
|
|
1555
|
-
const maxScore = completedMaxScore ?? 1;
|
|
1556
|
-
if (quizPassed) {
|
|
1557
|
-
return { score: completedScore ?? maxScore, maxScore };
|
|
1558
|
-
}
|
|
1559
|
-
if (selected !== null && selectionCorrect) {
|
|
1560
|
-
return { score: completedMaxScore ?? maxScore, maxScore };
|
|
1561
|
-
}
|
|
1562
|
-
return { score: 0, maxScore };
|
|
1563
|
-
};
|
|
1564
|
-
const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
|
|
1565
|
-
if (!nextPassed || telemetryReplayedRef.current) return;
|
|
1566
|
-
telemetryReplayedRef.current = true;
|
|
1567
|
-
if (nextSelected !== null) {
|
|
1568
|
-
quiz.answer({
|
|
1569
|
-
checkId,
|
|
1570
|
-
question: props.question,
|
|
1571
|
-
choice: nextSelected,
|
|
1572
|
-
correct: nextCorrect ?? false
|
|
1573
|
-
});
|
|
1574
|
-
}
|
|
1575
|
-
quiz.complete({
|
|
1576
|
-
checkId,
|
|
1577
|
-
score: nextScore,
|
|
1578
|
-
maxScore: nextMaxScore,
|
|
1579
|
-
passingScore: props.passingScore ?? nextMaxScore
|
|
1580
|
-
});
|
|
1581
|
-
};
|
|
1582
|
-
const handle = useMemo5(
|
|
1583
|
-
() => buildAssessmentHandle({
|
|
1584
|
-
checkId,
|
|
1585
|
-
getScore: () => resolveScores().score,
|
|
1586
|
-
getMaxScore: () => resolveScores().maxScore,
|
|
1587
|
-
getAnswerGiven: () => selected !== null,
|
|
1588
|
-
resetTask: () => {
|
|
1589
|
-
completedRef.current = false;
|
|
1590
|
-
telemetryReplayedRef.current = false;
|
|
1591
|
-
setQuizPassed(false);
|
|
1592
|
-
setSelected(null);
|
|
1593
|
-
setSelectionCorrect(null);
|
|
1594
|
-
setCompletedScore(null);
|
|
1595
|
-
setCompletedMaxScore(null);
|
|
1596
|
-
},
|
|
1597
|
-
showSolutions: () => {
|
|
1598
|
-
},
|
|
1599
|
-
getXAPIData: () => {
|
|
1600
|
-
const { score, maxScore } = resolveScores();
|
|
1601
|
-
return {
|
|
1602
|
-
checkId,
|
|
1603
|
-
interactionType: "mcq",
|
|
1604
|
-
response: selected ?? void 0,
|
|
1605
|
-
correct: selectionCorrect ?? void 0,
|
|
1606
|
-
score,
|
|
1607
|
-
maxScore
|
|
1608
|
-
};
|
|
1609
|
-
},
|
|
1610
|
-
getCurrentState: () => ({
|
|
1611
|
-
selected,
|
|
1612
|
-
selectionCorrect,
|
|
1613
|
-
quizPassed,
|
|
1614
|
-
completedScore,
|
|
1615
|
-
completedMaxScore
|
|
1616
|
-
}),
|
|
1617
|
-
resume: (state) => {
|
|
1618
|
-
const nextSelected = readStringField(state, "selected");
|
|
1619
|
-
if (typeof nextSelected === "string" || nextSelected === null) setSelected(nextSelected);
|
|
1620
|
-
const nextCorrect = readBooleanField(state, "selectionCorrect");
|
|
1621
|
-
if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
|
|
1622
|
-
setSelectionCorrect(nextCorrect);
|
|
1623
|
-
}
|
|
1624
|
-
const nextCompletedScore = readNumberField(state, "completedScore");
|
|
1625
|
-
if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
|
|
1626
|
-
const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
|
|
1627
|
-
if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
|
|
1628
|
-
const nextPassed = readBooleanField(state, "quizPassed");
|
|
1629
|
-
if (nextPassed === true || nextPassed === false) {
|
|
1630
|
-
setQuizPassed(nextPassed);
|
|
1631
|
-
completedRef.current = nextPassed;
|
|
1632
|
-
if (nextPassed) {
|
|
1633
|
-
const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
|
|
1634
|
-
const score = nextCompletedScore ?? completedScore ?? maxScore;
|
|
1635
|
-
replayTelemetry(
|
|
1636
|
-
nextSelected ?? null,
|
|
1637
|
-
nextCorrect ?? null,
|
|
1638
|
-
nextPassed,
|
|
1639
|
-
score,
|
|
1640
|
-
maxScore
|
|
1641
|
-
);
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
}),
|
|
1646
|
-
[
|
|
1647
|
-
checkId,
|
|
1648
|
-
completedMaxScore,
|
|
1649
|
-
completedScore,
|
|
1650
|
-
props.passingScore,
|
|
1651
|
-
props.question,
|
|
1652
|
-
quiz,
|
|
1653
|
-
quizPassed,
|
|
1654
|
-
selected,
|
|
1655
|
-
selectionCorrect
|
|
1656
|
-
]
|
|
1657
|
-
);
|
|
1658
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1659
|
-
return /* @__PURE__ */ jsxs2("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
1660
|
-
/* @__PURE__ */ jsx6("p", { id: questionId, children: props.question }),
|
|
1661
|
-
/* @__PURE__ */ jsxs2("fieldset", { "aria-labelledby": questionId, children: [
|
|
1662
|
-
/* @__PURE__ */ jsx6("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
|
|
1663
|
-
props.choices.map((c, i) => /* @__PURE__ */ jsxs2("label", { style: { display: "block" }, children: [
|
|
1664
|
-
/* @__PURE__ */ jsx6(
|
|
1665
|
-
"input",
|
|
1666
|
-
{
|
|
1667
|
-
type: "radio",
|
|
1668
|
-
name: questionId,
|
|
1669
|
-
value: c,
|
|
1670
|
-
checked: selected === c,
|
|
1671
|
-
disabled: passed,
|
|
1672
|
-
"aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
|
|
1673
|
-
onChange: () => {
|
|
1674
|
-
if (passed) return;
|
|
1675
|
-
setSelected(c);
|
|
1676
|
-
const custom = getPluginScore(c);
|
|
1677
|
-
const correct = isChoiceCorrect(c, props.answer, custom, props.passingScore);
|
|
1678
|
-
setSelectionCorrect(correct);
|
|
1679
|
-
quiz.answer({
|
|
1680
|
-
checkId,
|
|
1681
|
-
question: props.question,
|
|
1682
|
-
choice: c,
|
|
1683
|
-
correct
|
|
1684
|
-
});
|
|
1685
|
-
if (correct && !completedRef.current) {
|
|
1686
|
-
completedRef.current = true;
|
|
1687
|
-
setQuizPassed(true);
|
|
1688
|
-
const maxScore = custom?.maxScore ?? 1;
|
|
1689
|
-
const score = custom?.score ?? maxScore;
|
|
1690
|
-
setCompletedScore(score);
|
|
1691
|
-
setCompletedMaxScore(maxScore);
|
|
1692
|
-
quiz.complete({
|
|
1693
|
-
checkId,
|
|
1694
|
-
score,
|
|
1695
|
-
maxScore,
|
|
1696
|
-
passingScore: props.passingScore ?? maxScore
|
|
1697
|
-
});
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
),
|
|
1702
|
-
c
|
|
1703
|
-
] }, `${questionId}-${i}`))
|
|
1704
|
-
] }),
|
|
1705
|
-
selected && selectionCorrect !== null ? /* @__PURE__ */ jsx6("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
1706
|
-
] });
|
|
1707
|
-
}
|
|
1708
|
-
var QuizInnerForwarded = forwardRef(QuizInner);
|
|
1709
|
-
var Quiz = forwardRef(function Quiz2(props, ref) {
|
|
1710
|
-
return /* @__PURE__ */ jsx6(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx6(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1711
|
-
});
|
|
1712
|
-
function KnowledgeCheck(props) {
|
|
1713
|
-
return /* @__PURE__ */ jsx6(
|
|
1714
|
-
Quiz,
|
|
1715
|
-
{
|
|
1716
|
-
checkId: props.checkId,
|
|
1717
|
-
question: props.question,
|
|
1718
|
-
choices: props.choices,
|
|
1719
|
-
answer: props.answer,
|
|
1720
|
-
passingScore: props.passingScore
|
|
1721
|
-
}
|
|
1722
|
-
);
|
|
1723
|
-
}
|
|
1724
|
-
function resetQuizWarningsForTests() {
|
|
1725
|
-
resetAssessmentWarningsForTests();
|
|
1726
|
-
}
|
|
43
|
+
LessonContext,
|
|
44
|
+
LessonkitProvider,
|
|
45
|
+
assertProductionCourseConfig,
|
|
46
|
+
normalizeComponentId,
|
|
47
|
+
resetAssessmentWarningsForTests,
|
|
48
|
+
shouldEnforceProductionGuard,
|
|
49
|
+
useAssessmentState,
|
|
50
|
+
useCompletion,
|
|
51
|
+
useLessonkit,
|
|
52
|
+
useProgress,
|
|
53
|
+
useQuizState,
|
|
54
|
+
useTracking
|
|
55
|
+
} from "./chunk-TDM3ARE7.js";
|
|
1727
56
|
|
|
1728
57
|
// src/components.tsx
|
|
1729
|
-
import {
|
|
58
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
59
|
+
import { visuallyHiddenStyle } from "@lessonkit/accessibility";
|
|
60
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1730
61
|
function Course(props) {
|
|
1731
|
-
const courseId =
|
|
1732
|
-
const providerConfig =
|
|
62
|
+
const courseId = useMemo(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
|
|
63
|
+
const providerConfig = useMemo(
|
|
1733
64
|
() => ({ ...props.config, courseId }),
|
|
1734
65
|
[props.config, courseId]
|
|
1735
|
-
);
|
|
1736
|
-
return /* @__PURE__ */
|
|
1737
|
-
/* @__PURE__ */
|
|
1738
|
-
/* @__PURE__ */
|
|
1739
|
-
] }) });
|
|
1740
|
-
}
|
|
1741
|
-
function Lesson(props) {
|
|
1742
|
-
const lessonId = useMemo6(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
1743
|
-
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
1744
|
-
const { setActiveLesson, config } = useLessonkit();
|
|
1745
|
-
const { completeLesson } = useCompletion();
|
|
1746
|
-
const lessonMountGenerationRef = useRef5(0);
|
|
1747
|
-
const liveCourseIdRef = useRef5(config.courseId);
|
|
1748
|
-
liveCourseIdRef.current = config.courseId;
|
|
1749
|
-
useEffect4(() => {
|
|
1750
|
-
const unregister = registerLessonMount(lessonId);
|
|
1751
|
-
const generation = ++lessonMountGenerationRef.current;
|
|
1752
|
-
const mountedCourseId = config.courseId;
|
|
1753
|
-
let effectSurvivedTick = false;
|
|
1754
|
-
queueMicrotask(() => {
|
|
1755
|
-
queueMicrotask(() => {
|
|
1756
|
-
effectSurvivedTick = true;
|
|
1757
|
-
});
|
|
1758
|
-
});
|
|
1759
|
-
setActiveLesson(lessonId);
|
|
1760
|
-
return () => {
|
|
1761
|
-
unregister();
|
|
1762
|
-
if (getLessonMountCount(lessonId) > 0) {
|
|
1763
|
-
return;
|
|
1764
|
-
}
|
|
1765
|
-
if (!autoComplete) return;
|
|
1766
|
-
queueMicrotask(() => {
|
|
1767
|
-
if (!effectSurvivedTick) return;
|
|
1768
|
-
if (lessonMountGenerationRef.current !== generation) return;
|
|
1769
|
-
if (liveCourseIdRef.current !== mountedCourseId) return;
|
|
1770
|
-
completeLesson(lessonId, { courseId: mountedCourseId });
|
|
1771
|
-
});
|
|
1772
|
-
};
|
|
1773
|
-
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
1774
|
-
return /* @__PURE__ */ jsx7(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs3("article", { "aria-label": props.title, children: [
|
|
1775
|
-
/* @__PURE__ */ jsx7("h2", { children: props.title }),
|
|
1776
|
-
/* @__PURE__ */ jsx7("div", { children: props.children })
|
|
1777
|
-
] }) });
|
|
1778
|
-
}
|
|
1779
|
-
function Scenario(props) {
|
|
1780
|
-
const blockId = useMemo6(
|
|
1781
|
-
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1782
|
-
[props.blockId]
|
|
1783
|
-
);
|
|
1784
|
-
return /* @__PURE__ */ jsx7("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
1785
|
-
}
|
|
1786
|
-
function Reflection(props) {
|
|
1787
|
-
const blockId = useMemo6(
|
|
1788
|
-
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1789
|
-
[props.blockId]
|
|
1790
|
-
);
|
|
1791
|
-
const promptId = useId2();
|
|
1792
|
-
const hintId = useId2();
|
|
1793
|
-
const [internalValue, setInternalValue] = useState4("");
|
|
1794
|
-
const isControlled = props.value !== void 0;
|
|
1795
|
-
const value = isControlled ? props.value : internalValue;
|
|
1796
|
-
const handleChange = (event) => {
|
|
1797
|
-
if (!isControlled) setInternalValue(event.target.value);
|
|
1798
|
-
props.onChange?.(event.target.value);
|
|
1799
|
-
};
|
|
1800
|
-
return /* @__PURE__ */ jsxs3("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
|
|
1801
|
-
props.prompt ? /* @__PURE__ */ jsx7("p", { id: promptId, children: props.prompt }) : null,
|
|
1802
|
-
props.hint ? /* @__PURE__ */ jsx7("p", { id: hintId, style: visuallyHiddenStyle2, children: props.hint }) : null,
|
|
1803
|
-
props.children,
|
|
1804
|
-
/* @__PURE__ */ jsx7(
|
|
1805
|
-
"textarea",
|
|
1806
|
-
{
|
|
1807
|
-
value,
|
|
1808
|
-
onChange: handleChange,
|
|
1809
|
-
"aria-labelledby": props.prompt ? promptId : void 0,
|
|
1810
|
-
"aria-describedby": props.hint ? hintId : void 0,
|
|
1811
|
-
"aria-label": props.prompt ? void 0 : "Reflection response"
|
|
1812
|
-
}
|
|
1813
|
-
)
|
|
1814
|
-
] });
|
|
1815
|
-
}
|
|
1816
|
-
function ProgressTracker(props) {
|
|
1817
|
-
const { progress } = useLessonkit();
|
|
1818
|
-
const completed = progress.completedLessonIds.size;
|
|
1819
|
-
if (props.totalLessons != null) {
|
|
1820
|
-
const total = props.totalLessons;
|
|
1821
|
-
const displayed = Math.min(completed, total);
|
|
1822
|
-
return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx7(
|
|
1823
|
-
"div",
|
|
1824
|
-
{
|
|
1825
|
-
role: "progressbar",
|
|
1826
|
-
"aria-valuemin": 0,
|
|
1827
|
-
"aria-valuemax": total,
|
|
1828
|
-
"aria-valuenow": displayed,
|
|
1829
|
-
"aria-label": "Lessons completed",
|
|
1830
|
-
children: /* @__PURE__ */ jsxs3("p", { children: [
|
|
1831
|
-
"Lessons completed: ",
|
|
1832
|
-
displayed,
|
|
1833
|
-
" of ",
|
|
1834
|
-
total
|
|
1835
|
-
] })
|
|
1836
|
-
}
|
|
1837
|
-
) });
|
|
1838
|
-
}
|
|
1839
|
-
return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs3("p", { children: [
|
|
1840
|
-
"Lessons completed: ",
|
|
1841
|
-
completed
|
|
1842
|
-
] }) });
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
// src/blocks/TrueFalse.tsx
|
|
1846
|
-
import React9, { forwardRef as forwardRef2, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef6, useState as useState5 } from "react";
|
|
1847
|
-
import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1848
|
-
var INTERACTION = "trueFalse";
|
|
1849
|
-
function TrueFalseInner(props, ref) {
|
|
1850
|
-
const { enclosingLessonId } = props;
|
|
1851
|
-
const checkId = useMemo7(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1852
|
-
const assessment = useAssessmentState(enclosingLessonId);
|
|
1853
|
-
const { config } = useLessonkit();
|
|
1854
|
-
const { scoreResponse } = usePluginScoring(checkId, enclosingLessonId);
|
|
1855
|
-
const [selected, setSelected] = useState5(null);
|
|
1856
|
-
const [selectionCorrect, setSelectionCorrect] = useState5(null);
|
|
1857
|
-
const [showSolutions, setShowSolutions] = useState5(false);
|
|
1858
|
-
const [passed, setPassed] = useState5(false);
|
|
1859
|
-
const [completedScore, setCompletedScore] = useState5(null);
|
|
1860
|
-
const [completedMaxScore, setCompletedMaxScore] = useState5(null);
|
|
1861
|
-
const completedRef = useRef6(false);
|
|
1862
|
-
const telemetryReplayedRef = useRef6(false);
|
|
1863
|
-
const questionId = React9.useId();
|
|
1864
|
-
const reset = () => {
|
|
1865
|
-
completedRef.current = false;
|
|
1866
|
-
telemetryReplayedRef.current = false;
|
|
1867
|
-
setPassed(false);
|
|
1868
|
-
setSelected(null);
|
|
1869
|
-
setSelectionCorrect(null);
|
|
1870
|
-
setShowSolutions(false);
|
|
1871
|
-
setCompletedScore(null);
|
|
1872
|
-
setCompletedMaxScore(null);
|
|
1873
|
-
};
|
|
1874
|
-
useEffect5(() => {
|
|
1875
|
-
reset();
|
|
1876
|
-
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
|
|
1877
|
-
const resolveScores = () => {
|
|
1878
|
-
const maxScore = completedMaxScore ?? 1;
|
|
1879
|
-
if (passed) {
|
|
1880
|
-
return { score: completedScore ?? maxScore, maxScore };
|
|
1881
|
-
}
|
|
1882
|
-
if (selectionCorrect) {
|
|
1883
|
-
return { score: completedMaxScore ?? maxScore, maxScore };
|
|
1884
|
-
}
|
|
1885
|
-
return { score: 0, maxScore };
|
|
1886
|
-
};
|
|
1887
|
-
const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
|
|
1888
|
-
if (!nextPassed || telemetryReplayedRef.current) return;
|
|
1889
|
-
telemetryReplayedRef.current = true;
|
|
1890
|
-
if (nextSelected !== null) {
|
|
1891
|
-
assessment.answer({
|
|
1892
|
-
checkId,
|
|
1893
|
-
interactionType: INTERACTION,
|
|
1894
|
-
question: props.question,
|
|
1895
|
-
response: nextSelected,
|
|
1896
|
-
correct: nextCorrect ?? false
|
|
1897
|
-
});
|
|
1898
|
-
}
|
|
1899
|
-
assessment.complete({
|
|
1900
|
-
checkId,
|
|
1901
|
-
interactionType: INTERACTION,
|
|
1902
|
-
score: nextScore,
|
|
1903
|
-
maxScore: nextMaxScore,
|
|
1904
|
-
passingScore: props.passingScore ?? nextMaxScore
|
|
1905
|
-
});
|
|
1906
|
-
};
|
|
1907
|
-
const handle = useMemo7(
|
|
1908
|
-
() => buildAssessmentHandle({
|
|
1909
|
-
checkId,
|
|
1910
|
-
getScore: () => resolveScores().score,
|
|
1911
|
-
getMaxScore: () => resolveScores().maxScore,
|
|
1912
|
-
getAnswerGiven: () => selected !== null,
|
|
1913
|
-
resetTask: reset,
|
|
1914
|
-
showSolutions: () => setShowSolutions(true),
|
|
1915
|
-
getXAPIData: () => {
|
|
1916
|
-
const { score, maxScore } = resolveScores();
|
|
1917
|
-
return {
|
|
1918
|
-
checkId,
|
|
1919
|
-
interactionType: INTERACTION,
|
|
1920
|
-
response: selected ?? void 0,
|
|
1921
|
-
correct: selectionCorrect ?? void 0,
|
|
1922
|
-
score,
|
|
1923
|
-
maxScore
|
|
1924
|
-
};
|
|
1925
|
-
},
|
|
1926
|
-
getCurrentState: () => ({
|
|
1927
|
-
selected,
|
|
1928
|
-
selectionCorrect,
|
|
1929
|
-
passed,
|
|
1930
|
-
showSolutions,
|
|
1931
|
-
completedScore,
|
|
1932
|
-
completedMaxScore
|
|
1933
|
-
}),
|
|
1934
|
-
resume: (state) => {
|
|
1935
|
-
const nextSelected = readBooleanField(state, "selected");
|
|
1936
|
-
if (nextSelected === true || nextSelected === false || nextSelected === null) {
|
|
1937
|
-
setSelected(nextSelected);
|
|
1938
|
-
}
|
|
1939
|
-
const nextCorrect = readBooleanField(state, "selectionCorrect");
|
|
1940
|
-
if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
|
|
1941
|
-
setSelectionCorrect(nextCorrect);
|
|
1942
|
-
}
|
|
1943
|
-
const nextCompletedScore = readNumberField(state, "completedScore");
|
|
1944
|
-
if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
|
|
1945
|
-
const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
|
|
1946
|
-
if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
|
|
1947
|
-
const nextPassed = readBooleanField(state, "passed");
|
|
1948
|
-
if (nextPassed === true || nextPassed === false) {
|
|
1949
|
-
setPassed(nextPassed);
|
|
1950
|
-
completedRef.current = nextPassed;
|
|
1951
|
-
if (nextPassed) {
|
|
1952
|
-
const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
|
|
1953
|
-
const score = nextCompletedScore ?? completedScore ?? maxScore;
|
|
1954
|
-
replayTelemetry(nextSelected ?? null, nextCorrect ?? null, nextPassed, score, maxScore);
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
1958
|
-
}
|
|
1959
|
-
}),
|
|
1960
|
-
[
|
|
1961
|
-
assessment,
|
|
1962
|
-
checkId,
|
|
1963
|
-
completedMaxScore,
|
|
1964
|
-
completedScore,
|
|
1965
|
-
passed,
|
|
1966
|
-
props.passingScore,
|
|
1967
|
-
props.question,
|
|
1968
|
-
selected,
|
|
1969
|
-
selectionCorrect,
|
|
1970
|
-
showSolutions
|
|
1971
|
-
]
|
|
1972
|
-
);
|
|
1973
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1974
|
-
const submit = (value) => {
|
|
1975
|
-
if (passed && !props.enableRetry) return;
|
|
1976
|
-
setSelected(value);
|
|
1977
|
-
const correct = value === props.answer;
|
|
1978
|
-
const scored = scoreResponse(value, correct, 1, props.passingScore);
|
|
1979
|
-
setSelectionCorrect(scored.passed);
|
|
1980
|
-
assessment.answer({
|
|
1981
|
-
checkId,
|
|
1982
|
-
interactionType: INTERACTION,
|
|
1983
|
-
question: props.question,
|
|
1984
|
-
response: value,
|
|
1985
|
-
correct: scored.passed
|
|
1986
|
-
});
|
|
1987
|
-
if (scored.passed && !completedRef.current) {
|
|
1988
|
-
completedRef.current = true;
|
|
1989
|
-
setPassed(true);
|
|
1990
|
-
setCompletedScore(scored.score);
|
|
1991
|
-
setCompletedMaxScore(scored.maxScore);
|
|
1992
|
-
assessment.complete({
|
|
1993
|
-
checkId,
|
|
1994
|
-
interactionType: INTERACTION,
|
|
1995
|
-
score: scored.score,
|
|
1996
|
-
maxScore: scored.maxScore,
|
|
1997
|
-
passingScore: props.passingScore ?? scored.maxScore
|
|
1998
|
-
});
|
|
1999
|
-
}
|
|
2000
|
-
};
|
|
2001
|
-
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
2002
|
-
return /* @__PURE__ */ jsxs4("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
|
|
2003
|
-
/* @__PURE__ */ jsx8("p", { id: questionId, children: props.question }),
|
|
2004
|
-
/* @__PURE__ */ jsxs4("fieldset", { "aria-labelledby": questionId, children: [
|
|
2005
|
-
/* @__PURE__ */ jsx8("legend", { className: "lk-visually-hidden", children: "True or False" }),
|
|
2006
|
-
/* @__PURE__ */ jsxs4("label", { style: { display: "block", marginRight: "1rem" }, children: [
|
|
2007
|
-
/* @__PURE__ */ jsx8(
|
|
2008
|
-
"input",
|
|
2009
|
-
{
|
|
2010
|
-
type: "radio",
|
|
2011
|
-
name: `${questionId}-tf`,
|
|
2012
|
-
checked: selected === true,
|
|
2013
|
-
disabled: passed && !props.enableRetry,
|
|
2014
|
-
onChange: () => submit(true)
|
|
2015
|
-
}
|
|
2016
|
-
),
|
|
2017
|
-
"True"
|
|
2018
|
-
] }),
|
|
2019
|
-
/* @__PURE__ */ jsxs4("label", { style: { display: "block" }, children: [
|
|
2020
|
-
/* @__PURE__ */ jsx8(
|
|
2021
|
-
"input",
|
|
2022
|
-
{
|
|
2023
|
-
type: "radio",
|
|
2024
|
-
name: `${questionId}-tf`,
|
|
2025
|
-
checked: selected === false,
|
|
2026
|
-
disabled: passed && !props.enableRetry,
|
|
2027
|
-
onChange: () => submit(false)
|
|
2028
|
-
}
|
|
2029
|
-
),
|
|
2030
|
-
"False"
|
|
2031
|
-
] })
|
|
2032
|
-
] }),
|
|
2033
|
-
reveal ? /* @__PURE__ */ jsxs4("p", { children: [
|
|
2034
|
-
"Correct answer: ",
|
|
2035
|
-
/* @__PURE__ */ jsx8("strong", { children: props.answer ? "True" : "False" })
|
|
2036
|
-
] }) : null,
|
|
2037
|
-
selected !== null && selectionCorrect !== null ? /* @__PURE__ */ jsx8("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
|
|
2038
|
-
props.enableRetry && passed ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2039
|
-
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
2040
|
-
] });
|
|
2041
|
-
}
|
|
2042
|
-
var TrueFalseInnerForwarded = forwardRef2(TrueFalseInner);
|
|
2043
|
-
var TrueFalse = forwardRef2(function TrueFalse2(props, ref) {
|
|
2044
|
-
return /* @__PURE__ */ jsx8(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx8(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2045
|
-
});
|
|
2046
|
-
|
|
2047
|
-
// src/blocks/MarkTheWords.tsx
|
|
2048
|
-
import React10, { forwardRef as forwardRef3, useEffect as useEffect6, useMemo as useMemo8, useRef as useRef7, useState as useState6 } from "react";
|
|
2049
|
-
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2050
|
-
var INTERACTION2 = "markTheWords";
|
|
2051
|
-
function tokenize(text) {
|
|
2052
|
-
return text.split(/(\s+)/).filter((t) => t.length > 0);
|
|
2053
|
-
}
|
|
2054
|
-
function MarkTheWordsInner(props, ref) {
|
|
2055
|
-
const checkId = useMemo8(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
2056
|
-
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2057
|
-
const tokens = useMemo8(() => tokenize(props.text), [props.text]);
|
|
2058
|
-
const correctSet = useMemo8(
|
|
2059
|
-
() => new Set(props.correctWords.map((w) => w.toLowerCase())),
|
|
2060
|
-
[props.correctWords]
|
|
2061
|
-
);
|
|
2062
|
-
const [marked, setMarked] = useState6(() => /* @__PURE__ */ new Set());
|
|
2063
|
-
const [passed, setPassed] = useState6(false);
|
|
2064
|
-
const [showSolutions, setShowSolutions] = useState6(false);
|
|
2065
|
-
const completedRef = useRef7(false);
|
|
2066
|
-
const reset = () => {
|
|
2067
|
-
completedRef.current = false;
|
|
2068
|
-
setPassed(false);
|
|
2069
|
-
setMarked(/* @__PURE__ */ new Set());
|
|
2070
|
-
setShowSolutions(false);
|
|
2071
|
-
};
|
|
2072
|
-
useEffect6(() => {
|
|
2073
|
-
reset();
|
|
2074
|
-
}, [checkId, props.text, props.correctWords.join("\0")]);
|
|
2075
|
-
const selectableIndices = useMemo8(() => {
|
|
2076
|
-
const indices = [];
|
|
2077
|
-
tokens.forEach((t, i) => {
|
|
2078
|
-
if (!/^\s+$/.test(t) && correctSet.has(t.toLowerCase())) indices.push(i);
|
|
2079
|
-
});
|
|
2080
|
-
return indices;
|
|
2081
|
-
}, [tokens, correctSet]);
|
|
2082
|
-
const hasTargets = selectableIndices.length > 0;
|
|
2083
|
-
const allMarked = hasTargets && selectableIndices.every((i) => marked.has(i));
|
|
2084
|
-
const maxScore = selectableIndices.length;
|
|
2085
|
-
const score = allMarked ? maxScore : marked.size;
|
|
2086
|
-
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
2087
|
-
const handle = useMemo8(
|
|
2088
|
-
() => buildAssessmentHandle({
|
|
2089
|
-
checkId,
|
|
2090
|
-
getScore: () => score,
|
|
2091
|
-
getMaxScore: () => maxScore || 1,
|
|
2092
|
-
getAnswerGiven: () => marked.size > 0,
|
|
2093
|
-
resetTask: reset,
|
|
2094
|
-
showSolutions: () => setShowSolutions(true),
|
|
2095
|
-
getXAPIData: () => ({
|
|
2096
|
-
checkId,
|
|
2097
|
-
interactionType: INTERACTION2,
|
|
2098
|
-
response: [...marked].map((i) => tokens[i]),
|
|
2099
|
-
correct: passedThreshold,
|
|
2100
|
-
score,
|
|
2101
|
-
maxScore: maxScore || 1
|
|
2102
|
-
}),
|
|
2103
|
-
getCurrentState: () => ({ marked: [...marked], passed, showSolutions }),
|
|
2104
|
-
resume: (state) => {
|
|
2105
|
-
const raw = state.marked;
|
|
2106
|
-
if (Array.isArray(raw)) setMarked(new Set(raw.filter((i) => typeof i === "number")));
|
|
2107
|
-
readBooleanStateField(state, "passed", (value) => {
|
|
2108
|
-
setPassed(value);
|
|
2109
|
-
completedRef.current = value;
|
|
2110
|
-
});
|
|
2111
|
-
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
2112
|
-
}
|
|
2113
|
-
}),
|
|
2114
|
-
[checkId, marked, maxScore, passed, passedThreshold, score, showSolutions, tokens]
|
|
2115
|
-
);
|
|
2116
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2117
|
-
const toggle = (index) => {
|
|
2118
|
-
if (passed && !props.enableRetry) return;
|
|
2119
|
-
setMarked((prev) => {
|
|
2120
|
-
const next = new Set(prev);
|
|
2121
|
-
if (next.has(index)) next.delete(index);
|
|
2122
|
-
else next.add(index);
|
|
2123
|
-
return next;
|
|
2124
|
-
});
|
|
2125
|
-
};
|
|
2126
|
-
useEffect6(() => {
|
|
2127
|
-
if (!hasTargets) {
|
|
2128
|
-
if (isDevEnvironment4()) {
|
|
2129
|
-
console.warn(
|
|
2130
|
-
"[lessonkit] MarkTheWords: no tokens match correctWords",
|
|
2131
|
-
props.correctWords
|
|
2132
|
-
);
|
|
2133
|
-
}
|
|
2134
|
-
return;
|
|
2135
|
-
}
|
|
2136
|
-
if (!passedThreshold || completedRef.current) return;
|
|
2137
|
-
completedRef.current = true;
|
|
2138
|
-
setPassed(true);
|
|
2139
|
-
assessment.answer({
|
|
2140
|
-
checkId,
|
|
2141
|
-
interactionType: INTERACTION2,
|
|
2142
|
-
question: props.text,
|
|
2143
|
-
response: [...marked].map((i) => tokens[i]),
|
|
2144
|
-
correct: passedThreshold
|
|
2145
|
-
});
|
|
2146
|
-
assessment.complete({
|
|
2147
|
-
checkId,
|
|
2148
|
-
interactionType: INTERACTION2,
|
|
2149
|
-
score,
|
|
2150
|
-
maxScore,
|
|
2151
|
-
passingScore: props.passingScore ?? maxScore
|
|
2152
|
-
});
|
|
2153
|
-
}, [
|
|
2154
|
-
assessment,
|
|
2155
|
-
checkId,
|
|
2156
|
-
hasTargets,
|
|
2157
|
-
marked,
|
|
2158
|
-
maxScore,
|
|
2159
|
-
passedThreshold,
|
|
2160
|
-
props.passingScore,
|
|
2161
|
-
props.correctWords,
|
|
2162
|
-
props.text,
|
|
2163
|
-
score,
|
|
2164
|
-
tokens
|
|
2165
|
-
]);
|
|
2166
|
-
return /* @__PURE__ */ jsxs5("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
|
|
2167
|
-
!hasTargets ? /* @__PURE__ */ jsxs5("p", { role: "alert", children: [
|
|
2168
|
-
"No words in this sentence match ",
|
|
2169
|
-
/* @__PURE__ */ jsx9("code", { children: "correctWords" }),
|
|
2170
|
-
". Check spelling and capitalization in the source text."
|
|
2171
|
-
] }) : null,
|
|
2172
|
-
/* @__PURE__ */ jsx9("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
|
|
2173
|
-
/* @__PURE__ */ jsx9("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
|
|
2174
|
-
const isWord = !/^\s+$/.test(token);
|
|
2175
|
-
const isTarget = isWord && correctSet.has(token.toLowerCase());
|
|
2176
|
-
if (!isTarget) return /* @__PURE__ */ jsx9(React10.Fragment, { children: token }, i);
|
|
2177
|
-
const selected = marked.has(i);
|
|
2178
|
-
const solution = showSolutions || passed && props.enableSolutionsButton;
|
|
2179
|
-
return /* @__PURE__ */ jsx9(
|
|
2180
|
-
"button",
|
|
2181
|
-
{
|
|
2182
|
-
type: "button",
|
|
2183
|
-
"data-testid": `mark-word-${i}`,
|
|
2184
|
-
"aria-pressed": selected,
|
|
2185
|
-
disabled: passed && !props.enableRetry,
|
|
2186
|
-
onClick: () => toggle(i),
|
|
2187
|
-
style: {
|
|
2188
|
-
margin: "0 0.1em",
|
|
2189
|
-
textDecoration: solution ? "underline" : void 0,
|
|
2190
|
-
fontWeight: selected || solution ? "bold" : void 0
|
|
2191
|
-
},
|
|
2192
|
-
children: token
|
|
2193
|
-
},
|
|
2194
|
-
i
|
|
2195
|
-
);
|
|
2196
|
-
}) }),
|
|
2197
|
-
allMarked ? /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
|
|
2198
|
-
props.enableRetry && passed ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2199
|
-
props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
2200
|
-
] });
|
|
2201
|
-
}
|
|
2202
|
-
var MarkTheWordsInnerForwarded = forwardRef3(MarkTheWordsInner);
|
|
2203
|
-
var MarkTheWords = forwardRef3(function MarkTheWords2(props, ref) {
|
|
2204
|
-
return /* @__PURE__ */ jsx9(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx9(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2205
|
-
});
|
|
2206
|
-
|
|
2207
|
-
// src/blocks/FillInTheBlanks.tsx
|
|
2208
|
-
import React11, { forwardRef as forwardRef4, useEffect as useEffect7, useMemo as useMemo9, useRef as useRef8, useState as useState7 } from "react";
|
|
2209
|
-
|
|
2210
|
-
// src/assessment/internal/parseStarDelimitedTemplate.ts
|
|
2211
|
-
function parseStarDelimitedTemplate(template, idPrefix) {
|
|
2212
|
-
const parts = [];
|
|
2213
|
-
const values = [];
|
|
2214
|
-
const re = /\*([^*]+)\*/g;
|
|
2215
|
-
let last = 0;
|
|
2216
|
-
let match;
|
|
2217
|
-
let n = 0;
|
|
2218
|
-
while ((match = re.exec(template)) !== null) {
|
|
2219
|
-
parts.push(template.slice(last, match.index));
|
|
2220
|
-
values.push(match[1].trim());
|
|
2221
|
-
parts.push(`${idPrefix}-${n++}`);
|
|
2222
|
-
last = match.index + match[0].length;
|
|
2223
|
-
}
|
|
2224
|
-
parts.push(template.slice(last));
|
|
2225
|
-
return { parts, values };
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
// src/blocks/FillInTheBlanks.tsx
|
|
2229
|
-
import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2230
|
-
var INTERACTION3 = "fillInBlanks";
|
|
2231
|
-
function parseTemplate(template) {
|
|
2232
|
-
const { parts, values } = parseStarDelimitedTemplate(template, "blank");
|
|
2233
|
-
return {
|
|
2234
|
-
parts,
|
|
2235
|
-
blanks: values.map((answer, i) => ({ id: `blank-${i}`, answer }))
|
|
2236
|
-
};
|
|
2237
|
-
}
|
|
2238
|
-
function FillInTheBlanksInner(props, ref) {
|
|
2239
|
-
const checkId = useMemo9(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
2240
|
-
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2241
|
-
const parsed = useMemo9(() => parseTemplate(props.template), [props.template]);
|
|
2242
|
-
const blanks = props.blanks ?? parsed.blanks;
|
|
2243
|
-
const [values, setValues] = useState7(
|
|
2244
|
-
() => Object.fromEntries(blanks.map((b) => [b.id, ""]))
|
|
2245
|
-
);
|
|
2246
|
-
const [passed, setPassed] = useState7(false);
|
|
2247
|
-
const [showSolutions, setShowSolutions] = useState7(false);
|
|
2248
|
-
const [submitted, setSubmitted] = useState7(false);
|
|
2249
|
-
const completedRef = useRef8(false);
|
|
2250
|
-
const answeredRef = useRef8(false);
|
|
2251
|
-
const checkSnapshotRef = useRef8(null);
|
|
2252
|
-
const telemetryReplayedRef = useRef8(false);
|
|
2253
|
-
const reset = () => {
|
|
2254
|
-
completedRef.current = false;
|
|
2255
|
-
answeredRef.current = false;
|
|
2256
|
-
checkSnapshotRef.current = null;
|
|
2257
|
-
telemetryReplayedRef.current = false;
|
|
2258
|
-
setPassed(false);
|
|
2259
|
-
setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
|
|
2260
|
-
setShowSolutions(false);
|
|
2261
|
-
setSubmitted(false);
|
|
2262
|
-
};
|
|
2263
|
-
useEffect7(() => {
|
|
2264
|
-
reset();
|
|
2265
|
-
}, [checkId, props.template, blanks.map((b) => b.answer).join("\0")]);
|
|
2266
|
-
const hasBlanks = blanks.length > 0;
|
|
2267
|
-
const allFilled = hasBlanks && blanks.every((b) => (values[b.id] ?? "").trim().length > 0);
|
|
2268
|
-
let score = 0;
|
|
2269
|
-
blanks.forEach((b) => {
|
|
2270
|
-
if ((values[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) score += 1;
|
|
2271
|
-
});
|
|
2272
|
-
const maxScore = blanks.length;
|
|
2273
|
-
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
2274
|
-
const replayTelemetry = (nextValues, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
|
|
2275
|
-
if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
|
|
2276
|
-
telemetryReplayedRef.current = true;
|
|
2277
|
-
const nextPassedThreshold = meetsPassingThreshold(
|
|
2278
|
-
nextScore,
|
|
2279
|
-
nextMaxScore || 1,
|
|
2280
|
-
props.passingScore
|
|
2281
|
-
);
|
|
2282
|
-
assessment.answer({
|
|
2283
|
-
checkId,
|
|
2284
|
-
interactionType: INTERACTION3,
|
|
2285
|
-
question: props.template,
|
|
2286
|
-
response: nextValues,
|
|
2287
|
-
correct: nextPassedThreshold
|
|
2288
|
-
});
|
|
2289
|
-
if (nextPassed || nextPassedThreshold) {
|
|
2290
|
-
assessment.complete({
|
|
2291
|
-
checkId,
|
|
2292
|
-
interactionType: INTERACTION3,
|
|
2293
|
-
score: nextScore,
|
|
2294
|
-
maxScore: nextMaxScore,
|
|
2295
|
-
passingScore: props.passingScore ?? nextMaxScore
|
|
2296
|
-
});
|
|
2297
|
-
}
|
|
2298
|
-
};
|
|
2299
|
-
const handle = useMemo9(
|
|
2300
|
-
() => buildAssessmentHandle({
|
|
2301
|
-
checkId,
|
|
2302
|
-
getScore: () => score,
|
|
2303
|
-
getMaxScore: () => maxScore || 1,
|
|
2304
|
-
getAnswerGiven: () => allFilled,
|
|
2305
|
-
resetTask: reset,
|
|
2306
|
-
showSolutions: () => setShowSolutions(true),
|
|
2307
|
-
getXAPIData: () => ({
|
|
2308
|
-
checkId,
|
|
2309
|
-
interactionType: INTERACTION3,
|
|
2310
|
-
response: values,
|
|
2311
|
-
correct: passedThreshold,
|
|
2312
|
-
score,
|
|
2313
|
-
maxScore: maxScore || 1
|
|
2314
|
-
}),
|
|
2315
|
-
getCurrentState: () => ({ values, passed, showSolutions, submitted }),
|
|
2316
|
-
resume: (state) => {
|
|
2317
|
-
const raw = state.values;
|
|
2318
|
-
let nextValues = values;
|
|
2319
|
-
if (raw && typeof raw === "object") {
|
|
2320
|
-
nextValues = { ...raw };
|
|
2321
|
-
setValues(nextValues);
|
|
2322
|
-
}
|
|
2323
|
-
let nextPassed = passed;
|
|
2324
|
-
let nextSubmitted = submitted;
|
|
2325
|
-
readBooleanStateField(state, "passed", (value) => {
|
|
2326
|
-
nextPassed = value;
|
|
2327
|
-
setPassed(value);
|
|
2328
|
-
completedRef.current = value;
|
|
2329
|
-
answeredRef.current = value;
|
|
2330
|
-
});
|
|
2331
|
-
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
2332
|
-
readBooleanStateField(state, "submitted", (value) => {
|
|
2333
|
-
nextSubmitted = value;
|
|
2334
|
-
setSubmitted(value);
|
|
2335
|
-
if (value) answeredRef.current = true;
|
|
2336
|
-
});
|
|
2337
|
-
let nextScore = 0;
|
|
2338
|
-
blanks.forEach((b) => {
|
|
2339
|
-
if ((nextValues[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) nextScore += 1;
|
|
2340
|
-
});
|
|
2341
|
-
replayTelemetry(nextValues, nextPassed, nextSubmitted, nextScore, blanks.length);
|
|
2342
|
-
}
|
|
2343
|
-
}),
|
|
2344
|
-
[allFilled, assessment, blanks, checkId, maxScore, passed, passedThreshold, props.passingScore, props.template, score, showSolutions, submitted, values]
|
|
2345
|
-
);
|
|
2346
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2347
|
-
const check = () => {
|
|
2348
|
-
if (!hasBlanks) {
|
|
2349
|
-
if (isDevEnvironment4()) {
|
|
2350
|
-
console.warn("[lessonkit] FillInTheBlanks has no blanks in template");
|
|
2351
|
-
}
|
|
2352
|
-
return;
|
|
2353
|
-
}
|
|
2354
|
-
if (!allFilled) return;
|
|
2355
|
-
if (passed) return;
|
|
2356
|
-
const snapshot = JSON.stringify(values);
|
|
2357
|
-
if (checkSnapshotRef.current === snapshot) return;
|
|
2358
|
-
checkSnapshotRef.current = snapshot;
|
|
2359
|
-
answeredRef.current = true;
|
|
2360
|
-
setSubmitted(true);
|
|
2361
|
-
assessment.answer({
|
|
2362
|
-
checkId,
|
|
2363
|
-
interactionType: INTERACTION3,
|
|
2364
|
-
question: props.template,
|
|
2365
|
-
response: values,
|
|
2366
|
-
correct: passedThreshold
|
|
2367
|
-
});
|
|
2368
|
-
if (passedThreshold && !completedRef.current) {
|
|
2369
|
-
completedRef.current = true;
|
|
2370
|
-
setPassed(true);
|
|
2371
|
-
assessment.complete({
|
|
2372
|
-
checkId,
|
|
2373
|
-
interactionType: INTERACTION3,
|
|
2374
|
-
score,
|
|
2375
|
-
maxScore,
|
|
2376
|
-
passingScore: props.passingScore ?? maxScore
|
|
2377
|
-
});
|
|
2378
|
-
}
|
|
2379
|
-
};
|
|
2380
|
-
useEffect7(() => {
|
|
2381
|
-
if (!allFilled) {
|
|
2382
|
-
answeredRef.current = false;
|
|
2383
|
-
checkSnapshotRef.current = null;
|
|
2384
|
-
setSubmitted(false);
|
|
2385
|
-
}
|
|
2386
|
-
}, [allFilled]);
|
|
2387
|
-
useEffect7(() => {
|
|
2388
|
-
if (props.autoCheck && allFilled && !passed) check();
|
|
2389
|
-
}, [allFilled, props.autoCheck, values, passedThreshold, passed]);
|
|
2390
|
-
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
2391
|
-
return /* @__PURE__ */ jsxs6("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
|
|
2392
|
-
/* @__PURE__ */ jsx10("p", { children: parsed.parts.map((part, i) => {
|
|
2393
|
-
const blank = blanks.find((b) => b.id === part);
|
|
2394
|
-
if (!blank) return /* @__PURE__ */ jsx10(React11.Fragment, { children: part }, i);
|
|
2395
|
-
return /* @__PURE__ */ jsxs6("label", { style: { margin: "0 0.25em" }, children: [
|
|
2396
|
-
/* @__PURE__ */ jsx10("span", { className: "lk-visually-hidden", children: blank.answer }),
|
|
2397
|
-
/* @__PURE__ */ jsx10(
|
|
2398
|
-
"input",
|
|
2399
|
-
{
|
|
2400
|
-
type: "text",
|
|
2401
|
-
"data-testid": `blank-${blank.id}`,
|
|
2402
|
-
"aria-label": `Blank ${blank.id}`,
|
|
2403
|
-
value: reveal ? blank.answer : values[blank.id] ?? "",
|
|
2404
|
-
readOnly: reveal,
|
|
2405
|
-
disabled: passed && !props.enableRetry,
|
|
2406
|
-
onChange: (e) => setValues((v) => ({ ...v, [blank.id]: e.target.value })),
|
|
2407
|
-
onBlur: () => props.autoCheck && check(),
|
|
2408
|
-
size: Math.max(8, blank.answer.length + 2)
|
|
2409
|
-
}
|
|
2410
|
-
)
|
|
2411
|
-
] }, blank.id);
|
|
2412
|
-
}) }),
|
|
2413
|
-
!props.autoCheck ? /* @__PURE__ */ jsx10("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
|
|
2414
|
-
!hasBlanks ? /* @__PURE__ */ jsx10("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
|
|
2415
|
-
submitted ? /* @__PURE__ */ jsx10("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
|
|
2416
|
-
props.enableRetry && passed ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2417
|
-
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
2418
|
-
] });
|
|
2419
|
-
}
|
|
2420
|
-
var FillInTheBlanksInnerForwarded = forwardRef4(FillInTheBlanksInner);
|
|
2421
|
-
var FillInTheBlanks = forwardRef4(
|
|
2422
|
-
function FillInTheBlanks2(props, ref) {
|
|
2423
|
-
return /* @__PURE__ */ jsx10(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx10(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2424
|
-
}
|
|
2425
|
-
);
|
|
2426
|
-
|
|
2427
|
-
// src/blocks/DragTheWords.tsx
|
|
2428
|
-
import React12, { forwardRef as forwardRef5, useEffect as useEffect8, useMemo as useMemo10, useRef as useRef9, useState as useState8 } from "react";
|
|
2429
|
-
import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2430
|
-
var INTERACTION4 = "dragTheWords";
|
|
2431
|
-
function parseZones(template) {
|
|
2432
|
-
const { parts, values } = parseStarDelimitedTemplate(template, "zone");
|
|
2433
|
-
return { parts, answers: values };
|
|
2434
|
-
}
|
|
2435
|
-
function DragTheWordsInner(props, ref) {
|
|
2436
|
-
const checkId = useMemo10(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
2437
|
-
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2438
|
-
const { parts, answers } = useMemo10(() => parseZones(props.template), [props.template]);
|
|
2439
|
-
const [zones, setZones] = useState8(
|
|
2440
|
-
() => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
|
|
2441
|
-
);
|
|
2442
|
-
const [pool, setPool] = useState8(() => [...props.words]);
|
|
2443
|
-
const [keyboardWord, setKeyboardWord] = useState8(null);
|
|
2444
|
-
const [passed, setPassed] = useState8(false);
|
|
2445
|
-
const [submitted, setSubmitted] = useState8(false);
|
|
2446
|
-
const completedRef = useRef9(false);
|
|
2447
|
-
const answeredRef = useRef9(false);
|
|
2448
|
-
const checkSnapshotRef = useRef9(null);
|
|
2449
|
-
const telemetryReplayedRef = useRef9(false);
|
|
2450
|
-
const reset = () => {
|
|
2451
|
-
completedRef.current = false;
|
|
2452
|
-
answeredRef.current = false;
|
|
2453
|
-
checkSnapshotRef.current = null;
|
|
2454
|
-
telemetryReplayedRef.current = false;
|
|
2455
|
-
setPassed(false);
|
|
2456
|
-
setSubmitted(false);
|
|
2457
|
-
setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
|
|
2458
|
-
setPool([...props.words]);
|
|
2459
|
-
setKeyboardWord(null);
|
|
2460
|
-
};
|
|
2461
|
-
useEffect8(() => {
|
|
2462
|
-
reset();
|
|
2463
|
-
}, [checkId, props.template, props.words.join("\0")]);
|
|
2464
|
-
const hasZones = answers.length > 0;
|
|
2465
|
-
const allFilled = hasZones && answers.every((_, i) => (zones[`zone-${i}`] ?? "").length > 0);
|
|
2466
|
-
let score = 0;
|
|
2467
|
-
answers.forEach((ans, i) => {
|
|
2468
|
-
if ((zones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) score += 1;
|
|
2469
|
-
});
|
|
2470
|
-
const maxScore = answers.length;
|
|
2471
|
-
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
2472
|
-
const replayTelemetry = (nextZones, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
|
|
2473
|
-
if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
|
|
2474
|
-
telemetryReplayedRef.current = true;
|
|
2475
|
-
const nextPassedThreshold = meetsPassingThreshold(
|
|
2476
|
-
nextScore,
|
|
2477
|
-
nextMaxScore || 1,
|
|
2478
|
-
props.passingScore
|
|
2479
|
-
);
|
|
2480
|
-
assessment.answer({
|
|
2481
|
-
checkId,
|
|
2482
|
-
interactionType: INTERACTION4,
|
|
2483
|
-
question: props.template,
|
|
2484
|
-
response: nextZones,
|
|
2485
|
-
correct: nextPassedThreshold
|
|
2486
|
-
});
|
|
2487
|
-
if (nextPassed || nextPassedThreshold) {
|
|
2488
|
-
assessment.complete({
|
|
2489
|
-
checkId,
|
|
2490
|
-
interactionType: INTERACTION4,
|
|
2491
|
-
score: nextScore,
|
|
2492
|
-
maxScore: nextMaxScore,
|
|
2493
|
-
passingScore: props.passingScore ?? nextMaxScore
|
|
2494
|
-
});
|
|
2495
|
-
}
|
|
2496
|
-
};
|
|
2497
|
-
const handle = useMemo10(
|
|
2498
|
-
() => buildAssessmentHandle({
|
|
2499
|
-
checkId,
|
|
2500
|
-
getScore: () => score,
|
|
2501
|
-
getMaxScore: () => maxScore || 1,
|
|
2502
|
-
getAnswerGiven: () => allFilled,
|
|
2503
|
-
resetTask: reset,
|
|
2504
|
-
showSolutions: () => {
|
|
2505
|
-
},
|
|
2506
|
-
getXAPIData: () => ({
|
|
2507
|
-
checkId,
|
|
2508
|
-
interactionType: INTERACTION4,
|
|
2509
|
-
response: zones,
|
|
2510
|
-
correct: passedThreshold,
|
|
2511
|
-
score,
|
|
2512
|
-
maxScore: maxScore || 1
|
|
2513
|
-
}),
|
|
2514
|
-
getCurrentState: () => ({ zones, pool, passed, keyboardWord, submitted }),
|
|
2515
|
-
resume: (state) => {
|
|
2516
|
-
const rawZones = state.zones;
|
|
2517
|
-
let nextZones = zones;
|
|
2518
|
-
if (rawZones && typeof rawZones === "object") {
|
|
2519
|
-
nextZones = { ...rawZones };
|
|
2520
|
-
setZones(nextZones);
|
|
2521
|
-
}
|
|
2522
|
-
if (Array.isArray(state.pool)) setPool([...state.pool]);
|
|
2523
|
-
let nextPassed = passed;
|
|
2524
|
-
let nextSubmitted = submitted;
|
|
2525
|
-
readBooleanStateField(state, "passed", (value) => {
|
|
2526
|
-
nextPassed = value;
|
|
2527
|
-
setPassed(value);
|
|
2528
|
-
completedRef.current = value;
|
|
2529
|
-
answeredRef.current = value;
|
|
2530
|
-
});
|
|
2531
|
-
readBooleanStateField(state, "submitted", (value) => {
|
|
2532
|
-
nextSubmitted = value;
|
|
2533
|
-
setSubmitted(value);
|
|
2534
|
-
if (value) answeredRef.current = true;
|
|
2535
|
-
});
|
|
2536
|
-
const kw = state.keyboardWord;
|
|
2537
|
-
if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
|
|
2538
|
-
let nextScore = 0;
|
|
2539
|
-
answers.forEach((ans, i) => {
|
|
2540
|
-
if ((nextZones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) nextScore += 1;
|
|
2541
|
-
});
|
|
2542
|
-
replayTelemetry(nextZones, nextPassed, nextSubmitted, nextScore, answers.length);
|
|
2543
|
-
}
|
|
2544
|
-
}),
|
|
2545
|
-
[allFilled, answers, assessment, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, props.passingScore, props.template, score, submitted, zones]
|
|
2546
|
-
);
|
|
2547
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2548
|
-
const placeInZone = (zoneId, word) => {
|
|
2549
|
-
if (passed && !props.enableRetry) return;
|
|
2550
|
-
const prev = zones[zoneId];
|
|
2551
|
-
setZones((z) => ({ ...z, [zoneId]: word }));
|
|
2552
|
-
setPool((p) => {
|
|
2553
|
-
const next = p.filter((w) => w !== word);
|
|
2554
|
-
if (prev) next.push(prev);
|
|
2555
|
-
return next;
|
|
2556
|
-
});
|
|
2557
|
-
setKeyboardWord(null);
|
|
2558
|
-
};
|
|
2559
|
-
const onDragStart = (word) => (e) => {
|
|
2560
|
-
e.dataTransfer.setData("text/plain", word);
|
|
2561
|
-
};
|
|
2562
|
-
const onDrop = (zoneId) => (e) => {
|
|
2563
|
-
e.preventDefault();
|
|
2564
|
-
const word = e.dataTransfer.getData("text/plain");
|
|
2565
|
-
if (word) placeInZone(zoneId, word);
|
|
2566
|
-
};
|
|
2567
|
-
const check = () => {
|
|
2568
|
-
if (!hasZones) {
|
|
2569
|
-
if (isDevEnvironment4()) {
|
|
2570
|
-
console.warn("[lessonkit] DragTheWords has no drop zones in template");
|
|
2571
|
-
}
|
|
2572
|
-
return;
|
|
2573
|
-
}
|
|
2574
|
-
if (!allFilled) return;
|
|
2575
|
-
if (passed) return;
|
|
2576
|
-
const snapshot = JSON.stringify(zones);
|
|
2577
|
-
if (checkSnapshotRef.current === snapshot) return;
|
|
2578
|
-
checkSnapshotRef.current = snapshot;
|
|
2579
|
-
answeredRef.current = true;
|
|
2580
|
-
setSubmitted(true);
|
|
2581
|
-
assessment.answer({
|
|
2582
|
-
checkId,
|
|
2583
|
-
interactionType: INTERACTION4,
|
|
2584
|
-
question: props.template,
|
|
2585
|
-
response: zones,
|
|
2586
|
-
correct: passedThreshold
|
|
2587
|
-
});
|
|
2588
|
-
if (passedThreshold && !completedRef.current) {
|
|
2589
|
-
completedRef.current = true;
|
|
2590
|
-
setPassed(true);
|
|
2591
|
-
assessment.complete({
|
|
2592
|
-
checkId,
|
|
2593
|
-
interactionType: INTERACTION4,
|
|
2594
|
-
score,
|
|
2595
|
-
maxScore,
|
|
2596
|
-
passingScore: props.passingScore ?? maxScore
|
|
2597
|
-
});
|
|
2598
|
-
}
|
|
2599
|
-
};
|
|
2600
|
-
useEffect8(() => {
|
|
2601
|
-
if (!allFilled) {
|
|
2602
|
-
answeredRef.current = false;
|
|
2603
|
-
checkSnapshotRef.current = null;
|
|
2604
|
-
setSubmitted(false);
|
|
2605
|
-
}
|
|
2606
|
-
}, [allFilled]);
|
|
2607
|
-
useEffect8(() => {
|
|
2608
|
-
if (props.autoCheck && allFilled && !passed) check();
|
|
2609
|
-
}, [allFilled, props.autoCheck, zones, passedThreshold, passed]);
|
|
2610
|
-
return /* @__PURE__ */ jsxs7("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
|
|
2611
|
-
/* @__PURE__ */ jsx11("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
|
|
2612
|
-
/* @__PURE__ */ jsx11("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ jsx11(
|
|
2613
|
-
"button",
|
|
2614
|
-
{
|
|
2615
|
-
type: "button",
|
|
2616
|
-
draggable: true,
|
|
2617
|
-
"data-testid": `word-${word}`,
|
|
2618
|
-
"aria-pressed": keyboardWord === word,
|
|
2619
|
-
onDragStart: onDragStart(word),
|
|
2620
|
-
onClick: () => setKeyboardWord(keyboardWord === word ? null : word),
|
|
2621
|
-
style: { margin: "0.25rem" },
|
|
2622
|
-
children: word
|
|
2623
|
-
},
|
|
2624
|
-
word
|
|
2625
|
-
)) }),
|
|
2626
|
-
/* @__PURE__ */ jsx11("p", { children: parts.map((part, i) => {
|
|
2627
|
-
if (!part.startsWith("zone-")) return /* @__PURE__ */ jsx11(React12.Fragment, { children: part }, i);
|
|
2628
|
-
return /* @__PURE__ */ jsx11(
|
|
2629
|
-
"span",
|
|
2630
|
-
{
|
|
2631
|
-
role: "button",
|
|
2632
|
-
tabIndex: 0,
|
|
2633
|
-
"data-testid": part,
|
|
2634
|
-
onDragOver: (e) => e.preventDefault(),
|
|
2635
|
-
onDrop: onDrop(part),
|
|
2636
|
-
onClick: () => keyboardWord && placeInZone(part, keyboardWord),
|
|
2637
|
-
onKeyDown: (e) => {
|
|
2638
|
-
if (e.key === "Enter" && keyboardWord) placeInZone(part, keyboardWord);
|
|
2639
|
-
},
|
|
2640
|
-
style: {
|
|
2641
|
-
display: "inline-block",
|
|
2642
|
-
minWidth: "6em",
|
|
2643
|
-
border: "1px dashed currentColor",
|
|
2644
|
-
padding: "0.2em 0.5em",
|
|
2645
|
-
margin: "0 0.2em"
|
|
2646
|
-
},
|
|
2647
|
-
children: zones[part] || "___"
|
|
2648
|
-
},
|
|
2649
|
-
part
|
|
2650
|
-
);
|
|
2651
|
-
}) }),
|
|
2652
|
-
/* @__PURE__ */ jsx11("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
|
|
2653
|
-
!hasZones ? /* @__PURE__ */ jsx11("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
|
|
2654
|
-
submitted ? /* @__PURE__ */ jsx11("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
|
|
2655
|
-
] });
|
|
2656
|
-
}
|
|
2657
|
-
var DragTheWordsInnerForwarded = forwardRef5(DragTheWordsInner);
|
|
2658
|
-
var DragTheWords = forwardRef5(function DragTheWords2(props, ref) {
|
|
2659
|
-
return /* @__PURE__ */ jsx11(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx11(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2660
|
-
});
|
|
2661
|
-
|
|
2662
|
-
// src/blocks/DragAndDrop.tsx
|
|
2663
|
-
import { forwardRef as forwardRef6, useEffect as useEffect9, useMemo as useMemo11, useRef as useRef10, useState as useState9 } from "react";
|
|
2664
|
-
import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2665
|
-
var INTERACTION5 = "dragAndDrop";
|
|
2666
|
-
function DragAndDropInner(props, ref) {
|
|
2667
|
-
const checkId = useMemo11(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
2668
|
-
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2669
|
-
const [assignments, setAssignments] = useState9(
|
|
2670
|
-
() => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
|
|
2671
|
-
);
|
|
2672
|
-
const [pool, setPool] = useState9(() => props.items.map((i) => i.id));
|
|
2673
|
-
const [keyboardItem, setKeyboardItem] = useState9(null);
|
|
2674
|
-
const [passed, setPassed] = useState9(false);
|
|
2675
|
-
const [checked, setChecked] = useState9(false);
|
|
2676
|
-
const completedRef = useRef10(false);
|
|
2677
|
-
const reset = () => {
|
|
2678
|
-
completedRef.current = false;
|
|
2679
|
-
setPassed(false);
|
|
2680
|
-
setChecked(false);
|
|
2681
|
-
setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
|
|
2682
|
-
setPool(props.items.map((i) => i.id));
|
|
2683
|
-
setKeyboardItem(null);
|
|
2684
|
-
};
|
|
2685
|
-
useEffect9(() => {
|
|
2686
|
-
reset();
|
|
2687
|
-
}, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
|
|
2688
|
-
const hasTargets = props.targets.length > 0;
|
|
2689
|
-
const allFilled = hasTargets && props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
|
|
2690
|
-
let score = 0;
|
|
2691
|
-
props.targets.forEach((t) => {
|
|
2692
|
-
if (assignments[t.id] === t.accepts) score += 1;
|
|
2693
|
-
});
|
|
2694
|
-
const maxScore = props.targets.length || 1;
|
|
2695
|
-
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
2696
|
-
const handle = useMemo11(() => {
|
|
2697
|
-
return buildAssessmentHandle({
|
|
2698
|
-
checkId,
|
|
2699
|
-
getScore: () => score,
|
|
2700
|
-
getMaxScore: () => maxScore,
|
|
2701
|
-
getAnswerGiven: () => hasTargets && allFilled,
|
|
2702
|
-
resetTask: reset,
|
|
2703
|
-
showSolutions: () => {
|
|
2704
|
-
},
|
|
2705
|
-
getXAPIData: () => ({
|
|
2706
|
-
checkId,
|
|
2707
|
-
interactionType: INTERACTION5,
|
|
2708
|
-
response: assignments,
|
|
2709
|
-
correct: passedThreshold,
|
|
2710
|
-
score,
|
|
2711
|
-
maxScore
|
|
2712
|
-
}),
|
|
2713
|
-
getCurrentState: () => ({ assignments, pool, passed, checked, keyboardItem }),
|
|
2714
|
-
resume: (state) => {
|
|
2715
|
-
const rawAssignments = state.assignments;
|
|
2716
|
-
if (rawAssignments && typeof rawAssignments === "object") {
|
|
2717
|
-
setAssignments({ ...rawAssignments });
|
|
2718
|
-
}
|
|
2719
|
-
if (Array.isArray(state.pool)) setPool([...state.pool]);
|
|
2720
|
-
readBooleanStateField(state, "passed", (value) => {
|
|
2721
|
-
setPassed(value);
|
|
2722
|
-
completedRef.current = value;
|
|
2723
|
-
});
|
|
2724
|
-
readBooleanStateField(state, "checked", setChecked);
|
|
2725
|
-
const item = state.keyboardItem;
|
|
2726
|
-
if (item === null || typeof item === "string") setKeyboardItem(item ?? null);
|
|
2727
|
-
}
|
|
2728
|
-
});
|
|
2729
|
-
}, [allFilled, assignments, checkId, checked, hasTargets, keyboardItem, maxScore, passed, passedThreshold, pool, props.targets, score]);
|
|
2730
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2731
|
-
const place = (targetId, itemId) => {
|
|
2732
|
-
if (passed && !props.enableRetry) return;
|
|
2733
|
-
setChecked(false);
|
|
2734
|
-
const prev = assignments[targetId];
|
|
2735
|
-
setAssignments((a) => ({ ...a, [targetId]: itemId }));
|
|
2736
|
-
setPool((p) => {
|
|
2737
|
-
const next = p.filter((id) => id !== itemId);
|
|
2738
|
-
if (prev) next.push(prev);
|
|
2739
|
-
return next;
|
|
2740
|
-
});
|
|
2741
|
-
setKeyboardItem(null);
|
|
2742
|
-
};
|
|
2743
|
-
const check = () => {
|
|
2744
|
-
if (!allFilled) return;
|
|
2745
|
-
setChecked(true);
|
|
2746
|
-
assessment.answer({
|
|
2747
|
-
checkId,
|
|
2748
|
-
interactionType: INTERACTION5,
|
|
2749
|
-
response: assignments,
|
|
2750
|
-
correct: passedThreshold
|
|
2751
|
-
});
|
|
2752
|
-
if (passedThreshold && !completedRef.current) {
|
|
2753
|
-
completedRef.current = true;
|
|
2754
|
-
setPassed(true);
|
|
2755
|
-
assessment.complete({
|
|
2756
|
-
checkId,
|
|
2757
|
-
interactionType: INTERACTION5,
|
|
2758
|
-
score,
|
|
2759
|
-
maxScore,
|
|
2760
|
-
passingScore: props.passingScore ?? maxScore
|
|
2761
|
-
});
|
|
2762
|
-
}
|
|
2763
|
-
};
|
|
2764
|
-
return /* @__PURE__ */ jsxs8("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
|
|
2765
|
-
/* @__PURE__ */ jsx12("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
|
|
2766
|
-
/* @__PURE__ */ jsx12("div", { role: "list", "aria-label": "Draggable items", children: pool.flatMap((id) => {
|
|
2767
|
-
const item = props.items.find((i) => i.id === id);
|
|
2768
|
-
if (!item) return [];
|
|
2769
|
-
return /* @__PURE__ */ jsx12(
|
|
2770
|
-
"button",
|
|
2771
|
-
{
|
|
2772
|
-
type: "button",
|
|
2773
|
-
draggable: true,
|
|
2774
|
-
"data-testid": `drag-item-${id}`,
|
|
2775
|
-
"aria-pressed": keyboardItem === id,
|
|
2776
|
-
onDragStart: (e) => e.dataTransfer.setData("text/plain", id),
|
|
2777
|
-
onClick: () => setKeyboardItem(keyboardItem === id ? null : id),
|
|
2778
|
-
style: { margin: "0.25rem" },
|
|
2779
|
-
children: item.label
|
|
2780
|
-
},
|
|
2781
|
-
id
|
|
2782
|
-
);
|
|
2783
|
-
}) }),
|
|
2784
|
-
/* @__PURE__ */ jsx12("ul", { children: props.targets.map((target) => {
|
|
2785
|
-
const assigned = assignments[target.id];
|
|
2786
|
-
const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
|
|
2787
|
-
return /* @__PURE__ */ jsxs8("li", { children: [
|
|
2788
|
-
/* @__PURE__ */ jsx12("strong", { children: target.label }),
|
|
2789
|
-
" ",
|
|
2790
|
-
/* @__PURE__ */ jsx12(
|
|
2791
|
-
"span",
|
|
2792
|
-
{
|
|
2793
|
-
role: "button",
|
|
2794
|
-
tabIndex: 0,
|
|
2795
|
-
"data-testid": `drop-${target.id}`,
|
|
2796
|
-
onDragOver: (e) => e.preventDefault(),
|
|
2797
|
-
onDrop: (e) => {
|
|
2798
|
-
e.preventDefault();
|
|
2799
|
-
const id = e.dataTransfer.getData("text/plain");
|
|
2800
|
-
if (id) place(target.id, id);
|
|
2801
|
-
},
|
|
2802
|
-
onClick: () => keyboardItem && place(target.id, keyboardItem),
|
|
2803
|
-
onKeyDown: (e) => {
|
|
2804
|
-
if (e.key === "Enter" && keyboardItem) place(target.id, keyboardItem);
|
|
2805
|
-
},
|
|
2806
|
-
style: {
|
|
2807
|
-
display: "inline-block",
|
|
2808
|
-
minWidth: "8em",
|
|
2809
|
-
border: "1px dashed currentColor",
|
|
2810
|
-
padding: "0.25em"
|
|
2811
|
-
},
|
|
2812
|
-
children: label
|
|
2813
|
-
}
|
|
2814
|
-
)
|
|
2815
|
-
] }, target.id);
|
|
2816
|
-
}) }),
|
|
2817
|
-
/* @__PURE__ */ jsx12("button", { type: "button", "data-testid": "check-drag-drop", disabled: !hasTargets || !allFilled || passed, onClick: check, children: "Check" }),
|
|
2818
|
-
checked ? /* @__PURE__ */ jsx12("p", { role: "status", "aria-live": "polite", children: passedThreshold ? "Correct" : "Try again" }) : null
|
|
2819
|
-
] });
|
|
2820
|
-
}
|
|
2821
|
-
var DragAndDropInnerForwarded = forwardRef6(DragAndDropInner);
|
|
2822
|
-
var DragAndDrop = forwardRef6(function DragAndDrop2(props, ref) {
|
|
2823
|
-
return /* @__PURE__ */ jsx12(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx12(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2824
|
-
});
|
|
2825
|
-
|
|
2826
|
-
// src/blocks/AssessmentSequence.tsx
|
|
2827
|
-
import React16, { forwardRef as forwardRef7, useCallback as useCallback7, useEffect as useEffect12, useId as useId3, useMemo as useMemo13, useRef as useRef13, useState as useState10 } from "react";
|
|
2828
|
-
import { deriveId } from "@lessonkit/core";
|
|
2829
|
-
|
|
2830
|
-
// src/compound/useCompoundShell.ts
|
|
2831
|
-
import { useMemo as useMemo12 } from "react";
|
|
2832
|
-
import { clampCompoundPageIndex as clampCompoundPageIndex3 } from "@lessonkit/core";
|
|
2833
|
-
|
|
2834
|
-
// src/compound/useCompoundNavigation.ts
|
|
2835
|
-
import { useCallback as useCallback4 } from "react";
|
|
2836
|
-
function useCompoundNavigation(pageCount, index, setIndex) {
|
|
2837
|
-
const goNext = useCallback4(() => {
|
|
2838
|
-
if (pageCount < 1) return;
|
|
2839
|
-
setIndex((i) => Math.min(i + 1, pageCount - 1));
|
|
2840
|
-
}, [pageCount, setIndex]);
|
|
2841
|
-
const goPrev = useCallback4(() => {
|
|
2842
|
-
setIndex((i) => Math.max(i - 1, 0));
|
|
2843
|
-
}, [setIndex]);
|
|
2844
|
-
const clampedIndex = pageCount < 1 ? 0 : Math.min(index, pageCount - 1);
|
|
2845
|
-
return {
|
|
2846
|
-
index: clampedIndex,
|
|
2847
|
-
setIndex,
|
|
2848
|
-
goNext,
|
|
2849
|
-
goPrev,
|
|
2850
|
-
progress: { current: pageCount < 1 ? 0 : clampedIndex + 1, total: pageCount }
|
|
2851
|
-
};
|
|
2852
|
-
}
|
|
2853
|
-
|
|
2854
|
-
// src/compound/useCompoundPersistence.ts
|
|
2855
|
-
import { useCallback as useCallback6, useContext as useContext7, useEffect as useEffect11, useRef as useRef12 } from "react";
|
|
2856
|
-
import {
|
|
2857
|
-
clampCompoundPageIndex as clampCompoundPageIndex2,
|
|
2858
|
-
createCompoundResumeState as createCompoundResumeState2,
|
|
2859
|
-
createSessionStoragePort as createSessionStoragePort3,
|
|
2860
|
-
loadCompoundState as loadCompoundState2
|
|
2861
|
-
} from "@lessonkit/core";
|
|
2862
|
-
|
|
2863
|
-
// src/compound/resumeChildHandles.ts
|
|
2864
|
-
function filterRegisteredChildStates(handles, childStates) {
|
|
2865
|
-
const filtered = {};
|
|
2866
|
-
for (const [key, value] of Object.entries(childStates)) {
|
|
2867
|
-
if (handles.has(key)) {
|
|
2868
|
-
filtered[key] = value;
|
|
2869
|
-
}
|
|
2870
|
-
}
|
|
2871
|
-
return filtered;
|
|
2872
|
-
}
|
|
2873
|
-
function resumeChildHandles(handles, childStates, opts) {
|
|
2874
|
-
const pendingKeys = Object.keys(childStates);
|
|
2875
|
-
const alreadyResumed = opts?.alreadyResumed;
|
|
2876
|
-
if (opts?.waitForHandles && pendingKeys.length > 0) {
|
|
2877
|
-
if (handles.size === 0) return false;
|
|
2878
|
-
const registeredPending = pendingKeys.filter((k) => handles.has(k));
|
|
2879
|
-
if (registeredPending.length === 0) {
|
|
2880
|
-
return false;
|
|
2881
|
-
}
|
|
2882
|
-
if (registeredPending.length < pendingKeys.length) {
|
|
2883
|
-
for (const key of registeredPending) {
|
|
2884
|
-
if (alreadyResumed?.has(key)) continue;
|
|
2885
|
-
const handle = handles.get(key);
|
|
2886
|
-
const child = childStates[key];
|
|
2887
|
-
if (handle?.resume && child) {
|
|
2888
|
-
handle.resume(child);
|
|
2889
|
-
alreadyResumed?.add(key);
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
|
-
return false;
|
|
2893
|
-
}
|
|
2894
|
-
}
|
|
2895
|
-
for (const [checkId, handle] of handles) {
|
|
2896
|
-
if (alreadyResumed?.has(checkId)) continue;
|
|
2897
|
-
const child = childStates[checkId];
|
|
2898
|
-
if (child && handle.resume) {
|
|
2899
|
-
handle.resume(child);
|
|
2900
|
-
alreadyResumed?.add(checkId);
|
|
2901
|
-
}
|
|
2902
|
-
}
|
|
2903
|
-
return true;
|
|
2904
|
-
}
|
|
2905
|
-
|
|
2906
|
-
// src/compound/useCompoundResume.ts
|
|
2907
|
-
import { useCallback as useCallback5, useContext as useContext6, useEffect as useEffect10, useRef as useRef11 } from "react";
|
|
2908
|
-
import { loadCompoundState, saveCompoundState } from "@lessonkit/core";
|
|
2909
|
-
import { createSessionStoragePort as createSessionStoragePort2 } from "@lessonkit/core";
|
|
2910
|
-
var warnedCompoundPersistFailure = false;
|
|
2911
|
-
function warnCompoundPersistFailure() {
|
|
2912
|
-
if (warnedCompoundPersistFailure || !isDevEnvironment4()) return;
|
|
2913
|
-
warnedCompoundPersistFailure = true;
|
|
2914
|
-
console.warn(
|
|
2915
|
-
"[lessonkit] compound resume state could not be saved to sessionStorage (quota or privacy mode); progress may be lost on reload."
|
|
2916
|
-
);
|
|
2917
|
-
}
|
|
2918
|
-
function useCompoundResume(opts) {
|
|
2919
|
-
const lessonkitCtx = useContext6(LessonkitContext);
|
|
2920
|
-
const storageRef = useRef11(opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2());
|
|
2921
|
-
const resumedRef = useRef11(false);
|
|
2922
|
-
const resumeKeyRef = useRef11("");
|
|
2923
|
-
const prevEnabledRef = useRef11(opts.enabled);
|
|
2924
|
-
useEffect10(() => {
|
|
2925
|
-
storageRef.current = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2();
|
|
2926
|
-
}, [opts.storage, lessonkitCtx?.storage]);
|
|
2927
|
-
useEffect10(() => {
|
|
2928
|
-
if (!prevEnabledRef.current && opts.enabled) {
|
|
2929
|
-
resumedRef.current = false;
|
|
2930
|
-
}
|
|
2931
|
-
prevEnabledRef.current = opts.enabled;
|
|
2932
|
-
const key = `${opts.courseId ?? ""}:${opts.compoundId}`;
|
|
2933
|
-
if (resumeKeyRef.current !== key) {
|
|
2934
|
-
resumeKeyRef.current = key;
|
|
2935
|
-
resumedRef.current = false;
|
|
2936
|
-
}
|
|
2937
|
-
if (!opts.enabled || !opts.courseId || resumedRef.current) return;
|
|
2938
|
-
const saved = loadCompoundState(storageRef.current, opts.courseId, opts.compoundId);
|
|
2939
|
-
if (saved) {
|
|
2940
|
-
resumedRef.current = true;
|
|
2941
|
-
opts.onResume?.(saved);
|
|
2942
|
-
}
|
|
2943
|
-
}, [opts.enabled, opts.courseId, opts.compoundId, opts.onResume]);
|
|
2944
|
-
return useCallback5(
|
|
2945
|
-
(state) => {
|
|
2946
|
-
if (!opts.enabled || !opts.courseId) return;
|
|
2947
|
-
const persisted = saveCompoundState(storageRef.current, opts.courseId, opts.compoundId, state);
|
|
2948
|
-
if (!persisted) warnCompoundPersistFailure();
|
|
2949
|
-
},
|
|
2950
|
-
[opts.enabled, opts.courseId, opts.compoundId]
|
|
2951
|
-
);
|
|
2952
|
-
}
|
|
2953
|
-
|
|
2954
|
-
// src/compound/useCompoundPersistence.ts
|
|
2955
|
-
function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, storage = createSessionStoragePort3()) {
|
|
2956
|
-
if (!enabled || !courseId || pageCount < 1) return 0;
|
|
2957
|
-
const saved = loadCompoundState2(storage, courseId, compoundId);
|
|
2958
|
-
if (!saved) return 0;
|
|
2959
|
-
return clampCompoundPageIndex2(saved.activePageIndex, pageCount);
|
|
2960
|
-
}
|
|
2961
|
-
function stripOrphanChildStates(handles, childStates) {
|
|
2962
|
-
return filterRegisteredChildStates(handles, childStates);
|
|
2963
|
-
}
|
|
2964
|
-
function useCompoundPersistence(opts) {
|
|
2965
|
-
const lessonkitCtx = useContext7(LessonkitContext);
|
|
2966
|
-
const storage = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort3();
|
|
2967
|
-
const ctx = useCompoundRegistry();
|
|
2968
|
-
const handlesVersion = useCompoundHandlesVersion();
|
|
2969
|
-
const bridgeRef = useCompoundHydrationBridgeRef();
|
|
2970
|
-
const pendingChildResumeRef = useRef12(null);
|
|
2971
|
-
const resumedChildKeysRef = useRef12(/* @__PURE__ */ new Set());
|
|
2972
|
-
const loadedChildStatesRef = useRef12({});
|
|
2973
|
-
const skipSaveUntilHydratedRef = useRef12(false);
|
|
2974
|
-
const hydrationKeyRef = useRef12("");
|
|
2975
|
-
const hydrationInitRef = useRef12(false);
|
|
2976
|
-
const hydrationKey = `${opts.courseId ?? ""}:${opts.compoundId}`;
|
|
2977
|
-
if (hydrationKeyRef.current !== hydrationKey) {
|
|
2978
|
-
hydrationKeyRef.current = hydrationKey;
|
|
2979
|
-
hydrationInitRef.current = false;
|
|
2980
|
-
loadedChildStatesRef.current = {};
|
|
2981
|
-
skipSaveUntilHydratedRef.current = false;
|
|
2982
|
-
pendingChildResumeRef.current = null;
|
|
2983
|
-
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
2984
|
-
}
|
|
2985
|
-
if (!hydrationInitRef.current && opts.enabled && opts.courseId) {
|
|
2986
|
-
hydrationInitRef.current = true;
|
|
2987
|
-
const saved = loadCompoundState2(storage, opts.courseId, opts.compoundId);
|
|
2988
|
-
if (saved && Object.keys(saved.childStates).length > 0) {
|
|
2989
|
-
loadedChildStatesRef.current = { ...saved.childStates };
|
|
2990
|
-
skipSaveUntilHydratedRef.current = true;
|
|
2991
|
-
pendingChildResumeRef.current = saved;
|
|
2992
|
-
}
|
|
2993
|
-
}
|
|
2994
|
-
const buildState = useCallback6(() => {
|
|
2995
|
-
const childStates = {
|
|
2996
|
-
...loadedChildStatesRef.current
|
|
2997
|
-
};
|
|
2998
|
-
if (ctx) {
|
|
2999
|
-
for (const [checkId, entry] of ctx.getRegisteredHandles()) {
|
|
3000
|
-
const handle = entry.handle;
|
|
3001
|
-
if (handle.getCurrentState) {
|
|
3002
|
-
childStates[checkId] = handle.getCurrentState();
|
|
3003
|
-
delete loadedChildStatesRef.current[checkId];
|
|
3004
|
-
}
|
|
3005
|
-
}
|
|
3006
|
-
}
|
|
3007
|
-
return createCompoundResumeState2({
|
|
3008
|
-
activePageIndex: clampCompoundPageIndex2(opts.index, opts.pageCount),
|
|
3009
|
-
childStates
|
|
3010
|
-
});
|
|
3011
|
-
}, [ctx, opts.index, opts.pageCount]);
|
|
3012
|
-
const buildStateRef = useRef12(buildState);
|
|
3013
|
-
buildStateRef.current = buildState;
|
|
3014
|
-
const persistNowRef = useRef12(() => {
|
|
3015
|
-
});
|
|
3016
|
-
const finalizeHydration = useCallback6(
|
|
3017
|
-
(childStates) => {
|
|
3018
|
-
loadedChildStatesRef.current = {
|
|
3019
|
-
...loadedChildStatesRef.current,
|
|
3020
|
-
...childStates
|
|
3021
|
-
};
|
|
3022
|
-
skipSaveUntilHydratedRef.current = false;
|
|
3023
|
-
pendingChildResumeRef.current = null;
|
|
3024
|
-
queueMicrotask(() => persistNowRef.current());
|
|
3025
|
-
},
|
|
3026
|
-
[]
|
|
3027
|
-
);
|
|
3028
|
-
const applyPendingChildResume = useCallback6(() => {
|
|
3029
|
-
const pending = pendingChildResumeRef.current;
|
|
3030
|
-
if (!pending || !ctx) return;
|
|
3031
|
-
const handles = ctx.getHandles();
|
|
3032
|
-
const applied = resumeChildHandles(handles, pending.childStates, {
|
|
3033
|
-
waitForHandles: true,
|
|
3034
|
-
alreadyResumed: resumedChildKeysRef.current
|
|
3035
|
-
});
|
|
3036
|
-
if (!applied) {
|
|
3037
|
-
if (handles.size === 0) {
|
|
3038
|
-
const registeredOnly2 = stripOrphanChildStates(handles, pending.childStates);
|
|
3039
|
-
resumeChildHandles(handles, registeredOnly2, {
|
|
3040
|
-
alreadyResumed: resumedChildKeysRef.current
|
|
3041
|
-
});
|
|
3042
|
-
finalizeHydration(registeredOnly2);
|
|
3043
|
-
return;
|
|
3044
|
-
}
|
|
3045
|
-
const handlesAtWait = handles.size;
|
|
3046
|
-
queueMicrotask(() => {
|
|
3047
|
-
if (pendingChildResumeRef.current !== pending) return;
|
|
3048
|
-
const handlesNow = ctx.getHandles();
|
|
3049
|
-
if (handlesNow.size !== handlesAtWait) return;
|
|
3050
|
-
const registeredOnly2 = stripOrphanChildStates(handlesNow, pending.childStates);
|
|
3051
|
-
resumeChildHandles(handlesNow, registeredOnly2, {
|
|
3052
|
-
alreadyResumed: resumedChildKeysRef.current
|
|
3053
|
-
});
|
|
3054
|
-
finalizeHydration(registeredOnly2);
|
|
3055
|
-
});
|
|
3056
|
-
return;
|
|
3057
|
-
}
|
|
3058
|
-
const registeredOnly = stripOrphanChildStates(handles, pending.childStates);
|
|
3059
|
-
finalizeHydration(registeredOnly);
|
|
3060
|
-
}, [ctx, finalizeHydration]);
|
|
3061
|
-
const saveResume = useCompoundResume({
|
|
3062
|
-
courseId: opts.courseId,
|
|
3063
|
-
compoundId: opts.compoundId,
|
|
3064
|
-
enabled: opts.enabled,
|
|
3065
|
-
storage,
|
|
3066
|
-
onResume: (state) => {
|
|
3067
|
-
const clamped = clampCompoundPageIndex2(state.activePageIndex, opts.pageCount);
|
|
3068
|
-
loadedChildStatesRef.current = { ...state.childStates };
|
|
3069
|
-
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
3070
|
-
opts.setIndex(clamped);
|
|
3071
|
-
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
3072
|
-
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
3073
|
-
queueMicrotask(() => applyPendingChildResume());
|
|
3074
|
-
}
|
|
3075
|
-
});
|
|
3076
|
-
const persistNow = useCallback6(() => {
|
|
3077
|
-
if (!opts.enabled || !opts.courseId) return;
|
|
3078
|
-
if (skipSaveUntilHydratedRef.current) return;
|
|
3079
|
-
saveResume(buildStateRef.current());
|
|
3080
|
-
}, [opts.enabled, opts.courseId, saveResume]);
|
|
3081
|
-
useEffect11(() => {
|
|
3082
|
-
persistNowRef.current = persistNow;
|
|
3083
|
-
}, [persistNow]);
|
|
3084
|
-
const notifyImperativeResume = useCallback6(
|
|
3085
|
-
(state) => {
|
|
3086
|
-
const clamped = clampCompoundPageIndex2(state.activePageIndex, opts.pageCount);
|
|
3087
|
-
loadedChildStatesRef.current = { ...state.childStates };
|
|
3088
|
-
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
3089
|
-
opts.setIndex(clamped);
|
|
3090
|
-
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
3091
|
-
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
3092
|
-
queueMicrotask(() => applyPendingChildResume());
|
|
3093
|
-
},
|
|
3094
|
-
[opts.pageCount, opts.setIndex, applyPendingChildResume]
|
|
3095
|
-
);
|
|
3096
|
-
useEffect11(() => {
|
|
3097
|
-
if (!bridgeRef) return;
|
|
3098
|
-
bridgeRef.current = { notifyImperativeResume };
|
|
3099
|
-
return () => {
|
|
3100
|
-
if (bridgeRef.current?.notifyImperativeResume === notifyImperativeResume) {
|
|
3101
|
-
bridgeRef.current = null;
|
|
3102
|
-
}
|
|
3103
|
-
};
|
|
3104
|
-
}, [bridgeRef, notifyImperativeResume]);
|
|
3105
|
-
useEffect11(() => {
|
|
3106
|
-
applyPendingChildResume();
|
|
3107
|
-
}, [opts.index, handlesVersion, applyPendingChildResume]);
|
|
3108
|
-
useEffect11(() => {
|
|
3109
|
-
persistNow();
|
|
3110
|
-
}, [persistNow, opts.index, opts.pageCount, handlesVersion]);
|
|
3111
|
-
useEffect11(() => {
|
|
3112
|
-
if (!opts.enabled || !opts.courseId || typeof document === "undefined") return;
|
|
3113
|
-
const flushOnExit = () => {
|
|
3114
|
-
if (document.visibilityState === "hidden") persistNow();
|
|
3115
|
-
};
|
|
3116
|
-
document.addEventListener("visibilitychange", flushOnExit);
|
|
3117
|
-
window.addEventListener("pagehide", flushOnExit);
|
|
3118
|
-
return () => {
|
|
3119
|
-
document.removeEventListener("visibilitychange", flushOnExit);
|
|
3120
|
-
window.removeEventListener("pagehide", flushOnExit);
|
|
3121
|
-
};
|
|
3122
|
-
}, [opts.enabled, opts.courseId, persistNow]);
|
|
3123
|
-
}
|
|
3124
|
-
|
|
3125
|
-
// src/compound/useCompoundShell.ts
|
|
3126
|
-
function useCompoundShell(opts) {
|
|
3127
|
-
const ctx = useCompoundRegistry();
|
|
3128
|
-
useCompoundPersistence({
|
|
3129
|
-
courseId: opts.courseId,
|
|
3130
|
-
compoundId: opts.compoundId,
|
|
3131
|
-
pageCount: opts.pageCount,
|
|
3132
|
-
index: opts.index,
|
|
3133
|
-
setIndex: opts.setIndex,
|
|
3134
|
-
enabled: opts.persistEnabled,
|
|
3135
|
-
storage: opts.storage
|
|
3136
|
-
});
|
|
3137
|
-
const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
|
|
3138
|
-
const visibleIndex = clampCompoundPageIndex3(opts.index, opts.pageCount);
|
|
3139
|
-
useCompoundHandleRef(opts.ref, {
|
|
3140
|
-
activePageIndex: visibleIndex,
|
|
3141
|
-
setActivePageIndex: opts.setIndex,
|
|
3142
|
-
getHandles: () => ctx?.getHandles() ?? /* @__PURE__ */ new Map(),
|
|
3143
|
-
getRegisteredHandles: () => ctx?.getRegisteredHandles() ?? /* @__PURE__ */ new Map(),
|
|
3144
|
-
pageCount: opts.pageCount,
|
|
3145
|
-
enableSolutionsButton: opts.enableSolutionsButton
|
|
3146
|
-
});
|
|
3147
|
-
return { visibleIndex, goNext, goPrev, progress, ctx };
|
|
3148
|
-
}
|
|
3149
|
-
function useCompoundInitialIndex(opts) {
|
|
3150
|
-
return useMemo12(
|
|
3151
|
-
() => readCompoundInitialIndex(
|
|
3152
|
-
opts.courseId,
|
|
3153
|
-
opts.compoundId,
|
|
3154
|
-
opts.pageCount,
|
|
3155
|
-
opts.persistEnabled,
|
|
3156
|
-
opts.storage
|
|
3157
|
-
),
|
|
3158
|
-
[opts.courseId, opts.compoundId, opts.pageCount, opts.persistEnabled, opts.storage]
|
|
3159
|
-
);
|
|
3160
|
-
}
|
|
3161
|
-
|
|
3162
|
-
// src/compound/validateChildren.ts
|
|
3163
|
-
import React15 from "react";
|
|
3164
|
-
import {
|
|
3165
|
-
ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
3166
|
-
COMPOUND_MAX_NESTING_DEPTH,
|
|
3167
|
-
isChildTypeAllowed
|
|
3168
|
-
} from "@lessonkit/core";
|
|
3169
|
-
|
|
3170
|
-
// src/compound/blockType.ts
|
|
3171
|
-
var LESSONKIT_BLOCK_TYPE = /* @__PURE__ */ Symbol.for("lessonkit.blockType");
|
|
3172
|
-
function setLessonkitBlockType(component, blockType) {
|
|
3173
|
-
component[LESSONKIT_BLOCK_TYPE] = blockType;
|
|
3174
|
-
if (!component.displayName) {
|
|
3175
|
-
component.displayName = blockType;
|
|
3176
|
-
}
|
|
3177
|
-
return component;
|
|
3178
|
-
}
|
|
3179
|
-
function getLessonkitBlockType(component) {
|
|
3180
|
-
if (!component || typeof component !== "object" && typeof component !== "function") {
|
|
3181
|
-
return void 0;
|
|
3182
|
-
}
|
|
3183
|
-
const typed = component;
|
|
3184
|
-
return typed[LESSONKIT_BLOCK_TYPE] ?? typed.displayName;
|
|
3185
|
-
}
|
|
3186
|
-
|
|
3187
|
-
// src/compound/validateChildren.ts
|
|
3188
|
-
var warnedPairs = /* @__PURE__ */ new Set();
|
|
3189
|
-
var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
3190
|
-
"Page",
|
|
3191
|
-
"InteractiveBook",
|
|
3192
|
-
"Slide",
|
|
3193
|
-
"SlideDeck",
|
|
3194
|
-
"AssessmentSequence"
|
|
3195
|
-
]);
|
|
3196
|
-
function warnOrThrow(msg, strict) {
|
|
3197
|
-
if (strict) throw new Error(msg);
|
|
3198
|
-
if (!warnedPairs.has(msg)) {
|
|
3199
|
-
warnedPairs.add(msg);
|
|
3200
|
-
console.warn(msg);
|
|
3201
|
-
}
|
|
3202
|
-
}
|
|
3203
|
-
function validateNode(parent, node, depth, strict) {
|
|
3204
|
-
React15.Children.forEach(node, (child) => {
|
|
3205
|
-
if (!React15.isValidElement(child)) return;
|
|
3206
|
-
const blockType = getLessonkitBlockType(child.type);
|
|
3207
|
-
if (!blockType) {
|
|
3208
|
-
if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
3209
|
-
validateNode(parent, child.props.children, depth, strict);
|
|
3210
|
-
}
|
|
3211
|
-
return;
|
|
3212
|
-
}
|
|
3213
|
-
if (!isChildTypeAllowed(parent, blockType)) {
|
|
3214
|
-
const key = `${parent}:${blockType}`;
|
|
3215
|
-
if (!warnedPairs.has(key)) {
|
|
3216
|
-
warnedPairs.add(key);
|
|
3217
|
-
const msg = `[lessonkit] Block "${blockType}" is not in the allowlist for "${parent}"`;
|
|
3218
|
-
if (strict) throw new Error(msg);
|
|
3219
|
-
console.warn(msg);
|
|
3220
|
-
}
|
|
3221
|
-
}
|
|
3222
|
-
if (COMPOUND_CONTAINER_TYPES.has(blockType)) {
|
|
3223
|
-
const maxDepth = COMPOUND_MAX_NESTING_DEPTH[parent];
|
|
3224
|
-
if (depth >= maxDepth) {
|
|
3225
|
-
warnOrThrow(
|
|
3226
|
-
`[lessonkit] Block "${blockType}" exceeds max nesting depth (${maxDepth}) for "${parent}"`,
|
|
3227
|
-
strict
|
|
3228
|
-
);
|
|
3229
|
-
}
|
|
3230
|
-
const nestedParent = blockType;
|
|
3231
|
-
validateNode(nestedParent, child.props.children, depth + 1, strict);
|
|
3232
|
-
} else if (blockType === "Accordion") {
|
|
3233
|
-
const sections = child.props.sections;
|
|
3234
|
-
if (sections) validateAccordionSections(sections, strict);
|
|
3235
|
-
} else if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
3236
|
-
validateSubtreeForForbidden(
|
|
3237
|
-
child.props.children,
|
|
3238
|
-
ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
3239
|
-
strict
|
|
3240
|
-
);
|
|
3241
|
-
}
|
|
3242
|
-
});
|
|
3243
|
-
}
|
|
3244
|
-
function validateSubtreeForForbidden(node, forbidden, strict) {
|
|
3245
|
-
React15.Children.forEach(node, (child) => {
|
|
3246
|
-
if (!React15.isValidElement(child)) return;
|
|
3247
|
-
const blockType = getLessonkitBlockType(child.type);
|
|
3248
|
-
if (blockType && forbidden.includes(blockType)) {
|
|
3249
|
-
warnOrThrow(`[lessonkit] Block "${blockType}" must not nest inside Accordion`, strict);
|
|
3250
|
-
}
|
|
3251
|
-
if (blockType === "Accordion") {
|
|
3252
|
-
const sections = child.props.sections;
|
|
3253
|
-
if (sections) validateAccordionSections(sections, strict);
|
|
3254
|
-
return;
|
|
3255
|
-
}
|
|
3256
|
-
if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
3257
|
-
validateSubtreeForForbidden(
|
|
3258
|
-
child.props.children,
|
|
3259
|
-
forbidden,
|
|
3260
|
-
strict
|
|
3261
|
-
);
|
|
3262
|
-
}
|
|
3263
|
-
});
|
|
3264
|
-
}
|
|
3265
|
-
function validateAccordionSections(sections, strict) {
|
|
3266
|
-
if (!isDevEnvironment4() && !strict) return;
|
|
3267
|
-
for (const section of sections) {
|
|
3268
|
-
validateSubtreeForForbidden(section.content, ACCORDION_FORBIDDEN_CHILD_TYPES, strict);
|
|
3269
|
-
}
|
|
3270
|
-
}
|
|
3271
|
-
function validateCompoundChildren(parent, children, strict) {
|
|
3272
|
-
if (!isDevEnvironment4() && !strict) return;
|
|
3273
|
-
validateNode(parent, children, 0, strict);
|
|
3274
|
-
}
|
|
3275
|
-
|
|
3276
|
-
// src/compound/warnPersistence.ts
|
|
3277
|
-
var DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID = "assessment-sequence";
|
|
3278
|
-
function warnSharedCompoundStorageKey(opts) {
|
|
3279
|
-
if (!opts.persistEnabled || opts.hasExplicitBlockId || !isDevEnvironment4()) return;
|
|
3280
|
-
console.warn(
|
|
3281
|
-
`[lessonkit] <${opts.componentName}> without blockId shares one sessionStorage key when persistCompoundState is enabled; set a unique blockId per instance.`
|
|
3282
|
-
);
|
|
3283
|
-
}
|
|
3284
|
-
|
|
3285
|
-
// src/blocks/AssessmentSequence.tsx
|
|
3286
|
-
import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
3287
|
-
var AssessmentSequenceInner = forwardRef7(
|
|
3288
|
-
function AssessmentSequenceInner2(props, ref) {
|
|
3289
|
-
const { compoundId, childArray, index, setIndex, persistEnabled } = props;
|
|
3290
|
-
const sequential = props.sequential !== false;
|
|
3291
|
-
const { config } = useLessonkit();
|
|
3292
|
-
const { visibleIndex, goNext, goPrev, progress } = useCompoundShell({
|
|
3293
|
-
courseId: config.courseId,
|
|
3294
|
-
compoundId,
|
|
3295
|
-
pageCount: childArray.length,
|
|
3296
|
-
index,
|
|
3297
|
-
setIndex,
|
|
3298
|
-
persistEnabled,
|
|
3299
|
-
ref,
|
|
3300
|
-
enableSolutionsButton: props.enableSolutionsButton
|
|
3301
|
-
});
|
|
3302
|
-
validateCompoundChildren("AssessmentSequence", props.children);
|
|
3303
|
-
if (!sequential) {
|
|
3304
|
-
return /* @__PURE__ */ jsx13("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
|
|
3305
|
-
}
|
|
3306
|
-
return /* @__PURE__ */ jsxs9("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
|
|
3307
|
-
/* @__PURE__ */ jsxs9("p", { children: [
|
|
3308
|
-
"Question ",
|
|
3309
|
-
progress.current,
|
|
3310
|
-
" of ",
|
|
3311
|
-
progress.total
|
|
3312
|
-
] }),
|
|
3313
|
-
/* @__PURE__ */ jsx13("div", { "data-testid": "assessment-sequence-step", children: childArray.map((child, i) => /* @__PURE__ */ jsx13("div", { hidden: i !== visibleIndex, children: /* @__PURE__ */ jsx13(CompoundPageIndexProvider, { pageIndex: i, children: child }) }, child.key ?? i)) }),
|
|
3314
|
-
/* @__PURE__ */ jsxs9("nav", { "aria-label": "Sequence navigation", children: [
|
|
3315
|
-
/* @__PURE__ */ jsx13(
|
|
3316
|
-
"button",
|
|
3317
|
-
{
|
|
3318
|
-
type: "button",
|
|
3319
|
-
"data-testid": "sequence-prev",
|
|
3320
|
-
disabled: visibleIndex === 0 || childArray.length === 0,
|
|
3321
|
-
onClick: goPrev,
|
|
3322
|
-
children: "Previous"
|
|
3323
|
-
}
|
|
3324
|
-
),
|
|
3325
|
-
/* @__PURE__ */ jsx13(
|
|
3326
|
-
"button",
|
|
3327
|
-
{
|
|
3328
|
-
type: "button",
|
|
3329
|
-
"data-testid": "sequence-next",
|
|
3330
|
-
disabled: visibleIndex >= childArray.length - 1 || childArray.length === 0,
|
|
3331
|
-
onClick: goNext,
|
|
3332
|
-
children: "Next"
|
|
3333
|
-
}
|
|
3334
|
-
)
|
|
3335
|
-
] })
|
|
3336
|
-
] });
|
|
3337
|
-
}
|
|
3338
|
-
);
|
|
3339
|
-
var AssessmentSequence = forwardRef7(
|
|
3340
|
-
function AssessmentSequence2(props, ref) {
|
|
3341
|
-
const reactInstanceId = useId3();
|
|
3342
|
-
const autoCompoundIdRef = useRef13(null);
|
|
3343
|
-
if (!props.blockId && !autoCompoundIdRef.current) {
|
|
3344
|
-
autoCompoundIdRef.current = deriveId(`assessment-sequence-${reactInstanceId}`);
|
|
3345
|
-
}
|
|
3346
|
-
const compoundId = useMemo13(
|
|
3347
|
-
() => props.blockId ? normalizeComponentId(props.blockId, "blockId") : autoCompoundIdRef.current ?? DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
|
|
3348
|
-
[props.blockId]
|
|
3349
|
-
);
|
|
3350
|
-
const childArray = React16.Children.toArray(props.children).filter(
|
|
3351
|
-
React16.isValidElement
|
|
3352
|
-
);
|
|
3353
|
-
const { config, storage } = useLessonkit();
|
|
3354
|
-
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
3355
|
-
useEffect12(() => {
|
|
3356
|
-
warnSharedCompoundStorageKey({
|
|
3357
|
-
persistEnabled,
|
|
3358
|
-
hasExplicitBlockId: Boolean(props.blockId),
|
|
3359
|
-
componentName: "AssessmentSequence"
|
|
3360
|
-
});
|
|
3361
|
-
}, [persistEnabled, props.blockId]);
|
|
3362
|
-
const initialIndex = useCompoundInitialIndex({
|
|
3363
|
-
courseId: config.courseId,
|
|
3364
|
-
compoundId,
|
|
3365
|
-
pageCount: childArray.length,
|
|
3366
|
-
persistEnabled,
|
|
3367
|
-
storage
|
|
3368
|
-
});
|
|
3369
|
-
const [index, setIndex] = useState10(initialIndex);
|
|
3370
|
-
const setIndexStable = useCallback7((i) => setIndex(i), []);
|
|
3371
|
-
useEffect12(() => {
|
|
3372
|
-
setIndex(initialIndex);
|
|
3373
|
-
}, [config.courseId, compoundId, initialIndex]);
|
|
3374
|
-
return /* @__PURE__ */ jsx13(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx13(
|
|
3375
|
-
AssessmentSequenceInner,
|
|
3376
|
-
{
|
|
3377
|
-
...props,
|
|
3378
|
-
ref,
|
|
3379
|
-
compoundId,
|
|
3380
|
-
childArray,
|
|
3381
|
-
index,
|
|
3382
|
-
setIndex,
|
|
3383
|
-
persistEnabled
|
|
3384
|
-
}
|
|
3385
|
-
) });
|
|
3386
|
-
}
|
|
3387
|
-
);
|
|
3388
|
-
setLessonkitBlockType(AssessmentSequence, "AssessmentSequence");
|
|
3389
|
-
|
|
3390
|
-
// src/blocks/Text.tsx
|
|
3391
|
-
import "react";
|
|
3392
|
-
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
3393
|
-
function Text(props) {
|
|
3394
|
-
return /* @__PURE__ */ jsx14("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
|
|
3395
|
-
}
|
|
3396
|
-
setLessonkitBlockType(Text, "Text");
|
|
3397
|
-
|
|
3398
|
-
// src/blocks/Heading.tsx
|
|
3399
|
-
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
3400
|
-
function Heading(props) {
|
|
3401
|
-
const Tag = `h${props.level}`;
|
|
3402
|
-
return /* @__PURE__ */ jsx15(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
|
|
3403
|
-
}
|
|
3404
|
-
setLessonkitBlockType(Heading, "Heading");
|
|
3405
|
-
|
|
3406
|
-
// src/blocks/Image.tsx
|
|
3407
|
-
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
3408
|
-
function Image(props) {
|
|
3409
|
-
return /* @__PURE__ */ jsx16(
|
|
3410
|
-
"img",
|
|
3411
|
-
{
|
|
3412
|
-
src: props.src,
|
|
3413
|
-
alt: props.alt,
|
|
3414
|
-
"data-lk-block-id": props.blockId,
|
|
3415
|
-
"data-testid": props.blockId ? `image-${props.blockId}` : "image",
|
|
3416
|
-
style: { maxWidth: "100%", height: "auto" }
|
|
3417
|
-
}
|
|
3418
|
-
);
|
|
3419
|
-
}
|
|
3420
|
-
setLessonkitBlockType(Image, "Image");
|
|
3421
|
-
|
|
3422
|
-
// src/blocks/Page.tsx
|
|
3423
|
-
import { useEffect as useEffect13 } from "react";
|
|
3424
|
-
import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3425
|
-
function Page(props) {
|
|
3426
|
-
validateCompoundChildren("Page", props.children);
|
|
3427
|
-
const { track } = useLessonkit();
|
|
3428
|
-
const lessonId = useEnclosingLessonId();
|
|
3429
|
-
useEffect13(() => {
|
|
3430
|
-
if (props.hidden || !lessonId || props.parentType) return;
|
|
3431
|
-
track(
|
|
3432
|
-
"compound_page_viewed",
|
|
3433
|
-
{
|
|
3434
|
-
blockId: props.blockId,
|
|
3435
|
-
pageIndex: props.pageIndex ?? 0,
|
|
3436
|
-
parentType: props.parentType
|
|
3437
|
-
},
|
|
3438
|
-
{ lessonId }
|
|
3439
|
-
);
|
|
3440
|
-
}, [props.hidden, props.pageIndex, props.parentType, props.blockId, lessonId, track]);
|
|
3441
|
-
return /* @__PURE__ */ jsxs10(
|
|
3442
|
-
"section",
|
|
3443
|
-
{
|
|
3444
|
-
"aria-label": props.title ?? "Page",
|
|
3445
|
-
"data-lk-block-id": props.blockId,
|
|
3446
|
-
"data-testid": `page-${props.blockId}`,
|
|
3447
|
-
hidden: props.hidden ? true : void 0,
|
|
3448
|
-
children: [
|
|
3449
|
-
props.title ? /* @__PURE__ */ jsx17("h3", { children: props.title }) : null,
|
|
3450
|
-
/* @__PURE__ */ jsx17(CompoundPageIndexProvider, { pageIndex: props.pageIndex ?? 0, children: /* @__PURE__ */ jsx17("div", { children: props.children }) })
|
|
3451
|
-
]
|
|
3452
|
-
}
|
|
3453
|
-
);
|
|
3454
|
-
}
|
|
3455
|
-
setLessonkitBlockType(Page, "Page");
|
|
3456
|
-
|
|
3457
|
-
// src/blocks/InteractiveBook.tsx
|
|
3458
|
-
import React19, { forwardRef as forwardRef8, useCallback as useCallback8, useEffect as useEffect14, useMemo as useMemo14, useState as useState11 } from "react";
|
|
3459
|
-
import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3460
|
-
var InteractiveBookInner = forwardRef8(
|
|
3461
|
-
function InteractiveBookInner2(props, ref) {
|
|
3462
|
-
const { blockId, pages, index, setIndex, persistEnabled } = props;
|
|
3463
|
-
validateCompoundChildren("InteractiveBook", pages);
|
|
3464
|
-
const { config, track } = useLessonkit();
|
|
3465
|
-
const lessonId = useEnclosingLessonId();
|
|
3466
|
-
const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
|
|
3467
|
-
courseId: config.courseId,
|
|
3468
|
-
compoundId: blockId,
|
|
3469
|
-
pageCount: pages.length,
|
|
3470
|
-
index,
|
|
3471
|
-
setIndex,
|
|
3472
|
-
persistEnabled,
|
|
3473
|
-
ref
|
|
3474
|
-
});
|
|
3475
|
-
const pageTitles = useMemo14(
|
|
3476
|
-
() => pages.map((page) => page.props.title),
|
|
3477
|
-
[pages]
|
|
3478
|
-
);
|
|
3479
|
-
useEffect14(() => {
|
|
3480
|
-
if (!lessonId || pages.length === 0) return;
|
|
3481
|
-
track(
|
|
3482
|
-
"book_page_viewed",
|
|
3483
|
-
{
|
|
3484
|
-
blockId,
|
|
3485
|
-
pageIndex: visibleIndex,
|
|
3486
|
-
pageTitle: pageTitles[visibleIndex]
|
|
3487
|
-
},
|
|
3488
|
-
{ lessonId }
|
|
3489
|
-
);
|
|
3490
|
-
}, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
|
|
3491
|
-
return /* @__PURE__ */ jsxs11("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
|
|
3492
|
-
/* @__PURE__ */ jsx18("h3", { children: props.title }),
|
|
3493
|
-
/* @__PURE__ */ jsxs11("p", { children: [
|
|
3494
|
-
"Page ",
|
|
3495
|
-
progress.current,
|
|
3496
|
-
" of ",
|
|
3497
|
-
progress.total
|
|
3498
|
-
] }),
|
|
3499
|
-
props.showBookScore && ctx ? /* @__PURE__ */ jsxs11("p", { "data-testid": "book-score", children: [
|
|
3500
|
-
"Score: ",
|
|
3501
|
-
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
3502
|
-
" /",
|
|
3503
|
-
" ",
|
|
3504
|
-
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
3505
|
-
] }) : null,
|
|
3506
|
-
/* @__PURE__ */ jsx18("div", { "data-testid": "interactive-book-page", children: pages.map(
|
|
3507
|
-
(page, i) => React19.cloneElement(page, {
|
|
3508
|
-
key: page.key ?? page.props.blockId,
|
|
3509
|
-
hidden: i !== visibleIndex,
|
|
3510
|
-
pageIndex: i,
|
|
3511
|
-
parentType: "InteractiveBook"
|
|
3512
|
-
})
|
|
3513
|
-
) }),
|
|
3514
|
-
/* @__PURE__ */ jsxs11("nav", { "aria-label": "Book navigation", children: [
|
|
3515
|
-
/* @__PURE__ */ jsx18(
|
|
3516
|
-
"button",
|
|
3517
|
-
{
|
|
3518
|
-
type: "button",
|
|
3519
|
-
"data-testid": "book-prev",
|
|
3520
|
-
disabled: visibleIndex === 0 || pages.length === 0,
|
|
3521
|
-
onClick: goPrev,
|
|
3522
|
-
children: "Previous"
|
|
3523
|
-
}
|
|
3524
|
-
),
|
|
3525
|
-
/* @__PURE__ */ jsx18(
|
|
3526
|
-
"button",
|
|
3527
|
-
{
|
|
3528
|
-
type: "button",
|
|
3529
|
-
"data-testid": "book-next",
|
|
3530
|
-
disabled: visibleIndex >= pages.length - 1 || pages.length === 0,
|
|
3531
|
-
onClick: goNext,
|
|
3532
|
-
children: "Next"
|
|
3533
|
-
}
|
|
3534
|
-
)
|
|
3535
|
-
] })
|
|
3536
|
-
] });
|
|
3537
|
-
}
|
|
3538
|
-
);
|
|
3539
|
-
var InteractiveBook = forwardRef8(function InteractiveBook2(props, ref) {
|
|
3540
|
-
const blockId = useMemo14(
|
|
3541
|
-
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3542
|
-
[props.blockId]
|
|
3543
|
-
);
|
|
3544
|
-
const pages = React19.Children.toArray(props.children).filter(
|
|
3545
|
-
React19.isValidElement
|
|
3546
|
-
);
|
|
3547
|
-
const { config, storage } = useLessonkit();
|
|
3548
|
-
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
3549
|
-
const initialIndex = useCompoundInitialIndex({
|
|
3550
|
-
courseId: config.courseId,
|
|
3551
|
-
compoundId: blockId,
|
|
3552
|
-
pageCount: pages.length,
|
|
3553
|
-
persistEnabled,
|
|
3554
|
-
storage
|
|
3555
|
-
});
|
|
3556
|
-
const [index, setIndex] = useState11(initialIndex);
|
|
3557
|
-
const setIndexStable = useCallback8((i) => setIndex(i), []);
|
|
3558
|
-
useEffect14(() => {
|
|
3559
|
-
setIndex(initialIndex);
|
|
3560
|
-
}, [config.courseId, blockId, initialIndex]);
|
|
3561
|
-
return /* @__PURE__ */ jsx18(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx18(
|
|
3562
|
-
InteractiveBookInner,
|
|
3563
|
-
{
|
|
3564
|
-
...props,
|
|
3565
|
-
ref,
|
|
3566
|
-
blockId,
|
|
3567
|
-
pages,
|
|
3568
|
-
index,
|
|
3569
|
-
setIndex,
|
|
3570
|
-
persistEnabled
|
|
3571
|
-
}
|
|
3572
|
-
) });
|
|
3573
|
-
});
|
|
3574
|
-
setLessonkitBlockType(InteractiveBook, "InteractiveBook");
|
|
3575
|
-
|
|
3576
|
-
// src/blocks/Slide.tsx
|
|
3577
|
-
import { useEffect as useEffect15 } from "react";
|
|
3578
|
-
import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3579
|
-
function Slide(props) {
|
|
3580
|
-
validateCompoundChildren("Slide", props.children);
|
|
3581
|
-
const { track } = useLessonkit();
|
|
3582
|
-
const lessonId = useEnclosingLessonId();
|
|
3583
|
-
useEffect15(() => {
|
|
3584
|
-
if (props.hidden || !lessonId || props.parentType) return;
|
|
3585
|
-
track(
|
|
3586
|
-
"compound_page_viewed",
|
|
3587
|
-
{
|
|
3588
|
-
blockId: props.blockId,
|
|
3589
|
-
pageIndex: props.slideIndex ?? 0,
|
|
3590
|
-
parentType: props.parentType
|
|
3591
|
-
},
|
|
3592
|
-
{ lessonId }
|
|
3593
|
-
);
|
|
3594
|
-
}, [props.hidden, props.slideIndex, props.parentType, props.blockId, lessonId, track]);
|
|
3595
|
-
return /* @__PURE__ */ jsxs12(
|
|
3596
|
-
"section",
|
|
3597
|
-
{
|
|
3598
|
-
"aria-label": props.title ?? "Slide",
|
|
3599
|
-
"data-lk-block-id": props.blockId,
|
|
3600
|
-
"data-testid": `slide-${props.blockId}`,
|
|
3601
|
-
hidden: props.hidden ? true : void 0,
|
|
3602
|
-
children: [
|
|
3603
|
-
props.title ? /* @__PURE__ */ jsx19("h3", { children: props.title }) : null,
|
|
3604
|
-
/* @__PURE__ */ jsx19(CompoundPageIndexProvider, { pageIndex: props.slideIndex ?? 0, children: /* @__PURE__ */ jsx19("div", { children: props.children }) })
|
|
3605
|
-
]
|
|
3606
|
-
}
|
|
3607
|
-
);
|
|
3608
|
-
}
|
|
3609
|
-
setLessonkitBlockType(Slide, "Slide");
|
|
3610
|
-
|
|
3611
|
-
// src/blocks/SlideDeck.tsx
|
|
3612
|
-
import React21, { forwardRef as forwardRef9, useCallback as useCallback9, useEffect as useEffect17, useMemo as useMemo15, useRef as useRef14, useState as useState12 } from "react";
|
|
3613
|
-
|
|
3614
|
-
// src/compound/useCompoundKeyboardNav.ts
|
|
3615
|
-
import { useEffect as useEffect16 } from "react";
|
|
3616
|
-
var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT", "BUTTON"]);
|
|
3617
|
-
function isEditableTarget(target) {
|
|
3618
|
-
if (!(target instanceof HTMLElement)) return false;
|
|
3619
|
-
if (INTERACTIVE_TAGS.has(target.tagName)) return true;
|
|
3620
|
-
if (target.isContentEditable) return true;
|
|
3621
|
-
if (target.closest("[role='slider'], [role='listbox'], [data-lk-assessment-interactive]")) {
|
|
3622
|
-
return true;
|
|
3623
|
-
}
|
|
3624
|
-
return false;
|
|
66
|
+
);
|
|
67
|
+
return /* @__PURE__ */ jsx(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs("section", { "aria-label": props.title, children: [
|
|
68
|
+
/* @__PURE__ */ jsx("h1", { children: props.title }),
|
|
69
|
+
/* @__PURE__ */ jsx("div", { children: props.children })
|
|
70
|
+
] }) });
|
|
3625
71
|
}
|
|
3626
|
-
function
|
|
3627
|
-
const
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
72
|
+
function Lesson(props) {
|
|
73
|
+
const lessonId = useMemo(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
74
|
+
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
75
|
+
const { setActiveLesson, config } = useLessonkit();
|
|
76
|
+
const { completeLesson } = useCompletion();
|
|
77
|
+
const lessonMountGenerationRef = useRef(0);
|
|
78
|
+
const liveCourseIdRef = useRef(config.courseId);
|
|
79
|
+
liveCourseIdRef.current = config.courseId;
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const unregister = registerLessonMount(lessonId);
|
|
82
|
+
const generation = ++lessonMountGenerationRef.current;
|
|
83
|
+
const mountedCourseId = config.courseId;
|
|
84
|
+
let effectSurvivedTick = false;
|
|
85
|
+
queueMicrotask(() => {
|
|
86
|
+
queueMicrotask(() => {
|
|
87
|
+
effectSurvivedTick = true;
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
setActiveLesson(lessonId);
|
|
91
|
+
return () => {
|
|
92
|
+
unregister();
|
|
93
|
+
if (getLessonMountCount(lessonId) > 0) {
|
|
3633
94
|
return;
|
|
3634
95
|
}
|
|
3635
|
-
if (
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
}
|
|
3643
|
-
break;
|
|
3644
|
-
case "ArrowLeft":
|
|
3645
|
-
case "ArrowUp":
|
|
3646
|
-
if (visibleIndex > 0) {
|
|
3647
|
-
event.preventDefault();
|
|
3648
|
-
goPrev();
|
|
3649
|
-
}
|
|
3650
|
-
break;
|
|
3651
|
-
case "Home":
|
|
3652
|
-
if (visibleIndex !== 0) {
|
|
3653
|
-
event.preventDefault();
|
|
3654
|
-
setIndex(0);
|
|
3655
|
-
}
|
|
3656
|
-
break;
|
|
3657
|
-
case "End":
|
|
3658
|
-
if (visibleIndex !== pageCount - 1) {
|
|
3659
|
-
event.preventDefault();
|
|
3660
|
-
setIndex(pageCount - 1);
|
|
3661
|
-
}
|
|
3662
|
-
break;
|
|
3663
|
-
default:
|
|
3664
|
-
break;
|
|
3665
|
-
}
|
|
96
|
+
if (!autoComplete) return;
|
|
97
|
+
queueMicrotask(() => {
|
|
98
|
+
if (!effectSurvivedTick) return;
|
|
99
|
+
if (lessonMountGenerationRef.current !== generation) return;
|
|
100
|
+
if (liveCourseIdRef.current !== mountedCourseId) return;
|
|
101
|
+
completeLesson(lessonId, { courseId: mountedCourseId });
|
|
102
|
+
});
|
|
3666
103
|
};
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
104
|
+
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
105
|
+
return /* @__PURE__ */ jsx(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs("article", { "aria-label": props.title, children: [
|
|
106
|
+
/* @__PURE__ */ jsx("h2", { children: props.title }),
|
|
107
|
+
/* @__PURE__ */ jsx("div", { children: props.children })
|
|
108
|
+
] }) });
|
|
3670
109
|
}
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
var SlideDeckInner = forwardRef9(function SlideDeckInner2(props, ref) {
|
|
3675
|
-
const { blockId, slides, index, setIndex, persistEnabled } = props;
|
|
3676
|
-
validateCompoundChildren("SlideDeck", slides);
|
|
3677
|
-
const { config, track } = useLessonkit();
|
|
3678
|
-
const lessonId = useEnclosingLessonId();
|
|
3679
|
-
const containerRef = useRef14(null);
|
|
3680
|
-
const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
|
|
3681
|
-
courseId: config.courseId,
|
|
3682
|
-
compoundId: blockId,
|
|
3683
|
-
pageCount: slides.length,
|
|
3684
|
-
index,
|
|
3685
|
-
setIndex,
|
|
3686
|
-
persistEnabled,
|
|
3687
|
-
ref
|
|
3688
|
-
});
|
|
3689
|
-
const setIndexStable = useCallback9((i) => setIndex(i), [setIndex]);
|
|
3690
|
-
useCompoundKeyboardNav({
|
|
3691
|
-
containerRef,
|
|
3692
|
-
visibleIndex,
|
|
3693
|
-
pageCount: slides.length,
|
|
3694
|
-
goNext,
|
|
3695
|
-
goPrev,
|
|
3696
|
-
setIndex: setIndexStable
|
|
3697
|
-
});
|
|
3698
|
-
const slideTitles = useMemo15(
|
|
3699
|
-
() => slides.map((slide) => slide.props.title),
|
|
3700
|
-
[slides]
|
|
3701
|
-
);
|
|
3702
|
-
useEffect17(() => {
|
|
3703
|
-
if (!lessonId || slides.length === 0) return;
|
|
3704
|
-
track(
|
|
3705
|
-
"slide_viewed",
|
|
3706
|
-
{
|
|
3707
|
-
blockId,
|
|
3708
|
-
slideIndex: visibleIndex,
|
|
3709
|
-
slideTitle: slideTitles[visibleIndex]
|
|
3710
|
-
},
|
|
3711
|
-
{ lessonId }
|
|
3712
|
-
);
|
|
3713
|
-
}, [visibleIndex, blockId, lessonId, slides.length, slideTitles, track]);
|
|
3714
|
-
return /* @__PURE__ */ jsxs13(
|
|
3715
|
-
"section",
|
|
3716
|
-
{
|
|
3717
|
-
ref: containerRef,
|
|
3718
|
-
tabIndex: -1,
|
|
3719
|
-
"aria-label": props.title,
|
|
3720
|
-
"data-testid": "slide-deck",
|
|
3721
|
-
"data-lk-block-id": blockId,
|
|
3722
|
-
children: [
|
|
3723
|
-
/* @__PURE__ */ jsx20("h3", { children: props.title }),
|
|
3724
|
-
/* @__PURE__ */ jsxs13("p", { children: [
|
|
3725
|
-
"Slide ",
|
|
3726
|
-
progress.current,
|
|
3727
|
-
" of ",
|
|
3728
|
-
progress.total
|
|
3729
|
-
] }),
|
|
3730
|
-
props.showDeckScore && ctx ? /* @__PURE__ */ jsxs13("p", { "data-testid": "deck-score", children: [
|
|
3731
|
-
"Score: ",
|
|
3732
|
-
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
3733
|
-
" /",
|
|
3734
|
-
" ",
|
|
3735
|
-
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
3736
|
-
] }) : null,
|
|
3737
|
-
/* @__PURE__ */ jsx20("div", { "data-testid": "slide-deck-slide", children: slides.map(
|
|
3738
|
-
(slide, i) => React21.cloneElement(slide, {
|
|
3739
|
-
key: slide.key ?? slide.props.blockId,
|
|
3740
|
-
hidden: i !== visibleIndex,
|
|
3741
|
-
slideIndex: i,
|
|
3742
|
-
parentType: "SlideDeck"
|
|
3743
|
-
})
|
|
3744
|
-
) }),
|
|
3745
|
-
/* @__PURE__ */ jsxs13("nav", { "aria-label": "Slide navigation", children: [
|
|
3746
|
-
/* @__PURE__ */ jsx20(
|
|
3747
|
-
"button",
|
|
3748
|
-
{
|
|
3749
|
-
type: "button",
|
|
3750
|
-
"data-testid": "slide-prev",
|
|
3751
|
-
disabled: visibleIndex === 0 || slides.length === 0,
|
|
3752
|
-
onClick: goPrev,
|
|
3753
|
-
children: "Previous slide"
|
|
3754
|
-
}
|
|
3755
|
-
),
|
|
3756
|
-
/* @__PURE__ */ jsx20(
|
|
3757
|
-
"button",
|
|
3758
|
-
{
|
|
3759
|
-
type: "button",
|
|
3760
|
-
"data-testid": "slide-next",
|
|
3761
|
-
disabled: visibleIndex >= slides.length - 1 || slides.length === 0,
|
|
3762
|
-
onClick: goNext,
|
|
3763
|
-
children: "Next slide"
|
|
3764
|
-
}
|
|
3765
|
-
)
|
|
3766
|
-
] })
|
|
3767
|
-
]
|
|
3768
|
-
}
|
|
3769
|
-
);
|
|
3770
|
-
});
|
|
3771
|
-
var SlideDeck = forwardRef9(function SlideDeck2(props, ref) {
|
|
3772
|
-
const blockId = useMemo15(
|
|
3773
|
-
() => normalizeComponentId(props.blockId, "blockId"),
|
|
110
|
+
function Scenario(props) {
|
|
111
|
+
const blockId = useMemo(
|
|
112
|
+
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
3774
113
|
[props.blockId]
|
|
3775
114
|
);
|
|
3776
|
-
|
|
3777
|
-
React21.isValidElement
|
|
3778
|
-
);
|
|
3779
|
-
const { config, storage } = useLessonkit();
|
|
3780
|
-
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
3781
|
-
const initialIndex = useCompoundInitialIndex({
|
|
3782
|
-
courseId: config.courseId,
|
|
3783
|
-
compoundId: blockId,
|
|
3784
|
-
pageCount: slides.length,
|
|
3785
|
-
persistEnabled,
|
|
3786
|
-
storage
|
|
3787
|
-
});
|
|
3788
|
-
const [index, setIndex] = useState12(initialIndex);
|
|
3789
|
-
const setIndexStable = useCallback9((i) => setIndex(i), []);
|
|
3790
|
-
useEffect17(() => {
|
|
3791
|
-
setIndex(initialIndex);
|
|
3792
|
-
}, [config.courseId, blockId, initialIndex]);
|
|
3793
|
-
return /* @__PURE__ */ jsx20(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx20(
|
|
3794
|
-
SlideDeckInner,
|
|
3795
|
-
{
|
|
3796
|
-
...props,
|
|
3797
|
-
ref,
|
|
3798
|
-
blockId,
|
|
3799
|
-
slides,
|
|
3800
|
-
index,
|
|
3801
|
-
setIndex,
|
|
3802
|
-
persistEnabled
|
|
3803
|
-
}
|
|
3804
|
-
) });
|
|
3805
|
-
});
|
|
3806
|
-
setLessonkitBlockType(SlideDeck, "SlideDeck");
|
|
3807
|
-
|
|
3808
|
-
// src/blocks/Accordion.tsx
|
|
3809
|
-
import { useId as useId4, useState as useState13 } from "react";
|
|
3810
|
-
import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
3811
|
-
function Accordion(props) {
|
|
3812
|
-
if (isDevEnvironment4()) {
|
|
3813
|
-
validateAccordionSections(props.sections);
|
|
3814
|
-
}
|
|
3815
|
-
const [open, setOpen] = useState13(/* @__PURE__ */ new Set());
|
|
3816
|
-
const { track } = useLessonkit();
|
|
3817
|
-
const lessonId = useEnclosingLessonId();
|
|
3818
|
-
const baseId = useId4();
|
|
3819
|
-
const toggle = (sectionId) => {
|
|
3820
|
-
setOpen((prev) => {
|
|
3821
|
-
const next = new Set(prev);
|
|
3822
|
-
const expanded = !next.has(sectionId);
|
|
3823
|
-
if (expanded) next.add(sectionId);
|
|
3824
|
-
else next.delete(sectionId);
|
|
3825
|
-
track(
|
|
3826
|
-
"accordion_section_toggled",
|
|
3827
|
-
{ blockId: props.blockId, sectionId, expanded },
|
|
3828
|
-
lessonId ? { lessonId } : void 0
|
|
3829
|
-
);
|
|
3830
|
-
return next;
|
|
3831
|
-
});
|
|
3832
|
-
};
|
|
3833
|
-
return /* @__PURE__ */ jsx21("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
|
|
3834
|
-
const expanded = open.has(section.id);
|
|
3835
|
-
const panelId = `${baseId}-${section.id}`;
|
|
3836
|
-
const triggerId = `${baseId}-trigger-${section.id}`;
|
|
3837
|
-
return /* @__PURE__ */ jsxs14("div", { "data-testid": `accordion-section-${section.id}`, children: [
|
|
3838
|
-
/* @__PURE__ */ jsx21("h4", { children: /* @__PURE__ */ jsx21(
|
|
3839
|
-
"button",
|
|
3840
|
-
{
|
|
3841
|
-
id: triggerId,
|
|
3842
|
-
type: "button",
|
|
3843
|
-
"aria-expanded": expanded,
|
|
3844
|
-
"aria-controls": panelId,
|
|
3845
|
-
"data-testid": `accordion-trigger-${section.id}`,
|
|
3846
|
-
onClick: () => toggle(section.id),
|
|
3847
|
-
children: section.title
|
|
3848
|
-
}
|
|
3849
|
-
) }),
|
|
3850
|
-
expanded ? /* @__PURE__ */ jsx21("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
|
|
3851
|
-
] }, section.id);
|
|
3852
|
-
}) });
|
|
3853
|
-
}
|
|
3854
|
-
setLessonkitBlockType(Accordion, "Accordion");
|
|
3855
|
-
|
|
3856
|
-
// src/blocks/DialogCards.tsx
|
|
3857
|
-
import { useState as useState14 } from "react";
|
|
3858
|
-
import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
3859
|
-
function DialogCards(props) {
|
|
3860
|
-
const [index, setIndex] = useState14(0);
|
|
3861
|
-
const [flipped, setFlipped] = useState14(false);
|
|
3862
|
-
const card = props.cards[index];
|
|
3863
|
-
if (!card) return null;
|
|
3864
|
-
return /* @__PURE__ */ jsxs15("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
|
|
3865
|
-
/* @__PURE__ */ jsxs15("p", { children: [
|
|
3866
|
-
"Card ",
|
|
3867
|
-
index + 1,
|
|
3868
|
-
" of ",
|
|
3869
|
-
props.cards.length
|
|
3870
|
-
] }),
|
|
3871
|
-
/* @__PURE__ */ jsx22(
|
|
3872
|
-
"button",
|
|
3873
|
-
{
|
|
3874
|
-
type: "button",
|
|
3875
|
-
"data-testid": "dialog-card-flip",
|
|
3876
|
-
"aria-pressed": flipped,
|
|
3877
|
-
onClick: () => setFlipped((f) => !f),
|
|
3878
|
-
style: { minHeight: "6rem", width: "100%" },
|
|
3879
|
-
children: flipped ? card.back : card.front
|
|
3880
|
-
}
|
|
3881
|
-
),
|
|
3882
|
-
/* @__PURE__ */ jsxs15("nav", { "aria-label": "Card navigation", children: [
|
|
3883
|
-
/* @__PURE__ */ jsx22(
|
|
3884
|
-
"button",
|
|
3885
|
-
{
|
|
3886
|
-
type: "button",
|
|
3887
|
-
"data-testid": "dialog-prev",
|
|
3888
|
-
disabled: index === 0,
|
|
3889
|
-
onClick: () => {
|
|
3890
|
-
setIndex((i) => i - 1);
|
|
3891
|
-
setFlipped(false);
|
|
3892
|
-
},
|
|
3893
|
-
children: "Previous"
|
|
3894
|
-
}
|
|
3895
|
-
),
|
|
3896
|
-
/* @__PURE__ */ jsx22(
|
|
3897
|
-
"button",
|
|
3898
|
-
{
|
|
3899
|
-
type: "button",
|
|
3900
|
-
"data-testid": "dialog-next",
|
|
3901
|
-
disabled: index >= props.cards.length - 1,
|
|
3902
|
-
onClick: () => {
|
|
3903
|
-
setIndex((i) => i + 1);
|
|
3904
|
-
setFlipped(false);
|
|
3905
|
-
},
|
|
3906
|
-
children: "Next"
|
|
3907
|
-
}
|
|
3908
|
-
)
|
|
3909
|
-
] })
|
|
3910
|
-
] });
|
|
115
|
+
return /* @__PURE__ */ jsx("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
3911
116
|
}
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
const
|
|
3919
|
-
const [
|
|
3920
|
-
const
|
|
3921
|
-
const
|
|
3922
|
-
const
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
const next = face === "front" ? "back" : "front";
|
|
3926
|
-
setFace(next);
|
|
3927
|
-
track(
|
|
3928
|
-
"flashcard_flipped",
|
|
3929
|
-
{ blockId: props.blockId, cardIndex: index, face: next },
|
|
3930
|
-
lessonId ? { lessonId } : void 0
|
|
3931
|
-
);
|
|
117
|
+
function Reflection(props) {
|
|
118
|
+
const blockId = useMemo(
|
|
119
|
+
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
120
|
+
[props.blockId]
|
|
121
|
+
);
|
|
122
|
+
const promptId = useId();
|
|
123
|
+
const hintId = useId();
|
|
124
|
+
const [internalValue, setInternalValue] = useState("");
|
|
125
|
+
const isControlled = props.value !== void 0;
|
|
126
|
+
const value = isControlled ? props.value : internalValue;
|
|
127
|
+
const handleChange = (event) => {
|
|
128
|
+
if (!isControlled) setInternalValue(event.target.value);
|
|
129
|
+
props.onChange?.(event.target.value);
|
|
3932
130
|
};
|
|
3933
|
-
return /* @__PURE__ */
|
|
3934
|
-
/* @__PURE__ */
|
|
3935
|
-
props.
|
|
3936
|
-
|
|
3937
|
-
|
|
131
|
+
return /* @__PURE__ */ jsxs("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
|
|
132
|
+
props.prompt ? /* @__PURE__ */ jsx("p", { id: promptId, children: props.prompt }) : null,
|
|
133
|
+
props.hint ? /* @__PURE__ */ jsx("p", { id: hintId, style: visuallyHiddenStyle, children: props.hint }) : null,
|
|
134
|
+
props.children,
|
|
135
|
+
/* @__PURE__ */ jsx(
|
|
136
|
+
"textarea",
|
|
3938
137
|
{
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
setFace("front");
|
|
3945
|
-
},
|
|
3946
|
-
children: "Next card"
|
|
138
|
+
value,
|
|
139
|
+
onChange: handleChange,
|
|
140
|
+
"aria-labelledby": props.prompt ? promptId : void 0,
|
|
141
|
+
"aria-describedby": props.hint ? hintId : void 0,
|
|
142
|
+
"aria-label": props.prompt ? void 0 : "Reflection response"
|
|
3947
143
|
}
|
|
3948
144
|
)
|
|
3949
145
|
] });
|
|
3950
146
|
}
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
props.hotspots.map((h) => /* @__PURE__ */ jsx24(
|
|
3972
|
-
"button",
|
|
3973
|
-
{
|
|
3974
|
-
type: "button",
|
|
3975
|
-
"aria-expanded": active === h.id,
|
|
3976
|
-
"aria-label": h.label,
|
|
3977
|
-
"data-testid": `hotspot-${h.id}`,
|
|
3978
|
-
style: {
|
|
3979
|
-
position: "absolute",
|
|
3980
|
-
left: `${h.x}%`,
|
|
3981
|
-
top: `${h.y}%`,
|
|
3982
|
-
transform: "translate(-50%, -50%)"
|
|
3983
|
-
},
|
|
3984
|
-
onClick: () => open(h.id),
|
|
3985
|
-
children: "+"
|
|
3986
|
-
},
|
|
3987
|
-
h.id
|
|
3988
|
-
))
|
|
3989
|
-
] }),
|
|
3990
|
-
active ? /* @__PURE__ */ jsxs17("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
|
|
3991
|
-
props.hotspots.find((h) => h.id === active)?.content,
|
|
3992
|
-
/* @__PURE__ */ jsx24("button", { type: "button", onClick: () => setActive(null), children: "Close" })
|
|
3993
|
-
] }) : null
|
|
3994
|
-
] });
|
|
3995
|
-
}
|
|
3996
|
-
setLessonkitBlockType(ImageHotspots, "ImageHotspots");
|
|
3997
|
-
|
|
3998
|
-
// src/blocks/ImageSlider.tsx
|
|
3999
|
-
import { useState as useState17 } from "react";
|
|
4000
|
-
import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
4001
|
-
function ImageSlider(props) {
|
|
4002
|
-
const [index, setIndex] = useState17(0);
|
|
4003
|
-
const { track } = useLessonkit();
|
|
4004
|
-
const lessonId = useEnclosingLessonId();
|
|
4005
|
-
const slide = props.slides[index];
|
|
4006
|
-
if (!slide) return null;
|
|
4007
|
-
const goTo = (next) => {
|
|
4008
|
-
setIndex(next);
|
|
4009
|
-
track(
|
|
4010
|
-
"image_slider_changed",
|
|
4011
|
-
{ blockId: props.blockId, slideIndex: next },
|
|
4012
|
-
lessonId ? { lessonId } : void 0
|
|
4013
|
-
);
|
|
4014
|
-
};
|
|
4015
|
-
return /* @__PURE__ */ jsxs18("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
|
|
4016
|
-
/* @__PURE__ */ jsx25("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
|
|
4017
|
-
slide.caption ? /* @__PURE__ */ jsx25("p", { children: slide.caption }) : null,
|
|
4018
|
-
/* @__PURE__ */ jsxs18("nav", { "aria-label": "Slide navigation", children: [
|
|
4019
|
-
/* @__PURE__ */ jsx25(
|
|
4020
|
-
"button",
|
|
4021
|
-
{
|
|
4022
|
-
type: "button",
|
|
4023
|
-
"data-testid": "slider-prev",
|
|
4024
|
-
disabled: index === 0,
|
|
4025
|
-
onClick: () => goTo(index - 1),
|
|
4026
|
-
children: "Previous"
|
|
4027
|
-
}
|
|
4028
|
-
),
|
|
4029
|
-
/* @__PURE__ */ jsxs18("span", { children: [
|
|
4030
|
-
index + 1,
|
|
4031
|
-
" / ",
|
|
4032
|
-
props.slides.length
|
|
4033
|
-
] }),
|
|
4034
|
-
/* @__PURE__ */ jsx25(
|
|
4035
|
-
"button",
|
|
4036
|
-
{
|
|
4037
|
-
type: "button",
|
|
4038
|
-
"data-testid": "slider-next",
|
|
4039
|
-
disabled: index >= props.slides.length - 1,
|
|
4040
|
-
onClick: () => goTo(index + 1),
|
|
4041
|
-
children: "Next"
|
|
4042
|
-
}
|
|
4043
|
-
)
|
|
4044
|
-
] })
|
|
4045
|
-
] });
|
|
4046
|
-
}
|
|
4047
|
-
setLessonkitBlockType(ImageSlider, "ImageSlider");
|
|
4048
|
-
|
|
4049
|
-
// src/blocks/FindHotspot.tsx
|
|
4050
|
-
import { forwardRef as forwardRef10, useEffect as useEffect18, useMemo as useMemo16, useRef as useRef15, useState as useState18 } from "react";
|
|
4051
|
-
import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
4052
|
-
var INTERACTION6 = "findHotspot";
|
|
4053
|
-
function FindHotspotInner(props, ref) {
|
|
4054
|
-
const checkId = useMemo16(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4055
|
-
const [selected, setSelected] = useState18(null);
|
|
4056
|
-
const [checked, setChecked] = useState18(false);
|
|
4057
|
-
const telemetryReplayedRef = useRef15(false);
|
|
4058
|
-
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4059
|
-
const targetIdsKey = props.targets.map((t) => t.id).join("\0");
|
|
4060
|
-
useEffect18(() => {
|
|
4061
|
-
setSelected(null);
|
|
4062
|
-
setChecked(false);
|
|
4063
|
-
telemetryReplayedRef.current = false;
|
|
4064
|
-
}, [checkId, props.correctTargetId, targetIdsKey]);
|
|
4065
|
-
const correct = selected === props.correctTargetId;
|
|
4066
|
-
const replayTelemetry = (nextSelected, nextChecked, nextCorrect) => {
|
|
4067
|
-
if (telemetryReplayedRef.current || !nextChecked || nextSelected === null) return;
|
|
4068
|
-
telemetryReplayedRef.current = true;
|
|
4069
|
-
assessment.answer({
|
|
4070
|
-
checkId,
|
|
4071
|
-
interactionType: INTERACTION6,
|
|
4072
|
-
response: nextSelected,
|
|
4073
|
-
correct: nextCorrect
|
|
4074
|
-
});
|
|
4075
|
-
if (nextCorrect) {
|
|
4076
|
-
assessment.complete({
|
|
4077
|
-
checkId,
|
|
4078
|
-
interactionType: INTERACTION6,
|
|
4079
|
-
score: 1,
|
|
4080
|
-
maxScore: 1,
|
|
4081
|
-
passingScore: props.passingScore ?? 1
|
|
4082
|
-
});
|
|
4083
|
-
}
|
|
4084
|
-
};
|
|
4085
|
-
const handle = useMemo16(
|
|
4086
|
-
() => buildAssessmentHandle({
|
|
4087
|
-
checkId,
|
|
4088
|
-
getScore: () => checked && correct ? 1 : 0,
|
|
4089
|
-
getMaxScore: () => 1,
|
|
4090
|
-
getAnswerGiven: () => selected !== null,
|
|
4091
|
-
resetTask: () => {
|
|
4092
|
-
setSelected(null);
|
|
4093
|
-
setChecked(false);
|
|
4094
|
-
telemetryReplayedRef.current = false;
|
|
4095
|
-
},
|
|
4096
|
-
showSolutions: () => setSelected(props.correctTargetId),
|
|
4097
|
-
getXAPIData: () => ({
|
|
4098
|
-
checkId,
|
|
4099
|
-
interactionType: INTERACTION6,
|
|
4100
|
-
response: selected ?? void 0,
|
|
4101
|
-
correct: checked ? correct : void 0,
|
|
4102
|
-
score: checked && correct ? 1 : 0,
|
|
4103
|
-
maxScore: 1
|
|
4104
|
-
}),
|
|
4105
|
-
getCurrentState: () => ({ selected, checked }),
|
|
4106
|
-
resume: (state) => {
|
|
4107
|
-
let nextSelected = selected;
|
|
4108
|
-
const rawSelected = readStringField(state, "selected");
|
|
4109
|
-
if (typeof rawSelected === "string" || rawSelected === null) {
|
|
4110
|
-
const valid = rawSelected === null || props.targets.some((t) => t.id === rawSelected);
|
|
4111
|
-
nextSelected = valid ? rawSelected : null;
|
|
4112
|
-
setSelected(nextSelected);
|
|
4113
|
-
}
|
|
4114
|
-
let nextChecked = checked;
|
|
4115
|
-
readBooleanStateField(state, "checked", (value) => {
|
|
4116
|
-
nextChecked = value;
|
|
4117
|
-
setChecked(value);
|
|
4118
|
-
});
|
|
4119
|
-
const nextCorrect = nextSelected === props.correctTargetId;
|
|
4120
|
-
replayTelemetry(nextSelected, nextChecked, nextCorrect);
|
|
4121
|
-
}
|
|
4122
|
-
}),
|
|
4123
|
-
[assessment, checkId, checked, correct, props.correctTargetId, props.passingScore, props.targets, selected]
|
|
4124
|
-
);
|
|
4125
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
4126
|
-
const selectTarget = (id) => {
|
|
4127
|
-
setSelected(id);
|
|
4128
|
-
setChecked(false);
|
|
4129
|
-
};
|
|
4130
|
-
const submit = () => {
|
|
4131
|
-
if (!selected || checked) return;
|
|
4132
|
-
setChecked(true);
|
|
4133
|
-
assessment.answer({
|
|
4134
|
-
checkId,
|
|
4135
|
-
interactionType: INTERACTION6,
|
|
4136
|
-
response: selected,
|
|
4137
|
-
correct
|
|
4138
|
-
});
|
|
4139
|
-
if (correct) {
|
|
4140
|
-
assessment.complete({
|
|
4141
|
-
checkId,
|
|
4142
|
-
interactionType: INTERACTION6,
|
|
4143
|
-
score: 1,
|
|
4144
|
-
maxScore: 1,
|
|
4145
|
-
passingScore: props.passingScore ?? 1
|
|
4146
|
-
});
|
|
4147
|
-
}
|
|
4148
|
-
};
|
|
4149
|
-
return /* @__PURE__ */ jsxs19("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
|
|
4150
|
-
/* @__PURE__ */ jsxs19("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
4151
|
-
/* @__PURE__ */ jsx26("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
4152
|
-
props.targets.map((t) => /* @__PURE__ */ jsx26(
|
|
4153
|
-
"button",
|
|
4154
|
-
{
|
|
4155
|
-
type: "button",
|
|
4156
|
-
"aria-label": t.label,
|
|
4157
|
-
"aria-pressed": selected === t.id,
|
|
4158
|
-
"data-testid": `target-${t.id}`,
|
|
4159
|
-
style: {
|
|
4160
|
-
position: "absolute",
|
|
4161
|
-
left: `${t.x}%`,
|
|
4162
|
-
top: `${t.y}%`,
|
|
4163
|
-
transform: "translate(-50%, -50%)"
|
|
4164
|
-
},
|
|
4165
|
-
onClick: () => selectTarget(t.id),
|
|
4166
|
-
children: t.label
|
|
4167
|
-
},
|
|
4168
|
-
t.id
|
|
4169
|
-
))
|
|
4170
|
-
] }),
|
|
4171
|
-
/* @__PURE__ */ jsx26("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
|
|
4172
|
-
checked ? /* @__PURE__ */ jsx26("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
4173
|
-
] });
|
|
4174
|
-
}
|
|
4175
|
-
var FindHotspotInnerForwarded = forwardRef10(FindHotspotInner);
|
|
4176
|
-
var FindHotspot = forwardRef10(function FindHotspot2(props, ref) {
|
|
4177
|
-
return /* @__PURE__ */ jsx26(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx26(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
4178
|
-
});
|
|
4179
|
-
setLessonkitBlockType(FindHotspot, "FindHotspot");
|
|
4180
|
-
|
|
4181
|
-
// src/blocks/FindMultipleHotspots.tsx
|
|
4182
|
-
import { forwardRef as forwardRef11, useMemo as useMemo17, useState as useState19 } from "react";
|
|
4183
|
-
import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
4184
|
-
var INTERACTION7 = "findMultipleHotspots";
|
|
4185
|
-
function FindMultipleHotspotsInner(props, ref) {
|
|
4186
|
-
const checkId = useMemo17(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
4187
|
-
const [selected, setSelected] = useState19(/* @__PURE__ */ new Set());
|
|
4188
|
-
const [checked, setChecked] = useState19(false);
|
|
4189
|
-
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
4190
|
-
const toggle = (id) => {
|
|
4191
|
-
setSelected((prev) => {
|
|
4192
|
-
const next = new Set(prev);
|
|
4193
|
-
if (next.has(id)) next.delete(id);
|
|
4194
|
-
else next.add(id);
|
|
4195
|
-
return next;
|
|
4196
|
-
});
|
|
4197
|
-
setChecked(false);
|
|
4198
|
-
};
|
|
4199
|
-
const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
|
|
4200
|
-
const handle = useMemo17(
|
|
4201
|
-
() => buildAssessmentHandle({
|
|
4202
|
-
checkId,
|
|
4203
|
-
getScore: () => checked && correct ? 1 : 0,
|
|
4204
|
-
getMaxScore: () => 1,
|
|
4205
|
-
getAnswerGiven: () => selected.size > 0,
|
|
4206
|
-
resetTask: () => {
|
|
4207
|
-
setSelected(/* @__PURE__ */ new Set());
|
|
4208
|
-
setChecked(false);
|
|
4209
|
-
},
|
|
4210
|
-
showSolutions: () => setSelected(new Set(props.correctTargetIds)),
|
|
4211
|
-
getXAPIData: () => ({
|
|
4212
|
-
checkId,
|
|
4213
|
-
interactionType: INTERACTION7,
|
|
4214
|
-
response: [...selected],
|
|
4215
|
-
correct: checked ? correct : void 0,
|
|
4216
|
-
score: checked && correct ? 1 : 0,
|
|
4217
|
-
maxScore: 1
|
|
4218
|
-
}),
|
|
4219
|
-
getCurrentState: () => ({ selected: [...selected], checked }),
|
|
4220
|
-
resume: (state) => {
|
|
4221
|
-
const raw = state.selected;
|
|
4222
|
-
if (Array.isArray(raw)) setSelected(new Set(raw.filter((id) => typeof id === "string")));
|
|
4223
|
-
readBooleanStateField(state, "checked", setChecked);
|
|
147
|
+
function ProgressTracker(props) {
|
|
148
|
+
const { progress } = useLessonkit();
|
|
149
|
+
const completed = progress.completedLessonIds.size;
|
|
150
|
+
if (props.totalLessons != null) {
|
|
151
|
+
const total = props.totalLessons;
|
|
152
|
+
const displayed = Math.min(completed, total);
|
|
153
|
+
return /* @__PURE__ */ jsx("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx(
|
|
154
|
+
"div",
|
|
155
|
+
{
|
|
156
|
+
role: "progressbar",
|
|
157
|
+
"aria-valuemin": 0,
|
|
158
|
+
"aria-valuemax": total,
|
|
159
|
+
"aria-valuenow": displayed,
|
|
160
|
+
"aria-label": "Lessons completed",
|
|
161
|
+
children: /* @__PURE__ */ jsxs("p", { children: [
|
|
162
|
+
"Lessons completed: ",
|
|
163
|
+
displayed,
|
|
164
|
+
" of ",
|
|
165
|
+
total
|
|
166
|
+
] })
|
|
4224
167
|
}
|
|
4225
|
-
})
|
|
4226
|
-
[checkId, selected, checked, correct, props.correctTargetIds]
|
|
4227
|
-
);
|
|
4228
|
-
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
4229
|
-
const submit = () => {
|
|
4230
|
-
if (selected.size === 0 || checked) return;
|
|
4231
|
-
setChecked(true);
|
|
4232
|
-
assessment.answer({
|
|
4233
|
-
checkId,
|
|
4234
|
-
interactionType: INTERACTION7,
|
|
4235
|
-
response: [...selected],
|
|
4236
|
-
correct
|
|
4237
|
-
});
|
|
4238
|
-
if (correct) {
|
|
4239
|
-
assessment.complete({
|
|
4240
|
-
checkId,
|
|
4241
|
-
interactionType: INTERACTION7,
|
|
4242
|
-
score: 1,
|
|
4243
|
-
maxScore: 1,
|
|
4244
|
-
passingScore: props.passingScore ?? 1
|
|
4245
|
-
});
|
|
4246
|
-
}
|
|
4247
|
-
};
|
|
4248
|
-
return /* @__PURE__ */ jsxs20("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
|
|
4249
|
-
/* @__PURE__ */ jsxs20("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
4250
|
-
/* @__PURE__ */ jsx27("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
4251
|
-
props.targets.map((t) => /* @__PURE__ */ jsx27(
|
|
4252
|
-
"button",
|
|
4253
|
-
{
|
|
4254
|
-
type: "button",
|
|
4255
|
-
"aria-label": t.label,
|
|
4256
|
-
"aria-pressed": selected.has(t.id),
|
|
4257
|
-
"data-testid": `target-${t.id}`,
|
|
4258
|
-
style: {
|
|
4259
|
-
position: "absolute",
|
|
4260
|
-
left: `${t.x}%`,
|
|
4261
|
-
top: `${t.y}%`,
|
|
4262
|
-
transform: "translate(-50%, -50%)"
|
|
4263
|
-
},
|
|
4264
|
-
onClick: () => toggle(t.id),
|
|
4265
|
-
children: t.label
|
|
4266
|
-
},
|
|
4267
|
-
t.id
|
|
4268
|
-
))
|
|
4269
|
-
] }),
|
|
4270
|
-
/* @__PURE__ */ jsx27("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
|
|
4271
|
-
checked ? /* @__PURE__ */ jsx27("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
4272
|
-
] });
|
|
4273
|
-
}
|
|
4274
|
-
var FindMultipleHotspotsInnerForwarded = forwardRef11(FindMultipleHotspotsInner);
|
|
4275
|
-
var FindMultipleHotspots = forwardRef11(
|
|
4276
|
-
function FindMultipleHotspots2(props, ref) {
|
|
4277
|
-
return /* @__PURE__ */ jsx27(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx27(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
168
|
+
) });
|
|
4278
169
|
}
|
|
4279
|
-
|
|
4280
|
-
|
|
170
|
+
return /* @__PURE__ */ jsx("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs("p", { children: [
|
|
171
|
+
"Lessons completed: ",
|
|
172
|
+
completed
|
|
173
|
+
] }) });
|
|
174
|
+
}
|
|
4281
175
|
|
|
4282
176
|
// src/index.tsx
|
|
4283
177
|
import {
|
|
4284
|
-
buildTelemetryEvent
|
|
4285
|
-
createLessonkitRuntime
|
|
4286
|
-
createPluginRegistry
|
|
4287
|
-
createTelemetryPipeline
|
|
178
|
+
buildTelemetryEvent,
|
|
179
|
+
createLessonkitRuntime,
|
|
180
|
+
createPluginRegistry,
|
|
181
|
+
createTelemetryPipeline,
|
|
4288
182
|
defineAssessmentPlugin,
|
|
4289
183
|
defineLifecyclePlugin,
|
|
4290
184
|
defineTelemetryPlugin
|
|
4291
185
|
} from "@lessonkit/core";
|
|
4292
186
|
|
|
4293
187
|
// src/theme/ThemeProvider.tsx
|
|
4294
|
-
import
|
|
4295
|
-
createContext
|
|
4296
|
-
useCallback
|
|
4297
|
-
useContext
|
|
4298
|
-
useLayoutEffect
|
|
4299
|
-
useMemo as
|
|
4300
|
-
useRef as
|
|
4301
|
-
useState as
|
|
188
|
+
import React2, {
|
|
189
|
+
createContext,
|
|
190
|
+
useCallback,
|
|
191
|
+
useContext,
|
|
192
|
+
useLayoutEffect,
|
|
193
|
+
useMemo as useMemo2,
|
|
194
|
+
useRef as useRef2,
|
|
195
|
+
useState as useState2
|
|
4302
196
|
} from "react";
|
|
4303
197
|
import {
|
|
4304
198
|
brandThemeOverrides,
|
|
@@ -4325,11 +219,11 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
4325
219
|
}
|
|
4326
220
|
|
|
4327
221
|
// src/theme/ThemeProvider.tsx
|
|
4328
|
-
import { jsx as
|
|
4329
|
-
var ThemeContext =
|
|
4330
|
-
var
|
|
222
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
223
|
+
var ThemeContext = createContext(null);
|
|
224
|
+
var useIsoLayoutEffect = (
|
|
4331
225
|
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
4332
|
-
typeof window !== "undefined" ?
|
|
226
|
+
typeof window !== "undefined" ? useLayoutEffect : React2.useEffect
|
|
4333
227
|
);
|
|
4334
228
|
function getSystemMode() {
|
|
4335
229
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
@@ -4348,10 +242,10 @@ function ThemeProvider(props) {
|
|
|
4348
242
|
const preset = props.preset ?? "default";
|
|
4349
243
|
const mode = props.mode ?? "light";
|
|
4350
244
|
const targetKind = props.target ?? "document";
|
|
4351
|
-
const [resolvedMode, setResolvedMode] =
|
|
245
|
+
const [resolvedMode, setResolvedMode] = useState2(
|
|
4352
246
|
() => mode === "system" ? getSystemMode() : mode
|
|
4353
247
|
);
|
|
4354
|
-
|
|
248
|
+
useIsoLayoutEffect(() => {
|
|
4355
249
|
if (mode !== "system") {
|
|
4356
250
|
setResolvedMode(mode);
|
|
4357
251
|
return;
|
|
@@ -4364,26 +258,26 @@ function ThemeProvider(props) {
|
|
|
4364
258
|
return () => mq.removeEventListener("change", onChange);
|
|
4365
259
|
}, [mode]);
|
|
4366
260
|
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
4367
|
-
const effectiveTheme =
|
|
261
|
+
const effectiveTheme = useMemo2(() => {
|
|
4368
262
|
const modeBase = resolveModeBase(mode, dataTheme);
|
|
4369
263
|
const base = preset === "default" ? modeBase : preset === "brand" ? mergeThemes(modeBase, brandThemeOverrides) : mergeThemes(modeBase, getPresetTheme(preset));
|
|
4370
264
|
return mergeThemes(base, props.theme ?? {});
|
|
4371
265
|
}, [preset, mode, dataTheme, props.theme]);
|
|
4372
|
-
const hostRef =
|
|
4373
|
-
const appliedKeysRef =
|
|
4374
|
-
|
|
266
|
+
const hostRef = useRef2(null);
|
|
267
|
+
const appliedKeysRef = useRef2(/* @__PURE__ */ new Set());
|
|
268
|
+
useIsoLayoutEffect(() => {
|
|
4375
269
|
if (targetKind === "document" && typeof document !== "undefined") {
|
|
4376
270
|
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
4377
271
|
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
4378
272
|
}
|
|
4379
273
|
}, [targetKind, dataTheme]);
|
|
4380
|
-
const inject =
|
|
274
|
+
const inject = useCallback(() => {
|
|
4381
275
|
const vars = themeToCssVariables(effectiveTheme);
|
|
4382
276
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
4383
277
|
if (!el) return;
|
|
4384
278
|
appliedKeysRef.current = applyCssVariables(el, vars, appliedKeysRef.current);
|
|
4385
279
|
}, [effectiveTheme, targetKind]);
|
|
4386
|
-
|
|
280
|
+
useIsoLayoutEffect(() => {
|
|
4387
281
|
inject();
|
|
4388
282
|
return () => {
|
|
4389
283
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
@@ -4394,7 +288,7 @@ function ThemeProvider(props) {
|
|
|
4394
288
|
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
4395
289
|
};
|
|
4396
290
|
}, [inject, targetKind]);
|
|
4397
|
-
const value =
|
|
291
|
+
const value = useMemo2(
|
|
4398
292
|
() => ({
|
|
4399
293
|
theme: effectiveTheme,
|
|
4400
294
|
preset,
|
|
@@ -4404,12 +298,12 @@ function ThemeProvider(props) {
|
|
|
4404
298
|
[effectiveTheme, preset, mode, dataTheme]
|
|
4405
299
|
);
|
|
4406
300
|
if (targetKind === "document") {
|
|
4407
|
-
return /* @__PURE__ */
|
|
301
|
+
return /* @__PURE__ */ jsx2(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx2("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
|
|
4408
302
|
}
|
|
4409
|
-
return /* @__PURE__ */
|
|
303
|
+
return /* @__PURE__ */ jsx2(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx2("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
4410
304
|
}
|
|
4411
305
|
function useTheme() {
|
|
4412
|
-
const ctx =
|
|
306
|
+
const ctx = useContext(ThemeContext);
|
|
4413
307
|
if (!ctx) {
|
|
4414
308
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
4415
309
|
}
|
|
@@ -4420,10 +314,12 @@ function useTheme() {
|
|
|
4420
314
|
import {
|
|
4421
315
|
ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
4422
316
|
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
317
|
+
INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
|
|
4423
318
|
PAGE_ALLOWED_CHILD_TYPES,
|
|
4424
319
|
SLIDE_ALLOWED_CHILD_TYPES,
|
|
4425
320
|
SLIDE_DECK_ALLOWED_CHILD_TYPES,
|
|
4426
|
-
|
|
321
|
+
TIMED_CUE_ALLOWED_CHILD_TYPES,
|
|
322
|
+
COMPOUND_MAX_NESTING_DEPTH
|
|
4427
323
|
} from "@lessonkit/core";
|
|
4428
324
|
var COMPOUND_PARENTS = [
|
|
4429
325
|
"Lesson",
|
|
@@ -4431,6 +327,8 @@ var COMPOUND_PARENTS = [
|
|
|
4431
327
|
"InteractiveBook",
|
|
4432
328
|
"Slide",
|
|
4433
329
|
"SlideDeck",
|
|
330
|
+
"TimedCue",
|
|
331
|
+
"InteractiveVideo",
|
|
4434
332
|
"AssessmentSequence"
|
|
4435
333
|
];
|
|
4436
334
|
function extendParents(entry) {
|
|
@@ -4489,6 +387,23 @@ var v3CompoundAndContentEntries = [
|
|
|
4489
387
|
theming: { surface: "global-inherit", stylingNotes: "Responsive max-width." },
|
|
4490
388
|
telemetry: { emits: [] }
|
|
4491
389
|
},
|
|
390
|
+
{
|
|
391
|
+
type: "Video",
|
|
392
|
+
category: "content",
|
|
393
|
+
description: "Self-hosted video with native controls and optional captions.",
|
|
394
|
+
props: [
|
|
395
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
396
|
+
{ name: "src", type: "string", required: true, description: "Video URL." },
|
|
397
|
+
{ name: "poster", type: "string", required: false, description: "Poster image URL." },
|
|
398
|
+
{ name: "captions", type: "string", required: false, description: "WebVTT captions URL." },
|
|
399
|
+
{ name: "title", type: "string", required: false, description: "Accessible title." }
|
|
400
|
+
],
|
|
401
|
+
requiredIds: ["blockId"],
|
|
402
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
403
|
+
a11y: { element: "video", ariaLabel: "Video", keyboard: "Native video controls.", notes: "No autoplay with sound." },
|
|
404
|
+
theming: { surface: "global-inherit", stylingNotes: "Responsive video." },
|
|
405
|
+
telemetry: { emits: [] }
|
|
406
|
+
},
|
|
4492
407
|
{
|
|
4493
408
|
type: "Page",
|
|
4494
409
|
category: "container",
|
|
@@ -4497,7 +412,7 @@ var v3CompoundAndContentEntries = [
|
|
|
4497
412
|
h5pAlias: "Column",
|
|
4498
413
|
description: "Column layout container (H5P Column / Page).",
|
|
4499
414
|
allowedChildTypes: [...PAGE_ALLOWED_CHILD_TYPES],
|
|
4500
|
-
maxNestingDepth:
|
|
415
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.Page,
|
|
4501
416
|
props: [
|
|
4502
417
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4503
418
|
{ name: "title", type: "string", required: false, description: "Page title." },
|
|
@@ -4518,7 +433,7 @@ var v3CompoundAndContentEntries = [
|
|
|
4518
433
|
h5pAlias: "Interactive Book",
|
|
4519
434
|
description: "Multi-page book with chapter navigation.",
|
|
4520
435
|
allowedChildTypes: [...INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
|
|
4521
|
-
maxNestingDepth:
|
|
436
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.InteractiveBook,
|
|
4522
437
|
props: [
|
|
4523
438
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4524
439
|
{ name: "title", type: "string", required: true, description: "Book title." },
|
|
@@ -4542,9 +457,9 @@ var v3CompoundAndContentEntries = [
|
|
|
4542
457
|
compoundContract: true,
|
|
4543
458
|
h5pMachineName: "H5P.CoursePresentation",
|
|
4544
459
|
h5pAlias: "Course Presentation slide",
|
|
4545
|
-
description: "Single slide row in a SlideDeck.
|
|
460
|
+
description: "Single slide row in a SlideDeck. Supports Video, Summary, and 1.4 blocks.",
|
|
4546
461
|
allowedChildTypes: [...SLIDE_ALLOWED_CHILD_TYPES],
|
|
4547
|
-
maxNestingDepth:
|
|
462
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.Slide,
|
|
4548
463
|
props: [
|
|
4549
464
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4550
465
|
{ name: "title", type: "string", required: false, description: "Slide title." },
|
|
@@ -4565,7 +480,7 @@ var v3CompoundAndContentEntries = [
|
|
|
4565
480
|
h5pAlias: "Course Presentation",
|
|
4566
481
|
description: "Multi-slide presentation with keyboard navigation.",
|
|
4567
482
|
allowedChildTypes: [...SLIDE_DECK_ALLOWED_CHILD_TYPES],
|
|
4568
|
-
maxNestingDepth:
|
|
483
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.SlideDeck,
|
|
4569
484
|
props: [
|
|
4570
485
|
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4571
486
|
{ name: "title", type: "string", required: true, description: "Deck title." },
|
|
@@ -4583,6 +498,121 @@ var v3CompoundAndContentEntries = [
|
|
|
4583
498
|
theming: { surface: "global-inherit", stylingNotes: "Deck chrome." },
|
|
4584
499
|
telemetry: { emits: ["slide_viewed"], requiresActiveLesson: true }
|
|
4585
500
|
},
|
|
501
|
+
{
|
|
502
|
+
type: "TimedCue",
|
|
503
|
+
category: "container",
|
|
504
|
+
compoundContract: true,
|
|
505
|
+
h5pMachineName: "H5P.InteractiveVideo",
|
|
506
|
+
h5pAlias: "Interactive Video timed cue",
|
|
507
|
+
description: "Timed overlay cue within InteractiveVideo.",
|
|
508
|
+
allowedChildTypes: [...TIMED_CUE_ALLOWED_CHILD_TYPES],
|
|
509
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.TimedCue,
|
|
510
|
+
props: [
|
|
511
|
+
{ name: "atSeconds", type: "number", required: true, description: "Cue time in seconds." },
|
|
512
|
+
{ name: "label", type: "string", required: false, description: "Cue label." },
|
|
513
|
+
{ name: "mustComplete", type: "boolean", required: false, description: "Block seek until completed." },
|
|
514
|
+
{ name: "children", type: "ReactNode", required: true, description: "Single allowed child block." }
|
|
515
|
+
],
|
|
516
|
+
requiredIds: [],
|
|
517
|
+
parentConstraints: ["InteractiveVideo"],
|
|
518
|
+
a11y: { element: "dialog", ariaLabel: "Timed cue", keyboard: "Focus moves to overlay content.", notes: "Pauses parent video." },
|
|
519
|
+
theming: { surface: "global-inherit", stylingNotes: "Overlay panel." },
|
|
520
|
+
telemetry: { emits: [] }
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
type: "InteractiveVideo",
|
|
524
|
+
category: "container",
|
|
525
|
+
compoundContract: true,
|
|
526
|
+
h5pMachineName: "H5P.InteractiveVideo",
|
|
527
|
+
h5pAlias: "Interactive Video",
|
|
528
|
+
description: "Video with timed interaction overlays.",
|
|
529
|
+
allowedChildTypes: [...INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES],
|
|
530
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.InteractiveVideo,
|
|
531
|
+
props: [
|
|
532
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
533
|
+
{ name: "title", type: "string", required: true, description: "Video title." },
|
|
534
|
+
{ name: "src", type: "string", required: true, description: "Video URL." },
|
|
535
|
+
{ name: "poster", type: "string", required: false, description: "Poster image." },
|
|
536
|
+
{ name: "captions", type: "string", required: false, description: "WebVTT captions." },
|
|
537
|
+
{ name: "showVideoScore", type: "boolean", required: false, description: "Show aggregate score." },
|
|
538
|
+
{ name: "children", type: "TimedCue[]", required: true, description: "Timed cues." }
|
|
539
|
+
],
|
|
540
|
+
requiredIds: ["blockId"],
|
|
541
|
+
parentConstraints: ["Lesson"],
|
|
542
|
+
a11y: {
|
|
543
|
+
element: "section",
|
|
544
|
+
ariaLabel: "Interactive video",
|
|
545
|
+
keyboard: "Native video controls; overlay interactions when paused.",
|
|
546
|
+
notes: "H5P Interactive Video equivalent."
|
|
547
|
+
},
|
|
548
|
+
theming: { surface: "global-inherit", stylingNotes: "Video + overlay." },
|
|
549
|
+
telemetry: { emits: ["video_cue_reached", "video_segment_completed"], requiresActiveLesson: true }
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
type: "Questionnaire",
|
|
553
|
+
category: "content",
|
|
554
|
+
h5pMachineName: "H5P.Questionnaire",
|
|
555
|
+
h5pAlias: "Questionnaire",
|
|
556
|
+
description: "Unscored multi-field survey.",
|
|
557
|
+
props: [
|
|
558
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
559
|
+
{ name: "fields", type: "QuestionnaireField[]", required: true, description: "Form fields." }
|
|
560
|
+
],
|
|
561
|
+
requiredIds: ["blockId"],
|
|
562
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
563
|
+
a11y: { element: "form", ariaLabel: "Questionnaire", keyboard: "Tab through fields.", notes: "Unscored survey." },
|
|
564
|
+
theming: { surface: "global-inherit", stylingNotes: "Form layout." },
|
|
565
|
+
telemetry: { emits: ["questionnaire_submitted"], requiresActiveLesson: true }
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
type: "MemoryGame",
|
|
569
|
+
category: "content",
|
|
570
|
+
h5pMachineName: "H5P.MemoryGame",
|
|
571
|
+
h5pAlias: "Memory Game",
|
|
572
|
+
description: "Card flip memory matching game.",
|
|
573
|
+
props: [
|
|
574
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
575
|
+
{ name: "pairs", type: "MemoryPair[]", required: true, description: "Card pairs." },
|
|
576
|
+
{ name: "selfScore", type: "boolean", required: false, description: "Optional self-score mode." }
|
|
577
|
+
],
|
|
578
|
+
requiredIds: ["blockId"],
|
|
579
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
580
|
+
a11y: { element: "section", ariaLabel: "Memory game", keyboard: "Flip cards with buttons.", notes: "Reduced motion safe." },
|
|
581
|
+
theming: { surface: "global-inherit", stylingNotes: "Card grid." },
|
|
582
|
+
telemetry: { emits: ["memory_card_flipped"] }
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
type: "InformationWall",
|
|
586
|
+
category: "content",
|
|
587
|
+
h5pMachineName: "H5P.InformationWall",
|
|
588
|
+
h5pAlias: "Information Wall",
|
|
589
|
+
description: "Searchable information panels.",
|
|
590
|
+
props: [
|
|
591
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
592
|
+
{ name: "panels", type: "InformationPanel[]", required: true, description: "Content panels." }
|
|
593
|
+
],
|
|
594
|
+
requiredIds: ["blockId"],
|
|
595
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
596
|
+
a11y: { element: "section", ariaLabel: "Information wall", keyboard: "Search and browse panels.", notes: "Filterable grid." },
|
|
597
|
+
theming: { surface: "global-inherit", stylingNotes: "Panel grid." },
|
|
598
|
+
telemetry: { emits: ["information_wall_search"] }
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
type: "ParallaxSlideshow",
|
|
602
|
+
category: "content",
|
|
603
|
+
h5pMachineName: "H5P.ImpressivePresentation",
|
|
604
|
+
h5pAlias: "Slideshow (parallax)",
|
|
605
|
+
description: "Slideshow with parallax; static fallback when reduced motion.",
|
|
606
|
+
props: [
|
|
607
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
608
|
+
{ name: "slides", type: "ParallaxSlide[]", required: true, description: "Slides." }
|
|
609
|
+
],
|
|
610
|
+
requiredIds: ["blockId"],
|
|
611
|
+
parentConstraints: [...COMPOUND_PARENTS],
|
|
612
|
+
a11y: { element: "section", ariaLabel: "Slideshow", keyboard: "Previous/next slide.", notes: "prefers-reduced-motion fallback." },
|
|
613
|
+
theming: { surface: "global-inherit", stylingNotes: "Slide deck." },
|
|
614
|
+
telemetry: { emits: ["parallax_slide_viewed"] }
|
|
615
|
+
},
|
|
4586
616
|
{
|
|
4587
617
|
type: "Accordion",
|
|
4588
618
|
category: "content",
|
|
@@ -4717,7 +747,7 @@ function buildV3CatalogFromV2(v2) {
|
|
|
4717
747
|
...base,
|
|
4718
748
|
compoundContract: true,
|
|
4719
749
|
allowedChildTypes: [...ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
|
|
4720
|
-
maxNestingDepth:
|
|
750
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.AssessmentSequence
|
|
4721
751
|
};
|
|
4722
752
|
}
|
|
4723
753
|
return base;
|
|
@@ -5064,6 +1094,100 @@ var v2AssessmentEntries = [
|
|
|
5064
1094
|
},
|
|
5065
1095
|
theming: { surface: "global-inherit", stylingNotes: "Container for assessments." },
|
|
5066
1096
|
telemetry: { emits: [], manualTracking: "Child assessments emit assessment_* events." }
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
type: "Summary",
|
|
1100
|
+
category: "assessment",
|
|
1101
|
+
assessmentContract: true,
|
|
1102
|
+
h5pMachineName: "H5P.Summary",
|
|
1103
|
+
h5pAlias: "Summary",
|
|
1104
|
+
description: "Construct a summary from a statement bank in correct order.",
|
|
1105
|
+
props: [
|
|
1106
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
1107
|
+
{ name: "statements", type: "string[]", required: true, description: "Available statements." },
|
|
1108
|
+
{ name: "correct", type: "string[]", required: true, description: "Correct ordered summary." },
|
|
1109
|
+
...assessmentBehaviourProps2
|
|
1110
|
+
],
|
|
1111
|
+
requiredIds: ["checkId"],
|
|
1112
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
1113
|
+
a11y: { element: "section", ariaLabel: "Summary", keyboard: "Select statements in order.", notes: "H5P Summary equivalent." },
|
|
1114
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
1115
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
type: "ImagePairing",
|
|
1119
|
+
category: "assessment",
|
|
1120
|
+
assessmentContract: true,
|
|
1121
|
+
h5pMachineName: "H5P.ImagePair",
|
|
1122
|
+
h5pAlias: "Image Pairing",
|
|
1123
|
+
description: "Match image pairs in a memory-style task.",
|
|
1124
|
+
props: [
|
|
1125
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
1126
|
+
{ name: "pairs", type: "ImagePair[]", required: true, description: "Image pairs to match." },
|
|
1127
|
+
...assessmentBehaviourProps2
|
|
1128
|
+
],
|
|
1129
|
+
requiredIds: ["checkId"],
|
|
1130
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
1131
|
+
a11y: { element: "section", ariaLabel: "Image Pairing", keyboard: "Select two cards to match.", notes: "H5P Image Pairing equivalent." },
|
|
1132
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
1133
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
type: "ImageSequencing",
|
|
1137
|
+
category: "assessment",
|
|
1138
|
+
assessmentContract: true,
|
|
1139
|
+
h5pMachineName: "H5P.ImageSequencing",
|
|
1140
|
+
h5pAlias: "Image Sequencing",
|
|
1141
|
+
description: "Order images in the correct sequence.",
|
|
1142
|
+
props: [
|
|
1143
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
1144
|
+
{ name: "images", type: "SequencingImage[]", required: true, description: "Images to order." },
|
|
1145
|
+
{ name: "correctOrder", type: "string[]", required: true, description: "Correct id order." },
|
|
1146
|
+
...assessmentBehaviourProps2
|
|
1147
|
+
],
|
|
1148
|
+
requiredIds: ["checkId"],
|
|
1149
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
1150
|
+
a11y: { element: "section", ariaLabel: "Image Sequencing", keyboard: "Reorder with up/down.", notes: "H5P Image Sequencing equivalent." },
|
|
1151
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
1152
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
1153
|
+
},
|
|
1154
|
+
{
|
|
1155
|
+
type: "ArithmeticQuiz",
|
|
1156
|
+
category: "assessment",
|
|
1157
|
+
assessmentContract: true,
|
|
1158
|
+
h5pMachineName: "H5P.ArithmeticQuiz",
|
|
1159
|
+
h5pAlias: "Arithmetic Quiz",
|
|
1160
|
+
description: "Timed arithmetic problems with optional timer.",
|
|
1161
|
+
props: [
|
|
1162
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
1163
|
+
{ name: "problems", type: "ArithmeticProblem[]", required: true, description: "Math problems." },
|
|
1164
|
+
{ name: "timeLimitSeconds", type: "number", required: false, description: "Optional time limit." },
|
|
1165
|
+
...assessmentBehaviourProps2
|
|
1166
|
+
],
|
|
1167
|
+
requiredIds: ["checkId"],
|
|
1168
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
1169
|
+
a11y: { element: "section", ariaLabel: "Arithmetic Quiz", keyboard: "Text input per problem.", notes: "H5P Arithmetic Quiz equivalent." },
|
|
1170
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
1171
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
type: "Essay",
|
|
1175
|
+
category: "assessment",
|
|
1176
|
+
assessmentContract: true,
|
|
1177
|
+
h5pMachineName: "H5P.Essay",
|
|
1178
|
+
h5pAlias: "Essay",
|
|
1179
|
+
description: "Open text response; manual or plugin grading.",
|
|
1180
|
+
props: [
|
|
1181
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
1182
|
+
{ name: "question", type: "string", required: true, description: "Essay prompt." },
|
|
1183
|
+
{ name: "minLength", type: "number", required: false, description: "Minimum character length." },
|
|
1184
|
+
...assessmentBehaviourProps2
|
|
1185
|
+
],
|
|
1186
|
+
requiredIds: ["checkId"],
|
|
1187
|
+
parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
|
|
1188
|
+
a11y: { element: "section", ariaLabel: "Essay", keyboard: "Textarea input.", notes: "H5P Essay equivalent." },
|
|
1189
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
1190
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
5067
1191
|
}
|
|
5068
1192
|
];
|
|
5069
1193
|
var BLOCK_CATALOG_V2 = [
|
|
@@ -5102,6 +1226,7 @@ function getBlockCatalogEntry(type, opts) {
|
|
|
5102
1226
|
}
|
|
5103
1227
|
export {
|
|
5104
1228
|
Accordion,
|
|
1229
|
+
ArithmeticQuiz,
|
|
5105
1230
|
AssessmentSequence,
|
|
5106
1231
|
BLOCK_CATALOG,
|
|
5107
1232
|
BLOCK_CATALOG_V2,
|
|
@@ -5110,6 +1235,7 @@ export {
|
|
|
5110
1235
|
DialogCards,
|
|
5111
1236
|
DragAndDrop,
|
|
5112
1237
|
DragTheWords,
|
|
1238
|
+
Essay,
|
|
5113
1239
|
FillInTheBlanks,
|
|
5114
1240
|
FindHotspot,
|
|
5115
1241
|
FindMultipleHotspots,
|
|
@@ -5117,36 +1243,48 @@ export {
|
|
|
5117
1243
|
Heading,
|
|
5118
1244
|
Image,
|
|
5119
1245
|
ImageHotspots,
|
|
1246
|
+
ImagePairing,
|
|
1247
|
+
ImageSequencing,
|
|
5120
1248
|
ImageSlider,
|
|
1249
|
+
InformationWall,
|
|
5121
1250
|
InteractiveBook,
|
|
1251
|
+
InteractiveVideo,
|
|
5122
1252
|
KnowledgeCheck,
|
|
5123
1253
|
Lesson,
|
|
5124
1254
|
LessonkitProvider,
|
|
5125
1255
|
MarkTheWords,
|
|
1256
|
+
MemoryGame,
|
|
5126
1257
|
Page,
|
|
1258
|
+
ParallaxSlideshow,
|
|
5127
1259
|
ProgressTracker,
|
|
1260
|
+
Questionnaire,
|
|
5128
1261
|
Quiz,
|
|
5129
1262
|
Reflection,
|
|
5130
1263
|
Scenario,
|
|
5131
1264
|
Slide,
|
|
5132
1265
|
SlideDeck,
|
|
1266
|
+
Summary,
|
|
5133
1267
|
Text,
|
|
5134
1268
|
ThemeProvider,
|
|
1269
|
+
TimedCue,
|
|
5135
1270
|
TrueFalse,
|
|
1271
|
+
Video,
|
|
1272
|
+
assertProductionCourseConfig,
|
|
5136
1273
|
blockCatalogV2Version,
|
|
5137
1274
|
blockCatalogV3Version,
|
|
5138
1275
|
blockCatalogVersion,
|
|
5139
1276
|
buildBlockCatalog,
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
1277
|
+
buildTelemetryEvent,
|
|
1278
|
+
createLessonkitRuntime,
|
|
1279
|
+
createPluginRegistry,
|
|
1280
|
+
createTelemetryPipeline,
|
|
5144
1281
|
defineAssessmentPlugin,
|
|
5145
1282
|
defineLifecyclePlugin,
|
|
5146
1283
|
defineTelemetryPlugin,
|
|
5147
1284
|
getBlockCatalogEntry,
|
|
5148
1285
|
resetAssessmentWarningsForTests,
|
|
5149
1286
|
resetQuizWarningsForTests,
|
|
1287
|
+
shouldEnforceProductionGuard,
|
|
5150
1288
|
useAssessmentState,
|
|
5151
1289
|
useCompletion,
|
|
5152
1290
|
useLessonkit,
|