Blanks Exercises were removing the blanks on the editor and text but not on solutions, added submit and preview to writing and reading
This commit is contained in:
247
src/components/ExamEditor/Exercises/Blanks/BlanksReducer.tsx
Normal file
247
src/components/ExamEditor/Exercises/Blanks/BlanksReducer.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
type TextToken = {
|
||||
type: 'text';
|
||||
content: string;
|
||||
isWhitespace: boolean;
|
||||
isLineBreak?: boolean;
|
||||
};
|
||||
|
||||
type BlankToken = {
|
||||
type: 'blank';
|
||||
id: number;
|
||||
};
|
||||
|
||||
type Token = TextToken | BlankToken;
|
||||
|
||||
export type BlankState = {
|
||||
id: number;
|
||||
position: number;
|
||||
};
|
||||
|
||||
|
||||
export const getTextSegments = (text: string): Token[] => {
|
||||
const tokens: Token[] = [];
|
||||
let lastIndex = 0;
|
||||
const regex = /{{(\d+)}}/g;
|
||||
let match;
|
||||
|
||||
const addTextTokens = (text: string) => {
|
||||
// Split by newlines first
|
||||
const lines = text.replaceAll("\\n",'\n').split(/(\n)/);
|
||||
|
||||
lines.forEach((line, i) => {
|
||||
if (line === '\n') {
|
||||
tokens.push({
|
||||
type: 'text',
|
||||
content: '<br>',
|
||||
isWhitespace: false,
|
||||
isLineBreak: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedText = line.replace(/\s+/g, ' ');
|
||||
if (normalizedText) {
|
||||
const parts = normalizedText.split(/(\s)/);
|
||||
parts.forEach(part => {
|
||||
if (part) {
|
||||
tokens.push({
|
||||
type: 'text',
|
||||
content: part,
|
||||
isWhitespace: /^\s+$/.test(part)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
addTextTokens(text.slice(lastIndex, match.index));
|
||||
}
|
||||
tokens.push({
|
||||
type: 'blank',
|
||||
id: parseInt(match[1])
|
||||
});
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
addTextTokens(text.slice(lastIndex));
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export const reconstructTextFromTokens = (tokens: Token[]): string => {
|
||||
return tokens.map(token => {
|
||||
if (token.type === 'blank') {
|
||||
return `{{${token.id}}}`;
|
||||
}
|
||||
if (token.type === 'text' && token.isLineBreak) {
|
||||
return '\n';
|
||||
}
|
||||
return token.content;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
|
||||
export type BlanksState = {
|
||||
text: string;
|
||||
blanks: BlankState[];
|
||||
selectedBlankId: number | null;
|
||||
draggedItemId: string | null;
|
||||
textMode: boolean;
|
||||
setEditing: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export type BlanksAction =
|
||||
| { type: "SET_TEXT"; payload: string }
|
||||
| { type: "SET_BLANKS"; payload: BlankState[] }
|
||||
| { type: "ADD_BLANK" }
|
||||
| { type: "REMOVE_BLANK"; payload: number }
|
||||
| { type: "SELECT_BLANK"; payload: number | null }
|
||||
| { type: "SET_DRAGGED_ITEM"; payload: string | null }
|
||||
| { type: "MOVE_BLANK"; payload: { blankId: number; newPosition: number } }
|
||||
| { type: "TOGGLE_EDIT_MODE" }
|
||||
| { type: "RESET", payload: { text: string } };
|
||||
|
||||
|
||||
export const blanksReducer = (state: BlanksState, action: BlanksAction): BlanksState => {
|
||||
switch (action.type) {
|
||||
case "SET_TEXT": {
|
||||
return {
|
||||
...state,
|
||||
text: action.payload,
|
||||
};
|
||||
}
|
||||
case "SET_BLANKS": {
|
||||
return {
|
||||
...state,
|
||||
blanks: action.payload,
|
||||
};
|
||||
}
|
||||
case "ADD_BLANK":
|
||||
state.setEditing(true);
|
||||
const newBlankId = Math.max(...state.blanks.map(b => b.id), 0) + 1;
|
||||
const newBlanks = [
|
||||
...state.blanks,
|
||||
{ id: newBlankId, position: state.blanks.length }
|
||||
];
|
||||
const newText = state.text + ` {{${newBlankId}}}`;
|
||||
|
||||
return {
|
||||
...state,
|
||||
blanks: newBlanks,
|
||||
text: newText
|
||||
};
|
||||
|
||||
case "REMOVE_BLANK": {
|
||||
if (state.blanks.length === 1) {
|
||||
toast.error("There needs to be at least 1 blank!");
|
||||
break;
|
||||
}
|
||||
state.setEditing(true);
|
||||
const blanksToKeep = state.blanks.filter(b => b.id !== action.payload);
|
||||
const updatedBlanks = blanksToKeep.map((blank, index) => ({
|
||||
...blank,
|
||||
position: index
|
||||
}));
|
||||
|
||||
const tokens = getTextSegments(state.text).filter(
|
||||
token => !(token.type === 'blank' && token.id === action.payload)
|
||||
);
|
||||
|
||||
const newText = reconstructTextFromTokens(tokens);
|
||||
|
||||
return {
|
||||
...state,
|
||||
blanks: updatedBlanks,
|
||||
text: newText,
|
||||
selectedBlankId: state.selectedBlankId === action.payload ? null : state.selectedBlankId
|
||||
};
|
||||
}
|
||||
|
||||
case "MOVE_BLANK": {
|
||||
state.setEditing(true);
|
||||
const { blankId, newPosition } = action.payload;
|
||||
const tokens = getTextSegments(state.text);
|
||||
|
||||
// Find the current position of the blank
|
||||
const currentPosition = tokens.findIndex(
|
||||
token => token.type === 'blank' && token.id === blankId
|
||||
);
|
||||
|
||||
if (currentPosition === -1) return state;
|
||||
|
||||
// Remove the blank and its surrounding whitespace
|
||||
const blankToken = tokens[currentPosition];
|
||||
tokens.splice(currentPosition, 1);
|
||||
|
||||
// When inserting at new position, ensure there's whitespace around the blank
|
||||
let insertPosition = newPosition;
|
||||
const prevToken = tokens[insertPosition - 1];
|
||||
const nextToken = tokens[insertPosition];
|
||||
|
||||
// Insert space before if needed
|
||||
if (!prevToken || (prevToken.type === 'text' && !prevToken.isWhitespace)) {
|
||||
tokens.splice(insertPosition, 0, {
|
||||
type: 'text',
|
||||
content: ' ',
|
||||
isWhitespace: true
|
||||
});
|
||||
insertPosition++;
|
||||
}
|
||||
|
||||
// Insert the blank
|
||||
tokens.splice(insertPosition, 0, blankToken);
|
||||
insertPosition++;
|
||||
|
||||
// Insert space after if needed
|
||||
if (!nextToken || (nextToken.type === 'text' && !nextToken.isWhitespace)) {
|
||||
tokens.splice(insertPosition, 0, {
|
||||
type: 'text',
|
||||
content: ' ',
|
||||
isWhitespace: true
|
||||
});
|
||||
}
|
||||
|
||||
// Reconstruct the text
|
||||
const newText = reconstructTextFromTokens(tokens);
|
||||
|
||||
// Update blank positions
|
||||
const updatedBlanks = tokens.reduce((acc, token, idx) => {
|
||||
if (token.type === 'blank') {
|
||||
acc.push({ id: token.id, position: idx });
|
||||
}
|
||||
return acc;
|
||||
}, [] as BlankState[]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
text: newText,
|
||||
blanks: updatedBlanks
|
||||
};
|
||||
}
|
||||
case "SELECT_BLANK":
|
||||
return { ...state, selectedBlankId: action.payload };
|
||||
case "SET_DRAGGED_ITEM":
|
||||
state.setEditing(true);
|
||||
return { ...state, draggedItemId: action.payload };
|
||||
case "TOGGLE_EDIT_MODE":
|
||||
return { ...state, textMode: !state.textMode };
|
||||
|
||||
case "RESET":
|
||||
return {
|
||||
text: action.payload.text || "",
|
||||
blanks: [],
|
||||
selectedBlankId: null,
|
||||
draggedItemId: null,
|
||||
textMode: false,
|
||||
setEditing: state.setEditing
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
Reference in New Issue
Block a user