@scrabble-solver/scrabble-solver 2.8.9 → 2.8.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +11 -11
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/eslint/.cache_8dgz12 +1 -1
  5. package/.next/cache/next-server.js.nft.json +1 -1
  6. package/.next/cache/webpack/client-production/0.pack +0 -0
  7. package/.next/cache/webpack/client-production/index.pack +0 -0
  8. package/.next/cache/webpack/server-production/0.pack +0 -0
  9. package/.next/cache/webpack/server-production/index.pack +0 -0
  10. package/.next/next-server.js.nft.json +1 -1
  11. package/.next/prerender-manifest.json +1 -1
  12. package/.next/routes-manifest.json +1 -1
  13. package/.next/server/chunks/413.js +189 -43
  14. package/.next/server/chunks/429.js +2 -13
  15. package/.next/server/chunks/515.js +469 -254
  16. package/.next/server/chunks/911.js +25 -3
  17. package/.next/server/middleware-build-manifest.js +1 -1
  18. package/.next/server/pages/404.html +2 -2
  19. package/.next/server/pages/404.js.nft.json +1 -1
  20. package/.next/server/pages/500.html +2 -2
  21. package/.next/server/pages/_app.js.nft.json +1 -1
  22. package/.next/server/pages/api/solve.js +77 -20
  23. package/.next/server/pages/index.html +3 -3
  24. package/.next/server/pages/index.js +1 -1
  25. package/.next/server/pages/index.js.nft.json +1 -1
  26. package/.next/server/pages/index.json +1 -1
  27. package/.next/static/VJkrGviICslA_8zNVJ-g-/_buildManifest.js +1 -0
  28. package/.next/static/{yCxjzzYpw5JjJE53PO_s6 → VJkrGviICslA_8zNVJ-g-}/_ssgManifest.js +0 -0
  29. package/.next/static/chunks/317-8e8909dd2f587b64.js +1 -0
  30. package/.next/static/chunks/546-447e243fc9de2c59.js +1 -0
  31. package/.next/static/chunks/pages/{404-90c624da3c83fd17.js → 404-7082923654d5996f.js} +1 -1
  32. package/.next/static/chunks/pages/_app-57c77cad0f197d93.js +1 -0
  33. package/.next/static/chunks/pages/index-d3360e075ca3c222.js +1 -0
  34. package/.next/static/css/9ac903004135f4b1.css +1 -0
  35. package/.next/trace +42 -42
  36. package/package.json +12 -12
  37. package/src/components/Badge/Badge.module.scss +1 -1
  38. package/src/components/Board/Board.tsx +4 -2
  39. package/src/components/Board/BoardPure.tsx +25 -5
  40. package/src/components/Board/components/Cell/CellPure.tsx +33 -31
  41. package/src/components/Board/hooks/useGrid.ts +217 -91
  42. package/src/components/Dictionary/Dictionary.tsx +8 -1
  43. package/src/components/Rack/Rack.tsx +51 -11
  44. package/src/components/Rack/RackTile.tsx +33 -16
  45. package/src/components/Results/Results.tsx +19 -3
  46. package/src/components/Sidebar/Sidebar.tsx +20 -1
  47. package/src/components/SquareButton/Link.tsx +1 -1
  48. package/src/components/Tile/Tile.module.scss +4 -0
  49. package/src/components/Tile/Tile.tsx +13 -4
  50. package/src/components/Tile/TilePure.tsx +3 -4
  51. package/src/lib/extractCharacters.ts +26 -0
  52. package/src/lib/extractInputValue.ts +17 -0
  53. package/src/lib/index.ts +2 -0
  54. package/src/lib/isCtrl.ts +1 -1
  55. package/src/lib/memoize.ts +15 -1
  56. package/src/pages/api/solve.ts +1 -1
  57. package/src/sdk/fetchJson.ts +36 -0
  58. package/src/sdk/findWordDefinitions.ts +4 -3
  59. package/src/sdk/solve.ts +8 -7
  60. package/src/sdk/verify.ts +5 -6
  61. package/src/state/sagas.ts +9 -3
  62. package/src/state/selectors.ts +9 -1
  63. package/src/state/slices/dictionaryInitialState.ts +10 -2
  64. package/src/state/slices/dictionarySlice.ts +10 -16
  65. package/src/state/slices/rackSlice.ts +7 -5
  66. package/src/state/slices/solveInitialState.ts +14 -2
  67. package/src/state/slices/solveSlice.ts +7 -4
  68. package/src/types/index.ts +2 -0
  69. package/.next/static/chunks/317-a33dd38e9b9a17ed.js +0 -1
  70. package/.next/static/chunks/758-f333b1dcdb941547.js +0 -1
  71. package/.next/static/chunks/pages/_app-f8f360878e1c2aff.js +0 -1
  72. package/.next/static/chunks/pages/index-ecea697d3e5d8a6f.js +0 -1
  73. package/.next/static/css/64dc2ce1811912f1.css +0 -1
  74. package/.next/static/yCxjzzYpw5JjJE53PO_s6/_buildManifest.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrabble-solver/scrabble-solver",
3
- "version": "2.8.9",
3
+ "version": "2.8.11",
4
4
  "description": "Scrabble Solver 2 - App",
5
5
  "engines": {
6
6
  "node": ">=16"
@@ -29,20 +29,20 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@popperjs/core": "^2.11.6",
32
- "@reduxjs/toolkit": "^1.8.5",
33
- "@scrabble-solver/configs": "^2.8.9",
34
- "@scrabble-solver/constants": "^2.8.9",
35
- "@scrabble-solver/dictionaries": "^2.8.9",
36
- "@scrabble-solver/logger": "^2.8.9",
37
- "@scrabble-solver/solver": "^2.8.9",
38
- "@scrabble-solver/types": "^2.8.9",
39
- "@scrabble-solver/word-definitions": "^2.8.9",
32
+ "@reduxjs/toolkit": "^1.8.6",
33
+ "@scrabble-solver/configs": "^2.8.11",
34
+ "@scrabble-solver/constants": "^2.8.11",
35
+ "@scrabble-solver/dictionaries": "^2.8.11",
36
+ "@scrabble-solver/logger": "^2.8.11",
37
+ "@scrabble-solver/solver": "^2.8.11",
38
+ "@scrabble-solver/types": "^2.8.11",
39
+ "@scrabble-solver/word-definitions": "^2.8.11",
40
40
  "classnames": "^2.3.2",
41
41
  "next": "^12.3.1",
42
42
  "normalize.css": "^8.0.1",
43
43
  "react": "^18.2.0",
44
44
  "react-dom": "^18.2.0",
45
- "react-modal": "^3.15.1",
45
+ "react-modal": "^3.16.1",
46
46
  "react-popper": "^2.3.0",
47
47
  "react-portal": "^4.2.2",
48
48
  "react-redux": "^8.0.4",
@@ -54,7 +54,7 @@
54
54
  "uuid": "^9.0.0"
55
55
  },
56
56
  "devDependencies": {
57
- "@svgr/webpack": "^6.4.0",
57
+ "@svgr/webpack": "^6.5.0",
58
58
  "@types/classnames": "^2.3.0",
59
59
  "@types/react": "^18.0.21",
60
60
  "@types/react-dom": "^18.0.6",
@@ -68,5 +68,5 @@
68
68
  "env-cmd": "^10.1.0",
69
69
  "sass": "^1.55.0"
70
70
  },
71
- "gitHead": "8557cc2c5214e6689a5c59373b228b28f5dd8ed4"
71
+ "gitHead": "ab4bacd7711b47e2392da417225559effe574252"
72
72
  }
@@ -4,7 +4,7 @@
4
4
  justify-content: center;
5
5
  padding: var(--spacing--xs) var(--spacing--s);
6
6
  border-radius: var(--border--radius);
7
- background-color: var(--color--violet--light);
7
+ background-color: var(--color--foreground--secondary);
8
8
  box-shadow: va(--box-shadow);
9
9
  line-height: var(--line-height);
10
10
  font-size: var(--font--size--s);
@@ -14,20 +14,22 @@ interface Props {
14
14
  const Board: FunctionComponent<Props> = ({ cellSize, className, innerRef }) => {
15
15
  const rows = useTypedSelector(selectRowsWithCandidate);
16
16
  const board = useTypedSelector(selectBoard);
17
- const [{ lastDirection, refs }, { onDirectionToggle, onFocus, onKeyDown }] = useGrid(rows);
17
+ const [{ direction, refs }, { onChange, onDirectionToggle, onFocus, onKeyDown, onPaste }] = useGrid(rows);
18
18
 
19
19
  return (
20
20
  <BoardPure
21
21
  className={className}
22
22
  cellSize={cellSize}
23
23
  center={board.center}
24
+ direction={direction}
24
25
  innerRef={innerRef}
25
- lastDirection={lastDirection}
26
26
  refs={refs}
27
27
  rows={rows}
28
+ onChange={onChange}
28
29
  onDirectionToggle={onDirectionToggle}
29
30
  onFocus={onFocus}
30
31
  onKeyDown={onKeyDown}
32
+ onPaste={onPaste}
31
33
  />
32
34
  );
33
35
  };
@@ -1,6 +1,16 @@
1
1
  import { Cell } from '@scrabble-solver/types';
2
2
  import classNames from 'classnames';
3
- import { FunctionComponent, KeyboardEventHandler, memo, Ref, RefObject } from 'react';
3
+ import {
4
+ ChangeEventHandler,
5
+ ClipboardEventHandler,
6
+ FunctionComponent,
7
+ KeyboardEventHandler,
8
+ memo,
9
+ Ref,
10
+ RefObject,
11
+ } from 'react';
12
+
13
+ import { Direction } from 'types';
4
14
 
5
15
  import styles from './Board.module.scss';
6
16
  import { Cell as CellComponent } from './components';
@@ -9,35 +19,45 @@ interface Props {
9
19
  className?: string;
10
20
  cellSize: number;
11
21
  center: Cell;
22
+ direction: Direction;
12
23
  innerRef?: Ref<HTMLDivElement>;
13
- lastDirection: 'horizontal' | 'vertical';
14
24
  refs: RefObject<HTMLInputElement>[][];
15
25
  rows: Cell[][];
26
+ onChange: ChangeEventHandler<HTMLInputElement>;
16
27
  onDirectionToggle: () => void;
17
28
  onFocus: (x: number, y: number) => void;
18
29
  onKeyDown: KeyboardEventHandler<HTMLInputElement>;
30
+ onPaste: ClipboardEventHandler<HTMLInputElement>;
19
31
  }
20
32
 
21
33
  const BoardPure: FunctionComponent<Props> = ({
22
34
  className,
23
35
  cellSize,
24
36
  center,
37
+ direction,
25
38
  innerRef,
26
- lastDirection,
27
39
  refs,
28
40
  rows,
41
+ onChange,
29
42
  onDirectionToggle,
30
43
  onFocus,
31
44
  onKeyDown,
45
+ onPaste,
32
46
  }) => (
33
- <div className={classNames(styles.board, className)} ref={innerRef} onKeyDown={onKeyDown}>
47
+ <div
48
+ className={classNames(styles.board, className)}
49
+ ref={innerRef}
50
+ onChange={onChange}
51
+ onKeyDown={onKeyDown}
52
+ onPaste={onPaste}
53
+ >
34
54
  {rows.map((cells, y) => (
35
55
  <div className={styles.row} key={y}>
36
56
  {cells.map((cell, x) => (
37
57
  <CellComponent
38
58
  className={styles.cell}
39
59
  cell={cell}
40
- direction={lastDirection}
60
+ direction={direction}
41
61
  inputRef={refs[y][x]}
42
62
  isCenter={center.x === x && center.y === y}
43
63
  key={x}
@@ -81,39 +81,41 @@ const CellPure: FunctionComponent<Props> = ({
81
81
  onFocus={onFocus}
82
82
  />
83
83
 
84
- <div className={styles.actions}>
85
- <Button tooltip={translate('cell.toggle-direction')} onClick={onDirectionToggleClick}>
86
- <ArrowDown
87
- className={classNames(styles.toggleDirection, {
88
- [styles.right]: direction === 'horizontal',
89
- })}
90
- />
91
- </Button>
92
-
93
- {isEmpty && (
94
- <Button
95
- className={classNames(styles.filterCell, {
96
- [styles.filtered]: isFiltered,
97
- })}
98
- tooltip={translate('cell.filter-cell')}
99
- onClick={onToggleFilterCellClick}
100
- >
101
- <Flag />
84
+ {!cell.isCandidate() && (
85
+ <div className={styles.actions}>
86
+ <Button tooltip={translate('cell.toggle-direction')} onClick={onDirectionToggleClick}>
87
+ <ArrowDown
88
+ className={classNames(styles.toggleDirection, {
89
+ [styles.right]: direction === 'horizontal',
90
+ })}
91
+ />
102
92
  </Button>
103
- )}
104
93
 
105
- {!isEmpty && (
106
- <Button
107
- className={classNames(styles.blank, {
108
- [styles.active]: tile.isBlank,
109
- })}
110
- tooltip={tile.isBlank ? translate('cell.set-not-blank') : translate('cell.set-blank')}
111
- onClick={onToggleBlankClick}
112
- >
113
- B
114
- </Button>
115
- )}
116
- </div>
94
+ {isEmpty && (
95
+ <Button
96
+ className={classNames(styles.filterCell, {
97
+ [styles.filtered]: isFiltered,
98
+ })}
99
+ tooltip={translate('cell.filter-cell')}
100
+ onClick={onToggleFilterCellClick}
101
+ >
102
+ <Flag />
103
+ </Button>
104
+ )}
105
+
106
+ {!isEmpty && (
107
+ <Button
108
+ className={classNames(styles.blank, {
109
+ [styles.active]: tile.isBlank,
110
+ })}
111
+ tooltip={tile.isBlank ? translate('cell.set-not-blank') : translate('cell.set-blank')}
112
+ onClick={onToggleBlankClick}
113
+ >
114
+ B
115
+ </Button>
116
+ )}
117
+ </div>
118
+ )}
117
119
  </div>
118
120
  );
119
121
 
@@ -1,25 +1,42 @@
1
1
  /* eslint-disable max-lines, max-statements */
2
- import { EMPTY_CELL } from '@scrabble-solver/constants';
3
- import { Cell } from '@scrabble-solver/types';
4
- import { createRef, KeyboardEventHandler, RefObject, useCallback, useMemo, useState, useRef } from 'react';
2
+ import { BLANK, EMPTY_CELL } from '@scrabble-solver/constants';
3
+ import { Board, Cell } from '@scrabble-solver/types';
4
+ import {
5
+ createRef,
6
+ KeyboardEventHandler,
7
+ RefObject,
8
+ useCallback,
9
+ useMemo,
10
+ useState,
11
+ useRef,
12
+ ChangeEventHandler,
13
+ ChangeEvent,
14
+ ClipboardEventHandler,
15
+ } from 'react';
5
16
  import { useDispatch } from 'react-redux';
6
17
  import { useLatest } from 'react-use';
18
+ import { AnyAction } from 'redux';
7
19
 
8
- import { createGridOf, createKeyboardNavigation, isCtrl } from 'lib';
20
+ import { createGridOf, createKeyboardNavigation, extractCharacters, extractInputValue, isCtrl } from 'lib';
9
21
  import { boardSlice, selectConfig, useTypedSelector } from 'state';
22
+ import { Direction } from 'types';
10
23
 
11
24
  import { getPositionInGrid } from '../lib';
12
25
  import { Point } from '../types';
13
26
 
27
+ const toggleDirection = (direction: Direction) => (direction === 'vertical' ? 'horizontal' : 'vertical');
28
+
14
29
  interface State {
15
- lastDirection: 'horizontal' | 'vertical';
30
+ direction: Direction;
16
31
  refs: RefObject<HTMLInputElement>[][];
17
32
  }
18
33
 
19
34
  interface Actions {
20
- onFocus: (x: number, y: number) => void;
35
+ onChange: ChangeEventHandler<HTMLInputElement>;
21
36
  onDirectionToggle: () => void;
37
+ onFocus: (x: number, y: number) => void;
22
38
  onKeyDown: KeyboardEventHandler<HTMLInputElement>;
39
+ onPaste: ClipboardEventHandler<HTMLInputElement>;
23
40
  }
24
41
 
25
42
  const useGrid = (rows: Cell[][]): [State, Actions] => {
@@ -32,8 +49,8 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
32
49
  [width, height],
33
50
  );
34
51
  const activeIndexRef = useRef<Point>({ x: 0, y: 0 });
35
- const [lastDirection, setLastDirection] = useState<'horizontal' | 'vertical'>('horizontal');
36
- const lastDirectionRef = useLatest(lastDirection);
52
+ const [direction, setLastDirection] = useState<Direction>('horizontal');
53
+ const directionRef = useLatest(direction);
37
54
 
38
55
  const changeActiveIndex = useCallback(
39
56
  (offsetX: number, offsetY: number) => {
@@ -52,32 +69,160 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
52
69
  [refs],
53
70
  );
54
71
 
55
- const onDirectionToggle = useCallback(() => {
56
- setLastDirection((direction) => {
57
- return direction === 'vertical' ? 'horizontal' : 'vertical';
58
- });
59
- }, []);
60
-
61
- const onFocus = useCallback((x: number, y: number) => {
62
- activeIndexRef.current = { x, y };
63
- }, []);
64
-
65
- const onMoveFocus = useCallback(
66
- (direction: 'backward' | 'forward') => {
67
- const offset = direction === 'forward' ? 1 : -1;
68
-
69
- if (lastDirectionRef.current === 'horizontal') {
72
+ const moveFocus = useCallback(
73
+ (offset: number) => {
74
+ if (directionRef.current === 'horizontal') {
70
75
  changeActiveIndex(offset, 0);
71
76
  } else {
72
77
  changeActiveIndex(0, offset);
73
78
  }
74
79
  },
75
- [changeActiveIndex, lastDirectionRef],
80
+ [changeActiveIndex, directionRef],
81
+ );
82
+
83
+ const insertValue = useCallback(
84
+ (position: Point, value: string) => {
85
+ const characters = value ? extractCharacters(config, value).filter((character) => character !== BLANK) : [BLANK];
86
+ const actions: AnyAction[] = [];
87
+ let board = new Board({ rows: rows.map((row) => row.map((cell) => cell.clone())) });
88
+ let { x, y } = position;
89
+
90
+ const scheduleMoveFocus = () => {
91
+ if (directionRef.current === 'horizontal') {
92
+ ++x;
93
+ } else {
94
+ ++y;
95
+ }
96
+ };
97
+
98
+ characters.forEach((character) => {
99
+ if (x >= config.boardWidth || y >= config.boardHeight) {
100
+ return;
101
+ }
102
+
103
+ const canCheckUp = y - 1 > 0;
104
+ const canCheckLeft = x > 0;
105
+ const canCheckRight = x + 1 < width;
106
+ const canCheckDown = y + 1 < height;
107
+
108
+ if (canCheckUp) {
109
+ const cellUp = board.rows[y - 1][x];
110
+ const twoCharacterCandidate = cellUp.tile.character + character;
111
+
112
+ if (!cellUp.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
113
+ const action = boardSlice.actions.changeCellValue({ x, y: y - 1, value: twoCharacterCandidate });
114
+ board = boardSlice.reducer(board, action);
115
+ actions.push(action);
116
+ return;
117
+ }
118
+ }
119
+
120
+ if (canCheckDown) {
121
+ const cellDown = board.rows[y + 1][x];
122
+ const twoCharacterCandidate = character + cellDown.tile.character;
123
+
124
+ if (!cellDown.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
125
+ const action1 = boardSlice.actions.changeCellValue({ x, y, value: character });
126
+ const action2 = boardSlice.actions.changeCellValue({ x, y: y + 1, value: EMPTY_CELL });
127
+ board = boardSlice.reducer(boardSlice.reducer(board, action1), action2);
128
+ actions.push(action1, action2);
129
+ scheduleMoveFocus();
130
+ return;
131
+ }
132
+ }
133
+
134
+ if (canCheckLeft) {
135
+ const cellLeft = board.rows[y][x - 1];
136
+ const twoCharacterCandidate = cellLeft.tile.character + character;
137
+
138
+ if (!cellLeft.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
139
+ const action = boardSlice.actions.changeCellValue({ x: x - 1, y, value: twoCharacterCandidate });
140
+ board = boardSlice.reducer(board, action);
141
+ actions.push(action);
142
+ return;
143
+ }
144
+ }
145
+
146
+ if (canCheckRight) {
147
+ const cellRight = board.rows[y][x + 1];
148
+ const twoCharacterCandidate = character + cellRight.tile.character;
149
+
150
+ if (!cellRight.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
151
+ const action1 = boardSlice.actions.changeCellValue({ x, y, value: character });
152
+ const action2 = boardSlice.actions.changeCellValue({ x: x + 1, y, value: EMPTY_CELL });
153
+ board = boardSlice.reducer(boardSlice.reducer(board, action1), action2);
154
+ actions.push(action1, action2);
155
+ scheduleMoveFocus();
156
+ return;
157
+ }
158
+ }
159
+
160
+ if (!canCheckDown || !canCheckRight) {
161
+ const cell = board.rows[y][x];
162
+ const twoCharacterCandidate = cell.tile.character + character;
163
+
164
+ if (!cell.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
165
+ const action = boardSlice.actions.changeCellValue({ x, y, value: twoCharacterCandidate });
166
+ board = boardSlice.reducer(board, action);
167
+ actions.push(action);
168
+ return;
169
+ }
170
+ }
171
+
172
+ const action = boardSlice.actions.changeCellValue({ x, y, value: character });
173
+ board = boardSlice.reducer(board, action);
174
+ actions.push(action);
175
+ scheduleMoveFocus();
176
+ });
177
+
178
+ moveFocus(Math.abs(position.x - x) + Math.abs(position.y - y));
179
+ actions.forEach(dispatch);
180
+ },
181
+ [config, directionRef, dispatch, moveFocus, rows],
182
+ );
183
+
184
+ const onChange = useCallback(
185
+ (event: ChangeEvent<HTMLInputElement>) => {
186
+ const position = getInputRefPosition(event.target);
187
+
188
+ if (!position) {
189
+ return;
190
+ }
191
+
192
+ const value = extractInputValue(event.target);
193
+
194
+ if (!value) {
195
+ dispatch(boardSlice.actions.changeCellValue({ ...position, value: EMPTY_CELL }));
196
+ moveFocus(-1);
197
+ return;
198
+ }
199
+
200
+ if (value === EMPTY_CELL) {
201
+ const { x, y } = position;
202
+ const cell = rows[y][x];
203
+
204
+ if (cell.hasTile()) {
205
+ dispatch(boardSlice.actions.toggleCellIsBlank(position));
206
+ return;
207
+ }
208
+ }
209
+
210
+ insertValue(position, value);
211
+ },
212
+ [dispatch, insertValue, moveFocus, rows],
76
213
  );
77
214
 
215
+ const onDirectionToggle = useCallback(() => setLastDirection(toggleDirection), []);
216
+
217
+ const onFocus = useCallback((x: number, y: number) => {
218
+ activeIndexRef.current = { x, y };
219
+ }, []);
220
+
78
221
  const onKeyDown = useMemo(() => {
79
222
  return createKeyboardNavigation({
80
223
  onArrowDown: (event) => {
224
+ event.preventDefault();
225
+
81
226
  if (isCtrl(event)) {
82
227
  onDirectionToggle();
83
228
  } else {
@@ -85,6 +230,8 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
85
230
  }
86
231
  },
87
232
  onArrowLeft: (event) => {
233
+ event.preventDefault();
234
+
88
235
  if (isCtrl(event)) {
89
236
  onDirectionToggle();
90
237
  } else {
@@ -92,6 +239,8 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
92
239
  }
93
240
  },
94
241
  onArrowRight: (event) => {
242
+ event.preventDefault();
243
+
95
244
  if (isCtrl(event)) {
96
245
  onDirectionToggle();
97
246
  } else {
@@ -99,6 +248,8 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
99
248
  }
100
249
  },
101
250
  onArrowUp: (event) => {
251
+ event.preventDefault();
252
+
102
253
  if (isCtrl(event)) {
103
254
  onDirectionToggle();
104
255
  } else {
@@ -112,8 +263,9 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
112
263
  return;
113
264
  }
114
265
 
266
+ event.preventDefault();
115
267
  dispatch(boardSlice.actions.changeCellValue({ ...position, value: EMPTY_CELL }));
116
- onMoveFocus('backward');
268
+ moveFocus(-1);
117
269
  },
118
270
  onDelete: (event) => {
119
271
  const position = getInputRefPosition(event.target as HTMLInputElement);
@@ -122,8 +274,9 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
122
274
  return;
123
275
  }
124
276
 
277
+ event.preventDefault();
125
278
  dispatch(boardSlice.actions.changeCellValue({ ...position, value: EMPTY_CELL }));
126
- onMoveFocus('forward');
279
+ moveFocus(1);
127
280
  },
128
281
  onKeyDown: (event) => {
129
282
  const position = getInputRefPosition(event.target as HTMLInputElement);
@@ -138,6 +291,7 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
138
291
  const twoCharacterTile = config.getTwoCharacterTileByPrefix(character);
139
292
 
140
293
  if (isTogglingBlank) {
294
+ event.preventDefault();
141
295
  dispatch(boardSlice.actions.toggleCellIsBlank(position));
142
296
  return;
143
297
  }
@@ -145,75 +299,26 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
145
299
  if (isCtrl(event) && twoCharacterTile) {
146
300
  event.preventDefault();
147
301
  dispatch(boardSlice.actions.changeCellValue({ x, y, value: twoCharacterTile }));
148
- onMoveFocus('forward');
302
+ moveFocus(1);
149
303
  return;
150
304
  }
151
305
 
152
- if (!config.hasCharacter(character)) {
153
- return;
154
- }
155
-
156
- const canCheckUp = y - 1 > 0;
157
- const canCheckLeft = x > 0;
158
- const canCheckRight = x + 1 < width;
159
- const canCheckDown = y + 1 < height;
160
-
161
- if (canCheckUp) {
162
- const cellUp = rows[y - 1][x];
163
- const twoCharacterCandidate = cellUp.tile.character + character;
164
-
165
- if (config.twoCharacterTiles.includes(twoCharacterCandidate)) {
166
- dispatch(boardSlice.actions.changeCellValue({ ...position, y: y - 1, value: twoCharacterCandidate }));
167
- return;
168
- }
169
- }
170
-
171
- if (canCheckDown) {
172
- const cellDown = rows[y + 1][x];
173
- const twoCharacterCandidate = character + cellDown.tile.character;
174
-
175
- if (config.twoCharacterTiles.includes(twoCharacterCandidate)) {
176
- dispatch(boardSlice.actions.changeCellValue({ ...position, value: character }));
177
- dispatch(boardSlice.actions.changeCellValue({ ...position, y: y + 1, value: EMPTY_CELL }));
178
- onMoveFocus('forward');
179
- return;
180
- }
181
- }
182
-
183
- if (canCheckLeft) {
184
- const cellLeft = rows[y][x - 1];
185
- const twoCharacterCandidate = cellLeft.tile.character + character;
186
-
187
- if (config.twoCharacterTiles.includes(twoCharacterCandidate)) {
188
- dispatch(boardSlice.actions.changeCellValue({ ...position, x: x - 1, value: twoCharacterCandidate }));
189
- return;
190
- }
191
- }
192
-
193
- if (canCheckRight) {
194
- const cellRight = rows[y][x + 1];
195
- const twoCharacterCandidate = character + cellRight.tile.character;
306
+ const cell = rows[y][x];
307
+ const twoCharacterCandidate = cell.tile.character + character;
196
308
 
197
- if (config.twoCharacterTiles.includes(twoCharacterCandidate)) {
198
- dispatch(boardSlice.actions.changeCellValue({ ...position, value: character }));
199
- dispatch(boardSlice.actions.changeCellValue({ ...position, x: x + 1, value: EMPTY_CELL }));
200
- onMoveFocus('forward');
201
- return;
202
- }
309
+ if (config.twoCharacterTiles.includes(twoCharacterCandidate)) {
310
+ event.preventDefault();
311
+ dispatch(boardSlice.actions.changeCellValue({ ...position, value: twoCharacterCandidate }));
312
+ moveFocus(1);
313
+ return;
203
314
  }
204
315
 
205
- if (!canCheckDown || !canCheckRight) {
206
- const cell = rows[y][x];
207
- const twoCharacterCandidate = cell.tile.character + character;
208
-
209
- if (config.twoCharacterTiles.includes(twoCharacterCandidate)) {
210
- dispatch(boardSlice.actions.changeCellValue({ ...position, value: twoCharacterCandidate }));
211
- return;
212
- }
316
+ if (event.target instanceof HTMLInputElement && event.target.value === event.key) {
317
+ // change event did not fire because the same character was typed over the current one
318
+ // but we still want to move the caret
319
+ event.preventDefault();
320
+ moveFocus(1);
213
321
  }
214
-
215
- dispatch(boardSlice.actions.changeCellValue({ ...position, value: character }));
216
- onMoveFocus('forward');
217
322
  },
218
323
  onSpace: (event) => {
219
324
  const position = getInputRefPosition(event.target as HTMLInputElement);
@@ -222,14 +327,35 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
222
327
  return;
223
328
  }
224
329
 
330
+ event.preventDefault();
225
331
  dispatch(boardSlice.actions.toggleCellIsBlank(position));
226
332
  },
227
333
  });
228
- }, [changeActiveIndex, config, dispatch, lastDirectionRef, onDirectionToggle, rows]);
334
+ }, [changeActiveIndex, config, dispatch, onDirectionToggle, rows]);
335
+
336
+ const onPaste = useCallback<ClipboardEventHandler>(
337
+ (event) => {
338
+ if (!(event.target instanceof HTMLInputElement)) {
339
+ return;
340
+ }
341
+
342
+ const position = getInputRefPosition(event.target);
343
+
344
+ if (!position) {
345
+ return;
346
+ }
347
+
348
+ event.preventDefault();
349
+
350
+ const value = event.clipboardData.getData('text/plain').toLocaleLowerCase();
351
+ insertValue(position, value);
352
+ },
353
+ [insertValue],
354
+ );
229
355
 
230
356
  return [
231
- { lastDirection, refs },
232
- { onDirectionToggle, onFocus, onKeyDown },
357
+ { direction, refs },
358
+ { onChange, onDirectionToggle, onFocus, onKeyDown, onPaste },
233
359
  ];
234
360
  };
235
361
 
@@ -1,7 +1,7 @@
1
1
  import classNames from 'classnames';
2
2
  import { FunctionComponent } from 'react';
3
3
 
4
- import { selectDictionary, useTranslate, useTypedSelector } from 'state';
4
+ import { selectDictionary, selectDictionaryError, useTranslate, useTypedSelector } from 'state';
5
5
 
6
6
  import EmptyState from '../EmptyState';
7
7
  import Loading from '../Loading';
@@ -15,6 +15,7 @@ interface Props {
15
15
  const Dictionary: FunctionComponent<Props> = ({ className }) => {
16
16
  const translate = useTranslate();
17
17
  const { results, isLoading } = useTypedSelector(selectDictionary);
18
+ const error = useTypedSelector(selectDictionaryError);
18
19
  const isFirstAllowed = results.length > 0 ? results[0].isAllowed : undefined;
19
20
 
20
21
  return (
@@ -24,6 +25,12 @@ const Dictionary: FunctionComponent<Props> = ({ className }) => {
24
25
  [styles.isNotAllowed]: isFirstAllowed === false,
25
26
  })}
26
27
  >
28
+ {typeof error !== 'undefined' && (
29
+ <EmptyState className={styles.emptyState} type="error">
30
+ {error.message}
31
+ </EmptyState>
32
+ )}
33
+
27
34
  {results.map(({ definitions, isAllowed, word }) => (
28
35
  <div
29
36
  className={classNames(styles.result, {