@lessonkit/react 1.0.2 → 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 +1153 -76
- package/dist/index.d.cts +116 -9
- package/dist/index.d.ts +116 -9
- package/dist/index.js +1133 -66
- package/package.json +12 -8
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
|
|
|
@@ -240,7 +263,10 @@ async function disposeTrackingClient(client) {
|
|
|
240
263
|
}
|
|
241
264
|
|
|
242
265
|
// src/provider/useLessonkitProviderRuntime.ts
|
|
243
|
-
var useIsoLayoutEffect =
|
|
266
|
+
var useIsoLayoutEffect = (
|
|
267
|
+
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
268
|
+
typeof window !== "undefined" ? useLayoutEffect : useEffect
|
|
269
|
+
);
|
|
244
270
|
var defaultStorage = createSessionStoragePort();
|
|
245
271
|
var courseStartedTrackingFlightKey = null;
|
|
246
272
|
function isTrackingActive(tracking) {
|
|
@@ -313,22 +339,15 @@ async function emitCourseStartedPipelineOnly(opts) {
|
|
|
313
339
|
async function emitCourseStarted(opts) {
|
|
314
340
|
const event = buildCourseStartedEvent(opts);
|
|
315
341
|
if (event === null) return "filtered";
|
|
316
|
-
const
|
|
342
|
+
const tracked = await emitCourseStartedToTracking(
|
|
343
|
+
opts.tracking,
|
|
317
344
|
opts.storage,
|
|
318
345
|
opts.sessionId,
|
|
319
|
-
opts.courseId
|
|
346
|
+
opts.courseId,
|
|
347
|
+
event,
|
|
348
|
+
opts.shouldCommit
|
|
320
349
|
);
|
|
321
|
-
if (!
|
|
322
|
-
const tracked = await emitCourseStartedToTracking(
|
|
323
|
-
opts.tracking,
|
|
324
|
-
opts.storage,
|
|
325
|
-
opts.sessionId,
|
|
326
|
-
opts.courseId,
|
|
327
|
-
event,
|
|
328
|
-
opts.shouldCommit
|
|
329
|
-
);
|
|
330
|
-
if (!tracked) return "failed";
|
|
331
|
-
}
|
|
350
|
+
if (!tracked) return "failed";
|
|
332
351
|
return emitCourseStartedPipelineOnly({
|
|
333
352
|
...opts,
|
|
334
353
|
event,
|
|
@@ -340,22 +359,15 @@ async function emitCourseStarted(opts) {
|
|
|
340
359
|
async function emitCourseStartedToTrackingOnly(opts) {
|
|
341
360
|
const event = buildCourseStartedEvent(opts);
|
|
342
361
|
if (event === null) return "filtered";
|
|
343
|
-
const
|
|
362
|
+
const tracked = await emitCourseStartedToTracking(
|
|
363
|
+
opts.tracking,
|
|
344
364
|
opts.storage,
|
|
345
365
|
opts.sessionId,
|
|
346
|
-
opts.courseId
|
|
366
|
+
opts.courseId,
|
|
367
|
+
event,
|
|
368
|
+
opts.shouldCommit
|
|
347
369
|
);
|
|
348
|
-
if (!
|
|
349
|
-
const tracked = await emitCourseStartedToTracking(
|
|
350
|
-
opts.tracking,
|
|
351
|
-
opts.storage,
|
|
352
|
-
opts.sessionId,
|
|
353
|
-
opts.courseId,
|
|
354
|
-
event,
|
|
355
|
-
opts.shouldCommit
|
|
356
|
-
);
|
|
357
|
-
if (!tracked) return "failed";
|
|
358
|
-
}
|
|
370
|
+
if (!tracked) return "failed";
|
|
359
371
|
try {
|
|
360
372
|
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
361
373
|
await emitCourseStartedNonTrackingPipeline({
|
|
@@ -394,6 +406,9 @@ async function emitPendingCourseStarted(opts) {
|
|
|
394
406
|
opts.sessionId,
|
|
395
407
|
opts.courseId
|
|
396
408
|
);
|
|
409
|
+
if (sessionStarted && trackingEmitted && pipelineDelivered) {
|
|
410
|
+
return "emitted";
|
|
411
|
+
}
|
|
397
412
|
if (sessionStarted && trackingEmitted && !pipelineDelivered) {
|
|
398
413
|
const event = buildCourseStartedEvent(opts);
|
|
399
414
|
if (event === null) return "filtered";
|
|
@@ -597,7 +612,10 @@ function useLessonkitProviderRuntime(config) {
|
|
|
597
612
|
const baseSink = normalizedConfig.tracking?.sink;
|
|
598
613
|
const userBatchSink = normalizedConfig.tracking?.batchSink;
|
|
599
614
|
assertTrackingSinkConfig(normalizedConfig.tracking);
|
|
600
|
-
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;
|
|
601
619
|
const batchSink = pluginHostRef.current && userBatchSink ? async (events) => {
|
|
602
620
|
const host = pluginHostRef.current;
|
|
603
621
|
const ctx = buildCurrentPluginCtx();
|
|
@@ -934,7 +952,27 @@ function LessonkitProvider(props) {
|
|
|
934
952
|
}
|
|
935
953
|
|
|
936
954
|
// src/hooks.ts
|
|
937
|
-
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
|
|
938
976
|
function useLessonkit() {
|
|
939
977
|
const ctx = useContext(LessonkitContext);
|
|
940
978
|
if (!ctx) throw new Error("LessonKit: missing LessonkitProvider");
|
|
@@ -946,16 +984,16 @@ function useProgress() {
|
|
|
946
984
|
}
|
|
947
985
|
function useTracking() {
|
|
948
986
|
const { track } = useLessonkit();
|
|
949
|
-
return
|
|
987
|
+
return useMemo3(() => ({ track }), [track]);
|
|
950
988
|
}
|
|
951
989
|
function useCompletion() {
|
|
952
990
|
const { completeLesson, completeCourse } = useLessonkit();
|
|
953
|
-
return
|
|
991
|
+
return useMemo3(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
|
|
954
992
|
}
|
|
955
993
|
function useQuizState(enclosingLessonId) {
|
|
956
994
|
const { track } = useLessonkit();
|
|
957
995
|
const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
|
|
958
|
-
return
|
|
996
|
+
return useMemo3(
|
|
959
997
|
() => ({
|
|
960
998
|
answer: (opts) => {
|
|
961
999
|
track("quiz_answered", opts, trackOpts);
|
|
@@ -1020,8 +1058,8 @@ function resetQuizWarningsForTests() {
|
|
|
1020
1058
|
warnedQuizOutsideLesson = false;
|
|
1021
1059
|
}
|
|
1022
1060
|
function Course(props) {
|
|
1023
|
-
const courseId =
|
|
1024
|
-
const providerConfig =
|
|
1061
|
+
const courseId = useMemo4(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
|
|
1062
|
+
const providerConfig = useMemo4(
|
|
1025
1063
|
() => ({ ...props.config, courseId }),
|
|
1026
1064
|
[props.config, courseId]
|
|
1027
1065
|
);
|
|
@@ -1031,7 +1069,7 @@ function Course(props) {
|
|
|
1031
1069
|
] }) });
|
|
1032
1070
|
}
|
|
1033
1071
|
function Lesson(props) {
|
|
1034
|
-
const lessonId =
|
|
1072
|
+
const lessonId = useMemo4(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
|
|
1035
1073
|
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
1036
1074
|
const { setActiveLesson, config } = useLessonkit();
|
|
1037
1075
|
const { completeLesson } = useCompletion();
|
|
@@ -1069,14 +1107,14 @@ function Lesson(props) {
|
|
|
1069
1107
|
] }) });
|
|
1070
1108
|
}
|
|
1071
1109
|
function Scenario(props) {
|
|
1072
|
-
const blockId =
|
|
1110
|
+
const blockId = useMemo4(
|
|
1073
1111
|
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1074
1112
|
[props.blockId]
|
|
1075
1113
|
);
|
|
1076
1114
|
return /* @__PURE__ */ jsx2("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
1077
1115
|
}
|
|
1078
1116
|
function Reflection(props) {
|
|
1079
|
-
const blockId =
|
|
1117
|
+
const blockId = useMemo4(
|
|
1080
1118
|
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1081
1119
|
[props.blockId]
|
|
1082
1120
|
);
|
|
@@ -1139,7 +1177,7 @@ function Quiz(props) {
|
|
|
1139
1177
|
}
|
|
1140
1178
|
function QuizInner(props) {
|
|
1141
1179
|
const { enclosingLessonId } = props;
|
|
1142
|
-
const checkId =
|
|
1180
|
+
const checkId = useMemo4(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1143
1181
|
const quiz = useQuizState(enclosingLessonId);
|
|
1144
1182
|
const { plugins, config, session } = useLessonkit();
|
|
1145
1183
|
const [selected, setSelected] = useState2(null);
|
|
@@ -1157,8 +1195,8 @@ function QuizInner(props) {
|
|
|
1157
1195
|
const isChoiceCorrect = (choice, custom) => {
|
|
1158
1196
|
if (!custom) return choice === props.answer;
|
|
1159
1197
|
if (custom.passed !== void 0) return custom.passed;
|
|
1160
|
-
if (custom.maxScore != null && custom.maxScore > 0) {
|
|
1161
|
-
return custom.score
|
|
1198
|
+
if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
1199
|
+
return meetsPassingThreshold(custom.score, custom.maxScore, props.passingScore);
|
|
1162
1200
|
}
|
|
1163
1201
|
return choice === props.answer;
|
|
1164
1202
|
};
|
|
@@ -1208,7 +1246,7 @@ function QuizInner(props) {
|
|
|
1208
1246
|
const maxScore = custom?.maxScore ?? 1;
|
|
1209
1247
|
quiz.complete({
|
|
1210
1248
|
checkId,
|
|
1211
|
-
score: custom?.score ??
|
|
1249
|
+
score: custom?.score ?? maxScore,
|
|
1212
1250
|
maxScore,
|
|
1213
1251
|
passingScore: props.passingScore ?? maxScore
|
|
1214
1252
|
});
|
|
@@ -1251,6 +1289,859 @@ function ProgressTracker(props) {
|
|
|
1251
1289
|
] }) });
|
|
1252
1290
|
}
|
|
1253
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
|
+
|
|
1254
2145
|
// src/index.tsx
|
|
1255
2146
|
import {
|
|
1256
2147
|
buildTelemetryEvent as buildTelemetryEvent2,
|
|
@@ -1263,14 +2154,14 @@ import {
|
|
|
1263
2154
|
} from "@lessonkit/core";
|
|
1264
2155
|
|
|
1265
2156
|
// src/theme/ThemeProvider.tsx
|
|
1266
|
-
import
|
|
1267
|
-
createContext as
|
|
1268
|
-
useCallback as
|
|
1269
|
-
useContext as
|
|
2157
|
+
import React11, {
|
|
2158
|
+
createContext as createContext4,
|
|
2159
|
+
useCallback as useCallback4,
|
|
2160
|
+
useContext as useContext4,
|
|
1270
2161
|
useLayoutEffect as useLayoutEffect2,
|
|
1271
|
-
useMemo as
|
|
1272
|
-
useRef as
|
|
1273
|
-
useState as
|
|
2162
|
+
useMemo as useMemo12,
|
|
2163
|
+
useRef as useRef9,
|
|
2164
|
+
useState as useState9
|
|
1274
2165
|
} from "react";
|
|
1275
2166
|
import {
|
|
1276
2167
|
brandThemeOverrides,
|
|
@@ -1297,9 +2188,12 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
1297
2188
|
}
|
|
1298
2189
|
|
|
1299
2190
|
// src/theme/ThemeProvider.tsx
|
|
1300
|
-
import { jsx as
|
|
1301
|
-
var ThemeContext =
|
|
1302
|
-
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
|
+
);
|
|
1303
2197
|
function getSystemMode() {
|
|
1304
2198
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
1305
2199
|
return "light";
|
|
@@ -1317,7 +2211,7 @@ function ThemeProvider(props) {
|
|
|
1317
2211
|
const preset = props.preset ?? "default";
|
|
1318
2212
|
const mode = props.mode ?? "light";
|
|
1319
2213
|
const targetKind = props.target ?? "document";
|
|
1320
|
-
const [resolvedMode, setResolvedMode] =
|
|
2214
|
+
const [resolvedMode, setResolvedMode] = useState9(
|
|
1321
2215
|
() => mode === "system" ? getSystemMode() : mode
|
|
1322
2216
|
);
|
|
1323
2217
|
useIsoLayoutEffect2(() => {
|
|
@@ -1333,20 +2227,20 @@ function ThemeProvider(props) {
|
|
|
1333
2227
|
return () => mq.removeEventListener("change", onChange);
|
|
1334
2228
|
}, [mode]);
|
|
1335
2229
|
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
1336
|
-
const effectiveTheme =
|
|
2230
|
+
const effectiveTheme = useMemo12(() => {
|
|
1337
2231
|
const modeBase = resolveModeBase(mode, dataTheme);
|
|
1338
2232
|
const base = preset === "default" ? modeBase : preset === "brand" ? mergeThemes(modeBase, brandThemeOverrides) : mergeThemes(modeBase, getPresetTheme(preset));
|
|
1339
2233
|
return mergeThemes(base, props.theme ?? {});
|
|
1340
2234
|
}, [preset, mode, dataTheme, props.theme]);
|
|
1341
|
-
const hostRef =
|
|
1342
|
-
const appliedKeysRef =
|
|
2235
|
+
const hostRef = useRef9(null);
|
|
2236
|
+
const appliedKeysRef = useRef9(/* @__PURE__ */ new Set());
|
|
1343
2237
|
useIsoLayoutEffect2(() => {
|
|
1344
2238
|
if (targetKind === "document" && typeof document !== "undefined") {
|
|
1345
2239
|
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
1346
2240
|
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
1347
2241
|
}
|
|
1348
2242
|
}, [targetKind, dataTheme]);
|
|
1349
|
-
const inject =
|
|
2243
|
+
const inject = useCallback4(() => {
|
|
1350
2244
|
const vars = themeToCssVariables(effectiveTheme);
|
|
1351
2245
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
1352
2246
|
if (!el) return;
|
|
@@ -1363,7 +2257,7 @@ function ThemeProvider(props) {
|
|
|
1363
2257
|
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
1364
2258
|
};
|
|
1365
2259
|
}, [inject, targetKind]);
|
|
1366
|
-
const value =
|
|
2260
|
+
const value = useMemo12(
|
|
1367
2261
|
() => ({
|
|
1368
2262
|
theme: effectiveTheme,
|
|
1369
2263
|
preset,
|
|
@@ -1373,12 +2267,12 @@ function ThemeProvider(props) {
|
|
|
1373
2267
|
[effectiveTheme, preset, mode, dataTheme]
|
|
1374
2268
|
);
|
|
1375
2269
|
if (targetKind === "document") {
|
|
1376
|
-
return /* @__PURE__ */
|
|
2270
|
+
return /* @__PURE__ */ jsx11(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx11("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
|
|
1377
2271
|
}
|
|
1378
|
-
return /* @__PURE__ */
|
|
2272
|
+
return /* @__PURE__ */ jsx11(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx11("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
1379
2273
|
}
|
|
1380
2274
|
function useTheme() {
|
|
1381
|
-
const ctx =
|
|
2275
|
+
const ctx = useContext4(ThemeContext);
|
|
1382
2276
|
if (!ctx) {
|
|
1383
2277
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
1384
2278
|
}
|
|
@@ -1387,6 +2281,7 @@ function useTheme() {
|
|
|
1387
2281
|
|
|
1388
2282
|
// src/blockCatalog.ts
|
|
1389
2283
|
var blockCatalogVersion = 1;
|
|
2284
|
+
var blockCatalogV2Version = 2;
|
|
1390
2285
|
var BLOCK_CATALOG = [
|
|
1391
2286
|
{
|
|
1392
2287
|
type: "Course",
|
|
@@ -1573,8 +2468,163 @@ var BLOCK_CATALOG = [
|
|
|
1573
2468
|
}
|
|
1574
2469
|
}
|
|
1575
2470
|
];
|
|
1576
|
-
|
|
1577
|
-
|
|
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 {
|
|
1578
2628
|
...entry,
|
|
1579
2629
|
props: entry.props.map((p) => ({ ...p })),
|
|
1580
2630
|
aliases: entry.aliases ? [...entry.aliases] : void 0,
|
|
@@ -1589,22 +2639,37 @@ function buildBlockCatalog() {
|
|
|
1589
2639
|
...entry.telemetry,
|
|
1590
2640
|
emits: [...entry.telemetry.emits]
|
|
1591
2641
|
}
|
|
1592
|
-
}
|
|
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));
|
|
1593
2648
|
}
|
|
1594
|
-
function getBlockCatalogEntry(type) {
|
|
1595
|
-
|
|
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));
|
|
1596
2653
|
}
|
|
1597
2654
|
export {
|
|
2655
|
+
AssessmentSequence,
|
|
1598
2656
|
BLOCK_CATALOG,
|
|
2657
|
+
BLOCK_CATALOG_V2,
|
|
1599
2658
|
Course,
|
|
2659
|
+
DragAndDrop,
|
|
2660
|
+
DragTheWords,
|
|
2661
|
+
FillInTheBlanks,
|
|
1600
2662
|
KnowledgeCheck,
|
|
1601
2663
|
Lesson,
|
|
1602
2664
|
LessonkitProvider,
|
|
2665
|
+
MarkTheWords,
|
|
1603
2666
|
ProgressTracker,
|
|
1604
2667
|
Quiz,
|
|
1605
2668
|
Reflection,
|
|
1606
2669
|
Scenario,
|
|
1607
2670
|
ThemeProvider,
|
|
2671
|
+
TrueFalse,
|
|
2672
|
+
blockCatalogV2Version,
|
|
1608
2673
|
blockCatalogVersion,
|
|
1609
2674
|
buildBlockCatalog,
|
|
1610
2675
|
buildTelemetryEvent2 as buildTelemetryEvent,
|
|
@@ -1615,7 +2680,9 @@ export {
|
|
|
1615
2680
|
defineLifecyclePlugin,
|
|
1616
2681
|
defineTelemetryPlugin,
|
|
1617
2682
|
getBlockCatalogEntry,
|
|
2683
|
+
resetAssessmentWarningsForTests,
|
|
1618
2684
|
resetQuizWarningsForTests,
|
|
2685
|
+
useAssessmentState,
|
|
1619
2686
|
useCompletion,
|
|
1620
2687
|
useLessonkit,
|
|
1621
2688
|
useProgress,
|