Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ interface IPuzPuzzle {
| `leftNavElements` | `React.ReactNode` | Elements to display in the left side of the actions bar. |
| `onStart` | `() => void` | Called when the user starts the puzzle (dismisses the splash modal). |
| `isComplete` | `boolean` | If true, the puzzle is shown as completed and locked. |
| `skipFilledCells` | `boolean` | Optional. If `true` (default) typing in a filled cell attempts to place the letter in the next empty cell of the clue and advances focus intelligently. If `false`, letters overwrite the current cell, and focus moves to the next structural cell. |

### Completion Hooks

Expand Down
112 changes: 110 additions & 2 deletions src/components/CrosswordGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface CrosswordGridProps {
revealedCells?: boolean[][] | null;
useMobileKeyboard?: boolean;
disabled?: boolean;
skipFilledCells?: boolean;
}

const CrosswordGrid: React.FC<CrosswordGridProps> = ({
Expand All @@ -35,19 +36,126 @@ const CrosswordGrid: React.FC<CrosswordGridProps> = ({
validatedCells,
revealedCells,
useMobileKeyboard = false,
disabled = false
disabled = false,
skipFilledCells = false
}) => {
const gridRef = useRef<HTMLDivElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const [cellSize, setCellSize] = useState<number>(32); // default fallback

// Helper to find the immediate next cell in the clue's structure, regardless of content.
const findImmediateNextCellInClueStructure = (currentR: number, currentC: number): [number, number] | null => {
if (activeClueNumber === null) {
return null;
}

let nextR = currentR;
let nextC = currentC;

if (clueOrientation === "across") {
nextC++;
} else { // "down"
nextR++;
}

if (nextR >= 0 && nextR < rows &&
nextC >= 0 && nextC < columns &&
grid[nextR] !== undefined && !grid[nextR][nextC] &&
isPartOfActiveClue(nextR, nextC)) {
return [nextR, nextC];
}
return null;
};

// Find the next empty cell within the current active clue, skipping filled ones.
// Starts searching from the cell *after* (currentR, currentC).
const findNextEmptyCellInClue = (currentR: number, currentC: number): [number, number] | null => {
if (activeClueNumber === null) {
return null;
}

let r = currentR;
let c = currentC;

// eslint-disable-next-line no-constant-condition
while (true) {
const nextStructuralCell = findImmediateNextCellInClueStructure(r, c);
if (!nextStructuralCell) {
return null; // End of clue or blocked
}
const [nextR, nextC] = nextStructuralCell;
if (letters[nextR] === undefined || !letters[nextR][nextC]) { // Found an empty cell
return [nextR, nextC];
} else { // Cell is filled, continue search from this cell
r = nextR;
c = nextC;
}
}
};

const handleKeyDown = (e: React.KeyboardEvent, row: number, col: number) => {
if (disabled) return;

// Handle letter input
if (e.key.length === 1 && /^[a-zA-Z]$/.test(e.key)) {
e.preventDefault();
onLetterChange(row, col, e.key.toUpperCase());
const newLetter = e.key.toUpperCase();

if (skipFilledCells) {
let placementRow = row; // Cell where key event occurred (current focus)
let placementCol = col;
let letterActuallyPlaced = false;

// 1. Determine where to place the letter
if (letters[row] && letters[row][col]) { // If focused cell is filled
const nextEmptyCellForLetter = findNextEmptyCellInClue(row, col);
if (nextEmptyCellForLetter) {
placementRow = nextEmptyCellForLetter[0];
placementCol = nextEmptyCellForLetter[1];
onLetterChange(placementRow, placementCol, newLetter);
letterActuallyPlaced = true;
} else {
return; // No place to put the letter
}
} else { // Current focused cell is empty
onLetterChange(placementRow, placementCol, newLetter);
letterActuallyPlaced = true;
}

// 2. If letter was placed, determine where to advance focus
if (letterActuallyPlaced) {
let cellToAdvanceFocusTo: [number, number] | null = null;
const nextEmptyCellForFocus = findNextEmptyCellInClue(placementRow, placementCol);

if (nextEmptyCellForFocus) {
cellToAdvanceFocusTo = nextEmptyCellForFocus;
} else {
const immediateNextStructural = findImmediateNextCellInClueStructure(placementRow, placementCol);
if (immediateNextStructural) {
cellToAdvanceFocusTo = immediateNextStructural;
}
}

if (cellToAdvanceFocusTo) {
if (onNavigateToClue && activeClueNumber !== null) {
onNavigateToClue(activeClueNumber, clueOrientation, cellToAdvanceFocusTo);
} else if (onCellClick) {
onCellClick(cellToAdvanceFocusTo[0], cellToAdvanceFocusTo[1]);
}
}
}
} else { // Old logic: skipFilledCells is false
onLetterChange(row, col, newLetter); // Place letter in current cell

const cellToAdvanceFocusTo = findImmediateNextCellInClueStructure(row, col);
if (cellToAdvanceFocusTo) {
if (onNavigateToClue && activeClueNumber !== null) {
onNavigateToClue(activeClueNumber, clueOrientation, cellToAdvanceFocusTo);
} else if (onCellClick) {
onCellClick(cellToAdvanceFocusTo[0], cellToAdvanceFocusTo[1]);
}
}
}
} else if (e.key === "Backspace" || e.key === "Delete") {
e.preventDefault();
onLetterChange(row, col, "");
Expand Down
7 changes: 7 additions & 0 deletions src/components/CrosswordSolver.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ interface CrosswordSolverProps {
* If true, the puzzle is shown as completed and locked (no further editing, all answers revealed, timer stopped, and success modal shown).
*/
isComplete?: boolean;
/**
* Optional. If true, typing in a filled cell attempts to place the letter in the next empty cell of the clue and advances focus intelligently.
* If false (default), letters overwrite the current cell, and focus moves to the next structural cell.
*/
skipFilledCells?: boolean;
}

const CrosswordSolver: React.FC<CrosswordSolverProps> = ({
Expand All @@ -36,6 +41,7 @@ const CrosswordSolver: React.FC<CrosswordSolverProps> = ({
leftNavElements,
onStart,
isComplete,
skipFilledCells = true,
}) => {
const [grid, setGrid] = useState<boolean[][]>([]);
const [letters, setLetters] = useState<string[][]>([]);
Expand Down Expand Up @@ -1502,6 +1508,7 @@ const CrosswordSolver: React.FC<CrosswordSolverProps> = ({
revealedCells={revealedCells}
useMobileKeyboard={useMobileKeyboard}
disabled={hasCompleted || isComplete}
skipFilledCells={skipFilledCells}
/>
</div>

Expand Down