- Overview
- Module MVC components
- User-event handlers
- Published events
- String representations of crosswordModel cells and clues
- API use cases
Refer to the Quickstart section for instructions on adding the crosswords-js package to your Node.js project
The design of crosswords-js follows the Model-view-controller (MVC) design pattern. The naming of files and code artifacts follow from this pattern.
The package exposes:
-
A factory function,
(newCrosswordController
), to create the MVC controller object, which surfaces the model and views as object properties (model
,gridView
andcluesView
respectively). -
Functions to create crossword definitions from text strings (
newCrosswordDefinition
) or files (convertSourceFileToDefinition
). -
A function (
compileCrossword
) to independently validate crossword source files - indirectly as crossword definitions. -
Helper functions (
assert
,ecs
,eid
,trace
,tracing
), which are useful aids for developers, but not essential.
As a module user, you will typically interact with the crosswordController to programmatically manipulate and monitor:
- the crosswordGridView - the crossword grid DOM element.
- the crosswordCluesView - the crossword clues DOM element.
A crosswordModel object is surfaced as a property of a crosswordController, but can be created explicitly (to separately test a puzzle's integrity) via:
const model = compileCrossword(crosswordDefinition);
which is an alias for
function newCrosswordModel(crosswordDefinition)
found in src/crossword-model.mjs
in the module source code.
A
crosswordDefinition
is a JavaScriptObject
, created by:
import
ing a JSON crosswordSource file, or...- creating from a String via
newCrosswordDefinition
, or...- from a file path via
convertSourceFileToDefinition
.
Firstly get the DOM elements which will be the parents for the crossword grid and clues blocks:
For example, if we have placeholder
div
elements somewhere in our webpage:
- gridView element location...
... <div id="crossword-grid-placeholder" /> ...
- Optional cluesView element location...
... <div id="crossword-clues-placeholder" /> ...We locate the elements via the webpage DOM global variable
document
:// Using module helper functions... const gridParent = eid('crossword-grid-placeholder'); const cluesParent = eid('crossword-clues-placeholder'); // Or, using simple JavaScript... const gridParent = document.getElementById('crossword-grid-placeholder'); const cluesParent = document.getElementById('crossword-clues-placeholder');
Create a crosswordController object by passing a crosswordDefinition
and the gridParent
and cluesParent
DOM elements into the factory function:
// cluesParent is an optional argument. Omit it if you don't want to use the cluesView element
const controller = newCrosswordController(
crosswordDefinition,
gridParent,
cluesParent,
);
found in src/crossword-controller.mjs
in the module source code.
This binds the crossword gridView and cluesView into the webpage DOM.
The crosswordGridView and crosswordCluesView are surfaced as properties GridView
and CluesView
of the crosswordController object. These are the corresponding DOM elements for the crossword grid and crossword clues.
// The object behind the crossword grid DOM element
controller.gridView;
// The object behind the optional crossword clues DOM element
controller.cluesView;
- The
crossword-grid
div element is a flat container ofcrossword-cell
div elements. - The grid contains
crosswordModel.height * crosswordModel.width
cell elements. - Grid rows are visually delimited using the CSS Grid layout in the
style/crosswords.less
stylesheet. - Cell elements are listed in row-major order (row-by-row) within the grid.
- The currentClue is visually identified by toggling the
active
class on the the cell elements - The currentCell is visually identified by toggling the
highlighted
class on the the cell element. - The
cwcell-revealed
andcwcell-incorrect
div elements are exposed/hidden by toggling thehidden
class on the element. These elements come into play when the puzzle solver tests or reveals letters, clues or the whole crossword.
Duplicate cell types have been removed for clarity
<div class="crossword-grid">
<!-- labelled clue cell -->
<div class="cwcell light noselect">
<div class="cwclue-label">1</div>
<div class="cwcell-revealed hidden"></div>
<div class="cwcell-incorrect hidden"></div>
</div>
<!-- unlabelled clue cell -->
<div class="cwcell light noselect">
<div class="cwcell-revealed hidden"></div>
<div class="cwcell-incorrect hidden"></div>
</div>
<!-- dark grid cell -->
<div class="cwcell dark"></div>
<!-- cell in the current clue -->
<div class="cwcell light noselect active">
<div class="cwclue-label">4</div>
<div class="cwcell-revealed hidden"></div>
<div class="cwcell-incorrect hidden"></div>
</div>
<!-- current cell -->
<div class="cwcell light noselect active highlighted">
<div class="cwcell-revealed hidden"></div>
<div class="cwcell-incorrect hidden"></div>
</div>
<!-- cell with across word separator -->
<!-- cell with across word separator -->
<div class="cwcell light noselect cw-across-word-separator">
<div class="cwcell-revealed hidden"></div>
<div class="cwcell-incorrect hidden"></div>
</div>
<!-- cell with down word separator -->
<div class="cwcell light noselect cw-down-word-separator">
<div class="cwcell-revealed hidden"></div>
<div class="cwcell-incorrect hidden"></div>
</div>
<!-- revealed cell -->
<div class="cwcell light noselect">
<div class="cwcell-revealed"></div>
<div class="cwcell-incorrect hidden"></div>
</div>
<!-- incorrect cell -->
<div class="cwcell light noselect active highlighted">
<div class="cwcell-revealed hidden"></div>
<div class="cwcell-incorrect"></div>
</div>
</div>
- The
crossword-clues
div element is a simple container of twocrossword-clue-block
div elements- the
crossword-across-clues
clueBlock element - the
crossword-down-clues
clueBlock element
- the
- The clueBlock elements contain a
crossword-clue-block-title
paragraph (p
) element, followed by a flat list ofcrossword-clue
div elements - The crosswordCluesView is visually laid out using CSS Flexbox in the
style/crosswords.less
stylesheet.
Duplicate cell types have been removed for clarity
<div class="crossword-clues">
<!-- across clues block -->
<div class="crossword-clue-block" id="crossword-across-clues">
<!-- across clues title -->
<p class="crossword-clue-block-title">Across</p>
<!-- typical clue -->
<div class="crossword-clue">
<span class="crossword-clue-label">1.</span>
<span class="crossword-clue-text">
Married woman shows animosity (6)
</span>
</div>
<!-- current clue -->
<div class="crossword-clue current-clue-segment">
<span class="crossword-clue-label">4.</span>
<span class="crossword-clue-text">
One's held back by a stout grating (8)
</span>
</div>
</div>
<!-- down clues block -->
<div class="crossword-clue-block" id="crossword-down-clues">
<!-- down clues title -->
<p class="crossword-clue-block-title">Down</p>
<!-- typical clue -->
<div class="crossword-clue">
<span class="crossword-clue-label">1.</span>
<span class="crossword-clue-text">
Ordered risotto after introduction to Minnie Driver (8)
</span>
</div>
</div>
</div>
The controller
exposes methods which can be used to respond to user-generated events.
For example, they can be used as handlers for DOM element events such as a button click.
Method name | Method ID | Description |
---|---|---|
testCurrentClue |
"test-clue" |
Check the current clue answer against the solution. |
cleanCurrentClue |
"clean-clue" |
Clear incorrect letters in the answer for the current clue after testing. |
revealCurrentCell |
"reveal-cell" |
Reveal the current letter only, in the answer for the current clue. |
revealCurrentClue |
"reveal-clue" |
Reveal the entire solution for the current clue. |
resetCurrentClue |
"reset-clue" |
Clear out the answer for the current clue. |
testCrossword |
"test-crossword" |
Check all the answers against the solutions. |
cleanCrossword |
"clean-crossword" |
Clear incorrect letters for the entire crossword after testing. |
revealCrossword |
"reveal-crossword" |
Reveal the solutions for the entire crossword. |
resetCrossword |
"reset-crossword" |
Clear out all the answers across the entire crossword. |
- Incorrect letters revealed by testing have distinct styling, which is removed when a new letter is entered or the cell is cleaned or reset.
- Revealed cells have distinct styling which remains for the duration of the puzzle. Public shaming is strictly enforced!
The event handlers can be called explicitly in code
controller.testCurrentClue();
The event handlers can be bound to DOM elements like buttons
, with id
or class
attributes that match the method IDs in the table above.
- Bind by
class
if you have more than one DOM element you want to generate the user event. - Bind by
id
(orclass
) if only one element will generate the user event. For example...
<div id="user-actions">
<p>Clue</p>
<button id="test-clue">Test</button>
<button id="clean-clue">Clean</button>
<button id="reveal-clue">Reveal</button>
<button class="reset-clue">Reset</button>
<button class="reset-clue">MoSet</button>
<p>Crossword</p>
<button id="test-crossword">Test</button>
<button id="clean-crossword">Clean</button>
<button id="reveal-crossword">Reveal</button>
<button class="reset-crossword">Reset</button>
<button class="reset-crossword">MoSet</button>
</div>
The default second and third arguments for the controller.bind*
methods are:
- event:
click
- dom:
document
(global DOM variable)
//// method prototype - arguments have defaults as indicated ////
bindEventHandlerToId(elementId, [(eventName = 'click')], [(dom = document)]);
// Example: Bind the "click" event of the element with id 'test-clue'
controller.bindEventHandlerToId('test-clue', 'click', document);
// Example: Using default arguments for eventName ("click") and dom (document)
controller.bindEventHandlerToId('reveal-clue');
//// method prototype - arguments have defaults as indicated ////
bindEventHandlerToClass(
elementClass,
[(eventName = 'click')],
[(dom = document)],
);
// Example: Bind event handler to multiple elements with class 'reset-clue'
controller.bindEventHandlerToClass('reset-clue', 'click', document);
//// method prototype - arguments have defaults as indicated ////
bindEventHandlersToIds(
// The first argument, elementIds, is an ARRAY
// The default value is all controller user event handlers
[(elementIds = this.userEventHandlerIds)],
[(eventName = 'click')],
[(dom = document)],
);
// Example: Bind ALL the user event handlers, using defaults
controller.bindEventHandlersToIds();
//// method prototype - arguments have defaults as indicated ////
bindEventHandlersToClass(
// The first argument, elementClasses, is an ARRAY
// The default value is all controller user event handlers
[(elementClasses = this.userEventHandlerIds)],
[(eventName = 'click')],
[(dom = document)],
);
// Example: Bind the user event handlers to the click events of
// ALL elements with class 'reset-clue' or 'reset-crossword'
controller.bindEventHandlersToClass(['reset-clue', 'reset-crossword'], 'click');
The controller
also publishes a collection of events reflecting changes in internal state.
Event name | Event handler argument | Description |
---|---|---|
cellRevealed |
controller.currentCell |
The current letter in the current clue has been revealed. |
clueCleaned |
controller.currentClue |
The current clue has been cleaned - all incorrect letters cleared. |
clueIncomplete |
controller.currentClue |
The current clue has been tested - no errors, but incomplete. |
clueReset |
controller.currentClue |
The current clue has been cleared. |
clueRevealed |
controller.currentClue |
The current clue has been revealed. |
clueSelected |
controller.currentClue |
A new clue has been selected. This can be in response to keyboard or mouse/touch events. A new clue can be selected in the crosswordGridView or the crosswordCluesView DOM elements. The new (current) clue has distinct styling and is automatically synchronised between the crosswordGridView and the crosswordCluesView. Moving to, or selecting, a different cell in the current clue does not generate this event. If a cell intersects two clues (an across and a down clue), a second selection of the cell will toggle the current clue between the across and down clues and the clueSelected event will be emitted. |
clueSolved |
controller.currentClue |
Follows a clueTested event when the current clue answer is correct. |
clueTested |
controller.currentClue |
The current clue has been tested. |
crosswordCleaned |
controller.model |
The crossword has been cleaned - all incorrect letters cleared. |
crosswordIncomplete |
controller.model |
The crossword has been tested - no errors, but incomplete. |
crosswordLoaded |
crosswordDefinition |
A new crossword puzzle has been loaded from crosswordDefinition. |
crosswordReset |
controller.model |
The entire crossword has been cleared. |
crosswordRevealed |
controller.model |
The entire crossword has been revealed. A crosswordSolved event is not subsequently emitted in this case. |
crosswordSolved |
controller.model |
The entire crossword has been solved. This event occurs when all clues have complete and correct answers. This is emitted when the last cell is filled and all clues are complete and correctly answered. |
crosswordTested |
controller.model |
The entire crossword has been tested. A crosswordSolved event is emitted if all answers are complete and correct. |
You can register your own event listeners via
controller.addEventsListener(events, listener);
-
events
is an array of event names, such as['click', 'hover'];
A single event must still be passed in an array.
-
listener
is your event listener function.It must be a function or arrow function that takes a single argument. Refer to the Event handler argument column in the table above for the
data
argument passed to each event listener
// event function prototype
function (data) { ... }
// arrow function prototype
(data) => { ... }
Here is a complete example from dev/index.js
:
// Wire up current-clue elements
const currentClueLabel = eid('current-clue-label');
const currentClueText = eid('current-clue-text');
// Initialise content of #current-clue
const cc = controller.currentClue;
currentClueLabel.innerHTML = cc.labelText;
currentClueText.innerHTML = `${cc.clueText} ${cc.solutionLengthText}`;
// Update content of #current-clue when current clue changes
controller.addEventsListener(['clueSelected'], (clue) => {
currentClueLabel.innerHTML = clue.labelText;
currentClueText.innerHTML = `${clue.clueText} ${clue.solutionLengthText}`;
});
A crosswordModel cell is converted to a string automatically by crosswordModel cell.toString()
wherever a string version of a cell is required. For example,
- In a parameterised string...
// '${cell}' in assert() string argument is implicitly converted to a string
inputElement = (cell) => {
assert(cell.light, `dark cell! ${cell}`);
- When a cell DOM element is created...
// 'modelCell' is implicitly converted to a string before assignment to 'cellElement.id'
function newCellElement(document, modelCell) {
...
let cellElement = document.createElement('div');
// Identify cellElement with id of associated modelCell.
cellElement.id = modelCell;
addClass(cellElement, 'cwcell');
Likewise, a crosswordModel clue is converted to a string automatically by clueModel.toString()
wherever a string version of a clue is required. For example,
- In a parameterised string...
// '${clue}' in trace() string argument is implicitly converted to a string
function revealClue(controller, clue) {
...
trace(`revealClue: '${clue}'`);
Crossword puzzles can be loaded via the crosswordController method loadCrosswordSource
controller.loadCrosswordSource(mimeType, documentText);
-
documentText
: This is the plain-text content of a crossword source file. -
mimeType
: The format of thedocumentText
- Currently, two document formats are supported:
Format MimeType JSON application/json
YAML application/yaml
orapplication/x-yaml
Here is a complete example from dev/index.js
:
// Wire up crossword file picker
function addCrosswordFileListener() {
const cf = eid('crossword-file');
cf.addEventListener('change', loadCrosswordSource, false);
}
// Crossword file picker "change" event handler
function loadCrosswordSource(event) {
// Nested helper executed when a file has been picked
function reloadController(file) {
const fr = new FileReader();
// The onload event fires when the file has completed loading
// The file content is in e.target.result
fr.onload = (e) => {
const fileText = e.target.result;
// load new crossword into controller
window.controller.loadCrosswordSource(file.type, fileText);
};
// Asynchronous
fr.readAsText(file);
}
// Has a file been picked?
if (this.files.length > 0) {
// Yes!
const file = this.files[0];
trace(
`loadCrosswordSource: ${file.name} (${file.size} B) type:"${file.type}"`,
);
reloadController(file);
} else {
// Nope
trace(`loadCrosswordSource: cancelled`);
}
}
Grid cells can be filled programmatically via the crosswordController method setGridCell
controller.setGridCell(cellElementId, character);
character
: The new text content for the gridcell.cellElementId
: The id of the associated cell DOM element (cellElement.id
).
GridView keyboard shortcuts can be overridden via the crosswordController method setKeyboardEventBindings
controller.setKeyboardEventBindings(eventBindings);
eventBindings
: An array ofeventBinding
.
An eventBinding is an object with properties:
eventName
: The name of the keyboard event you're overriding.- Currently,
keydown
andkeyup
are supported.
- Currently,
keyBindings
: An array ofkeyBinding
.
A keyBinding is an object with properties:
key
: The KeyboardEvent.key for the key pressed by the puzzle solver.action
: A function implementing your new behaviour.
An action function can be any kind of JavaScript function. It must declare the following arguments:
controller
: The CrosswordController object is passed in.event
: The DOM event object is passed in.eventCell
: The CrosswordModel modelCell object associated with the grid cell is passed in.
// Within a literal keyBinding object declaration...
// Arrow function
action: (controller, event, eventCell) => <function-body>
// Anonymous function
action: (function (controller, event, eventCell) <function-body> );
Code examples of eventBinding objects are found in src/default-eventbindings.mjs
Note that the action function implementations are all composed of calls to helper functions declared in src/crossword-gridview.mjs
:
deleteCellContent
moveToCellAhead
moveToCellBehind
moveToCellDown
moveToCellLeft
moveToCellRight
moveToCellUp
moveToClueAhead
moveToClueBehind
setCellContent
toggleClueDirection
The EventKey
enumeration values used below are also defined in src/crossword-gridview.mjs
const defaultKeyDownBinding = {
eventName: 'keydown',
keyBindings: [
{
key: EventKey.backspace,
action: (controller, event, eventCell) => {
deleteCellContent(controller, event, eventCell);
moveToCellBehind(controller, eventCell);
},
},
{
key: EventKey.delete,
action: (controller, event, eventCell) => {
deleteCellContent(controller, event, eventCell);
},
},
{
key: EventKey.enter,
action: (controller, event, eventCell) => {
toggleClueDirection(controller, eventCell);
},
},
{
key: EventKey.tab,
action: (controller, event, eventCell) => {
event.shiftKey
? moveToClueBehind(controller, eventCell)
: moveToClueAhead(controller, eventCell);
},
},
{
key: EventKey.space,
action: (controller, event, eventCell) => {
event.shiftKey
? moveToCellBehind(controller, eventCell)
: moveToCellAhead(controller, eventCell);
},
},
],
};
The default eventBinding objects are assigned in the CrosswordController
constructor in src/crossword-controller.mjs
:
// Set keyboard event keyBindings for gridView cells.
this.setKeyboardEventBindings([defaultKeyDownBinding, defaultKeyUpBinding]);