Improving CSS-Only Crossword - with JavaScript

Part 5 of 8

Right now when we enter each letter into the crossword, the active cell remains in the same place. Instead of that, we want the active cell to move to the next cell to help us enter the next letter.

Moving the active cell on text entry

In the takeAction function if we attempt to match a single letter being entered, and move the active cell right:

        if (actions[key]) {
			actions[key](cell);
		} else {
			// bad code, the cursor moves at the wrong time
			const singleLetterRx = /^\w$/;
            if (key.match(singleLetterRx)) {
				cursorRight(cell);
			}
        }

But that results in the cell moving first before the letter is entered. We can’t do it as shown above. The keydown event isn’t suitable for us to use.

If using the keyup event, a rapid entry of keys results in missing letters on the grid. So the keypress event is the more reliable solution here.

    function keyPressHandler(evt) {
        evt.preventDefault();
        const key = evt.key;
        const cell = evt.target;
        const singleLetterRx = /^\w$/;
        if (key.match(singleLetterRx)) {
            cursorRight(cell);
        }
    }
    ...
    board.addEventListener("keypress", keyPressHandler);

where evt.preventDefault() helps to prevent problems that can occur in the Edge browser.

That works well with across clues, but down clues are a problem.

Entering down clues

We can use the spacebar to switch from across to down entries. We just need to have a boolean to save the state:

    const cells = initCells(board);
    let acrossEntry = true;

We can now flip that with the spacebar, and update the keyPressHandler function to use cursorDown when we’re not using acrossEntry.

		if (key === " ") {
			acrossEntry = !acrossEntry;
		}
		if (key.match(singleLetterRx)) {
			if (acrossEntry) {
				cursorRight(cell);
			} else {
				cursorDown(cell);
			}
		}

And in the HTML code I can leave a note about using the spacebar.

<label for="checkvaliditems">Check for valid squares</label>
<span>Spacebar to switch from across to down entry</span>

That works, but there are some things to fix:

  • already filled cells don’t get their value replaced
  • you can’t tell whether you are in across or down entry

We can fix that first thing now, and we’ll highlight the current word in the next post to help with that second issue.

Replacing already existing text

When we enter text in a cell that already has a value, we want our entered text to replace what was already there. An easy way to do that is to replace the contents of the cell before anything else is done.

		if (key.match(singleLetterRx)) {
			cell.value = key;
            ...

Simplify handler

The keypress hander function is looking quite complex now, so let’s move checking the letters out to a separate function, and the spacebar too.

    function checkSpacebar(key) {
        if (key === " ") {
            acrossEntry = !acrossEntry;
        }
    }
	function checkSingleLetter(key, cell) {
		const singleLetterRx = /^\w$/;
		if (key.match(singleLetterRx)) {
			cell.value = key;
			if (acrossEntry) {
				cursorRight(cell);
			} else {
				cursorDown(cell);
			}
		}
	}
    ...
    function keyPressHandler(evt) {
        evt.preventDefault();
        const key = evt.key;
        const cell = evt.target;
        checkSpacebar(key);
        checkSingleLetter(key, cell);
    }

Summary

Here is the code that we currently have for navigation and entering text.

function crosswordCursor(board) {
    function initCells(board) {
    	const allCells = Array.from(board.querySelectorAll("input"));
		function index(cell) {
			return allCells.findIndex(function (foundCell) {
				return foundCell.id === cell.id;
			});
		}
		function coords(cell) {
			const [match, row, col] = cell.id.match(/(\d+)-(\d+)/);
			return {
                row: Number(row),
                col: Number(col)
            };
		}
		function aboveRow(cell, cellList) {
			const row = coords(cell).row;
			return cellList.find(
				eachCell => coords(eachCell).row < row
			);
		}
		function belowRow(cell, cellList) {
			const row = coords(cell).row;
			return cellList.find(
				eachCell => coords(eachCell).row > row
			);
		}
		function sameCol(cell) {
			const col = coords(cell).col;
			return allCells.filter(
				eachCell => coords(eachCell).col === col
			);
		}
		function prev(cell) {
			const cellIndex = index(cell);
			return allCells[cellIndex - 1];
		}
		function next(cell) {
			const cellIndex = index(cell);
			return allCells[cellIndex + 1];
		}
		function up(cell) {
			const cellList = sameCol(cell);
			return aboveRow(cell, cellList.reverse());
		}
		function down(cell) {
			const cellList = sameCol(cell);
			return belowRow(cell, cellList);
		}
		function moveTo(cell) {
			if (!cell) {
				return;
			}
			cell.focus();
		}
		return {
			prev,
			next,
			up,
			down,
			moveTo
		};
	}
	const cells = initCells(board);
	let acrossEntry = true;
    
	function moveCursor(direction, cell) {
        const newCell = cells[direction](cell);
		if (!newCell) {
			return;
		}
        cells.moveTo(newCell);
    }
	function cursorLeft(cell) {
		moveCursor("prev", cell);
	}
	function cursorRight(cell) {
		moveCursor("next", cell);
	}
	function cursorUp(cell) {
		moveCursor("up", cell);
	}
	function cursorDown(cell) {
		moveCursor("down", cell);
	}
	function takeAction(key, cell) {
		const actions = {
			"ArrowLeft": cursorLeft,
			"ArrowRight": cursorRight,
			"ArrowUp": cursorUp,
			"ArrowDown": cursorDown
		};
		if (!actions[key]) {
			return;
		}
		actions[key](cell);
	}
	function checkSpacebar(key) {
		if (key === " ") {
			acrossEntry = !acrossEntry;
		}
	}
	function checkSingleLetter(key, cell) {
		const singleLetterRx = /^\w$/;
		if (key.match(singleLetterRx)) {
			cell.value = key;
			if (acrossEntry) {
				cursorRight(cell);
			} else {
				cursorDown(cell);
			}
		}
	}
	function keyDownHandler(evt) {
		const key = evt.key;
		const cell = evt.target;
		takeAction(key, cell);
	}
	function keyPressHandler(evt) {
		evt.preventDefault();
        const key = evt.key;
        const cell = evt.target;
		checkSpacebar(key);
        checkSingleLetter(key, cell);
	}
	board.addEventListener("keydown", keyDownHandler);
	board.addEventListener("keypress", keyPressHandler);
}
const board = document.querySelector(".crossword-board");
crosswordCursor(board);

And the updated HTML code to inform about using the spacebar is:

<label for="checkvaliditems">Check for valid squares</label>
<span>Spacebar to switch from across to down entry</span>

We’ve improved the way that crossword letters are entered. In the next post we’ll add highlighting, so that eventually we can tell which word we’re working with.

The above code can be explored at https://codepen.io/pmw57/pen/ZEbmjbB

3 Likes