@lessonkit/react 1.0.1 → 1.1.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.v2.json +770 -0
- package/block-contract.v2.json +104 -0
- package/dist/index.cjs +1282 -109
- package/dist/index.d.cts +122 -11
- package/dist/index.d.ts +122 -11
- package/dist/index.js +1263 -100
- package/package.json +13 -9
package/dist/index.cjs
CHANGED
|
@@ -30,16 +30,24 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.tsx
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
AssessmentSequence: () => AssessmentSequence,
|
|
33
34
|
BLOCK_CATALOG: () => BLOCK_CATALOG,
|
|
35
|
+
BLOCK_CATALOG_V2: () => BLOCK_CATALOG_V2,
|
|
34
36
|
Course: () => Course,
|
|
37
|
+
DragAndDrop: () => DragAndDrop,
|
|
38
|
+
DragTheWords: () => DragTheWords,
|
|
39
|
+
FillInTheBlanks: () => FillInTheBlanks,
|
|
35
40
|
KnowledgeCheck: () => KnowledgeCheck,
|
|
36
41
|
Lesson: () => Lesson,
|
|
37
42
|
LessonkitProvider: () => LessonkitProvider,
|
|
43
|
+
MarkTheWords: () => MarkTheWords,
|
|
38
44
|
ProgressTracker: () => ProgressTracker,
|
|
39
45
|
Quiz: () => Quiz,
|
|
40
46
|
Reflection: () => Reflection,
|
|
41
47
|
Scenario: () => Scenario,
|
|
42
48
|
ThemeProvider: () => ThemeProvider,
|
|
49
|
+
TrueFalse: () => TrueFalse,
|
|
50
|
+
blockCatalogV2Version: () => blockCatalogV2Version,
|
|
43
51
|
blockCatalogVersion: () => blockCatalogVersion,
|
|
44
52
|
buildBlockCatalog: () => buildBlockCatalog,
|
|
45
53
|
buildTelemetryEvent: () => import_core10.buildTelemetryEvent,
|
|
@@ -50,7 +58,9 @@ __export(index_exports, {
|
|
|
50
58
|
defineLifecyclePlugin: () => import_core10.defineLifecyclePlugin,
|
|
51
59
|
defineTelemetryPlugin: () => import_core10.defineTelemetryPlugin,
|
|
52
60
|
getBlockCatalogEntry: () => getBlockCatalogEntry,
|
|
61
|
+
resetAssessmentWarningsForTests: () => resetAssessmentWarningsForTests,
|
|
53
62
|
resetQuizWarningsForTests: () => resetQuizWarningsForTests,
|
|
63
|
+
useAssessmentState: () => useAssessmentState,
|
|
54
64
|
useCompletion: () => useCompletion,
|
|
55
65
|
useLessonkit: () => useLessonkit,
|
|
56
66
|
useProgress: () => useProgress,
|
|
@@ -61,9 +71,32 @@ __export(index_exports, {
|
|
|
61
71
|
module.exports = __toCommonJS(index_exports);
|
|
62
72
|
|
|
63
73
|
// src/components.tsx
|
|
64
|
-
var
|
|
74
|
+
var import_react6 = require("react");
|
|
65
75
|
var import_accessibility = require("@lessonkit/accessibility");
|
|
66
76
|
|
|
77
|
+
// src/assessment/scoring.ts
|
|
78
|
+
function resolvePassingThreshold(passingScore, maxScore) {
|
|
79
|
+
return passingScore ?? maxScore;
|
|
80
|
+
}
|
|
81
|
+
function meetsPassingThreshold(score, maxScore, passingScore) {
|
|
82
|
+
const threshold = resolvePassingThreshold(passingScore, maxScore);
|
|
83
|
+
return score >= threshold;
|
|
84
|
+
}
|
|
85
|
+
function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
|
|
86
|
+
const maxScore = custom?.maxScore ?? fallbackMax;
|
|
87
|
+
if (custom?.passed !== void 0) {
|
|
88
|
+
const score2 = custom.passed ? custom.score ?? maxScore : custom.score ?? 0;
|
|
89
|
+
return { score: score2, maxScore, passed: custom.passed };
|
|
90
|
+
}
|
|
91
|
+
if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
92
|
+
const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
|
|
93
|
+
return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
|
|
94
|
+
}
|
|
95
|
+
const score = fallbackCorrect ? maxScore : 0;
|
|
96
|
+
const passed = meetsPassingThreshold(score, maxScore, passingScore);
|
|
97
|
+
return { score, maxScore, passed };
|
|
98
|
+
}
|
|
99
|
+
|
|
67
100
|
// src/context.tsx
|
|
68
101
|
var import_react2 = require("react");
|
|
69
102
|
|
|
@@ -171,7 +204,40 @@ var import_core5 = require("@lessonkit/core");
|
|
|
171
204
|
|
|
172
205
|
// src/runtime/courseStartedPipeline.ts
|
|
173
206
|
var import_xapi3 = require("@lessonkit/xapi");
|
|
174
|
-
function
|
|
207
|
+
function isDevEnvironment3() {
|
|
208
|
+
const g = globalThis;
|
|
209
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
210
|
+
}
|
|
211
|
+
function warnExtraSinkFailure(sinkId, err) {
|
|
212
|
+
if (isDevEnvironment3()) {
|
|
213
|
+
console.warn(
|
|
214
|
+
`[lessonkit] course_started extra sink "${sinkId}" failed:`,
|
|
215
|
+
err instanceof Error ? err.message : err
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function emitExtraSinks(sinks, event, emitCtx) {
|
|
220
|
+
await Promise.all(
|
|
221
|
+
sinks.map(async (sink) => {
|
|
222
|
+
let result;
|
|
223
|
+
try {
|
|
224
|
+
result = sink.emit(event, emitCtx);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
warnExtraSinkFailure(sink.id, err);
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
if (result != null && typeof result.then === "function") {
|
|
230
|
+
try {
|
|
231
|
+
await result;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
warnExtraSinkFailure(sink.id, err);
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
async function emitCourseStartedNonTrackingPipeline(opts) {
|
|
175
241
|
let xapiStatementSent = false;
|
|
176
242
|
if (!opts.skipXapi && opts.xapi) {
|
|
177
243
|
const statement = (0, import_xapi3.telemetryEventToXAPIStatement)(opts.event);
|
|
@@ -186,9 +252,7 @@ function emitCourseStartedNonTrackingPipeline(opts) {
|
|
|
186
252
|
sessionId: opts.event.sessionId,
|
|
187
253
|
attemptId: opts.event.attemptId
|
|
188
254
|
};
|
|
189
|
-
|
|
190
|
-
sink.emit(opts.event, emitCtx);
|
|
191
|
-
}
|
|
255
|
+
await emitExtraSinks(opts.extraSinks ?? [], opts.event, emitCtx);
|
|
192
256
|
return { xapiStatementSent };
|
|
193
257
|
}
|
|
194
258
|
|
|
@@ -238,8 +302,12 @@ async function disposeTrackingClient(client) {
|
|
|
238
302
|
}
|
|
239
303
|
|
|
240
304
|
// src/provider/useLessonkitProviderRuntime.ts
|
|
241
|
-
var useIsoLayoutEffect =
|
|
305
|
+
var useIsoLayoutEffect = (
|
|
306
|
+
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
307
|
+
typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect
|
|
308
|
+
);
|
|
242
309
|
var defaultStorage = (0, import_core3.createSessionStoragePort)();
|
|
310
|
+
var courseStartedTrackingFlightKey = null;
|
|
243
311
|
function isTrackingActive(tracking) {
|
|
244
312
|
return tracking?.enabled !== false;
|
|
245
313
|
}
|
|
@@ -262,15 +330,41 @@ function buildCourseStartedEvent(opts) {
|
|
|
262
330
|
});
|
|
263
331
|
return opts.pluginHost ? opts.pluginHost.runTelemetry(built, pluginCtx) : built;
|
|
264
332
|
}
|
|
265
|
-
function
|
|
333
|
+
async function emitCourseStartedToTracking(tracking, storage, sessionId, courseId, event, shouldCommit) {
|
|
334
|
+
const flightKey = `${sessionId}:${courseId}`;
|
|
335
|
+
if ((0, import_core5.hasCourseStartedEmittedToTracking)(storage, sessionId, courseId)) {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
if (courseStartedTrackingFlightKey === flightKey) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
courseStartedTrackingFlightKey = flightKey;
|
|
342
|
+
try {
|
|
343
|
+
if (shouldCommit && !shouldCommit()) return false;
|
|
344
|
+
tracking.track(event);
|
|
345
|
+
await tracking.flush?.();
|
|
346
|
+
if (shouldCommit && !shouldCommit()) return false;
|
|
347
|
+
(0, import_core5.markCourseStartedEmittedToTracking)(storage, sessionId, courseId);
|
|
348
|
+
return true;
|
|
349
|
+
} catch {
|
|
350
|
+
return false;
|
|
351
|
+
} finally {
|
|
352
|
+
if (courseStartedTrackingFlightKey === flightKey) {
|
|
353
|
+
courseStartedTrackingFlightKey = null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async function emitCourseStartedPipelineOnly(opts) {
|
|
266
358
|
try {
|
|
267
|
-
|
|
359
|
+
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
360
|
+
const { xapiStatementSent } = await emitCourseStartedNonTrackingPipeline({
|
|
268
361
|
event: opts.event,
|
|
269
362
|
xapi: opts.xapi,
|
|
270
363
|
lxpackBridge: opts.lxpackBridge,
|
|
271
364
|
extraSinks: opts.extraSinks,
|
|
272
365
|
skipXapi: opts.skipXapi
|
|
273
366
|
});
|
|
367
|
+
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
274
368
|
(0, import_core5.markCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
|
|
275
369
|
(0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId);
|
|
276
370
|
if (xapiStatementSent) {
|
|
@@ -281,47 +375,41 @@ function emitCourseStartedPipelineOnly(opts) {
|
|
|
281
375
|
return "failed";
|
|
282
376
|
}
|
|
283
377
|
}
|
|
284
|
-
function emitCourseStarted(opts) {
|
|
378
|
+
async function emitCourseStarted(opts) {
|
|
285
379
|
const event = buildCourseStartedEvent(opts);
|
|
286
380
|
if (event === null) return "filtered";
|
|
287
|
-
const
|
|
381
|
+
const tracked = await emitCourseStartedToTracking(
|
|
382
|
+
opts.tracking,
|
|
288
383
|
opts.storage,
|
|
289
384
|
opts.sessionId,
|
|
290
|
-
opts.courseId
|
|
385
|
+
opts.courseId,
|
|
386
|
+
event,
|
|
387
|
+
opts.shouldCommit
|
|
291
388
|
);
|
|
292
|
-
if (!
|
|
293
|
-
try {
|
|
294
|
-
opts.tracking.track(event);
|
|
295
|
-
(0, import_core5.markCourseStartedEmittedToTracking)(opts.storage, opts.sessionId, opts.courseId);
|
|
296
|
-
} catch {
|
|
297
|
-
return "failed";
|
|
298
|
-
}
|
|
299
|
-
}
|
|
389
|
+
if (!tracked) return "failed";
|
|
300
390
|
return emitCourseStartedPipelineOnly({
|
|
301
391
|
...opts,
|
|
302
392
|
event,
|
|
303
393
|
skipXapi: opts.skipXapi,
|
|
304
|
-
onXapiStatementSent: opts.onXapiStatementSent
|
|
394
|
+
onXapiStatementSent: opts.onXapiStatementSent,
|
|
395
|
+
shouldCommit: opts.shouldCommit
|
|
305
396
|
});
|
|
306
397
|
}
|
|
307
|
-
function emitCourseStartedToTrackingOnly(opts) {
|
|
398
|
+
async function emitCourseStartedToTrackingOnly(opts) {
|
|
308
399
|
const event = buildCourseStartedEvent(opts);
|
|
309
400
|
if (event === null) return "filtered";
|
|
310
|
-
const
|
|
401
|
+
const tracked = await emitCourseStartedToTracking(
|
|
402
|
+
opts.tracking,
|
|
311
403
|
opts.storage,
|
|
312
404
|
opts.sessionId,
|
|
313
|
-
opts.courseId
|
|
405
|
+
opts.courseId,
|
|
406
|
+
event,
|
|
407
|
+
opts.shouldCommit
|
|
314
408
|
);
|
|
315
|
-
if (!
|
|
316
|
-
try {
|
|
317
|
-
opts.tracking.track(event);
|
|
318
|
-
(0, import_core5.markCourseStartedEmittedToTracking)(opts.storage, opts.sessionId, opts.courseId);
|
|
319
|
-
} catch {
|
|
320
|
-
return "failed";
|
|
321
|
-
}
|
|
322
|
-
}
|
|
409
|
+
if (!tracked) return "failed";
|
|
323
410
|
try {
|
|
324
|
-
|
|
411
|
+
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
412
|
+
await emitCourseStartedNonTrackingPipeline({
|
|
325
413
|
event,
|
|
326
414
|
xapi: null,
|
|
327
415
|
lxpackBridge: opts.lxpackBridge,
|
|
@@ -334,7 +422,7 @@ function emitCourseStartedToTrackingOnly(opts) {
|
|
|
334
422
|
return "failed";
|
|
335
423
|
}
|
|
336
424
|
}
|
|
337
|
-
function emitPendingCourseStarted(opts) {
|
|
425
|
+
async function emitPendingCourseStarted(opts) {
|
|
338
426
|
const trackingEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
|
|
339
427
|
opts.storage,
|
|
340
428
|
opts.sessionId,
|
|
@@ -357,6 +445,9 @@ function emitPendingCourseStarted(opts) {
|
|
|
357
445
|
opts.sessionId,
|
|
358
446
|
opts.courseId
|
|
359
447
|
);
|
|
448
|
+
if (sessionStarted && trackingEmitted && pipelineDelivered) {
|
|
449
|
+
return "emitted";
|
|
450
|
+
}
|
|
360
451
|
if (sessionStarted && trackingEmitted && !pipelineDelivered) {
|
|
361
452
|
const event = buildCourseStartedEvent(opts);
|
|
362
453
|
if (event === null) return "filtered";
|
|
@@ -408,6 +499,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
408
499
|
pluginHostRef.current = pluginHost;
|
|
409
500
|
const progressRef = (0, import_react.useRef)((0, import_core4.createProgressController)());
|
|
410
501
|
const courseStartedEmittedToSinkRef = (0, import_react.useRef)(false);
|
|
502
|
+
const courseStartedEmitGenerationRef = (0, import_react.useRef)(0);
|
|
411
503
|
const prevCourseIdForProgressRef = (0, import_react.useRef)(normalizedCourseId);
|
|
412
504
|
const pendingCourseIdResetRef = (0, import_react.useRef)(false);
|
|
413
505
|
const prevUseV2RuntimeRef = (0, import_react.useRef)(useV2Runtime);
|
|
@@ -427,6 +519,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
427
519
|
}
|
|
428
520
|
pendingCourseIdResetRef.current = true;
|
|
429
521
|
courseStartedEmittedToSinkRef.current = false;
|
|
522
|
+
courseStartedEmitGenerationRef.current += 1;
|
|
430
523
|
} else if (useV2Runtime && !headlessRef.current) {
|
|
431
524
|
headlessRef.current = (0, import_core8.createLessonkitRuntime)({
|
|
432
525
|
courseId: normalizedCourseId,
|
|
@@ -444,6 +537,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
444
537
|
}
|
|
445
538
|
pendingCourseIdResetRef.current = true;
|
|
446
539
|
courseStartedEmittedToSinkRef.current = false;
|
|
540
|
+
courseStartedEmitGenerationRef.current += 1;
|
|
447
541
|
}
|
|
448
542
|
if (useV2Runtime && headlessRef.current) {
|
|
449
543
|
progressRef.current = headlessRef.current.progress;
|
|
@@ -557,7 +651,10 @@ function useLessonkitProviderRuntime(config) {
|
|
|
557
651
|
const baseSink = normalizedConfig.tracking?.sink;
|
|
558
652
|
const userBatchSink = normalizedConfig.tracking?.batchSink;
|
|
559
653
|
assertTrackingSinkConfig(normalizedConfig.tracking);
|
|
560
|
-
const sink = pluginHostRef.current && baseSink ?
|
|
654
|
+
const sink = pluginHostRef.current && baseSink ? (
|
|
655
|
+
/* v8 ignore next -- composeTrackingSink may return null; fall back to base sink */
|
|
656
|
+
pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink
|
|
657
|
+
) : baseSink;
|
|
561
658
|
const batchSink = pluginHostRef.current && userBatchSink ? async (events) => {
|
|
562
659
|
const host = pluginHostRef.current;
|
|
563
660
|
const ctx = buildCurrentPluginCtx();
|
|
@@ -581,30 +678,39 @@ function useLessonkitProviderRuntime(config) {
|
|
|
581
678
|
const sessionId = sessionIdRef.current;
|
|
582
679
|
const cid = courseIdRef.current;
|
|
583
680
|
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
681
|
+
const courseStartedFullySettled = (0, import_core5.hasCourseStartedEmittedToTracking)(defaultStorage, sessionId, cid) && (0, import_core5.hasCourseStarted)(defaultStorage, sessionId, cid) && (0, import_core5.hasCourseStartedPipelineDelivered)(defaultStorage, sessionId, cid);
|
|
584
682
|
if (!trackingActive) {
|
|
585
683
|
courseStartedEmittedToSinkRef.current = false;
|
|
586
|
-
} else if (
|
|
587
|
-
const result = emitPendingCourseStarted({
|
|
588
|
-
pluginHost: pluginHostRef.current,
|
|
589
|
-
tracking: next,
|
|
590
|
-
xapi: xapiRef.current,
|
|
591
|
-
storage: defaultStorage,
|
|
592
|
-
sessionId,
|
|
593
|
-
courseId: cid,
|
|
594
|
-
attemptId: attemptIdRef.current,
|
|
595
|
-
user: userRef.current,
|
|
596
|
-
lxpackBridge: lxpackBridgeModeRef.current,
|
|
597
|
-
extraSinks: extraSinksRef.current,
|
|
598
|
-
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
599
|
-
onXapiStatementSent: () => {
|
|
600
|
-
xapiCourseStartedSentOnClientRef.current = true;
|
|
601
|
-
}
|
|
602
|
-
});
|
|
603
|
-
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
604
|
-
} else if (trackingActive) {
|
|
684
|
+
} else if (courseStartedFullySettled) {
|
|
605
685
|
courseStartedEmittedToSinkRef.current = true;
|
|
686
|
+
} else if (!courseStartedEmittedToSinkRef.current) {
|
|
687
|
+
const generation = ++courseStartedEmitGenerationRef.current;
|
|
688
|
+
const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
|
|
689
|
+
void (async () => {
|
|
690
|
+
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
691
|
+
const result = await emitPendingCourseStarted({
|
|
692
|
+
pluginHost: pluginHostRef.current,
|
|
693
|
+
tracking: next,
|
|
694
|
+
xapi: xapiRef.current,
|
|
695
|
+
storage: defaultStorage,
|
|
696
|
+
sessionId,
|
|
697
|
+
courseId: cid,
|
|
698
|
+
attemptId: attemptIdRef.current,
|
|
699
|
+
user: userRef.current,
|
|
700
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
701
|
+
extraSinks: extraSinksRef.current,
|
|
702
|
+
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
703
|
+
onXapiStatementSent: () => {
|
|
704
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
705
|
+
},
|
|
706
|
+
shouldCommit
|
|
707
|
+
});
|
|
708
|
+
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
709
|
+
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
710
|
+
})();
|
|
606
711
|
}
|
|
607
712
|
return () => {
|
|
713
|
+
courseStartedEmitGenerationRef.current += 1;
|
|
608
714
|
if (prev !== trackingRef.current) {
|
|
609
715
|
void disposeTrackingClient(prev);
|
|
610
716
|
}
|
|
@@ -681,7 +787,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
681
787
|
} catch {
|
|
682
788
|
}
|
|
683
789
|
if (!courseStartedEmittedToSinkRef.current) {
|
|
684
|
-
const result = emitPendingCourseStarted({
|
|
790
|
+
const result = await emitPendingCourseStarted({
|
|
685
791
|
pluginHost: pluginHostRef.current,
|
|
686
792
|
tracking: trackingRef.current,
|
|
687
793
|
xapi: xapiRef.current,
|
|
@@ -707,7 +813,10 @@ function useLessonkitProviderRuntime(config) {
|
|
|
707
813
|
[track]
|
|
708
814
|
);
|
|
709
815
|
const completeLesson = (0, import_react.useCallback)(
|
|
710
|
-
(lessonId) => {
|
|
816
|
+
(lessonId, opts) => {
|
|
817
|
+
if (opts?.courseId !== void 0 && opts.courseId !== courseIdRef.current) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
711
820
|
if (useV2Runtime && headlessRef.current) {
|
|
712
821
|
headlessRef.current.completeLesson(lessonId, emitLifecycleEvent);
|
|
713
822
|
syncProgress();
|
|
@@ -882,9 +991,29 @@ function LessonkitProvider(props) {
|
|
|
882
991
|
}
|
|
883
992
|
|
|
884
993
|
// src/hooks.ts
|
|
994
|
+
var import_react4 = require("react");
|
|
995
|
+
|
|
996
|
+
// src/assessment/useAssessmentState.ts
|
|
885
997
|
var import_react3 = require("react");
|
|
998
|
+
function useAssessmentState(enclosingLessonId) {
|
|
999
|
+
const { track } = useLessonkit();
|
|
1000
|
+
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
1001
|
+
return (0, import_react3.useMemo)(
|
|
1002
|
+
() => ({
|
|
1003
|
+
answer: (data) => {
|
|
1004
|
+
track("assessment_answered", data, trackOpts);
|
|
1005
|
+
},
|
|
1006
|
+
complete: (data) => {
|
|
1007
|
+
track("assessment_completed", data, trackOpts);
|
|
1008
|
+
}
|
|
1009
|
+
}),
|
|
1010
|
+
[track, enclosingLessonId]
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/hooks.ts
|
|
886
1015
|
function useLessonkit() {
|
|
887
|
-
const ctx = (0,
|
|
1016
|
+
const ctx = (0, import_react4.useContext)(LessonkitContext);
|
|
888
1017
|
if (!ctx) throw new Error("LessonKit: missing LessonkitProvider");
|
|
889
1018
|
return ctx;
|
|
890
1019
|
}
|
|
@@ -894,16 +1023,16 @@ function useProgress() {
|
|
|
894
1023
|
}
|
|
895
1024
|
function useTracking() {
|
|
896
1025
|
const { track } = useLessonkit();
|
|
897
|
-
return (0,
|
|
1026
|
+
return (0, import_react4.useMemo)(() => ({ track }), [track]);
|
|
898
1027
|
}
|
|
899
1028
|
function useCompletion() {
|
|
900
1029
|
const { completeLesson, completeCourse } = useLessonkit();
|
|
901
|
-
return (0,
|
|
1030
|
+
return (0, import_react4.useMemo)(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
|
|
902
1031
|
}
|
|
903
1032
|
function useQuizState(enclosingLessonId) {
|
|
904
1033
|
const { track } = useLessonkit();
|
|
905
1034
|
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
906
|
-
return (0,
|
|
1035
|
+
return (0, import_react4.useMemo)(
|
|
907
1036
|
() => ({
|
|
908
1037
|
answer: (opts) => {
|
|
909
1038
|
track("quiz_answered", opts, trackOpts);
|
|
@@ -917,15 +1046,15 @@ function useQuizState(enclosingLessonId) {
|
|
|
917
1046
|
}
|
|
918
1047
|
|
|
919
1048
|
// src/lessonContext.tsx
|
|
920
|
-
var
|
|
921
|
-
var LessonContext = (0,
|
|
1049
|
+
var import_react5 = require("react");
|
|
1050
|
+
var LessonContext = (0, import_react5.createContext)(void 0);
|
|
922
1051
|
function useEnclosingLessonId() {
|
|
923
|
-
return (0,
|
|
1052
|
+
return (0, import_react5.useContext)(LessonContext);
|
|
924
1053
|
}
|
|
925
1054
|
|
|
926
1055
|
// src/runtime/validateComponentId.ts
|
|
927
1056
|
var import_core9 = require("@lessonkit/core");
|
|
928
|
-
function
|
|
1057
|
+
function isDevEnvironment4() {
|
|
929
1058
|
const g = globalThis;
|
|
930
1059
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
931
1060
|
}
|
|
@@ -941,7 +1070,7 @@ function normalizeComponentId(id, path) {
|
|
|
941
1070
|
var mountCounts = /* @__PURE__ */ new Map();
|
|
942
1071
|
var warnedConcurrentLessons = false;
|
|
943
1072
|
function registerLessonMount(lessonId) {
|
|
944
|
-
if (
|
|
1073
|
+
if (isDevEnvironment4() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
|
|
945
1074
|
warnedConcurrentLessons = true;
|
|
946
1075
|
console.warn(
|
|
947
1076
|
"[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."
|
|
@@ -968,8 +1097,8 @@ function resetQuizWarningsForTests() {
|
|
|
968
1097
|
warnedQuizOutsideLesson = false;
|
|
969
1098
|
}
|
|
970
1099
|
function Course(props) {
|
|
971
|
-
const courseId = (0,
|
|
972
|
-
const providerConfig = (0,
|
|
1100
|
+
const courseId = (0, import_react6.useMemo)(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
|
|
1101
|
+
const providerConfig = (0, import_react6.useMemo)(
|
|
973
1102
|
() => ({ ...props.config, courseId }),
|
|
974
1103
|
[props.config, courseId]
|
|
975
1104
|
);
|
|
@@ -979,14 +1108,23 @@ function Course(props) {
|
|
|
979
1108
|
] }) });
|
|
980
1109
|
}
|
|
981
1110
|
function Lesson(props) {
|
|
982
|
-
const lessonId = (0,
|
|
1111
|
+
const lessonId = (0, import_react6.useMemo)(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
983
1112
|
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
984
1113
|
const { setActiveLesson, config } = useLessonkit();
|
|
985
1114
|
const { completeLesson } = useCompletion();
|
|
986
|
-
const lessonMountGenerationRef = (0,
|
|
987
|
-
(0,
|
|
1115
|
+
const lessonMountGenerationRef = (0, import_react6.useRef)(0);
|
|
1116
|
+
const liveCourseIdRef = (0, import_react6.useRef)(config.courseId);
|
|
1117
|
+
liveCourseIdRef.current = config.courseId;
|
|
1118
|
+
(0, import_react6.useEffect)(() => {
|
|
988
1119
|
const unregister = registerLessonMount(lessonId);
|
|
989
1120
|
const generation = ++lessonMountGenerationRef.current;
|
|
1121
|
+
const mountedCourseId = config.courseId;
|
|
1122
|
+
let effectSurvivedTick = false;
|
|
1123
|
+
queueMicrotask(() => {
|
|
1124
|
+
queueMicrotask(() => {
|
|
1125
|
+
effectSurvivedTick = true;
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
990
1128
|
setActiveLesson(lessonId);
|
|
991
1129
|
return () => {
|
|
992
1130
|
unregister();
|
|
@@ -995,8 +1133,10 @@ function Lesson(props) {
|
|
|
995
1133
|
}
|
|
996
1134
|
if (!autoComplete) return;
|
|
997
1135
|
queueMicrotask(() => {
|
|
1136
|
+
if (!effectSurvivedTick) return;
|
|
998
1137
|
if (lessonMountGenerationRef.current !== generation) return;
|
|
999
|
-
|
|
1138
|
+
if (liveCourseIdRef.current !== mountedCourseId) return;
|
|
1139
|
+
completeLesson(lessonId, { courseId: mountedCourseId });
|
|
1000
1140
|
});
|
|
1001
1141
|
};
|
|
1002
1142
|
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
@@ -1006,20 +1146,20 @@ function Lesson(props) {
|
|
|
1006
1146
|
] }) });
|
|
1007
1147
|
}
|
|
1008
1148
|
function Scenario(props) {
|
|
1009
|
-
const blockId = (0,
|
|
1149
|
+
const blockId = (0, import_react6.useMemo)(
|
|
1010
1150
|
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1011
1151
|
[props.blockId]
|
|
1012
1152
|
);
|
|
1013
1153
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
1014
1154
|
}
|
|
1015
1155
|
function Reflection(props) {
|
|
1016
|
-
const blockId = (0,
|
|
1156
|
+
const blockId = (0, import_react6.useMemo)(
|
|
1017
1157
|
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1018
1158
|
[props.blockId]
|
|
1019
1159
|
);
|
|
1020
|
-
const promptId = (0,
|
|
1021
|
-
const hintId = (0,
|
|
1022
|
-
const [internalValue, setInternalValue] = (0,
|
|
1160
|
+
const promptId = (0, import_react6.useId)();
|
|
1161
|
+
const hintId = (0, import_react6.useId)();
|
|
1162
|
+
const [internalValue, setInternalValue] = (0, import_react6.useState)("");
|
|
1023
1163
|
const isControlled = props.value !== void 0;
|
|
1024
1164
|
const value = isControlled ? props.value : internalValue;
|
|
1025
1165
|
const handleChange = (event) => {
|
|
@@ -1055,11 +1195,10 @@ function KnowledgeCheck(props) {
|
|
|
1055
1195
|
);
|
|
1056
1196
|
}
|
|
1057
1197
|
function Quiz(props) {
|
|
1058
|
-
const checkId = (0, import_react5.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1059
1198
|
const enclosingLessonId = useEnclosingLessonId();
|
|
1060
1199
|
const missingLesson = enclosingLessonId === void 0;
|
|
1061
|
-
(0,
|
|
1062
|
-
if (!missingLesson ||
|
|
1200
|
+
(0, import_react6.useEffect)(() => {
|
|
1201
|
+
if (!missingLesson || isDevEnvironment4()) return;
|
|
1063
1202
|
if (!warnedQuizOutsideLesson) {
|
|
1064
1203
|
warnedQuizOutsideLesson = true;
|
|
1065
1204
|
console.error(
|
|
@@ -1067,18 +1206,26 @@ function Quiz(props) {
|
|
|
1067
1206
|
);
|
|
1068
1207
|
}
|
|
1069
1208
|
}, [missingLesson]);
|
|
1070
|
-
if (missingLesson &&
|
|
1209
|
+
if (missingLesson && isDevEnvironment4()) {
|
|
1071
1210
|
throw new Error("[lessonkit] <Quiz> must be wrapped in <Lesson>");
|
|
1072
1211
|
}
|
|
1212
|
+
if (missingLesson) {
|
|
1213
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": props.checkId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: "Quiz must be placed inside a Lesson." }) });
|
|
1214
|
+
}
|
|
1215
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(QuizInner, { ...props, enclosingLessonId });
|
|
1216
|
+
}
|
|
1217
|
+
function QuizInner(props) {
|
|
1218
|
+
const { enclosingLessonId } = props;
|
|
1219
|
+
const checkId = (0, import_react6.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1073
1220
|
const quiz = useQuizState(enclosingLessonId);
|
|
1074
1221
|
const { plugins, config, session } = useLessonkit();
|
|
1075
|
-
const [selected, setSelected] = (0,
|
|
1076
|
-
const [selectionCorrect, setSelectionCorrect] = (0,
|
|
1077
|
-
const [quizPassed, setQuizPassed] = (0,
|
|
1078
|
-
const completedRef = (0,
|
|
1079
|
-
const questionId = (0,
|
|
1222
|
+
const [selected, setSelected] = (0, import_react6.useState)(null);
|
|
1223
|
+
const [selectionCorrect, setSelectionCorrect] = (0, import_react6.useState)(null);
|
|
1224
|
+
const [quizPassed, setQuizPassed] = (0, import_react6.useState)(false);
|
|
1225
|
+
const completedRef = (0, import_react6.useRef)(false);
|
|
1226
|
+
const questionId = (0, import_react6.useId)();
|
|
1080
1227
|
const choicesKey = props.choices.join("\0");
|
|
1081
|
-
(0,
|
|
1228
|
+
(0, import_react6.useEffect)(() => {
|
|
1082
1229
|
completedRef.current = false;
|
|
1083
1230
|
setQuizPassed(false);
|
|
1084
1231
|
setSelected(null);
|
|
@@ -1087,14 +1234,11 @@ function Quiz(props) {
|
|
|
1087
1234
|
const isChoiceCorrect = (choice, custom) => {
|
|
1088
1235
|
if (!custom) return choice === props.answer;
|
|
1089
1236
|
if (custom.passed !== void 0) return custom.passed;
|
|
1090
|
-
if (custom.maxScore != null && custom.maxScore > 0) {
|
|
1091
|
-
return custom.score
|
|
1237
|
+
if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
1238
|
+
return meetsPassingThreshold(custom.score, custom.maxScore, props.passingScore);
|
|
1092
1239
|
}
|
|
1093
1240
|
return choice === props.answer;
|
|
1094
1241
|
};
|
|
1095
|
-
if (missingLesson) {
|
|
1096
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": checkId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: "Quiz must be placed inside a Lesson." }) });
|
|
1097
|
-
}
|
|
1098
1242
|
const passed = quizPassed;
|
|
1099
1243
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
1100
1244
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: questionId, children: props.question }),
|
|
@@ -1141,7 +1285,7 @@ function Quiz(props) {
|
|
|
1141
1285
|
const maxScore = custom?.maxScore ?? 1;
|
|
1142
1286
|
quiz.complete({
|
|
1143
1287
|
checkId,
|
|
1144
|
-
score: custom?.score ??
|
|
1288
|
+
score: custom?.score ?? maxScore,
|
|
1145
1289
|
maxScore,
|
|
1146
1290
|
passingScore: props.passingScore ?? maxScore
|
|
1147
1291
|
});
|
|
@@ -1184,11 +1328,864 @@ function ProgressTracker(props) {
|
|
|
1184
1328
|
] }) });
|
|
1185
1329
|
}
|
|
1186
1330
|
|
|
1331
|
+
// src/blocks/TrueFalse.tsx
|
|
1332
|
+
var import_react9 = __toESM(require("react"), 1);
|
|
1333
|
+
|
|
1334
|
+
// src/assessment/AssessmentLessonGuard.tsx
|
|
1335
|
+
var import_react7 = require("react");
|
|
1336
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1337
|
+
var warnedAssessmentOutsideLesson = false;
|
|
1338
|
+
function resetAssessmentWarningsForTests() {
|
|
1339
|
+
warnedAssessmentOutsideLesson = false;
|
|
1340
|
+
}
|
|
1341
|
+
function AssessmentLessonGuard(props) {
|
|
1342
|
+
const enclosingLessonId = useEnclosingLessonId();
|
|
1343
|
+
const missingLesson = enclosingLessonId === void 0;
|
|
1344
|
+
(0, import_react7.useEffect)(() => {
|
|
1345
|
+
if (!missingLesson || isDevEnvironment4()) return;
|
|
1346
|
+
if (!warnedAssessmentOutsideLesson) {
|
|
1347
|
+
warnedAssessmentOutsideLesson = true;
|
|
1348
|
+
console.error(
|
|
1349
|
+
`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>; assessment telemetry will not be emitted.`
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
}, [missingLesson, props.blockLabel]);
|
|
1353
|
+
if (missingLesson && isDevEnvironment4()) {
|
|
1354
|
+
throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
|
|
1355
|
+
}
|
|
1356
|
+
if (missingLesson) {
|
|
1357
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("section", { role: "alert", "aria-label": `${props.blockLabel} configuration error`, "data-lk-check-id": props.checkId, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { children: [
|
|
1358
|
+
props.blockLabel,
|
|
1359
|
+
" must be placed inside a Lesson."
|
|
1360
|
+
] }) });
|
|
1361
|
+
}
|
|
1362
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: props.children(enclosingLessonId) });
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// src/assessment/AssessmentSequenceContext.tsx
|
|
1366
|
+
var import_react8 = __toESM(require("react"), 1);
|
|
1367
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1368
|
+
var AssessmentSequenceContext = (0, import_react8.createContext)(null);
|
|
1369
|
+
function AssessmentSequenceProvider({ children }) {
|
|
1370
|
+
const registryRef = (0, import_react8.useRef)(/* @__PURE__ */ new Map());
|
|
1371
|
+
const register = (0, import_react8.useCallback)((checkId, handle) => {
|
|
1372
|
+
registryRef.current.set(checkId, handle);
|
|
1373
|
+
return () => {
|
|
1374
|
+
registryRef.current.delete(checkId);
|
|
1375
|
+
};
|
|
1376
|
+
}, []);
|
|
1377
|
+
const value = (0, import_react8.useMemo)(
|
|
1378
|
+
() => ({
|
|
1379
|
+
register,
|
|
1380
|
+
getHandles: () => registryRef.current
|
|
1381
|
+
}),
|
|
1382
|
+
[register]
|
|
1383
|
+
);
|
|
1384
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AssessmentSequenceContext.Provider, { value, children });
|
|
1385
|
+
}
|
|
1386
|
+
function useAssessmentSequenceRegistry() {
|
|
1387
|
+
return (0, import_react8.useContext)(AssessmentSequenceContext);
|
|
1388
|
+
}
|
|
1389
|
+
function useRegisterAssessmentHandle(checkId, handle) {
|
|
1390
|
+
const ctx = useAssessmentSequenceRegistry();
|
|
1391
|
+
import_react8.default.useEffect(() => {
|
|
1392
|
+
if (!ctx || !handle) return;
|
|
1393
|
+
return ctx.register(checkId, handle);
|
|
1394
|
+
}, [ctx, checkId, handle]);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// src/blocks/TrueFalse.tsx
|
|
1398
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1399
|
+
var INTERACTION = "trueFalse";
|
|
1400
|
+
function TrueFalseInner(props, ref) {
|
|
1401
|
+
const { enclosingLessonId } = props;
|
|
1402
|
+
const checkId = (0, import_react9.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1403
|
+
const assessment = useAssessmentState(enclosingLessonId);
|
|
1404
|
+
const { plugins, config, session } = useLessonkit();
|
|
1405
|
+
const [selected, setSelected] = (0, import_react9.useState)(null);
|
|
1406
|
+
const [selectionCorrect, setSelectionCorrect] = (0, import_react9.useState)(null);
|
|
1407
|
+
const [showSolutions, setShowSolutions] = (0, import_react9.useState)(false);
|
|
1408
|
+
const [passed, setPassed] = (0, import_react9.useState)(false);
|
|
1409
|
+
const completedRef = (0, import_react9.useRef)(false);
|
|
1410
|
+
const questionId = import_react9.default.useId();
|
|
1411
|
+
const reset = () => {
|
|
1412
|
+
completedRef.current = false;
|
|
1413
|
+
setPassed(false);
|
|
1414
|
+
setSelected(null);
|
|
1415
|
+
setSelectionCorrect(null);
|
|
1416
|
+
setShowSolutions(false);
|
|
1417
|
+
};
|
|
1418
|
+
(0, import_react9.useEffect)(() => {
|
|
1419
|
+
reset();
|
|
1420
|
+
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
|
|
1421
|
+
const handle = (0, import_react9.useMemo)(() => {
|
|
1422
|
+
const maxScore = 1;
|
|
1423
|
+
const score = passed ? maxScore : selected === null ? 0 : selected === props.answer ? maxScore : 0;
|
|
1424
|
+
return {
|
|
1425
|
+
getScore: () => score,
|
|
1426
|
+
getMaxScore: () => maxScore,
|
|
1427
|
+
getAnswerGiven: () => selected !== null,
|
|
1428
|
+
resetTask: reset,
|
|
1429
|
+
showSolutions: () => setShowSolutions(true),
|
|
1430
|
+
getXAPIData: () => ({
|
|
1431
|
+
checkId,
|
|
1432
|
+
interactionType: INTERACTION,
|
|
1433
|
+
response: selected ?? void 0,
|
|
1434
|
+
correct: selected === props.answer,
|
|
1435
|
+
score,
|
|
1436
|
+
maxScore
|
|
1437
|
+
})
|
|
1438
|
+
};
|
|
1439
|
+
}, [checkId, passed, props.answer, selected]);
|
|
1440
|
+
(0, import_react9.useImperativeHandle)(ref, () => handle, [handle]);
|
|
1441
|
+
useRegisterAssessmentHandle(checkId, handle);
|
|
1442
|
+
const submit = (value) => {
|
|
1443
|
+
if (passed && !props.enableRetry) return;
|
|
1444
|
+
setSelected(value);
|
|
1445
|
+
const pluginCtx = buildPluginContext({
|
|
1446
|
+
courseId: config.courseId,
|
|
1447
|
+
sessionId: session.sessionId,
|
|
1448
|
+
attemptId: session.attemptId,
|
|
1449
|
+
user: session.user
|
|
1450
|
+
});
|
|
1451
|
+
const custom = plugins?.scoreAssessment(
|
|
1452
|
+
{ checkId, lessonId: enclosingLessonId, response: value },
|
|
1453
|
+
pluginCtx
|
|
1454
|
+
) ?? null;
|
|
1455
|
+
const correct = value === props.answer;
|
|
1456
|
+
const scored = scoreFromCustom(custom, correct, 1, props.passingScore);
|
|
1457
|
+
setSelectionCorrect(scored.passed);
|
|
1458
|
+
assessment.answer({
|
|
1459
|
+
checkId,
|
|
1460
|
+
interactionType: INTERACTION,
|
|
1461
|
+
question: props.question,
|
|
1462
|
+
response: value,
|
|
1463
|
+
correct: scored.passed
|
|
1464
|
+
});
|
|
1465
|
+
if (scored.passed && !completedRef.current) {
|
|
1466
|
+
completedRef.current = true;
|
|
1467
|
+
setPassed(true);
|
|
1468
|
+
assessment.complete({
|
|
1469
|
+
checkId,
|
|
1470
|
+
interactionType: INTERACTION,
|
|
1471
|
+
score: scored.score,
|
|
1472
|
+
maxScore: scored.maxScore,
|
|
1473
|
+
passingScore: props.passingScore ?? scored.maxScore
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
1478
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
|
|
1479
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { id: questionId, children: props.question }),
|
|
1480
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
|
|
1481
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("legend", { className: "lk-visually-hidden", children: "True or False" }),
|
|
1482
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { style: { display: "block", marginRight: "1rem" }, children: [
|
|
1483
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1484
|
+
"input",
|
|
1485
|
+
{
|
|
1486
|
+
type: "radio",
|
|
1487
|
+
name: `${questionId}-tf`,
|
|
1488
|
+
checked: selected === true,
|
|
1489
|
+
disabled: passed && !props.enableRetry,
|
|
1490
|
+
onChange: () => submit(true)
|
|
1491
|
+
}
|
|
1492
|
+
),
|
|
1493
|
+
"True"
|
|
1494
|
+
] }),
|
|
1495
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { style: { display: "block" }, children: [
|
|
1496
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1497
|
+
"input",
|
|
1498
|
+
{
|
|
1499
|
+
type: "radio",
|
|
1500
|
+
name: `${questionId}-tf`,
|
|
1501
|
+
checked: selected === false,
|
|
1502
|
+
disabled: passed && !props.enableRetry,
|
|
1503
|
+
onChange: () => submit(false)
|
|
1504
|
+
}
|
|
1505
|
+
),
|
|
1506
|
+
"False"
|
|
1507
|
+
] })
|
|
1508
|
+
] }),
|
|
1509
|
+
reveal ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { children: [
|
|
1510
|
+
"Correct answer: ",
|
|
1511
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: props.answer ? "True" : "False" })
|
|
1512
|
+
] }) : null,
|
|
1513
|
+
selected !== null && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
|
|
1514
|
+
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
1515
|
+
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1516
|
+
] });
|
|
1517
|
+
}
|
|
1518
|
+
var TrueFalseInnerForwarded = (0, import_react9.forwardRef)(TrueFalseInner);
|
|
1519
|
+
var TrueFalse = (0, import_react9.forwardRef)(function TrueFalse2(props, ref) {
|
|
1520
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
// src/blocks/MarkTheWords.tsx
|
|
1524
|
+
var import_react10 = __toESM(require("react"), 1);
|
|
1525
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1526
|
+
var INTERACTION2 = "markTheWords";
|
|
1527
|
+
function tokenize(text) {
|
|
1528
|
+
return text.split(/(\s+)/).filter((t) => t.length > 0);
|
|
1529
|
+
}
|
|
1530
|
+
function MarkTheWordsInner(props, ref) {
|
|
1531
|
+
const checkId = (0, import_react10.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1532
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1533
|
+
const tokens = (0, import_react10.useMemo)(() => tokenize(props.text), [props.text]);
|
|
1534
|
+
const correctSet = (0, import_react10.useMemo)(
|
|
1535
|
+
() => new Set(props.correctWords.map((w) => w.toLowerCase())),
|
|
1536
|
+
[props.correctWords]
|
|
1537
|
+
);
|
|
1538
|
+
const [marked, setMarked] = (0, import_react10.useState)(() => /* @__PURE__ */ new Set());
|
|
1539
|
+
const [passed, setPassed] = (0, import_react10.useState)(false);
|
|
1540
|
+
const [showSolutions, setShowSolutions] = (0, import_react10.useState)(false);
|
|
1541
|
+
const completedRef = (0, import_react10.useRef)(false);
|
|
1542
|
+
const reset = () => {
|
|
1543
|
+
completedRef.current = false;
|
|
1544
|
+
setPassed(false);
|
|
1545
|
+
setMarked(/* @__PURE__ */ new Set());
|
|
1546
|
+
setShowSolutions(false);
|
|
1547
|
+
};
|
|
1548
|
+
(0, import_react10.useEffect)(() => {
|
|
1549
|
+
reset();
|
|
1550
|
+
}, [checkId, props.text, props.correctWords.join("\0")]);
|
|
1551
|
+
const selectableIndices = (0, import_react10.useMemo)(() => {
|
|
1552
|
+
const indices = [];
|
|
1553
|
+
tokens.forEach((t, i) => {
|
|
1554
|
+
if (!/^\s+$/.test(t) && correctSet.has(t.toLowerCase())) indices.push(i);
|
|
1555
|
+
});
|
|
1556
|
+
return indices;
|
|
1557
|
+
}, [tokens, correctSet]);
|
|
1558
|
+
const hasTargets = selectableIndices.length > 0;
|
|
1559
|
+
const allMarked = hasTargets && selectableIndices.every((i) => marked.has(i));
|
|
1560
|
+
const maxScore = selectableIndices.length;
|
|
1561
|
+
const score = allMarked ? maxScore : marked.size;
|
|
1562
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
1563
|
+
const handle = (0, import_react10.useMemo)(() => {
|
|
1564
|
+
const handleMax = maxScore || 1;
|
|
1565
|
+
return {
|
|
1566
|
+
getScore: () => score,
|
|
1567
|
+
getMaxScore: () => handleMax,
|
|
1568
|
+
getAnswerGiven: () => marked.size > 0,
|
|
1569
|
+
resetTask: reset,
|
|
1570
|
+
showSolutions: () => setShowSolutions(true),
|
|
1571
|
+
getXAPIData: () => ({
|
|
1572
|
+
checkId,
|
|
1573
|
+
interactionType: INTERACTION2,
|
|
1574
|
+
response: [...marked].map((i) => tokens[i]),
|
|
1575
|
+
correct: passedThreshold,
|
|
1576
|
+
score,
|
|
1577
|
+
maxScore: handleMax
|
|
1578
|
+
})
|
|
1579
|
+
};
|
|
1580
|
+
}, [checkId, marked, maxScore, passedThreshold, score, tokens]);
|
|
1581
|
+
(0, import_react10.useImperativeHandle)(ref, () => handle, [handle]);
|
|
1582
|
+
useRegisterAssessmentHandle(checkId, handle);
|
|
1583
|
+
const toggle = (index) => {
|
|
1584
|
+
if (passed && !props.enableRetry) return;
|
|
1585
|
+
setMarked((prev) => {
|
|
1586
|
+
const next = new Set(prev);
|
|
1587
|
+
if (next.has(index)) next.delete(index);
|
|
1588
|
+
else next.add(index);
|
|
1589
|
+
return next;
|
|
1590
|
+
});
|
|
1591
|
+
};
|
|
1592
|
+
(0, import_react10.useEffect)(() => {
|
|
1593
|
+
if (!hasTargets) {
|
|
1594
|
+
if (isDevEnvironment4()) {
|
|
1595
|
+
console.warn(
|
|
1596
|
+
"[lessonkit] MarkTheWords: no tokens match correctWords",
|
|
1597
|
+
props.correctWords
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
if (!passedThreshold || completedRef.current) return;
|
|
1603
|
+
completedRef.current = true;
|
|
1604
|
+
setPassed(true);
|
|
1605
|
+
assessment.answer({
|
|
1606
|
+
checkId,
|
|
1607
|
+
interactionType: INTERACTION2,
|
|
1608
|
+
question: props.text,
|
|
1609
|
+
response: [...marked].map((i) => tokens[i]),
|
|
1610
|
+
correct: true
|
|
1611
|
+
});
|
|
1612
|
+
assessment.complete({
|
|
1613
|
+
checkId,
|
|
1614
|
+
interactionType: INTERACTION2,
|
|
1615
|
+
score,
|
|
1616
|
+
maxScore,
|
|
1617
|
+
passingScore: props.passingScore ?? maxScore
|
|
1618
|
+
});
|
|
1619
|
+
}, [
|
|
1620
|
+
assessment,
|
|
1621
|
+
checkId,
|
|
1622
|
+
hasTargets,
|
|
1623
|
+
marked,
|
|
1624
|
+
maxScore,
|
|
1625
|
+
passedThreshold,
|
|
1626
|
+
props.passingScore,
|
|
1627
|
+
props.correctWords,
|
|
1628
|
+
props.text,
|
|
1629
|
+
score,
|
|
1630
|
+
tokens
|
|
1631
|
+
]);
|
|
1632
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
|
|
1633
|
+
!hasTargets ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { role: "alert", children: [
|
|
1634
|
+
"No words in this sentence match ",
|
|
1635
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("code", { children: "correctWords" }),
|
|
1636
|
+
". Check spelling and capitalization in the source text."
|
|
1637
|
+
] }) : null,
|
|
1638
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
|
|
1639
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
|
|
1640
|
+
const isWord = !/^\s+$/.test(token);
|
|
1641
|
+
const isTarget = isWord && correctSet.has(token.toLowerCase());
|
|
1642
|
+
if (!isTarget) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react10.default.Fragment, { children: token }, i);
|
|
1643
|
+
const selected = marked.has(i);
|
|
1644
|
+
const solution = showSolutions || passed && props.enableSolutionsButton;
|
|
1645
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1646
|
+
"button",
|
|
1647
|
+
{
|
|
1648
|
+
type: "button",
|
|
1649
|
+
"data-testid": `mark-word-${i}`,
|
|
1650
|
+
"aria-pressed": selected,
|
|
1651
|
+
disabled: passed && !props.enableRetry,
|
|
1652
|
+
onClick: () => toggle(i),
|
|
1653
|
+
style: {
|
|
1654
|
+
margin: "0 0.1em",
|
|
1655
|
+
textDecoration: solution ? "underline" : void 0,
|
|
1656
|
+
fontWeight: selected || solution ? "bold" : void 0
|
|
1657
|
+
},
|
|
1658
|
+
children: token
|
|
1659
|
+
},
|
|
1660
|
+
i
|
|
1661
|
+
);
|
|
1662
|
+
}) }),
|
|
1663
|
+
allMarked ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
|
|
1664
|
+
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
1665
|
+
props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1666
|
+
] });
|
|
1667
|
+
}
|
|
1668
|
+
var MarkTheWordsInnerForwarded = (0, import_react10.forwardRef)(MarkTheWordsInner);
|
|
1669
|
+
var MarkTheWords = (0, import_react10.forwardRef)(function MarkTheWords2(props, ref) {
|
|
1670
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
// src/blocks/FillInTheBlanks.tsx
|
|
1674
|
+
var import_react11 = __toESM(require("react"), 1);
|
|
1675
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
1676
|
+
var INTERACTION3 = "fillInBlanks";
|
|
1677
|
+
function parseTemplate(template) {
|
|
1678
|
+
const parts = [];
|
|
1679
|
+
const blanks = [];
|
|
1680
|
+
const re = /\*([^*]+)\*/g;
|
|
1681
|
+
let last = 0;
|
|
1682
|
+
let match;
|
|
1683
|
+
let n = 0;
|
|
1684
|
+
while ((match = re.exec(template)) !== null) {
|
|
1685
|
+
parts.push(template.slice(last, match.index));
|
|
1686
|
+
const id = `blank-${n++}`;
|
|
1687
|
+
blanks.push({ id, answer: match[1].trim() });
|
|
1688
|
+
parts.push(id);
|
|
1689
|
+
last = match.index + match[0].length;
|
|
1690
|
+
}
|
|
1691
|
+
parts.push(template.slice(last));
|
|
1692
|
+
return { parts, blanks };
|
|
1693
|
+
}
|
|
1694
|
+
function FillInTheBlanksInner(props, ref) {
|
|
1695
|
+
const checkId = (0, import_react11.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1696
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1697
|
+
const parsed = (0, import_react11.useMemo)(() => parseTemplate(props.template), [props.template]);
|
|
1698
|
+
const blanks = props.blanks ?? parsed.blanks;
|
|
1699
|
+
const [values, setValues] = (0, import_react11.useState)(
|
|
1700
|
+
() => Object.fromEntries(blanks.map((b) => [b.id, ""]))
|
|
1701
|
+
);
|
|
1702
|
+
const [passed, setPassed] = (0, import_react11.useState)(false);
|
|
1703
|
+
const [showSolutions, setShowSolutions] = (0, import_react11.useState)(false);
|
|
1704
|
+
const completedRef = (0, import_react11.useRef)(false);
|
|
1705
|
+
const answeredRef = (0, import_react11.useRef)(false);
|
|
1706
|
+
const reset = () => {
|
|
1707
|
+
completedRef.current = false;
|
|
1708
|
+
answeredRef.current = false;
|
|
1709
|
+
setPassed(false);
|
|
1710
|
+
setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
|
|
1711
|
+
setShowSolutions(false);
|
|
1712
|
+
};
|
|
1713
|
+
(0, import_react11.useEffect)(() => {
|
|
1714
|
+
reset();
|
|
1715
|
+
}, [checkId, props.template, blanks.map((b) => b.answer).join("\0")]);
|
|
1716
|
+
const hasBlanks = blanks.length > 0;
|
|
1717
|
+
const allFilled = hasBlanks && blanks.every((b) => (values[b.id] ?? "").trim().length > 0);
|
|
1718
|
+
let score = 0;
|
|
1719
|
+
blanks.forEach((b) => {
|
|
1720
|
+
if ((values[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) score += 1;
|
|
1721
|
+
});
|
|
1722
|
+
const maxScore = blanks.length;
|
|
1723
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
1724
|
+
const handle = (0, import_react11.useMemo)(() => {
|
|
1725
|
+
const handleMax = maxScore || 1;
|
|
1726
|
+
return {
|
|
1727
|
+
getScore: () => score,
|
|
1728
|
+
getMaxScore: () => handleMax,
|
|
1729
|
+
getAnswerGiven: () => allFilled,
|
|
1730
|
+
resetTask: reset,
|
|
1731
|
+
showSolutions: () => setShowSolutions(true),
|
|
1732
|
+
getXAPIData: () => ({
|
|
1733
|
+
checkId,
|
|
1734
|
+
interactionType: INTERACTION3,
|
|
1735
|
+
response: values,
|
|
1736
|
+
correct: passedThreshold,
|
|
1737
|
+
score,
|
|
1738
|
+
maxScore: handleMax
|
|
1739
|
+
})
|
|
1740
|
+
};
|
|
1741
|
+
}, [allFilled, blanks.length, checkId, maxScore, passedThreshold, score, values]);
|
|
1742
|
+
(0, import_react11.useImperativeHandle)(ref, () => handle, [handle]);
|
|
1743
|
+
useRegisterAssessmentHandle(checkId, handle);
|
|
1744
|
+
const check = () => {
|
|
1745
|
+
if (!hasBlanks) {
|
|
1746
|
+
if (isDevEnvironment4()) {
|
|
1747
|
+
console.warn("[lessonkit] FillInTheBlanks has no blanks in template");
|
|
1748
|
+
}
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
if (!allFilled) return;
|
|
1752
|
+
if (!answeredRef.current) {
|
|
1753
|
+
answeredRef.current = true;
|
|
1754
|
+
assessment.answer({
|
|
1755
|
+
checkId,
|
|
1756
|
+
interactionType: INTERACTION3,
|
|
1757
|
+
question: props.template,
|
|
1758
|
+
response: values,
|
|
1759
|
+
correct: passedThreshold
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
if (passedThreshold && !completedRef.current) {
|
|
1763
|
+
completedRef.current = true;
|
|
1764
|
+
setPassed(true);
|
|
1765
|
+
assessment.complete({
|
|
1766
|
+
checkId,
|
|
1767
|
+
interactionType: INTERACTION3,
|
|
1768
|
+
score,
|
|
1769
|
+
maxScore,
|
|
1770
|
+
passingScore: props.passingScore ?? maxScore
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
(0, import_react11.useEffect)(() => {
|
|
1775
|
+
if (!allFilled) answeredRef.current = false;
|
|
1776
|
+
}, [allFilled]);
|
|
1777
|
+
(0, import_react11.useEffect)(() => {
|
|
1778
|
+
if (props.autoCheck && allFilled) check();
|
|
1779
|
+
}, [allFilled, props.autoCheck, values, passedThreshold]);
|
|
1780
|
+
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
1781
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
|
|
1782
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { children: parsed.parts.map((part, i) => {
|
|
1783
|
+
const blank = blanks.find((b) => b.id === part);
|
|
1784
|
+
if (!blank) return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react11.default.Fragment, { children: part }, i);
|
|
1785
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { style: { margin: "0 0.25em" }, children: [
|
|
1786
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "lk-visually-hidden", children: blank.answer }),
|
|
1787
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1788
|
+
"input",
|
|
1789
|
+
{
|
|
1790
|
+
type: "text",
|
|
1791
|
+
"data-testid": `blank-${blank.id}`,
|
|
1792
|
+
"aria-label": `Blank ${blank.id}`,
|
|
1793
|
+
value: reveal ? blank.answer : values[blank.id] ?? "",
|
|
1794
|
+
readOnly: reveal,
|
|
1795
|
+
disabled: passed && !props.enableRetry,
|
|
1796
|
+
onChange: (e) => setValues((v) => ({ ...v, [blank.id]: e.target.value })),
|
|
1797
|
+
onBlur: () => props.autoCheck && check(),
|
|
1798
|
+
size: Math.max(8, blank.answer.length + 2)
|
|
1799
|
+
}
|
|
1800
|
+
)
|
|
1801
|
+
] }, blank.id);
|
|
1802
|
+
}) }),
|
|
1803
|
+
!props.autoCheck ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
|
|
1804
|
+
!hasBlanks ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
|
|
1805
|
+
allFilled ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
|
|
1806
|
+
props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
1807
|
+
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1808
|
+
] });
|
|
1809
|
+
}
|
|
1810
|
+
var FillInTheBlanksInnerForwarded = (0, import_react11.forwardRef)(FillInTheBlanksInner);
|
|
1811
|
+
var FillInTheBlanks = (0, import_react11.forwardRef)(
|
|
1812
|
+
function FillInTheBlanks2(props, ref) {
|
|
1813
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1814
|
+
}
|
|
1815
|
+
);
|
|
1816
|
+
|
|
1817
|
+
// src/blocks/DragTheWords.tsx
|
|
1818
|
+
var import_react12 = __toESM(require("react"), 1);
|
|
1819
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1820
|
+
var INTERACTION4 = "dragTheWords";
|
|
1821
|
+
function parseZones(template) {
|
|
1822
|
+
const parts = [];
|
|
1823
|
+
const answers = [];
|
|
1824
|
+
const re = /\*([^*]+)\*/g;
|
|
1825
|
+
let last = 0;
|
|
1826
|
+
let match;
|
|
1827
|
+
let n = 0;
|
|
1828
|
+
while ((match = re.exec(template)) !== null) {
|
|
1829
|
+
parts.push(template.slice(last, match.index));
|
|
1830
|
+
answers.push(match[1].trim());
|
|
1831
|
+
parts.push(`zone-${n++}`);
|
|
1832
|
+
last = match.index + match[0].length;
|
|
1833
|
+
}
|
|
1834
|
+
parts.push(template.slice(last));
|
|
1835
|
+
return { parts, answers };
|
|
1836
|
+
}
|
|
1837
|
+
function DragTheWordsInner(props, ref) {
|
|
1838
|
+
const checkId = (0, import_react12.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1839
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1840
|
+
const { parts, answers } = (0, import_react12.useMemo)(() => parseZones(props.template), [props.template]);
|
|
1841
|
+
const [zones, setZones] = (0, import_react12.useState)(
|
|
1842
|
+
() => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
|
|
1843
|
+
);
|
|
1844
|
+
const [pool, setPool] = (0, import_react12.useState)(() => [...props.words]);
|
|
1845
|
+
const [keyboardWord, setKeyboardWord] = (0, import_react12.useState)(null);
|
|
1846
|
+
const [passed, setPassed] = (0, import_react12.useState)(false);
|
|
1847
|
+
const completedRef = (0, import_react12.useRef)(false);
|
|
1848
|
+
const answeredRef = (0, import_react12.useRef)(false);
|
|
1849
|
+
const reset = () => {
|
|
1850
|
+
completedRef.current = false;
|
|
1851
|
+
answeredRef.current = false;
|
|
1852
|
+
setPassed(false);
|
|
1853
|
+
setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
|
|
1854
|
+
setPool([...props.words]);
|
|
1855
|
+
setKeyboardWord(null);
|
|
1856
|
+
};
|
|
1857
|
+
(0, import_react12.useEffect)(() => {
|
|
1858
|
+
reset();
|
|
1859
|
+
}, [checkId, props.template, props.words.join("\0")]);
|
|
1860
|
+
const hasZones = answers.length > 0;
|
|
1861
|
+
const allFilled = hasZones && answers.every((_, i) => (zones[`zone-${i}`] ?? "").length > 0);
|
|
1862
|
+
let score = 0;
|
|
1863
|
+
answers.forEach((ans, i) => {
|
|
1864
|
+
if ((zones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) score += 1;
|
|
1865
|
+
});
|
|
1866
|
+
const maxScore = answers.length;
|
|
1867
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
1868
|
+
const handle = (0, import_react12.useMemo)(() => {
|
|
1869
|
+
const handleMax = maxScore || 1;
|
|
1870
|
+
return {
|
|
1871
|
+
getScore: () => score,
|
|
1872
|
+
getMaxScore: () => handleMax,
|
|
1873
|
+
getAnswerGiven: () => allFilled,
|
|
1874
|
+
resetTask: reset,
|
|
1875
|
+
showSolutions: () => {
|
|
1876
|
+
},
|
|
1877
|
+
getXAPIData: () => ({
|
|
1878
|
+
checkId,
|
|
1879
|
+
interactionType: INTERACTION4,
|
|
1880
|
+
response: zones,
|
|
1881
|
+
correct: passedThreshold,
|
|
1882
|
+
score,
|
|
1883
|
+
maxScore: handleMax
|
|
1884
|
+
})
|
|
1885
|
+
};
|
|
1886
|
+
}, [allFilled, answers.length, checkId, maxScore, passedThreshold, score, zones]);
|
|
1887
|
+
(0, import_react12.useImperativeHandle)(ref, () => handle, [handle]);
|
|
1888
|
+
useRegisterAssessmentHandle(checkId, handle);
|
|
1889
|
+
const placeInZone = (zoneId, word) => {
|
|
1890
|
+
if (passed && !props.enableRetry) return;
|
|
1891
|
+
const prev = zones[zoneId];
|
|
1892
|
+
setZones((z) => ({ ...z, [zoneId]: word }));
|
|
1893
|
+
setPool((p) => {
|
|
1894
|
+
const next = p.filter((w) => w !== word);
|
|
1895
|
+
if (prev) next.push(prev);
|
|
1896
|
+
return next;
|
|
1897
|
+
});
|
|
1898
|
+
setKeyboardWord(null);
|
|
1899
|
+
};
|
|
1900
|
+
const onDragStart = (word) => (e) => {
|
|
1901
|
+
e.dataTransfer.setData("text/plain", word);
|
|
1902
|
+
};
|
|
1903
|
+
const onDrop = (zoneId) => (e) => {
|
|
1904
|
+
e.preventDefault();
|
|
1905
|
+
const word = e.dataTransfer.getData("text/plain");
|
|
1906
|
+
if (word) placeInZone(zoneId, word);
|
|
1907
|
+
};
|
|
1908
|
+
const check = () => {
|
|
1909
|
+
if (!hasZones) {
|
|
1910
|
+
if (isDevEnvironment4()) {
|
|
1911
|
+
console.warn("[lessonkit] DragTheWords has no drop zones in template");
|
|
1912
|
+
}
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
if (!allFilled) return;
|
|
1916
|
+
if (!answeredRef.current) {
|
|
1917
|
+
answeredRef.current = true;
|
|
1918
|
+
assessment.answer({
|
|
1919
|
+
checkId,
|
|
1920
|
+
interactionType: INTERACTION4,
|
|
1921
|
+
question: props.template,
|
|
1922
|
+
response: zones,
|
|
1923
|
+
correct: passedThreshold
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
if (passedThreshold && !completedRef.current) {
|
|
1927
|
+
completedRef.current = true;
|
|
1928
|
+
setPassed(true);
|
|
1929
|
+
assessment.complete({
|
|
1930
|
+
checkId,
|
|
1931
|
+
interactionType: INTERACTION4,
|
|
1932
|
+
score,
|
|
1933
|
+
maxScore,
|
|
1934
|
+
passingScore: props.passingScore ?? maxScore
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
(0, import_react12.useEffect)(() => {
|
|
1939
|
+
if (!allFilled) answeredRef.current = false;
|
|
1940
|
+
}, [allFilled]);
|
|
1941
|
+
(0, import_react12.useEffect)(() => {
|
|
1942
|
+
if (props.autoCheck && allFilled) check();
|
|
1943
|
+
}, [allFilled, props.autoCheck, zones, passedThreshold]);
|
|
1944
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
|
|
1945
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
|
|
1946
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1947
|
+
"button",
|
|
1948
|
+
{
|
|
1949
|
+
type: "button",
|
|
1950
|
+
draggable: true,
|
|
1951
|
+
"data-testid": `word-${word}`,
|
|
1952
|
+
"aria-pressed": keyboardWord === word,
|
|
1953
|
+
onDragStart: onDragStart(word),
|
|
1954
|
+
onClick: () => setKeyboardWord(keyboardWord === word ? null : word),
|
|
1955
|
+
style: { margin: "0.25rem" },
|
|
1956
|
+
children: word
|
|
1957
|
+
},
|
|
1958
|
+
word
|
|
1959
|
+
)) }),
|
|
1960
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: parts.map((part, i) => {
|
|
1961
|
+
if (!part.startsWith("zone-")) return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react12.default.Fragment, { children: part }, i);
|
|
1962
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1963
|
+
"span",
|
|
1964
|
+
{
|
|
1965
|
+
role: "button",
|
|
1966
|
+
tabIndex: 0,
|
|
1967
|
+
"data-testid": part,
|
|
1968
|
+
onDragOver: (e) => e.preventDefault(),
|
|
1969
|
+
onDrop: onDrop(part),
|
|
1970
|
+
onClick: () => keyboardWord && placeInZone(part, keyboardWord),
|
|
1971
|
+
onKeyDown: (e) => {
|
|
1972
|
+
if (e.key === "Enter" && keyboardWord) placeInZone(part, keyboardWord);
|
|
1973
|
+
},
|
|
1974
|
+
style: {
|
|
1975
|
+
display: "inline-block",
|
|
1976
|
+
minWidth: "6em",
|
|
1977
|
+
border: "1px dashed currentColor",
|
|
1978
|
+
padding: "0.2em 0.5em",
|
|
1979
|
+
margin: "0 0.2em"
|
|
1980
|
+
},
|
|
1981
|
+
children: zones[part] || "___"
|
|
1982
|
+
},
|
|
1983
|
+
part
|
|
1984
|
+
);
|
|
1985
|
+
}) }),
|
|
1986
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
|
|
1987
|
+
!hasZones ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
|
|
1988
|
+
allFilled ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
|
|
1989
|
+
] });
|
|
1990
|
+
}
|
|
1991
|
+
var DragTheWordsInnerForwarded = (0, import_react12.forwardRef)(DragTheWordsInner);
|
|
1992
|
+
var DragTheWords = (0, import_react12.forwardRef)(function DragTheWords2(props, ref) {
|
|
1993
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
// src/blocks/DragAndDrop.tsx
|
|
1997
|
+
var import_react13 = require("react");
|
|
1998
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
1999
|
+
var INTERACTION5 = "dragAndDrop";
|
|
2000
|
+
function DragAndDropInner(props, ref) {
|
|
2001
|
+
const checkId = (0, import_react13.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
2002
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
2003
|
+
const [assignments, setAssignments] = (0, import_react13.useState)(
|
|
2004
|
+
() => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
|
|
2005
|
+
);
|
|
2006
|
+
const [pool, setPool] = (0, import_react13.useState)(() => props.items.map((i) => i.id));
|
|
2007
|
+
const [keyboardItem, setKeyboardItem] = (0, import_react13.useState)(null);
|
|
2008
|
+
const [passed, setPassed] = (0, import_react13.useState)(false);
|
|
2009
|
+
const completedRef = (0, import_react13.useRef)(false);
|
|
2010
|
+
const reset = () => {
|
|
2011
|
+
completedRef.current = false;
|
|
2012
|
+
setPassed(false);
|
|
2013
|
+
setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
|
|
2014
|
+
setPool(props.items.map((i) => i.id));
|
|
2015
|
+
setKeyboardItem(null);
|
|
2016
|
+
};
|
|
2017
|
+
(0, import_react13.useEffect)(() => {
|
|
2018
|
+
reset();
|
|
2019
|
+
}, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
|
|
2020
|
+
const allFilled = props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
|
|
2021
|
+
const allCorrect = props.targets.every((t) => assignments[t.id] === t.accepts);
|
|
2022
|
+
const handle = (0, import_react13.useMemo)(() => {
|
|
2023
|
+
const maxScore = props.targets.length || 1;
|
|
2024
|
+
let score = 0;
|
|
2025
|
+
props.targets.forEach((t) => {
|
|
2026
|
+
if (assignments[t.id] === t.accepts) score += 1;
|
|
2027
|
+
});
|
|
2028
|
+
return {
|
|
2029
|
+
getScore: () => score,
|
|
2030
|
+
getMaxScore: () => maxScore,
|
|
2031
|
+
getAnswerGiven: () => allFilled,
|
|
2032
|
+
resetTask: reset,
|
|
2033
|
+
showSolutions: () => {
|
|
2034
|
+
},
|
|
2035
|
+
getXAPIData: () => ({
|
|
2036
|
+
checkId,
|
|
2037
|
+
interactionType: INTERACTION5,
|
|
2038
|
+
response: assignments,
|
|
2039
|
+
correct: allCorrect,
|
|
2040
|
+
score,
|
|
2041
|
+
maxScore
|
|
2042
|
+
})
|
|
2043
|
+
};
|
|
2044
|
+
}, [allCorrect, allFilled, assignments, checkId, props.targets]);
|
|
2045
|
+
(0, import_react13.useImperativeHandle)(ref, () => handle, [handle]);
|
|
2046
|
+
useRegisterAssessmentHandle(checkId, handle);
|
|
2047
|
+
const place = (targetId, itemId) => {
|
|
2048
|
+
if (passed && !props.enableRetry) return;
|
|
2049
|
+
const prev = assignments[targetId];
|
|
2050
|
+
setAssignments((a) => ({ ...a, [targetId]: itemId }));
|
|
2051
|
+
setPool((p) => {
|
|
2052
|
+
const next = p.filter((id) => id !== itemId);
|
|
2053
|
+
if (prev) next.push(prev);
|
|
2054
|
+
return next;
|
|
2055
|
+
});
|
|
2056
|
+
setKeyboardItem(null);
|
|
2057
|
+
};
|
|
2058
|
+
const check = () => {
|
|
2059
|
+
if (!allFilled) return;
|
|
2060
|
+
assessment.answer({
|
|
2061
|
+
checkId,
|
|
2062
|
+
interactionType: INTERACTION5,
|
|
2063
|
+
response: assignments,
|
|
2064
|
+
correct: allCorrect
|
|
2065
|
+
});
|
|
2066
|
+
if (allCorrect && !completedRef.current) {
|
|
2067
|
+
completedRef.current = true;
|
|
2068
|
+
setPassed(true);
|
|
2069
|
+
assessment.complete({
|
|
2070
|
+
checkId,
|
|
2071
|
+
interactionType: INTERACTION5,
|
|
2072
|
+
score: props.targets.length,
|
|
2073
|
+
maxScore: props.targets.length,
|
|
2074
|
+
passingScore: props.passingScore ?? props.targets.length
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
|
|
2079
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
|
|
2080
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { role: "list", "aria-label": "Draggable items", children: pool.map((id) => {
|
|
2081
|
+
const item = props.items.find((i) => i.id === id);
|
|
2082
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2083
|
+
"button",
|
|
2084
|
+
{
|
|
2085
|
+
type: "button",
|
|
2086
|
+
draggable: true,
|
|
2087
|
+
"data-testid": `drag-item-${id}`,
|
|
2088
|
+
"aria-pressed": keyboardItem === id,
|
|
2089
|
+
onDragStart: (e) => e.dataTransfer.setData("text/plain", id),
|
|
2090
|
+
onClick: () => setKeyboardItem(keyboardItem === id ? null : id),
|
|
2091
|
+
style: { margin: "0.25rem" },
|
|
2092
|
+
children: item.label
|
|
2093
|
+
},
|
|
2094
|
+
id
|
|
2095
|
+
);
|
|
2096
|
+
}) }),
|
|
2097
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ul", { children: props.targets.map((target) => {
|
|
2098
|
+
const assigned = assignments[target.id];
|
|
2099
|
+
const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
|
|
2100
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("li", { children: [
|
|
2101
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: target.label }),
|
|
2102
|
+
" ",
|
|
2103
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2104
|
+
"span",
|
|
2105
|
+
{
|
|
2106
|
+
role: "button",
|
|
2107
|
+
tabIndex: 0,
|
|
2108
|
+
"data-testid": `drop-${target.id}`,
|
|
2109
|
+
onDragOver: (e) => e.preventDefault(),
|
|
2110
|
+
onDrop: (e) => {
|
|
2111
|
+
e.preventDefault();
|
|
2112
|
+
const id = e.dataTransfer.getData("text/plain");
|
|
2113
|
+
if (id) place(target.id, id);
|
|
2114
|
+
},
|
|
2115
|
+
onClick: () => keyboardItem && place(target.id, keyboardItem),
|
|
2116
|
+
onKeyDown: (e) => {
|
|
2117
|
+
if (e.key === "Enter" && keyboardItem) place(target.id, keyboardItem);
|
|
2118
|
+
},
|
|
2119
|
+
style: {
|
|
2120
|
+
display: "inline-block",
|
|
2121
|
+
minWidth: "8em",
|
|
2122
|
+
border: "1px dashed currentColor",
|
|
2123
|
+
padding: "0.25em"
|
|
2124
|
+
},
|
|
2125
|
+
children: label
|
|
2126
|
+
}
|
|
2127
|
+
)
|
|
2128
|
+
] }, target.id);
|
|
2129
|
+
}) }),
|
|
2130
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", "data-testid": "check-drag-drop", disabled: !allFilled || passed, onClick: check, children: "Check" }),
|
|
2131
|
+
allFilled ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "status", "aria-live": "polite", children: passed || allCorrect ? "Correct" : "Try again" }) : null
|
|
2132
|
+
] });
|
|
2133
|
+
}
|
|
2134
|
+
var DragAndDropInnerForwarded = (0, import_react13.forwardRef)(DragAndDropInner);
|
|
2135
|
+
var DragAndDrop = (0, import_react13.forwardRef)(function DragAndDrop2(props, ref) {
|
|
2136
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2137
|
+
});
|
|
2138
|
+
|
|
2139
|
+
// src/blocks/AssessmentSequence.tsx
|
|
2140
|
+
var import_react14 = __toESM(require("react"), 1);
|
|
2141
|
+
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
2142
|
+
function AssessmentSequence(props) {
|
|
2143
|
+
const sequential = props.sequential !== false;
|
|
2144
|
+
const childArray = import_react14.default.Children.toArray(props.children).filter(import_react14.default.isValidElement);
|
|
2145
|
+
const [index, setIndex] = (0, import_react14.useState)(0);
|
|
2146
|
+
const current = childArray[index] ?? null;
|
|
2147
|
+
const goNext = (0, import_react14.useCallback)(() => {
|
|
2148
|
+
setIndex((i) => Math.min(i + 1, childArray.length - 1));
|
|
2149
|
+
}, [childArray.length]);
|
|
2150
|
+
const goPrev = (0, import_react14.useCallback)(() => {
|
|
2151
|
+
setIndex((i) => Math.max(i - 1, 0));
|
|
2152
|
+
}, []);
|
|
2153
|
+
const progress = (0, import_react14.useMemo)(
|
|
2154
|
+
() => ({ current: index + 1, total: childArray.length }),
|
|
2155
|
+
[index, childArray.length]
|
|
2156
|
+
);
|
|
2157
|
+
if (!sequential) {
|
|
2158
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(AssessmentSequenceProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("section", { "aria-label": "Assessment sequence", children: props.children }) });
|
|
2159
|
+
}
|
|
2160
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(AssessmentSequenceProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
|
|
2161
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("p", { children: [
|
|
2162
|
+
"Question ",
|
|
2163
|
+
progress.current,
|
|
2164
|
+
" of ",
|
|
2165
|
+
progress.total
|
|
2166
|
+
] }),
|
|
2167
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { "data-testid": "assessment-sequence-step", children: current }),
|
|
2168
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("nav", { "aria-label": "Sequence navigation", children: [
|
|
2169
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", "data-testid": "sequence-prev", disabled: index === 0, onClick: goPrev, children: "Previous" }),
|
|
2170
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
2171
|
+
"button",
|
|
2172
|
+
{
|
|
2173
|
+
type: "button",
|
|
2174
|
+
"data-testid": "sequence-next",
|
|
2175
|
+
disabled: index >= childArray.length - 1,
|
|
2176
|
+
onClick: goNext,
|
|
2177
|
+
children: "Next"
|
|
2178
|
+
}
|
|
2179
|
+
)
|
|
2180
|
+
] })
|
|
2181
|
+
] }) });
|
|
2182
|
+
}
|
|
2183
|
+
|
|
1187
2184
|
// src/index.tsx
|
|
1188
2185
|
var import_core10 = require("@lessonkit/core");
|
|
1189
2186
|
|
|
1190
2187
|
// src/theme/ThemeProvider.tsx
|
|
1191
|
-
var
|
|
2188
|
+
var import_react15 = __toESM(require("react"), 1);
|
|
1192
2189
|
var import_themes = require("@lessonkit/themes");
|
|
1193
2190
|
|
|
1194
2191
|
// src/theme/applyCssVariables.ts
|
|
@@ -1207,9 +2204,12 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
1207
2204
|
}
|
|
1208
2205
|
|
|
1209
2206
|
// src/theme/ThemeProvider.tsx
|
|
1210
|
-
var
|
|
1211
|
-
var ThemeContext = (0,
|
|
1212
|
-
var useIsoLayoutEffect2 =
|
|
2207
|
+
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
2208
|
+
var ThemeContext = (0, import_react15.createContext)(null);
|
|
2209
|
+
var useIsoLayoutEffect2 = (
|
|
2210
|
+
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
2211
|
+
typeof window !== "undefined" ? import_react15.useLayoutEffect : import_react15.default.useEffect
|
|
2212
|
+
);
|
|
1213
2213
|
function getSystemMode() {
|
|
1214
2214
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
1215
2215
|
return "light";
|
|
@@ -1227,7 +2227,7 @@ function ThemeProvider(props) {
|
|
|
1227
2227
|
const preset = props.preset ?? "default";
|
|
1228
2228
|
const mode = props.mode ?? "light";
|
|
1229
2229
|
const targetKind = props.target ?? "document";
|
|
1230
|
-
const [resolvedMode, setResolvedMode] = (0,
|
|
2230
|
+
const [resolvedMode, setResolvedMode] = (0, import_react15.useState)(
|
|
1231
2231
|
() => mode === "system" ? getSystemMode() : mode
|
|
1232
2232
|
);
|
|
1233
2233
|
useIsoLayoutEffect2(() => {
|
|
@@ -1243,20 +2243,20 @@ function ThemeProvider(props) {
|
|
|
1243
2243
|
return () => mq.removeEventListener("change", onChange);
|
|
1244
2244
|
}, [mode]);
|
|
1245
2245
|
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
1246
|
-
const effectiveTheme = (0,
|
|
2246
|
+
const effectiveTheme = (0, import_react15.useMemo)(() => {
|
|
1247
2247
|
const modeBase = resolveModeBase(mode, dataTheme);
|
|
1248
2248
|
const base = preset === "default" ? modeBase : preset === "brand" ? (0, import_themes.mergeThemes)(modeBase, import_themes.brandThemeOverrides) : (0, import_themes.mergeThemes)(modeBase, (0, import_themes.getPresetTheme)(preset));
|
|
1249
2249
|
return (0, import_themes.mergeThemes)(base, props.theme ?? {});
|
|
1250
2250
|
}, [preset, mode, dataTheme, props.theme]);
|
|
1251
|
-
const hostRef = (0,
|
|
1252
|
-
const appliedKeysRef = (0,
|
|
2251
|
+
const hostRef = (0, import_react15.useRef)(null);
|
|
2252
|
+
const appliedKeysRef = (0, import_react15.useRef)(/* @__PURE__ */ new Set());
|
|
1253
2253
|
useIsoLayoutEffect2(() => {
|
|
1254
2254
|
if (targetKind === "document" && typeof document !== "undefined") {
|
|
1255
2255
|
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
1256
2256
|
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
1257
2257
|
}
|
|
1258
2258
|
}, [targetKind, dataTheme]);
|
|
1259
|
-
const inject = (0,
|
|
2259
|
+
const inject = (0, import_react15.useCallback)(() => {
|
|
1260
2260
|
const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
|
|
1261
2261
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
1262
2262
|
if (!el) return;
|
|
@@ -1273,7 +2273,7 @@ function ThemeProvider(props) {
|
|
|
1273
2273
|
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
1274
2274
|
};
|
|
1275
2275
|
}, [inject, targetKind]);
|
|
1276
|
-
const value = (0,
|
|
2276
|
+
const value = (0, import_react15.useMemo)(
|
|
1277
2277
|
() => ({
|
|
1278
2278
|
theme: effectiveTheme,
|
|
1279
2279
|
preset,
|
|
@@ -1283,12 +2283,12 @@ function ThemeProvider(props) {
|
|
|
1283
2283
|
[effectiveTheme, preset, mode, dataTheme]
|
|
1284
2284
|
);
|
|
1285
2285
|
if (targetKind === "document") {
|
|
1286
|
-
return /* @__PURE__ */ (0,
|
|
2286
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
|
|
1287
2287
|
}
|
|
1288
|
-
return /* @__PURE__ */ (0,
|
|
2288
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
1289
2289
|
}
|
|
1290
2290
|
function useTheme() {
|
|
1291
|
-
const ctx = (0,
|
|
2291
|
+
const ctx = (0, import_react15.useContext)(ThemeContext);
|
|
1292
2292
|
if (!ctx) {
|
|
1293
2293
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
1294
2294
|
}
|
|
@@ -1297,6 +2297,7 @@ function useTheme() {
|
|
|
1297
2297
|
|
|
1298
2298
|
// src/blockCatalog.ts
|
|
1299
2299
|
var blockCatalogVersion = 1;
|
|
2300
|
+
var blockCatalogV2Version = 2;
|
|
1300
2301
|
var BLOCK_CATALOG = [
|
|
1301
2302
|
{
|
|
1302
2303
|
type: "Course",
|
|
@@ -1483,8 +2484,163 @@ var BLOCK_CATALOG = [
|
|
|
1483
2484
|
}
|
|
1484
2485
|
}
|
|
1485
2486
|
];
|
|
1486
|
-
|
|
1487
|
-
|
|
2487
|
+
var assessmentBehaviourProps = [
|
|
2488
|
+
{ name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
|
|
2489
|
+
{ name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
|
|
2490
|
+
{ name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
|
|
2491
|
+
{ name: "passingScore", type: "number", required: false, description: "Minimum score to pass." }
|
|
2492
|
+
];
|
|
2493
|
+
var v2AssessmentEntries = [
|
|
2494
|
+
{
|
|
2495
|
+
type: "TrueFalse",
|
|
2496
|
+
category: "assessment",
|
|
2497
|
+
assessmentContract: true,
|
|
2498
|
+
h5pMachineName: "H5P.TrueFalse",
|
|
2499
|
+
h5pAlias: "True/False",
|
|
2500
|
+
description: "Binary true/false question with assessment contract.",
|
|
2501
|
+
props: [
|
|
2502
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
2503
|
+
{ name: "question", type: "string", required: true, description: "Question text." },
|
|
2504
|
+
{ name: "answer", type: "boolean", required: true, description: "Correct answer." },
|
|
2505
|
+
...assessmentBehaviourProps
|
|
2506
|
+
],
|
|
2507
|
+
requiredIds: ["checkId"],
|
|
2508
|
+
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
2509
|
+
a11y: {
|
|
2510
|
+
element: "section",
|
|
2511
|
+
ariaLabel: "True or False",
|
|
2512
|
+
keyboard: "Radio group with True/False options.",
|
|
2513
|
+
liveRegions: "role='status' for feedback.",
|
|
2514
|
+
notes: "H5P True/False equivalent."
|
|
2515
|
+
},
|
|
2516
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
2517
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
2518
|
+
},
|
|
2519
|
+
{
|
|
2520
|
+
type: "FillInTheBlanks",
|
|
2521
|
+
category: "assessment",
|
|
2522
|
+
assessmentContract: true,
|
|
2523
|
+
h5pMachineName: "H5P.Blanks",
|
|
2524
|
+
h5pAlias: "Fill in the Blanks",
|
|
2525
|
+
description: "Fill-in-the-blank text with *answer* markers in template.",
|
|
2526
|
+
props: [
|
|
2527
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
2528
|
+
{ name: "template", type: "string", required: true, description: "Text with *blank* markers." },
|
|
2529
|
+
{ name: "blanks", type: "FillInBlankSpec[]", required: false, description: "Explicit blank specs." },
|
|
2530
|
+
...assessmentBehaviourProps
|
|
2531
|
+
],
|
|
2532
|
+
requiredIds: ["checkId"],
|
|
2533
|
+
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
2534
|
+
a11y: {
|
|
2535
|
+
element: "section",
|
|
2536
|
+
ariaLabel: "Fill in the Blanks",
|
|
2537
|
+
keyboard: "Tab between text inputs.",
|
|
2538
|
+
notes: "H5P Fill in the Blanks equivalent."
|
|
2539
|
+
},
|
|
2540
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
2541
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
2542
|
+
},
|
|
2543
|
+
{
|
|
2544
|
+
type: "DragAndDrop",
|
|
2545
|
+
category: "assessment",
|
|
2546
|
+
assessmentContract: true,
|
|
2547
|
+
h5pMachineName: "H5P.DragQuestion",
|
|
2548
|
+
h5pAlias: "Drag and Drop",
|
|
2549
|
+
description: "Drag items onto labeled targets.",
|
|
2550
|
+
props: [
|
|
2551
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
2552
|
+
{ name: "items", type: "DragItem[]", required: true, description: "Draggable items." },
|
|
2553
|
+
{ name: "targets", type: "DropTarget[]", required: true, description: "Drop targets." },
|
|
2554
|
+
...assessmentBehaviourProps
|
|
2555
|
+
],
|
|
2556
|
+
requiredIds: ["checkId"],
|
|
2557
|
+
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
2558
|
+
a11y: {
|
|
2559
|
+
element: "section",
|
|
2560
|
+
ariaLabel: "Drag and Drop",
|
|
2561
|
+
keyboard: "Select item then activate target; drag also supported.",
|
|
2562
|
+
notes: "H5P Drag and Drop equivalent."
|
|
2563
|
+
},
|
|
2564
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
2565
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
2566
|
+
},
|
|
2567
|
+
{
|
|
2568
|
+
type: "DragTheWords",
|
|
2569
|
+
category: "assessment",
|
|
2570
|
+
assessmentContract: true,
|
|
2571
|
+
h5pMachineName: "H5P.DragText",
|
|
2572
|
+
h5pAlias: "Drag the Words",
|
|
2573
|
+
description: "Drag words into inline blanks.",
|
|
2574
|
+
props: [
|
|
2575
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
2576
|
+
{ name: "template", type: "string", required: true, description: "Sentence with *blank* zones." },
|
|
2577
|
+
{ name: "words", type: "string[]", required: true, description: "Draggable word bank." },
|
|
2578
|
+
...assessmentBehaviourProps
|
|
2579
|
+
],
|
|
2580
|
+
requiredIds: ["checkId"],
|
|
2581
|
+
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
2582
|
+
a11y: {
|
|
2583
|
+
element: "section",
|
|
2584
|
+
ariaLabel: "Drag the Words",
|
|
2585
|
+
keyboard: "Select word then activate zone.",
|
|
2586
|
+
notes: "H5P Drag the Words equivalent."
|
|
2587
|
+
},
|
|
2588
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
2589
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
2590
|
+
},
|
|
2591
|
+
{
|
|
2592
|
+
type: "MarkTheWords",
|
|
2593
|
+
category: "assessment",
|
|
2594
|
+
assessmentContract: true,
|
|
2595
|
+
h5pMachineName: "H5P.MarkTheWords",
|
|
2596
|
+
h5pAlias: "Mark the Words",
|
|
2597
|
+
description: "Select correct words in a sentence.",
|
|
2598
|
+
props: [
|
|
2599
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
2600
|
+
{ name: "text", type: "string", required: true, description: "Source text." },
|
|
2601
|
+
{ name: "correctWords", type: "string[]", required: true, description: "Words to mark." },
|
|
2602
|
+
...assessmentBehaviourProps
|
|
2603
|
+
],
|
|
2604
|
+
requiredIds: ["checkId"],
|
|
2605
|
+
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
2606
|
+
a11y: {
|
|
2607
|
+
element: "section",
|
|
2608
|
+
ariaLabel: "Mark the Words",
|
|
2609
|
+
keyboard: "Toggle words with buttons.",
|
|
2610
|
+
notes: "H5P Mark the Words equivalent."
|
|
2611
|
+
},
|
|
2612
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
2613
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
2614
|
+
},
|
|
2615
|
+
{
|
|
2616
|
+
type: "AssessmentSequence",
|
|
2617
|
+
category: "container",
|
|
2618
|
+
h5pMachineName: "H5P.QuestionSet",
|
|
2619
|
+
h5pAlias: "Question Set",
|
|
2620
|
+
description: "Ordered sequence of contract-compliant assessments.",
|
|
2621
|
+
props: [
|
|
2622
|
+
{ name: "children", type: "ReactNode", required: true, description: "Assessment blocks." },
|
|
2623
|
+
{ name: "sequential", type: "boolean", required: false, description: "One question at a time." },
|
|
2624
|
+
...assessmentBehaviourProps.filter((p) => p.name !== "passingScore")
|
|
2625
|
+
],
|
|
2626
|
+
requiredIds: [],
|
|
2627
|
+
parentConstraints: ["Lesson"],
|
|
2628
|
+
a11y: {
|
|
2629
|
+
element: "section",
|
|
2630
|
+
ariaLabel: "Assessment sequence",
|
|
2631
|
+
keyboard: "Previous/Next navigation between steps.",
|
|
2632
|
+
notes: "H5P Question Set equivalent."
|
|
2633
|
+
},
|
|
2634
|
+
theming: { surface: "global-inherit", stylingNotes: "Container for assessments." },
|
|
2635
|
+
telemetry: { emits: [], manualTracking: "Child assessments emit assessment_* events." }
|
|
2636
|
+
}
|
|
2637
|
+
];
|
|
2638
|
+
var BLOCK_CATALOG_V2 = [
|
|
2639
|
+
...BLOCK_CATALOG,
|
|
2640
|
+
...v2AssessmentEntries
|
|
2641
|
+
];
|
|
2642
|
+
function cloneCatalogEntry(entry) {
|
|
2643
|
+
return {
|
|
1488
2644
|
...entry,
|
|
1489
2645
|
props: entry.props.map((p) => ({ ...p })),
|
|
1490
2646
|
aliases: entry.aliases ? [...entry.aliases] : void 0,
|
|
@@ -1499,23 +2655,38 @@ function buildBlockCatalog() {
|
|
|
1499
2655
|
...entry.telemetry,
|
|
1500
2656
|
emits: [...entry.telemetry.emits]
|
|
1501
2657
|
}
|
|
1502
|
-
}
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
function buildBlockCatalog(opts) {
|
|
2661
|
+
const version = opts?.version ?? 2;
|
|
2662
|
+
const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
|
|
2663
|
+
return source.map((entry) => cloneCatalogEntry(entry));
|
|
1503
2664
|
}
|
|
1504
|
-
function getBlockCatalogEntry(type) {
|
|
1505
|
-
|
|
2665
|
+
function getBlockCatalogEntry(type, opts) {
|
|
2666
|
+
const version = opts?.version ?? 2;
|
|
2667
|
+
const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
|
|
2668
|
+
return source.find((entry) => entry.type === type || entry.aliases?.includes(type));
|
|
1506
2669
|
}
|
|
1507
2670
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1508
2671
|
0 && (module.exports = {
|
|
2672
|
+
AssessmentSequence,
|
|
1509
2673
|
BLOCK_CATALOG,
|
|
2674
|
+
BLOCK_CATALOG_V2,
|
|
1510
2675
|
Course,
|
|
2676
|
+
DragAndDrop,
|
|
2677
|
+
DragTheWords,
|
|
2678
|
+
FillInTheBlanks,
|
|
1511
2679
|
KnowledgeCheck,
|
|
1512
2680
|
Lesson,
|
|
1513
2681
|
LessonkitProvider,
|
|
2682
|
+
MarkTheWords,
|
|
1514
2683
|
ProgressTracker,
|
|
1515
2684
|
Quiz,
|
|
1516
2685
|
Reflection,
|
|
1517
2686
|
Scenario,
|
|
1518
2687
|
ThemeProvider,
|
|
2688
|
+
TrueFalse,
|
|
2689
|
+
blockCatalogV2Version,
|
|
1519
2690
|
blockCatalogVersion,
|
|
1520
2691
|
buildBlockCatalog,
|
|
1521
2692
|
buildTelemetryEvent,
|
|
@@ -1526,7 +2697,9 @@ function getBlockCatalogEntry(type) {
|
|
|
1526
2697
|
defineLifecyclePlugin,
|
|
1527
2698
|
defineTelemetryPlugin,
|
|
1528
2699
|
getBlockCatalogEntry,
|
|
2700
|
+
resetAssessmentWarningsForTests,
|
|
1529
2701
|
resetQuizWarningsForTests,
|
|
2702
|
+
useAssessmentState,
|
|
1530
2703
|
useCompletion,
|
|
1531
2704
|
useLessonkit,
|
|
1532
2705
|
useProgress,
|