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 } from "./FillBlanksReducer"; interface Props { title?: string; initialText: string; description: string; state: BlanksState; module: string; editing: boolean; showBlankBank: boolean; alerts: AlertItem[]; setEditing: React.Dispatch>; blanksDispatcher: React.Dispatch onBlankSelect?: (blankId: number | null) => void; onSave: () => void; onDiscard: () => void; onDelete: () => void; children: ReactNode; } const BlanksEditor: React.FC = ({ title = "Fill Blanks", initialText, description, state, editing, module, children, showBlankBank = true, alerts, blanksDispatcher, onBlankSelect, onSave, onDiscard, onDelete, setEditing }) => { 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}}"); blanksDispatcher({ type: "SET_TEXT", payload: processedText }); }, [blanksDispatcher] ); 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) => { blanksDispatcher({ type: "REMOVE_BLANK", payload: blankId }); }, [blanksDispatcher]); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 4, tolerance: 5, }, }) ); const modifiers = [snapCenterToCursor, restrictToWindowEdges]; const measuring = { droppable: { strategy: MeasuringStrategy.Always, }, }; return (
{alerts.length > 0 && } {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;