summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZhineng Li <[email protected]>2026-04-30 10:33:23 +0800
committerZhineng Li <[email protected]>2026-04-30 10:34:20 +0800
commit68c9b7560642c802ca3bfe6d7e0f7a8c54412c40 (patch)
treea262f6e37f4991cc5c8d88b50993c58af722e70c
parent6f2282ca7c13fdb43f21e8f52ac20235ff7e4ded (diff)
downloadword-search-game-68c9b7560642c802ca3bfe6d7e0f7a8c54412c40.tar.gz
word-search-game-68c9b7560642c802ca3bfe6d7e0f7a8c54412c40.zip
keyboard & touch navigation, configurable settings, and refactor
- keyboard support: arrow keys or `hjkl` to move cursor, Space to start selection, Enter to confirm, Escape to cancel - configurable settings: word placement directions, grid size, cell size, colors, fonts, debug mode, and more via `GameSettings` - modernize CSS (logical properties, grid layout) and HTML semantics - refactor JavaScript code around single-responsibility principles
-rw-r--r--css/style.css163
-rw-r--r--css/style.min.css1
-rw-r--r--css/wordsearch.css65
-rw-r--r--css/wordsearch.min.css1
-rw-r--r--images/forkme_right_red.pngbin7927 -> 0 bytes
-rw-r--r--index.html97
-rw-r--r--js/utility.js32
-rw-r--r--js/utility.min.js1
-rw-r--r--js/wordsearch.js1253
-rw-r--r--js/wordsearch.min.js1
10 files changed, 964 insertions, 650 deletions
diff --git a/css/style.css b/css/style.css
index 51304ed..73e9a77 100644
--- a/css/style.css
+++ b/css/style.css
@@ -1,58 +1,159 @@
-/* CSS reset */
+*, *::before, *::after {
+ box-sizing: border-box;
+}
+
+:root {
+ --stone-50: oklch(98.5% 0.001 106.423);
+ --stone-100: oklch(97% 0.001 106.424);
+ --stone-200: oklch(92.3% 0.003 48.717);
+ --stone-500: oklch(55.3% 0.013 58.071);
+ --stone-700: oklch(37.4% 0.01 67.558);
+ --stone-800: oklch(26.8% 0.007 34.298);
+}
-body, h1 {
+body, h1, h2, p, ul {
margin: 0;
padding: 0;
}
-h1 {
- text-transform: uppercase;
+ul {
+ list-style-type: none;
}
body {
- color: #333;
+ font-family: system-ui, -apple-system, sans-serif;
+ color: var(--stone-800);
+ background-color: var(--stone-100);
+ min-height: 100vh;
}
-/* Common */
+.page {
+ position: relative;
+ display: grid;
+ grid-template-areas:
+ "title title title"
+ "board . words"
+ "status status status";
+ grid-template-columns: auto 2.5rem auto;
+ justify-content: center;
+ margin-inline: auto;
+ width: min(62rem, calc(100% - 2rem));
+}
-.fix {
- *zoom: 1;
+.page__header {
+ margin-block: 2rem;
+ grid-area: title;
+ text-align: center;
}
-.fix:after {
- display: table;
- clear: both;
- content: '';
+.page__board {
+ grid-area: board;
+ display: flex;
+ justify-content: center;
}
-/* Home */
+.page__words {
+ grid-area: words;
+}
-.wrap {
- width: 960px;
- margin: 0 auto;
- padding: 40px 0;
+.page__footer {
+ margin-block: 2rem;
+ grid-area: status;
text-align: center;
- position:relative;
}
-.logo, #gameArea {
- margin-bottom: 40px;
+.page__title {
+ font-weight: 700;
+ font-size: clamp(1.25rem, 4vw, 2rem);
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--stone-800);
+}
+
+.page__status {
+ font-size: 0.85rem;
+ letter-spacing: 0.02em;
+ color: var(--stone-500);
+}
+
+.word-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.word-list__item {
+ font-weight: 500;
+ font-size: 0.875rem;
+ letter-spacing: 0.03em;
+ color: var(--stone-800);
+ transition: opacity 0.2s, color 0.2s;
}
-.ws-area, .ws-words {
- display: inline-block;
- vertical-align: top;
+.word-list__item--found {
+ text-decoration: line-through;
+ opacity: 0.4;
}
-.ws-words {
- margin-left: 20px;
- text-align: left;
+@media (max-width: 860px) {
+ .page {
+ grid-template-columns: auto;
+ grid-template-areas:
+ "title"
+ "board"
+ "words"
+ "status";
+ }
+
+ .page__header {
+ margin-block: 1rem;
+ }
+
+ .word-list {
+ margin-block-start: 1rem;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 0.75rem 1.25rem;
+ }
+
+ .page__footer {
+ margin-block: 2rem 1rem;
+ }
}
-.ws-word {
- margin-bottom: 4px;
+.complete {
+ position: absolute;
+ background-color: color-mix(in srgb, var(--stone-50) 92%, transparent);
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
}
-.ws-words ::first-letter{
- text-transform : capitalize;
-} \ No newline at end of file
+.complete__inner {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+}
+
+.complete__content {
+ padding: 1.5rem;
+ text-align: center;
+}
+
+.complete__title {
+ font-weight: 700;
+ font-size: 1.75rem;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: var(--stone-800);
+}
+
+.complete__message {
+ margin-block-start: 0.5rem;
+ font-size: 1rem;
+ color: var(--stone-700);
+}
diff --git a/css/style.min.css b/css/style.min.css
deleted file mode 100644
index 6754647..0000000
--- a/css/style.min.css
+++ /dev/null
@@ -1 +0,0 @@
-body,h1{margin:0;padding:0}h1{text-transform:uppercase}body{color:#333}.fix:after{display:table;clear:both;content:''}.wrap{width:960px;margin:0 auto;padding:40px 0;text-align:center;position:relative}#gameArea,.logo{margin-bottom:40px}.ws-area,.ws-words{display:inline-block;vertical-align:top}.ws-words{margin-left:20px;text-align:left}.ws-word{margin-bottom:4px}.ws-words ::first-letter{text-transform:capitalize} \ No newline at end of file
diff --git a/css/wordsearch.css b/css/wordsearch.css
deleted file mode 100644
index 149c36e..0000000
--- a/css/wordsearch.css
+++ /dev/null
@@ -1,65 +0,0 @@
-/* Wordsearch */
-
-.ws-area {
- background: #fafafa;
- display: inline-block;
- padding: 20px;
- border-radius: 10px;
- -moz-user-select: -moz-none;
- -khtml-user-select: none;
- -webkit-user-select: none;
- -ms-user-select: none;
-}
-
-.ws-row {
- line-height: 0;
-}
-
-.ws-col {
- cursor: pointer;
-}
-
-.ws-col.ws-selected {
- background: #eee;
-}
-
-.ws-found {
- background: yellow;
-}
-
-.ws-game-over-outer {
- background: rgba(0, 0, 0, 0.85);
- height: 100%;
- left: 0;
- position: absolute;
- top: 0;
- width: 100%;
-}
-
-.ws-game-over-inner {
- width:100%;
- height:100%;
- padding:0;
- margin:0;
- display:table;
-}
-
-.ws-game-over {
- display:table-cell;
- vertical-align:middle;
-}
-
-.ws-game-over h2 {
- color:#FFFFFF;
- font-size:2em;
- text-transform:uppercase;
- padding:0;
- margin:0 0 9px 0;
-}
-
-.ws-game-over p {
- color:#FFFFFF;
- font-size:1em;
- padding:0;
- margin:0;
-} \ No newline at end of file
diff --git a/css/wordsearch.min.css b/css/wordsearch.min.css
deleted file mode 100644
index 0e49dc3..0000000
--- a/css/wordsearch.min.css
+++ /dev/null
@@ -1 +0,0 @@
-.ws-area{background:#fafafa;display:inline-block;border-radius:10px;-moz-user-select:0;-khtml-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:20px}.ws-row{line-height:0}.ws-col{cursor:pointer}.ws-col.ws-selected{background:#eee}.ws-found{background:#FF0}.ws-game-over-outer{background:rgba(0,0,0,0.85);height:100%;left:0;position:absolute;top:0;width:100%}.ws-game-over-inner{width:100%;height:100%;display:table;margin:0;padding:0}.ws-game-over{display:table-cell;vertical-align:middle}.ws-game-over h2{color:#FFF;font-size:2em;text-transform:uppercase;margin:0 0 9px;padding:0}.ws-game-over p{color:#FFF;font-size:1em;margin:0;padding:0} \ No newline at end of file
diff --git a/images/forkme_right_red.png b/images/forkme_right_red.png
deleted file mode 100644
index 1e19c21..0000000
--- a/images/forkme_right_red.png
+++ /dev/null
Binary files differ
diff --git a/index.html b/index.html
index 28574fc..358497e 100644
--- a/index.html
+++ b/index.html
@@ -2,32 +2,81 @@
<html lang="en">
<head>
<meta charset="utf-8">
- <title>Word search game</title>
- <link rel="stylesheet" href="css/wordsearch.min.css" />
- <link rel="stylesheet" href="css/style.min.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="theme-color" content="#f5f3f0">
+ <title>Word Search</title>
+ <link rel="stylesheet" href="css/style.css">
</head>
<body>
- <div class="wrap">
- <h1 class="logo">Word search game</h1>
- <section id="ws-area"></section>
- <ul class="ws-words"></ul>
- </div>
- <a href="https://github.com/lizhineng/word-search-game"><img style="position: absolute; top: 0; right: 0; border: 0;" src="images/forkme_right_red.png" alt="Fork me on GitHub"></a>
- <script src="js/utility.min.js"></script>
- <script src="js/wordsearch.min.js"></script>
- <script type="text/javascript">
- var gameAreaEl = document.getElementById('ws-area');
- var gameobj = gameAreaEl.wordSearch();
-
- // Put words into `.ws-words`
- var words = gameobj.settings.wordsList,
- wordsWrap = document.querySelector('.ws-words');
- for (i in words) {
- var liEl = document.createElement('li');
- liEl.setAttribute('class', 'ws-word');
- liEl.innerText = words[i];
- wordsWrap.appendChild(liEl);
+ <main class="page">
+ <header class="page__header">
+ <h1 class="page__title">Word Search Game</h1>
+ </header>
+ <section id="gameboard" class="page__board"></section>
+ <aside class="page__words" aria-label="Words to find">
+ <ul class="word-list"></ul>
+ </aside>
+ <footer class="page__footer">
+ <p id="status" class="page__status" aria-live="polite"></p>
+ </footer>
+ </main>
+ <script type="module">
+ import { WordSearch } from './js/wordsearch.js';
+
+ const game = new WordSearch({
+ container: 'gameboard',
+ words: ['Bangkok', 'Hong Kong', 'London', 'Macau', 'Istanbul', 'Dubai', 'Mecca', 'Antalya', 'Paris', 'Kuala Lumpur'],
+ });
+
+ const list = document.querySelector('.word-list');
+
+ if (list) {
+ list.innerHTML = '';
+
+ for (let i = 0; i < game.words.length; i++) {
+ const li = document.createElement('li');
+ li.className = 'word-list__item';
+ li.dataset.word = game.normalizedWords[i];
+ li.textContent = game.words[i];
+ list.appendChild(li);
+ }
+
+ const statusEl = document.getElementById('status');
+ const STATUS_MESSAGES = {
+ 'game-started': 'Find all the words!',
+ 'board-focused': 'Use arrow keys to move, Space to start a selection, Enter to confirm.',
+ 'keyboard-started': 'Move with arrow keys, then press Enter to confirm.',
+ 'keyboard-cancelled': 'Selection canceled.',
+ 'word-found': ({ displayWord }) => `Found: ${displayWord}!`,
+ 'game-completed': 'You found all the words — well done!',
+ };
+
+ game.container.addEventListener('wordsearch:status', ({ detail }) => {
+ const msg = STATUS_MESSAGES[detail.name];
+ if (statusEl && msg !== undefined) {
+ statusEl.textContent = typeof msg === 'function' ? msg(detail) : msg;
+ }
+
+ if (detail.name === 'word-found') {
+ const item = list.querySelector(`[data-word="${detail.normalizedWord}"]`);
+ item?.classList.add('word-list__item--found');
+ }
+
+ if (detail.name === 'game-completed') {
+ const overlay = document.createElement('div');
+ overlay.className = 'complete';
+ overlay.innerHTML = `
+ <div class="complete__inner">
+ <div class="complete__content">
+ <h2 class="complete__title">Congratulations!</h2>
+ <p class="complete__message">You found all of the words!</p>
+ </div>
+ </div>
+ `;
+ game.container.parentNode.appendChild(overlay);
+ }
+ });
}
</script>
</body>
-</html> \ No newline at end of file
+</html>
diff --git a/js/utility.js b/js/utility.js
deleted file mode 100644
index fe1d0f1..0000000
--- a/js/utility.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Returns a random integer between min and max
- *
- * @param {Number} min
- * @param {Number} max
- * @return {Number}
- */
-if (typeof Math.rangeInt != 'function') {
- Math.rangeInt = function(min, max){
- if (max == undefined) {
- max = min;
- min = 0;
- }
- return Math.floor(Math.random() * (max - min + 1)) + min;
- }
-}
-
-/**
- * Mege two objects
- *
- * @param {Object} o1 Object 1
- * @param {Object} o2 Object 2
- * @return {Object}
- */
-if (typeof Object.merge != 'function') {
- Object.merge = function(o1, o2) {
- for (var i in o1) {
- o2[i] = o1[i];
- }
- return o2;
- }
-}
diff --git a/js/utility.min.js b/js/utility.min.js
deleted file mode 100644
index a8b0a58..0000000
--- a/js/utility.min.js
+++ /dev/null
@@ -1 +0,0 @@
-"function"!=typeof Math.rangeInt&&(Math.rangeInt=function(n,t){return void 0==t&&(t=n,n=0),Math.floor(Math.random()*(t-n+1))+n}),"function"!=typeof Object.merge&&(Object.merge=function(n,t){for(var e in n)t[e]=n[e];return t}); \ No newline at end of file
diff --git a/js/wordsearch.js b/js/wordsearch.js
index 5a3915f..c3390af 100644
--- a/js/wordsearch.js
+++ b/js/wordsearch.js
@@ -1,609 +1,874 @@
-(function(){
- 'use strict';
-
- // Extend the element method
- Element.prototype.wordSearch = function(settings) {
- return new WordSearch(this, settings);
+// @ts-check
+/**
+ * @typedef {object} GridPoint
+ * @property {number} row
+ * @property {number} col
+ */
+
+/**
+ * @typedef {object} Cell
+ * @property {number} row
+ * @property {number} col
+ * @property {string} letter
+ */
+
+/**
+ * @typedef {object} GameSettings
+ * @property {Direction[]} directions - The allowed word placement directions (e.g., 'RIGHT', 'DOWN', 'DIAG_RIGHT', 'DIAG_LEFT').
+ * @property {number} gridSize - The size of the game grid (e.g., 15 for a 15x15 grid).
+ * @property {number} cellSize - The size of each cell in pixels.
+ * @property {number} minCanvasWidth - The minimum width of the canvas in pixels.
+ * @property {number} canvasViewportGutter - The minimum distance in pixels between the canvas and the viewport edge.
+ * @property {string[]} words - The list of words to be placed in the grid.
+ * @property {number} fontWeight - The font weight for the letters (e.g., 600).
+ * @property {number} fontSize - The font size for the letters in pixels.
+ * @property {string} fontFamily - The font family for the letters (e.g., 'system-ui, -apple-system, sans-serif').
+ * @property {object} colors - An object defining the color scheme for the game.
+ * @property {string} colors.cell - The background color of unselected cells.
+ * @property {string} colors.selectedCell - The background color of currently selected cells.
+ * @property {string} colors.foundCell - The background color of cells that are part of found words.
+ * @property {string} colors.stroke - The color of the grid lines.
+ * @property {string} colors.focusStroke - The color of the focus indicator around the current cursor cell when the canvas is focused.
+ * @property {string} colors.text - The color of the letters in the cells.
+ * @property {boolean} debug - If true, empty cells will be rendered as blank instead of filled with random letters, which can help with debugging word placement.
+ */
+
+const DIRECTIONS = {
+ RIGHT: [0, 1],
+ DOWN: [1, 0],
+ DIAG_RIGHT: [1, 1],
+ DIAG_LEFT: [1, -1],
+};
+
+/**
+ * @typedef {keyof typeof DIRECTIONS} Direction
+ */
+
+class Linguist {
+ /**
+ * Normalizes a word for grid placement.
+ *
+ * The normalization process includes:
+ * 1. Unicode normalization (NFKD) to decompose characters into their base forms.
+ * 2. Removal of diacritical marks (accents).
+ * 3. Removal of whitespace characters.
+ * 4. Conversion to uppercase.
+ *
+ * @param {string} word
+ * @returns {string}
+ */
+ static normalize(word) {
+ return word
+ .normalize('NFKD')
+ .replace(/[\u0300-\u036f]/g, '')
+ .replace(/\s+/g, '')
+ .toUpperCase();
}
/**
- * Word seach
+ * Detects the Unicode range for different languages.
*
- * @param {Element} wrapWl the game's wrap element
- * @param {Array} settings
- * constructor
+ * @param {string} firstLetter
+ * @returns {[number, number]} A tuple containing the lower and upper bounds of the Unicode range for the detected language.
*/
- function WordSearch(wrapEl, settings) {
- this.wrapEl = wrapEl;
+ static detectLanguageRange(firstLetter) {
+ if (!firstLetter) {
+ return [65, 90];
+ }
- // Add `.ws-area` to wrap element
- this.wrapEl.classList.add('ws-area');
+ const code = firstLetter.charCodeAt(0);
- //Words solved.
- this.solved = 0;
+ // Latin
+ // See: https://www.unicode.org/charts/PDF/U0000.pdf
+ // See: https://www.unicode.org/charts/PDF/U0080.pdf
+ if (code >= 0x0000 && code <= 0x00ff) {
+ return [65, 90];
+ }
- // Default settings
- var default_settings = {
- 'directions': ['W', 'N', 'WN', 'EN'],
- 'gridSize': 10,
- 'words': ['one', 'two', 'three', 'four', 'five'],
- 'wordsList' : [],
- 'debug': false
+ // Cyrillic
+ // See: https://www.unicode.org/charts/PDF/U0400.pdf
+ if (code >= 0x0400 && code <= 0x04ff) {
+ return [1040, 1071];
}
- this.settings = Object.merge(settings, default_settings);
- // Check the words' length if it is overflow the grid
- if (this.parseWords(this.settings.gridSize)) {
- // Add words into the matrix data
- var isWorked = false;
+ // Hebrew
+ // See: https://unicode.org/charts/PDF/U0590.pdf
+ if (code >= 0x0590 && code <= 0x05ff) {
+ return [1488, 1514];
+ }
- while (isWorked == false) {
- // initialize the application
- this.initialize();
+ // Arabic
+ // See: https://www.unicode.org/charts/PDF/U0600.pdf
+ if (code >= 0x0600 && code <= 0x06ff) {
+ return [1569, 1610];
+ }
- isWorked = this.addWords();
- }
+ // Hiragana
+ // See: https://www.unicode.org/charts/PDF/U3040.pdf
+ if (code >= 0x3040 && code <= 0x309f) {
+ return [12353, 12438];
+ }
- // Fill up the remaining blank items
- if (!this.settings.debug) {
- this.fillUpFools();
- }
+ // Katakana
+ // See: https://www.unicode.org/charts/PDF/U30A0.pdf
+ if (code >= 0x30a0 && code <= 0x30ff) {
+ return [12449, 12538];
+ }
- // Draw the matrix into wrap element
- this.drawmatrix();
+ // CJK Unified Ideographs
+ // See: https://www.unicode.org/charts/PDF/U4E00.pdf
+ if (code >= 0x4e00 && code <= 0x9fff) {
+ return [19968, 40959];
}
+
+ // Latin
+ return [65, 90];
}
+}
+class Random {
/**
- * Parse words
- * @param {Number} Max size
- * @return {Boolean}
+ * @param {number} min
+ * @param {number} max
+ * @returns {number} A random integer in [min, max].
*/
- WordSearch.prototype.parseWords = function(maxSize) {
- var itWorked = true;
-
- for (var i = 0; i < this.settings.words.length; i++) {
- // Convert all the letters to upper case
- this.settings.wordsList[i] = this.settings.words[i].trim();
- this.settings.words[i] = removeDiacritics(this.settings.wordsList[i].trim().toUpperCase());
-
- var word = this.settings.words[i];
- if (word.length > maxSize) {
- alert('The length of word `' + word + '` is overflow the gridSize.');
- console.error('The length of word `' + word + '` is overflow the gridSize.');
- itWorked = false;
- }
+ static integer(min, max) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+ }
+}
+
+class State {
+ /**
+ * The game grid represented as a 2D array of cells.
+ *
+ * @type {Cell[][]}
+ */
+ grid = [];
+
+ /**
+ * The currently selected cells during an active selection.
+ *
+ * @type {GridPoint[]}
+ */
+ selection = [];
+
+ /**
+ * A set of numeric cell keys (`row * gridSize + col`) that are part of found words.
+ *
+ * @type {Set<number>}
+ */
+ founds = new Set();
+
+ /**
+ * A set of normalized words that have been found by the player.
+ *
+ * @type {Set<string>}
+ */
+ solvedWords = new Set();
+
+ /**
+ * The starting point of the current selection, or `null` if no selection is active.
+ *
+ * @type {GridPoint|null}
+ */
+ anchor = null;
+
+ /**
+ * The current position of the cursor.
+ *
+ * @type {GridPoint}
+ */
+ cursor = { row: 0, col: 0 };
+
+ /**
+ * Whether the user is currently dragging to select cells.
+ *
+ * @type {boolean}
+ */
+ dragging = false;
+
+ /**
+ * Whether the canvas currently has focus, which affects keyboard navigation and selection.
+ *
+ * @type {boolean}
+ */
+ canvasHasFocus = false;
+}
+
+class Canvas {
+ #mount;
+ #settings;
+ #logicalBoardSize;
+
+ /**
+ * The canvas element.
+ *
+ * @type {HTMLCanvasElement}
+ */
+ #canvas = document.createElement('canvas');
+
+ /**
+ * The 2D rendering context for the canvas.
+ *
+ * @type {CanvasRenderingContext2D|null}
+ */
+ #ctx = this.#canvas.getContext('2d');
+
+ /**
+ * Initializes the canvas for the word search game.
+ *
+ * @param {Element} element - The DOM element to mount the canvas into.
+ * @param {GameSettings} settings - The game settings, including grid size, cell size, and colors.
+ */
+ constructor(element, settings) {
+ this.#mount = element;
+ this.#settings = settings;
+ this.#logicalBoardSize = settings.gridSize * settings.cellSize;
+ this.#configureCanvas();
+ }
+
+ #configureCanvas() {
+ this.#canvas.setAttribute('aria-label', 'Word search game board');
+ this.#canvas.setAttribute('role', 'img');
+ this.#canvas.tabIndex = 0;
+ this.#styleCanvas();
+
+ this.#mount.innerHTML = '';
+ this.#mount.appendChild(this.#canvas);
+
+ this.#resize();
+ }
+
+ #styleCanvas() {
+ this.#canvas.style.cursor = 'crosshair';
+ this.#canvas.style.display = 'block';
+ this.#canvas.style.touchAction = 'none';
+ this.#canvas.style.boxShadow = `0 0 0 1px ${this.#settings.colors.stroke}`;
+ }
+
+ #resize() {
+ const dpr = window.devicePixelRatio || 1;
+ const maxWidth = Math.min(
+ this.#logicalBoardSize,
+ Math.max(this.#settings.minCanvasWidth, window.innerWidth - this.#settings.canvasViewportGutter)
+ );
+
+ this.#canvas.style.width = `${maxWidth}px`;
+ this.#canvas.style.height = `${maxWidth}px`;
+ this.#canvas.width = Math.floor(this.#logicalBoardSize * dpr);
+ this.#canvas.height = Math.floor(this.#logicalBoardSize * dpr);
+
+ if (!this.#ctx) {
+ throw new Error('Failed to get 2D context from canvas.');
}
- return itWorked;
+ this.#ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+ }
+
+ resize() {
+ this.#resize();
}
/**
- * Put the words into the matrix
+ * Resolve the grid cell coordinates from the given client (mouse/touch) coordinates.
+ *
+ * @param {number} clientX
+ * @param {number} clientY
+ * @returns {GridPoint|null}
*/
- WordSearch.prototype.addWords = function() {
- var keepGoing = true,
- counter = 0,
- isWorked = true;
-
- while (keepGoing) {
- // Getting random direction
- var dir = this.settings.directions[Math.rangeInt(this.settings.directions.length - 1)],
- result = this.addWord(this.settings.words[counter], dir),
- isWorked = true;
-
- if (result == false) {
- keepGoing = false;
- isWorked = false;
- }
+ toGridCell(clientX, clientY) {
+ const { gridSize } = this.#settings;
+ const rect = this.#canvas.getBoundingClientRect();
+ const x = clientX - rect.left;
+ const y = clientY - rect.top;
+
+ if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
+ return null;
+ }
- counter++;
- if (counter >= this.settings.words.length) {
- keepGoing = false;
- }
- }
+ const col = Math.floor((x / rect.width) * gridSize);
+ const row = Math.floor((y / rect.height) * gridSize);
+
+ if (row < 0 || col < 0 || row >= gridSize || col >= gridSize) {
+ return null;
+ }
- return isWorked;
+ return { row, col };
}
/**
- * Add word into the matrix
+ * Render the current state of the game onto the canvas.
*
- * @param {String} word
- * @param {Number} direction
+ * @param {State} state - The current state of the game, including the grid, selection, and found words.
*/
- WordSearch.prototype.addWord = function(word, direction) {
- var itWorked = true,
- directions = {
- 'W': [0, 1], // Horizontal (From left to right)
- 'N': [1, 0], // Vertical (From top to bottom)
- 'WN': [1, 1], // From top left to bottom right
- 'EN': [1, -1] // From top right to bottom left
- },
- row, col; // y, x
+ render(state) {
+ if (!this.#ctx) {
+ return;
+ }
- switch (direction) {
- case 'W': // Horizontal (From left to right)
- var row = Math.rangeInt(this.settings.gridSize - 1),
- col = Math.rangeInt(this.settings.gridSize - word.length);
- break;
+ const {
+ cellSize,
+ gridSize,
+ colors,
+ fontWeight,
+ fontSize,
+ fontFamily,
+ } = this.#settings;
+ const boardSize = this.#logicalBoardSize;
+ const ctx = this.#ctx;
- case 'N': // Vertical (From top to bottom)
- var row = Math.rangeInt(this.settings.gridSize - word.length),
- col = Math.rangeInt(this.settings.gridSize - 1);
- break;
+ ctx.clearRect(0, 0, boardSize, boardSize);
+ ctx.fillStyle = colors.cell;
+ ctx.fillRect(0, 0, boardSize, boardSize);
- case 'WN': // From top left to bottom right
- var row = Math.rangeInt(this.settings.gridSize - word.length),
- col = Math.rangeInt(this.settings.gridSize - word.length);
- break;
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
- case 'EN': // From top right to bottom left
- var row = Math.rangeInt(this.settings.gridSize - word.length),
- col = Math.rangeInt(word.length - 1, this.settings.gridSize - 1);
- break;
+ const selection = new Set(state.selection.map(c => c.row * gridSize + c.col));
- default:
- var error = 'UNKNOWN DIRECTION ' + direction + '!';
- alert(error);
- console.log(error);
- break;
- }
+ for (let row = 0; row < gridSize; row++) {
+ for (let col = 0; col < gridSize; col++) {
+ const x = col * cellSize;
+ const y = row * cellSize;
+ const key = row * gridSize + col;
- // Add words to the matrix
- for (var i = 0; i < word.length; i++) {
- var newRow = row + i * directions[direction][0],
- newCol = col + i * directions[direction][1];
+ ctx.fillStyle =
+ state.founds.has(key) ? colors.foundCell :
+ selection.has(key) ? colors.selectedCell : colors.cell;
- // The letter on the board
- var origin = this.matrix[newRow][newCol].letter;
+ ctx.fillRect(x, y, cellSize, cellSize);
- if (origin == '.' || origin == word[i]) {
- this.matrix[newRow][newCol].letter = word[i];
- } else {
- itWorked = false;
+ ctx.strokeStyle = colors.stroke;
+ ctx.strokeRect(x + 0.5, y + 0.5, cellSize - 1, cellSize - 1);
+
+ ctx.fillStyle = colors.text;
+ ctx.fillText(state.grid[row][col].letter, x + cellSize / 2, y + cellSize / 2);
}
}
- return itWorked;
+ // If the canvas has focus, draw a focus indicator around the current cursor cell.
+ if (state.canvasHasFocus) {
+ const { row, col } = state.cursor;
+ ctx.strokeStyle = colors.focusStroke;
+ ctx.lineWidth = 2;
+ ctx.strokeRect(col * cellSize + 2, row * cellSize + 2, cellSize - 4, cellSize - 4);
+ ctx.lineWidth = 1;
+ }
}
+ get canvas() {
+ return this.#canvas;
+ }
+}
+
+class Handler {
+ #canvas;
+ #gridSize;
+ #state;
+ #callbacks;
+ #render;
+
/**
- * Initialize the application
+ * @param {Canvas} canvas
+ * @param {number} gridSize
+ * @param {State} state
+ * @param {() => void} render - A function to call to re-render the canvas after state changes.
+ * @param {object} [callbacks]
+ * @param {() => void} [callbacks.onCanvasFocus]
+ * @param {() => void} [callbacks.onCanvasBlur]
+ * @param {() => void} [callbacks.onKeyboardStart]
+ * @param {() => void} [callbacks.onKeyboardCancel]
+ * @param {(selection: GridPoint[]) => void} [callbacks.onSelection]
*/
- WordSearch.prototype.initialize = function() {
- /**
- * Letter matrix
- *
- * param {Array}
- */
- this.matrix = [];
+ constructor(canvas, gridSize, state, render, callbacks = {}) {
+ this.#canvas = canvas;
+ this.#gridSize = gridSize;
+ this.#state = state;
+ this.#render = render;
+ this.#callbacks = callbacks;
+ this.#registerEvents();
+ }
- /**
- * Selection from
- * @Param {Object}
- */
- this.selectFrom = null;
+ #registerEvents() {
+ const canvas = this.#canvas.canvas;
+
+ canvas.addEventListener('pointerdown', this.#onPointerDown);
+ canvas.addEventListener('keydown', this.#onKeyDown);
+ canvas.addEventListener('focus', this.#onFocus);
+ canvas.addEventListener('blur', this.#onBlur);
+ window.addEventListener('pointermove', this.#onPointerMove);
+ window.addEventListener('pointerup', this.#onPointerUp);
+ window.addEventListener('pointercancel', this.#onPointerCancel);
+ window.addEventListener('resize', this.#onResize);
+ }
- /**
- * Selected items
- */
- this.selected = [];
+ #onFocus = () => {
+ this.#state.canvasHasFocus = true;
+ this.#callbacks.onCanvasFocus?.();
+ this.#render();
+ };
- this.initmatrix(this.settings.gridSize);
- }
+ #onBlur = () => {
+ this.#state.canvasHasFocus = false;
+ this.#callbacks.onCanvasBlur?.();
+ this.#render();
+ };
/**
- * Fill default items into the matrix
- * @param {Number} size Grid size
+ * @param {KeyboardEvent} event
*/
- WordSearch.prototype.initmatrix = function(size) {
- for (var row = 0; row < size; row++) {
- for (var col = 0; col < size; col++) {
- var item = {
- letter: '.', // Default value
- row: row,
- col: col
- }
+ #onKeyDown = (event) => {
+ let { row, col } = this.#state.cursor;
+ let handled = true;
+
+ switch (event.key) {
+ case 'ArrowUp':
+ case 'k':
+ row = Math.max(0, row - 1);
+ break;
+ case 'ArrowDown':
+ case 'j':
+ row = Math.min(this.#gridSize - 1, row + 1);
+ break;
+ case 'ArrowLeft':
+ case 'h':
+ col = Math.max(0, col - 1);
+ break;
+ case 'ArrowRight':
+ case 'l':
+ col = Math.min(this.#gridSize - 1, col + 1);
+ break;
+ case ' ':
+ case 'Spacebar':
+ this.#startKeyboardSelection();
+ break;
+ case 'Enter':
+ this.#finishKeyboardSelection();
+ break;
+ case 'Escape':
+ this.#state.anchor = null;
+ this.#state.selection = [];
+ this.#callbacks.onKeyboardCancel?.();
+ break;
+ default:
+ handled = false;
+ break;
+ }
- if (!this.matrix[row]) {
- this.matrix[row] = [];
- }
+ this.#state.cursor = { row, col };
- this.matrix[row][col] = item;
- }
+ if (this.#state.anchor) {
+ this.#state.selection = this.#getSelectionCells(this.#state.anchor, this.#state.cursor);
+ }
+
+ if (handled) {
+ this.#render();
+ event.preventDefault();
}
+ };
+
+ #startKeyboardSelection() {
+ this.#state.anchor = { ...this.#state.cursor };
+ this.#callbacks.onKeyboardStart?.();
+ }
+
+ #finishKeyboardSelection() {
+ if (!this.#state.anchor) {
+ this.#startKeyboardSelection();
+ return;
+ }
+ this.#callbacks.onSelection?.(this.#state.selection);
+ this.#state.anchor = null;
+ this.#state.selection = [];
}
/**
- * Draw the matrix
+ * @param {PointerEvent} event
*/
- WordSearch.prototype.drawmatrix = function() {
- for (var row = 0; row < this.settings.gridSize; row++) {
- // New row
- var divEl = document.createElement('div');
- divEl.setAttribute('class', 'ws-row');
- this.wrapEl.appendChild(divEl);
-
- for (var col = 0; col < this.settings.gridSize; col++) {
- var cvEl = document.createElement('canvas');
- cvEl.setAttribute('class', 'ws-col');
- cvEl.setAttribute('width', 40);
- cvEl.setAttribute('height', 40);
-
- // Fill text in middle center
- var x = cvEl.width / 2,
- y = cvEl.height / 2;
-
- var ctx = cvEl.getContext('2d');
- ctx.font = '400 28px Calibri';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillStyle = '#333'; // Text color
- ctx.fillText(this.matrix[row][col].letter, x, y);
-
- // Add event listeners
- cvEl.addEventListener('mousedown', this.onMousedown(this.matrix[row][col]));
- cvEl.addEventListener('mouseover', this.onMouseover(this.matrix[row][col]));
- cvEl.addEventListener('mouseup', this.onMouseup());
-
- divEl.appendChild(cvEl);
- }
+ #onPointerDown = (event) => {
+ const start = this.#canvas.toGridCell(event.clientX, event.clientY);
+ if (!start) {
+ return;
}
- }
+
+ this.#canvas.canvas.focus({ preventScroll: true });
+ this.#state.dragging = true;
+ this.#state.anchor = start;
+ this.#state.cursor = { ...start };
+ this.#state.selection = [];
+
+ this.#canvas.canvas.setPointerCapture(event.pointerId);
+ this.#render();
+ };
/**
- * Fill up the remaining items
+ * @param {PointerEvent} event
*/
- WordSearch.prototype.fillUpFools = function() {
- var rangeLanguage = searchLanguage(this.settings.words[0].split('')[0]);
- for (var row = 0; row < this.settings.gridSize; row++) {
- for (var col = 0; col < this.settings.gridSize; col++) {
- if (this.matrix[row][col].letter == '.') {
- // Math.rangeInt(65, 90) => A ~ Z
- this.matrix[row][col].letter = String.fromCharCode(Math.rangeInt(rangeLanguage[0], rangeLanguage[1]));
- }
- }
+ #onPointerMove = (event) => {
+ if (!this.#state.dragging || !this.#state.anchor) {
+ return;
+ }
+
+ const current = this.#canvas.toGridCell(event.clientX, event.clientY);
+ if (!current) {
+ return;
}
- }
+
+ this.#state.cursor = current;
+ this.#state.selection = this.#getSelectionCells(this.#state.anchor, this.#state.cursor);
+ this.#render();
+ };
/**
- * Returns matrix items
- * @param rowFrom
- * @param colFrom
- * @param rowTo
- * @param colTo
- * @return {Array}
+ * @param {PointerEvent} event
*/
- WordSearch.prototype.getItems = function(rowFrom, colFrom, rowTo, colTo) {
- var items = [];
+ #onPointerUp = (event) => {
+ if (!this.#state.dragging) {
+ return;
+ }
+
+ this.#state.dragging = false;
+
+ if (this.#canvas.canvas.hasPointerCapture(event.pointerId)) {
+ this.#canvas.canvas.releasePointerCapture(event.pointerId);
+ }
+
+ if (this.#state.selection.length > 0) {
+ this.#callbacks.onSelection?.(this.#state.selection);
+ }
- if ( rowFrom === rowTo || colFrom === colTo || Math.abs(rowTo - rowFrom) == Math.abs(colTo - colFrom) ) {
- var shiftY = (rowFrom === rowTo) ? 0 : (rowTo > rowFrom) ? 1 : -1,
- shiftX = (colFrom === colTo) ? 0 : (colTo > colFrom) ? 1 : -1,
- row = rowFrom,
- col = colFrom;
+ this.#state.anchor = null;
+ this.#state.selection = [];
+ this.#render();
+ };
- items.push(this.getItem(row, col));
- do {
+ #onPointerCancel = () => {
+ this.#state.dragging = false;
+ this.#state.anchor = null;
+ this.#state.selection = [];
+ this.#render();
+ };
+
+ #onResize = () => {
+ this.#canvas.resize();
+ this.#render();
+ };
+
+ /**
+ * Returns all grid points along the straight line from `from` to `to`,
+ * inclusive of both endpoints. Returns an empty array if the path is not
+ * horizontal, vertical, or diagonal.
+ *
+ * @param {GridPoint} from - The starting grid position.
+ * @param {GridPoint} to - The ending grid position.
+ * @returns {GridPoint[]} The points along the path, or `[]` if not a straight line.
+ */
+ #getSelectionCells(from, to) {
+ const items = [];
+ const isDiag = Math.abs(to.row - from.row) === Math.abs(to.col - from.col);
+
+ if (from.row === to.row || from.col === to.col || isDiag) {
+ const shiftY = from.row === to.row ? 0 : to.row > from.row ? 1 : -1;
+ const shiftX = from.col === to.col ? 0 : to.col > from.col ? 1 : -1;
+
+ let row = from.row;
+ let col = from.col;
+
+ items.push({ row, col });
+
+ while (row !== to.row || col !== to.col) {
row += shiftY;
col += shiftX;
- items.push(this.getItem(row, col));
- } while( row !== rowTo || col !== colTo );
+ items.push({ row, col });
+ }
}
return items;
}
+}
+
+class WordSearch {
+ static #directions = DIRECTIONS;
/**
- * Returns matrix item
- * @param {Number} row
- * @param {Number} col
- * @return {*}
+ * @type {GameSettings}
*/
- WordSearch.prototype.getItem = function(row, col) {
- return (this.matrix[row] ? this.matrix[row][col] : undefined);
- }
+ static #defaultSettings = {
+ directions: ['RIGHT', 'DOWN', 'DIAG_RIGHT', 'DIAG_LEFT'],
+ gridSize: 15,
+ cellSize: 36,
+ minCanvasWidth: 260,
+ canvasViewportGutter: 32,
+ words: [],
+ fontWeight: 600,
+ fontSize: 18,
+ fontFamily: 'system-ui, -apple-system, sans-serif',
+ colors: {
+ cell: 'oklch(98.5% 0.001 106.423)',
+ selectedCell: 'oklch(95.6% 0.045 203.388)',
+ foundCell: 'oklch(96.2% 0.059 95.617)',
+ stroke: 'oklch(92.3% 0.003 48.717)',
+ focusStroke: 'oklch(78.9% 0.154 211.53)',
+ text: 'oklch(26.8% 0.007 34.298)',
+ },
+ debug: false,
+ };
+
+ #container;
+ #settings;
+ #normalizedWords;
+ #state = new State();
/**
- * Clear the exist highlights
+ * @type {Canvas|null}
*/
- WordSearch.prototype.clearHighlight = function() {
- var selectedEls = document.querySelectorAll('.ws-selected');
- for (var i = 0; i < selectedEls.length; i++) {
- selectedEls[i].classList.remove('ws-selected');
- }
- }
+ #canvas = null;
/**
- * Lookup if the wordlist contains the selected
- * @param {Array} selected
+ * @param {Partial<GameSettings> & { container?: string | Element }} [options]
*/
- WordSearch.prototype.lookup = function(selected) {
- var words = [''];
-
- for (var i = 0; i < selected.length; i++) {
- words[0] += selected[i].letter;
+ constructor(options = {}) {
+ const { container, ...settings } = options;
+
+ if (typeof container === 'string') {
+ const el = document.getElementById(container);
+ if (!el) throw new Error(`No element found with id "${container}".`);
+ this.#container = el;
+ } else if (container instanceof Element) {
+ this.#container = container;
+ } else {
+ throw new Error('`container` must be an element id string or an HTMLElement.');
}
- words.push(words[0].split('').reverse().join(''));
- if (this.settings.words.indexOf(words[0]) > -1 ||
- this.settings.words.indexOf(words[1]) > -1) {
- for (var i = 0; i < selected.length; i++) {
- var row = selected[i].row + 1,
- col = selected[i].col + 1,
- el = document.querySelector('.ws-area .ws-row:nth-child(' + row + ') .ws-col:nth-child(' + col + ')');
+ this.#settings = {
+ ...WordSearch.#defaultSettings,
+ ...settings,
+ colors: {
+ ...WordSearch.#defaultSettings.colors,
+ ...(settings.colors && typeof settings.colors === 'object' ? settings.colors : {}),
+ },
+ };
- el.classList.add('ws-found');
- }
+ this.#settings.words = this.#settings.words
+ .map(word => String(word || '').trim())
+ .filter(word => word.length > 0);
- //Cross word off list.
- var wordList = document.querySelector(".ws-words");
- var wordListItems = wordList.getElementsByTagName("li");
- for(var i=0; i<wordListItems.length; i++){
- if(words[0] == removeDiacritics(wordListItems[i].innerHTML.toUpperCase())){
- if(wordListItems[i].innerHTML != "<del>"+wordListItems[i].innerHTML+"</del>") { //Check the word is never found
- wordListItems[i].innerHTML = "<del>"+wordListItems[i].innerHTML+"</del>";
- //Increment solved words.
- this.solved++;
- }
-
-
- }
+ this.#normalizedWords = this.#settings.words.map(Linguist.normalize);
+
+ if (!this.#validateWords()) return;
+
+ this.#setupBoardState();
+ this.#initializeCanvas();
+ queueMicrotask(() => this.#updateStatus('game-started'));
+ }
+
+ #validateWords() {
+ for (let i = 0; i < this.#normalizedWords.length; i++) {
+ if (this.#normalizedWords[i].length > this.#settings.gridSize) {
+ const error = `The length of word "${this.#normalizedWords[i]}" exceeds gridSize.`;
+ alert(error);
+ console.error(error);
+ return false;
}
+ }
+ return true;
+ }
- //Game over?
- if(this.solved == this.settings.words.length){
- this.gameOver();
+ #setupBoardState() {
+ let placed = false;
+ for (let attempts = 0; !placed && attempts < 300; attempts++) {
+ this.#initializeGrid();
+ placed = this.#placeAllWords();
+ }
+
+ if (!placed) {
+ throw new Error('Unable to place all words in the grid after multiple attempts.');
+ }
+
+ if (!this.#settings.debug) {
+ this.#fillBlankCells();
+ }
+ }
+
+ #initializeGrid() {
+ this.#state.grid = Array.from({ length: this.#settings.gridSize }, (_, row) =>
+ Array.from({ length: this.#settings.gridSize }, (__, col) => ({
+ row,
+ col,
+ letter: '',
+ }))
+ );
+ }
+
+ #placeAllWords() {
+ for (const word of this.#normalizedWords) {
+ let placed = false;
+ for (let tries = 0; tries < 150 && !placed; tries++) {
+ const direction = this.#settings.directions[
+ Random.integer(0, this.#settings.directions.length - 1)
+ ];
+ placed = this.#placeWord(word, direction);
+ }
+ if (!placed) {
+ return false;
}
}
+ return true;
}
/**
- * Game Over
+ * @param {string} word
+ * @param {Direction} direction
+ * @returns {boolean}
*/
- WordSearch.prototype.gameOver = function() {
- //Create overlay.
- var overlay = document.createElement("div");
- overlay.setAttribute("id", "ws-game-over-outer");
- overlay.setAttribute("class", "ws-game-over-outer");
- this.wrapEl.parentNode.appendChild(overlay);
-
- //Create overlay content.
- var overlay = document.getElementById("ws-game-over-outer");
- overlay.innerHTML = "<div class='ws-game-over-inner' id='ws-game-over-inner'>"+
- "<div class='ws-game-over' id='ws-game-over'>"+
- "<h2>Congratulations!</h2>"+
- "<p>You've found all of the words!</p>"+
- "</div>"+
- "</div>";
+ #placeWord(word, direction) {
+ const step = WordSearch.#directions[direction];
+ if (!step) {
+ return false;
+ }
+
+ const [startRow, startCol] = this.#randomStart(word, direction);
+
+ for (let i = 0; i < word.length; i++) {
+ const r = startRow + i * step[0];
+ const c = startCol + i * step[1];
+ const l = this.#state.grid[r][c].letter;
+ if (l !== '' && l !== word[i]) {
+ return false;
+ }
+ }
+
+ for (let i = 0; i < word.length; i++) {
+ const r = startRow + i * step[0];
+ const c = startCol + i * step[1];
+ this.#state.grid[r][c].letter = word[i];
+ }
+
+ return true;
}
/**
- * Mouse event - Mouse down
- * @param {Object} item
+ * @param {string} word
+ * @param {Direction} direction
+ * @returns {[number, number]}
*/
- WordSearch.prototype.onMousedown = function(item) {
- var _this = this;
- return function() {
- _this.selectFrom = item;
+ #randomStart(word, direction) {
+ const max = this.#settings.gridSize;
+ const len = word.length;
+
+ switch (direction) {
+ case 'RIGHT':
+ return [Random.integer(0, max - 1), Random.integer(0, max - len)];
+ case 'DOWN':
+ return [Random.integer(0, max - len), Random.integer(0, max - 1)];
+ case 'DIAG_RIGHT':
+ return [Random.integer(0, max - len), Random.integer(0, max - len)];
+ default:
+ return [Random.integer(0, max - len), Random.integer(len - 1, max - 1)];
+ }
+ }
+
+ #fillBlankCells() {
+ const first = this.#normalizedWords[0]?.[0] ?? 'A';
+ const [lo, hi] = Linguist.detectLanguageRange(first);
+
+ for (const row of this.#state.grid) {
+ for (const cell of row) {
+ if (cell.letter === '') {
+ cell.letter = String.fromCharCode(Random.integer(lo, hi));
+ }
+ }
+ }
+ }
+
+ #initializeCanvas() {
+ this.#canvas = new Canvas(this.#container, this.#settings);
+
+ new Handler(this.#canvas, this.#settings.gridSize, this.#state, () => this.#render(), {
+ onCanvasFocus: () => this.#updateStatus('board-focused'),
+ onKeyboardStart: () => this.#updateStatus('keyboard-started'),
+ onKeyboardCancel: () => this.#updateStatus('keyboard-cancelled'),
+ onSelection: (selection) => this.#lookupSelection(selection),
+ });
+
+ this.#render();
+ }
+
+ #render() {
+ if (this.#canvas) {
+ this.#canvas.render(this.#state);
}
}
/**
- * Mouse event - Mouse move
- * @param {Object}
+ * @param {GridPoint[]} selection
+ * @returns {void}
*/
- WordSearch.prototype.onMouseover = function(item) {
- var _this = this;
- return function() {
- if (_this.selectFrom) {
- _this.selected = _this.getItems(_this.selectFrom.row, _this.selectFrom.col, item.row, item.col);
+ #lookupSelection(selection) {
+ const cells = selection.map(({ row, col }) => this.#state.grid[row][col]);
+
+ if (this.#settings.debug && cells.some(cell => cell.letter === '')) {
+ return;
+ }
- _this.clearHighlight();
+ const letters = cells.map(c => c.letter).join('');
+ const reversed = [...letters].reverse().join('');
- for (var i = 0; i < _this.selected.length; i ++) {
- var current = _this.selected[i],
- row = current.row + 1,
- col = current.col + 1,
- el = document.querySelector('.ws-area .ws-row:nth-child(' + row + ') .ws-col:nth-child(' + col + ')');
+ const foundWord =
+ this.#normalizedWords.includes(letters) ? letters :
+ this.#normalizedWords.includes(reversed) ? reversed : null;
- el.className += ' ws-selected';
- }
- }
+ if (!foundWord || this.#state.solvedWords.has(foundWord)) {
+ return;
+ }
+
+ this.#state.solvedWords.add(foundWord);
+
+ for (const cell of cells) {
+ this.#state.founds.add(cell.row * this.#settings.gridSize + cell.col);
+ }
+
+ const index = this.#normalizedWords.indexOf(foundWord);
+ const displayWord = index === -1 ? foundWord : this.#settings.words[index];
+
+ this.#updateStatus('word-found', {
+ normalizedWord: foundWord,
+ displayWord,
+ solvedCount: this.#state.solvedWords.size,
+ totalWords: this.#normalizedWords.length,
+ });
+
+ if (this.#state.solvedWords.size === this.#normalizedWords.length) {
+ this.#completeGame();
}
}
/**
- * Mouse event - Mouse up
+ * @param {string} name
+ * @param {object} data
*/
- WordSearch.prototype.onMouseup = function() {
- var _this = this;
- return function() {
- _this.selectFrom = null;
- _this.clearHighlight();
- _this.lookup(_this.selected);
- _this.selected = [];
- }
- }
-
-})();
-//-----------------------------Remove accent for latin/hebrew letters---------------------------------------------------//
-var defaultDiacriticsRemovalMap = [{
- 'base': "A",
- 'letters': /(&#65;|&#9398;|&#65313;|&#192;|&#193;|&#194;|&#7846;|&#7844;|&#7850;|&#7848;|&#195;|&#256;|&#258;|&#7856;|&#7854;|&#7860;|&#7858;|&#550;|&#480;|&#196;|&#478;|&#7842;|&#197;|&#506;|&#461;|&#512;|&#514;|&#7840;|&#7852;|&#7862;|&#7680;|&#260;|&#570;|&#11375;|[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F])/g
-}, {
- 'base': "AA",
- 'letters': /(&#42802;|[\uA732])/g
-}, {
- 'base': "AE",
- 'letters': /(&#198;|&#508;|&#482;|[\u00C6\u01FC\u01E2])/g
-}, {
- 'base': "AO",
- 'letters': /(&#42804;|[\uA734])/g
-}, {
- 'base': "AU",
- 'letters': /(&#42806;|[\uA736])/g
-}, {
- 'base': "AV",
- 'letters': /(&#42808;|&#42810;|[\uA738\uA73A])/g
-}, {
- 'base': "AY",
- 'letters': /(&#42812;|[\uA73C])/g
-}, {
- 'base': "B",
- 'letters': /(&#66;|&#9399;|&#65314;|&#7682;|&#7684;|&#7686;|&#579;|&#386;|&#385;|[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181])/g
-}, {
- 'base': "C",
- 'letters': /(&#67;|&#9400;|&#65315;|&#262;|&#264;|&#266;|&#268;|&#199;|&#7688;|&#391;|&#571;|&#42814;|[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E])/g
-}, {
- 'base': "D",
- 'letters': /(&#68;|&#9401;|&#65316;|&#7690;|&#270;|&#7692;|&#7696;|&#7698;|&#7694;|&#272;|&#395;|&#394;|&#393;|&#42873;|&#208;|[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779\u00D0])/g
-}, {
- 'base': "DZ",
- 'letters': /(&#497;|&#452;|[\u01F1\u01C4])/g
-}, {
- 'base': "Dz",
- 'letters': /(&#498;|&#453;|[\u01F2\u01C5])/g
-}, {
- 'base': "E",
- 'letters': /(&#69;|&#9402;|&#65317;|&#200;|&#201;|&#202;|&#7872;|&#7870;|&#7876;|&#7874;|&#7868;|&#274;|&#7700;|&#7702;|&#276;|&#278;|&#203;|&#7866;|&#282;|&#516;|&#518;|&#7864;|&#7878;|&#552;|&#7708;|&#280;|&#7704;|&#7706;|&#400;|&#398;|[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E])/g
-}, {
- 'base': "F",
- 'letters': /(&#70;|&#9403;|&#65318;|&#7710;|&#401;|&#42875;|[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B])/g
-}, {
- 'base': "G",
- 'letters': /(&#71;|&#9404;|&#65319;|&#500;|&#284;|&#7712;|&#286;|&#288;|&#486;|&#290;|&#484;|&#403;|&#42912;|&#42877;|&#42878;|[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E])/g
-}, {
- 'base': "H",
- 'letters': /(&#72;|&#9405;|&#65320;|&#292;|&#7714;|&#7718;|&#542;|&#7716;|&#7720;|&#7722;|&#294;|&#11367;|&#11381;|&#42893;|[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D])/g
-}, {
- 'base': "I",
- 'letters': /(&#73;|&#9406;|&#65321;|&#204;|&#205;|&#206;|&#296;|&#298;|&#300;|&#304;|&#207;|&#7726;|&#7880;|&#463;|&#520;|&#522;|&#7882;|&#302;|&#7724;|&#407;|[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197])/g
-}, {
- 'base': "J",
- 'letters': /(&#74;|&#9407;|&#65322;|&#308;|&#584;|[\u004A\u24BF\uFF2A\u0134\u0248])/g
-}, {
- 'base': "K",
- 'letters': /(&#75;|&#9408;|&#65323;|&#7728;|&#488;|&#7730;|&#310;|&#7732;|&#408;|&#11369;|&#42816;|&#42818;|&#42820;|&#42914;|[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2])/g
-}, {
- 'base': "L",
- 'letters': /(&#76;|&#9409;|&#65324;|&#319;|&#313;|&#317;|&#7734;|&#7736;|&#315;|&#7740;|&#7738;|&#321;|&#573;|&#11362;|&#11360;|&#42824;|&#42822;|&#42880;|[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780])/g
-}, {
- 'base': "LJ",
- 'letters': /(&#455;|[\u01C7])/g
-}, {
- 'base': "Lj",
- 'letters': /(&#456;|[\u01C8])/g
-}, {
- 'base': "M",
- 'letters': /(&#77;|&#9410;|&#65325;|&#7742;|&#7744;|&#7746;|&#11374;|&#412;|[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C])/g
-}, {
- 'base': "N",
- 'letters': /(&#78;|&#9411;|&#65326;|&#504;|&#323;|&#209;|&#7748;|&#327;|&#7750;|&#325;|&#7754;|&#7752;|&#544;|&#413;|&#42896;|&#42916;|&#330;|[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4\u014A])/g
-}, {
- 'base': "NJ",
- 'letters': /(&#458;|[\u01CA])/g
-}, {
- 'base': "Nj",
- 'letters': /(&#459;|[\u01CB])/g
-}, {
- 'base': "O",
- 'letters': /(&#79;|&#9412;|&#65327;|&#210;|&#211;|&#212;|&#7890;|&#7888;|&#7894;|&#7892;|&#213;|&#7756;|&#556;|&#7758;|&#332;|&#7760;|&#7762;|&#334;|&#558;|&#560;|&#214;|&#554;|&#7886;|&#336;|&#465;|&#524;|&#526;|&#416;|&#7900;|&#7898;|&#7904;|&#7902;|&#7906;|&#7884;|&#7896;|&#490;|&#492;|&#216;|&#510;|&#390;|&#415;|&#42826;|&#42828;|[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C])/g
-}, {
- 'base': "OE",
- 'letters': /(&#338;|[\u0152])/g
-}, {
- 'base': "OI",
- 'letters': /(&#418;|[\u01A2])/g
-}, {
- 'base': "OO",
- 'letters': /(&#42830;|[\uA74E])/g
-}, {
- 'base': "OU",
- 'letters': /(&#546;|[\u0222])/g
-}, {
- 'base': "P",
- 'letters': /(&#80;|&#9413;|&#65328;|&#7764;|&#7766;|&#420;|&#11363;|&#42832;|&#42834;|&#42836;|[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754])/g
-}, {
- 'base': "Q",
- 'letters': /(&#81;|&#9414;|&#65329;|&#42838;|&#42840;|&#586;|[\u0051\u24C6\uFF31\uA756\uA758\u024A])/g
-}, {
- 'base': "R",
- 'letters': /(&#82;|&#9415;|&#65330;|&#340;|&#7768;|&#344;|&#528;|&#530;|&#7770;|&#7772;|&#342;|&#7774;|&#588;|&#11364;|&#42842;|&#42918;|&#42882;|[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782])/g
-}, {
- 'base': "S",
- 'letters': /(&#83;|&#9416;|&#65331;|&#7838;|&#346;|&#7780;|&#348;|&#7776;|&#352;|&#7782;|&#7778;|&#7784;|&#536;|&#350;|&#11390;|&#42920;|&#42884;|[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784])/g
-}, {
- 'base': "T",
- 'letters': /(&#84;|&#9417;|&#65332;|&#7786;|&#356;|&#7788;|&#538;|&#354;|&#7792;|&#7790;|&#358;|&#428;|&#430;|&#574;|&#42886;|[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786])/g
-}, {
- 'base': "TH",
- 'letters': /(&#222;|[\u00DE])/g
-}, {
- 'base': "TZ",
- 'letters': /(&#42792;|[\uA728])/g
-}, {
- 'base': "U",
- 'letters': /(&#85;|&#9418;|&#65333;|&#217;|&#218;|&#219;|&#360;|&#7800;|&#362;|&#7802;|&#364;|&#220;|&#475;|&#471;|&#469;|&#473;|&#7910;|&#366;|&#368;|&#467;|&#532;|&#534;|&#431;|&#7914;|&#7912;|&#7918;|&#7916;|&#7920;|&#7908;|&#7794;|&#370;|&#7798;|&#7796;|&#580;|[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244])/g
-}, {
- 'base': "V",
- 'letters': /(&#86;|&#9419;|&#65334;|&#7804;|&#7806;|&#434;|&#42846;|&#581;|[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245])/g
-}, {
- 'base': "VY",
- 'letters': /(&#42848;|[\uA760])/g
-}, {
- 'base': "W",
- 'letters': /(&#87;|&#9420;|&#65335;|&#7808;|&#7810;|&#372;|&#7814;|&#7812;|&#7816;|&#11378;|[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72])/g
-}, {
- 'base': "X",
- 'letters': /(&#88;|&#9421;|&#65336;|&#7818;|&#7820;|[\u0058\u24CD\uFF38\u1E8A\u1E8C])/g
-}, {
- 'base': "Y",
- 'letters': /(&#89;|&#9422;|&#65337;|&#7922;|&#221;|&#374;|&#7928;|&#562;|&#7822;|&#376;|&#7926;|&#7924;|&#435;|&#590;|&#7934;|[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE])/g
-}, {
- 'base': "Z",
- 'letters': /(&#90;|&#9423;|&#65338;|&#377;|&#7824;|&#379;|&#381;|&#7826;|&#7828;|&#437;|&#548;|&#11391;|&#11371;|&#42850;|[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762])/g
-}, {
- 'base': "", //delete Niqqud in Hebrew
- 'letters': /[\u0591-\u05C7]/g
-}]
-
-function removeDiacritics(str) {
- for (var i = 0; i < defaultDiacriticsRemovalMap.length; i++) {
- str = str.replace(defaultDiacriticsRemovalMap[i].letters, defaultDiacriticsRemovalMap[i].base);
- }
- return str;
+ #updateStatus(name, data = {}) {
+ this.#container.dispatchEvent(
+ new CustomEvent('wordsearch:status', { bubbles: true, detail: { name, ...data } })
+ );
+ }
+
+ #completeGame() {
+ this.#updateStatus('game-completed');
+ }
+
+ get container() {
+ return this.#container;
+ }
+
+ get words() {
+ return this.#settings.words.slice();
+ }
+
+ get normalizedWords() {
+ return this.#normalizedWords.slice();
+ }
}
-//------------------------------Search language--------------------------------------------------//
-// Determine what letters injected on grid
-function searchLanguage(firstLetter)
-{
- codefirstLetter = firstLetter.charCodeAt();
- var codeLetter = [65,90];
- if((codefirstLetter>=65) && (codefirstLetter<=90)) { // Latin
- return codeLetter = [65,90];
- }
- if((codefirstLetter>=1488) && (codefirstLetter<=1514)) { //Hebrew א -> ת
- return codeLetter = [1488,1514];
- }
- if((codefirstLetter>=913) && (codefirstLetter<=937)) { //Greek Α -> Ω
- return codeLetter = [913,929]; //930 is blank
- }
- if((codefirstLetter>=1040) && (codefirstLetter<=1071)) { //Cyrillic А -> Я
- return codeLetter = [1040,1071]; //930 is blank
- }
- if((codefirstLetter>=1569) && (codefirstLetter<=1610)) { //Arab
- return codeLetter = [1569,1594]; //Between 1595 and 1600, no letter
- }
- if((codefirstLetter>=19969) && (codefirstLetter<=40891)) { //Chinese
- return codeLetter = [19969,40891];
- }
- if((codefirstLetter>=12354) && (codefirstLetter<=12436)) { //Japan Hiragana
- return codeLetter = [12388,12418]; //Only no small letter
- }
- console.log("Letter not detected : "+firstLetter+":"+codefirstLetter);
- return codeLetter;
-
-
-} \ No newline at end of file
+
+export { WordSearch };
diff --git a/js/wordsearch.min.js b/js/wordsearch.min.js
deleted file mode 100644
index 57e0209..0000000
--- a/js/wordsearch.min.js
+++ /dev/null
@@ -1 +0,0 @@
-function removeDiacritics(e){for(var t=0;t<defaultDiacriticsRemovalMap.length;t++)e=e.replace(defaultDiacriticsRemovalMap[t].letters,defaultDiacriticsRemovalMap[t].base);return e}function searchLanguage(e){codefirstLetter=e.charCodeAt();var t=[65,90];return codefirstLetter>=65&&codefirstLetter<=90?t=[65,90]:codefirstLetter>=1488&&codefirstLetter<=1514?t=[1488,1514]:codefirstLetter>=913&&codefirstLetter<=937?t=[913,929]:codefirstLetter>=1040&&codefirstLetter<=1071?t=[1040,1071]:codefirstLetter>=1569&&codefirstLetter<=1610?t=[1569,1594]:codefirstLetter>=19969&&codefirstLetter<=40891?t=[19969,40891]:codefirstLetter>=12354&&codefirstLetter<=12436?t=[12388,12418]:(console.log("Letter not detected : "+e+":"+codefirstLetter),t)}!function(){"use strict";function e(e,t){this.wrapEl=e,this.wrapEl.classList.add("ws-area"),this.solved=0;var u={directions:["W","N","WN","EN"],gridSize:10,words:["one","two","three","four","five"],wordsList:[],debug:!1};if(this.settings=Object.merge(t,u),this.parseWords(this.settings.gridSize)){for(var s=!1;0==s;)this.initialize(),s=this.addWords();this.settings.debug||this.fillUpFools(),this.drawmatrix()}}Element.prototype.wordSearch=function(t){return new e(this,t)},e.prototype.parseWords=function(e){for(var t=!0,u=0;u<this.settings.words.length;u++){this.settings.wordsList[u]=this.settings.words[u].trim(),this.settings.words[u]=removeDiacritics(this.settings.wordsList[u].trim().toUpperCase());var s=this.settings.words[u];s.length>e&&(alert("The length of word `"+s+"` is overflow the gridSize."),console.error("The length of word `"+s+"` is overflow the gridSize."),t=!1)}return t},e.prototype.addWords=function(){for(var e=!0,t=0,u=!0;e;){var s=this.settings.directions[Math.rangeInt(this.settings.directions.length-1)],r=this.addWord(this.settings.words[t],s),u=!0;0==r&&(e=!1,u=!1),t++,t>=this.settings.words.length&&(e=!1)}return u},e.prototype.addWord=function(e,t){var u,s,r=!0,i={W:[0,1],N:[1,0],WN:[1,1],EN:[1,-1]};switch(t){case"W":var u=Math.rangeInt(this.settings.gridSize-1),s=Math.rangeInt(this.settings.gridSize-e.length);break;case"N":var u=Math.rangeInt(this.settings.gridSize-e.length),s=Math.rangeInt(this.settings.gridSize-1);break;case"WN":var u=Math.rangeInt(this.settings.gridSize-e.length),s=Math.rangeInt(this.settings.gridSize-e.length);break;case"EN":var u=Math.rangeInt(this.settings.gridSize-e.length),s=Math.rangeInt(e.length-1,this.settings.gridSize-1);break;default:var o="UNKNOWN DIRECTION "+t+"!";alert(o),console.log(o)}for(var a=0;a<e.length;a++){var n=u+a*i[t][0],E=s+a*i[t][1],l=this.matrix[n][E].letter;"."==l||l==e[a]?this.matrix[n][E].letter=e[a]:r=!1}return r},e.prototype.initialize=function(){this.matrix=[],this.selectFrom=null,this.selected=[],this.initmatrix(this.settings.gridSize)},e.prototype.initmatrix=function(e){for(var t=0;e>t;t++)for(var u=0;e>u;u++){var s={letter:".",row:t,col:u};this.matrix[t]||(this.matrix[t]=[]),this.matrix[t][u]=s}},e.prototype.drawmatrix=function(){for(var e=0;e<this.settings.gridSize;e++){var t=document.createElement("div");t.setAttribute("class","ws-row"),this.wrapEl.appendChild(t);for(var u=0;u<this.settings.gridSize;u++){var s=document.createElement("canvas");s.setAttribute("class","ws-col"),s.setAttribute("width",40),s.setAttribute("height",40);var r=s.width/2,i=s.height/2,o=s.getContext("2d");o.font="400 28px Calibri",o.textAlign="center",o.textBaseline="middle",o.fillStyle="#333",o.fillText(this.matrix[e][u].letter,r,i),s.addEventListener("mousedown",this.onMousedown(this.matrix[e][u])),s.addEventListener("mouseover",this.onMouseover(this.matrix[e][u])),s.addEventListener("mouseup",this.onMouseup()),t.appendChild(s)}}},e.prototype.fillUpFools=function(){for(var e=searchLanguage(this.settings.words[0].split("")[0]),t=0;t<this.settings.gridSize;t++)for(var u=0;u<this.settings.gridSize;u++)"."==this.matrix[t][u].letter&&(this.matrix[t][u].letter=String.fromCharCode(Math.rangeInt(e[0],e[1])))},e.prototype.getItems=function(e,t,u,s){var r=[];if(e===u||t===s||Math.abs(u-e)==Math.abs(s-t)){var i=e===u?0:u>e?1:-1,o=t===s?0:s>t?1:-1,a=e,n=t;r.push(this.getItem(a,n));do a+=i,n+=o,r.push(this.getItem(a,n));while(a!==u||n!==s)}return r},e.prototype.getItem=function(e,t){return this.matrix[e]?this.matrix[e][t]:void 0},e.prototype.clearHighlight=function(){for(var e=document.querySelectorAll(".ws-selected"),t=0;t<e.length;t++)e[t].classList.remove("ws-selected")},e.prototype.lookup=function(e){for(var t=[""],u=0;u<e.length;u++)t[0]+=e[u].letter;if(t.push(t[0].split("").reverse().join("")),this.settings.words.indexOf(t[0])>-1||this.settings.words.indexOf(t[1])>-1){for(var u=0;u<e.length;u++){var s=e[u].row+1,r=e[u].col+1,i=document.querySelector(".ws-area .ws-row:nth-child("+s+") .ws-col:nth-child("+r+")");i.classList.add("ws-found")}for(var o=document.querySelector(".ws-words"),a=o.getElementsByTagName("li"),u=0;u<a.length;u++)t[0]==removeDiacritics(a[u].innerHTML.toUpperCase())&&a[u].innerHTML!="<del>"+a[u].innerHTML+"</del>"&&(a[u].innerHTML="<del>"+a[u].innerHTML+"</del>",this.solved++);this.solved==this.settings.words.length&&this.gameOver()}},e.prototype.gameOver=function(){var e=document.createElement("div");e.setAttribute("id","ws-game-over-outer"),e.setAttribute("class","ws-game-over-outer"),this.wrapEl.parentNode.appendChild(e);var e=document.getElementById("ws-game-over-outer");e.innerHTML="<div class='ws-game-over-inner' id='ws-game-over-inner'><div class='ws-game-over' id='ws-game-over'><h2>Congratulations!</h2><p>You've found all of the words!</p></div></div>"},e.prototype.onMousedown=function(e){var t=this;return function(){t.selectFrom=e}},e.prototype.onMouseover=function(e){var t=this;return function(){if(t.selectFrom){t.selected=t.getItems(t.selectFrom.row,t.selectFrom.col,e.row,e.col),t.clearHighlight();for(var u=0;u<t.selected.length;u++){var s=t.selected[u],r=s.row+1,i=s.col+1,o=document.querySelector(".ws-area .ws-row:nth-child("+r+") .ws-col:nth-child("+i+")");o.className+=" ws-selected"}}}},e.prototype.onMouseup=function(){var e=this;return function(){e.selectFrom=null,e.clearHighlight(),e.lookup(e.selected),e.selected=[]}}}();var defaultDiacriticsRemovalMap=[{base:"A",letters:/(&#65;|&#9398;|&#65313;|&#192;|&#193;|&#194;|&#7846;|&#7844;|&#7850;|&#7848;|&#195;|&#256;|&#258;|&#7856;|&#7854;|&#7860;|&#7858;|&#550;|&#480;|&#196;|&#478;|&#7842;|&#197;|&#506;|&#461;|&#512;|&#514;|&#7840;|&#7852;|&#7862;|&#7680;|&#260;|&#570;|&#11375;|[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F])/g},{base:"AA",letters:/(&#42802;|[\uA732])/g},{base:"AE",letters:/(&#198;|&#508;|&#482;|[\u00C6\u01FC\u01E2])/g},{base:"AO",letters:/(&#42804;|[\uA734])/g},{base:"AU",letters:/(&#42806;|[\uA736])/g},{base:"AV",letters:/(&#42808;|&#42810;|[\uA738\uA73A])/g},{base:"AY",letters:/(&#42812;|[\uA73C])/g},{base:"B",letters:/(&#66;|&#9399;|&#65314;|&#7682;|&#7684;|&#7686;|&#579;|&#386;|&#385;|[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181])/g},{base:"C",letters:/(&#67;|&#9400;|&#65315;|&#262;|&#264;|&#266;|&#268;|&#199;|&#7688;|&#391;|&#571;|&#42814;|[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E])/g},{base:"D",letters:/(&#68;|&#9401;|&#65316;|&#7690;|&#270;|&#7692;|&#7696;|&#7698;|&#7694;|&#272;|&#395;|&#394;|&#393;|&#42873;|&#208;|[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779\u00D0])/g},{base:"DZ",letters:/(&#497;|&#452;|[\u01F1\u01C4])/g},{base:"Dz",letters:/(&#498;|&#453;|[\u01F2\u01C5])/g},{base:"E",letters:/(&#69;|&#9402;|&#65317;|&#200;|&#201;|&#202;|&#7872;|&#7870;|&#7876;|&#7874;|&#7868;|&#274;|&#7700;|&#7702;|&#276;|&#278;|&#203;|&#7866;|&#282;|&#516;|&#518;|&#7864;|&#7878;|&#552;|&#7708;|&#280;|&#7704;|&#7706;|&#400;|&#398;|[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E])/g},{base:"F",letters:/(&#70;|&#9403;|&#65318;|&#7710;|&#401;|&#42875;|[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B])/g},{base:"G",letters:/(&#71;|&#9404;|&#65319;|&#500;|&#284;|&#7712;|&#286;|&#288;|&#486;|&#290;|&#484;|&#403;|&#42912;|&#42877;|&#42878;|[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E])/g},{base:"H",letters:/(&#72;|&#9405;|&#65320;|&#292;|&#7714;|&#7718;|&#542;|&#7716;|&#7720;|&#7722;|&#294;|&#11367;|&#11381;|&#42893;|[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D])/g},{base:"I",letters:/(&#73;|&#9406;|&#65321;|&#204;|&#205;|&#206;|&#296;|&#298;|&#300;|&#304;|&#207;|&#7726;|&#7880;|&#463;|&#520;|&#522;|&#7882;|&#302;|&#7724;|&#407;|[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197])/g},{base:"J",letters:/(&#74;|&#9407;|&#65322;|&#308;|&#584;|[\u004A\u24BF\uFF2A\u0134\u0248])/g},{base:"K",letters:/(&#75;|&#9408;|&#65323;|&#7728;|&#488;|&#7730;|&#310;|&#7732;|&#408;|&#11369;|&#42816;|&#42818;|&#42820;|&#42914;|[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2])/g},{base:"L",letters:/(&#76;|&#9409;|&#65324;|&#319;|&#313;|&#317;|&#7734;|&#7736;|&#315;|&#7740;|&#7738;|&#321;|&#573;|&#11362;|&#11360;|&#42824;|&#42822;|&#42880;|[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780])/g},{base:"LJ",letters:/(&#455;|[\u01C7])/g},{base:"Lj",letters:/(&#456;|[\u01C8])/g},{base:"M",letters:/(&#77;|&#9410;|&#65325;|&#7742;|&#7744;|&#7746;|&#11374;|&#412;|[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C])/g},{base:"N",letters:/(&#78;|&#9411;|&#65326;|&#504;|&#323;|&#209;|&#7748;|&#327;|&#7750;|&#325;|&#7754;|&#7752;|&#544;|&#413;|&#42896;|&#42916;|&#330;|[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4\u014A])/g},{base:"NJ",letters:/(&#458;|[\u01CA])/g},{base:"Nj",letters:/(&#459;|[\u01CB])/g},{base:"O",letters:/(&#79;|&#9412;|&#65327;|&#210;|&#211;|&#212;|&#7890;|&#7888;|&#7894;|&#7892;|&#213;|&#7756;|&#556;|&#7758;|&#332;|&#7760;|&#7762;|&#334;|&#558;|&#560;|&#214;|&#554;|&#7886;|&#336;|&#465;|&#524;|&#526;|&#416;|&#7900;|&#7898;|&#7904;|&#7902;|&#7906;|&#7884;|&#7896;|&#490;|&#492;|&#216;|&#510;|&#390;|&#415;|&#42826;|&#42828;|[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C])/g},{base:"OE",letters:/(&#338;|[\u0152])/g},{base:"OI",letters:/(&#418;|[\u01A2])/g},{base:"OO",letters:/(&#42830;|[\uA74E])/g},{base:"OU",letters:/(&#546;|[\u0222])/g},{base:"P",letters:/(&#80;|&#9413;|&#65328;|&#7764;|&#7766;|&#420;|&#11363;|&#42832;|&#42834;|&#42836;|[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754])/g},{base:"Q",letters:/(&#81;|&#9414;|&#65329;|&#42838;|&#42840;|&#586;|[\u0051\u24C6\uFF31\uA756\uA758\u024A])/g},{base:"R",letters:/(&#82;|&#9415;|&#65330;|&#340;|&#7768;|&#344;|&#528;|&#530;|&#7770;|&#7772;|&#342;|&#7774;|&#588;|&#11364;|&#42842;|&#42918;|&#42882;|[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782])/g},{base:"S",letters:/(&#83;|&#9416;|&#65331;|&#7838;|&#346;|&#7780;|&#348;|&#7776;|&#352;|&#7782;|&#7778;|&#7784;|&#536;|&#350;|&#11390;|&#42920;|&#42884;|[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784])/g},{base:"T",letters:/(&#84;|&#9417;|&#65332;|&#7786;|&#356;|&#7788;|&#538;|&#354;|&#7792;|&#7790;|&#358;|&#428;|&#430;|&#574;|&#42886;|[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786])/g},{base:"TH",letters:/(&#222;|[\u00DE])/g},{base:"TZ",letters:/(&#42792;|[\uA728])/g},{base:"U",letters:/(&#85;|&#9418;|&#65333;|&#217;|&#218;|&#219;|&#360;|&#7800;|&#362;|&#7802;|&#364;|&#220;|&#475;|&#471;|&#469;|&#473;|&#7910;|&#366;|&#368;|&#467;|&#532;|&#534;|&#431;|&#7914;|&#7912;|&#7918;|&#7916;|&#7920;|&#7908;|&#7794;|&#370;|&#7798;|&#7796;|&#580;|[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244])/g},{base:"V",letters:/(&#86;|&#9419;|&#65334;|&#7804;|&#7806;|&#434;|&#42846;|&#581;|[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245])/g},{base:"VY",letters:/(&#42848;|[\uA760])/g},{base:"W",letters:/(&#87;|&#9420;|&#65335;|&#7808;|&#7810;|&#372;|&#7814;|&#7812;|&#7816;|&#11378;|[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72])/g},{base:"X",letters:/(&#88;|&#9421;|&#65336;|&#7818;|&#7820;|[\u0058\u24CD\uFF38\u1E8A\u1E8C])/g},{base:"Y",letters:/(&#89;|&#9422;|&#65337;|&#7922;|&#221;|&#374;|&#7928;|&#562;|&#7822;|&#376;|&#7926;|&#7924;|&#435;|&#590;|&#7934;|[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE])/g},{base:"Z",letters:/(&#90;|&#9423;|&#65338;|&#377;|&#7824;|&#379;|&#381;|&#7826;|&#7828;|&#437;|&#548;|&#11391;|&#11371;|&#42850;|[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762])/g},{base:"",letters:/[\u0591-\u05C7]/g}]; \ No newline at end of file