@scrabble-solver/scrabble-solver 2.8.9 → 2.8.10

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 (73) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +10 -10
  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 +181 -39
  14. package/.next/server/chunks/429.js +2 -13
  15. package/.next/server/chunks/515.js +408 -227
  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/_document.js.nft.json +1 -1
  23. package/.next/server/pages/_error.js.nft.json +1 -1
  24. package/.next/server/pages/api/solve.js +52 -19
  25. package/.next/server/pages/index.html +3 -3
  26. package/.next/server/pages/index.js +1 -1
  27. package/.next/server/pages/index.js.nft.json +1 -1
  28. package/.next/server/pages/index.json +1 -1
  29. package/.next/static/A8A_Lmg8cM-Bkf-Jo1CLh/_buildManifest.js +1 -0
  30. package/.next/static/{yCxjzzYpw5JjJE53PO_s6 → A8A_Lmg8cM-Bkf-Jo1CLh}/_ssgManifest.js +0 -0
  31. package/.next/static/chunks/317-95ab9051449362fa.js +1 -0
  32. package/.next/static/chunks/758-eff80059a1365d5d.js +1 -0
  33. package/.next/static/chunks/pages/_app-0e358b5622cf9e66.js +1 -0
  34. package/.next/static/chunks/pages/index-0cc5e6eda5adac73.js +1 -0
  35. package/.next/static/css/9ac903004135f4b1.css +1 -0
  36. package/.next/trace +42 -42
  37. package/package.json +12 -12
  38. package/src/components/Badge/Badge.module.scss +1 -1
  39. package/src/components/Board/Board.tsx +4 -2
  40. package/src/components/Board/BoardPure.tsx +25 -5
  41. package/src/components/Board/hooks/useGrid.ts +212 -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/SquareButton/Link.tsx +1 -1
  47. package/src/components/Tile/Tile.module.scss +4 -0
  48. package/src/components/Tile/Tile.tsx +13 -4
  49. package/src/components/Tile/TilePure.tsx +3 -4
  50. package/src/lib/extractCharacters.ts +26 -0
  51. package/src/lib/extractInputValue.ts +17 -0
  52. package/src/lib/index.ts +2 -0
  53. package/src/lib/isCtrl.ts +1 -1
  54. package/src/lib/memoize.ts +15 -1
  55. package/src/pages/api/solve.ts +1 -1
  56. package/src/sdk/fetchJson.ts +36 -0
  57. package/src/sdk/findWordDefinitions.ts +4 -3
  58. package/src/sdk/solve.ts +8 -7
  59. package/src/sdk/verify.ts +5 -6
  60. package/src/state/sagas.ts +3 -3
  61. package/src/state/selectors.ts +9 -1
  62. package/src/state/slices/dictionaryInitialState.ts +10 -2
  63. package/src/state/slices/dictionarySlice.ts +10 -16
  64. package/src/state/slices/rackSlice.ts +7 -0
  65. package/src/state/slices/solveInitialState.ts +14 -2
  66. package/src/state/slices/solveSlice.ts +7 -4
  67. package/src/types/index.ts +2 -0
  68. package/.next/static/chunks/317-a33dd38e9b9a17ed.js +0 -1
  69. package/.next/static/chunks/758-f333b1dcdb941547.js +0 -1
  70. package/.next/static/chunks/pages/_app-f8f360878e1c2aff.js +0 -1
  71. package/.next/static/chunks/pages/index-ecea697d3e5d8a6f.js +0 -1
  72. package/.next/static/css/64dc2ce1811912f1.css +0 -1
  73. 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.10",
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.10",
34
+ "@scrabble-solver/constants": "^2.8.10",
35
+ "@scrabble-solver/dictionaries": "^2.8.10",
36
+ "@scrabble-solver/logger": "^2.8.10",
37
+ "@scrabble-solver/solver": "^2.8.10",
38
+ "@scrabble-solver/types": "^2.8.10",
39
+ "@scrabble-solver/word-definitions": "^2.8.10",
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": "8822c74d0d632ee51bca3bf69ccfc7f517bfadb3"
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}
@@ -1,25 +1,41 @@
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';
7
18
 
8
- import { createGridOf, createKeyboardNavigation, isCtrl } from 'lib';
19
+ import { createGridOf, createKeyboardNavigation, extractCharacters, extractInputValue, isCtrl } from 'lib';
9
20
  import { boardSlice, selectConfig, useTypedSelector } from 'state';
21
+ import { Direction } from 'types';
10
22
 
11
23
  import { getPositionInGrid } from '../lib';
12
24
  import { Point } from '../types';
13
25
 
26
+ const toggleDirection = (direction: Direction) => (direction === 'vertical' ? 'horizontal' : 'vertical');
27
+
14
28
  interface State {
15
- lastDirection: 'horizontal' | 'vertical';
29
+ direction: Direction;
16
30
  refs: RefObject<HTMLInputElement>[][];
17
31
  }
18
32
 
19
33
  interface Actions {
20
- onFocus: (x: number, y: number) => void;
34
+ onChange: ChangeEventHandler<HTMLInputElement>;
21
35
  onDirectionToggle: () => void;
36
+ onFocus: (x: number, y: number) => void;
22
37
  onKeyDown: KeyboardEventHandler<HTMLInputElement>;
38
+ onPaste: ClipboardEventHandler<HTMLInputElement>;
23
39
  }
24
40
 
25
41
  const useGrid = (rows: Cell[][]): [State, Actions] => {
@@ -32,8 +48,8 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
32
48
  [width, height],
33
49
  );
34
50
  const activeIndexRef = useRef<Point>({ x: 0, y: 0 });
35
- const [lastDirection, setLastDirection] = useState<'horizontal' | 'vertical'>('horizontal');
36
- const lastDirectionRef = useLatest(lastDirection);
51
+ const [direction, setLastDirection] = useState<Direction>('horizontal');
52
+ const directionRef = useLatest(direction);
37
53
 
38
54
  const changeActiveIndex = useCallback(
39
55
  (offsetX: number, offsetY: number) => {
@@ -52,32 +68,156 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
52
68
  [refs],
53
69
  );
54
70
 
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') {
71
+ const moveFocus = useCallback(
72
+ (offset: number) => {
73
+ if (directionRef.current === 'horizontal') {
70
74
  changeActiveIndex(offset, 0);
71
75
  } else {
72
76
  changeActiveIndex(0, offset);
73
77
  }
74
78
  },
75
- [changeActiveIndex, lastDirectionRef],
79
+ [changeActiveIndex, directionRef],
80
+ );
81
+
82
+ const insertValue = useCallback(
83
+ (position: Point, value: string) => {
84
+ const characters = value ? extractCharacters(config, value).filter((character) => character !== BLANK) : [BLANK];
85
+ let board = new Board({ rows: rows.map((row) => row.map((cell) => cell.clone())) });
86
+ let { x, y } = position;
87
+
88
+ const scheduleMoveFocus = () => {
89
+ if (directionRef.current === 'horizontal') {
90
+ ++x;
91
+ } else {
92
+ ++y;
93
+ }
94
+ };
95
+
96
+ characters.forEach((character) => {
97
+ if (x >= config.boardWidth || y >= config.boardHeight) {
98
+ return;
99
+ }
100
+
101
+ const canCheckUp = y - 1 > 0;
102
+ const canCheckLeft = x > 0;
103
+ const canCheckRight = x + 1 < width;
104
+ const canCheckDown = y + 1 < height;
105
+
106
+ if (canCheckUp) {
107
+ const cellUp = board.rows[y - 1][x];
108
+ const twoCharacterCandidate = cellUp.tile.character + character;
109
+
110
+ if (!cellUp.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
111
+ board = boardSlice.reducer(
112
+ board,
113
+ boardSlice.actions.changeCellValue({ x, y: y - 1, value: twoCharacterCandidate }),
114
+ );
115
+ return;
116
+ }
117
+ }
118
+
119
+ if (canCheckDown) {
120
+ const cellDown = board.rows[y + 1][x];
121
+ const twoCharacterCandidate = character + cellDown.tile.character;
122
+
123
+ if (!cellDown.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
124
+ board = boardSlice.reducer(board, boardSlice.actions.changeCellValue({ x, y, value: character }));
125
+ board = boardSlice.reducer(board, boardSlice.actions.changeCellValue({ x, y: y + 1, value: EMPTY_CELL }));
126
+ scheduleMoveFocus();
127
+ return;
128
+ }
129
+ }
130
+
131
+ if (canCheckLeft) {
132
+ const cellLeft = board.rows[y][x - 1];
133
+ const twoCharacterCandidate = cellLeft.tile.character + character;
134
+
135
+ if (!cellLeft.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
136
+ board = boardSlice.reducer(
137
+ board,
138
+ boardSlice.actions.changeCellValue({ x: x - 1, y, value: twoCharacterCandidate }),
139
+ );
140
+ return;
141
+ }
142
+ }
143
+
144
+ if (canCheckRight) {
145
+ const cellRight = board.rows[y][x + 1];
146
+ const twoCharacterCandidate = character + cellRight.tile.character;
147
+
148
+ if (!cellRight.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
149
+ board = boardSlice.reducer(board, boardSlice.actions.changeCellValue({ x, y, value: character }));
150
+ board = boardSlice.reducer(board, boardSlice.actions.changeCellValue({ x: x + 1, y, value: EMPTY_CELL }));
151
+ scheduleMoveFocus();
152
+ return;
153
+ }
154
+ }
155
+
156
+ if (!canCheckDown || !canCheckRight) {
157
+ const cell = board.rows[y][x];
158
+ const twoCharacterCandidate = cell.tile.character + character;
159
+
160
+ if (!cell.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
161
+ board = boardSlice.reducer(
162
+ board,
163
+ boardSlice.actions.changeCellValue({ x, y, value: twoCharacterCandidate }),
164
+ );
165
+ return;
166
+ }
167
+ }
168
+
169
+ board = boardSlice.reducer(board, boardSlice.actions.changeCellValue({ x, y, value: character }));
170
+ scheduleMoveFocus();
171
+ });
172
+
173
+ moveFocus(Math.abs(position.x - x) + Math.abs(position.y - y));
174
+ dispatch(boardSlice.actions.change(board));
175
+ },
176
+ [config, directionRef, dispatch, moveFocus, rows],
177
+ );
178
+
179
+ const onChange = useCallback(
180
+ (event: ChangeEvent<HTMLInputElement>) => {
181
+ const position = getInputRefPosition(event.target);
182
+
183
+ if (!position) {
184
+ return;
185
+ }
186
+
187
+ const value = extractInputValue(event.target);
188
+
189
+ if (!value) {
190
+ dispatch(boardSlice.actions.changeCellValue({ ...position, value: EMPTY_CELL }));
191
+ moveFocus(-1);
192
+ return;
193
+ }
194
+
195
+ if (value === EMPTY_CELL) {
196
+ const { x, y } = position;
197
+ const cell = rows[y][x];
198
+
199
+ if (cell.hasTile()) {
200
+ dispatch(boardSlice.actions.toggleCellIsBlank(position));
201
+ return;
202
+ }
203
+ }
204
+
205
+ insertValue(position, value);
206
+ },
207
+ [dispatch, insertValue, moveFocus, rows],
76
208
  );
77
209
 
210
+ const onDirectionToggle = useCallback(() => setLastDirection(toggleDirection), []);
211
+
212
+ const onFocus = useCallback((x: number, y: number) => {
213
+ activeIndexRef.current = { x, y };
214
+ }, []);
215
+
78
216
  const onKeyDown = useMemo(() => {
79
217
  return createKeyboardNavigation({
80
218
  onArrowDown: (event) => {
219
+ event.preventDefault();
220
+
81
221
  if (isCtrl(event)) {
82
222
  onDirectionToggle();
83
223
  } else {
@@ -85,6 +225,8 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
85
225
  }
86
226
  },
87
227
  onArrowLeft: (event) => {
228
+ event.preventDefault();
229
+
88
230
  if (isCtrl(event)) {
89
231
  onDirectionToggle();
90
232
  } else {
@@ -92,6 +234,8 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
92
234
  }
93
235
  },
94
236
  onArrowRight: (event) => {
237
+ event.preventDefault();
238
+
95
239
  if (isCtrl(event)) {
96
240
  onDirectionToggle();
97
241
  } else {
@@ -99,6 +243,8 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
99
243
  }
100
244
  },
101
245
  onArrowUp: (event) => {
246
+ event.preventDefault();
247
+
102
248
  if (isCtrl(event)) {
103
249
  onDirectionToggle();
104
250
  } else {
@@ -112,8 +258,9 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
112
258
  return;
113
259
  }
114
260
 
261
+ event.preventDefault();
115
262
  dispatch(boardSlice.actions.changeCellValue({ ...position, value: EMPTY_CELL }));
116
- onMoveFocus('backward');
263
+ moveFocus(-1);
117
264
  },
118
265
  onDelete: (event) => {
119
266
  const position = getInputRefPosition(event.target as HTMLInputElement);
@@ -122,8 +269,9 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
122
269
  return;
123
270
  }
124
271
 
272
+ event.preventDefault();
125
273
  dispatch(boardSlice.actions.changeCellValue({ ...position, value: EMPTY_CELL }));
126
- onMoveFocus('forward');
274
+ moveFocus(1);
127
275
  },
128
276
  onKeyDown: (event) => {
129
277
  const position = getInputRefPosition(event.target as HTMLInputElement);
@@ -138,6 +286,7 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
138
286
  const twoCharacterTile = config.getTwoCharacterTileByPrefix(character);
139
287
 
140
288
  if (isTogglingBlank) {
289
+ event.preventDefault();
141
290
  dispatch(boardSlice.actions.toggleCellIsBlank(position));
142
291
  return;
143
292
  }
@@ -145,75 +294,26 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
145
294
  if (isCtrl(event) && twoCharacterTile) {
146
295
  event.preventDefault();
147
296
  dispatch(boardSlice.actions.changeCellValue({ x, y, value: twoCharacterTile }));
148
- onMoveFocus('forward');
297
+ moveFocus(1);
149
298
  return;
150
299
  }
151
300
 
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;
301
+ const cell = rows[y][x];
302
+ const twoCharacterCandidate = cell.tile.character + character;
196
303
 
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
- }
304
+ if (config.twoCharacterTiles.includes(twoCharacterCandidate)) {
305
+ event.preventDefault();
306
+ dispatch(boardSlice.actions.changeCellValue({ ...position, value: twoCharacterCandidate }));
307
+ moveFocus(1);
308
+ return;
203
309
  }
204
310
 
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
- }
311
+ if (event.target instanceof HTMLInputElement && event.target.value === event.key) {
312
+ // change event did not fire because the same character was typed over the current one
313
+ // but we still want to move the caret
314
+ event.preventDefault();
315
+ moveFocus(1);
213
316
  }
214
-
215
- dispatch(boardSlice.actions.changeCellValue({ ...position, value: character }));
216
- onMoveFocus('forward');
217
317
  },
218
318
  onSpace: (event) => {
219
319
  const position = getInputRefPosition(event.target as HTMLInputElement);
@@ -222,14 +322,35 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
222
322
  return;
223
323
  }
224
324
 
325
+ event.preventDefault();
225
326
  dispatch(boardSlice.actions.toggleCellIsBlank(position));
226
327
  },
227
328
  });
228
- }, [changeActiveIndex, config, dispatch, lastDirectionRef, onDirectionToggle, rows]);
329
+ }, [changeActiveIndex, config, dispatch, onDirectionToggle, rows]);
330
+
331
+ const onPaste = useCallback<ClipboardEventHandler>(
332
+ (event) => {
333
+ if (!(event.target instanceof HTMLInputElement)) {
334
+ return;
335
+ }
336
+
337
+ const position = getInputRefPosition(event.target);
338
+
339
+ if (!position) {
340
+ return;
341
+ }
342
+
343
+ event.preventDefault();
344
+
345
+ const value = event.clipboardData.getData('text/plain').toLocaleLowerCase();
346
+ insertValue(position, value);
347
+ },
348
+ [insertValue],
349
+ );
229
350
 
230
351
  return [
231
- { lastDirection, refs },
232
- { onDirectionToggle, onFocus, onKeyDown },
352
+ { direction, refs },
353
+ { onChange, onDirectionToggle, onFocus, onKeyDown, onPaste },
233
354
  ];
234
355
  };
235
356
 
@@ -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, {