@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.js
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
// src/components.tsx
|
|
2
|
-
import { useEffect as useEffect2, useId, useMemo as
|
|
2
|
+
import { useEffect as useEffect2, useId, useMemo as useMemo4, useRef as useRef2, useState as useState2 } from "react";
|
|
3
3
|
import { visuallyHiddenStyle } from "@lessonkit/accessibility";
|
|
4
4
|
|
|
5
|
+
// src/assessment/scoring.ts
|
|
6
|
+
function resolvePassingThreshold(passingScore, maxScore) {
|
|
7
|
+
return passingScore ?? maxScore;
|
|
8
|
+
}
|
|
9
|
+
function meetsPassingThreshold(score, maxScore, passingScore) {
|
|
10
|
+
const threshold = resolvePassingThreshold(passingScore, maxScore);
|
|
11
|
+
return score >= threshold;
|
|
12
|
+
}
|
|
13
|
+
function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
|
|
14
|
+
const maxScore = custom?.maxScore ?? fallbackMax;
|
|
15
|
+
if (custom?.passed !== void 0) {
|
|
16
|
+
const score2 = custom.passed ? custom.score ?? maxScore : custom.score ?? 0;
|
|
17
|
+
return { score: score2, maxScore, passed: custom.passed };
|
|
18
|
+
}
|
|
19
|
+
if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
20
|
+
const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
|
|
21
|
+
return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
|
|
22
|
+
}
|
|
23
|
+
const score = fallbackCorrect ? maxScore : 0;
|
|
24
|
+
const passed = meetsPassingThreshold(score, maxScore, passingScore);
|
|
25
|
+
return { score, maxScore, passed };
|
|
26
|
+
}
|
|
27
|
+
|
|
5
28
|
// src/context.tsx
|
|
6
29
|
import { createContext } from "react";
|
|
7
30
|
|
|
@@ -142,7 +165,40 @@ import {
|
|
|
142
165
|
|
|
143
166
|
// src/runtime/courseStartedPipeline.ts
|
|
144
167
|
import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement2 } from "@lessonkit/xapi";
|
|
145
|
-
function
|
|
168
|
+
function isDevEnvironment3() {
|
|
169
|
+
const g = globalThis;
|
|
170
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
171
|
+
}
|
|
172
|
+
function warnExtraSinkFailure(sinkId, err) {
|
|
173
|
+
if (isDevEnvironment3()) {
|
|
174
|
+
console.warn(
|
|
175
|
+
`[lessonkit] course_started extra sink "${sinkId}" failed:`,
|
|
176
|
+
err instanceof Error ? err.message : err
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function emitExtraSinks(sinks, event, emitCtx) {
|
|
181
|
+
await Promise.all(
|
|
182
|
+
sinks.map(async (sink) => {
|
|
183
|
+
let result;
|
|
184
|
+
try {
|
|
185
|
+
result = sink.emit(event, emitCtx);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
warnExtraSinkFailure(sink.id, err);
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
if (result != null && typeof result.then === "function") {
|
|
191
|
+
try {
|
|
192
|
+
await result;
|
|
193
|
+
} catch (err) {
|
|
194
|
+
warnExtraSinkFailure(sink.id, err);
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
async function emitCourseStartedNonTrackingPipeline(opts) {
|
|
146
202
|
let xapiStatementSent = false;
|
|
147
203
|
if (!opts.skipXapi && opts.xapi) {
|
|
148
204
|
const statement = telemetryEventToXAPIStatement2(opts.event);
|
|
@@ -157,9 +213,7 @@ function emitCourseStartedNonTrackingPipeline(opts) {
|
|
|
157
213
|
sessionId: opts.event.sessionId,
|
|
158
214
|
attemptId: opts.event.attemptId
|
|
159
215
|
};
|
|
160
|
-
|
|
161
|
-
sink.emit(opts.event, emitCtx);
|
|
162
|
-
}
|
|
216
|
+
await emitExtraSinks(opts.extraSinks ?? [], opts.event, emitCtx);
|
|
163
217
|
return { xapiStatementSent };
|
|
164
218
|
}
|
|
165
219
|
|
|
@@ -209,8 +263,12 @@ async function disposeTrackingClient(client) {
|
|
|
209
263
|
}
|
|
210
264
|
|
|
211
265
|
// src/provider/useLessonkitProviderRuntime.ts
|
|
212
|
-
var useIsoLayoutEffect =
|
|
266
|
+
var useIsoLayoutEffect = (
|
|
267
|
+
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
268
|
+
typeof window !== "undefined" ? useLayoutEffect : useEffect
|
|
269
|
+
);
|
|
213
270
|
var defaultStorage = createSessionStoragePort();
|
|
271
|
+
var courseStartedTrackingFlightKey = null;
|
|
214
272
|
function isTrackingActive(tracking) {
|
|
215
273
|
return tracking?.enabled !== false;
|
|
216
274
|
}
|
|
@@ -233,15 +291,41 @@ function buildCourseStartedEvent(opts) {
|
|
|
233
291
|
});
|
|
234
292
|
return opts.pluginHost ? opts.pluginHost.runTelemetry(built, pluginCtx) : built;
|
|
235
293
|
}
|
|
236
|
-
function
|
|
294
|
+
async function emitCourseStartedToTracking(tracking, storage, sessionId, courseId, event, shouldCommit) {
|
|
295
|
+
const flightKey = `${sessionId}:${courseId}`;
|
|
296
|
+
if (hasCourseStartedEmittedToTracking(storage, sessionId, courseId)) {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
if (courseStartedTrackingFlightKey === flightKey) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
courseStartedTrackingFlightKey = flightKey;
|
|
303
|
+
try {
|
|
304
|
+
if (shouldCommit && !shouldCommit()) return false;
|
|
305
|
+
tracking.track(event);
|
|
306
|
+
await tracking.flush?.();
|
|
307
|
+
if (shouldCommit && !shouldCommit()) return false;
|
|
308
|
+
markCourseStartedEmittedToTracking(storage, sessionId, courseId);
|
|
309
|
+
return true;
|
|
310
|
+
} catch {
|
|
311
|
+
return false;
|
|
312
|
+
} finally {
|
|
313
|
+
if (courseStartedTrackingFlightKey === flightKey) {
|
|
314
|
+
courseStartedTrackingFlightKey = null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async function emitCourseStartedPipelineOnly(opts) {
|
|
237
319
|
try {
|
|
238
|
-
|
|
320
|
+
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
321
|
+
const { xapiStatementSent } = await emitCourseStartedNonTrackingPipeline({
|
|
239
322
|
event: opts.event,
|
|
240
323
|
xapi: opts.xapi,
|
|
241
324
|
lxpackBridge: opts.lxpackBridge,
|
|
242
325
|
extraSinks: opts.extraSinks,
|
|
243
326
|
skipXapi: opts.skipXapi
|
|
244
327
|
});
|
|
328
|
+
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
245
329
|
markCourseStarted(opts.storage, opts.sessionId, opts.courseId);
|
|
246
330
|
markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId);
|
|
247
331
|
if (xapiStatementSent) {
|
|
@@ -252,47 +336,41 @@ function emitCourseStartedPipelineOnly(opts) {
|
|
|
252
336
|
return "failed";
|
|
253
337
|
}
|
|
254
338
|
}
|
|
255
|
-
function emitCourseStarted(opts) {
|
|
339
|
+
async function emitCourseStarted(opts) {
|
|
256
340
|
const event = buildCourseStartedEvent(opts);
|
|
257
341
|
if (event === null) return "filtered";
|
|
258
|
-
const
|
|
342
|
+
const tracked = await emitCourseStartedToTracking(
|
|
343
|
+
opts.tracking,
|
|
259
344
|
opts.storage,
|
|
260
345
|
opts.sessionId,
|
|
261
|
-
opts.courseId
|
|
346
|
+
opts.courseId,
|
|
347
|
+
event,
|
|
348
|
+
opts.shouldCommit
|
|
262
349
|
);
|
|
263
|
-
if (!
|
|
264
|
-
try {
|
|
265
|
-
opts.tracking.track(event);
|
|
266
|
-
markCourseStartedEmittedToTracking(opts.storage, opts.sessionId, opts.courseId);
|
|
267
|
-
} catch {
|
|
268
|
-
return "failed";
|
|
269
|
-
}
|
|
270
|
-
}
|
|
350
|
+
if (!tracked) return "failed";
|
|
271
351
|
return emitCourseStartedPipelineOnly({
|
|
272
352
|
...opts,
|
|
273
353
|
event,
|
|
274
354
|
skipXapi: opts.skipXapi,
|
|
275
|
-
onXapiStatementSent: opts.onXapiStatementSent
|
|
355
|
+
onXapiStatementSent: opts.onXapiStatementSent,
|
|
356
|
+
shouldCommit: opts.shouldCommit
|
|
276
357
|
});
|
|
277
358
|
}
|
|
278
|
-
function emitCourseStartedToTrackingOnly(opts) {
|
|
359
|
+
async function emitCourseStartedToTrackingOnly(opts) {
|
|
279
360
|
const event = buildCourseStartedEvent(opts);
|
|
280
361
|
if (event === null) return "filtered";
|
|
281
|
-
const
|
|
362
|
+
const tracked = await emitCourseStartedToTracking(
|
|
363
|
+
opts.tracking,
|
|
282
364
|
opts.storage,
|
|
283
365
|
opts.sessionId,
|
|
284
|
-
opts.courseId
|
|
366
|
+
opts.courseId,
|
|
367
|
+
event,
|
|
368
|
+
opts.shouldCommit
|
|
285
369
|
);
|
|
286
|
-
if (!
|
|
287
|
-
try {
|
|
288
|
-
opts.tracking.track(event);
|
|
289
|
-
markCourseStartedEmittedToTracking(opts.storage, opts.sessionId, opts.courseId);
|
|
290
|
-
} catch {
|
|
291
|
-
return "failed";
|
|
292
|
-
}
|
|
293
|
-
}
|
|
370
|
+
if (!tracked) return "failed";
|
|
294
371
|
try {
|
|
295
|
-
|
|
372
|
+
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
373
|
+
await emitCourseStartedNonTrackingPipeline({
|
|
296
374
|
event,
|
|
297
375
|
xapi: null,
|
|
298
376
|
lxpackBridge: opts.lxpackBridge,
|
|
@@ -305,7 +383,7 @@ function emitCourseStartedToTrackingOnly(opts) {
|
|
|
305
383
|
return "failed";
|
|
306
384
|
}
|
|
307
385
|
}
|
|
308
|
-
function emitPendingCourseStarted(opts) {
|
|
386
|
+
async function emitPendingCourseStarted(opts) {
|
|
309
387
|
const trackingEmitted = hasCourseStartedEmittedToTracking(
|
|
310
388
|
opts.storage,
|
|
311
389
|
opts.sessionId,
|
|
@@ -328,6 +406,9 @@ function emitPendingCourseStarted(opts) {
|
|
|
328
406
|
opts.sessionId,
|
|
329
407
|
opts.courseId
|
|
330
408
|
);
|
|
409
|
+
if (sessionStarted && trackingEmitted && pipelineDelivered) {
|
|
410
|
+
return "emitted";
|
|
411
|
+
}
|
|
331
412
|
if (sessionStarted && trackingEmitted && !pipelineDelivered) {
|
|
332
413
|
const event = buildCourseStartedEvent(opts);
|
|
333
414
|
if (event === null) return "filtered";
|
|
@@ -379,6 +460,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
379
460
|
pluginHostRef.current = pluginHost;
|
|
380
461
|
const progressRef = useRef(createProgressController());
|
|
381
462
|
const courseStartedEmittedToSinkRef = useRef(false);
|
|
463
|
+
const courseStartedEmitGenerationRef = useRef(0);
|
|
382
464
|
const prevCourseIdForProgressRef = useRef(normalizedCourseId);
|
|
383
465
|
const pendingCourseIdResetRef = useRef(false);
|
|
384
466
|
const prevUseV2RuntimeRef = useRef(useV2Runtime);
|
|
@@ -398,6 +480,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
398
480
|
}
|
|
399
481
|
pendingCourseIdResetRef.current = true;
|
|
400
482
|
courseStartedEmittedToSinkRef.current = false;
|
|
483
|
+
courseStartedEmitGenerationRef.current += 1;
|
|
401
484
|
} else if (useV2Runtime && !headlessRef.current) {
|
|
402
485
|
headlessRef.current = createLessonkitRuntime({
|
|
403
486
|
courseId: normalizedCourseId,
|
|
@@ -415,6 +498,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
415
498
|
}
|
|
416
499
|
pendingCourseIdResetRef.current = true;
|
|
417
500
|
courseStartedEmittedToSinkRef.current = false;
|
|
501
|
+
courseStartedEmitGenerationRef.current += 1;
|
|
418
502
|
}
|
|
419
503
|
if (useV2Runtime && headlessRef.current) {
|
|
420
504
|
progressRef.current = headlessRef.current.progress;
|
|
@@ -528,7 +612,10 @@ function useLessonkitProviderRuntime(config) {
|
|
|
528
612
|
const baseSink = normalizedConfig.tracking?.sink;
|
|
529
613
|
const userBatchSink = normalizedConfig.tracking?.batchSink;
|
|
530
614
|
assertTrackingSinkConfig(normalizedConfig.tracking);
|
|
531
|
-
const sink = pluginHostRef.current && baseSink ?
|
|
615
|
+
const sink = pluginHostRef.current && baseSink ? (
|
|
616
|
+
/* v8 ignore next -- composeTrackingSink may return null; fall back to base sink */
|
|
617
|
+
pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink
|
|
618
|
+
) : baseSink;
|
|
532
619
|
const batchSink = pluginHostRef.current && userBatchSink ? async (events) => {
|
|
533
620
|
const host = pluginHostRef.current;
|
|
534
621
|
const ctx = buildCurrentPluginCtx();
|
|
@@ -552,30 +639,39 @@ function useLessonkitProviderRuntime(config) {
|
|
|
552
639
|
const sessionId = sessionIdRef.current;
|
|
553
640
|
const cid = courseIdRef.current;
|
|
554
641
|
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
642
|
+
const courseStartedFullySettled = hasCourseStartedEmittedToTracking(defaultStorage, sessionId, cid) && hasCourseStarted(defaultStorage, sessionId, cid) && hasCourseStartedPipelineDelivered(defaultStorage, sessionId, cid);
|
|
555
643
|
if (!trackingActive) {
|
|
556
644
|
courseStartedEmittedToSinkRef.current = false;
|
|
557
|
-
} else if (
|
|
558
|
-
const result = emitPendingCourseStarted({
|
|
559
|
-
pluginHost: pluginHostRef.current,
|
|
560
|
-
tracking: next,
|
|
561
|
-
xapi: xapiRef.current,
|
|
562
|
-
storage: defaultStorage,
|
|
563
|
-
sessionId,
|
|
564
|
-
courseId: cid,
|
|
565
|
-
attemptId: attemptIdRef.current,
|
|
566
|
-
user: userRef.current,
|
|
567
|
-
lxpackBridge: lxpackBridgeModeRef.current,
|
|
568
|
-
extraSinks: extraSinksRef.current,
|
|
569
|
-
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
570
|
-
onXapiStatementSent: () => {
|
|
571
|
-
xapiCourseStartedSentOnClientRef.current = true;
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
575
|
-
} else if (trackingActive) {
|
|
645
|
+
} else if (courseStartedFullySettled) {
|
|
576
646
|
courseStartedEmittedToSinkRef.current = true;
|
|
647
|
+
} else if (!courseStartedEmittedToSinkRef.current) {
|
|
648
|
+
const generation = ++courseStartedEmitGenerationRef.current;
|
|
649
|
+
const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
|
|
650
|
+
void (async () => {
|
|
651
|
+
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
652
|
+
const result = await emitPendingCourseStarted({
|
|
653
|
+
pluginHost: pluginHostRef.current,
|
|
654
|
+
tracking: next,
|
|
655
|
+
xapi: xapiRef.current,
|
|
656
|
+
storage: defaultStorage,
|
|
657
|
+
sessionId,
|
|
658
|
+
courseId: cid,
|
|
659
|
+
attemptId: attemptIdRef.current,
|
|
660
|
+
user: userRef.current,
|
|
661
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
662
|
+
extraSinks: extraSinksRef.current,
|
|
663
|
+
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
664
|
+
onXapiStatementSent: () => {
|
|
665
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
666
|
+
},
|
|
667
|
+
shouldCommit
|
|
668
|
+
});
|
|
669
|
+
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
670
|
+
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
671
|
+
})();
|
|
577
672
|
}
|
|
578
673
|
return () => {
|
|
674
|
+
courseStartedEmitGenerationRef.current += 1;
|
|
579
675
|
if (prev !== trackingRef.current) {
|
|
580
676
|
void disposeTrackingClient(prev);
|
|
581
677
|
}
|
|
@@ -652,7 +748,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
652
748
|
} catch {
|
|
653
749
|
}
|
|
654
750
|
if (!courseStartedEmittedToSinkRef.current) {
|
|
655
|
-
const result = emitPendingCourseStarted({
|
|
751
|
+
const result = await emitPendingCourseStarted({
|
|
656
752
|
pluginHost: pluginHostRef.current,
|
|
657
753
|
tracking: trackingRef.current,
|
|
658
754
|
xapi: xapiRef.current,
|
|
@@ -678,7 +774,10 @@ function useLessonkitProviderRuntime(config) {
|
|
|
678
774
|
[track]
|
|
679
775
|
);
|
|
680
776
|
const completeLesson = useCallback(
|
|
681
|
-
(lessonId) => {
|
|
777
|
+
(lessonId, opts) => {
|
|
778
|
+
if (opts?.courseId !== void 0 && opts.courseId !== courseIdRef.current) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
682
781
|
if (useV2Runtime && headlessRef.current) {
|
|
683
782
|
headlessRef.current.completeLesson(lessonId, emitLifecycleEvent);
|
|
684
783
|
syncProgress();
|
|
@@ -853,7 +952,27 @@ function LessonkitProvider(props) {
|
|
|
853
952
|
}
|
|
854
953
|
|
|
855
954
|
// src/hooks.ts
|
|
856
|
-
import { useContext, useMemo as
|
|
955
|
+
import { useContext, useMemo as useMemo3 } from "react";
|
|
956
|
+
|
|
957
|
+
// src/assessment/useAssessmentState.ts
|
|
958
|
+
import { useMemo as useMemo2 } from "react";
|
|
959
|
+
function useAssessmentState(enclosingLessonId) {
|
|
960
|
+
const { track } = useLessonkit();
|
|
961
|
+
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
962
|
+
return useMemo2(
|
|
963
|
+
() => ({
|
|
964
|
+
answer: (data) => {
|
|
965
|
+
track("assessment_answered", data, trackOpts);
|
|
966
|
+
},
|
|
967
|
+
complete: (data) => {
|
|
968
|
+
track("assessment_completed", data, trackOpts);
|
|
969
|
+
}
|
|
970
|
+
}),
|
|
971
|
+
[track, enclosingLessonId]
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// src/hooks.ts
|
|
857
976
|
function useLessonkit() {
|
|
858
977
|
const ctx = useContext(LessonkitContext);
|
|
859
978
|
if (!ctx) throw new Error("LessonKit: missing LessonkitProvider");
|
|
@@ -865,16 +984,16 @@ function useProgress() {
|
|
|
865
984
|
}
|
|
866
985
|
function useTracking() {
|
|
867
986
|
const { track } = useLessonkit();
|
|
868
|
-
return
|
|
987
|
+
return useMemo3(() => ({ track }), [track]);
|
|
869
988
|
}
|
|
870
989
|
function useCompletion() {
|
|
871
990
|
const { completeLesson, completeCourse } = useLessonkit();
|
|
872
|
-
return
|
|
991
|
+
return useMemo3(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
|
|
873
992
|
}
|
|
874
993
|
function useQuizState(enclosingLessonId) {
|
|
875
994
|
const { track } = useLessonkit();
|
|
876
995
|
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
877
|
-
return
|
|
996
|
+
return useMemo3(
|
|
878
997
|
() => ({
|
|
879
998
|
answer: (opts) => {
|
|
880
999
|
track("quiz_answered", opts, trackOpts);
|
|
@@ -896,7 +1015,7 @@ function useEnclosingLessonId() {
|
|
|
896
1015
|
|
|
897
1016
|
// src/runtime/validateComponentId.ts
|
|
898
1017
|
import { assertValidId as assertValidId2 } from "@lessonkit/core";
|
|
899
|
-
function
|
|
1018
|
+
function isDevEnvironment4() {
|
|
900
1019
|
const g = globalThis;
|
|
901
1020
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
902
1021
|
}
|
|
@@ -912,7 +1031,7 @@ function normalizeComponentId(id, path) {
|
|
|
912
1031
|
var mountCounts = /* @__PURE__ */ new Map();
|
|
913
1032
|
var warnedConcurrentLessons = false;
|
|
914
1033
|
function registerLessonMount(lessonId) {
|
|
915
|
-
if (
|
|
1034
|
+
if (isDevEnvironment4() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
|
|
916
1035
|
warnedConcurrentLessons = true;
|
|
917
1036
|
console.warn(
|
|
918
1037
|
"[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."
|
|
@@ -939,8 +1058,8 @@ function resetQuizWarningsForTests() {
|
|
|
939
1058
|
warnedQuizOutsideLesson = false;
|
|
940
1059
|
}
|
|
941
1060
|
function Course(props) {
|
|
942
|
-
const courseId =
|
|
943
|
-
const providerConfig =
|
|
1061
|
+
const courseId = useMemo4(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
|
|
1062
|
+
const providerConfig = useMemo4(
|
|
944
1063
|
() => ({ ...props.config, courseId }),
|
|
945
1064
|
[props.config, courseId]
|
|
946
1065
|
);
|
|
@@ -950,14 +1069,23 @@ function Course(props) {
|
|
|
950
1069
|
] }) });
|
|
951
1070
|
}
|
|
952
1071
|
function Lesson(props) {
|
|
953
|
-
const lessonId =
|
|
1072
|
+
const lessonId = useMemo4(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
954
1073
|
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
955
1074
|
const { setActiveLesson, config } = useLessonkit();
|
|
956
1075
|
const { completeLesson } = useCompletion();
|
|
957
1076
|
const lessonMountGenerationRef = useRef2(0);
|
|
1077
|
+
const liveCourseIdRef = useRef2(config.courseId);
|
|
1078
|
+
liveCourseIdRef.current = config.courseId;
|
|
958
1079
|
useEffect2(() => {
|
|
959
1080
|
const unregister = registerLessonMount(lessonId);
|
|
960
1081
|
const generation = ++lessonMountGenerationRef.current;
|
|
1082
|
+
const mountedCourseId = config.courseId;
|
|
1083
|
+
let effectSurvivedTick = false;
|
|
1084
|
+
queueMicrotask(() => {
|
|
1085
|
+
queueMicrotask(() => {
|
|
1086
|
+
effectSurvivedTick = true;
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
961
1089
|
setActiveLesson(lessonId);
|
|
962
1090
|
return () => {
|
|
963
1091
|
unregister();
|
|
@@ -966,8 +1094,10 @@ function Lesson(props) {
|
|
|
966
1094
|
}
|
|
967
1095
|
if (!autoComplete) return;
|
|
968
1096
|
queueMicrotask(() => {
|
|
1097
|
+
if (!effectSurvivedTick) return;
|
|
969
1098
|
if (lessonMountGenerationRef.current !== generation) return;
|
|
970
|
-
|
|
1099
|
+
if (liveCourseIdRef.current !== mountedCourseId) return;
|
|
1100
|
+
completeLesson(lessonId, { courseId: mountedCourseId });
|
|
971
1101
|
});
|
|
972
1102
|
};
|
|
973
1103
|
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
@@ -977,14 +1107,14 @@ function Lesson(props) {
|
|
|
977
1107
|
] }) });
|
|
978
1108
|
}
|
|
979
1109
|
function Scenario(props) {
|
|
980
|
-
const blockId =
|
|
1110
|
+
const blockId = useMemo4(
|
|
981
1111
|
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
982
1112
|
[props.blockId]
|
|
983
1113
|
);
|
|
984
1114
|
return /* @__PURE__ */ jsx2("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
985
1115
|
}
|
|
986
1116
|
function Reflection(props) {
|
|
987
|
-
const blockId =
|
|
1117
|
+
const blockId = useMemo4(
|
|
988
1118
|
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
989
1119
|
[props.blockId]
|
|
990
1120
|
);
|
|
@@ -1026,11 +1156,10 @@ function KnowledgeCheck(props) {
|
|
|
1026
1156
|
);
|
|
1027
1157
|
}
|
|
1028
1158
|
function Quiz(props) {
|
|
1029
|
-
const checkId = useMemo3(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1030
1159
|
const enclosingLessonId = useEnclosingLessonId();
|
|
1031
1160
|
const missingLesson = enclosingLessonId === void 0;
|
|
1032
1161
|
useEffect2(() => {
|
|
1033
|
-
if (!missingLesson ||
|
|
1162
|
+
if (!missingLesson || isDevEnvironment4()) return;
|
|
1034
1163
|
if (!warnedQuizOutsideLesson) {
|
|
1035
1164
|
warnedQuizOutsideLesson = true;
|
|
1036
1165
|
console.error(
|
|
@@ -1038,9 +1167,17 @@ function Quiz(props) {
|
|
|
1038
1167
|
);
|
|
1039
1168
|
}
|
|
1040
1169
|
}, [missingLesson]);
|
|
1041
|
-
if (missingLesson &&
|
|
1170
|
+
if (missingLesson && isDevEnvironment4()) {
|
|
1042
1171
|
throw new Error("[lessonkit] <Quiz> must be wrapped in <Lesson>");
|
|
1043
1172
|
}
|
|
1173
|
+
if (missingLesson) {
|
|
1174
|
+
return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": props.checkId, children: /* @__PURE__ */ jsx2("p", { children: "Quiz must be placed inside a Lesson." }) });
|
|
1175
|
+
}
|
|
1176
|
+
return /* @__PURE__ */ jsx2(QuizInner, { ...props, enclosingLessonId });
|
|
1177
|
+
}
|
|
1178
|
+
function QuizInner(props) {
|
|
1179
|
+
const { enclosingLessonId } = props;
|
|
1180
|
+
const checkId = useMemo4(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1044
1181
|
const quiz = useQuizState(enclosingLessonId);
|
|
1045
1182
|
const { plugins, config, session } = useLessonkit();
|
|
1046
1183
|
const [selected, setSelected] = useState2(null);
|
|
@@ -1058,14 +1195,11 @@ function Quiz(props) {
|
|
|
1058
1195
|
const isChoiceCorrect = (choice, custom) => {
|
|
1059
1196
|
if (!custom) return choice === props.answer;
|
|
1060
1197
|
if (custom.passed !== void 0) return custom.passed;
|
|
1061
|
-
if (custom.maxScore != null && custom.maxScore > 0) {
|
|
1062
|
-
return custom.score
|
|
1198
|
+
if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
1199
|
+
return meetsPassingThreshold(custom.score, custom.maxScore, props.passingScore);
|
|
1063
1200
|
}
|
|
1064
1201
|
return choice === props.answer;
|
|
1065
1202
|
};
|
|
1066
|
-
if (missingLesson) {
|
|
1067
|
-
return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": checkId, children: /* @__PURE__ */ jsx2("p", { children: "Quiz must be placed inside a Lesson." }) });
|
|
1068
|
-
}
|
|
1069
1203
|
const passed = quizPassed;
|
|
1070
1204
|
return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
1071
1205
|
/* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
|
|
@@ -1112,7 +1246,7 @@ function Quiz(props) {
|
|
|
1112
1246
|
const maxScore = custom?.maxScore ?? 1;
|
|
1113
1247
|
quiz.complete({
|
|
1114
1248
|
checkId,
|
|
1115
|
-
score: custom?.score ??
|
|
1249
|
+
score: custom?.score ?? maxScore,
|
|
1116
1250
|
maxScore,
|
|
1117
1251
|
passingScore: props.passingScore ?? maxScore
|
|
1118
1252
|
});
|
|
@@ -1155,6 +1289,859 @@ function ProgressTracker(props) {
|
|
|
1155
1289
|
] }) });
|
|
1156
1290
|
}
|
|
1157
1291
|
|
|
1292
|
+
// src/blocks/TrueFalse.tsx
|
|
1293
|
+
import React5, { forwardRef, useEffect as useEffect4, useImperativeHandle, useMemo as useMemo6, useRef as useRef4, useState as useState3 } from "react";
|
|
1294
|
+
|
|
1295
|
+
// src/assessment/AssessmentLessonGuard.tsx
|
|
1296
|
+
import { useEffect as useEffect3 } from "react";
|
|
1297
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1298
|
+
var warnedAssessmentOutsideLesson = false;
|
|
1299
|
+
function resetAssessmentWarningsForTests() {
|
|
1300
|
+
warnedAssessmentOutsideLesson = false;
|
|
1301
|
+
}
|
|
1302
|
+
function AssessmentLessonGuard(props) {
|
|
1303
|
+
const enclosingLessonId = useEnclosingLessonId();
|
|
1304
|
+
const missingLesson = enclosingLessonId === void 0;
|
|
1305
|
+
useEffect3(() => {
|
|
1306
|
+
if (!missingLesson || isDevEnvironment4()) return;
|
|
1307
|
+
if (!warnedAssessmentOutsideLesson) {
|
|
1308
|
+
warnedAssessmentOutsideLesson = true;
|
|
1309
|
+
console.error(
|
|
1310
|
+
`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>; assessment telemetry will not be emitted.`
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
}, [missingLesson, props.blockLabel]);
|
|
1314
|
+
if (missingLesson && isDevEnvironment4()) {
|
|
1315
|
+
throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
|
|
1316
|
+
}
|
|
1317
|
+
if (missingLesson) {
|
|
1318
|
+
return /* @__PURE__ */ jsx3("section", { role: "alert", "aria-label": `${props.blockLabel} configuration error`, "data-lk-check-id": props.checkId, children: /* @__PURE__ */ jsxs2("p", { children: [
|
|
1319
|
+
props.blockLabel,
|
|
1320
|
+
" must be placed inside a Lesson."
|
|
1321
|
+
] }) });
|
|
1322
|
+
}
|
|
1323
|
+
return /* @__PURE__ */ jsx3(Fragment, { children: props.children(enclosingLessonId) });
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// src/assessment/AssessmentSequenceContext.tsx
|
|
1327
|
+
import React4, { createContext as createContext3, useCallback as useCallback2, useContext as useContext3, useMemo as useMemo5, useRef as useRef3 } from "react";
|
|
1328
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1329
|
+
var AssessmentSequenceContext = createContext3(null);
|
|
1330
|
+
function AssessmentSequenceProvider({ children }) {
|
|
1331
|
+
const registryRef = useRef3(/* @__PURE__ */ new Map());
|
|
1332
|
+
const register = useCallback2((checkId, handle) => {
|
|
1333
|
+
registryRef.current.set(checkId, handle);
|
|
1334
|
+
return () => {
|
|
1335
|
+
registryRef.current.delete(checkId);
|
|
1336
|
+
};
|
|
1337
|
+
}, []);
|
|
1338
|
+
const value = useMemo5(
|
|
1339
|
+
() => ({
|
|
1340
|
+
register,
|
|
1341
|
+
getHandles: () => registryRef.current
|
|
1342
|
+
}),
|
|
1343
|
+
[register]
|
|
1344
|
+
);
|
|
1345
|
+
return /* @__PURE__ */ jsx4(AssessmentSequenceContext.Provider, { value, children });
|
|
1346
|
+
}
|
|
1347
|
+
function useAssessmentSequenceRegistry() {
|
|
1348
|
+
return useContext3(AssessmentSequenceContext);
|
|
1349
|
+
}
|
|
1350
|
+
function useRegisterAssessmentHandle(checkId, handle) {
|
|
1351
|
+
const ctx = useAssessmentSequenceRegistry();
|
|
1352
|
+
React4.useEffect(() => {
|
|
1353
|
+
if (!ctx || !handle) return;
|
|
1354
|
+
return ctx.register(checkId, handle);
|
|
1355
|
+
}, [ctx, checkId, handle]);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// src/blocks/TrueFalse.tsx
|
|
1359
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1360
|
+
var INTERACTION = "trueFalse";
|
|
1361
|
+
function TrueFalseInner(props, ref) {
|
|
1362
|
+
const { enclosingLessonId } = props;
|
|
1363
|
+
const checkId = useMemo6(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1364
|
+
const assessment = useAssessmentState(enclosingLessonId);
|
|
1365
|
+
const { plugins, config, session } = useLessonkit();
|
|
1366
|
+
const [selected, setSelected] = useState3(null);
|
|
1367
|
+
const [selectionCorrect, setSelectionCorrect] = useState3(null);
|
|
1368
|
+
const [showSolutions, setShowSolutions] = useState3(false);
|
|
1369
|
+
const [passed, setPassed] = useState3(false);
|
|
1370
|
+
const completedRef = useRef4(false);
|
|
1371
|
+
const questionId = React5.useId();
|
|
1372
|
+
const reset = () => {
|
|
1373
|
+
completedRef.current = false;
|
|
1374
|
+
setPassed(false);
|
|
1375
|
+
setSelected(null);
|
|
1376
|
+
setSelectionCorrect(null);
|
|
1377
|
+
setShowSolutions(false);
|
|
1378
|
+
};
|
|
1379
|
+
useEffect4(() => {
|
|
1380
|
+
reset();
|
|
1381
|
+
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
|
|
1382
|
+
const handle = useMemo6(() => {
|
|
1383
|
+
const maxScore = 1;
|
|
1384
|
+
const score = passed ? maxScore : selected === null ? 0 : selected === props.answer ? maxScore : 0;
|
|
1385
|
+
return {
|
|
1386
|
+
getScore: () => score,
|
|
1387
|
+
getMaxScore: () => maxScore,
|
|
1388
|
+
getAnswerGiven: () => selected !== null,
|
|
1389
|
+
resetTask: reset,
|
|
1390
|
+
showSolutions: () => setShowSolutions(true),
|
|
1391
|
+
getXAPIData: () => ({
|
|
1392
|
+
checkId,
|
|
1393
|
+
interactionType: INTERACTION,
|
|
1394
|
+
response: selected ?? void 0,
|
|
1395
|
+
correct: selected === props.answer,
|
|
1396
|
+
score,
|
|
1397
|
+
maxScore
|
|
1398
|
+
})
|
|
1399
|
+
};
|
|
1400
|
+
}, [checkId, passed, props.answer, selected]);
|
|
1401
|
+
useImperativeHandle(ref, () => handle, [handle]);
|
|
1402
|
+
useRegisterAssessmentHandle(checkId, handle);
|
|
1403
|
+
const submit = (value) => {
|
|
1404
|
+
if (passed && !props.enableRetry) return;
|
|
1405
|
+
setSelected(value);
|
|
1406
|
+
const pluginCtx = buildPluginContext({
|
|
1407
|
+
courseId: config.courseId,
|
|
1408
|
+
sessionId: session.sessionId,
|
|
1409
|
+
attemptId: session.attemptId,
|
|
1410
|
+
user: session.user
|
|
1411
|
+
});
|
|
1412
|
+
const custom = plugins?.scoreAssessment(
|
|
1413
|
+
{ checkId, lessonId: enclosingLessonId, response: value },
|
|
1414
|
+
pluginCtx
|
|
1415
|
+
) ?? null;
|
|
1416
|
+
const correct = value === props.answer;
|
|
1417
|
+
const scored = scoreFromCustom(custom, correct, 1, props.passingScore);
|
|
1418
|
+
setSelectionCorrect(scored.passed);
|
|
1419
|
+
assessment.answer({
|
|
1420
|
+
checkId,
|
|
1421
|
+
interactionType: INTERACTION,
|
|
1422
|
+
question: props.question,
|
|
1423
|
+
response: value,
|
|
1424
|
+
correct: scored.passed
|
|
1425
|
+
});
|
|
1426
|
+
if (scored.passed && !completedRef.current) {
|
|
1427
|
+
completedRef.current = true;
|
|
1428
|
+
setPassed(true);
|
|
1429
|
+
assessment.complete({
|
|
1430
|
+
checkId,
|
|
1431
|
+
interactionType: INTERACTION,
|
|
1432
|
+
score: scored.score,
|
|
1433
|
+
maxScore: scored.maxScore,
|
|
1434
|
+
passingScore: props.passingScore ?? scored.maxScore
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
1439
|
+
return /* @__PURE__ */ jsxs3("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
|
|
1440
|
+
/* @__PURE__ */ jsx5("p", { id: questionId, children: props.question }),
|
|
1441
|
+
/* @__PURE__ */ jsxs3("fieldset", { "aria-labelledby": questionId, children: [
|
|
1442
|
+
/* @__PURE__ */ jsx5("legend", { className: "lk-visually-hidden", children: "True or False" }),
|
|
1443
|
+
/* @__PURE__ */ jsxs3("label", { style: { display: "block", marginRight: "1rem" }, children: [
|
|
1444
|
+
/* @__PURE__ */ jsx5(
|
|
1445
|
+
"input",
|
|
1446
|
+
{
|
|
1447
|
+
type: "radio",
|
|
1448
|
+
name: `${questionId}-tf`,
|
|
1449
|
+
checked: selected === true,
|
|
1450
|
+
disabled: passed && !props.enableRetry,
|
|
1451
|
+
onChange: () => submit(true)
|
|
1452
|
+
}
|
|
1453
|
+
),
|
|
1454
|
+
"True"
|
|
1455
|
+
] }),
|
|
1456
|
+
/* @__PURE__ */ jsxs3("label", { style: { display: "block" }, children: [
|
|
1457
|
+
/* @__PURE__ */ jsx5(
|
|
1458
|
+
"input",
|
|
1459
|
+
{
|
|
1460
|
+
type: "radio",
|
|
1461
|
+
name: `${questionId}-tf`,
|
|
1462
|
+
checked: selected === false,
|
|
1463
|
+
disabled: passed && !props.enableRetry,
|
|
1464
|
+
onChange: () => submit(false)
|
|
1465
|
+
}
|
|
1466
|
+
),
|
|
1467
|
+
"False"
|
|
1468
|
+
] })
|
|
1469
|
+
] }),
|
|
1470
|
+
reveal ? /* @__PURE__ */ jsxs3("p", { children: [
|
|
1471
|
+
"Correct answer: ",
|
|
1472
|
+
/* @__PURE__ */ jsx5("strong", { children: props.answer ? "True" : "False" })
|
|
1473
|
+
] }) : null,
|
|
1474
|
+
selected !== null && selectionCorrect !== null ? /* @__PURE__ */ jsx5("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
|
|
1475
|
+
props.enableRetry && passed ? /* @__PURE__ */ jsx5("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
1476
|
+
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx5("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1477
|
+
] });
|
|
1478
|
+
}
|
|
1479
|
+
var TrueFalseInnerForwarded = forwardRef(TrueFalseInner);
|
|
1480
|
+
var TrueFalse = forwardRef(function TrueFalse2(props, ref) {
|
|
1481
|
+
return /* @__PURE__ */ jsx5(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx5(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
// src/blocks/MarkTheWords.tsx
|
|
1485
|
+
import React6, { forwardRef as forwardRef2, useEffect as useEffect5, useImperativeHandle as useImperativeHandle2, useMemo as useMemo7, useRef as useRef5, useState as useState4 } from "react";
|
|
1486
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1487
|
+
var INTERACTION2 = "markTheWords";
|
|
1488
|
+
function tokenize(text) {
|
|
1489
|
+
return text.split(/(\s+)/).filter((t) => t.length > 0);
|
|
1490
|
+
}
|
|
1491
|
+
function MarkTheWordsInner(props, ref) {
|
|
1492
|
+
const checkId = useMemo7(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1493
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1494
|
+
const tokens = useMemo7(() => tokenize(props.text), [props.text]);
|
|
1495
|
+
const correctSet = useMemo7(
|
|
1496
|
+
() => new Set(props.correctWords.map((w) => w.toLowerCase())),
|
|
1497
|
+
[props.correctWords]
|
|
1498
|
+
);
|
|
1499
|
+
const [marked, setMarked] = useState4(() => /* @__PURE__ */ new Set());
|
|
1500
|
+
const [passed, setPassed] = useState4(false);
|
|
1501
|
+
const [showSolutions, setShowSolutions] = useState4(false);
|
|
1502
|
+
const completedRef = useRef5(false);
|
|
1503
|
+
const reset = () => {
|
|
1504
|
+
completedRef.current = false;
|
|
1505
|
+
setPassed(false);
|
|
1506
|
+
setMarked(/* @__PURE__ */ new Set());
|
|
1507
|
+
setShowSolutions(false);
|
|
1508
|
+
};
|
|
1509
|
+
useEffect5(() => {
|
|
1510
|
+
reset();
|
|
1511
|
+
}, [checkId, props.text, props.correctWords.join("\0")]);
|
|
1512
|
+
const selectableIndices = useMemo7(() => {
|
|
1513
|
+
const indices = [];
|
|
1514
|
+
tokens.forEach((t, i) => {
|
|
1515
|
+
if (!/^\s+$/.test(t) && correctSet.has(t.toLowerCase())) indices.push(i);
|
|
1516
|
+
});
|
|
1517
|
+
return indices;
|
|
1518
|
+
}, [tokens, correctSet]);
|
|
1519
|
+
const hasTargets = selectableIndices.length > 0;
|
|
1520
|
+
const allMarked = hasTargets && selectableIndices.every((i) => marked.has(i));
|
|
1521
|
+
const maxScore = selectableIndices.length;
|
|
1522
|
+
const score = allMarked ? maxScore : marked.size;
|
|
1523
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
1524
|
+
const handle = useMemo7(() => {
|
|
1525
|
+
const handleMax = maxScore || 1;
|
|
1526
|
+
return {
|
|
1527
|
+
getScore: () => score,
|
|
1528
|
+
getMaxScore: () => handleMax,
|
|
1529
|
+
getAnswerGiven: () => marked.size > 0,
|
|
1530
|
+
resetTask: reset,
|
|
1531
|
+
showSolutions: () => setShowSolutions(true),
|
|
1532
|
+
getXAPIData: () => ({
|
|
1533
|
+
checkId,
|
|
1534
|
+
interactionType: INTERACTION2,
|
|
1535
|
+
response: [...marked].map((i) => tokens[i]),
|
|
1536
|
+
correct: passedThreshold,
|
|
1537
|
+
score,
|
|
1538
|
+
maxScore: handleMax
|
|
1539
|
+
})
|
|
1540
|
+
};
|
|
1541
|
+
}, [checkId, marked, maxScore, passedThreshold, score, tokens]);
|
|
1542
|
+
useImperativeHandle2(ref, () => handle, [handle]);
|
|
1543
|
+
useRegisterAssessmentHandle(checkId, handle);
|
|
1544
|
+
const toggle = (index) => {
|
|
1545
|
+
if (passed && !props.enableRetry) return;
|
|
1546
|
+
setMarked((prev) => {
|
|
1547
|
+
const next = new Set(prev);
|
|
1548
|
+
if (next.has(index)) next.delete(index);
|
|
1549
|
+
else next.add(index);
|
|
1550
|
+
return next;
|
|
1551
|
+
});
|
|
1552
|
+
};
|
|
1553
|
+
useEffect5(() => {
|
|
1554
|
+
if (!hasTargets) {
|
|
1555
|
+
if (isDevEnvironment4()) {
|
|
1556
|
+
console.warn(
|
|
1557
|
+
"[lessonkit] MarkTheWords: no tokens match correctWords",
|
|
1558
|
+
props.correctWords
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
if (!passedThreshold || completedRef.current) return;
|
|
1564
|
+
completedRef.current = true;
|
|
1565
|
+
setPassed(true);
|
|
1566
|
+
assessment.answer({
|
|
1567
|
+
checkId,
|
|
1568
|
+
interactionType: INTERACTION2,
|
|
1569
|
+
question: props.text,
|
|
1570
|
+
response: [...marked].map((i) => tokens[i]),
|
|
1571
|
+
correct: true
|
|
1572
|
+
});
|
|
1573
|
+
assessment.complete({
|
|
1574
|
+
checkId,
|
|
1575
|
+
interactionType: INTERACTION2,
|
|
1576
|
+
score,
|
|
1577
|
+
maxScore,
|
|
1578
|
+
passingScore: props.passingScore ?? maxScore
|
|
1579
|
+
});
|
|
1580
|
+
}, [
|
|
1581
|
+
assessment,
|
|
1582
|
+
checkId,
|
|
1583
|
+
hasTargets,
|
|
1584
|
+
marked,
|
|
1585
|
+
maxScore,
|
|
1586
|
+
passedThreshold,
|
|
1587
|
+
props.passingScore,
|
|
1588
|
+
props.correctWords,
|
|
1589
|
+
props.text,
|
|
1590
|
+
score,
|
|
1591
|
+
tokens
|
|
1592
|
+
]);
|
|
1593
|
+
return /* @__PURE__ */ jsxs4("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
|
|
1594
|
+
!hasTargets ? /* @__PURE__ */ jsxs4("p", { role: "alert", children: [
|
|
1595
|
+
"No words in this sentence match ",
|
|
1596
|
+
/* @__PURE__ */ jsx6("code", { children: "correctWords" }),
|
|
1597
|
+
". Check spelling and capitalization in the source text."
|
|
1598
|
+
] }) : null,
|
|
1599
|
+
/* @__PURE__ */ jsx6("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
|
|
1600
|
+
/* @__PURE__ */ jsx6("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
|
|
1601
|
+
const isWord = !/^\s+$/.test(token);
|
|
1602
|
+
const isTarget = isWord && correctSet.has(token.toLowerCase());
|
|
1603
|
+
if (!isTarget) return /* @__PURE__ */ jsx6(React6.Fragment, { children: token }, i);
|
|
1604
|
+
const selected = marked.has(i);
|
|
1605
|
+
const solution = showSolutions || passed && props.enableSolutionsButton;
|
|
1606
|
+
return /* @__PURE__ */ jsx6(
|
|
1607
|
+
"button",
|
|
1608
|
+
{
|
|
1609
|
+
type: "button",
|
|
1610
|
+
"data-testid": `mark-word-${i}`,
|
|
1611
|
+
"aria-pressed": selected,
|
|
1612
|
+
disabled: passed && !props.enableRetry,
|
|
1613
|
+
onClick: () => toggle(i),
|
|
1614
|
+
style: {
|
|
1615
|
+
margin: "0 0.1em",
|
|
1616
|
+
textDecoration: solution ? "underline" : void 0,
|
|
1617
|
+
fontWeight: selected || solution ? "bold" : void 0
|
|
1618
|
+
},
|
|
1619
|
+
children: token
|
|
1620
|
+
},
|
|
1621
|
+
i
|
|
1622
|
+
);
|
|
1623
|
+
}) }),
|
|
1624
|
+
allMarked ? /* @__PURE__ */ jsx6("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
|
|
1625
|
+
props.enableRetry && passed ? /* @__PURE__ */ jsx6("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
1626
|
+
props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ jsx6("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1627
|
+
] });
|
|
1628
|
+
}
|
|
1629
|
+
var MarkTheWordsInnerForwarded = forwardRef2(MarkTheWordsInner);
|
|
1630
|
+
var MarkTheWords = forwardRef2(function MarkTheWords2(props, ref) {
|
|
1631
|
+
return /* @__PURE__ */ jsx6(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx6(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
// src/blocks/FillInTheBlanks.tsx
|
|
1635
|
+
import React7, { forwardRef as forwardRef3, useEffect as useEffect6, useImperativeHandle as useImperativeHandle3, useMemo as useMemo8, useRef as useRef6, useState as useState5 } from "react";
|
|
1636
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1637
|
+
var INTERACTION3 = "fillInBlanks";
|
|
1638
|
+
function parseTemplate(template) {
|
|
1639
|
+
const parts = [];
|
|
1640
|
+
const blanks = [];
|
|
1641
|
+
const re = /\*([^*]+)\*/g;
|
|
1642
|
+
let last = 0;
|
|
1643
|
+
let match;
|
|
1644
|
+
let n = 0;
|
|
1645
|
+
while ((match = re.exec(template)) !== null) {
|
|
1646
|
+
parts.push(template.slice(last, match.index));
|
|
1647
|
+
const id = `blank-${n++}`;
|
|
1648
|
+
blanks.push({ id, answer: match[1].trim() });
|
|
1649
|
+
parts.push(id);
|
|
1650
|
+
last = match.index + match[0].length;
|
|
1651
|
+
}
|
|
1652
|
+
parts.push(template.slice(last));
|
|
1653
|
+
return { parts, blanks };
|
|
1654
|
+
}
|
|
1655
|
+
function FillInTheBlanksInner(props, ref) {
|
|
1656
|
+
const checkId = useMemo8(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1657
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1658
|
+
const parsed = useMemo8(() => parseTemplate(props.template), [props.template]);
|
|
1659
|
+
const blanks = props.blanks ?? parsed.blanks;
|
|
1660
|
+
const [values, setValues] = useState5(
|
|
1661
|
+
() => Object.fromEntries(blanks.map((b) => [b.id, ""]))
|
|
1662
|
+
);
|
|
1663
|
+
const [passed, setPassed] = useState5(false);
|
|
1664
|
+
const [showSolutions, setShowSolutions] = useState5(false);
|
|
1665
|
+
const completedRef = useRef6(false);
|
|
1666
|
+
const answeredRef = useRef6(false);
|
|
1667
|
+
const reset = () => {
|
|
1668
|
+
completedRef.current = false;
|
|
1669
|
+
answeredRef.current = false;
|
|
1670
|
+
setPassed(false);
|
|
1671
|
+
setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
|
|
1672
|
+
setShowSolutions(false);
|
|
1673
|
+
};
|
|
1674
|
+
useEffect6(() => {
|
|
1675
|
+
reset();
|
|
1676
|
+
}, [checkId, props.template, blanks.map((b) => b.answer).join("\0")]);
|
|
1677
|
+
const hasBlanks = blanks.length > 0;
|
|
1678
|
+
const allFilled = hasBlanks && blanks.every((b) => (values[b.id] ?? "").trim().length > 0);
|
|
1679
|
+
let score = 0;
|
|
1680
|
+
blanks.forEach((b) => {
|
|
1681
|
+
if ((values[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) score += 1;
|
|
1682
|
+
});
|
|
1683
|
+
const maxScore = blanks.length;
|
|
1684
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
1685
|
+
const handle = useMemo8(() => {
|
|
1686
|
+
const handleMax = maxScore || 1;
|
|
1687
|
+
return {
|
|
1688
|
+
getScore: () => score,
|
|
1689
|
+
getMaxScore: () => handleMax,
|
|
1690
|
+
getAnswerGiven: () => allFilled,
|
|
1691
|
+
resetTask: reset,
|
|
1692
|
+
showSolutions: () => setShowSolutions(true),
|
|
1693
|
+
getXAPIData: () => ({
|
|
1694
|
+
checkId,
|
|
1695
|
+
interactionType: INTERACTION3,
|
|
1696
|
+
response: values,
|
|
1697
|
+
correct: passedThreshold,
|
|
1698
|
+
score,
|
|
1699
|
+
maxScore: handleMax
|
|
1700
|
+
})
|
|
1701
|
+
};
|
|
1702
|
+
}, [allFilled, blanks.length, checkId, maxScore, passedThreshold, score, values]);
|
|
1703
|
+
useImperativeHandle3(ref, () => handle, [handle]);
|
|
1704
|
+
useRegisterAssessmentHandle(checkId, handle);
|
|
1705
|
+
const check = () => {
|
|
1706
|
+
if (!hasBlanks) {
|
|
1707
|
+
if (isDevEnvironment4()) {
|
|
1708
|
+
console.warn("[lessonkit] FillInTheBlanks has no blanks in template");
|
|
1709
|
+
}
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
if (!allFilled) return;
|
|
1713
|
+
if (!answeredRef.current) {
|
|
1714
|
+
answeredRef.current = true;
|
|
1715
|
+
assessment.answer({
|
|
1716
|
+
checkId,
|
|
1717
|
+
interactionType: INTERACTION3,
|
|
1718
|
+
question: props.template,
|
|
1719
|
+
response: values,
|
|
1720
|
+
correct: passedThreshold
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
if (passedThreshold && !completedRef.current) {
|
|
1724
|
+
completedRef.current = true;
|
|
1725
|
+
setPassed(true);
|
|
1726
|
+
assessment.complete({
|
|
1727
|
+
checkId,
|
|
1728
|
+
interactionType: INTERACTION3,
|
|
1729
|
+
score,
|
|
1730
|
+
maxScore,
|
|
1731
|
+
passingScore: props.passingScore ?? maxScore
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
useEffect6(() => {
|
|
1736
|
+
if (!allFilled) answeredRef.current = false;
|
|
1737
|
+
}, [allFilled]);
|
|
1738
|
+
useEffect6(() => {
|
|
1739
|
+
if (props.autoCheck && allFilled) check();
|
|
1740
|
+
}, [allFilled, props.autoCheck, values, passedThreshold]);
|
|
1741
|
+
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
1742
|
+
return /* @__PURE__ */ jsxs5("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
|
|
1743
|
+
/* @__PURE__ */ jsx7("p", { children: parsed.parts.map((part, i) => {
|
|
1744
|
+
const blank = blanks.find((b) => b.id === part);
|
|
1745
|
+
if (!blank) return /* @__PURE__ */ jsx7(React7.Fragment, { children: part }, i);
|
|
1746
|
+
return /* @__PURE__ */ jsxs5("label", { style: { margin: "0 0.25em" }, children: [
|
|
1747
|
+
/* @__PURE__ */ jsx7("span", { className: "lk-visually-hidden", children: blank.answer }),
|
|
1748
|
+
/* @__PURE__ */ jsx7(
|
|
1749
|
+
"input",
|
|
1750
|
+
{
|
|
1751
|
+
type: "text",
|
|
1752
|
+
"data-testid": `blank-${blank.id}`,
|
|
1753
|
+
"aria-label": `Blank ${blank.id}`,
|
|
1754
|
+
value: reveal ? blank.answer : values[blank.id] ?? "",
|
|
1755
|
+
readOnly: reveal,
|
|
1756
|
+
disabled: passed && !props.enableRetry,
|
|
1757
|
+
onChange: (e) => setValues((v) => ({ ...v, [blank.id]: e.target.value })),
|
|
1758
|
+
onBlur: () => props.autoCheck && check(),
|
|
1759
|
+
size: Math.max(8, blank.answer.length + 2)
|
|
1760
|
+
}
|
|
1761
|
+
)
|
|
1762
|
+
] }, blank.id);
|
|
1763
|
+
}) }),
|
|
1764
|
+
!props.autoCheck ? /* @__PURE__ */ jsx7("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
|
|
1765
|
+
!hasBlanks ? /* @__PURE__ */ jsx7("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
|
|
1766
|
+
allFilled ? /* @__PURE__ */ jsx7("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
|
|
1767
|
+
props.enableRetry && passed ? /* @__PURE__ */ jsx7("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
1768
|
+
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx7("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1769
|
+
] });
|
|
1770
|
+
}
|
|
1771
|
+
var FillInTheBlanksInnerForwarded = forwardRef3(FillInTheBlanksInner);
|
|
1772
|
+
var FillInTheBlanks = forwardRef3(
|
|
1773
|
+
function FillInTheBlanks2(props, ref) {
|
|
1774
|
+
return /* @__PURE__ */ jsx7(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx7(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1775
|
+
}
|
|
1776
|
+
);
|
|
1777
|
+
|
|
1778
|
+
// src/blocks/DragTheWords.tsx
|
|
1779
|
+
import React8, { forwardRef as forwardRef4, useEffect as useEffect7, useImperativeHandle as useImperativeHandle4, useMemo as useMemo9, useRef as useRef7, useState as useState6 } from "react";
|
|
1780
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1781
|
+
var INTERACTION4 = "dragTheWords";
|
|
1782
|
+
function parseZones(template) {
|
|
1783
|
+
const parts = [];
|
|
1784
|
+
const answers = [];
|
|
1785
|
+
const re = /\*([^*]+)\*/g;
|
|
1786
|
+
let last = 0;
|
|
1787
|
+
let match;
|
|
1788
|
+
let n = 0;
|
|
1789
|
+
while ((match = re.exec(template)) !== null) {
|
|
1790
|
+
parts.push(template.slice(last, match.index));
|
|
1791
|
+
answers.push(match[1].trim());
|
|
1792
|
+
parts.push(`zone-${n++}`);
|
|
1793
|
+
last = match.index + match[0].length;
|
|
1794
|
+
}
|
|
1795
|
+
parts.push(template.slice(last));
|
|
1796
|
+
return { parts, answers };
|
|
1797
|
+
}
|
|
1798
|
+
function DragTheWordsInner(props, ref) {
|
|
1799
|
+
const checkId = useMemo9(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1800
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1801
|
+
const { parts, answers } = useMemo9(() => parseZones(props.template), [props.template]);
|
|
1802
|
+
const [zones, setZones] = useState6(
|
|
1803
|
+
() => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
|
|
1804
|
+
);
|
|
1805
|
+
const [pool, setPool] = useState6(() => [...props.words]);
|
|
1806
|
+
const [keyboardWord, setKeyboardWord] = useState6(null);
|
|
1807
|
+
const [passed, setPassed] = useState6(false);
|
|
1808
|
+
const completedRef = useRef7(false);
|
|
1809
|
+
const answeredRef = useRef7(false);
|
|
1810
|
+
const reset = () => {
|
|
1811
|
+
completedRef.current = false;
|
|
1812
|
+
answeredRef.current = false;
|
|
1813
|
+
setPassed(false);
|
|
1814
|
+
setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
|
|
1815
|
+
setPool([...props.words]);
|
|
1816
|
+
setKeyboardWord(null);
|
|
1817
|
+
};
|
|
1818
|
+
useEffect7(() => {
|
|
1819
|
+
reset();
|
|
1820
|
+
}, [checkId, props.template, props.words.join("\0")]);
|
|
1821
|
+
const hasZones = answers.length > 0;
|
|
1822
|
+
const allFilled = hasZones && answers.every((_, i) => (zones[`zone-${i}`] ?? "").length > 0);
|
|
1823
|
+
let score = 0;
|
|
1824
|
+
answers.forEach((ans, i) => {
|
|
1825
|
+
if ((zones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) score += 1;
|
|
1826
|
+
});
|
|
1827
|
+
const maxScore = answers.length;
|
|
1828
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
|
|
1829
|
+
const handle = useMemo9(() => {
|
|
1830
|
+
const handleMax = maxScore || 1;
|
|
1831
|
+
return {
|
|
1832
|
+
getScore: () => score,
|
|
1833
|
+
getMaxScore: () => handleMax,
|
|
1834
|
+
getAnswerGiven: () => allFilled,
|
|
1835
|
+
resetTask: reset,
|
|
1836
|
+
showSolutions: () => {
|
|
1837
|
+
},
|
|
1838
|
+
getXAPIData: () => ({
|
|
1839
|
+
checkId,
|
|
1840
|
+
interactionType: INTERACTION4,
|
|
1841
|
+
response: zones,
|
|
1842
|
+
correct: passedThreshold,
|
|
1843
|
+
score,
|
|
1844
|
+
maxScore: handleMax
|
|
1845
|
+
})
|
|
1846
|
+
};
|
|
1847
|
+
}, [allFilled, answers.length, checkId, maxScore, passedThreshold, score, zones]);
|
|
1848
|
+
useImperativeHandle4(ref, () => handle, [handle]);
|
|
1849
|
+
useRegisterAssessmentHandle(checkId, handle);
|
|
1850
|
+
const placeInZone = (zoneId, word) => {
|
|
1851
|
+
if (passed && !props.enableRetry) return;
|
|
1852
|
+
const prev = zones[zoneId];
|
|
1853
|
+
setZones((z) => ({ ...z, [zoneId]: word }));
|
|
1854
|
+
setPool((p) => {
|
|
1855
|
+
const next = p.filter((w) => w !== word);
|
|
1856
|
+
if (prev) next.push(prev);
|
|
1857
|
+
return next;
|
|
1858
|
+
});
|
|
1859
|
+
setKeyboardWord(null);
|
|
1860
|
+
};
|
|
1861
|
+
const onDragStart = (word) => (e) => {
|
|
1862
|
+
e.dataTransfer.setData("text/plain", word);
|
|
1863
|
+
};
|
|
1864
|
+
const onDrop = (zoneId) => (e) => {
|
|
1865
|
+
e.preventDefault();
|
|
1866
|
+
const word = e.dataTransfer.getData("text/plain");
|
|
1867
|
+
if (word) placeInZone(zoneId, word);
|
|
1868
|
+
};
|
|
1869
|
+
const check = () => {
|
|
1870
|
+
if (!hasZones) {
|
|
1871
|
+
if (isDevEnvironment4()) {
|
|
1872
|
+
console.warn("[lessonkit] DragTheWords has no drop zones in template");
|
|
1873
|
+
}
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (!allFilled) return;
|
|
1877
|
+
if (!answeredRef.current) {
|
|
1878
|
+
answeredRef.current = true;
|
|
1879
|
+
assessment.answer({
|
|
1880
|
+
checkId,
|
|
1881
|
+
interactionType: INTERACTION4,
|
|
1882
|
+
question: props.template,
|
|
1883
|
+
response: zones,
|
|
1884
|
+
correct: passedThreshold
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
if (passedThreshold && !completedRef.current) {
|
|
1888
|
+
completedRef.current = true;
|
|
1889
|
+
setPassed(true);
|
|
1890
|
+
assessment.complete({
|
|
1891
|
+
checkId,
|
|
1892
|
+
interactionType: INTERACTION4,
|
|
1893
|
+
score,
|
|
1894
|
+
maxScore,
|
|
1895
|
+
passingScore: props.passingScore ?? maxScore
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
useEffect7(() => {
|
|
1900
|
+
if (!allFilled) answeredRef.current = false;
|
|
1901
|
+
}, [allFilled]);
|
|
1902
|
+
useEffect7(() => {
|
|
1903
|
+
if (props.autoCheck && allFilled) check();
|
|
1904
|
+
}, [allFilled, props.autoCheck, zones, passedThreshold]);
|
|
1905
|
+
return /* @__PURE__ */ jsxs6("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
|
|
1906
|
+
/* @__PURE__ */ jsx8("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
|
|
1907
|
+
/* @__PURE__ */ jsx8("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ jsx8(
|
|
1908
|
+
"button",
|
|
1909
|
+
{
|
|
1910
|
+
type: "button",
|
|
1911
|
+
draggable: true,
|
|
1912
|
+
"data-testid": `word-${word}`,
|
|
1913
|
+
"aria-pressed": keyboardWord === word,
|
|
1914
|
+
onDragStart: onDragStart(word),
|
|
1915
|
+
onClick: () => setKeyboardWord(keyboardWord === word ? null : word),
|
|
1916
|
+
style: { margin: "0.25rem" },
|
|
1917
|
+
children: word
|
|
1918
|
+
},
|
|
1919
|
+
word
|
|
1920
|
+
)) }),
|
|
1921
|
+
/* @__PURE__ */ jsx8("p", { children: parts.map((part, i) => {
|
|
1922
|
+
if (!part.startsWith("zone-")) return /* @__PURE__ */ jsx8(React8.Fragment, { children: part }, i);
|
|
1923
|
+
return /* @__PURE__ */ jsx8(
|
|
1924
|
+
"span",
|
|
1925
|
+
{
|
|
1926
|
+
role: "button",
|
|
1927
|
+
tabIndex: 0,
|
|
1928
|
+
"data-testid": part,
|
|
1929
|
+
onDragOver: (e) => e.preventDefault(),
|
|
1930
|
+
onDrop: onDrop(part),
|
|
1931
|
+
onClick: () => keyboardWord && placeInZone(part, keyboardWord),
|
|
1932
|
+
onKeyDown: (e) => {
|
|
1933
|
+
if (e.key === "Enter" && keyboardWord) placeInZone(part, keyboardWord);
|
|
1934
|
+
},
|
|
1935
|
+
style: {
|
|
1936
|
+
display: "inline-block",
|
|
1937
|
+
minWidth: "6em",
|
|
1938
|
+
border: "1px dashed currentColor",
|
|
1939
|
+
padding: "0.2em 0.5em",
|
|
1940
|
+
margin: "0 0.2em"
|
|
1941
|
+
},
|
|
1942
|
+
children: zones[part] || "___"
|
|
1943
|
+
},
|
|
1944
|
+
part
|
|
1945
|
+
);
|
|
1946
|
+
}) }),
|
|
1947
|
+
/* @__PURE__ */ jsx8("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
|
|
1948
|
+
!hasZones ? /* @__PURE__ */ jsx8("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
|
|
1949
|
+
allFilled ? /* @__PURE__ */ jsx8("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
|
|
1950
|
+
] });
|
|
1951
|
+
}
|
|
1952
|
+
var DragTheWordsInnerForwarded = forwardRef4(DragTheWordsInner);
|
|
1953
|
+
var DragTheWords = forwardRef4(function DragTheWords2(props, ref) {
|
|
1954
|
+
return /* @__PURE__ */ jsx8(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx8(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
// src/blocks/DragAndDrop.tsx
|
|
1958
|
+
import { forwardRef as forwardRef5, useEffect as useEffect8, useImperativeHandle as useImperativeHandle5, useMemo as useMemo10, useRef as useRef8, useState as useState7 } from "react";
|
|
1959
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1960
|
+
var INTERACTION5 = "dragAndDrop";
|
|
1961
|
+
function DragAndDropInner(props, ref) {
|
|
1962
|
+
const checkId = useMemo10(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1963
|
+
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
1964
|
+
const [assignments, setAssignments] = useState7(
|
|
1965
|
+
() => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
|
|
1966
|
+
);
|
|
1967
|
+
const [pool, setPool] = useState7(() => props.items.map((i) => i.id));
|
|
1968
|
+
const [keyboardItem, setKeyboardItem] = useState7(null);
|
|
1969
|
+
const [passed, setPassed] = useState7(false);
|
|
1970
|
+
const completedRef = useRef8(false);
|
|
1971
|
+
const reset = () => {
|
|
1972
|
+
completedRef.current = false;
|
|
1973
|
+
setPassed(false);
|
|
1974
|
+
setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
|
|
1975
|
+
setPool(props.items.map((i) => i.id));
|
|
1976
|
+
setKeyboardItem(null);
|
|
1977
|
+
};
|
|
1978
|
+
useEffect8(() => {
|
|
1979
|
+
reset();
|
|
1980
|
+
}, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
|
|
1981
|
+
const allFilled = props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
|
|
1982
|
+
const allCorrect = props.targets.every((t) => assignments[t.id] === t.accepts);
|
|
1983
|
+
const handle = useMemo10(() => {
|
|
1984
|
+
const maxScore = props.targets.length || 1;
|
|
1985
|
+
let score = 0;
|
|
1986
|
+
props.targets.forEach((t) => {
|
|
1987
|
+
if (assignments[t.id] === t.accepts) score += 1;
|
|
1988
|
+
});
|
|
1989
|
+
return {
|
|
1990
|
+
getScore: () => score,
|
|
1991
|
+
getMaxScore: () => maxScore,
|
|
1992
|
+
getAnswerGiven: () => allFilled,
|
|
1993
|
+
resetTask: reset,
|
|
1994
|
+
showSolutions: () => {
|
|
1995
|
+
},
|
|
1996
|
+
getXAPIData: () => ({
|
|
1997
|
+
checkId,
|
|
1998
|
+
interactionType: INTERACTION5,
|
|
1999
|
+
response: assignments,
|
|
2000
|
+
correct: allCorrect,
|
|
2001
|
+
score,
|
|
2002
|
+
maxScore
|
|
2003
|
+
})
|
|
2004
|
+
};
|
|
2005
|
+
}, [allCorrect, allFilled, assignments, checkId, props.targets]);
|
|
2006
|
+
useImperativeHandle5(ref, () => handle, [handle]);
|
|
2007
|
+
useRegisterAssessmentHandle(checkId, handle);
|
|
2008
|
+
const place = (targetId, itemId) => {
|
|
2009
|
+
if (passed && !props.enableRetry) return;
|
|
2010
|
+
const prev = assignments[targetId];
|
|
2011
|
+
setAssignments((a) => ({ ...a, [targetId]: itemId }));
|
|
2012
|
+
setPool((p) => {
|
|
2013
|
+
const next = p.filter((id) => id !== itemId);
|
|
2014
|
+
if (prev) next.push(prev);
|
|
2015
|
+
return next;
|
|
2016
|
+
});
|
|
2017
|
+
setKeyboardItem(null);
|
|
2018
|
+
};
|
|
2019
|
+
const check = () => {
|
|
2020
|
+
if (!allFilled) return;
|
|
2021
|
+
assessment.answer({
|
|
2022
|
+
checkId,
|
|
2023
|
+
interactionType: INTERACTION5,
|
|
2024
|
+
response: assignments,
|
|
2025
|
+
correct: allCorrect
|
|
2026
|
+
});
|
|
2027
|
+
if (allCorrect && !completedRef.current) {
|
|
2028
|
+
completedRef.current = true;
|
|
2029
|
+
setPassed(true);
|
|
2030
|
+
assessment.complete({
|
|
2031
|
+
checkId,
|
|
2032
|
+
interactionType: INTERACTION5,
|
|
2033
|
+
score: props.targets.length,
|
|
2034
|
+
maxScore: props.targets.length,
|
|
2035
|
+
passingScore: props.passingScore ?? props.targets.length
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
return /* @__PURE__ */ jsxs7("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
|
|
2040
|
+
/* @__PURE__ */ jsx9("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
|
|
2041
|
+
/* @__PURE__ */ jsx9("div", { role: "list", "aria-label": "Draggable items", children: pool.map((id) => {
|
|
2042
|
+
const item = props.items.find((i) => i.id === id);
|
|
2043
|
+
return /* @__PURE__ */ jsx9(
|
|
2044
|
+
"button",
|
|
2045
|
+
{
|
|
2046
|
+
type: "button",
|
|
2047
|
+
draggable: true,
|
|
2048
|
+
"data-testid": `drag-item-${id}`,
|
|
2049
|
+
"aria-pressed": keyboardItem === id,
|
|
2050
|
+
onDragStart: (e) => e.dataTransfer.setData("text/plain", id),
|
|
2051
|
+
onClick: () => setKeyboardItem(keyboardItem === id ? null : id),
|
|
2052
|
+
style: { margin: "0.25rem" },
|
|
2053
|
+
children: item.label
|
|
2054
|
+
},
|
|
2055
|
+
id
|
|
2056
|
+
);
|
|
2057
|
+
}) }),
|
|
2058
|
+
/* @__PURE__ */ jsx9("ul", { children: props.targets.map((target) => {
|
|
2059
|
+
const assigned = assignments[target.id];
|
|
2060
|
+
const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
|
|
2061
|
+
return /* @__PURE__ */ jsxs7("li", { children: [
|
|
2062
|
+
/* @__PURE__ */ jsx9("strong", { children: target.label }),
|
|
2063
|
+
" ",
|
|
2064
|
+
/* @__PURE__ */ jsx9(
|
|
2065
|
+
"span",
|
|
2066
|
+
{
|
|
2067
|
+
role: "button",
|
|
2068
|
+
tabIndex: 0,
|
|
2069
|
+
"data-testid": `drop-${target.id}`,
|
|
2070
|
+
onDragOver: (e) => e.preventDefault(),
|
|
2071
|
+
onDrop: (e) => {
|
|
2072
|
+
e.preventDefault();
|
|
2073
|
+
const id = e.dataTransfer.getData("text/plain");
|
|
2074
|
+
if (id) place(target.id, id);
|
|
2075
|
+
},
|
|
2076
|
+
onClick: () => keyboardItem && place(target.id, keyboardItem),
|
|
2077
|
+
onKeyDown: (e) => {
|
|
2078
|
+
if (e.key === "Enter" && keyboardItem) place(target.id, keyboardItem);
|
|
2079
|
+
},
|
|
2080
|
+
style: {
|
|
2081
|
+
display: "inline-block",
|
|
2082
|
+
minWidth: "8em",
|
|
2083
|
+
border: "1px dashed currentColor",
|
|
2084
|
+
padding: "0.25em"
|
|
2085
|
+
},
|
|
2086
|
+
children: label
|
|
2087
|
+
}
|
|
2088
|
+
)
|
|
2089
|
+
] }, target.id);
|
|
2090
|
+
}) }),
|
|
2091
|
+
/* @__PURE__ */ jsx9("button", { type: "button", "data-testid": "check-drag-drop", disabled: !allFilled || passed, onClick: check, children: "Check" }),
|
|
2092
|
+
allFilled ? /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", children: passed || allCorrect ? "Correct" : "Try again" }) : null
|
|
2093
|
+
] });
|
|
2094
|
+
}
|
|
2095
|
+
var DragAndDropInnerForwarded = forwardRef5(DragAndDropInner);
|
|
2096
|
+
var DragAndDrop = forwardRef5(function DragAndDrop2(props, ref) {
|
|
2097
|
+
return /* @__PURE__ */ jsx9(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx9(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
// src/blocks/AssessmentSequence.tsx
|
|
2101
|
+
import React10, { useCallback as useCallback3, useMemo as useMemo11, useState as useState8 } from "react";
|
|
2102
|
+
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2103
|
+
function AssessmentSequence(props) {
|
|
2104
|
+
const sequential = props.sequential !== false;
|
|
2105
|
+
const childArray = React10.Children.toArray(props.children).filter(React10.isValidElement);
|
|
2106
|
+
const [index, setIndex] = useState8(0);
|
|
2107
|
+
const current = childArray[index] ?? null;
|
|
2108
|
+
const goNext = useCallback3(() => {
|
|
2109
|
+
setIndex((i) => Math.min(i + 1, childArray.length - 1));
|
|
2110
|
+
}, [childArray.length]);
|
|
2111
|
+
const goPrev = useCallback3(() => {
|
|
2112
|
+
setIndex((i) => Math.max(i - 1, 0));
|
|
2113
|
+
}, []);
|
|
2114
|
+
const progress = useMemo11(
|
|
2115
|
+
() => ({ current: index + 1, total: childArray.length }),
|
|
2116
|
+
[index, childArray.length]
|
|
2117
|
+
);
|
|
2118
|
+
if (!sequential) {
|
|
2119
|
+
return /* @__PURE__ */ jsx10(AssessmentSequenceProvider, { children: /* @__PURE__ */ jsx10("section", { "aria-label": "Assessment sequence", children: props.children }) });
|
|
2120
|
+
}
|
|
2121
|
+
return /* @__PURE__ */ jsx10(AssessmentSequenceProvider, { children: /* @__PURE__ */ jsxs8("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
|
|
2122
|
+
/* @__PURE__ */ jsxs8("p", { children: [
|
|
2123
|
+
"Question ",
|
|
2124
|
+
progress.current,
|
|
2125
|
+
" of ",
|
|
2126
|
+
progress.total
|
|
2127
|
+
] }),
|
|
2128
|
+
/* @__PURE__ */ jsx10("div", { "data-testid": "assessment-sequence-step", children: current }),
|
|
2129
|
+
/* @__PURE__ */ jsxs8("nav", { "aria-label": "Sequence navigation", children: [
|
|
2130
|
+
/* @__PURE__ */ jsx10("button", { type: "button", "data-testid": "sequence-prev", disabled: index === 0, onClick: goPrev, children: "Previous" }),
|
|
2131
|
+
/* @__PURE__ */ jsx10(
|
|
2132
|
+
"button",
|
|
2133
|
+
{
|
|
2134
|
+
type: "button",
|
|
2135
|
+
"data-testid": "sequence-next",
|
|
2136
|
+
disabled: index >= childArray.length - 1,
|
|
2137
|
+
onClick: goNext,
|
|
2138
|
+
children: "Next"
|
|
2139
|
+
}
|
|
2140
|
+
)
|
|
2141
|
+
] })
|
|
2142
|
+
] }) });
|
|
2143
|
+
}
|
|
2144
|
+
|
|
1158
2145
|
// src/index.tsx
|
|
1159
2146
|
import {
|
|
1160
2147
|
buildTelemetryEvent as buildTelemetryEvent2,
|
|
@@ -1167,14 +2154,14 @@ import {
|
|
|
1167
2154
|
} from "@lessonkit/core";
|
|
1168
2155
|
|
|
1169
2156
|
// src/theme/ThemeProvider.tsx
|
|
1170
|
-
import
|
|
1171
|
-
createContext as
|
|
1172
|
-
useCallback as
|
|
1173
|
-
useContext as
|
|
2157
|
+
import React11, {
|
|
2158
|
+
createContext as createContext4,
|
|
2159
|
+
useCallback as useCallback4,
|
|
2160
|
+
useContext as useContext4,
|
|
1174
2161
|
useLayoutEffect as useLayoutEffect2,
|
|
1175
|
-
useMemo as
|
|
1176
|
-
useRef as
|
|
1177
|
-
useState as
|
|
2162
|
+
useMemo as useMemo12,
|
|
2163
|
+
useRef as useRef9,
|
|
2164
|
+
useState as useState9
|
|
1178
2165
|
} from "react";
|
|
1179
2166
|
import {
|
|
1180
2167
|
brandThemeOverrides,
|
|
@@ -1201,9 +2188,12 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
1201
2188
|
}
|
|
1202
2189
|
|
|
1203
2190
|
// src/theme/ThemeProvider.tsx
|
|
1204
|
-
import { jsx as
|
|
1205
|
-
var ThemeContext =
|
|
1206
|
-
var useIsoLayoutEffect2 =
|
|
2191
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
2192
|
+
var ThemeContext = createContext4(null);
|
|
2193
|
+
var useIsoLayoutEffect2 = (
|
|
2194
|
+
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
2195
|
+
typeof window !== "undefined" ? useLayoutEffect2 : React11.useEffect
|
|
2196
|
+
);
|
|
1207
2197
|
function getSystemMode() {
|
|
1208
2198
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
1209
2199
|
return "light";
|
|
@@ -1221,7 +2211,7 @@ function ThemeProvider(props) {
|
|
|
1221
2211
|
const preset = props.preset ?? "default";
|
|
1222
2212
|
const mode = props.mode ?? "light";
|
|
1223
2213
|
const targetKind = props.target ?? "document";
|
|
1224
|
-
const [resolvedMode, setResolvedMode] =
|
|
2214
|
+
const [resolvedMode, setResolvedMode] = useState9(
|
|
1225
2215
|
() => mode === "system" ? getSystemMode() : mode
|
|
1226
2216
|
);
|
|
1227
2217
|
useIsoLayoutEffect2(() => {
|
|
@@ -1237,20 +2227,20 @@ function ThemeProvider(props) {
|
|
|
1237
2227
|
return () => mq.removeEventListener("change", onChange);
|
|
1238
2228
|
}, [mode]);
|
|
1239
2229
|
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
1240
|
-
const effectiveTheme =
|
|
2230
|
+
const effectiveTheme = useMemo12(() => {
|
|
1241
2231
|
const modeBase = resolveModeBase(mode, dataTheme);
|
|
1242
2232
|
const base = preset === "default" ? modeBase : preset === "brand" ? mergeThemes(modeBase, brandThemeOverrides) : mergeThemes(modeBase, getPresetTheme(preset));
|
|
1243
2233
|
return mergeThemes(base, props.theme ?? {});
|
|
1244
2234
|
}, [preset, mode, dataTheme, props.theme]);
|
|
1245
|
-
const hostRef =
|
|
1246
|
-
const appliedKeysRef =
|
|
2235
|
+
const hostRef = useRef9(null);
|
|
2236
|
+
const appliedKeysRef = useRef9(/* @__PURE__ */ new Set());
|
|
1247
2237
|
useIsoLayoutEffect2(() => {
|
|
1248
2238
|
if (targetKind === "document" && typeof document !== "undefined") {
|
|
1249
2239
|
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
1250
2240
|
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
1251
2241
|
}
|
|
1252
2242
|
}, [targetKind, dataTheme]);
|
|
1253
|
-
const inject =
|
|
2243
|
+
const inject = useCallback4(() => {
|
|
1254
2244
|
const vars = themeToCssVariables(effectiveTheme);
|
|
1255
2245
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
1256
2246
|
if (!el) return;
|
|
@@ -1267,7 +2257,7 @@ function ThemeProvider(props) {
|
|
|
1267
2257
|
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
1268
2258
|
};
|
|
1269
2259
|
}, [inject, targetKind]);
|
|
1270
|
-
const value =
|
|
2260
|
+
const value = useMemo12(
|
|
1271
2261
|
() => ({
|
|
1272
2262
|
theme: effectiveTheme,
|
|
1273
2263
|
preset,
|
|
@@ -1277,12 +2267,12 @@ function ThemeProvider(props) {
|
|
|
1277
2267
|
[effectiveTheme, preset, mode, dataTheme]
|
|
1278
2268
|
);
|
|
1279
2269
|
if (targetKind === "document") {
|
|
1280
|
-
return /* @__PURE__ */
|
|
2270
|
+
return /* @__PURE__ */ jsx11(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx11("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
|
|
1281
2271
|
}
|
|
1282
|
-
return /* @__PURE__ */
|
|
2272
|
+
return /* @__PURE__ */ jsx11(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx11("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
1283
2273
|
}
|
|
1284
2274
|
function useTheme() {
|
|
1285
|
-
const ctx =
|
|
2275
|
+
const ctx = useContext4(ThemeContext);
|
|
1286
2276
|
if (!ctx) {
|
|
1287
2277
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
1288
2278
|
}
|
|
@@ -1291,6 +2281,7 @@ function useTheme() {
|
|
|
1291
2281
|
|
|
1292
2282
|
// src/blockCatalog.ts
|
|
1293
2283
|
var blockCatalogVersion = 1;
|
|
2284
|
+
var blockCatalogV2Version = 2;
|
|
1294
2285
|
var BLOCK_CATALOG = [
|
|
1295
2286
|
{
|
|
1296
2287
|
type: "Course",
|
|
@@ -1477,8 +2468,163 @@ var BLOCK_CATALOG = [
|
|
|
1477
2468
|
}
|
|
1478
2469
|
}
|
|
1479
2470
|
];
|
|
1480
|
-
|
|
1481
|
-
|
|
2471
|
+
var assessmentBehaviourProps = [
|
|
2472
|
+
{ name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
|
|
2473
|
+
{ name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
|
|
2474
|
+
{ name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
|
|
2475
|
+
{ name: "passingScore", type: "number", required: false, description: "Minimum score to pass." }
|
|
2476
|
+
];
|
|
2477
|
+
var v2AssessmentEntries = [
|
|
2478
|
+
{
|
|
2479
|
+
type: "TrueFalse",
|
|
2480
|
+
category: "assessment",
|
|
2481
|
+
assessmentContract: true,
|
|
2482
|
+
h5pMachineName: "H5P.TrueFalse",
|
|
2483
|
+
h5pAlias: "True/False",
|
|
2484
|
+
description: "Binary true/false question with assessment contract.",
|
|
2485
|
+
props: [
|
|
2486
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
2487
|
+
{ name: "question", type: "string", required: true, description: "Question text." },
|
|
2488
|
+
{ name: "answer", type: "boolean", required: true, description: "Correct answer." },
|
|
2489
|
+
...assessmentBehaviourProps
|
|
2490
|
+
],
|
|
2491
|
+
requiredIds: ["checkId"],
|
|
2492
|
+
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
2493
|
+
a11y: {
|
|
2494
|
+
element: "section",
|
|
2495
|
+
ariaLabel: "True or False",
|
|
2496
|
+
keyboard: "Radio group with True/False options.",
|
|
2497
|
+
liveRegions: "role='status' for feedback.",
|
|
2498
|
+
notes: "H5P True/False equivalent."
|
|
2499
|
+
},
|
|
2500
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
2501
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
2502
|
+
},
|
|
2503
|
+
{
|
|
2504
|
+
type: "FillInTheBlanks",
|
|
2505
|
+
category: "assessment",
|
|
2506
|
+
assessmentContract: true,
|
|
2507
|
+
h5pMachineName: "H5P.Blanks",
|
|
2508
|
+
h5pAlias: "Fill in the Blanks",
|
|
2509
|
+
description: "Fill-in-the-blank text with *answer* markers in template.",
|
|
2510
|
+
props: [
|
|
2511
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
2512
|
+
{ name: "template", type: "string", required: true, description: "Text with *blank* markers." },
|
|
2513
|
+
{ name: "blanks", type: "FillInBlankSpec[]", required: false, description: "Explicit blank specs." },
|
|
2514
|
+
...assessmentBehaviourProps
|
|
2515
|
+
],
|
|
2516
|
+
requiredIds: ["checkId"],
|
|
2517
|
+
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
2518
|
+
a11y: {
|
|
2519
|
+
element: "section",
|
|
2520
|
+
ariaLabel: "Fill in the Blanks",
|
|
2521
|
+
keyboard: "Tab between text inputs.",
|
|
2522
|
+
notes: "H5P Fill in the Blanks equivalent."
|
|
2523
|
+
},
|
|
2524
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
2525
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
2526
|
+
},
|
|
2527
|
+
{
|
|
2528
|
+
type: "DragAndDrop",
|
|
2529
|
+
category: "assessment",
|
|
2530
|
+
assessmentContract: true,
|
|
2531
|
+
h5pMachineName: "H5P.DragQuestion",
|
|
2532
|
+
h5pAlias: "Drag and Drop",
|
|
2533
|
+
description: "Drag items onto labeled targets.",
|
|
2534
|
+
props: [
|
|
2535
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
2536
|
+
{ name: "items", type: "DragItem[]", required: true, description: "Draggable items." },
|
|
2537
|
+
{ name: "targets", type: "DropTarget[]", required: true, description: "Drop targets." },
|
|
2538
|
+
...assessmentBehaviourProps
|
|
2539
|
+
],
|
|
2540
|
+
requiredIds: ["checkId"],
|
|
2541
|
+
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
2542
|
+
a11y: {
|
|
2543
|
+
element: "section",
|
|
2544
|
+
ariaLabel: "Drag and Drop",
|
|
2545
|
+
keyboard: "Select item then activate target; drag also supported.",
|
|
2546
|
+
notes: "H5P Drag and Drop equivalent."
|
|
2547
|
+
},
|
|
2548
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
2549
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
2550
|
+
},
|
|
2551
|
+
{
|
|
2552
|
+
type: "DragTheWords",
|
|
2553
|
+
category: "assessment",
|
|
2554
|
+
assessmentContract: true,
|
|
2555
|
+
h5pMachineName: "H5P.DragText",
|
|
2556
|
+
h5pAlias: "Drag the Words",
|
|
2557
|
+
description: "Drag words into inline blanks.",
|
|
2558
|
+
props: [
|
|
2559
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
2560
|
+
{ name: "template", type: "string", required: true, description: "Sentence with *blank* zones." },
|
|
2561
|
+
{ name: "words", type: "string[]", required: true, description: "Draggable word bank." },
|
|
2562
|
+
...assessmentBehaviourProps
|
|
2563
|
+
],
|
|
2564
|
+
requiredIds: ["checkId"],
|
|
2565
|
+
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
2566
|
+
a11y: {
|
|
2567
|
+
element: "section",
|
|
2568
|
+
ariaLabel: "Drag the Words",
|
|
2569
|
+
keyboard: "Select word then activate zone.",
|
|
2570
|
+
notes: "H5P Drag the Words equivalent."
|
|
2571
|
+
},
|
|
2572
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
2573
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
2574
|
+
},
|
|
2575
|
+
{
|
|
2576
|
+
type: "MarkTheWords",
|
|
2577
|
+
category: "assessment",
|
|
2578
|
+
assessmentContract: true,
|
|
2579
|
+
h5pMachineName: "H5P.MarkTheWords",
|
|
2580
|
+
h5pAlias: "Mark the Words",
|
|
2581
|
+
description: "Select correct words in a sentence.",
|
|
2582
|
+
props: [
|
|
2583
|
+
{ name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
|
|
2584
|
+
{ name: "text", type: "string", required: true, description: "Source text." },
|
|
2585
|
+
{ name: "correctWords", type: "string[]", required: true, description: "Words to mark." },
|
|
2586
|
+
...assessmentBehaviourProps
|
|
2587
|
+
],
|
|
2588
|
+
requiredIds: ["checkId"],
|
|
2589
|
+
parentConstraints: ["Lesson", "AssessmentSequence"],
|
|
2590
|
+
a11y: {
|
|
2591
|
+
element: "section",
|
|
2592
|
+
ariaLabel: "Mark the Words",
|
|
2593
|
+
keyboard: "Toggle words with buttons.",
|
|
2594
|
+
notes: "H5P Mark the Words equivalent."
|
|
2595
|
+
},
|
|
2596
|
+
theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
|
|
2597
|
+
telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
|
|
2598
|
+
},
|
|
2599
|
+
{
|
|
2600
|
+
type: "AssessmentSequence",
|
|
2601
|
+
category: "container",
|
|
2602
|
+
h5pMachineName: "H5P.QuestionSet",
|
|
2603
|
+
h5pAlias: "Question Set",
|
|
2604
|
+
description: "Ordered sequence of contract-compliant assessments.",
|
|
2605
|
+
props: [
|
|
2606
|
+
{ name: "children", type: "ReactNode", required: true, description: "Assessment blocks." },
|
|
2607
|
+
{ name: "sequential", type: "boolean", required: false, description: "One question at a time." },
|
|
2608
|
+
...assessmentBehaviourProps.filter((p) => p.name !== "passingScore")
|
|
2609
|
+
],
|
|
2610
|
+
requiredIds: [],
|
|
2611
|
+
parentConstraints: ["Lesson"],
|
|
2612
|
+
a11y: {
|
|
2613
|
+
element: "section",
|
|
2614
|
+
ariaLabel: "Assessment sequence",
|
|
2615
|
+
keyboard: "Previous/Next navigation between steps.",
|
|
2616
|
+
notes: "H5P Question Set equivalent."
|
|
2617
|
+
},
|
|
2618
|
+
theming: { surface: "global-inherit", stylingNotes: "Container for assessments." },
|
|
2619
|
+
telemetry: { emits: [], manualTracking: "Child assessments emit assessment_* events." }
|
|
2620
|
+
}
|
|
2621
|
+
];
|
|
2622
|
+
var BLOCK_CATALOG_V2 = [
|
|
2623
|
+
...BLOCK_CATALOG,
|
|
2624
|
+
...v2AssessmentEntries
|
|
2625
|
+
];
|
|
2626
|
+
function cloneCatalogEntry(entry) {
|
|
2627
|
+
return {
|
|
1482
2628
|
...entry,
|
|
1483
2629
|
props: entry.props.map((p) => ({ ...p })),
|
|
1484
2630
|
aliases: entry.aliases ? [...entry.aliases] : void 0,
|
|
@@ -1493,22 +2639,37 @@ function buildBlockCatalog() {
|
|
|
1493
2639
|
...entry.telemetry,
|
|
1494
2640
|
emits: [...entry.telemetry.emits]
|
|
1495
2641
|
}
|
|
1496
|
-
}
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
function buildBlockCatalog(opts) {
|
|
2645
|
+
const version = opts?.version ?? 2;
|
|
2646
|
+
const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
|
|
2647
|
+
return source.map((entry) => cloneCatalogEntry(entry));
|
|
1497
2648
|
}
|
|
1498
|
-
function getBlockCatalogEntry(type) {
|
|
1499
|
-
|
|
2649
|
+
function getBlockCatalogEntry(type, opts) {
|
|
2650
|
+
const version = opts?.version ?? 2;
|
|
2651
|
+
const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
|
|
2652
|
+
return source.find((entry) => entry.type === type || entry.aliases?.includes(type));
|
|
1500
2653
|
}
|
|
1501
2654
|
export {
|
|
2655
|
+
AssessmentSequence,
|
|
1502
2656
|
BLOCK_CATALOG,
|
|
2657
|
+
BLOCK_CATALOG_V2,
|
|
1503
2658
|
Course,
|
|
2659
|
+
DragAndDrop,
|
|
2660
|
+
DragTheWords,
|
|
2661
|
+
FillInTheBlanks,
|
|
1504
2662
|
KnowledgeCheck,
|
|
1505
2663
|
Lesson,
|
|
1506
2664
|
LessonkitProvider,
|
|
2665
|
+
MarkTheWords,
|
|
1507
2666
|
ProgressTracker,
|
|
1508
2667
|
Quiz,
|
|
1509
2668
|
Reflection,
|
|
1510
2669
|
Scenario,
|
|
1511
2670
|
ThemeProvider,
|
|
2671
|
+
TrueFalse,
|
|
2672
|
+
blockCatalogV2Version,
|
|
1512
2673
|
blockCatalogVersion,
|
|
1513
2674
|
buildBlockCatalog,
|
|
1514
2675
|
buildTelemetryEvent2 as buildTelemetryEvent,
|
|
@@ -1519,7 +2680,9 @@ export {
|
|
|
1519
2680
|
defineLifecyclePlugin,
|
|
1520
2681
|
defineTelemetryPlugin,
|
|
1521
2682
|
getBlockCatalogEntry,
|
|
2683
|
+
resetAssessmentWarningsForTests,
|
|
1522
2684
|
resetQuizWarningsForTests,
|
|
2685
|
+
useAssessmentState,
|
|
1523
2686
|
useCompletion,
|
|
1524
2687
|
useLessonkit,
|
|
1525
2688
|
useProgress,
|