@lessonkit/react 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/block-catalog.v3.json +180 -5
- package/dist/index.cjs +1246 -558
- package/dist/index.d.cts +26 -2
- package/dist/index.d.ts +26 -2
- package/dist/index.js +1080 -392
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/components.tsx
|
|
2
|
-
import { useEffect as useEffect4, useId as useId2, useMemo as useMemo6, useRef as
|
|
2
|
+
import { useEffect as useEffect4, useId as useId2, useMemo as useMemo6, useRef as useRef5, useState as useState4 } from "react";
|
|
3
3
|
import { visuallyHiddenStyle as visuallyHiddenStyle2 } from "@lessonkit/accessibility";
|
|
4
4
|
|
|
5
5
|
// src/context.tsx
|
|
@@ -152,6 +152,9 @@ import {
|
|
|
152
152
|
resetStoragePortForTests
|
|
153
153
|
} from "@lessonkit/core";
|
|
154
154
|
|
|
155
|
+
// src/provider/useLessonkitProviderRuntime.ts
|
|
156
|
+
import { resetSharedVolatileSessionIdForTests } from "@lessonkit/core";
|
|
157
|
+
|
|
155
158
|
// src/runtime/progress.ts
|
|
156
159
|
import { createProgressController } from "@lessonkit/core";
|
|
157
160
|
|
|
@@ -295,9 +298,10 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
|
|
|
295
298
|
try {
|
|
296
299
|
if (shouldCommit && !shouldCommit()) return false;
|
|
297
300
|
tracking.track(event);
|
|
298
|
-
await tracking.flush?.();
|
|
299
|
-
if (shouldCommit && !shouldCommit()) return false;
|
|
300
301
|
markCourseStartedEmittedToTracking(storage, sessionId, courseId);
|
|
302
|
+
const delivered = await tracking.flush?.();
|
|
303
|
+
if (delivered === false) return false;
|
|
304
|
+
if (shouldCommit && !shouldCommit()) return false;
|
|
301
305
|
return true;
|
|
302
306
|
} catch {
|
|
303
307
|
return false;
|
|
@@ -424,13 +428,14 @@ function assertTrackingSinkConfig(tracking) {
|
|
|
424
428
|
|
|
425
429
|
// src/runtime/telemetry.ts
|
|
426
430
|
import { createTrackingClient } from "@lessonkit/core";
|
|
427
|
-
function createTrackingClientFromConfig(config) {
|
|
431
|
+
function createTrackingClientFromConfig(config, observability) {
|
|
428
432
|
if (config.tracking?.enabled === false) return createTrackingClient();
|
|
429
433
|
if (config.tracking?.createClient) return config.tracking.createClient();
|
|
430
434
|
return createTrackingClient({
|
|
431
435
|
sink: config.tracking?.sink,
|
|
432
436
|
batchSink: config.tracking?.batchSink,
|
|
433
|
-
batch: config.tracking?.batch
|
|
437
|
+
batch: config.tracking?.batch,
|
|
438
|
+
onBufferDrop: observability?.onTelemetryBufferDrop
|
|
434
439
|
});
|
|
435
440
|
}
|
|
436
441
|
async function disposeTrackingClient(client) {
|
|
@@ -512,7 +517,8 @@ function useLessonkitProviderRuntime(config) {
|
|
|
512
517
|
courseId: normalizedCourseId,
|
|
513
518
|
runtimeVersion: "v2",
|
|
514
519
|
session: normalizedConfig.session,
|
|
515
|
-
plugins: pluginHostRef.current ?? normalizedConfig.plugins
|
|
520
|
+
plugins: pluginHostRef.current ?? normalizedConfig.plugins,
|
|
521
|
+
deferPluginSetup: true
|
|
516
522
|
});
|
|
517
523
|
progressRef.current = headlessRef.current.progress;
|
|
518
524
|
} else {
|
|
@@ -527,7 +533,8 @@ function useLessonkitProviderRuntime(config) {
|
|
|
527
533
|
courseId: normalizedCourseId,
|
|
528
534
|
runtimeVersion: "v2",
|
|
529
535
|
session: normalizedConfig.session,
|
|
530
|
-
plugins: pluginHostRef.current ?? normalizedConfig.plugins
|
|
536
|
+
plugins: pluginHostRef.current ?? normalizedConfig.plugins,
|
|
537
|
+
deferPluginSetup: true
|
|
531
538
|
});
|
|
532
539
|
}
|
|
533
540
|
if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
|
|
@@ -672,9 +679,12 @@ function useLessonkitProviderRuntime(config) {
|
|
|
672
679
|
}
|
|
673
680
|
return userBatchSink(perEventForBatch);
|
|
674
681
|
} : userBatchSink;
|
|
675
|
-
const next = createTrackingClientFromConfig(
|
|
676
|
-
|
|
677
|
-
|
|
682
|
+
const next = createTrackingClientFromConfig(
|
|
683
|
+
{
|
|
684
|
+
tracking: { ...normalizedConfig.tracking, sink, batchSink }
|
|
685
|
+
},
|
|
686
|
+
observabilityRef.current
|
|
687
|
+
);
|
|
678
688
|
trackingRef.current = next;
|
|
679
689
|
trackingClientForUnmountRef.current = next;
|
|
680
690
|
setTracking(next);
|
|
@@ -992,6 +1002,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
992
1002
|
config: normalizedConfig,
|
|
993
1003
|
tracking,
|
|
994
1004
|
xapi,
|
|
1005
|
+
storage: defaultStorage,
|
|
995
1006
|
session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
|
|
996
1007
|
progress,
|
|
997
1008
|
setActiveLesson,
|
|
@@ -1127,7 +1138,7 @@ function getLessonMountCount(lessonId) {
|
|
|
1127
1138
|
}
|
|
1128
1139
|
|
|
1129
1140
|
// src/components/Quiz.tsx
|
|
1130
|
-
import { forwardRef, useEffect as useEffect3, useId, useMemo as useMemo5, useRef as
|
|
1141
|
+
import { forwardRef, useEffect as useEffect3, useId, useMemo as useMemo5, useRef as useRef4, useState as useState3 } from "react";
|
|
1131
1142
|
import { visuallyHiddenStyle } from "@lessonkit/accessibility";
|
|
1132
1143
|
|
|
1133
1144
|
// src/assessment/AssessmentLessonGuard.tsx
|
|
@@ -1186,6 +1197,12 @@ function readStringField(state, key) {
|
|
|
1186
1197
|
if (typeof value === "string" || value === null) return value;
|
|
1187
1198
|
return void 0;
|
|
1188
1199
|
}
|
|
1200
|
+
function readNumberField(state, key) {
|
|
1201
|
+
const value = state[key];
|
|
1202
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1203
|
+
if (value === null) return null;
|
|
1204
|
+
return void 0;
|
|
1205
|
+
}
|
|
1189
1206
|
function readBooleanStateField(state, key, apply) {
|
|
1190
1207
|
const value = state[key];
|
|
1191
1208
|
if (typeof value === "boolean") apply(value);
|
|
@@ -1195,53 +1212,81 @@ function readBooleanStateField(state, key, apply) {
|
|
|
1195
1212
|
import { useImperativeHandle as useImperativeHandle2 } from "react";
|
|
1196
1213
|
|
|
1197
1214
|
// src/compound/CompoundProvider.tsx
|
|
1198
|
-
import
|
|
1215
|
+
import React5, { createContext as createContext5, useCallback as useCallback2, useContext as useContext5, useImperativeHandle, useMemo as useMemo4, useRef as useRef3, useState as useState2 } from "react";
|
|
1199
1216
|
import { clampCompoundPageIndex, createCompoundResumeState } from "@lessonkit/core";
|
|
1200
1217
|
|
|
1201
1218
|
// src/compound/aggregateScores.ts
|
|
1202
|
-
function aggregateAssessmentScores(handles) {
|
|
1219
|
+
function aggregateAssessmentScores(handles, opts) {
|
|
1203
1220
|
let score = 0;
|
|
1204
1221
|
let maxScore = 0;
|
|
1205
1222
|
let allAnswered = true;
|
|
1206
|
-
for (const
|
|
1223
|
+
for (const entry of handles) {
|
|
1224
|
+
const handle = "handle" in entry ? entry.handle : entry;
|
|
1225
|
+
const pageIndex = "handle" in entry ? entry.pageIndex : void 0;
|
|
1207
1226
|
score += handle.getScore();
|
|
1208
1227
|
maxScore += handle.getMaxScore();
|
|
1209
|
-
|
|
1228
|
+
const countsForAnswerGiven = opts?.answerPageIndex === void 0 || pageIndex === void 0 || pageIndex === opts.answerPageIndex;
|
|
1229
|
+
if (countsForAnswerGiven && !handle.getAnswerGiven()) allAnswered = false;
|
|
1210
1230
|
}
|
|
1211
1231
|
return { score, maxScore, allAnswered };
|
|
1212
1232
|
}
|
|
1213
1233
|
|
|
1214
|
-
// src/compound/
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1234
|
+
// src/compound/CompoundHydrationBridge.tsx
|
|
1235
|
+
import { createContext as createContext3, useContext as useContext3, useRef as useRef2 } from "react";
|
|
1236
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
1237
|
+
var CompoundHydrationBridgeContext = createContext3(
|
|
1238
|
+
null
|
|
1239
|
+
);
|
|
1240
|
+
function CompoundHydrationBridgeProvider({ children }) {
|
|
1241
|
+
const bridgeRef = useRef2(null);
|
|
1242
|
+
return /* @__PURE__ */ jsx3(CompoundHydrationBridgeContext.Provider, { value: bridgeRef, children });
|
|
1243
|
+
}
|
|
1244
|
+
function useCompoundHydrationBridgeRef() {
|
|
1245
|
+
return useContext3(CompoundHydrationBridgeContext);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// src/compound/CompoundPageIndexContext.tsx
|
|
1249
|
+
import { createContext as createContext4, useContext as useContext4 } from "react";
|
|
1250
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1251
|
+
var CompoundPageIndexContext = createContext4(void 0);
|
|
1252
|
+
function CompoundPageIndexProvider({
|
|
1253
|
+
pageIndex,
|
|
1254
|
+
children
|
|
1255
|
+
}) {
|
|
1256
|
+
return /* @__PURE__ */ jsx4(CompoundPageIndexContext.Provider, { value: pageIndex, children });
|
|
1257
|
+
}
|
|
1258
|
+
function useCompoundPageIndex() {
|
|
1259
|
+
return useContext4(CompoundPageIndexContext);
|
|
1224
1260
|
}
|
|
1225
1261
|
|
|
1226
1262
|
// src/compound/CompoundProvider.tsx
|
|
1227
|
-
import { jsx as
|
|
1228
|
-
var CompoundRegistryContext =
|
|
1229
|
-
var CompoundHandlesVersionContext =
|
|
1263
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1264
|
+
var CompoundRegistryContext = createContext5(null);
|
|
1265
|
+
var CompoundHandlesVersionContext = createContext5(0);
|
|
1230
1266
|
function CompoundProvider({
|
|
1231
1267
|
children,
|
|
1232
1268
|
activePageIndex: _activePageIndex,
|
|
1233
1269
|
onActivePageIndexChange: _onActivePageIndexChange
|
|
1234
1270
|
}) {
|
|
1235
|
-
const registryRef =
|
|
1271
|
+
const registryRef = useRef3(/* @__PURE__ */ new Map());
|
|
1236
1272
|
const [handlesVersion, setHandlesVersion] = useState2(0);
|
|
1237
|
-
const register = useCallback2((checkId, handle) => {
|
|
1273
|
+
const register = useCallback2((checkId, handle, pageIndex) => {
|
|
1238
1274
|
const prev = registryRef.current.get(checkId);
|
|
1239
|
-
|
|
1240
|
-
|
|
1275
|
+
if (prev && prev.handle !== handle) {
|
|
1276
|
+
const message = `[lessonkit] duplicate checkId "${checkId}" registered in the same compound container; the previous handle was replaced.`;
|
|
1277
|
+
if (isDevEnvironment4()) {
|
|
1278
|
+
console.error(message);
|
|
1279
|
+
} else {
|
|
1280
|
+
console.warn(message);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
registryRef.current.set(checkId, { handle, pageIndex });
|
|
1284
|
+
if (prev?.handle !== handle || prev?.pageIndex !== pageIndex) {
|
|
1241
1285
|
setHandlesVersion((v) => v + 1);
|
|
1242
1286
|
}
|
|
1243
1287
|
return () => {
|
|
1244
|
-
|
|
1288
|
+
const current = registryRef.current.get(checkId);
|
|
1289
|
+
if (current?.handle === handle) {
|
|
1245
1290
|
registryRef.current.delete(checkId);
|
|
1246
1291
|
setHandlesVersion((v) => v + 1);
|
|
1247
1292
|
}
|
|
@@ -1250,30 +1295,39 @@ function CompoundProvider({
|
|
|
1250
1295
|
const registryValue = useMemo4(
|
|
1251
1296
|
() => ({
|
|
1252
1297
|
register,
|
|
1253
|
-
getHandles: () =>
|
|
1298
|
+
getHandles: () => {
|
|
1299
|
+
const handles = /* @__PURE__ */ new Map();
|
|
1300
|
+
for (const [checkId, entry] of registryRef.current) {
|
|
1301
|
+
handles.set(checkId, entry.handle);
|
|
1302
|
+
}
|
|
1303
|
+
return handles;
|
|
1304
|
+
},
|
|
1305
|
+
getRegisteredHandles: () => registryRef.current
|
|
1254
1306
|
}),
|
|
1255
1307
|
[register]
|
|
1256
1308
|
);
|
|
1257
|
-
return /* @__PURE__ */
|
|
1309
|
+
return /* @__PURE__ */ jsx5(CompoundHydrationBridgeProvider, { children: /* @__PURE__ */ jsx5(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ jsx5(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) }) });
|
|
1258
1310
|
}
|
|
1259
1311
|
function useCompoundRegistry() {
|
|
1260
|
-
const registry =
|
|
1261
|
-
const handlesVersion =
|
|
1312
|
+
const registry = useContext5(CompoundRegistryContext);
|
|
1313
|
+
const handlesVersion = useContext5(CompoundHandlesVersionContext);
|
|
1262
1314
|
if (!registry) return null;
|
|
1263
1315
|
return { ...registry, handlesVersion };
|
|
1264
1316
|
}
|
|
1265
1317
|
function useCompoundHandlesVersion() {
|
|
1266
|
-
return
|
|
1318
|
+
return useContext5(CompoundHandlesVersionContext);
|
|
1267
1319
|
}
|
|
1268
1320
|
function useRegisterAssessmentHandle(checkId, handle) {
|
|
1269
|
-
const registry =
|
|
1270
|
-
|
|
1321
|
+
const registry = useContext5(CompoundRegistryContext);
|
|
1322
|
+
const pageIndex = useCompoundPageIndex();
|
|
1323
|
+
React5.useLayoutEffect(() => {
|
|
1271
1324
|
if (!registry || !handle) return;
|
|
1272
|
-
return registry.register(checkId, handle);
|
|
1273
|
-
}, [registry, checkId, handle]);
|
|
1325
|
+
return registry.register(checkId, handle, pageIndex);
|
|
1326
|
+
}, [registry, checkId, handle, pageIndex]);
|
|
1274
1327
|
}
|
|
1275
1328
|
function useCompoundHandleRef(ref, opts) {
|
|
1276
|
-
const { activePageIndex, setActivePageIndex, getHandles, pageCount } = opts;
|
|
1329
|
+
const { activePageIndex, setActivePageIndex, getHandles, getRegisteredHandles, pageCount } = opts;
|
|
1330
|
+
const bridgeRef = useCompoundHydrationBridgeRef();
|
|
1277
1331
|
const setIndexClamped = useCallback2(
|
|
1278
1332
|
(index) => {
|
|
1279
1333
|
const next = pageCount !== void 0 ? clampCompoundPageIndex(index, pageCount) : Math.max(0, Math.floor(index));
|
|
@@ -1284,31 +1338,32 @@ function useCompoundHandleRef(ref, opts) {
|
|
|
1284
1338
|
useImperativeHandle(
|
|
1285
1339
|
ref,
|
|
1286
1340
|
() => ({
|
|
1287
|
-
getScore: () => aggregateAssessmentScores(
|
|
1288
|
-
getMaxScore: () => aggregateAssessmentScores(
|
|
1289
|
-
getAnswerGiven: () => aggregateAssessmentScores(
|
|
1341
|
+
getScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).score,
|
|
1342
|
+
getMaxScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).maxScore,
|
|
1343
|
+
getAnswerGiven: () => aggregateAssessmentScores(getRegisteredHandles().values(), {
|
|
1344
|
+
answerPageIndex: activePageIndex
|
|
1345
|
+
}).allAnswered,
|
|
1290
1346
|
resetTask: () => {
|
|
1291
|
-
for (const
|
|
1347
|
+
for (const entry of getRegisteredHandles().values()) entry.handle.resetTask();
|
|
1292
1348
|
},
|
|
1293
1349
|
showSolutions: () => {
|
|
1294
1350
|
if (!opts.enableSolutionsButton) return;
|
|
1295
|
-
for (const
|
|
1351
|
+
for (const entry of getRegisteredHandles().values()) entry.handle.showSolutions();
|
|
1296
1352
|
},
|
|
1297
1353
|
getCurrentState: () => {
|
|
1298
1354
|
const childStates = {};
|
|
1299
|
-
for (const [checkId,
|
|
1300
|
-
if (handle.getCurrentState) {
|
|
1301
|
-
childStates[checkId] = handle.getCurrentState();
|
|
1355
|
+
for (const [checkId, entry] of getRegisteredHandles()) {
|
|
1356
|
+
if (entry.handle.getCurrentState) {
|
|
1357
|
+
childStates[checkId] = entry.handle.getCurrentState();
|
|
1302
1358
|
}
|
|
1303
1359
|
}
|
|
1304
1360
|
return createCompoundResumeState({ activePageIndex, childStates });
|
|
1305
1361
|
},
|
|
1306
1362
|
resume: (state) => {
|
|
1307
|
-
|
|
1308
|
-
resumeChildHandles(getHandles(), state.childStates);
|
|
1363
|
+
bridgeRef?.current?.notifyImperativeResume(state);
|
|
1309
1364
|
}
|
|
1310
1365
|
}),
|
|
1311
|
-
[activePageIndex, setIndexClamped, getHandles, opts.enableSolutionsButton]
|
|
1366
|
+
[activePageIndex, setIndexClamped, getHandles, getRegisteredHandles, opts.enableSolutionsButton, bridgeRef]
|
|
1312
1367
|
);
|
|
1313
1368
|
}
|
|
1314
1369
|
|
|
@@ -1378,7 +1433,7 @@ function usePluginScoring(checkId, lessonId) {
|
|
|
1378
1433
|
}
|
|
1379
1434
|
|
|
1380
1435
|
// src/components/Quiz.tsx
|
|
1381
|
-
import { jsx as
|
|
1436
|
+
import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1382
1437
|
function QuizInner(props, ref) {
|
|
1383
1438
|
const { enclosingLessonId } = props;
|
|
1384
1439
|
const checkId = useMemo5(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
@@ -1387,44 +1442,85 @@ function QuizInner(props, ref) {
|
|
|
1387
1442
|
const [selected, setSelected] = useState3(null);
|
|
1388
1443
|
const [selectionCorrect, setSelectionCorrect] = useState3(null);
|
|
1389
1444
|
const [quizPassed, setQuizPassed] = useState3(false);
|
|
1390
|
-
const
|
|
1445
|
+
const [completedScore, setCompletedScore] = useState3(null);
|
|
1446
|
+
const [completedMaxScore, setCompletedMaxScore] = useState3(null);
|
|
1447
|
+
const completedRef = useRef4(false);
|
|
1448
|
+
const telemetryReplayedRef = useRef4(false);
|
|
1391
1449
|
const questionId = useId();
|
|
1392
1450
|
const choicesKey = props.choices.join("\0");
|
|
1393
1451
|
useEffect3(() => {
|
|
1394
1452
|
completedRef.current = false;
|
|
1453
|
+
telemetryReplayedRef.current = false;
|
|
1395
1454
|
setQuizPassed(false);
|
|
1396
1455
|
setSelected(null);
|
|
1397
1456
|
setSelectionCorrect(null);
|
|
1457
|
+
setCompletedScore(null);
|
|
1458
|
+
setCompletedMaxScore(null);
|
|
1398
1459
|
}, [checkId, props.answer, props.question, choicesKey]);
|
|
1399
1460
|
const passed = quizPassed;
|
|
1461
|
+
const resolveScores = () => {
|
|
1462
|
+
const maxScore = completedMaxScore ?? 1;
|
|
1463
|
+
if (quizPassed) {
|
|
1464
|
+
return { score: completedScore ?? maxScore, maxScore };
|
|
1465
|
+
}
|
|
1466
|
+
if (selected !== null && selectionCorrect) {
|
|
1467
|
+
return { score: completedMaxScore ?? maxScore, maxScore };
|
|
1468
|
+
}
|
|
1469
|
+
return { score: 0, maxScore };
|
|
1470
|
+
};
|
|
1471
|
+
const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
|
|
1472
|
+
if (!nextPassed || telemetryReplayedRef.current) return;
|
|
1473
|
+
telemetryReplayedRef.current = true;
|
|
1474
|
+
if (nextSelected !== null) {
|
|
1475
|
+
quiz.answer({
|
|
1476
|
+
checkId,
|
|
1477
|
+
question: props.question,
|
|
1478
|
+
choice: nextSelected,
|
|
1479
|
+
correct: nextCorrect ?? false
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
quiz.complete({
|
|
1483
|
+
checkId,
|
|
1484
|
+
score: nextScore,
|
|
1485
|
+
maxScore: nextMaxScore,
|
|
1486
|
+
passingScore: props.passingScore ?? nextMaxScore
|
|
1487
|
+
});
|
|
1488
|
+
};
|
|
1400
1489
|
const handle = useMemo5(
|
|
1401
1490
|
() => buildAssessmentHandle({
|
|
1402
1491
|
checkId,
|
|
1403
|
-
getScore: () =>
|
|
1404
|
-
|
|
1405
|
-
if (quizPassed && selected !== null) return maxScore;
|
|
1406
|
-
if (selected === null) return 0;
|
|
1407
|
-
return selectionCorrect ? maxScore : 0;
|
|
1408
|
-
},
|
|
1409
|
-
getMaxScore: () => 1,
|
|
1492
|
+
getScore: () => resolveScores().score,
|
|
1493
|
+
getMaxScore: () => resolveScores().maxScore,
|
|
1410
1494
|
getAnswerGiven: () => selected !== null,
|
|
1411
1495
|
resetTask: () => {
|
|
1412
1496
|
completedRef.current = false;
|
|
1497
|
+
telemetryReplayedRef.current = false;
|
|
1413
1498
|
setQuizPassed(false);
|
|
1414
1499
|
setSelected(null);
|
|
1415
1500
|
setSelectionCorrect(null);
|
|
1501
|
+
setCompletedScore(null);
|
|
1502
|
+
setCompletedMaxScore(null);
|
|
1416
1503
|
},
|
|
1417
1504
|
showSolutions: () => {
|
|
1418
1505
|
},
|
|
1419
|
-
getXAPIData: () =>
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1506
|
+
getXAPIData: () => {
|
|
1507
|
+
const { score, maxScore } = resolveScores();
|
|
1508
|
+
return {
|
|
1509
|
+
checkId,
|
|
1510
|
+
interactionType: "mcq",
|
|
1511
|
+
response: selected ?? void 0,
|
|
1512
|
+
correct: selectionCorrect ?? void 0,
|
|
1513
|
+
score,
|
|
1514
|
+
maxScore
|
|
1515
|
+
};
|
|
1516
|
+
},
|
|
1517
|
+
getCurrentState: () => ({
|
|
1518
|
+
selected,
|
|
1519
|
+
selectionCorrect,
|
|
1520
|
+
quizPassed,
|
|
1521
|
+
completedScore,
|
|
1522
|
+
completedMaxScore
|
|
1426
1523
|
}),
|
|
1427
|
-
getCurrentState: () => ({ selected, selectionCorrect, quizPassed }),
|
|
1428
1524
|
resume: (state) => {
|
|
1429
1525
|
const nextSelected = readStringField(state, "selected");
|
|
1430
1526
|
if (typeof nextSelected === "string" || nextSelected === null) setSelected(nextSelected);
|
|
@@ -1432,21 +1528,47 @@ function QuizInner(props, ref) {
|
|
|
1432
1528
|
if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
|
|
1433
1529
|
setSelectionCorrect(nextCorrect);
|
|
1434
1530
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1531
|
+
const nextCompletedScore = readNumberField(state, "completedScore");
|
|
1532
|
+
if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
|
|
1533
|
+
const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
|
|
1534
|
+
if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
|
|
1535
|
+
const nextPassed = readBooleanField(state, "quizPassed");
|
|
1536
|
+
if (nextPassed === true || nextPassed === false) {
|
|
1537
|
+
setQuizPassed(nextPassed);
|
|
1538
|
+
completedRef.current = nextPassed;
|
|
1539
|
+
if (nextPassed) {
|
|
1540
|
+
const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
|
|
1541
|
+
const score = nextCompletedScore ?? completedScore ?? maxScore;
|
|
1542
|
+
replayTelemetry(
|
|
1543
|
+
nextSelected ?? null,
|
|
1544
|
+
nextCorrect ?? null,
|
|
1545
|
+
nextPassed,
|
|
1546
|
+
score,
|
|
1547
|
+
maxScore
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1439
1551
|
}
|
|
1440
1552
|
}),
|
|
1441
|
-
[
|
|
1553
|
+
[
|
|
1554
|
+
checkId,
|
|
1555
|
+
completedMaxScore,
|
|
1556
|
+
completedScore,
|
|
1557
|
+
props.passingScore,
|
|
1558
|
+
props.question,
|
|
1559
|
+
quiz,
|
|
1560
|
+
quizPassed,
|
|
1561
|
+
selected,
|
|
1562
|
+
selectionCorrect
|
|
1563
|
+
]
|
|
1442
1564
|
);
|
|
1443
1565
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1444
1566
|
return /* @__PURE__ */ jsxs2("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
1445
|
-
/* @__PURE__ */
|
|
1567
|
+
/* @__PURE__ */ jsx6("p", { id: questionId, children: props.question }),
|
|
1446
1568
|
/* @__PURE__ */ jsxs2("fieldset", { "aria-labelledby": questionId, children: [
|
|
1447
|
-
/* @__PURE__ */
|
|
1569
|
+
/* @__PURE__ */ jsx6("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
|
|
1448
1570
|
props.choices.map((c, i) => /* @__PURE__ */ jsxs2("label", { style: { display: "block" }, children: [
|
|
1449
|
-
/* @__PURE__ */
|
|
1571
|
+
/* @__PURE__ */ jsx6(
|
|
1450
1572
|
"input",
|
|
1451
1573
|
{
|
|
1452
1574
|
type: "radio",
|
|
@@ -1471,9 +1593,12 @@ function QuizInner(props, ref) {
|
|
|
1471
1593
|
completedRef.current = true;
|
|
1472
1594
|
setQuizPassed(true);
|
|
1473
1595
|
const maxScore = custom?.maxScore ?? 1;
|
|
1596
|
+
const score = custom?.score ?? maxScore;
|
|
1597
|
+
setCompletedScore(score);
|
|
1598
|
+
setCompletedMaxScore(maxScore);
|
|
1474
1599
|
quiz.complete({
|
|
1475
1600
|
checkId,
|
|
1476
|
-
score
|
|
1601
|
+
score,
|
|
1477
1602
|
maxScore,
|
|
1478
1603
|
passingScore: props.passingScore ?? maxScore
|
|
1479
1604
|
});
|
|
@@ -1484,15 +1609,15 @@ function QuizInner(props, ref) {
|
|
|
1484
1609
|
c
|
|
1485
1610
|
] }, `${questionId}-${i}`))
|
|
1486
1611
|
] }),
|
|
1487
|
-
selected && selectionCorrect !== null ? /* @__PURE__ */
|
|
1612
|
+
selected && selectionCorrect !== null ? /* @__PURE__ */ jsx6("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
|
|
1488
1613
|
] });
|
|
1489
1614
|
}
|
|
1490
1615
|
var QuizInnerForwarded = forwardRef(QuizInner);
|
|
1491
1616
|
var Quiz = forwardRef(function Quiz2(props, ref) {
|
|
1492
|
-
return /* @__PURE__ */
|
|
1617
|
+
return /* @__PURE__ */ jsx6(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx6(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1493
1618
|
});
|
|
1494
1619
|
function KnowledgeCheck(props) {
|
|
1495
|
-
return /* @__PURE__ */
|
|
1620
|
+
return /* @__PURE__ */ jsx6(
|
|
1496
1621
|
Quiz,
|
|
1497
1622
|
{
|
|
1498
1623
|
checkId: props.checkId,
|
|
@@ -1508,16 +1633,16 @@ function resetQuizWarningsForTests() {
|
|
|
1508
1633
|
}
|
|
1509
1634
|
|
|
1510
1635
|
// src/components.tsx
|
|
1511
|
-
import { jsx as
|
|
1636
|
+
import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1512
1637
|
function Course(props) {
|
|
1513
1638
|
const courseId = useMemo6(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
|
|
1514
1639
|
const providerConfig = useMemo6(
|
|
1515
1640
|
() => ({ ...props.config, courseId }),
|
|
1516
1641
|
[props.config, courseId]
|
|
1517
1642
|
);
|
|
1518
|
-
return /* @__PURE__ */
|
|
1519
|
-
/* @__PURE__ */
|
|
1520
|
-
/* @__PURE__ */
|
|
1643
|
+
return /* @__PURE__ */ jsx7(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs3("section", { "aria-label": props.title, children: [
|
|
1644
|
+
/* @__PURE__ */ jsx7("h1", { children: props.title }),
|
|
1645
|
+
/* @__PURE__ */ jsx7("div", { children: props.children })
|
|
1521
1646
|
] }) });
|
|
1522
1647
|
}
|
|
1523
1648
|
function Lesson(props) {
|
|
@@ -1525,8 +1650,8 @@ function Lesson(props) {
|
|
|
1525
1650
|
const autoComplete = props.autoCompleteOnUnmount !== false;
|
|
1526
1651
|
const { setActiveLesson, config } = useLessonkit();
|
|
1527
1652
|
const { completeLesson } = useCompletion();
|
|
1528
|
-
const lessonMountGenerationRef =
|
|
1529
|
-
const liveCourseIdRef =
|
|
1653
|
+
const lessonMountGenerationRef = useRef5(0);
|
|
1654
|
+
const liveCourseIdRef = useRef5(config.courseId);
|
|
1530
1655
|
liveCourseIdRef.current = config.courseId;
|
|
1531
1656
|
useEffect4(() => {
|
|
1532
1657
|
const unregister = registerLessonMount(lessonId);
|
|
@@ -1553,9 +1678,9 @@ function Lesson(props) {
|
|
|
1553
1678
|
});
|
|
1554
1679
|
};
|
|
1555
1680
|
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
1556
|
-
return /* @__PURE__ */
|
|
1557
|
-
/* @__PURE__ */
|
|
1558
|
-
/* @__PURE__ */
|
|
1681
|
+
return /* @__PURE__ */ jsx7(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs3("article", { "aria-label": props.title, children: [
|
|
1682
|
+
/* @__PURE__ */ jsx7("h2", { children: props.title }),
|
|
1683
|
+
/* @__PURE__ */ jsx7("div", { children: props.children })
|
|
1559
1684
|
] }) });
|
|
1560
1685
|
}
|
|
1561
1686
|
function Scenario(props) {
|
|
@@ -1563,7 +1688,7 @@ function Scenario(props) {
|
|
|
1563
1688
|
() => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
|
|
1564
1689
|
[props.blockId]
|
|
1565
1690
|
);
|
|
1566
|
-
return /* @__PURE__ */
|
|
1691
|
+
return /* @__PURE__ */ jsx7("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
|
|
1567
1692
|
}
|
|
1568
1693
|
function Reflection(props) {
|
|
1569
1694
|
const blockId = useMemo6(
|
|
@@ -1580,10 +1705,10 @@ function Reflection(props) {
|
|
|
1580
1705
|
props.onChange?.(event.target.value);
|
|
1581
1706
|
};
|
|
1582
1707
|
return /* @__PURE__ */ jsxs3("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
|
|
1583
|
-
props.prompt ? /* @__PURE__ */
|
|
1584
|
-
props.hint ? /* @__PURE__ */
|
|
1708
|
+
props.prompt ? /* @__PURE__ */ jsx7("p", { id: promptId, children: props.prompt }) : null,
|
|
1709
|
+
props.hint ? /* @__PURE__ */ jsx7("p", { id: hintId, style: visuallyHiddenStyle2, children: props.hint }) : null,
|
|
1585
1710
|
props.children,
|
|
1586
|
-
/* @__PURE__ */
|
|
1711
|
+
/* @__PURE__ */ jsx7(
|
|
1587
1712
|
"textarea",
|
|
1588
1713
|
{
|
|
1589
1714
|
value,
|
|
@@ -1601,7 +1726,7 @@ function ProgressTracker(props) {
|
|
|
1601
1726
|
if (props.totalLessons != null) {
|
|
1602
1727
|
const total = props.totalLessons;
|
|
1603
1728
|
const displayed = Math.min(completed, total);
|
|
1604
|
-
return /* @__PURE__ */
|
|
1729
|
+
return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx7(
|
|
1605
1730
|
"div",
|
|
1606
1731
|
{
|
|
1607
1732
|
role: "progressbar",
|
|
@@ -1618,15 +1743,15 @@ function ProgressTracker(props) {
|
|
|
1618
1743
|
}
|
|
1619
1744
|
) });
|
|
1620
1745
|
}
|
|
1621
|
-
return /* @__PURE__ */
|
|
1746
|
+
return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs3("p", { children: [
|
|
1622
1747
|
"Lessons completed: ",
|
|
1623
1748
|
completed
|
|
1624
1749
|
] }) });
|
|
1625
1750
|
}
|
|
1626
1751
|
|
|
1627
1752
|
// src/blocks/TrueFalse.tsx
|
|
1628
|
-
import
|
|
1629
|
-
import { jsx as
|
|
1753
|
+
import React9, { forwardRef as forwardRef2, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef6, useState as useState5 } from "react";
|
|
1754
|
+
import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1630
1755
|
var INTERACTION = "trueFalse";
|
|
1631
1756
|
function TrueFalseInner(props, ref) {
|
|
1632
1757
|
const { enclosingLessonId } = props;
|
|
@@ -1638,38 +1763,81 @@ function TrueFalseInner(props, ref) {
|
|
|
1638
1763
|
const [selectionCorrect, setSelectionCorrect] = useState5(null);
|
|
1639
1764
|
const [showSolutions, setShowSolutions] = useState5(false);
|
|
1640
1765
|
const [passed, setPassed] = useState5(false);
|
|
1641
|
-
const
|
|
1642
|
-
const
|
|
1766
|
+
const [completedScore, setCompletedScore] = useState5(null);
|
|
1767
|
+
const [completedMaxScore, setCompletedMaxScore] = useState5(null);
|
|
1768
|
+
const completedRef = useRef6(false);
|
|
1769
|
+
const telemetryReplayedRef = useRef6(false);
|
|
1770
|
+
const questionId = React9.useId();
|
|
1643
1771
|
const reset = () => {
|
|
1644
1772
|
completedRef.current = false;
|
|
1773
|
+
telemetryReplayedRef.current = false;
|
|
1645
1774
|
setPassed(false);
|
|
1646
1775
|
setSelected(null);
|
|
1647
1776
|
setSelectionCorrect(null);
|
|
1648
1777
|
setShowSolutions(false);
|
|
1778
|
+
setCompletedScore(null);
|
|
1779
|
+
setCompletedMaxScore(null);
|
|
1649
1780
|
};
|
|
1650
1781
|
useEffect5(() => {
|
|
1651
1782
|
reset();
|
|
1652
1783
|
}, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
|
|
1784
|
+
const resolveScores = () => {
|
|
1785
|
+
const maxScore = completedMaxScore ?? 1;
|
|
1786
|
+
if (passed) {
|
|
1787
|
+
return { score: completedScore ?? maxScore, maxScore };
|
|
1788
|
+
}
|
|
1789
|
+
if (selectionCorrect) {
|
|
1790
|
+
return { score: completedMaxScore ?? maxScore, maxScore };
|
|
1791
|
+
}
|
|
1792
|
+
return { score: 0, maxScore };
|
|
1793
|
+
};
|
|
1794
|
+
const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
|
|
1795
|
+
if (!nextPassed || telemetryReplayedRef.current) return;
|
|
1796
|
+
telemetryReplayedRef.current = true;
|
|
1797
|
+
if (nextSelected !== null) {
|
|
1798
|
+
assessment.answer({
|
|
1799
|
+
checkId,
|
|
1800
|
+
interactionType: INTERACTION,
|
|
1801
|
+
question: props.question,
|
|
1802
|
+
response: nextSelected,
|
|
1803
|
+
correct: nextCorrect ?? false
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
assessment.complete({
|
|
1807
|
+
checkId,
|
|
1808
|
+
interactionType: INTERACTION,
|
|
1809
|
+
score: nextScore,
|
|
1810
|
+
maxScore: nextMaxScore,
|
|
1811
|
+
passingScore: props.passingScore ?? nextMaxScore
|
|
1812
|
+
});
|
|
1813
|
+
};
|
|
1653
1814
|
const handle = useMemo7(
|
|
1654
1815
|
() => buildAssessmentHandle({
|
|
1655
1816
|
checkId,
|
|
1656
|
-
getScore: () =>
|
|
1657
|
-
|
|
1658
|
-
return passed ? maxScore : selected === null ? 0 : selected === props.answer ? maxScore : 0;
|
|
1659
|
-
},
|
|
1660
|
-
getMaxScore: () => 1,
|
|
1817
|
+
getScore: () => resolveScores().score,
|
|
1818
|
+
getMaxScore: () => resolveScores().maxScore,
|
|
1661
1819
|
getAnswerGiven: () => selected !== null,
|
|
1662
1820
|
resetTask: reset,
|
|
1663
1821
|
showSolutions: () => setShowSolutions(true),
|
|
1664
|
-
getXAPIData: () =>
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1822
|
+
getXAPIData: () => {
|
|
1823
|
+
const { score, maxScore } = resolveScores();
|
|
1824
|
+
return {
|
|
1825
|
+
checkId,
|
|
1826
|
+
interactionType: INTERACTION,
|
|
1827
|
+
response: selected ?? void 0,
|
|
1828
|
+
correct: selectionCorrect ?? void 0,
|
|
1829
|
+
score,
|
|
1830
|
+
maxScore
|
|
1831
|
+
};
|
|
1832
|
+
},
|
|
1833
|
+
getCurrentState: () => ({
|
|
1834
|
+
selected,
|
|
1835
|
+
selectionCorrect,
|
|
1836
|
+
passed,
|
|
1837
|
+
showSolutions,
|
|
1838
|
+
completedScore,
|
|
1839
|
+
completedMaxScore
|
|
1671
1840
|
}),
|
|
1672
|
-
getCurrentState: () => ({ selected, selectionCorrect, passed, showSolutions }),
|
|
1673
1841
|
resume: (state) => {
|
|
1674
1842
|
const nextSelected = readBooleanField(state, "selected");
|
|
1675
1843
|
if (nextSelected === true || nextSelected === false || nextSelected === null) {
|
|
@@ -1679,14 +1847,35 @@ function TrueFalseInner(props, ref) {
|
|
|
1679
1847
|
if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
|
|
1680
1848
|
setSelectionCorrect(nextCorrect);
|
|
1681
1849
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1850
|
+
const nextCompletedScore = readNumberField(state, "completedScore");
|
|
1851
|
+
if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
|
|
1852
|
+
const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
|
|
1853
|
+
if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
|
|
1854
|
+
const nextPassed = readBooleanField(state, "passed");
|
|
1855
|
+
if (nextPassed === true || nextPassed === false) {
|
|
1856
|
+
setPassed(nextPassed);
|
|
1857
|
+
completedRef.current = nextPassed;
|
|
1858
|
+
if (nextPassed) {
|
|
1859
|
+
const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
|
|
1860
|
+
const score = nextCompletedScore ?? completedScore ?? maxScore;
|
|
1861
|
+
replayTelemetry(nextSelected ?? null, nextCorrect ?? null, nextPassed, score, maxScore);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1686
1864
|
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
1687
1865
|
}
|
|
1688
1866
|
}),
|
|
1689
|
-
[
|
|
1867
|
+
[
|
|
1868
|
+
assessment,
|
|
1869
|
+
checkId,
|
|
1870
|
+
completedMaxScore,
|
|
1871
|
+
completedScore,
|
|
1872
|
+
passed,
|
|
1873
|
+
props.passingScore,
|
|
1874
|
+
props.question,
|
|
1875
|
+
selected,
|
|
1876
|
+
selectionCorrect,
|
|
1877
|
+
showSolutions
|
|
1878
|
+
]
|
|
1690
1879
|
);
|
|
1691
1880
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
1692
1881
|
const submit = (value) => {
|
|
@@ -1705,6 +1894,8 @@ function TrueFalseInner(props, ref) {
|
|
|
1705
1894
|
if (scored.passed && !completedRef.current) {
|
|
1706
1895
|
completedRef.current = true;
|
|
1707
1896
|
setPassed(true);
|
|
1897
|
+
setCompletedScore(scored.score);
|
|
1898
|
+
setCompletedMaxScore(scored.maxScore);
|
|
1708
1899
|
assessment.complete({
|
|
1709
1900
|
checkId,
|
|
1710
1901
|
interactionType: INTERACTION,
|
|
@@ -1716,11 +1907,11 @@ function TrueFalseInner(props, ref) {
|
|
|
1716
1907
|
};
|
|
1717
1908
|
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
1718
1909
|
return /* @__PURE__ */ jsxs4("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
|
|
1719
|
-
/* @__PURE__ */
|
|
1910
|
+
/* @__PURE__ */ jsx8("p", { id: questionId, children: props.question }),
|
|
1720
1911
|
/* @__PURE__ */ jsxs4("fieldset", { "aria-labelledby": questionId, children: [
|
|
1721
|
-
/* @__PURE__ */
|
|
1912
|
+
/* @__PURE__ */ jsx8("legend", { className: "lk-visually-hidden", children: "True or False" }),
|
|
1722
1913
|
/* @__PURE__ */ jsxs4("label", { style: { display: "block", marginRight: "1rem" }, children: [
|
|
1723
|
-
/* @__PURE__ */
|
|
1914
|
+
/* @__PURE__ */ jsx8(
|
|
1724
1915
|
"input",
|
|
1725
1916
|
{
|
|
1726
1917
|
type: "radio",
|
|
@@ -1733,7 +1924,7 @@ function TrueFalseInner(props, ref) {
|
|
|
1733
1924
|
"True"
|
|
1734
1925
|
] }),
|
|
1735
1926
|
/* @__PURE__ */ jsxs4("label", { style: { display: "block" }, children: [
|
|
1736
|
-
/* @__PURE__ */
|
|
1927
|
+
/* @__PURE__ */ jsx8(
|
|
1737
1928
|
"input",
|
|
1738
1929
|
{
|
|
1739
1930
|
type: "radio",
|
|
@@ -1748,21 +1939,21 @@ function TrueFalseInner(props, ref) {
|
|
|
1748
1939
|
] }),
|
|
1749
1940
|
reveal ? /* @__PURE__ */ jsxs4("p", { children: [
|
|
1750
1941
|
"Correct answer: ",
|
|
1751
|
-
/* @__PURE__ */
|
|
1942
|
+
/* @__PURE__ */ jsx8("strong", { children: props.answer ? "True" : "False" })
|
|
1752
1943
|
] }) : null,
|
|
1753
|
-
selected !== null && selectionCorrect !== null ? /* @__PURE__ */
|
|
1754
|
-
props.enableRetry && passed ? /* @__PURE__ */
|
|
1755
|
-
props.enableSolutionsButton && !reveal ? /* @__PURE__ */
|
|
1944
|
+
selected !== null && selectionCorrect !== null ? /* @__PURE__ */ jsx8("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
|
|
1945
|
+
props.enableRetry && passed ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
1946
|
+
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1756
1947
|
] });
|
|
1757
1948
|
}
|
|
1758
1949
|
var TrueFalseInnerForwarded = forwardRef2(TrueFalseInner);
|
|
1759
1950
|
var TrueFalse = forwardRef2(function TrueFalse2(props, ref) {
|
|
1760
|
-
return /* @__PURE__ */
|
|
1951
|
+
return /* @__PURE__ */ jsx8(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx8(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1761
1952
|
});
|
|
1762
1953
|
|
|
1763
1954
|
// src/blocks/MarkTheWords.tsx
|
|
1764
|
-
import
|
|
1765
|
-
import { jsx as
|
|
1955
|
+
import React10, { forwardRef as forwardRef3, useEffect as useEffect6, useMemo as useMemo8, useRef as useRef7, useState as useState6 } from "react";
|
|
1956
|
+
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1766
1957
|
var INTERACTION2 = "markTheWords";
|
|
1767
1958
|
function tokenize(text) {
|
|
1768
1959
|
return text.split(/(\s+)/).filter((t) => t.length > 0);
|
|
@@ -1778,7 +1969,7 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1778
1969
|
const [marked, setMarked] = useState6(() => /* @__PURE__ */ new Set());
|
|
1779
1970
|
const [passed, setPassed] = useState6(false);
|
|
1780
1971
|
const [showSolutions, setShowSolutions] = useState6(false);
|
|
1781
|
-
const completedRef =
|
|
1972
|
+
const completedRef = useRef7(false);
|
|
1782
1973
|
const reset = () => {
|
|
1783
1974
|
completedRef.current = false;
|
|
1784
1975
|
setPassed(false);
|
|
@@ -1857,7 +2048,7 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1857
2048
|
interactionType: INTERACTION2,
|
|
1858
2049
|
question: props.text,
|
|
1859
2050
|
response: [...marked].map((i) => tokens[i]),
|
|
1860
|
-
correct:
|
|
2051
|
+
correct: passedThreshold
|
|
1861
2052
|
});
|
|
1862
2053
|
assessment.complete({
|
|
1863
2054
|
checkId,
|
|
@@ -1882,17 +2073,17 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1882
2073
|
return /* @__PURE__ */ jsxs5("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
|
|
1883
2074
|
!hasTargets ? /* @__PURE__ */ jsxs5("p", { role: "alert", children: [
|
|
1884
2075
|
"No words in this sentence match ",
|
|
1885
|
-
/* @__PURE__ */
|
|
2076
|
+
/* @__PURE__ */ jsx9("code", { children: "correctWords" }),
|
|
1886
2077
|
". Check spelling and capitalization in the source text."
|
|
1887
2078
|
] }) : null,
|
|
1888
|
-
/* @__PURE__ */
|
|
1889
|
-
/* @__PURE__ */
|
|
2079
|
+
/* @__PURE__ */ jsx9("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
|
|
2080
|
+
/* @__PURE__ */ jsx9("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
|
|
1890
2081
|
const isWord = !/^\s+$/.test(token);
|
|
1891
2082
|
const isTarget = isWord && correctSet.has(token.toLowerCase());
|
|
1892
|
-
if (!isTarget) return /* @__PURE__ */
|
|
2083
|
+
if (!isTarget) return /* @__PURE__ */ jsx9(React10.Fragment, { children: token }, i);
|
|
1893
2084
|
const selected = marked.has(i);
|
|
1894
2085
|
const solution = showSolutions || passed && props.enableSolutionsButton;
|
|
1895
|
-
return /* @__PURE__ */
|
|
2086
|
+
return /* @__PURE__ */ jsx9(
|
|
1896
2087
|
"button",
|
|
1897
2088
|
{
|
|
1898
2089
|
type: "button",
|
|
@@ -1910,18 +2101,18 @@ function MarkTheWordsInner(props, ref) {
|
|
|
1910
2101
|
i
|
|
1911
2102
|
);
|
|
1912
2103
|
}) }),
|
|
1913
|
-
allMarked ? /* @__PURE__ */
|
|
1914
|
-
props.enableRetry && passed ? /* @__PURE__ */
|
|
1915
|
-
props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */
|
|
2104
|
+
allMarked ? /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
|
|
2105
|
+
props.enableRetry && passed ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2106
|
+
props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
1916
2107
|
] });
|
|
1917
2108
|
}
|
|
1918
2109
|
var MarkTheWordsInnerForwarded = forwardRef3(MarkTheWordsInner);
|
|
1919
2110
|
var MarkTheWords = forwardRef3(function MarkTheWords2(props, ref) {
|
|
1920
|
-
return /* @__PURE__ */
|
|
2111
|
+
return /* @__PURE__ */ jsx9(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx9(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
1921
2112
|
});
|
|
1922
2113
|
|
|
1923
2114
|
// src/blocks/FillInTheBlanks.tsx
|
|
1924
|
-
import
|
|
2115
|
+
import React11, { forwardRef as forwardRef4, useEffect as useEffect7, useMemo as useMemo9, useRef as useRef8, useState as useState7 } from "react";
|
|
1925
2116
|
|
|
1926
2117
|
// src/assessment/internal/parseStarDelimitedTemplate.ts
|
|
1927
2118
|
function parseStarDelimitedTemplate(template, idPrefix) {
|
|
@@ -1942,7 +2133,7 @@ function parseStarDelimitedTemplate(template, idPrefix) {
|
|
|
1942
2133
|
}
|
|
1943
2134
|
|
|
1944
2135
|
// src/blocks/FillInTheBlanks.tsx
|
|
1945
|
-
import { jsx as
|
|
2136
|
+
import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1946
2137
|
var INTERACTION3 = "fillInBlanks";
|
|
1947
2138
|
function parseTemplate(template) {
|
|
1948
2139
|
const { parts, values } = parseStarDelimitedTemplate(template, "blank");
|
|
@@ -1961,14 +2152,16 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
1961
2152
|
);
|
|
1962
2153
|
const [passed, setPassed] = useState7(false);
|
|
1963
2154
|
const [showSolutions, setShowSolutions] = useState7(false);
|
|
1964
|
-
const
|
|
1965
|
-
const
|
|
2155
|
+
const [submitted, setSubmitted] = useState7(false);
|
|
2156
|
+
const completedRef = useRef8(false);
|
|
2157
|
+
const answeredRef = useRef8(false);
|
|
1966
2158
|
const reset = () => {
|
|
1967
2159
|
completedRef.current = false;
|
|
1968
2160
|
answeredRef.current = false;
|
|
1969
2161
|
setPassed(false);
|
|
1970
2162
|
setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
|
|
1971
2163
|
setShowSolutions(false);
|
|
2164
|
+
setSubmitted(false);
|
|
1972
2165
|
};
|
|
1973
2166
|
useEffect7(() => {
|
|
1974
2167
|
reset();
|
|
@@ -1997,7 +2190,7 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
1997
2190
|
score,
|
|
1998
2191
|
maxScore: maxScore || 1
|
|
1999
2192
|
}),
|
|
2000
|
-
getCurrentState: () => ({ values, passed, showSolutions }),
|
|
2193
|
+
getCurrentState: () => ({ values, passed, showSolutions, submitted }),
|
|
2001
2194
|
resume: (state) => {
|
|
2002
2195
|
const raw = state.values;
|
|
2003
2196
|
if (raw && typeof raw === "object") setValues({ ...raw });
|
|
@@ -2007,9 +2200,13 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2007
2200
|
answeredRef.current = value;
|
|
2008
2201
|
});
|
|
2009
2202
|
readBooleanStateField(state, "showSolutions", setShowSolutions);
|
|
2203
|
+
readBooleanStateField(state, "submitted", (value) => {
|
|
2204
|
+
setSubmitted(value);
|
|
2205
|
+
if (value) answeredRef.current = true;
|
|
2206
|
+
});
|
|
2010
2207
|
}
|
|
2011
2208
|
}),
|
|
2012
|
-
[allFilled, checkId, maxScore, passed, passedThreshold, score, showSolutions, values]
|
|
2209
|
+
[allFilled, checkId, maxScore, passed, passedThreshold, score, showSolutions, submitted, values]
|
|
2013
2210
|
);
|
|
2014
2211
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2015
2212
|
const check = () => {
|
|
@@ -2020,16 +2217,16 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2020
2217
|
return;
|
|
2021
2218
|
}
|
|
2022
2219
|
if (!allFilled) return;
|
|
2023
|
-
if (
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
}
|
|
2220
|
+
if (answeredRef.current || submitted) return;
|
|
2221
|
+
answeredRef.current = true;
|
|
2222
|
+
setSubmitted(true);
|
|
2223
|
+
assessment.answer({
|
|
2224
|
+
checkId,
|
|
2225
|
+
interactionType: INTERACTION3,
|
|
2226
|
+
question: props.template,
|
|
2227
|
+
response: values,
|
|
2228
|
+
correct: passedThreshold
|
|
2229
|
+
});
|
|
2033
2230
|
if (passedThreshold && !completedRef.current) {
|
|
2034
2231
|
completedRef.current = true;
|
|
2035
2232
|
setPassed(true);
|
|
@@ -2043,19 +2240,22 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2043
2240
|
}
|
|
2044
2241
|
};
|
|
2045
2242
|
useEffect7(() => {
|
|
2046
|
-
if (!allFilled)
|
|
2243
|
+
if (!allFilled) {
|
|
2244
|
+
answeredRef.current = false;
|
|
2245
|
+
setSubmitted(false);
|
|
2246
|
+
}
|
|
2047
2247
|
}, [allFilled]);
|
|
2048
2248
|
useEffect7(() => {
|
|
2049
2249
|
if (props.autoCheck && allFilled) check();
|
|
2050
2250
|
}, [allFilled, props.autoCheck, values, passedThreshold]);
|
|
2051
2251
|
const reveal = showSolutions || passed && props.enableSolutionsButton;
|
|
2052
2252
|
return /* @__PURE__ */ jsxs6("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
|
|
2053
|
-
/* @__PURE__ */
|
|
2253
|
+
/* @__PURE__ */ jsx10("p", { children: parsed.parts.map((part, i) => {
|
|
2054
2254
|
const blank = blanks.find((b) => b.id === part);
|
|
2055
|
-
if (!blank) return /* @__PURE__ */
|
|
2255
|
+
if (!blank) return /* @__PURE__ */ jsx10(React11.Fragment, { children: part }, i);
|
|
2056
2256
|
return /* @__PURE__ */ jsxs6("label", { style: { margin: "0 0.25em" }, children: [
|
|
2057
|
-
/* @__PURE__ */
|
|
2058
|
-
/* @__PURE__ */
|
|
2257
|
+
/* @__PURE__ */ jsx10("span", { className: "lk-visually-hidden", children: blank.answer }),
|
|
2258
|
+
/* @__PURE__ */ jsx10(
|
|
2059
2259
|
"input",
|
|
2060
2260
|
{
|
|
2061
2261
|
type: "text",
|
|
@@ -2071,23 +2271,23 @@ function FillInTheBlanksInner(props, ref) {
|
|
|
2071
2271
|
)
|
|
2072
2272
|
] }, blank.id);
|
|
2073
2273
|
}) }),
|
|
2074
|
-
!props.autoCheck ? /* @__PURE__ */
|
|
2075
|
-
!hasBlanks ? /* @__PURE__ */
|
|
2076
|
-
|
|
2077
|
-
props.enableRetry && passed ? /* @__PURE__ */
|
|
2078
|
-
props.enableSolutionsButton && !reveal ? /* @__PURE__ */
|
|
2274
|
+
!props.autoCheck ? /* @__PURE__ */ jsx10("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
|
|
2275
|
+
!hasBlanks ? /* @__PURE__ */ jsx10("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
|
|
2276
|
+
submitted ? /* @__PURE__ */ jsx10("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
|
|
2277
|
+
props.enableRetry && passed ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: reset, children: "Try again" }) : null,
|
|
2278
|
+
props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
|
|
2079
2279
|
] });
|
|
2080
2280
|
}
|
|
2081
2281
|
var FillInTheBlanksInnerForwarded = forwardRef4(FillInTheBlanksInner);
|
|
2082
2282
|
var FillInTheBlanks = forwardRef4(
|
|
2083
2283
|
function FillInTheBlanks2(props, ref) {
|
|
2084
|
-
return /* @__PURE__ */
|
|
2284
|
+
return /* @__PURE__ */ jsx10(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx10(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2085
2285
|
}
|
|
2086
2286
|
);
|
|
2087
2287
|
|
|
2088
2288
|
// src/blocks/DragTheWords.tsx
|
|
2089
|
-
import
|
|
2090
|
-
import { jsx as
|
|
2289
|
+
import React12, { forwardRef as forwardRef5, useEffect as useEffect8, useMemo as useMemo10, useRef as useRef9, useState as useState8 } from "react";
|
|
2290
|
+
import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2091
2291
|
var INTERACTION4 = "dragTheWords";
|
|
2092
2292
|
function parseZones(template) {
|
|
2093
2293
|
const { parts, values } = parseStarDelimitedTemplate(template, "zone");
|
|
@@ -2103,12 +2303,14 @@ function DragTheWordsInner(props, ref) {
|
|
|
2103
2303
|
const [pool, setPool] = useState8(() => [...props.words]);
|
|
2104
2304
|
const [keyboardWord, setKeyboardWord] = useState8(null);
|
|
2105
2305
|
const [passed, setPassed] = useState8(false);
|
|
2106
|
-
const
|
|
2107
|
-
const
|
|
2306
|
+
const [submitted, setSubmitted] = useState8(false);
|
|
2307
|
+
const completedRef = useRef9(false);
|
|
2308
|
+
const answeredRef = useRef9(false);
|
|
2108
2309
|
const reset = () => {
|
|
2109
2310
|
completedRef.current = false;
|
|
2110
2311
|
answeredRef.current = false;
|
|
2111
2312
|
setPassed(false);
|
|
2313
|
+
setSubmitted(false);
|
|
2112
2314
|
setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
|
|
2113
2315
|
setPool([...props.words]);
|
|
2114
2316
|
setKeyboardWord(null);
|
|
@@ -2141,7 +2343,7 @@ function DragTheWordsInner(props, ref) {
|
|
|
2141
2343
|
score,
|
|
2142
2344
|
maxScore: maxScore || 1
|
|
2143
2345
|
}),
|
|
2144
|
-
getCurrentState: () => ({ zones, pool, passed, keyboardWord }),
|
|
2346
|
+
getCurrentState: () => ({ zones, pool, passed, keyboardWord, submitted }),
|
|
2145
2347
|
resume: (state) => {
|
|
2146
2348
|
const rawZones = state.zones;
|
|
2147
2349
|
if (rawZones && typeof rawZones === "object") setZones({ ...rawZones });
|
|
@@ -2151,11 +2353,15 @@ function DragTheWordsInner(props, ref) {
|
|
|
2151
2353
|
completedRef.current = value;
|
|
2152
2354
|
answeredRef.current = value;
|
|
2153
2355
|
});
|
|
2356
|
+
readBooleanStateField(state, "submitted", (value) => {
|
|
2357
|
+
setSubmitted(value);
|
|
2358
|
+
if (value) answeredRef.current = true;
|
|
2359
|
+
});
|
|
2154
2360
|
const kw = state.keyboardWord;
|
|
2155
2361
|
if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
|
|
2156
2362
|
}
|
|
2157
2363
|
}),
|
|
2158
|
-
[allFilled, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, score, zones]
|
|
2364
|
+
[allFilled, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, score, submitted, zones]
|
|
2159
2365
|
);
|
|
2160
2366
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2161
2367
|
const placeInZone = (zoneId, word) => {
|
|
@@ -2185,16 +2391,16 @@ function DragTheWordsInner(props, ref) {
|
|
|
2185
2391
|
return;
|
|
2186
2392
|
}
|
|
2187
2393
|
if (!allFilled) return;
|
|
2188
|
-
if (
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
}
|
|
2394
|
+
if (answeredRef.current || submitted) return;
|
|
2395
|
+
answeredRef.current = true;
|
|
2396
|
+
setSubmitted(true);
|
|
2397
|
+
assessment.answer({
|
|
2398
|
+
checkId,
|
|
2399
|
+
interactionType: INTERACTION4,
|
|
2400
|
+
question: props.template,
|
|
2401
|
+
response: zones,
|
|
2402
|
+
correct: passedThreshold
|
|
2403
|
+
});
|
|
2198
2404
|
if (passedThreshold && !completedRef.current) {
|
|
2199
2405
|
completedRef.current = true;
|
|
2200
2406
|
setPassed(true);
|
|
@@ -2208,14 +2414,17 @@ function DragTheWordsInner(props, ref) {
|
|
|
2208
2414
|
}
|
|
2209
2415
|
};
|
|
2210
2416
|
useEffect8(() => {
|
|
2211
|
-
if (!allFilled)
|
|
2417
|
+
if (!allFilled) {
|
|
2418
|
+
answeredRef.current = false;
|
|
2419
|
+
setSubmitted(false);
|
|
2420
|
+
}
|
|
2212
2421
|
}, [allFilled]);
|
|
2213
2422
|
useEffect8(() => {
|
|
2214
2423
|
if (props.autoCheck && allFilled) check();
|
|
2215
2424
|
}, [allFilled, props.autoCheck, zones, passedThreshold]);
|
|
2216
2425
|
return /* @__PURE__ */ jsxs7("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
|
|
2217
|
-
/* @__PURE__ */
|
|
2218
|
-
/* @__PURE__ */
|
|
2426
|
+
/* @__PURE__ */ jsx11("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
|
|
2427
|
+
/* @__PURE__ */ jsx11("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ jsx11(
|
|
2219
2428
|
"button",
|
|
2220
2429
|
{
|
|
2221
2430
|
type: "button",
|
|
@@ -2229,9 +2438,9 @@ function DragTheWordsInner(props, ref) {
|
|
|
2229
2438
|
},
|
|
2230
2439
|
word
|
|
2231
2440
|
)) }),
|
|
2232
|
-
/* @__PURE__ */
|
|
2233
|
-
if (!part.startsWith("zone-")) return /* @__PURE__ */
|
|
2234
|
-
return /* @__PURE__ */
|
|
2441
|
+
/* @__PURE__ */ jsx11("p", { children: parts.map((part, i) => {
|
|
2442
|
+
if (!part.startsWith("zone-")) return /* @__PURE__ */ jsx11(React12.Fragment, { children: part }, i);
|
|
2443
|
+
return /* @__PURE__ */ jsx11(
|
|
2235
2444
|
"span",
|
|
2236
2445
|
{
|
|
2237
2446
|
role: "button",
|
|
@@ -2255,19 +2464,19 @@ function DragTheWordsInner(props, ref) {
|
|
|
2255
2464
|
part
|
|
2256
2465
|
);
|
|
2257
2466
|
}) }),
|
|
2258
|
-
/* @__PURE__ */
|
|
2259
|
-
!hasZones ? /* @__PURE__ */
|
|
2260
|
-
|
|
2467
|
+
/* @__PURE__ */ jsx11("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
|
|
2468
|
+
!hasZones ? /* @__PURE__ */ jsx11("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
|
|
2469
|
+
submitted ? /* @__PURE__ */ jsx11("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
|
|
2261
2470
|
] });
|
|
2262
2471
|
}
|
|
2263
2472
|
var DragTheWordsInnerForwarded = forwardRef5(DragTheWordsInner);
|
|
2264
2473
|
var DragTheWords = forwardRef5(function DragTheWords2(props, ref) {
|
|
2265
|
-
return /* @__PURE__ */
|
|
2474
|
+
return /* @__PURE__ */ jsx11(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx11(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2266
2475
|
});
|
|
2267
2476
|
|
|
2268
2477
|
// src/blocks/DragAndDrop.tsx
|
|
2269
|
-
import { forwardRef as forwardRef6, useEffect as useEffect9, useMemo as useMemo11, useRef as
|
|
2270
|
-
import { jsx as
|
|
2478
|
+
import { forwardRef as forwardRef6, useEffect as useEffect9, useMemo as useMemo11, useRef as useRef10, useState as useState9 } from "react";
|
|
2479
|
+
import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2271
2480
|
var INTERACTION5 = "dragAndDrop";
|
|
2272
2481
|
function DragAndDropInner(props, ref) {
|
|
2273
2482
|
const checkId = useMemo11(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
@@ -2278,10 +2487,12 @@ function DragAndDropInner(props, ref) {
|
|
|
2278
2487
|
const [pool, setPool] = useState9(() => props.items.map((i) => i.id));
|
|
2279
2488
|
const [keyboardItem, setKeyboardItem] = useState9(null);
|
|
2280
2489
|
const [passed, setPassed] = useState9(false);
|
|
2281
|
-
const
|
|
2490
|
+
const [checked, setChecked] = useState9(false);
|
|
2491
|
+
const completedRef = useRef10(false);
|
|
2282
2492
|
const reset = () => {
|
|
2283
2493
|
completedRef.current = false;
|
|
2284
2494
|
setPassed(false);
|
|
2495
|
+
setChecked(false);
|
|
2285
2496
|
setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
|
|
2286
2497
|
setPool(props.items.map((i) => i.id));
|
|
2287
2498
|
setKeyboardItem(null);
|
|
@@ -2289,19 +2500,20 @@ function DragAndDropInner(props, ref) {
|
|
|
2289
2500
|
useEffect9(() => {
|
|
2290
2501
|
reset();
|
|
2291
2502
|
}, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
|
|
2292
|
-
const
|
|
2293
|
-
const
|
|
2503
|
+
const hasTargets = props.targets.length > 0;
|
|
2504
|
+
const allFilled = hasTargets && props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
|
|
2505
|
+
let score = 0;
|
|
2506
|
+
props.targets.forEach((t) => {
|
|
2507
|
+
if (assignments[t.id] === t.accepts) score += 1;
|
|
2508
|
+
});
|
|
2509
|
+
const maxScore = props.targets.length || 1;
|
|
2510
|
+
const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
|
|
2294
2511
|
const handle = useMemo11(() => {
|
|
2295
|
-
const maxScore = props.targets.length || 1;
|
|
2296
|
-
let score = 0;
|
|
2297
|
-
props.targets.forEach((t) => {
|
|
2298
|
-
if (assignments[t.id] === t.accepts) score += 1;
|
|
2299
|
-
});
|
|
2300
2512
|
return buildAssessmentHandle({
|
|
2301
2513
|
checkId,
|
|
2302
2514
|
getScore: () => score,
|
|
2303
2515
|
getMaxScore: () => maxScore,
|
|
2304
|
-
getAnswerGiven: () => allFilled,
|
|
2516
|
+
getAnswerGiven: () => hasTargets && allFilled,
|
|
2305
2517
|
resetTask: reset,
|
|
2306
2518
|
showSolutions: () => {
|
|
2307
2519
|
},
|
|
@@ -2309,11 +2521,11 @@ function DragAndDropInner(props, ref) {
|
|
|
2309
2521
|
checkId,
|
|
2310
2522
|
interactionType: INTERACTION5,
|
|
2311
2523
|
response: assignments,
|
|
2312
|
-
correct:
|
|
2524
|
+
correct: passedThreshold,
|
|
2313
2525
|
score,
|
|
2314
2526
|
maxScore
|
|
2315
2527
|
}),
|
|
2316
|
-
getCurrentState: () => ({ assignments, pool, passed, keyboardItem }),
|
|
2528
|
+
getCurrentState: () => ({ assignments, pool, passed, checked, keyboardItem }),
|
|
2317
2529
|
resume: (state) => {
|
|
2318
2530
|
const rawAssignments = state.assignments;
|
|
2319
2531
|
if (rawAssignments && typeof rawAssignments === "object") {
|
|
@@ -2324,14 +2536,16 @@ function DragAndDropInner(props, ref) {
|
|
|
2324
2536
|
setPassed(value);
|
|
2325
2537
|
completedRef.current = value;
|
|
2326
2538
|
});
|
|
2539
|
+
readBooleanStateField(state, "checked", setChecked);
|
|
2327
2540
|
const item = state.keyboardItem;
|
|
2328
2541
|
if (item === null || typeof item === "string") setKeyboardItem(item ?? null);
|
|
2329
2542
|
}
|
|
2330
2543
|
});
|
|
2331
|
-
}, [
|
|
2544
|
+
}, [allFilled, assignments, checkId, checked, hasTargets, keyboardItem, maxScore, passed, passedThreshold, pool, props.targets, score]);
|
|
2332
2545
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
2333
2546
|
const place = (targetId, itemId) => {
|
|
2334
2547
|
if (passed && !props.enableRetry) return;
|
|
2548
|
+
setChecked(false);
|
|
2335
2549
|
const prev = assignments[targetId];
|
|
2336
2550
|
setAssignments((a) => ({ ...a, [targetId]: itemId }));
|
|
2337
2551
|
setPool((p) => {
|
|
@@ -2343,29 +2557,31 @@ function DragAndDropInner(props, ref) {
|
|
|
2343
2557
|
};
|
|
2344
2558
|
const check = () => {
|
|
2345
2559
|
if (!allFilled) return;
|
|
2560
|
+
setChecked(true);
|
|
2346
2561
|
assessment.answer({
|
|
2347
2562
|
checkId,
|
|
2348
2563
|
interactionType: INTERACTION5,
|
|
2349
2564
|
response: assignments,
|
|
2350
|
-
correct:
|
|
2565
|
+
correct: passedThreshold
|
|
2351
2566
|
});
|
|
2352
|
-
if (
|
|
2567
|
+
if (passedThreshold && !completedRef.current) {
|
|
2353
2568
|
completedRef.current = true;
|
|
2354
2569
|
setPassed(true);
|
|
2355
2570
|
assessment.complete({
|
|
2356
2571
|
checkId,
|
|
2357
2572
|
interactionType: INTERACTION5,
|
|
2358
|
-
score
|
|
2359
|
-
maxScore
|
|
2360
|
-
passingScore: props.passingScore ??
|
|
2573
|
+
score,
|
|
2574
|
+
maxScore,
|
|
2575
|
+
passingScore: props.passingScore ?? maxScore
|
|
2361
2576
|
});
|
|
2362
2577
|
}
|
|
2363
2578
|
};
|
|
2364
2579
|
return /* @__PURE__ */ jsxs8("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
|
|
2365
|
-
/* @__PURE__ */
|
|
2366
|
-
/* @__PURE__ */
|
|
2580
|
+
/* @__PURE__ */ jsx12("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
|
|
2581
|
+
/* @__PURE__ */ jsx12("div", { role: "list", "aria-label": "Draggable items", children: pool.flatMap((id) => {
|
|
2367
2582
|
const item = props.items.find((i) => i.id === id);
|
|
2368
|
-
|
|
2583
|
+
if (!item) return [];
|
|
2584
|
+
return /* @__PURE__ */ jsx12(
|
|
2369
2585
|
"button",
|
|
2370
2586
|
{
|
|
2371
2587
|
type: "button",
|
|
@@ -2380,13 +2596,13 @@ function DragAndDropInner(props, ref) {
|
|
|
2380
2596
|
id
|
|
2381
2597
|
);
|
|
2382
2598
|
}) }),
|
|
2383
|
-
/* @__PURE__ */
|
|
2599
|
+
/* @__PURE__ */ jsx12("ul", { children: props.targets.map((target) => {
|
|
2384
2600
|
const assigned = assignments[target.id];
|
|
2385
2601
|
const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
|
|
2386
2602
|
return /* @__PURE__ */ jsxs8("li", { children: [
|
|
2387
|
-
/* @__PURE__ */
|
|
2603
|
+
/* @__PURE__ */ jsx12("strong", { children: target.label }),
|
|
2388
2604
|
" ",
|
|
2389
|
-
/* @__PURE__ */
|
|
2605
|
+
/* @__PURE__ */ jsx12(
|
|
2390
2606
|
"span",
|
|
2391
2607
|
{
|
|
2392
2608
|
role: "button",
|
|
@@ -2413,17 +2629,18 @@ function DragAndDropInner(props, ref) {
|
|
|
2413
2629
|
)
|
|
2414
2630
|
] }, target.id);
|
|
2415
2631
|
}) }),
|
|
2416
|
-
/* @__PURE__ */
|
|
2417
|
-
|
|
2632
|
+
/* @__PURE__ */ jsx12("button", { type: "button", "data-testid": "check-drag-drop", disabled: !hasTargets || !allFilled || passed, onClick: check, children: "Check" }),
|
|
2633
|
+
checked ? /* @__PURE__ */ jsx12("p", { role: "status", "aria-live": "polite", children: passedThreshold ? "Correct" : "Try again" }) : null
|
|
2418
2634
|
] });
|
|
2419
2635
|
}
|
|
2420
2636
|
var DragAndDropInnerForwarded = forwardRef6(DragAndDropInner);
|
|
2421
2637
|
var DragAndDrop = forwardRef6(function DragAndDrop2(props, ref) {
|
|
2422
|
-
return /* @__PURE__ */
|
|
2638
|
+
return /* @__PURE__ */ jsx12(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx12(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
|
|
2423
2639
|
});
|
|
2424
2640
|
|
|
2425
2641
|
// src/blocks/AssessmentSequence.tsx
|
|
2426
|
-
import
|
|
2642
|
+
import React16, { forwardRef as forwardRef7, useCallback as useCallback7, useEffect as useEffect12, useId as useId3, useMemo as useMemo13, useRef as useRef13, useState as useState10 } from "react";
|
|
2643
|
+
import { deriveId } from "@lessonkit/core";
|
|
2427
2644
|
|
|
2428
2645
|
// src/compound/useCompoundShell.ts
|
|
2429
2646
|
import { useMemo as useMemo12 } from "react";
|
|
@@ -2450,7 +2667,7 @@ function useCompoundNavigation(pageCount, index, setIndex) {
|
|
|
2450
2667
|
}
|
|
2451
2668
|
|
|
2452
2669
|
// src/compound/useCompoundPersistence.ts
|
|
2453
|
-
import { useCallback as useCallback6, useEffect as useEffect11, useRef as
|
|
2670
|
+
import { useCallback as useCallback6, useContext as useContext7, useEffect as useEffect11, useRef as useRef12 } from "react";
|
|
2454
2671
|
import {
|
|
2455
2672
|
clampCompoundPageIndex as clampCompoundPageIndex2,
|
|
2456
2673
|
createCompoundResumeState as createCompoundResumeState2,
|
|
@@ -2458,14 +2675,80 @@ import {
|
|
|
2458
2675
|
loadCompoundState as loadCompoundState2
|
|
2459
2676
|
} from "@lessonkit/core";
|
|
2460
2677
|
|
|
2678
|
+
// src/compound/resumeChildHandles.ts
|
|
2679
|
+
function filterRegisteredChildStates(handles, childStates) {
|
|
2680
|
+
const filtered = {};
|
|
2681
|
+
for (const [key, value] of Object.entries(childStates)) {
|
|
2682
|
+
if (handles.has(key)) {
|
|
2683
|
+
filtered[key] = value;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
return filtered;
|
|
2687
|
+
}
|
|
2688
|
+
function resumeChildHandles(handles, childStates, opts) {
|
|
2689
|
+
const pendingKeys = Object.keys(childStates);
|
|
2690
|
+
const alreadyResumed = opts?.alreadyResumed;
|
|
2691
|
+
if (opts?.waitForHandles && pendingKeys.length > 0) {
|
|
2692
|
+
if (handles.size === 0) return false;
|
|
2693
|
+
const registeredPending = pendingKeys.filter((k) => handles.has(k));
|
|
2694
|
+
if (registeredPending.length === 0) {
|
|
2695
|
+
return false;
|
|
2696
|
+
}
|
|
2697
|
+
if (registeredPending.length < pendingKeys.length) {
|
|
2698
|
+
for (const key of registeredPending) {
|
|
2699
|
+
if (alreadyResumed?.has(key)) continue;
|
|
2700
|
+
const handle = handles.get(key);
|
|
2701
|
+
const child = childStates[key];
|
|
2702
|
+
if (handle?.resume && child) {
|
|
2703
|
+
handle.resume(child);
|
|
2704
|
+
alreadyResumed?.add(key);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
return false;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
for (const [checkId, handle] of handles) {
|
|
2711
|
+
if (alreadyResumed?.has(checkId)) continue;
|
|
2712
|
+
const child = childStates[checkId];
|
|
2713
|
+
if (child && handle.resume) {
|
|
2714
|
+
handle.resume(child);
|
|
2715
|
+
alreadyResumed?.add(checkId);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
return true;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2461
2721
|
// src/compound/useCompoundResume.ts
|
|
2462
|
-
import { useCallback as useCallback5, useEffect as useEffect10, useRef as
|
|
2722
|
+
import { useCallback as useCallback5, useContext as useContext6, useEffect as useEffect10, useRef as useRef11 } from "react";
|
|
2463
2723
|
import { loadCompoundState, saveCompoundState } from "@lessonkit/core";
|
|
2464
2724
|
import { createSessionStoragePort as createSessionStoragePort2 } from "@lessonkit/core";
|
|
2725
|
+
var warnedCompoundPersistFailure = false;
|
|
2726
|
+
function warnCompoundPersistFailure() {
|
|
2727
|
+
if (warnedCompoundPersistFailure || !isDevEnvironment4()) return;
|
|
2728
|
+
warnedCompoundPersistFailure = true;
|
|
2729
|
+
console.warn(
|
|
2730
|
+
"[lessonkit] compound resume state could not be saved to sessionStorage (quota or privacy mode); progress may be lost on reload."
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2465
2733
|
function useCompoundResume(opts) {
|
|
2466
|
-
const
|
|
2467
|
-
const
|
|
2734
|
+
const lessonkitCtx = useContext6(LessonkitContext);
|
|
2735
|
+
const storageRef = useRef11(opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2());
|
|
2736
|
+
const resumedRef = useRef11(false);
|
|
2737
|
+
const resumeKeyRef = useRef11("");
|
|
2738
|
+
const prevEnabledRef = useRef11(opts.enabled);
|
|
2739
|
+
useEffect10(() => {
|
|
2740
|
+
storageRef.current = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2();
|
|
2741
|
+
}, [opts.storage, lessonkitCtx?.storage]);
|
|
2468
2742
|
useEffect10(() => {
|
|
2743
|
+
if (!prevEnabledRef.current && opts.enabled) {
|
|
2744
|
+
resumedRef.current = false;
|
|
2745
|
+
}
|
|
2746
|
+
prevEnabledRef.current = opts.enabled;
|
|
2747
|
+
const key = `${opts.courseId ?? ""}:${opts.compoundId}`;
|
|
2748
|
+
if (resumeKeyRef.current !== key) {
|
|
2749
|
+
resumeKeyRef.current = key;
|
|
2750
|
+
resumedRef.current = false;
|
|
2751
|
+
}
|
|
2469
2752
|
if (!opts.enabled || !opts.courseId || resumedRef.current) return;
|
|
2470
2753
|
const saved = loadCompoundState(storageRef.current, opts.courseId, opts.compoundId);
|
|
2471
2754
|
if (saved) {
|
|
@@ -2476,7 +2759,8 @@ function useCompoundResume(opts) {
|
|
|
2476
2759
|
return useCallback5(
|
|
2477
2760
|
(state) => {
|
|
2478
2761
|
if (!opts.enabled || !opts.courseId) return;
|
|
2479
|
-
saveCompoundState(storageRef.current, opts.courseId, opts.compoundId, state);
|
|
2762
|
+
const persisted = saveCompoundState(storageRef.current, opts.courseId, opts.compoundId, state);
|
|
2763
|
+
if (!persisted) warnCompoundPersistFailure();
|
|
2480
2764
|
},
|
|
2481
2765
|
[opts.enabled, opts.courseId, opts.compoundId]
|
|
2482
2766
|
);
|
|
@@ -2489,19 +2773,46 @@ function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, stor
|
|
|
2489
2773
|
if (!saved) return 0;
|
|
2490
2774
|
return clampCompoundPageIndex2(saved.activePageIndex, pageCount);
|
|
2491
2775
|
}
|
|
2776
|
+
function stripOrphanChildStates(handles, childStates) {
|
|
2777
|
+
return filterRegisteredChildStates(handles, childStates);
|
|
2778
|
+
}
|
|
2492
2779
|
function useCompoundPersistence(opts) {
|
|
2493
|
-
const
|
|
2780
|
+
const lessonkitCtx = useContext7(LessonkitContext);
|
|
2781
|
+
const storage = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort3();
|
|
2494
2782
|
const ctx = useCompoundRegistry();
|
|
2495
2783
|
const handlesVersion = useCompoundHandlesVersion();
|
|
2496
|
-
const
|
|
2497
|
-
const
|
|
2498
|
-
const
|
|
2784
|
+
const bridgeRef = useCompoundHydrationBridgeRef();
|
|
2785
|
+
const pendingChildResumeRef = useRef12(null);
|
|
2786
|
+
const resumedChildKeysRef = useRef12(/* @__PURE__ */ new Set());
|
|
2787
|
+
const loadedChildStatesRef = useRef12({});
|
|
2788
|
+
const skipSaveUntilHydratedRef = useRef12(false);
|
|
2789
|
+
const hydrationKeyRef = useRef12("");
|
|
2790
|
+
const hydrationInitRef = useRef12(false);
|
|
2791
|
+
const hydrationKey = `${opts.courseId ?? ""}:${opts.compoundId}`;
|
|
2792
|
+
if (hydrationKeyRef.current !== hydrationKey) {
|
|
2793
|
+
hydrationKeyRef.current = hydrationKey;
|
|
2794
|
+
hydrationInitRef.current = false;
|
|
2795
|
+
loadedChildStatesRef.current = {};
|
|
2796
|
+
skipSaveUntilHydratedRef.current = false;
|
|
2797
|
+
pendingChildResumeRef.current = null;
|
|
2798
|
+
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
2799
|
+
}
|
|
2800
|
+
if (!hydrationInitRef.current && opts.enabled && opts.courseId) {
|
|
2801
|
+
hydrationInitRef.current = true;
|
|
2802
|
+
const saved = loadCompoundState2(storage, opts.courseId, opts.compoundId);
|
|
2803
|
+
if (saved && Object.keys(saved.childStates).length > 0) {
|
|
2804
|
+
loadedChildStatesRef.current = { ...saved.childStates };
|
|
2805
|
+
skipSaveUntilHydratedRef.current = true;
|
|
2806
|
+
pendingChildResumeRef.current = saved;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2499
2809
|
const buildState = useCallback6(() => {
|
|
2500
2810
|
const childStates = {
|
|
2501
2811
|
...loadedChildStatesRef.current
|
|
2502
2812
|
};
|
|
2503
2813
|
if (ctx) {
|
|
2504
|
-
for (const [checkId,
|
|
2814
|
+
for (const [checkId, entry] of ctx.getRegisteredHandles()) {
|
|
2815
|
+
const handle = entry.handle;
|
|
2505
2816
|
if (handle.getCurrentState) {
|
|
2506
2817
|
childStates[checkId] = handle.getCurrentState();
|
|
2507
2818
|
delete loadedChildStatesRef.current[checkId];
|
|
@@ -2513,14 +2824,44 @@ function useCompoundPersistence(opts) {
|
|
|
2513
2824
|
childStates
|
|
2514
2825
|
});
|
|
2515
2826
|
}, [ctx, opts.index, opts.pageCount]);
|
|
2827
|
+
const buildStateRef = useRef12(buildState);
|
|
2828
|
+
buildStateRef.current = buildState;
|
|
2829
|
+
const finalizeHydration = useCallback6(
|
|
2830
|
+
(childStates) => {
|
|
2831
|
+
loadedChildStatesRef.current = {
|
|
2832
|
+
...loadedChildStatesRef.current,
|
|
2833
|
+
...childStates
|
|
2834
|
+
};
|
|
2835
|
+
skipSaveUntilHydratedRef.current = false;
|
|
2836
|
+
pendingChildResumeRef.current = null;
|
|
2837
|
+
},
|
|
2838
|
+
[]
|
|
2839
|
+
);
|
|
2516
2840
|
const applyPendingChildResume = useCallback6(() => {
|
|
2517
2841
|
const pending = pendingChildResumeRef.current;
|
|
2518
2842
|
if (!pending || !ctx) return;
|
|
2519
|
-
const
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2843
|
+
const handles = ctx.getHandles();
|
|
2844
|
+
const applied = resumeChildHandles(handles, pending.childStates, {
|
|
2845
|
+
waitForHandles: true,
|
|
2846
|
+
alreadyResumed: resumedChildKeysRef.current
|
|
2847
|
+
});
|
|
2848
|
+
if (!applied) {
|
|
2849
|
+
const handlesAtWait = handles.size;
|
|
2850
|
+
queueMicrotask(() => {
|
|
2851
|
+
if (pendingChildResumeRef.current !== pending) return;
|
|
2852
|
+
const handlesNow = ctx.getHandles();
|
|
2853
|
+
if (handlesNow.size !== handlesAtWait) return;
|
|
2854
|
+
const registeredOnly2 = stripOrphanChildStates(handlesNow, pending.childStates);
|
|
2855
|
+
resumeChildHandles(handlesNow, registeredOnly2, {
|
|
2856
|
+
alreadyResumed: resumedChildKeysRef.current
|
|
2857
|
+
});
|
|
2858
|
+
finalizeHydration(registeredOnly2);
|
|
2859
|
+
});
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
const registeredOnly = stripOrphanChildStates(handles, pending.childStates);
|
|
2863
|
+
finalizeHydration(registeredOnly);
|
|
2864
|
+
}, [ctx, finalizeHydration]);
|
|
2524
2865
|
const saveResume = useCompoundResume({
|
|
2525
2866
|
courseId: opts.courseId,
|
|
2526
2867
|
compoundId: opts.compoundId,
|
|
@@ -2531,26 +2872,54 @@ function useCompoundPersistence(opts) {
|
|
|
2531
2872
|
loadedChildStatesRef.current = { ...state.childStates };
|
|
2532
2873
|
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
2533
2874
|
opts.setIndex(clamped);
|
|
2534
|
-
|
|
2875
|
+
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
2876
|
+
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
2535
2877
|
queueMicrotask(() => applyPendingChildResume());
|
|
2536
2878
|
}
|
|
2537
2879
|
});
|
|
2538
|
-
|
|
2880
|
+
const persistNow = useCallback6(() => {
|
|
2539
2881
|
if (!opts.enabled || !opts.courseId) return;
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2882
|
+
saveResume(buildStateRef.current());
|
|
2883
|
+
}, [opts.enabled, opts.courseId, saveResume]);
|
|
2884
|
+
const notifyImperativeResume = useCallback6(
|
|
2885
|
+
(state) => {
|
|
2886
|
+
const clamped = clampCompoundPageIndex2(state.activePageIndex, opts.pageCount);
|
|
2887
|
+
loadedChildStatesRef.current = { ...state.childStates };
|
|
2888
|
+
skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
|
|
2889
|
+
opts.setIndex(clamped);
|
|
2890
|
+
resumedChildKeysRef.current = /* @__PURE__ */ new Set();
|
|
2891
|
+
pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
|
|
2892
|
+
queueMicrotask(() => applyPendingChildResume());
|
|
2893
|
+
},
|
|
2894
|
+
[opts.pageCount, opts.setIndex, applyPendingChildResume]
|
|
2895
|
+
);
|
|
2896
|
+
useEffect11(() => {
|
|
2897
|
+
if (!bridgeRef) return;
|
|
2898
|
+
bridgeRef.current = { notifyImperativeResume };
|
|
2899
|
+
return () => {
|
|
2900
|
+
if (bridgeRef.current?.notifyImperativeResume === notifyImperativeResume) {
|
|
2901
|
+
bridgeRef.current = null;
|
|
2902
|
+
}
|
|
2903
|
+
};
|
|
2904
|
+
}, [bridgeRef, notifyImperativeResume]);
|
|
2905
|
+
useEffect11(() => {
|
|
2906
|
+
persistNow();
|
|
2907
|
+
}, [persistNow, opts.index, opts.pageCount, handlesVersion]);
|
|
2551
2908
|
useEffect11(() => {
|
|
2552
2909
|
applyPendingChildResume();
|
|
2553
2910
|
}, [opts.index, handlesVersion, applyPendingChildResume]);
|
|
2911
|
+
useEffect11(() => {
|
|
2912
|
+
if (!opts.enabled || !opts.courseId || typeof document === "undefined") return;
|
|
2913
|
+
const flushOnExit = () => {
|
|
2914
|
+
if (document.visibilityState === "hidden") persistNow();
|
|
2915
|
+
};
|
|
2916
|
+
document.addEventListener("visibilitychange", flushOnExit);
|
|
2917
|
+
window.addEventListener("pagehide", flushOnExit);
|
|
2918
|
+
return () => {
|
|
2919
|
+
document.removeEventListener("visibilitychange", flushOnExit);
|
|
2920
|
+
window.removeEventListener("pagehide", flushOnExit);
|
|
2921
|
+
};
|
|
2922
|
+
}, [opts.enabled, opts.courseId, persistNow]);
|
|
2554
2923
|
}
|
|
2555
2924
|
|
|
2556
2925
|
// src/compound/useCompoundShell.ts
|
|
@@ -2571,6 +2940,7 @@ function useCompoundShell(opts) {
|
|
|
2571
2940
|
activePageIndex: visibleIndex,
|
|
2572
2941
|
setActivePageIndex: opts.setIndex,
|
|
2573
2942
|
getHandles: () => ctx?.getHandles() ?? /* @__PURE__ */ new Map(),
|
|
2943
|
+
getRegisteredHandles: () => ctx?.getRegisteredHandles() ?? /* @__PURE__ */ new Map(),
|
|
2574
2944
|
pageCount: opts.pageCount,
|
|
2575
2945
|
enableSolutionsButton: opts.enableSolutionsButton
|
|
2576
2946
|
});
|
|
@@ -2590,7 +2960,7 @@ function useCompoundInitialIndex(opts) {
|
|
|
2590
2960
|
}
|
|
2591
2961
|
|
|
2592
2962
|
// src/compound/validateChildren.ts
|
|
2593
|
-
import
|
|
2963
|
+
import React15 from "react";
|
|
2594
2964
|
import {
|
|
2595
2965
|
ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
2596
2966
|
COMPOUND_MAX_NESTING_DEPTH,
|
|
@@ -2619,6 +2989,8 @@ var warnedPairs = /* @__PURE__ */ new Set();
|
|
|
2619
2989
|
var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
2620
2990
|
"Page",
|
|
2621
2991
|
"InteractiveBook",
|
|
2992
|
+
"Slide",
|
|
2993
|
+
"SlideDeck",
|
|
2622
2994
|
"AssessmentSequence"
|
|
2623
2995
|
]);
|
|
2624
2996
|
function warnOrThrow(msg, strict) {
|
|
@@ -2629,8 +3001,8 @@ function warnOrThrow(msg, strict) {
|
|
|
2629
3001
|
}
|
|
2630
3002
|
}
|
|
2631
3003
|
function validateNode(parent, node, depth, strict) {
|
|
2632
|
-
|
|
2633
|
-
if (!
|
|
3004
|
+
React15.Children.forEach(node, (child) => {
|
|
3005
|
+
if (!React15.isValidElement(child)) return;
|
|
2634
3006
|
const blockType = getLessonkitBlockType(child.type);
|
|
2635
3007
|
if (!blockType) {
|
|
2636
3008
|
if (child.props && typeof child.props === "object" && "children" in child.props) {
|
|
@@ -2670,8 +3042,8 @@ function validateNode(parent, node, depth, strict) {
|
|
|
2670
3042
|
});
|
|
2671
3043
|
}
|
|
2672
3044
|
function validateSubtreeForForbidden(node, forbidden, strict) {
|
|
2673
|
-
|
|
2674
|
-
if (!
|
|
3045
|
+
React15.Children.forEach(node, (child) => {
|
|
3046
|
+
if (!React15.isValidElement(child)) return;
|
|
2675
3047
|
const blockType = getLessonkitBlockType(child.type);
|
|
2676
3048
|
if (blockType && forbidden.includes(blockType)) {
|
|
2677
3049
|
warnOrThrow(`[lessonkit] Block "${blockType}" must not nest inside Accordion`, strict);
|
|
@@ -2711,7 +3083,7 @@ function warnSharedCompoundStorageKey(opts) {
|
|
|
2711
3083
|
}
|
|
2712
3084
|
|
|
2713
3085
|
// src/blocks/AssessmentSequence.tsx
|
|
2714
|
-
import { jsx as
|
|
3086
|
+
import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2715
3087
|
var AssessmentSequenceInner = forwardRef7(
|
|
2716
3088
|
function AssessmentSequenceInner2(props, ref) {
|
|
2717
3089
|
const { compoundId, childArray, index, setIndex, persistEnabled } = props;
|
|
@@ -2729,7 +3101,7 @@ var AssessmentSequenceInner = forwardRef7(
|
|
|
2729
3101
|
});
|
|
2730
3102
|
validateCompoundChildren("AssessmentSequence", props.children);
|
|
2731
3103
|
if (!sequential) {
|
|
2732
|
-
return /* @__PURE__ */
|
|
3104
|
+
return /* @__PURE__ */ jsx13("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
|
|
2733
3105
|
}
|
|
2734
3106
|
return /* @__PURE__ */ jsxs9("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
|
|
2735
3107
|
/* @__PURE__ */ jsxs9("p", { children: [
|
|
@@ -2738,9 +3110,9 @@ var AssessmentSequenceInner = forwardRef7(
|
|
|
2738
3110
|
" of ",
|
|
2739
3111
|
progress.total
|
|
2740
3112
|
] }),
|
|
2741
|
-
/* @__PURE__ */
|
|
3113
|
+
/* @__PURE__ */ jsx13("div", { "data-testid": "assessment-sequence-step", children: childArray.map((child, i) => /* @__PURE__ */ jsx13("div", { hidden: i !== visibleIndex, children: /* @__PURE__ */ jsx13(CompoundPageIndexProvider, { pageIndex: i, children: child }) }, child.key ?? i)) }),
|
|
2742
3114
|
/* @__PURE__ */ jsxs9("nav", { "aria-label": "Sequence navigation", children: [
|
|
2743
|
-
/* @__PURE__ */
|
|
3115
|
+
/* @__PURE__ */ jsx13(
|
|
2744
3116
|
"button",
|
|
2745
3117
|
{
|
|
2746
3118
|
type: "button",
|
|
@@ -2750,7 +3122,7 @@ var AssessmentSequenceInner = forwardRef7(
|
|
|
2750
3122
|
children: "Previous"
|
|
2751
3123
|
}
|
|
2752
3124
|
),
|
|
2753
|
-
/* @__PURE__ */
|
|
3125
|
+
/* @__PURE__ */ jsx13(
|
|
2754
3126
|
"button",
|
|
2755
3127
|
{
|
|
2756
3128
|
type: "button",
|
|
@@ -2766,14 +3138,19 @@ var AssessmentSequenceInner = forwardRef7(
|
|
|
2766
3138
|
);
|
|
2767
3139
|
var AssessmentSequence = forwardRef7(
|
|
2768
3140
|
function AssessmentSequence2(props, ref) {
|
|
3141
|
+
const reactInstanceId = useId3();
|
|
3142
|
+
const autoCompoundIdRef = useRef13(null);
|
|
3143
|
+
if (!props.blockId && !autoCompoundIdRef.current) {
|
|
3144
|
+
autoCompoundIdRef.current = deriveId(`assessment-sequence-${reactInstanceId}`);
|
|
3145
|
+
}
|
|
2769
3146
|
const compoundId = useMemo13(
|
|
2770
|
-
() => props.blockId ? normalizeComponentId(props.blockId, "blockId") : DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
|
|
3147
|
+
() => props.blockId ? normalizeComponentId(props.blockId, "blockId") : autoCompoundIdRef.current ?? DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
|
|
2771
3148
|
[props.blockId]
|
|
2772
3149
|
);
|
|
2773
|
-
const childArray =
|
|
2774
|
-
|
|
3150
|
+
const childArray = React16.Children.toArray(props.children).filter(
|
|
3151
|
+
React16.isValidElement
|
|
2775
3152
|
);
|
|
2776
|
-
const { config } = useLessonkit();
|
|
3153
|
+
const { config, storage } = useLessonkit();
|
|
2777
3154
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
2778
3155
|
useEffect12(() => {
|
|
2779
3156
|
warnSharedCompoundStorageKey({
|
|
@@ -2786,11 +3163,15 @@ var AssessmentSequence = forwardRef7(
|
|
|
2786
3163
|
courseId: config.courseId,
|
|
2787
3164
|
compoundId,
|
|
2788
3165
|
pageCount: childArray.length,
|
|
2789
|
-
persistEnabled
|
|
3166
|
+
persistEnabled,
|
|
3167
|
+
storage
|
|
2790
3168
|
});
|
|
2791
3169
|
const [index, setIndex] = useState10(initialIndex);
|
|
2792
3170
|
const setIndexStable = useCallback7((i) => setIndex(i), []);
|
|
2793
|
-
|
|
3171
|
+
useEffect12(() => {
|
|
3172
|
+
setIndex(initialIndex);
|
|
3173
|
+
}, [config.courseId, compoundId, initialIndex]);
|
|
3174
|
+
return /* @__PURE__ */ jsx13(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx13(
|
|
2794
3175
|
AssessmentSequenceInner,
|
|
2795
3176
|
{
|
|
2796
3177
|
...props,
|
|
@@ -2808,24 +3189,24 @@ setLessonkitBlockType(AssessmentSequence, "AssessmentSequence");
|
|
|
2808
3189
|
|
|
2809
3190
|
// src/blocks/Text.tsx
|
|
2810
3191
|
import "react";
|
|
2811
|
-
import { jsx as
|
|
3192
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
2812
3193
|
function Text(props) {
|
|
2813
|
-
return /* @__PURE__ */
|
|
3194
|
+
return /* @__PURE__ */ jsx14("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
|
|
2814
3195
|
}
|
|
2815
3196
|
setLessonkitBlockType(Text, "Text");
|
|
2816
3197
|
|
|
2817
3198
|
// src/blocks/Heading.tsx
|
|
2818
|
-
import { jsx as
|
|
3199
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
2819
3200
|
function Heading(props) {
|
|
2820
3201
|
const Tag = `h${props.level}`;
|
|
2821
|
-
return /* @__PURE__ */
|
|
3202
|
+
return /* @__PURE__ */ jsx15(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
|
|
2822
3203
|
}
|
|
2823
3204
|
setLessonkitBlockType(Heading, "Heading");
|
|
2824
3205
|
|
|
2825
3206
|
// src/blocks/Image.tsx
|
|
2826
|
-
import { jsx as
|
|
3207
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
2827
3208
|
function Image(props) {
|
|
2828
|
-
return /* @__PURE__ */
|
|
3209
|
+
return /* @__PURE__ */ jsx16(
|
|
2829
3210
|
"img",
|
|
2830
3211
|
{
|
|
2831
3212
|
src: props.src,
|
|
@@ -2840,13 +3221,13 @@ setLessonkitBlockType(Image, "Image");
|
|
|
2840
3221
|
|
|
2841
3222
|
// src/blocks/Page.tsx
|
|
2842
3223
|
import { useEffect as useEffect13 } from "react";
|
|
2843
|
-
import { jsx as
|
|
3224
|
+
import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2844
3225
|
function Page(props) {
|
|
2845
3226
|
validateCompoundChildren("Page", props.children);
|
|
2846
3227
|
const { track } = useLessonkit();
|
|
2847
3228
|
const lessonId = useEnclosingLessonId();
|
|
2848
3229
|
useEffect13(() => {
|
|
2849
|
-
if (props.hidden || !lessonId) return;
|
|
3230
|
+
if (props.hidden || !lessonId || props.parentType) return;
|
|
2850
3231
|
track(
|
|
2851
3232
|
"compound_page_viewed",
|
|
2852
3233
|
{
|
|
@@ -2865,8 +3246,8 @@ function Page(props) {
|
|
|
2865
3246
|
"data-testid": `page-${props.blockId}`,
|
|
2866
3247
|
hidden: props.hidden ? true : void 0,
|
|
2867
3248
|
children: [
|
|
2868
|
-
props.title ? /* @__PURE__ */
|
|
2869
|
-
/* @__PURE__ */
|
|
3249
|
+
props.title ? /* @__PURE__ */ jsx17("h3", { children: props.title }) : null,
|
|
3250
|
+
/* @__PURE__ */ jsx17(CompoundPageIndexProvider, { pageIndex: props.pageIndex ?? 0, children: /* @__PURE__ */ jsx17("div", { children: props.children }) })
|
|
2870
3251
|
]
|
|
2871
3252
|
}
|
|
2872
3253
|
);
|
|
@@ -2874,8 +3255,8 @@ function Page(props) {
|
|
|
2874
3255
|
setLessonkitBlockType(Page, "Page");
|
|
2875
3256
|
|
|
2876
3257
|
// src/blocks/InteractiveBook.tsx
|
|
2877
|
-
import
|
|
2878
|
-
import { jsx as
|
|
3258
|
+
import React19, { forwardRef as forwardRef8, useCallback as useCallback8, useEffect as useEffect14, useMemo as useMemo14, useState as useState11 } from "react";
|
|
3259
|
+
import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2879
3260
|
var InteractiveBookInner = forwardRef8(
|
|
2880
3261
|
function InteractiveBookInner2(props, ref) {
|
|
2881
3262
|
const { blockId, pages, index, setIndex, persistEnabled } = props;
|
|
@@ -2908,7 +3289,7 @@ var InteractiveBookInner = forwardRef8(
|
|
|
2908
3289
|
);
|
|
2909
3290
|
}, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
|
|
2910
3291
|
return /* @__PURE__ */ jsxs11("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
|
|
2911
|
-
/* @__PURE__ */
|
|
3292
|
+
/* @__PURE__ */ jsx18("h3", { children: props.title }),
|
|
2912
3293
|
/* @__PURE__ */ jsxs11("p", { children: [
|
|
2913
3294
|
"Page ",
|
|
2914
3295
|
progress.current,
|
|
@@ -2922,8 +3303,8 @@ var InteractiveBookInner = forwardRef8(
|
|
|
2922
3303
|
" ",
|
|
2923
3304
|
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
2924
3305
|
] }) : null,
|
|
2925
|
-
/* @__PURE__ */
|
|
2926
|
-
(page, i) =>
|
|
3306
|
+
/* @__PURE__ */ jsx18("div", { "data-testid": "interactive-book-page", children: pages.map(
|
|
3307
|
+
(page, i) => React19.cloneElement(page, {
|
|
2927
3308
|
key: page.key ?? page.props.blockId,
|
|
2928
3309
|
hidden: i !== visibleIndex,
|
|
2929
3310
|
pageIndex: i,
|
|
@@ -2931,7 +3312,7 @@ var InteractiveBookInner = forwardRef8(
|
|
|
2931
3312
|
})
|
|
2932
3313
|
) }),
|
|
2933
3314
|
/* @__PURE__ */ jsxs11("nav", { "aria-label": "Book navigation", children: [
|
|
2934
|
-
/* @__PURE__ */
|
|
3315
|
+
/* @__PURE__ */ jsx18(
|
|
2935
3316
|
"button",
|
|
2936
3317
|
{
|
|
2937
3318
|
type: "button",
|
|
@@ -2941,7 +3322,7 @@ var InteractiveBookInner = forwardRef8(
|
|
|
2941
3322
|
children: "Previous"
|
|
2942
3323
|
}
|
|
2943
3324
|
),
|
|
2944
|
-
/* @__PURE__ */
|
|
3325
|
+
/* @__PURE__ */ jsx18(
|
|
2945
3326
|
"button",
|
|
2946
3327
|
{
|
|
2947
3328
|
type: "button",
|
|
@@ -2960,20 +3341,24 @@ var InteractiveBook = forwardRef8(function InteractiveBook2(props, ref) {
|
|
|
2960
3341
|
() => normalizeComponentId(props.blockId, "blockId"),
|
|
2961
3342
|
[props.blockId]
|
|
2962
3343
|
);
|
|
2963
|
-
const pages =
|
|
2964
|
-
|
|
3344
|
+
const pages = React19.Children.toArray(props.children).filter(
|
|
3345
|
+
React19.isValidElement
|
|
2965
3346
|
);
|
|
2966
|
-
const { config } = useLessonkit();
|
|
3347
|
+
const { config, storage } = useLessonkit();
|
|
2967
3348
|
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
2968
3349
|
const initialIndex = useCompoundInitialIndex({
|
|
2969
3350
|
courseId: config.courseId,
|
|
2970
3351
|
compoundId: blockId,
|
|
2971
3352
|
pageCount: pages.length,
|
|
2972
|
-
persistEnabled
|
|
3353
|
+
persistEnabled,
|
|
3354
|
+
storage
|
|
2973
3355
|
});
|
|
2974
3356
|
const [index, setIndex] = useState11(initialIndex);
|
|
2975
3357
|
const setIndexStable = useCallback8((i) => setIndex(i), []);
|
|
2976
|
-
|
|
3358
|
+
useEffect14(() => {
|
|
3359
|
+
setIndex(initialIndex);
|
|
3360
|
+
}, [config.courseId, blockId, initialIndex]);
|
|
3361
|
+
return /* @__PURE__ */ jsx18(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx18(
|
|
2977
3362
|
InteractiveBookInner,
|
|
2978
3363
|
{
|
|
2979
3364
|
...props,
|
|
@@ -2988,17 +3373,249 @@ var InteractiveBook = forwardRef8(function InteractiveBook2(props, ref) {
|
|
|
2988
3373
|
});
|
|
2989
3374
|
setLessonkitBlockType(InteractiveBook, "InteractiveBook");
|
|
2990
3375
|
|
|
3376
|
+
// src/blocks/Slide.tsx
|
|
3377
|
+
import { useEffect as useEffect15 } from "react";
|
|
3378
|
+
import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3379
|
+
function Slide(props) {
|
|
3380
|
+
validateCompoundChildren("Slide", props.children);
|
|
3381
|
+
const { track } = useLessonkit();
|
|
3382
|
+
const lessonId = useEnclosingLessonId();
|
|
3383
|
+
useEffect15(() => {
|
|
3384
|
+
if (props.hidden || !lessonId || props.parentType) return;
|
|
3385
|
+
track(
|
|
3386
|
+
"compound_page_viewed",
|
|
3387
|
+
{
|
|
3388
|
+
blockId: props.blockId,
|
|
3389
|
+
pageIndex: props.slideIndex ?? 0,
|
|
3390
|
+
parentType: props.parentType
|
|
3391
|
+
},
|
|
3392
|
+
{ lessonId }
|
|
3393
|
+
);
|
|
3394
|
+
}, [props.hidden, props.slideIndex, props.parentType, props.blockId, lessonId, track]);
|
|
3395
|
+
return /* @__PURE__ */ jsxs12(
|
|
3396
|
+
"section",
|
|
3397
|
+
{
|
|
3398
|
+
"aria-label": props.title ?? "Slide",
|
|
3399
|
+
"data-lk-block-id": props.blockId,
|
|
3400
|
+
"data-testid": `slide-${props.blockId}`,
|
|
3401
|
+
hidden: props.hidden ? true : void 0,
|
|
3402
|
+
children: [
|
|
3403
|
+
props.title ? /* @__PURE__ */ jsx19("h3", { children: props.title }) : null,
|
|
3404
|
+
/* @__PURE__ */ jsx19(CompoundPageIndexProvider, { pageIndex: props.slideIndex ?? 0, children: /* @__PURE__ */ jsx19("div", { children: props.children }) })
|
|
3405
|
+
]
|
|
3406
|
+
}
|
|
3407
|
+
);
|
|
3408
|
+
}
|
|
3409
|
+
setLessonkitBlockType(Slide, "Slide");
|
|
3410
|
+
|
|
3411
|
+
// src/blocks/SlideDeck.tsx
|
|
3412
|
+
import React21, { forwardRef as forwardRef9, useCallback as useCallback9, useEffect as useEffect17, useMemo as useMemo15, useRef as useRef14, useState as useState12 } from "react";
|
|
3413
|
+
|
|
3414
|
+
// src/compound/useCompoundKeyboardNav.ts
|
|
3415
|
+
import { useEffect as useEffect16 } from "react";
|
|
3416
|
+
var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT", "BUTTON"]);
|
|
3417
|
+
function isEditableTarget(target) {
|
|
3418
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
3419
|
+
if (INTERACTIVE_TAGS.has(target.tagName)) return true;
|
|
3420
|
+
if (target.isContentEditable) return true;
|
|
3421
|
+
if (target.closest("[role='slider'], [role='listbox'], [data-lk-assessment-interactive]")) {
|
|
3422
|
+
return true;
|
|
3423
|
+
}
|
|
3424
|
+
return false;
|
|
3425
|
+
}
|
|
3426
|
+
function useCompoundKeyboardNav(opts) {
|
|
3427
|
+
const { containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex } = opts;
|
|
3428
|
+
useEffect16(() => {
|
|
3429
|
+
const el = containerRef.current;
|
|
3430
|
+
if (!el || pageCount === 0) return;
|
|
3431
|
+
const onKeyDown = (event) => {
|
|
3432
|
+
if (!el.contains(document.activeElement) && document.activeElement !== document.body) {
|
|
3433
|
+
return;
|
|
3434
|
+
}
|
|
3435
|
+
if (isEditableTarget(event.target)) return;
|
|
3436
|
+
switch (event.key) {
|
|
3437
|
+
case "ArrowRight":
|
|
3438
|
+
case "ArrowDown":
|
|
3439
|
+
if (visibleIndex < pageCount - 1) {
|
|
3440
|
+
event.preventDefault();
|
|
3441
|
+
goNext();
|
|
3442
|
+
}
|
|
3443
|
+
break;
|
|
3444
|
+
case "ArrowLeft":
|
|
3445
|
+
case "ArrowUp":
|
|
3446
|
+
if (visibleIndex > 0) {
|
|
3447
|
+
event.preventDefault();
|
|
3448
|
+
goPrev();
|
|
3449
|
+
}
|
|
3450
|
+
break;
|
|
3451
|
+
case "Home":
|
|
3452
|
+
if (visibleIndex !== 0) {
|
|
3453
|
+
event.preventDefault();
|
|
3454
|
+
setIndex(0);
|
|
3455
|
+
}
|
|
3456
|
+
break;
|
|
3457
|
+
case "End":
|
|
3458
|
+
if (visibleIndex !== pageCount - 1) {
|
|
3459
|
+
event.preventDefault();
|
|
3460
|
+
setIndex(pageCount - 1);
|
|
3461
|
+
}
|
|
3462
|
+
break;
|
|
3463
|
+
default:
|
|
3464
|
+
break;
|
|
3465
|
+
}
|
|
3466
|
+
};
|
|
3467
|
+
el.addEventListener("keydown", onKeyDown);
|
|
3468
|
+
return () => el.removeEventListener("keydown", onKeyDown);
|
|
3469
|
+
}, [containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex]);
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
// src/blocks/SlideDeck.tsx
|
|
3473
|
+
import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3474
|
+
var SlideDeckInner = forwardRef9(function SlideDeckInner2(props, ref) {
|
|
3475
|
+
const { blockId, slides, index, setIndex, persistEnabled } = props;
|
|
3476
|
+
validateCompoundChildren("SlideDeck", slides);
|
|
3477
|
+
const { config, track } = useLessonkit();
|
|
3478
|
+
const lessonId = useEnclosingLessonId();
|
|
3479
|
+
const containerRef = useRef14(null);
|
|
3480
|
+
const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
|
|
3481
|
+
courseId: config.courseId,
|
|
3482
|
+
compoundId: blockId,
|
|
3483
|
+
pageCount: slides.length,
|
|
3484
|
+
index,
|
|
3485
|
+
setIndex,
|
|
3486
|
+
persistEnabled,
|
|
3487
|
+
ref
|
|
3488
|
+
});
|
|
3489
|
+
const setIndexStable = useCallback9((i) => setIndex(i), [setIndex]);
|
|
3490
|
+
useCompoundKeyboardNav({
|
|
3491
|
+
containerRef,
|
|
3492
|
+
visibleIndex,
|
|
3493
|
+
pageCount: slides.length,
|
|
3494
|
+
goNext,
|
|
3495
|
+
goPrev,
|
|
3496
|
+
setIndex: setIndexStable
|
|
3497
|
+
});
|
|
3498
|
+
const slideTitles = useMemo15(
|
|
3499
|
+
() => slides.map((slide) => slide.props.title),
|
|
3500
|
+
[slides]
|
|
3501
|
+
);
|
|
3502
|
+
useEffect17(() => {
|
|
3503
|
+
if (!lessonId || slides.length === 0) return;
|
|
3504
|
+
track(
|
|
3505
|
+
"slide_viewed",
|
|
3506
|
+
{
|
|
3507
|
+
blockId,
|
|
3508
|
+
slideIndex: visibleIndex,
|
|
3509
|
+
slideTitle: slideTitles[visibleIndex]
|
|
3510
|
+
},
|
|
3511
|
+
{ lessonId }
|
|
3512
|
+
);
|
|
3513
|
+
}, [visibleIndex, blockId, lessonId, slides.length, slideTitles, track]);
|
|
3514
|
+
return /* @__PURE__ */ jsxs13(
|
|
3515
|
+
"section",
|
|
3516
|
+
{
|
|
3517
|
+
ref: containerRef,
|
|
3518
|
+
tabIndex: -1,
|
|
3519
|
+
"aria-label": props.title,
|
|
3520
|
+
"data-testid": "slide-deck",
|
|
3521
|
+
"data-lk-block-id": blockId,
|
|
3522
|
+
children: [
|
|
3523
|
+
/* @__PURE__ */ jsx20("h3", { children: props.title }),
|
|
3524
|
+
/* @__PURE__ */ jsxs13("p", { children: [
|
|
3525
|
+
"Slide ",
|
|
3526
|
+
progress.current,
|
|
3527
|
+
" of ",
|
|
3528
|
+
progress.total
|
|
3529
|
+
] }),
|
|
3530
|
+
props.showDeckScore && ctx ? /* @__PURE__ */ jsxs13("p", { "data-testid": "deck-score", children: [
|
|
3531
|
+
"Score: ",
|
|
3532
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
|
|
3533
|
+
" /",
|
|
3534
|
+
" ",
|
|
3535
|
+
Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
|
|
3536
|
+
] }) : null,
|
|
3537
|
+
/* @__PURE__ */ jsx20("div", { "data-testid": "slide-deck-slide", children: slides.map(
|
|
3538
|
+
(slide, i) => React21.cloneElement(slide, {
|
|
3539
|
+
key: slide.key ?? slide.props.blockId,
|
|
3540
|
+
hidden: i !== visibleIndex,
|
|
3541
|
+
slideIndex: i,
|
|
3542
|
+
parentType: "SlideDeck"
|
|
3543
|
+
})
|
|
3544
|
+
) }),
|
|
3545
|
+
/* @__PURE__ */ jsxs13("nav", { "aria-label": "Slide navigation", children: [
|
|
3546
|
+
/* @__PURE__ */ jsx20(
|
|
3547
|
+
"button",
|
|
3548
|
+
{
|
|
3549
|
+
type: "button",
|
|
3550
|
+
"data-testid": "slide-prev",
|
|
3551
|
+
disabled: visibleIndex === 0 || slides.length === 0,
|
|
3552
|
+
onClick: goPrev,
|
|
3553
|
+
children: "Previous slide"
|
|
3554
|
+
}
|
|
3555
|
+
),
|
|
3556
|
+
/* @__PURE__ */ jsx20(
|
|
3557
|
+
"button",
|
|
3558
|
+
{
|
|
3559
|
+
type: "button",
|
|
3560
|
+
"data-testid": "slide-next",
|
|
3561
|
+
disabled: visibleIndex >= slides.length - 1 || slides.length === 0,
|
|
3562
|
+
onClick: goNext,
|
|
3563
|
+
children: "Next slide"
|
|
3564
|
+
}
|
|
3565
|
+
)
|
|
3566
|
+
] })
|
|
3567
|
+
]
|
|
3568
|
+
}
|
|
3569
|
+
);
|
|
3570
|
+
});
|
|
3571
|
+
var SlideDeck = forwardRef9(function SlideDeck2(props, ref) {
|
|
3572
|
+
const blockId = useMemo15(
|
|
3573
|
+
() => normalizeComponentId(props.blockId, "blockId"),
|
|
3574
|
+
[props.blockId]
|
|
3575
|
+
);
|
|
3576
|
+
const slides = React21.Children.toArray(props.children).filter(
|
|
3577
|
+
React21.isValidElement
|
|
3578
|
+
);
|
|
3579
|
+
const { config, storage } = useLessonkit();
|
|
3580
|
+
const persistEnabled = config.session?.persistCompoundState !== false;
|
|
3581
|
+
const initialIndex = useCompoundInitialIndex({
|
|
3582
|
+
courseId: config.courseId,
|
|
3583
|
+
compoundId: blockId,
|
|
3584
|
+
pageCount: slides.length,
|
|
3585
|
+
persistEnabled,
|
|
3586
|
+
storage
|
|
3587
|
+
});
|
|
3588
|
+
const [index, setIndex] = useState12(initialIndex);
|
|
3589
|
+
const setIndexStable = useCallback9((i) => setIndex(i), []);
|
|
3590
|
+
useEffect17(() => {
|
|
3591
|
+
setIndex(initialIndex);
|
|
3592
|
+
}, [config.courseId, blockId, initialIndex]);
|
|
3593
|
+
return /* @__PURE__ */ jsx20(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx20(
|
|
3594
|
+
SlideDeckInner,
|
|
3595
|
+
{
|
|
3596
|
+
...props,
|
|
3597
|
+
ref,
|
|
3598
|
+
blockId,
|
|
3599
|
+
slides,
|
|
3600
|
+
index,
|
|
3601
|
+
setIndex,
|
|
3602
|
+
persistEnabled
|
|
3603
|
+
}
|
|
3604
|
+
) });
|
|
3605
|
+
});
|
|
3606
|
+
setLessonkitBlockType(SlideDeck, "SlideDeck");
|
|
3607
|
+
|
|
2991
3608
|
// src/blocks/Accordion.tsx
|
|
2992
|
-
import { useId as
|
|
2993
|
-
import { jsx as
|
|
3609
|
+
import { useId as useId4, useState as useState13 } from "react";
|
|
3610
|
+
import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2994
3611
|
function Accordion(props) {
|
|
2995
3612
|
if (isDevEnvironment4()) {
|
|
2996
3613
|
validateAccordionSections(props.sections);
|
|
2997
3614
|
}
|
|
2998
|
-
const [open, setOpen] =
|
|
3615
|
+
const [open, setOpen] = useState13(/* @__PURE__ */ new Set());
|
|
2999
3616
|
const { track } = useLessonkit();
|
|
3000
3617
|
const lessonId = useEnclosingLessonId();
|
|
3001
|
-
const baseId =
|
|
3618
|
+
const baseId = useId4();
|
|
3002
3619
|
const toggle = (sectionId) => {
|
|
3003
3620
|
setOpen((prev) => {
|
|
3004
3621
|
const next = new Set(prev);
|
|
@@ -3013,12 +3630,12 @@ function Accordion(props) {
|
|
|
3013
3630
|
return next;
|
|
3014
3631
|
});
|
|
3015
3632
|
};
|
|
3016
|
-
return /* @__PURE__ */
|
|
3633
|
+
return /* @__PURE__ */ jsx21("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
|
|
3017
3634
|
const expanded = open.has(section.id);
|
|
3018
3635
|
const panelId = `${baseId}-${section.id}`;
|
|
3019
3636
|
const triggerId = `${baseId}-trigger-${section.id}`;
|
|
3020
|
-
return /* @__PURE__ */
|
|
3021
|
-
/* @__PURE__ */
|
|
3637
|
+
return /* @__PURE__ */ jsxs14("div", { "data-testid": `accordion-section-${section.id}`, children: [
|
|
3638
|
+
/* @__PURE__ */ jsx21("h4", { children: /* @__PURE__ */ jsx21(
|
|
3022
3639
|
"button",
|
|
3023
3640
|
{
|
|
3024
3641
|
id: triggerId,
|
|
@@ -3030,28 +3647,28 @@ function Accordion(props) {
|
|
|
3030
3647
|
children: section.title
|
|
3031
3648
|
}
|
|
3032
3649
|
) }),
|
|
3033
|
-
expanded ? /* @__PURE__ */
|
|
3650
|
+
expanded ? /* @__PURE__ */ jsx21("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
|
|
3034
3651
|
] }, section.id);
|
|
3035
3652
|
}) });
|
|
3036
3653
|
}
|
|
3037
3654
|
setLessonkitBlockType(Accordion, "Accordion");
|
|
3038
3655
|
|
|
3039
3656
|
// src/blocks/DialogCards.tsx
|
|
3040
|
-
import { useState as
|
|
3041
|
-
import { jsx as
|
|
3657
|
+
import { useState as useState14 } from "react";
|
|
3658
|
+
import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
3042
3659
|
function DialogCards(props) {
|
|
3043
|
-
const [index, setIndex] =
|
|
3044
|
-
const [flipped, setFlipped] =
|
|
3660
|
+
const [index, setIndex] = useState14(0);
|
|
3661
|
+
const [flipped, setFlipped] = useState14(false);
|
|
3045
3662
|
const card = props.cards[index];
|
|
3046
3663
|
if (!card) return null;
|
|
3047
|
-
return /* @__PURE__ */
|
|
3048
|
-
/* @__PURE__ */
|
|
3664
|
+
return /* @__PURE__ */ jsxs15("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
|
|
3665
|
+
/* @__PURE__ */ jsxs15("p", { children: [
|
|
3049
3666
|
"Card ",
|
|
3050
3667
|
index + 1,
|
|
3051
3668
|
" of ",
|
|
3052
3669
|
props.cards.length
|
|
3053
3670
|
] }),
|
|
3054
|
-
/* @__PURE__ */
|
|
3671
|
+
/* @__PURE__ */ jsx22(
|
|
3055
3672
|
"button",
|
|
3056
3673
|
{
|
|
3057
3674
|
type: "button",
|
|
@@ -3062,8 +3679,8 @@ function DialogCards(props) {
|
|
|
3062
3679
|
children: flipped ? card.back : card.front
|
|
3063
3680
|
}
|
|
3064
3681
|
),
|
|
3065
|
-
/* @__PURE__ */
|
|
3066
|
-
/* @__PURE__ */
|
|
3682
|
+
/* @__PURE__ */ jsxs15("nav", { "aria-label": "Card navigation", children: [
|
|
3683
|
+
/* @__PURE__ */ jsx22(
|
|
3067
3684
|
"button",
|
|
3068
3685
|
{
|
|
3069
3686
|
type: "button",
|
|
@@ -3076,7 +3693,7 @@ function DialogCards(props) {
|
|
|
3076
3693
|
children: "Previous"
|
|
3077
3694
|
}
|
|
3078
3695
|
),
|
|
3079
|
-
/* @__PURE__ */
|
|
3696
|
+
/* @__PURE__ */ jsx22(
|
|
3080
3697
|
"button",
|
|
3081
3698
|
{
|
|
3082
3699
|
type: "button",
|
|
@@ -3095,11 +3712,11 @@ function DialogCards(props) {
|
|
|
3095
3712
|
setLessonkitBlockType(DialogCards, "DialogCards");
|
|
3096
3713
|
|
|
3097
3714
|
// src/blocks/Flashcards.tsx
|
|
3098
|
-
import { useState as
|
|
3099
|
-
import { jsx as
|
|
3715
|
+
import { useState as useState15 } from "react";
|
|
3716
|
+
import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
3100
3717
|
function Flashcards(props) {
|
|
3101
|
-
const [index, setIndex] =
|
|
3102
|
-
const [face, setFace] =
|
|
3718
|
+
const [index, setIndex] = useState15(0);
|
|
3719
|
+
const [face, setFace] = useState15("front");
|
|
3103
3720
|
const { track } = useLessonkit();
|
|
3104
3721
|
const lessonId = useEnclosingLessonId();
|
|
3105
3722
|
const card = props.cards[index];
|
|
@@ -3113,10 +3730,10 @@ function Flashcards(props) {
|
|
|
3113
3730
|
lessonId ? { lessonId } : void 0
|
|
3114
3731
|
);
|
|
3115
3732
|
};
|
|
3116
|
-
return /* @__PURE__ */
|
|
3117
|
-
/* @__PURE__ */
|
|
3118
|
-
props.selfScore ? /* @__PURE__ */
|
|
3119
|
-
/* @__PURE__ */
|
|
3733
|
+
return /* @__PURE__ */ jsxs16("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
|
|
3734
|
+
/* @__PURE__ */ jsx23("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
|
|
3735
|
+
props.selfScore ? /* @__PURE__ */ jsx23("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
|
|
3736
|
+
/* @__PURE__ */ jsx23(
|
|
3120
3737
|
"button",
|
|
3121
3738
|
{
|
|
3122
3739
|
type: "button",
|
|
@@ -3134,10 +3751,10 @@ function Flashcards(props) {
|
|
|
3134
3751
|
setLessonkitBlockType(Flashcards, "Flashcards");
|
|
3135
3752
|
|
|
3136
3753
|
// src/blocks/ImageHotspots.tsx
|
|
3137
|
-
import { useState as
|
|
3138
|
-
import { jsx as
|
|
3754
|
+
import { useState as useState16 } from "react";
|
|
3755
|
+
import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
3139
3756
|
function ImageHotspots(props) {
|
|
3140
|
-
const [active, setActive] =
|
|
3757
|
+
const [active, setActive] = useState16(null);
|
|
3141
3758
|
const { track } = useLessonkit();
|
|
3142
3759
|
const lessonId = useEnclosingLessonId();
|
|
3143
3760
|
const open = (hotspotId) => {
|
|
@@ -3148,10 +3765,10 @@ function ImageHotspots(props) {
|
|
|
3148
3765
|
lessonId ? { lessonId } : void 0
|
|
3149
3766
|
);
|
|
3150
3767
|
};
|
|
3151
|
-
return /* @__PURE__ */
|
|
3152
|
-
/* @__PURE__ */
|
|
3153
|
-
/* @__PURE__ */
|
|
3154
|
-
props.hotspots.map((h) => /* @__PURE__ */
|
|
3768
|
+
return /* @__PURE__ */ jsxs17("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
|
|
3769
|
+
/* @__PURE__ */ jsxs17("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
3770
|
+
/* @__PURE__ */ jsx24("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
3771
|
+
props.hotspots.map((h) => /* @__PURE__ */ jsx24(
|
|
3155
3772
|
"button",
|
|
3156
3773
|
{
|
|
3157
3774
|
type: "button",
|
|
@@ -3170,19 +3787,19 @@ function ImageHotspots(props) {
|
|
|
3170
3787
|
h.id
|
|
3171
3788
|
))
|
|
3172
3789
|
] }),
|
|
3173
|
-
active ? /* @__PURE__ */
|
|
3790
|
+
active ? /* @__PURE__ */ jsxs17("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
|
|
3174
3791
|
props.hotspots.find((h) => h.id === active)?.content,
|
|
3175
|
-
/* @__PURE__ */
|
|
3792
|
+
/* @__PURE__ */ jsx24("button", { type: "button", onClick: () => setActive(null), children: "Close" })
|
|
3176
3793
|
] }) : null
|
|
3177
3794
|
] });
|
|
3178
3795
|
}
|
|
3179
3796
|
setLessonkitBlockType(ImageHotspots, "ImageHotspots");
|
|
3180
3797
|
|
|
3181
3798
|
// src/blocks/ImageSlider.tsx
|
|
3182
|
-
import { useState as
|
|
3183
|
-
import { jsx as
|
|
3799
|
+
import { useState as useState17 } from "react";
|
|
3800
|
+
import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
3184
3801
|
function ImageSlider(props) {
|
|
3185
|
-
const [index, setIndex] =
|
|
3802
|
+
const [index, setIndex] = useState17(0);
|
|
3186
3803
|
const { track } = useLessonkit();
|
|
3187
3804
|
const lessonId = useEnclosingLessonId();
|
|
3188
3805
|
const slide = props.slides[index];
|
|
@@ -3195,11 +3812,11 @@ function ImageSlider(props) {
|
|
|
3195
3812
|
lessonId ? { lessonId } : void 0
|
|
3196
3813
|
);
|
|
3197
3814
|
};
|
|
3198
|
-
return /* @__PURE__ */
|
|
3199
|
-
/* @__PURE__ */
|
|
3200
|
-
slide.caption ? /* @__PURE__ */
|
|
3201
|
-
/* @__PURE__ */
|
|
3202
|
-
/* @__PURE__ */
|
|
3815
|
+
return /* @__PURE__ */ jsxs18("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
|
|
3816
|
+
/* @__PURE__ */ jsx25("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
|
|
3817
|
+
slide.caption ? /* @__PURE__ */ jsx25("p", { children: slide.caption }) : null,
|
|
3818
|
+
/* @__PURE__ */ jsxs18("nav", { "aria-label": "Slide navigation", children: [
|
|
3819
|
+
/* @__PURE__ */ jsx25(
|
|
3203
3820
|
"button",
|
|
3204
3821
|
{
|
|
3205
3822
|
type: "button",
|
|
@@ -3209,12 +3826,12 @@ function ImageSlider(props) {
|
|
|
3209
3826
|
children: "Previous"
|
|
3210
3827
|
}
|
|
3211
3828
|
),
|
|
3212
|
-
/* @__PURE__ */
|
|
3829
|
+
/* @__PURE__ */ jsxs18("span", { children: [
|
|
3213
3830
|
index + 1,
|
|
3214
3831
|
" / ",
|
|
3215
3832
|
props.slides.length
|
|
3216
3833
|
] }),
|
|
3217
|
-
/* @__PURE__ */
|
|
3834
|
+
/* @__PURE__ */ jsx25(
|
|
3218
3835
|
"button",
|
|
3219
3836
|
{
|
|
3220
3837
|
type: "button",
|
|
@@ -3230,16 +3847,21 @@ function ImageSlider(props) {
|
|
|
3230
3847
|
setLessonkitBlockType(ImageSlider, "ImageSlider");
|
|
3231
3848
|
|
|
3232
3849
|
// src/blocks/FindHotspot.tsx
|
|
3233
|
-
import { forwardRef as
|
|
3234
|
-
import { jsx as
|
|
3850
|
+
import { forwardRef as forwardRef10, useEffect as useEffect18, useMemo as useMemo16, useState as useState18 } from "react";
|
|
3851
|
+
import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
3235
3852
|
var INTERACTION6 = "findHotspot";
|
|
3236
3853
|
function FindHotspotInner(props, ref) {
|
|
3237
|
-
const checkId =
|
|
3238
|
-
const [selected, setSelected] =
|
|
3239
|
-
const [checked, setChecked] =
|
|
3854
|
+
const checkId = useMemo16(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
3855
|
+
const [selected, setSelected] = useState18(null);
|
|
3856
|
+
const [checked, setChecked] = useState18(false);
|
|
3240
3857
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
3858
|
+
const targetIdsKey = props.targets.map((t) => t.id).join("\0");
|
|
3859
|
+
useEffect18(() => {
|
|
3860
|
+
setSelected(null);
|
|
3861
|
+
setChecked(false);
|
|
3862
|
+
}, [checkId, props.correctTargetId, targetIdsKey]);
|
|
3241
3863
|
const correct = selected === props.correctTargetId;
|
|
3242
|
-
const handle =
|
|
3864
|
+
const handle = useMemo16(
|
|
3243
3865
|
() => buildAssessmentHandle({
|
|
3244
3866
|
checkId,
|
|
3245
3867
|
getScore: () => checked && correct ? 1 : 0,
|
|
@@ -3261,15 +3883,22 @@ function FindHotspotInner(props, ref) {
|
|
|
3261
3883
|
getCurrentState: () => ({ selected, checked }),
|
|
3262
3884
|
resume: (state) => {
|
|
3263
3885
|
const nextSelected = readStringField(state, "selected");
|
|
3264
|
-
if (typeof nextSelected === "string"
|
|
3886
|
+
if (typeof nextSelected === "string" || nextSelected === null) {
|
|
3887
|
+
const valid = nextSelected === null || props.targets.some((t) => t.id === nextSelected);
|
|
3888
|
+
setSelected(valid ? nextSelected : null);
|
|
3889
|
+
}
|
|
3265
3890
|
readBooleanStateField(state, "checked", setChecked);
|
|
3266
3891
|
}
|
|
3267
3892
|
}),
|
|
3268
|
-
[checkId, selected, checked, correct, props.correctTargetId]
|
|
3893
|
+
[checkId, selected, checked, correct, props.correctTargetId, props.targets]
|
|
3269
3894
|
);
|
|
3270
3895
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3896
|
+
const selectTarget = (id) => {
|
|
3897
|
+
setSelected(id);
|
|
3898
|
+
setChecked(false);
|
|
3899
|
+
};
|
|
3271
3900
|
const submit = () => {
|
|
3272
|
-
if (!selected) return;
|
|
3901
|
+
if (!selected || checked) return;
|
|
3273
3902
|
setChecked(true);
|
|
3274
3903
|
assessment.answer({
|
|
3275
3904
|
checkId,
|
|
@@ -3283,14 +3912,14 @@ function FindHotspotInner(props, ref) {
|
|
|
3283
3912
|
interactionType: INTERACTION6,
|
|
3284
3913
|
score: 1,
|
|
3285
3914
|
maxScore: 1,
|
|
3286
|
-
passingScore: props.passingScore
|
|
3915
|
+
passingScore: props.passingScore ?? 1
|
|
3287
3916
|
});
|
|
3288
3917
|
}
|
|
3289
3918
|
};
|
|
3290
|
-
return /* @__PURE__ */
|
|
3291
|
-
/* @__PURE__ */
|
|
3292
|
-
/* @__PURE__ */
|
|
3293
|
-
props.targets.map((t) => /* @__PURE__ */
|
|
3919
|
+
return /* @__PURE__ */ jsxs19("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
|
|
3920
|
+
/* @__PURE__ */ jsxs19("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
3921
|
+
/* @__PURE__ */ jsx26("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
3922
|
+
props.targets.map((t) => /* @__PURE__ */ jsx26(
|
|
3294
3923
|
"button",
|
|
3295
3924
|
{
|
|
3296
3925
|
type: "button",
|
|
@@ -3303,30 +3932,30 @@ function FindHotspotInner(props, ref) {
|
|
|
3303
3932
|
top: `${t.y}%`,
|
|
3304
3933
|
transform: "translate(-50%, -50%)"
|
|
3305
3934
|
},
|
|
3306
|
-
onClick: () =>
|
|
3935
|
+
onClick: () => selectTarget(t.id),
|
|
3307
3936
|
children: t.label
|
|
3308
3937
|
},
|
|
3309
3938
|
t.id
|
|
3310
3939
|
))
|
|
3311
3940
|
] }),
|
|
3312
|
-
/* @__PURE__ */
|
|
3313
|
-
checked ? /* @__PURE__ */
|
|
3941
|
+
/* @__PURE__ */ jsx26("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
|
|
3942
|
+
checked ? /* @__PURE__ */ jsx26("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
3314
3943
|
] });
|
|
3315
3944
|
}
|
|
3316
|
-
var FindHotspotInnerForwarded =
|
|
3317
|
-
var FindHotspot =
|
|
3318
|
-
return /* @__PURE__ */
|
|
3945
|
+
var FindHotspotInnerForwarded = forwardRef10(FindHotspotInner);
|
|
3946
|
+
var FindHotspot = forwardRef10(function FindHotspot2(props, ref) {
|
|
3947
|
+
return /* @__PURE__ */ jsx26(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx26(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
3319
3948
|
});
|
|
3320
3949
|
setLessonkitBlockType(FindHotspot, "FindHotspot");
|
|
3321
3950
|
|
|
3322
3951
|
// src/blocks/FindMultipleHotspots.tsx
|
|
3323
|
-
import { forwardRef as
|
|
3324
|
-
import { jsx as
|
|
3952
|
+
import { forwardRef as forwardRef11, useMemo as useMemo17, useState as useState19 } from "react";
|
|
3953
|
+
import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
3325
3954
|
var INTERACTION7 = "findMultipleHotspots";
|
|
3326
3955
|
function FindMultipleHotspotsInner(props, ref) {
|
|
3327
|
-
const checkId =
|
|
3328
|
-
const [selected, setSelected] =
|
|
3329
|
-
const [checked, setChecked] =
|
|
3956
|
+
const checkId = useMemo17(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
3957
|
+
const [selected, setSelected] = useState19(/* @__PURE__ */ new Set());
|
|
3958
|
+
const [checked, setChecked] = useState19(false);
|
|
3330
3959
|
const assessment = useAssessmentState(props.enclosingLessonId);
|
|
3331
3960
|
const toggle = (id) => {
|
|
3332
3961
|
setSelected((prev) => {
|
|
@@ -3335,9 +3964,10 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
3335
3964
|
else next.add(id);
|
|
3336
3965
|
return next;
|
|
3337
3966
|
});
|
|
3967
|
+
setChecked(false);
|
|
3338
3968
|
};
|
|
3339
3969
|
const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
|
|
3340
|
-
const handle =
|
|
3970
|
+
const handle = useMemo17(
|
|
3341
3971
|
() => buildAssessmentHandle({
|
|
3342
3972
|
checkId,
|
|
3343
3973
|
getScore: () => checked && correct ? 1 : 0,
|
|
@@ -3367,7 +3997,7 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
3367
3997
|
);
|
|
3368
3998
|
useAssessmentHandleRegistration(checkId, handle, ref);
|
|
3369
3999
|
const submit = () => {
|
|
3370
|
-
if (selected.size === 0) return;
|
|
4000
|
+
if (selected.size === 0 || checked) return;
|
|
3371
4001
|
setChecked(true);
|
|
3372
4002
|
assessment.answer({
|
|
3373
4003
|
checkId,
|
|
@@ -3381,14 +4011,14 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
3381
4011
|
interactionType: INTERACTION7,
|
|
3382
4012
|
score: 1,
|
|
3383
4013
|
maxScore: 1,
|
|
3384
|
-
passingScore: props.passingScore
|
|
4014
|
+
passingScore: props.passingScore ?? 1
|
|
3385
4015
|
});
|
|
3386
4016
|
}
|
|
3387
4017
|
};
|
|
3388
|
-
return /* @__PURE__ */
|
|
3389
|
-
/* @__PURE__ */
|
|
3390
|
-
/* @__PURE__ */
|
|
3391
|
-
props.targets.map((t) => /* @__PURE__ */
|
|
4018
|
+
return /* @__PURE__ */ jsxs20("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
|
|
4019
|
+
/* @__PURE__ */ jsxs20("div", { style: { position: "relative", display: "inline-block" }, children: [
|
|
4020
|
+
/* @__PURE__ */ jsx27("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
|
|
4021
|
+
props.targets.map((t) => /* @__PURE__ */ jsx27(
|
|
3392
4022
|
"button",
|
|
3393
4023
|
{
|
|
3394
4024
|
type: "button",
|
|
@@ -3407,14 +4037,14 @@ function FindMultipleHotspotsInner(props, ref) {
|
|
|
3407
4037
|
t.id
|
|
3408
4038
|
))
|
|
3409
4039
|
] }),
|
|
3410
|
-
/* @__PURE__ */
|
|
3411
|
-
checked ? /* @__PURE__ */
|
|
4040
|
+
/* @__PURE__ */ jsx27("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
|
|
4041
|
+
checked ? /* @__PURE__ */ jsx27("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
|
|
3412
4042
|
] });
|
|
3413
4043
|
}
|
|
3414
|
-
var FindMultipleHotspotsInnerForwarded =
|
|
3415
|
-
var FindMultipleHotspots =
|
|
4044
|
+
var FindMultipleHotspotsInnerForwarded = forwardRef11(FindMultipleHotspotsInner);
|
|
4045
|
+
var FindMultipleHotspots = forwardRef11(
|
|
3416
4046
|
function FindMultipleHotspots2(props, ref) {
|
|
3417
|
-
return /* @__PURE__ */
|
|
4047
|
+
return /* @__PURE__ */ jsx27(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx27(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
|
|
3418
4048
|
}
|
|
3419
4049
|
);
|
|
3420
4050
|
setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
|
|
@@ -3431,14 +4061,14 @@ import {
|
|
|
3431
4061
|
} from "@lessonkit/core";
|
|
3432
4062
|
|
|
3433
4063
|
// src/theme/ThemeProvider.tsx
|
|
3434
|
-
import
|
|
3435
|
-
createContext as
|
|
3436
|
-
useCallback as
|
|
3437
|
-
useContext as
|
|
4064
|
+
import React29, {
|
|
4065
|
+
createContext as createContext6,
|
|
4066
|
+
useCallback as useCallback10,
|
|
4067
|
+
useContext as useContext8,
|
|
3438
4068
|
useLayoutEffect as useLayoutEffect2,
|
|
3439
|
-
useMemo as
|
|
3440
|
-
useRef as
|
|
3441
|
-
useState as
|
|
4069
|
+
useMemo as useMemo18,
|
|
4070
|
+
useRef as useRef15,
|
|
4071
|
+
useState as useState20
|
|
3442
4072
|
} from "react";
|
|
3443
4073
|
import {
|
|
3444
4074
|
brandThemeOverrides,
|
|
@@ -3465,11 +4095,11 @@ function applyCssVariables(target, vars, previousKeys) {
|
|
|
3465
4095
|
}
|
|
3466
4096
|
|
|
3467
4097
|
// src/theme/ThemeProvider.tsx
|
|
3468
|
-
import { jsx as
|
|
3469
|
-
var ThemeContext =
|
|
4098
|
+
import { jsx as jsx28 } from "react/jsx-runtime";
|
|
4099
|
+
var ThemeContext = createContext6(null);
|
|
3470
4100
|
var useIsoLayoutEffect2 = (
|
|
3471
4101
|
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
3472
|
-
typeof window !== "undefined" ? useLayoutEffect2 :
|
|
4102
|
+
typeof window !== "undefined" ? useLayoutEffect2 : React29.useEffect
|
|
3473
4103
|
);
|
|
3474
4104
|
function getSystemMode() {
|
|
3475
4105
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
@@ -3488,7 +4118,7 @@ function ThemeProvider(props) {
|
|
|
3488
4118
|
const preset = props.preset ?? "default";
|
|
3489
4119
|
const mode = props.mode ?? "light";
|
|
3490
4120
|
const targetKind = props.target ?? "document";
|
|
3491
|
-
const [resolvedMode, setResolvedMode] =
|
|
4121
|
+
const [resolvedMode, setResolvedMode] = useState20(
|
|
3492
4122
|
() => mode === "system" ? getSystemMode() : mode
|
|
3493
4123
|
);
|
|
3494
4124
|
useIsoLayoutEffect2(() => {
|
|
@@ -3504,20 +4134,20 @@ function ThemeProvider(props) {
|
|
|
3504
4134
|
return () => mq.removeEventListener("change", onChange);
|
|
3505
4135
|
}, [mode]);
|
|
3506
4136
|
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
3507
|
-
const effectiveTheme =
|
|
4137
|
+
const effectiveTheme = useMemo18(() => {
|
|
3508
4138
|
const modeBase = resolveModeBase(mode, dataTheme);
|
|
3509
4139
|
const base = preset === "default" ? modeBase : preset === "brand" ? mergeThemes(modeBase, brandThemeOverrides) : mergeThemes(modeBase, getPresetTheme(preset));
|
|
3510
4140
|
return mergeThemes(base, props.theme ?? {});
|
|
3511
4141
|
}, [preset, mode, dataTheme, props.theme]);
|
|
3512
|
-
const hostRef =
|
|
3513
|
-
const appliedKeysRef =
|
|
4142
|
+
const hostRef = useRef15(null);
|
|
4143
|
+
const appliedKeysRef = useRef15(/* @__PURE__ */ new Set());
|
|
3514
4144
|
useIsoLayoutEffect2(() => {
|
|
3515
4145
|
if (targetKind === "document" && typeof document !== "undefined") {
|
|
3516
4146
|
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
3517
4147
|
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
3518
4148
|
}
|
|
3519
4149
|
}, [targetKind, dataTheme]);
|
|
3520
|
-
const inject =
|
|
4150
|
+
const inject = useCallback10(() => {
|
|
3521
4151
|
const vars = themeToCssVariables(effectiveTheme);
|
|
3522
4152
|
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
3523
4153
|
if (!el) return;
|
|
@@ -3534,7 +4164,7 @@ function ThemeProvider(props) {
|
|
|
3534
4164
|
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
3535
4165
|
};
|
|
3536
4166
|
}, [inject, targetKind]);
|
|
3537
|
-
const value =
|
|
4167
|
+
const value = useMemo18(
|
|
3538
4168
|
() => ({
|
|
3539
4169
|
theme: effectiveTheme,
|
|
3540
4170
|
preset,
|
|
@@ -3544,12 +4174,12 @@ function ThemeProvider(props) {
|
|
|
3544
4174
|
[effectiveTheme, preset, mode, dataTheme]
|
|
3545
4175
|
);
|
|
3546
4176
|
if (targetKind === "document") {
|
|
3547
|
-
return /* @__PURE__ */
|
|
4177
|
+
return /* @__PURE__ */ jsx28(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx28("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
|
|
3548
4178
|
}
|
|
3549
|
-
return /* @__PURE__ */
|
|
4179
|
+
return /* @__PURE__ */ jsx28(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx28("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
3550
4180
|
}
|
|
3551
4181
|
function useTheme() {
|
|
3552
|
-
const ctx =
|
|
4182
|
+
const ctx = useContext8(ThemeContext);
|
|
3553
4183
|
if (!ctx) {
|
|
3554
4184
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
3555
4185
|
}
|
|
@@ -3561,9 +4191,18 @@ import {
|
|
|
3561
4191
|
ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
3562
4192
|
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
3563
4193
|
PAGE_ALLOWED_CHILD_TYPES,
|
|
4194
|
+
SLIDE_ALLOWED_CHILD_TYPES,
|
|
4195
|
+
SLIDE_DECK_ALLOWED_CHILD_TYPES,
|
|
3564
4196
|
COMPOUND_MAX_NESTING_DEPTH as COMPOUND_MAX_NESTING_DEPTH2
|
|
3565
4197
|
} from "@lessonkit/core";
|
|
3566
|
-
var COMPOUND_PARENTS = [
|
|
4198
|
+
var COMPOUND_PARENTS = [
|
|
4199
|
+
"Lesson",
|
|
4200
|
+
"Page",
|
|
4201
|
+
"InteractiveBook",
|
|
4202
|
+
"Slide",
|
|
4203
|
+
"SlideDeck",
|
|
4204
|
+
"AssessmentSequence"
|
|
4205
|
+
];
|
|
3567
4206
|
function extendParents(entry) {
|
|
3568
4207
|
if (!entry.parentConstraints?.length) return entry;
|
|
3569
4208
|
const merged = /* @__PURE__ */ new Set([...entry.parentConstraints, ...COMPOUND_PARENTS]);
|
|
@@ -3667,6 +4306,53 @@ var v3CompoundAndContentEntries = [
|
|
|
3667
4306
|
theming: { surface: "global-inherit", stylingNotes: "Book chrome." },
|
|
3668
4307
|
telemetry: { emits: ["book_page_viewed"], requiresActiveLesson: true }
|
|
3669
4308
|
},
|
|
4309
|
+
{
|
|
4310
|
+
type: "Slide",
|
|
4311
|
+
category: "container",
|
|
4312
|
+
compoundContract: true,
|
|
4313
|
+
h5pMachineName: "H5P.CoursePresentation",
|
|
4314
|
+
h5pAlias: "Course Presentation slide",
|
|
4315
|
+
description: "Single slide row in a SlideDeck. Planned allowlist expansion: Video, Summary.",
|
|
4316
|
+
allowedChildTypes: [...SLIDE_ALLOWED_CHILD_TYPES],
|
|
4317
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.Slide,
|
|
4318
|
+
props: [
|
|
4319
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4320
|
+
{ name: "title", type: "string", required: false, description: "Slide title." },
|
|
4321
|
+
{ name: "children", type: "ReactNode", required: true, description: "Slide content." }
|
|
4322
|
+
],
|
|
4323
|
+
requiredIds: [],
|
|
4324
|
+
optionalIds: ["blockId"],
|
|
4325
|
+
parentConstraints: ["SlideDeck"],
|
|
4326
|
+
a11y: { element: "section", ariaLabel: "Slide", keyboard: "N/A", notes: "H5P Course Presentation slide row." },
|
|
4327
|
+
theming: { surface: "global-inherit", stylingNotes: "Container." },
|
|
4328
|
+
telemetry: { emits: ["compound_page_viewed"], requiresActiveLesson: true }
|
|
4329
|
+
},
|
|
4330
|
+
{
|
|
4331
|
+
type: "SlideDeck",
|
|
4332
|
+
category: "container",
|
|
4333
|
+
compoundContract: true,
|
|
4334
|
+
h5pMachineName: "H5P.CoursePresentation",
|
|
4335
|
+
h5pAlias: "Course Presentation",
|
|
4336
|
+
description: "Multi-slide presentation with keyboard navigation.",
|
|
4337
|
+
allowedChildTypes: [...SLIDE_DECK_ALLOWED_CHILD_TYPES],
|
|
4338
|
+
maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.SlideDeck,
|
|
4339
|
+
props: [
|
|
4340
|
+
{ name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
|
|
4341
|
+
{ name: "title", type: "string", required: true, description: "Deck title." },
|
|
4342
|
+
{ name: "showDeckScore", type: "boolean", required: false, description: "Show aggregate score." },
|
|
4343
|
+
{ name: "children", type: "Slide[]", required: true, description: "Slides." }
|
|
4344
|
+
],
|
|
4345
|
+
requiredIds: ["blockId"],
|
|
4346
|
+
parentConstraints: ["Lesson"],
|
|
4347
|
+
a11y: {
|
|
4348
|
+
element: "section",
|
|
4349
|
+
ariaLabel: "Slide deck",
|
|
4350
|
+
keyboard: "Arrow keys, Home, End, Previous/Next slide buttons.",
|
|
4351
|
+
notes: "H5P Course Presentation equivalent."
|
|
4352
|
+
},
|
|
4353
|
+
theming: { surface: "global-inherit", stylingNotes: "Deck chrome." },
|
|
4354
|
+
telemetry: { emits: ["slide_viewed"], requiresActiveLesson: true }
|
|
4355
|
+
},
|
|
3670
4356
|
{
|
|
3671
4357
|
type: "Accordion",
|
|
3672
4358
|
category: "content",
|
|
@@ -4212,6 +4898,8 @@ export {
|
|
|
4212
4898
|
Quiz,
|
|
4213
4899
|
Reflection,
|
|
4214
4900
|
Scenario,
|
|
4901
|
+
Slide,
|
|
4902
|
+
SlideDeck,
|
|
4215
4903
|
Text,
|
|
4216
4904
|
ThemeProvider,
|
|
4217
4905
|
TrueFalse,
|