import React, { useCallback, useMemo, useReducer, useEffect, ReactNode } from "react"; import { DndContext, DragEndEvent, DragStartEvent, MeasuringStrategy, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { restrictToWindowEdges, snapCenterToCursor, } from "@dnd-kit/modifiers"; import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea"; import Header from "../../Shared/Header"; import Alert, { AlertItem } from "../Shared/Alert"; import clsx from "clsx"; import { Card, CardContent } from "@/components/ui/card"; import { Blank, DropZone } from "./DragNDrop"; import { getTextSegments, BlankState, BlanksState, BlanksAction, BlankToken } from "./BlanksReducer"; import PromptEdit from "../Shared/PromptEdit"; import { Difficulty } from "@/interfaces/exam"; interface Props { title?: string; initialText: string; description: string; difficulty?: Difficulty; saveDifficulty: (difficulty: Difficulty) => void; state: BlanksState; module: string; editing: boolean; showBlankBank: boolean; alerts: AlertItem[]; prompt: string; updatePrompt: (prompt: string) => void; setEditing: React.Dispatch>; blanksDispatcher: React.Dispatch onBlankSelect?: (blankId: number | null) => void; onBlankRemove: (blankId: number) => void; onSave: () => void; onDiscard: () => void; onDelete: () => void; onPractice: () => void; isEvaluationEnabled?: boolean; children: ReactNode; } const BlanksEditor: React.FC = ({ title = "Fill Blanks", initialText, description, difficulty, saveDifficulty, state, editing, module, children, showBlankBank = true, alerts, blanksDispatcher, onBlankSelect, onBlankRemove, onSave, onDiscard, onDelete, onPractice, isEvaluationEnabled, setEditing, prompt, updatePrompt }) => { useEffect(() => { const tokens = getTextSegments(initialText); const initialBlanks = tokens.reduce((acc, token, idx) => { if (token.type === 'blank') { acc.push({ id: token.id, position: idx }); } return acc; }, [] as BlankState[]); blanksDispatcher({ type: "SET_TEXT", payload: initialText }); blanksDispatcher({ type: "SET_BLANKS", payload: initialBlanks }); }, [initialText, blanksDispatcher]); const tokens = useMemo(() => { return getTextSegments(state.text || ""); }, [state.text]); const handleDragStart = useCallback((event: DragStartEvent) => { blanksDispatcher({ type: "SET_DRAGGED_ITEM", payload: event.active.id.toString() }); }, [blanksDispatcher]); const handleDragEnd = useCallback( (event: DragEndEvent) => { const { active, over } = event; if (!over) return; const blankId = parseInt(active.id.toString().split("-").pop() || ""); const dropIndex = parseInt(over.id.toString().split("-")[1]); blanksDispatcher({ type: "MOVE_BLANK", payload: { blankId, newPosition: dropIndex }, }); blanksDispatcher({ type: "SET_DRAGGED_ITEM", payload: null }); }, [blanksDispatcher] ); const handleTextChange = useCallback( (newText: string) => { const processedText = newText.replace(/\[(\d+)\]/g, "{{$1}}"); const existingBlankIds = getTextSegments(state.text) .filter(token => token.type === 'blank') .map(token => (token as BlankToken).id); const newBlankIds = getTextSegments(processedText) .filter(token => token.type === 'blank') .map(token => (token as BlankToken).id); const removedBlankIds = existingBlankIds.filter(id => !newBlankIds.includes(id)); removedBlankIds.forEach(id => { onBlankRemove(id); }); blanksDispatcher({ type: "SET_TEXT", payload: processedText }); }, [blanksDispatcher, state.text, onBlankRemove] ); useEffect(() => { if (onBlankSelect !== undefined) onBlankSelect(state.selectedBlankId); }, [state.selectedBlankId, onBlankSelect]); const handleBlankSelect = (blankId: number) => { blanksDispatcher({ type: "SELECT_BLANK", payload: blankId === state.selectedBlankId ? null : blankId, }); }; const handleBlankRemove = useCallback((blankId: number) => { onBlankRemove(blankId); blanksDispatcher({ type: "REMOVE_BLANK", payload: blankId }); }, [blanksDispatcher, onBlankRemove]); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 4, tolerance: 5, }, }) ); const modifiers = [snapCenterToCursor, restrictToWindowEdges]; const measuring = { droppable: { strategy: MeasuringStrategy.Always, }, }; return (
{alerts.length > 0 && } updatePrompt(text)} /> {state.textMode ? ( { handleTextChange(text); if (!editing) setEditing(true) }} className="w-full h-full min-h-[200px] p-2 bg-white border rounded-md" placeholder="Enter text here. Use [1], [2], etc. for blanks..." /> ) : (
{tokens.map((token, index) => { const isWordToken = token.type === 'text' && !token.isWhitespace; const showDropZone = isWordToken || token.type === 'blank'; return ( {showDropZone && } {token.type === 'blank' ? ( ) : token.isLineBreak ? (
) : ( {token.content} )}
); })} {tokens.length > 0 && tokens[tokens.length - 1].type === 'text' && ( )}
)}
{(!state.textMode && showBlankBank) && ( {state.blanks.map(blank => ( ))} )} {children}
); } export default BlanksEditor;