@hydralms/components 0.1.3 → 0.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/dist/StudentProfile-BVfZMbnV.cjs +1 -0
- package/dist/StudentProfile-DeMxdrL3.js +3275 -0
- package/dist/assessment-toolbar/assessment-toolbar.d.ts +1 -1
- package/dist/assessment-toolbar/index.d.ts +5 -1
- package/dist/assessment-toolbar/question-header-bar.d.ts +2 -0
- package/dist/assessment-toolbar/question-materials-drawer.d.ts +2 -0
- package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
- package/dist/assessment-toolbar/timer-display.d.ts +1 -1
- package/dist/assessment-toolbar/types.d.ts +52 -4
- package/dist/assessment-toolbar/use-countdown.d.ts +43 -0
- package/dist/common/index.d.ts +3 -1
- package/dist/common/pagination.d.ts +26 -0
- package/dist/common/stepper.d.ts +6 -0
- package/dist/common/types.d.ts +38 -0
- package/dist/components.css +1 -1
- package/dist/content/attachment-list.d.ts +6 -0
- package/dist/content/audio-player.d.ts +22 -0
- package/dist/content/code-block.d.ts +30 -0
- package/dist/content/content-block.d.ts +1 -1
- package/dist/content/embed-block.d.ts +28 -0
- package/dist/content/index.d.ts +8 -1
- package/dist/content/types.d.ts +63 -0
- package/dist/curriculum/course-card.d.ts +51 -0
- package/dist/curriculum/curriculum-item.d.ts +1 -1
- package/dist/curriculum/index.d.ts +2 -0
- package/dist/curriculum/types.d.ts +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +597 -308
- package/dist/license/HydraContext.d.ts +16 -0
- package/dist/license/ProBadge.d.ts +6 -0
- package/dist/license/index.d.ts +7 -0
- package/dist/license/tiers.d.ts +3 -0
- package/dist/license/useHydraLicense.d.ts +6 -0
- package/dist/license/validate.d.ts +13 -0
- package/dist/license/withProGate.d.ts +6 -0
- package/dist/modules/AssignmentModule/AssignmentModule.d.ts +5 -0
- package/dist/modules/AssignmentModule/types.d.ts +69 -0
- package/dist/modules/CertificateModule/CertificateModule.d.ts +5 -0
- package/dist/modules/CertificateModule/types.d.ts +51 -0
- package/dist/modules/CourseCatalogModule/CourseCatalogModule.d.ts +5 -0
- package/dist/modules/CourseCatalogModule/types.d.ts +43 -0
- package/dist/modules/CoursePlayer/CoursePlayer.d.ts +4 -1
- package/dist/modules/DiscussionModule/DiscussionModule.d.ts +5 -0
- package/dist/modules/DiscussionModule/types.d.ts +47 -0
- package/dist/modules/ExamModule/ExamModule.d.ts +5 -0
- package/dist/modules/ExamModule/types.d.ts +55 -0
- package/dist/modules/FlashcardLab/FlashcardLab.d.ts +4 -1
- package/dist/modules/FlashcardLab/types.d.ts +2 -0
- package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +5 -0
- package/dist/modules/GradeCenterModule/types.d.ts +56 -0
- package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
- package/dist/modules/QuizModule/types.d.ts +10 -14
- package/dist/modules/StudentDashboardModule/StudentDashboardModule.d.ts +5 -0
- package/dist/modules/StudentDashboardModule/types.d.ts +54 -0
- package/dist/modules/StudentProfileModule/StudentProfileModule.d.ts +5 -0
- package/dist/modules/StudentProfileModule/types.d.ts +43 -0
- package/dist/modules/SurveyModule/SurveyModule.d.ts +5 -0
- package/dist/modules/SurveyModule/types.d.ts +51 -0
- package/dist/modules/_shared/assessment-intro.d.ts +16 -0
- package/dist/modules/_shared/assessment-results.d.ts +23 -0
- package/dist/modules/_shared/types.d.ts +10 -0
- package/dist/modules/_shared/use-timer.d.ts +9 -0
- package/dist/modules/index.d.ts +18 -0
- package/dist/modules.cjs +1 -0
- package/dist/modules.js +1834 -0
- package/dist/progress/achievement-badge.d.ts +6 -0
- package/dist/progress/activity-timeline.d.ts +6 -0
- package/dist/progress/index.d.ts +4 -1
- package/dist/progress/stat-card.d.ts +1 -1
- package/dist/progress/streak-badge.d.ts +6 -0
- package/dist/progress/types.d.ts +99 -0
- package/dist/provider/HydraProvider.d.ts +5 -1
- package/dist/questions/choice.d.ts +1 -1
- package/dist/questions/confidence-indicator.d.ts +37 -0
- package/dist/questions/essay.d.ts +2 -2
- package/dist/questions/fill-in-the-blank.d.ts +1 -1
- package/dist/questions/hotspot.d.ts +21 -0
- package/dist/questions/index.d.ts +11 -1
- package/dist/questions/inline-choice.d.ts +21 -0
- package/dist/questions/matching.d.ts +22 -0
- package/dist/questions/multiple-choice.d.ts +1 -1
- package/dist/questions/numeric.d.ts +11 -0
- package/dist/questions/ordering.d.ts +12 -0
- package/dist/questions/question-renderer.d.ts +1 -1
- package/dist/questions/scenario.d.ts +23 -0
- package/dist/questions/scoring.d.ts +22 -0
- package/dist/questions/spreadsheet.d.ts +29 -0
- package/dist/questions/true-false.d.ts +1 -1
- package/dist/questions/types.d.ts +106 -1
- package/dist/questions/use-drag-reorder.d.ts +17 -0
- package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
- package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
- package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
- package/dist/sections/AssessmentReview/types.d.ts +6 -0
- package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
- package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
- package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
- package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
- package/dist/sections/CertificateViewer/types.d.ts +13 -5
- package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
- package/dist/sections/CourseCatalog/types.d.ts +80 -0
- package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
- package/dist/sections/CourseOutline/types.d.ts +6 -0
- package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
- package/dist/sections/DiscussionThread/types.d.ts +6 -0
- package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
- package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
- package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
- package/dist/sections/ExamSession/types.d.ts +12 -1
- package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
- package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
- package/dist/sections/ForumBoard/ForumBoard.d.ts +8 -0
- package/dist/sections/ForumBoard/types.d.ts +78 -0
- package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
- package/dist/sections/GradebookTable/types.d.ts +14 -0
- package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
- package/dist/sections/LecturePlayer/types.d.ts +8 -0
- package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
- package/dist/sections/LessonPage/types.d.ts +6 -0
- package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
- package/dist/sections/PracticeQuiz/types.d.ts +6 -0
- package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
- package/dist/sections/ProgressDashboard/types.d.ts +6 -0
- package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
- package/dist/sections/QuizSession/types.d.ts +12 -1
- package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +8 -0
- package/dist/sections/RequirementsChecklist/types.d.ts +43 -0
- package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
- package/dist/sections/ResourceLibrary/types.d.ts +15 -1
- package/dist/sections/RubricView/RubricView.d.ts +9 -0
- package/dist/sections/RubricView/types.d.ts +56 -0
- package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
- package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
- package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
- package/dist/sections/StudentProfile/types.d.ts +98 -0
- package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
- package/dist/sections/SurveyForm/types.d.ts +6 -0
- package/dist/sections/_shared/merge-answers.d.ts +9 -0
- package/dist/sections/_shared/section-shell.d.ts +20 -0
- package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
- package/dist/sections/index.d.ts +13 -1
- package/dist/sections.cjs +1 -1
- package/dist/sections.js +282 -1786
- package/dist/social/post-card.d.ts +1 -1
- package/dist/tabs-BsfVo2Bl.cjs +173 -0
- package/dist/tabs-BuY1iNJE.js +22305 -0
- package/dist/ui/alert.d.ts +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/button.d.ts +1 -1
- package/dist/ui/drawer.d.ts +84 -0
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/progress.d.ts +1 -1
- package/dist/ui/rich-text-editor.d.ts +32 -0
- package/dist/ui/rich-text-toolbar.d.ts +8 -0
- package/dist/ui/toast.d.ts +43 -0
- package/dist/utils/array-utils.d.ts +4 -0
- package/dist/utils/debounce.d.ts +5 -1
- package/dist/utils/flatten-leaves.d.ts +6 -0
- package/dist/utils/format-file-size.d.ts +1 -0
- package/dist/utils/format-timestamp.d.ts +1 -0
- package/dist/utils/is-empty-html.d.ts +5 -0
- package/dist/utils/pick-palette-color.d.ts +19 -0
- package/dist/utils/shuffle.d.ts +1 -0
- package/dist/utils/string-utils.d.ts +12 -0
- package/dist/video/types.d.ts +15 -0
- package/dist/video/video-bookmark.d.ts +1 -1
- package/dist/video/video-player.d.ts +1 -1
- package/dist/video/video-playlist-item.d.ts +1 -1
- package/dist/withProGate-BWqcKdPM.js +137 -0
- package/dist/withProGate-DX6XqKLp.cjs +1 -0
- package/package.json +40 -137
- package/src/assessment-toolbar/assessment-toolbar.tsx +54 -49
- package/src/assessment-toolbar/index.ts +6 -0
- package/src/assessment-toolbar/question-header-bar.tsx +61 -0
- package/src/assessment-toolbar/question-materials-drawer.tsx +55 -0
- package/src/assessment-toolbar/question-navigator.tsx +13 -36
- package/src/assessment-toolbar/timer-display.tsx +6 -5
- package/src/assessment-toolbar/types.ts +54 -4
- package/src/assessment-toolbar/use-countdown.ts +153 -0
- package/src/common/empty-state.tsx +1 -0
- package/src/common/index.ts +5 -0
- package/src/common/pagination.tsx +135 -0
- package/src/common/search-input.tsx +8 -6
- package/src/common/stepper.tsx +100 -0
- package/src/common/types.ts +41 -0
- package/src/content/attachment-list.tsx +92 -0
- package/src/content/audio-player.tsx +196 -0
- package/src/content/code-block.tsx +113 -0
- package/src/content/content-block.tsx +68 -2
- package/src/content/embed-block.tsx +78 -0
- package/src/content/file-upload-zone.tsx +11 -6
- package/src/content/index.ts +9 -0
- package/src/content/types.ts +46 -0
- package/src/curriculum/course-card.tsx +199 -0
- package/src/curriculum/curriculum-item.tsx +9 -5
- package/src/curriculum/curriculum-tree.tsx +20 -13
- package/src/curriculum/index.ts +2 -0
- package/src/curriculum/types.ts +2 -2
- package/src/feedback/feedback-banner.tsx +12 -14
- package/src/flashcards/flashcard-deck.tsx +1 -9
- package/src/flashcards/flashcard.tsx +29 -9
- package/src/index.ts +3 -0
- package/src/license/HydraContext.tsx +62 -0
- package/src/license/ProBadge.tsx +43 -0
- package/src/license/index.ts +7 -0
- package/src/license/tiers.ts +24 -0
- package/src/license/useHydraLicense.ts +10 -0
- package/src/license/validate.ts +90 -0
- package/src/license/withProGate.tsx +21 -0
- package/src/modules/AssignmentModule/AssignmentModule.tsx +314 -0
- package/src/modules/AssignmentModule/types.ts +77 -0
- package/src/modules/CertificateModule/CertificateModule.tsx +173 -0
- package/src/modules/CertificateModule/types.ts +49 -0
- package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
- package/src/modules/CourseCatalogModule/types.ts +47 -0
- package/src/modules/CoursePlayer/CoursePlayer.tsx +80 -69
- package/src/modules/DiscussionModule/DiscussionModule.tsx +145 -0
- package/src/modules/DiscussionModule/types.ts +54 -0
- package/src/modules/ExamModule/ExamModule.tsx +151 -0
- package/src/modules/ExamModule/types.ts +57 -0
- package/src/modules/FlashcardLab/FlashcardLab.tsx +39 -21
- package/src/modules/FlashcardLab/types.ts +2 -0
- package/src/modules/GradeCenterModule/GradeCenterModule.tsx +174 -0
- package/src/modules/GradeCenterModule/types.ts +65 -0
- package/src/modules/QuizModule/QuizModule.tsx +58 -178
- package/src/modules/QuizModule/types.ts +10 -15
- package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
- package/src/modules/StudentDashboardModule/types.ts +56 -0
- package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
- package/src/modules/StudentProfileModule/types.ts +45 -0
- package/src/modules/SurveyModule/SurveyModule.tsx +185 -0
- package/src/modules/SurveyModule/types.ts +53 -0
- package/src/modules/_shared/assessment-intro.tsx +75 -0
- package/src/modules/_shared/assessment-results.tsx +133 -0
- package/src/modules/_shared/types.ts +11 -0
- package/src/modules/_shared/use-timer.ts +49 -0
- package/src/modules/index.ts +33 -0
- package/src/progress/achievement-badge.tsx +52 -0
- package/src/progress/activity-timeline.tsx +84 -0
- package/src/progress/grade-indicator.tsx +9 -1
- package/src/progress/index.ts +7 -0
- package/src/progress/progress-ring.tsx +2 -1
- package/src/progress/stat-card.tsx +37 -18
- package/src/progress/streak-badge.tsx +35 -0
- package/src/progress/types.ts +103 -0
- package/src/provider/HydraProvider.tsx +15 -6
- package/src/questions/choice.tsx +19 -14
- package/src/questions/confidence-indicator.tsx +107 -0
- package/src/questions/essay.tsx +28 -28
- package/src/questions/fill-in-the-blank.tsx +20 -19
- package/src/questions/hotspot.tsx +154 -0
- package/src/questions/index.ts +18 -0
- package/src/questions/inline-choice.tsx +152 -0
- package/src/questions/matching.tsx +229 -0
- package/src/questions/multiple-choice.tsx +19 -14
- package/src/questions/numeric.tsx +106 -0
- package/src/questions/ordering.tsx +167 -0
- package/src/questions/question-renderer.tsx +24 -2
- package/src/questions/scenario.tsx +140 -0
- package/src/questions/scoring.ts +201 -0
- package/src/questions/spreadsheet.tsx +260 -0
- package/src/questions/true-false.tsx +19 -14
- package/src/questions/types.ts +123 -1
- package/src/questions/use-drag-reorder.ts +80 -0
- package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +66 -23
- package/src/sections/AnnouncementFeed/types.ts +15 -1
- package/src/sections/AssessmentReview/AssessmentReview.tsx +50 -2
- package/src/sections/AssessmentReview/types.ts +6 -0
- package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +44 -6
- package/src/sections/AssignmentSubmission/types.ts +6 -0
- package/src/sections/CertificateViewer/CertificateViewer.tsx +215 -60
- package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
- package/src/sections/CertificateViewer/types.ts +19 -5
- package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
- package/src/sections/CourseCatalog/types.ts +76 -0
- package/src/sections/CourseOutline/CourseOutline.tsx +45 -14
- package/src/sections/CourseOutline/types.ts +6 -0
- package/src/sections/DiscussionThread/DiscussionThread.tsx +55 -11
- package/src/sections/DiscussionThread/types.ts +6 -0
- package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
- package/src/sections/EnrollmentWizard/types.ts +65 -0
- package/src/sections/ExamSession/ExamSession.tsx +125 -82
- package/src/sections/ExamSession/types.ts +12 -1
- package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
- package/src/sections/FlashcardStudySession/types.ts +6 -0
- package/src/sections/ForumBoard/ForumBoard.tsx +342 -0
- package/src/sections/ForumBoard/types.ts +81 -0
- package/src/sections/GradebookTable/GradebookTable.tsx +55 -2
- package/src/sections/GradebookTable/types.ts +14 -0
- package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
- package/src/sections/LecturePlayer/types.ts +8 -0
- package/src/sections/LessonPage/LessonPage.tsx +40 -13
- package/src/sections/LessonPage/types.ts +6 -0
- package/src/sections/PracticeQuiz/PracticeQuiz.tsx +119 -98
- package/src/sections/PracticeQuiz/types.ts +6 -0
- package/src/sections/ProgressDashboard/ProgressDashboard.tsx +121 -67
- package/src/sections/ProgressDashboard/types.ts +6 -0
- package/src/sections/QuizSession/QuizSession.tsx +115 -67
- package/src/sections/QuizSession/types.ts +12 -1
- package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +147 -0
- package/src/sections/RequirementsChecklist/types.ts +44 -0
- package/src/sections/ResourceLibrary/ResourceLibrary.tsx +68 -17
- package/src/sections/ResourceLibrary/types.ts +15 -1
- package/src/sections/RubricView/RubricView.tsx +174 -0
- package/src/sections/RubricView/types.ts +58 -0
- package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +58 -23
- package/src/sections/ScrollableQuiz/types.ts +6 -0
- package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
- package/src/sections/StudentProfile/types.ts +99 -0
- package/src/sections/SurveyForm/SurveyForm.tsx +40 -10
- package/src/sections/SurveyForm/types.ts +6 -0
- package/src/sections/_shared/merge-answers.ts +22 -0
- package/src/sections/_shared/section-shell.tsx +64 -0
- package/src/sections/_shared/use-assessment-session.ts +125 -0
- package/src/sections/index.ts +42 -1
- package/src/social/post-card.tsx +8 -19
- package/src/social/user-avatar.tsx +10 -5
- package/src/styles/globals.css +52 -41
- package/src/ui/badge.tsx +8 -0
- package/src/ui/drawer.tsx +600 -0
- package/src/ui/index.ts +21 -0
- package/src/ui/progress.tsx +4 -0
- package/src/ui/rich-text-editor.tsx +119 -0
- package/src/ui/rich-text-toolbar.tsx +157 -0
- package/src/ui/toast.tsx +170 -0
- package/src/utils/array-utils.ts +17 -0
- package/src/utils/debounce.ts +8 -2
- package/src/utils/flatten-leaves.ts +17 -0
- package/src/utils/format-file-size.ts +5 -0
- package/src/utils/format-timestamp.ts +13 -0
- package/src/utils/is-empty-html.ts +7 -0
- package/src/utils/pick-palette-color.ts +33 -0
- package/src/utils/shuffle.ts +8 -0
- package/src/utils/string-utils.ts +30 -0
- package/src/video/types.ts +16 -0
- package/src/video/video-bookmark.tsx +4 -3
- package/src/video/video-chapter-list.tsx +9 -4
- package/src/video/video-player.tsx +24 -5
- package/src/video/video-playlist-item.tsx +8 -3
- package/src/video/video-thumbnail-card.tsx +4 -0
- package/src/video/video-transcript.tsx +8 -5
- package/dist/table-BrS5cDQu.js +0 -2510
- package/dist/table-D6AkBBEo.cjs +0 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { useState, useMemo, useRef, useEffect, memo } from "react";
|
|
2
|
+
import { debounce } from "../utils/debounce";
|
|
3
|
+
import { Input } from "../ui/input";
|
|
4
|
+
import {
|
|
5
|
+
Table,
|
|
6
|
+
TableHeader,
|
|
7
|
+
TableBody,
|
|
8
|
+
TableFooter,
|
|
9
|
+
TableRow,
|
|
10
|
+
TableHead,
|
|
11
|
+
TableCell,
|
|
12
|
+
} from "../ui/table";
|
|
13
|
+
import { Alert, AlertDescription } from "../ui/alert";
|
|
14
|
+
import { cn } from "../lib/utils";
|
|
15
|
+
import type {
|
|
16
|
+
QuestionProps,
|
|
17
|
+
SpreadsheetColumn,
|
|
18
|
+
SpreadsheetCell as SpreadsheetCellType,
|
|
19
|
+
} from "./types";
|
|
20
|
+
|
|
21
|
+
function getCellStatus(
|
|
22
|
+
cell: SpreadsheetCellType,
|
|
23
|
+
col: SpreadsheetColumn,
|
|
24
|
+
userValue: string,
|
|
25
|
+
): "correct" | "incorrect" | "empty" | null {
|
|
26
|
+
if (cell.locked || !cell.correctAnswer) return null;
|
|
27
|
+
if (!userValue || userValue.trim() === "") return "empty";
|
|
28
|
+
|
|
29
|
+
if (col.type === "numeric" || col.type === "currency") {
|
|
30
|
+
const user = parseFloat(userValue.replace(/,/g, ""));
|
|
31
|
+
const correct = parseFloat(cell.correctAnswer);
|
|
32
|
+
if (isNaN(user) || isNaN(correct)) return "incorrect";
|
|
33
|
+
const tol = cell.tolerance ?? 0;
|
|
34
|
+
return Math.abs(user - correct) <= tol ? "correct" : "incorrect";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return userValue.trim().toLowerCase() === cell.correctAnswer.trim().toLowerCase()
|
|
38
|
+
? "correct"
|
|
39
|
+
: "incorrect";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Spreadsheet renders a grid-based question for accounting and tabular data entry.
|
|
44
|
+
* Students fill in editable cells while locked cells display pre-filled values.
|
|
45
|
+
* Each cell is graded independently with support for numeric tolerance.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* <Spreadsheet
|
|
49
|
+
* question={{
|
|
50
|
+
* uid: "q1",
|
|
51
|
+
* type: "spreadsheet",
|
|
52
|
+
* content: "Complete the trial balance:",
|
|
53
|
+
* spreadsheetColumns: [
|
|
54
|
+
* { uid: "col-acct", label: "Account", type: "text", width: 14 },
|
|
55
|
+
* { uid: "col-dr", label: "Debit ($)", type: "currency", width: 9 },
|
|
56
|
+
* { uid: "col-cr", label: "Credit ($)", type: "currency", width: 9 },
|
|
57
|
+
* ],
|
|
58
|
+
* spreadsheetRows: [
|
|
59
|
+
* { uid: "r1", cells: [
|
|
60
|
+
* { uid: "r1-acct", columnUid: "col-acct", locked: true, value: "Cash" },
|
|
61
|
+
* { uid: "r1-dr", columnUid: "col-dr", locked: false, correctAnswer: "5000" },
|
|
62
|
+
* { uid: "r1-cr", columnUid: "col-cr", locked: true, value: "" },
|
|
63
|
+
* ]},
|
|
64
|
+
* ],
|
|
65
|
+
* }}
|
|
66
|
+
* onAnswer={(answers) => handleAnswer(answers)}
|
|
67
|
+
* />
|
|
68
|
+
*/
|
|
69
|
+
export const Spreadsheet = memo(function Spreadsheet({
|
|
70
|
+
question,
|
|
71
|
+
sessionAnswers,
|
|
72
|
+
onAnswer,
|
|
73
|
+
readOnly = false,
|
|
74
|
+
showCorrectAnswers = false,
|
|
75
|
+
disabled = false,
|
|
76
|
+
}: QuestionProps) {
|
|
77
|
+
const columns = question.spreadsheetColumns ?? [];
|
|
78
|
+
const rows = question.spreadsheetRows ?? [];
|
|
79
|
+
|
|
80
|
+
const colMap = useMemo(() => {
|
|
81
|
+
const m = new Map<string, SpreadsheetColumn>();
|
|
82
|
+
for (const col of columns) m.set(col.uid, col);
|
|
83
|
+
return m;
|
|
84
|
+
}, [columns]);
|
|
85
|
+
|
|
86
|
+
const editableCellUids = useMemo(
|
|
87
|
+
() => rows.flatMap((r) => r.cells.filter((c) => !c.locked).map((c) => c.uid)),
|
|
88
|
+
[rows],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const hasRowLabels = useMemo(() => rows.some((r) => r.label != null), [rows]);
|
|
92
|
+
const bodyRows = useMemo(() => rows.filter((r) => !r.isTotals), [rows]);
|
|
93
|
+
const totalsRows = useMemo(() => rows.filter((r) => r.isTotals), [rows]);
|
|
94
|
+
|
|
95
|
+
const onAnswerRef = useRef(onAnswer);
|
|
96
|
+
onAnswerRef.current = onAnswer;
|
|
97
|
+
|
|
98
|
+
const debouncedEmit = useMemo(
|
|
99
|
+
() =>
|
|
100
|
+
debounce((next: Map<string, string>) => {
|
|
101
|
+
onAnswerRef.current?.(
|
|
102
|
+
[...next.entries()].map(([uid, content]) => ({ uid, content })),
|
|
103
|
+
);
|
|
104
|
+
}, 300),
|
|
105
|
+
[],
|
|
106
|
+
);
|
|
107
|
+
useEffect(() => () => debouncedEmit.cancel(), [debouncedEmit]);
|
|
108
|
+
|
|
109
|
+
const [values, setValues] = useState<Map<string, string>>(() => {
|
|
110
|
+
const map = new Map<string, string>();
|
|
111
|
+
for (const sa of sessionAnswers ?? []) {
|
|
112
|
+
if (sa.content !== undefined) map.set(sa.answerUid, sa.content);
|
|
113
|
+
}
|
|
114
|
+
for (const row of rows) {
|
|
115
|
+
for (const cell of row.cells) {
|
|
116
|
+
if (!cell.locked && !map.has(cell.uid)) {
|
|
117
|
+
map.set(cell.uid, cell.value ?? "");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return map;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const handleCellChange = (cellUid: string, raw: string) => {
|
|
125
|
+
if (readOnly || disabled) return;
|
|
126
|
+
const next = new Map(values);
|
|
127
|
+
next.set(cellUid, raw);
|
|
128
|
+
setValues(next);
|
|
129
|
+
debouncedEmit(next);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handleKeyDown = (cellUid: string, e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
133
|
+
if (e.key !== "Tab") return;
|
|
134
|
+
const idx = editableCellUids.indexOf(cellUid);
|
|
135
|
+
const nextIdx = e.shiftKey ? idx - 1 : idx + 1;
|
|
136
|
+
if (nextIdx < 0 || nextIdx >= editableCellUids.length) return;
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
const nextUid = editableCellUids[nextIdx];
|
|
139
|
+
const nextInput = document.querySelector<HTMLInputElement>(
|
|
140
|
+
`[data-cell-uid="${nextUid}"]`,
|
|
141
|
+
);
|
|
142
|
+
nextInput?.focus();
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const renderCell = (cell: SpreadsheetCellType) => {
|
|
146
|
+
const col = colMap.get(cell.columnUid);
|
|
147
|
+
if (!col) return null;
|
|
148
|
+
|
|
149
|
+
const userValue = values.get(cell.uid) ?? "";
|
|
150
|
+
const status = showCorrectAnswers ? getCellStatus(cell, col, userValue) : null;
|
|
151
|
+
|
|
152
|
+
const statusClasses =
|
|
153
|
+
status === "correct"
|
|
154
|
+
? "bg-success/10 border-l-2 border-l-success/50"
|
|
155
|
+
: status === "incorrect" || status === "empty"
|
|
156
|
+
? "bg-destructive/10 border-l-2 border-l-destructive/50"
|
|
157
|
+
: "";
|
|
158
|
+
|
|
159
|
+
if (cell.locked) {
|
|
160
|
+
return (
|
|
161
|
+
<TableCell
|
|
162
|
+
key={cell.uid}
|
|
163
|
+
className={cn(
|
|
164
|
+
"px-2 py-1.5 text-sm text-muted-foreground bg-muted/30",
|
|
165
|
+
col.type !== "text" && "text-right tabular-nums",
|
|
166
|
+
)}
|
|
167
|
+
>
|
|
168
|
+
{cell.value ?? ""}
|
|
169
|
+
</TableCell>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<TableCell
|
|
175
|
+
key={cell.uid}
|
|
176
|
+
className={cn("p-0 relative", statusClasses)}
|
|
177
|
+
>
|
|
178
|
+
<Input
|
|
179
|
+
type={col.type === "numeric" ? "number" : "text"}
|
|
180
|
+
inputMode={col.type === "currency" ? "decimal" : undefined}
|
|
181
|
+
step={col.type === "numeric" ? "any" : undefined}
|
|
182
|
+
value={userValue}
|
|
183
|
+
onChange={(e) => handleCellChange(cell.uid, e.target.value)}
|
|
184
|
+
onKeyDown={(e) => handleKeyDown(cell.uid, e)}
|
|
185
|
+
disabled={readOnly || disabled}
|
|
186
|
+
data-cell-uid={cell.uid}
|
|
187
|
+
placeholder={col.type === "text" ? "" : "0.00"}
|
|
188
|
+
className={cn(
|
|
189
|
+
"h-8 rounded-none border-0 shadow-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring",
|
|
190
|
+
col.type !== "text" && "text-right tabular-nums",
|
|
191
|
+
)}
|
|
192
|
+
/>
|
|
193
|
+
{showCorrectAnswers && status !== "correct" && status !== null && cell.correctAnswer && (
|
|
194
|
+
<div className="px-2 pb-1 text-xs">
|
|
195
|
+
<span className="text-success">{cell.correctAnswer}</span>
|
|
196
|
+
{cell.formula && (
|
|
197
|
+
<span className="ml-1 text-muted-foreground/70">({cell.formula})</span>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
</TableCell>
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const renderRow = (row: (typeof rows)[number]) => (
|
|
206
|
+
<TableRow
|
|
207
|
+
key={row.uid}
|
|
208
|
+
className={cn(
|
|
209
|
+
row.isHeader && "bg-muted/50 font-semibold hover:bg-muted/50",
|
|
210
|
+
)}
|
|
211
|
+
>
|
|
212
|
+
{hasRowLabels && (
|
|
213
|
+
<TableCell className="px-2 py-1.5 font-medium text-sm text-muted-foreground whitespace-nowrap">
|
|
214
|
+
{row.label ?? ""}
|
|
215
|
+
</TableCell>
|
|
216
|
+
)}
|
|
217
|
+
{row.cells.map(renderCell)}
|
|
218
|
+
</TableRow>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div className="flex flex-col gap-3">
|
|
223
|
+
<div dangerouslySetInnerHTML={{ __html: question.content }} />
|
|
224
|
+
|
|
225
|
+
<div className="rounded-md border border-border overflow-auto">
|
|
226
|
+
<Table>
|
|
227
|
+
<TableHeader>
|
|
228
|
+
<TableRow className="hover:bg-transparent">
|
|
229
|
+
{hasRowLabels && <TableHead className="w-auto" />}
|
|
230
|
+
{columns.map((col) => (
|
|
231
|
+
<TableHead
|
|
232
|
+
key={col.uid}
|
|
233
|
+
style={{ width: col.width ? `${col.width}rem` : undefined }}
|
|
234
|
+
className={cn(
|
|
235
|
+
col.type !== "text" && "text-right",
|
|
236
|
+
)}
|
|
237
|
+
>
|
|
238
|
+
{col.label}
|
|
239
|
+
</TableHead>
|
|
240
|
+
))}
|
|
241
|
+
</TableRow>
|
|
242
|
+
</TableHeader>
|
|
243
|
+
<TableBody>{bodyRows.map(renderRow)}</TableBody>
|
|
244
|
+
{totalsRows.length > 0 && (
|
|
245
|
+
<TableFooter>{totalsRows.map(renderRow)}</TableFooter>
|
|
246
|
+
)}
|
|
247
|
+
</Table>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{showCorrectAnswers && question.explanation && (
|
|
251
|
+
<Alert className="mt-1">
|
|
252
|
+
<AlertDescription>
|
|
253
|
+
<strong>Explanation:</strong>{" "}
|
|
254
|
+
<span dangerouslySetInnerHTML={{ __html: question.explanation }} />
|
|
255
|
+
</AlertDescription>
|
|
256
|
+
</Alert>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState,
|
|
1
|
+
import { useState, useMemo, memo } from "react";
|
|
2
2
|
import type { QuestionProps } from "./types";
|
|
3
3
|
import { Alert, AlertDescription } from "../ui/alert";
|
|
4
4
|
import { cn } from "../lib/utils";
|
|
@@ -12,18 +12,21 @@ import { cn } from "../lib/utils";
|
|
|
12
12
|
* onAnswer={(answers) => handleAnswer(answers)}
|
|
13
13
|
* />
|
|
14
14
|
*/
|
|
15
|
-
export const TrueFalse = ({
|
|
15
|
+
export const TrueFalse = memo(function TrueFalse({
|
|
16
16
|
question,
|
|
17
17
|
sessionAnswers,
|
|
18
18
|
onAnswer,
|
|
19
19
|
readOnly = false,
|
|
20
20
|
showCorrectAnswers = false,
|
|
21
21
|
disabled = false,
|
|
22
|
-
}: QuestionProps)
|
|
23
|
-
const [selectedAnswer, setSelectedAnswer] = useState<string>(
|
|
22
|
+
}: QuestionProps) {
|
|
23
|
+
const [selectedAnswer, setSelectedAnswer] = useState<string>(
|
|
24
|
+
() => sessionAnswers?.[0]?.answerUid || "",
|
|
25
|
+
);
|
|
24
26
|
|
|
25
|
-
const sortedAnswers =
|
|
26
|
-
(a, b) => a.sequence - b.sequence,
|
|
27
|
+
const sortedAnswers = useMemo(
|
|
28
|
+
() => [...(question.answers || [])].sort((a, b) => a.sequence - b.sequence),
|
|
29
|
+
[question.answers],
|
|
27
30
|
);
|
|
28
31
|
|
|
29
32
|
const handleChange = (uid: string) => {
|
|
@@ -33,11 +36,6 @@ export const TrueFalse = ({
|
|
|
33
36
|
onAnswer?.([{ uid }]);
|
|
34
37
|
};
|
|
35
38
|
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
const current = sessionAnswers?.[0]?.answerUid || "";
|
|
38
|
-
setSelectedAnswer(current);
|
|
39
|
-
}, [sessionAnswers]);
|
|
40
|
-
|
|
41
39
|
const getAnswerClasses = (answerUid: string) => {
|
|
42
40
|
if (!showCorrectAnswers) return "px-2";
|
|
43
41
|
|
|
@@ -55,7 +53,8 @@ export const TrueFalse = ({
|
|
|
55
53
|
<div className="flex flex-col gap-4">
|
|
56
54
|
<div dangerouslySetInnerHTML={{ __html: question.content }} />
|
|
57
55
|
|
|
58
|
-
<
|
|
56
|
+
<fieldset className="flex flex-col gap-2 border-0 p-0 m-0">
|
|
57
|
+
<legend className="sr-only">True or False</legend>
|
|
59
58
|
{sortedAnswers.map((answer) => (
|
|
60
59
|
<div
|
|
61
60
|
key={answer.uid}
|
|
@@ -72,10 +71,16 @@ export const TrueFalse = ({
|
|
|
72
71
|
className="accent-primary m-0 shrink-0"
|
|
73
72
|
/>
|
|
74
73
|
<span dangerouslySetInnerHTML={{ __html: answer.content }} />
|
|
74
|
+
{showCorrectAnswers && answer.isCorrect && (
|
|
75
|
+
<span className="sr-only">(Correct answer)</span>
|
|
76
|
+
)}
|
|
77
|
+
{showCorrectAnswers && selectedAnswer === answer.uid && !answer.isCorrect && (
|
|
78
|
+
<span className="sr-only">(Your answer — incorrect)</span>
|
|
79
|
+
)}
|
|
75
80
|
</label>
|
|
76
81
|
</div>
|
|
77
82
|
))}
|
|
78
|
-
</
|
|
83
|
+
</fieldset>
|
|
79
84
|
|
|
80
85
|
{showCorrectAnswers && question.explanation && (
|
|
81
86
|
<Alert className="mt-2">
|
|
@@ -87,4 +92,4 @@ export const TrueFalse = ({
|
|
|
87
92
|
)}
|
|
88
93
|
</div>
|
|
89
94
|
);
|
|
90
|
-
};
|
|
95
|
+
});
|
package/src/questions/types.ts
CHANGED
|
@@ -3,7 +3,17 @@ export type QuestionTypeEnum =
|
|
|
3
3
|
| "choice"
|
|
4
4
|
| "true_false"
|
|
5
5
|
| "fill_in_the_blank"
|
|
6
|
-
| "essay"
|
|
6
|
+
| "essay"
|
|
7
|
+
| "matching"
|
|
8
|
+
| "ordering"
|
|
9
|
+
| "numeric"
|
|
10
|
+
| "hotspot"
|
|
11
|
+
| "inline_choice"
|
|
12
|
+
| "scenario"
|
|
13
|
+
| "spreadsheet";
|
|
14
|
+
|
|
15
|
+
/** Scoring strategy for scenario questions. */
|
|
16
|
+
export type ScenarioScoringMode = "per_question" | "all_or_nothing";
|
|
7
17
|
|
|
8
18
|
export interface AnswerOption {
|
|
9
19
|
uid: string;
|
|
@@ -20,12 +30,124 @@ export interface SessionAnswer {
|
|
|
20
30
|
confidence?: string;
|
|
21
31
|
}
|
|
22
32
|
|
|
33
|
+
/** A pair linking a left-side item to its correct right-side target. */
|
|
34
|
+
export interface MatchingPair {
|
|
35
|
+
uid: string;
|
|
36
|
+
/** Left-side item content (HTML) */
|
|
37
|
+
item: string;
|
|
38
|
+
/** Right-side target content (HTML) */
|
|
39
|
+
target: string;
|
|
40
|
+
sequence: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A clickable region on a hotspot image. Coordinates are percentages (0–100). */
|
|
44
|
+
export interface HotspotRegion {
|
|
45
|
+
uid: string;
|
|
46
|
+
shape: "rect" | "circle";
|
|
47
|
+
/** rect: [x, y, width, height]; circle: [centerX, centerY, radius] */
|
|
48
|
+
coords: number[];
|
|
49
|
+
isCorrect?: boolean;
|
|
50
|
+
label?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A dropdown blank embedded in an inline-choice (cloze) passage. */
|
|
54
|
+
export interface InlineBlank {
|
|
55
|
+
uid: string;
|
|
56
|
+
options: { uid: string; content: string; isCorrect?: boolean }[];
|
|
57
|
+
sequence: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A column definition for a spreadsheet question. */
|
|
61
|
+
export interface SpreadsheetColumn {
|
|
62
|
+
uid: string;
|
|
63
|
+
/** Column header text */
|
|
64
|
+
label: string;
|
|
65
|
+
/** Data type hint for input rendering and scoring */
|
|
66
|
+
type: "text" | "numeric" | "currency";
|
|
67
|
+
/** Column width in rems (default 8) */
|
|
68
|
+
width?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A single cell in a spreadsheet row. */
|
|
72
|
+
export interface SpreadsheetCell {
|
|
73
|
+
/** Unique identifier — becomes the answerUid in SessionAnswer */
|
|
74
|
+
uid: string;
|
|
75
|
+
/** Which column this cell belongs to */
|
|
76
|
+
columnUid: string;
|
|
77
|
+
/** When true, cell is pre-filled and not editable */
|
|
78
|
+
locked: boolean;
|
|
79
|
+
/** Display value for locked cells, or pre-fill for editable cells */
|
|
80
|
+
value?: string;
|
|
81
|
+
/** Expected answer for grading (editable cells only) */
|
|
82
|
+
correctAnswer?: string;
|
|
83
|
+
/** Acceptable numeric deviation (default 0 = exact match) */
|
|
84
|
+
tolerance?: number;
|
|
85
|
+
/** Hint shown in review mode (e.g., "= Revenue – Expenses") */
|
|
86
|
+
formula?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A row in a spreadsheet question grid. */
|
|
90
|
+
export interface SpreadsheetRow {
|
|
91
|
+
uid: string;
|
|
92
|
+
/** Optional row header label (e.g., "Cash", "Accounts Receivable") */
|
|
93
|
+
label?: string;
|
|
94
|
+
/** Ordered cells — one per column */
|
|
95
|
+
cells: SpreadsheetCell[];
|
|
96
|
+
/** Renders as a bold section divider row */
|
|
97
|
+
isHeader?: boolean;
|
|
98
|
+
/** Renders in table footer with top border */
|
|
99
|
+
isTotals?: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Content blocks associated with a question for open-book reference. */
|
|
103
|
+
export interface QuestionMaterial {
|
|
104
|
+
/** UID of the question this material is linked to */
|
|
105
|
+
questionUid: string;
|
|
106
|
+
/** Display label for the material group (e.g., "Chapter 3: Bonds") */
|
|
107
|
+
label?: string;
|
|
108
|
+
/** Content blocks to render in the materials drawer */
|
|
109
|
+
blocks: import("../content/types").LessonBlock[];
|
|
110
|
+
}
|
|
111
|
+
|
|
23
112
|
export interface QuestionData {
|
|
24
113
|
uid: string;
|
|
25
114
|
type: QuestionTypeEnum;
|
|
115
|
+
/** Question prompt (HTML) */
|
|
26
116
|
content: string;
|
|
117
|
+
/** Explanation shown in review mode (HTML) */
|
|
27
118
|
explanation?: string;
|
|
119
|
+
/** Answer options for choice / multiple_choice / true_false / ordering */
|
|
28
120
|
answers?: AnswerOption[];
|
|
121
|
+
|
|
122
|
+
/** Matching-specific: pairs of items and targets */
|
|
123
|
+
matchingPairs?: MatchingPair[];
|
|
124
|
+
|
|
125
|
+
/** Numeric-specific: correct answer */
|
|
126
|
+
numericAnswer?: number;
|
|
127
|
+
/** Numeric-specific: acceptable deviation (default 0 = exact) */
|
|
128
|
+
numericTolerance?: number;
|
|
129
|
+
/** Numeric-specific: unit label displayed after input */
|
|
130
|
+
numericUnit?: string;
|
|
131
|
+
|
|
132
|
+
/** Hotspot-specific: image URL */
|
|
133
|
+
hotspotImageUrl?: string;
|
|
134
|
+
/** Hotspot-specific: clickable regions */
|
|
135
|
+
hotspotRegions?: HotspotRegion[];
|
|
136
|
+
/** Hotspot-specific: allow selecting multiple regions */
|
|
137
|
+
hotspotMultiSelect?: boolean;
|
|
138
|
+
|
|
139
|
+
/** InlineChoice-specific: dropdown blanks embedded in content */
|
|
140
|
+
inlineBlanks?: InlineBlank[];
|
|
141
|
+
|
|
142
|
+
/** Scenario-specific: sub-questions grouped under this scenario's shared context */
|
|
143
|
+
scenarioQuestions?: QuestionData[];
|
|
144
|
+
/** Scenario-specific: how the scenario is scored (default "per_question") */
|
|
145
|
+
scenarioScoringMode?: ScenarioScoringMode;
|
|
146
|
+
|
|
147
|
+
/** Spreadsheet-specific: column definitions */
|
|
148
|
+
spreadsheetColumns?: SpreadsheetColumn[];
|
|
149
|
+
/** Spreadsheet-specific: row data with cells */
|
|
150
|
+
spreadsheetRows?: SpreadsheetRow[];
|
|
29
151
|
}
|
|
30
152
|
|
|
31
153
|
/**
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
interface UseDragReorderOptions<T> {
|
|
4
|
+
items: T[];
|
|
5
|
+
onReorder: (items: T[]) => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function useDragReorder<T>({
|
|
10
|
+
items,
|
|
11
|
+
onReorder,
|
|
12
|
+
disabled = false,
|
|
13
|
+
}: UseDragReorderOptions<T>) {
|
|
14
|
+
const [dragIndex, setDragIndex] = useState<number | null>(null);
|
|
15
|
+
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
|
16
|
+
const dragIndexRef = useRef<number | null>(null);
|
|
17
|
+
|
|
18
|
+
const handleDragStart = useCallback(
|
|
19
|
+
(index: number) => (e: React.DragEvent) => {
|
|
20
|
+
if (disabled) {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
dragIndexRef.current = index;
|
|
25
|
+
setDragIndex(index);
|
|
26
|
+
e.dataTransfer.effectAllowed = "move";
|
|
27
|
+
e.dataTransfer.setData("text/plain", String(index));
|
|
28
|
+
},
|
|
29
|
+
[disabled],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const handleDragOver = useCallback(
|
|
33
|
+
(index: number) => (e: React.DragEvent) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
e.dataTransfer.dropEffect = "move";
|
|
36
|
+
setDragOverIndex(index);
|
|
37
|
+
},
|
|
38
|
+
[],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const handleDrop = useCallback(
|
|
42
|
+
(index: number) => (e: React.DragEvent) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
const fromIndex = dragIndexRef.current;
|
|
45
|
+
if (fromIndex === null || fromIndex === index) {
|
|
46
|
+
setDragIndex(null);
|
|
47
|
+
setDragOverIndex(null);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const next = [...items];
|
|
52
|
+
const [moved] = next.splice(fromIndex, 1);
|
|
53
|
+
next.splice(index, 0, moved);
|
|
54
|
+
|
|
55
|
+
setDragIndex(null);
|
|
56
|
+
setDragOverIndex(null);
|
|
57
|
+
onReorder(next);
|
|
58
|
+
},
|
|
59
|
+
[items, onReorder],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const handleDragEnd = useCallback(() => {
|
|
63
|
+
setDragIndex(null);
|
|
64
|
+
setDragOverIndex(null);
|
|
65
|
+
dragIndexRef.current = null;
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const getDragProps = useCallback(
|
|
69
|
+
(index: number) => ({
|
|
70
|
+
draggable: !disabled,
|
|
71
|
+
onDragStart: handleDragStart(index),
|
|
72
|
+
onDragOver: handleDragOver(index),
|
|
73
|
+
onDrop: handleDrop(index),
|
|
74
|
+
onDragEnd: handleDragEnd,
|
|
75
|
+
}),
|
|
76
|
+
[disabled, handleDragStart, handleDragOver, handleDrop, handleDragEnd],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return { getDragProps, dragIndex, dragOverIndex };
|
|
80
|
+
}
|