kokopu-react 3.0.2 → 3.1.0
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.
- package/CHANGELOG.md +7 -1
- package/dist/lib/chessboard/BoardProperties.d.ts +91 -0
- package/dist/lib/chessboard/BoardProperties.js +43 -0
- package/dist/lib/chessboard/BoardProperties.js.map +1 -0
- package/dist/lib/chessboard/Chessboard.d.ts +14 -58
- package/dist/lib/chessboard/Chessboard.js +26 -92
- package/dist/lib/chessboard/Chessboard.js.map +1 -1
- package/dist/lib/errorbox/parsing.d.ts +57 -0
- package/dist/lib/errorbox/parsing.js +140 -0
- package/dist/lib/errorbox/parsing.js.map +1 -0
- package/dist/lib/i18n.d.ts +5 -0
- package/dist/lib/i18n.js +6 -0
- package/dist/lib/i18n.js.map +1 -1
- package/dist/lib/index.d.ts +4 -1
- package/dist/lib/index.js +2 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/movetext/Movetext.css +0 -13
- package/dist/lib/movetext/Movetext.d.ts +9 -43
- package/dist/lib/movetext/Movetext.js +15 -67
- package/dist/lib/movetext/Movetext.js.map +1 -1
- package/dist/lib/movetext/MovetextImpl.d.ts +7 -28
- package/dist/lib/movetext/MovetextImpl.js +15 -94
- package/dist/lib/movetext/MovetextImpl.js.map +1 -1
- package/dist/lib/navigationboard/NavigationBoard.d.ts +118 -0
- package/dist/lib/navigationboard/NavigationBoard.js +112 -0
- package/dist/lib/navigationboard/NavigationBoard.js.map +1 -0
- package/dist/lib/navigationboard/NavigationField.css +36 -0
- package/dist/lib/navigationboard/NavigationField.d.ts +81 -0
- package/dist/lib/navigationboard/NavigationField.js +154 -0
- package/dist/lib/navigationboard/NavigationField.js.map +1 -0
- package/dist/lib/navigationboard/NavigationToolbar.css +51 -0
- package/dist/lib/navigationboard/NavigationToolbar.d.ts +41 -0
- package/dist/lib/navigationboard/NavigationToolbar.js +124 -0
- package/dist/lib/navigationboard/NavigationToolbar.js.map +1 -0
- package/doc_src/demo/PageChessboardMove.tsx +17 -0
- package/doc_src/demo/PageNavigationBoardBase.tsx +194 -0
- package/doc_src/demo/PageNavigationBoardUncontrolledVsControlled.tsx +114 -0
- package/doc_src/demo/game-3.pgn +10 -0
- package/doc_src/examples/Movetext.md +1 -1
- package/doc_src/examples/NavigationBoard.md +15 -0
- package/package.json +16 -16
- package/scripts/doc.styleguide.config.js +12 -2
- package/scripts/docker-compose.yml +1 -1
- package/src/chessboard/BoardProperties.ts +131 -0
- package/src/chessboard/Chessboard.tsx +41 -170
- package/src/errorbox/parsing.tsx +158 -0
- package/src/i18n.ts +7 -0
- package/src/index.ts +5 -1
- package/src/movetext/Movetext.css +0 -13
- package/src/movetext/Movetext.tsx +20 -85
- package/src/movetext/MovetextImpl.tsx +28 -115
- package/src/navigationboard/NavigationBoard.tsx +231 -0
- package/src/navigationboard/NavigationField.css +36 -0
- package/src/navigationboard/NavigationField.tsx +185 -0
- package/src/navigationboard/NavigationToolbar.css +51 -0
- package/src/navigationboard/NavigationToolbar.tsx +174 -0
- package/test/05_chessboard_graphic.js +2 -0
- package/test/09_chessboard_small_screens.js +21 -0
- package/test/10_movetext_graphic.js +5 -0
- package/test/11_movetext_interaction.js +12 -8
- package/test/12_navigation_board.js +168 -0
- package/test/graphic_references/05_chessboard_graphic/move/illegal_null-move.png +0 -0
- package/test/graphic_references/05_chessboard_graphic/move/null-move.png +0 -0
- package/test/graphic_references/09_chessboard_small_screens/top_bottom_components/default_bottom_component.png +0 -0
- package/test/graphic_references/09_chessboard_small_screens/top_bottom_components/default_top_component.png +0 -0
- package/test/graphic_references/09_chessboard_small_screens/top_bottom_components/reduced_bottom_component.png +0 -0
- package/test/graphic_references/09_chessboard_small_screens/top_bottom_components/reduced_top_component.png +0 -0
- package/test/graphic_references/10_movetext_graphic/format_move/format_text.png +0 -0
- package/test/graphic_references/10_movetext_graphic/options/wrong_piece_symbol_value.png +0 -0
- package/test/graphic_references/11_movetext_interaction/select_moves/click_on_unselected_without_callback.png +0 -0
- package/test/graphic_references/12_navigation_board/controlled/default.png +0 -0
- package/test/graphic_references/12_navigation_board/controlled/forbidden_navigation_0.png +0 -0
- package/test/graphic_references/12_navigation_board/controlled/forbidden_navigation_1.png +0 -0
- package/test/graphic_references/12_navigation_board/controlled/invalid_node_id.png +0 -0
- package/test/graphic_references/12_navigation_board/controlled/no_callback_defined_0.png +0 -0
- package/test/graphic_references/12_navigation_board/controlled/no_callback_defined_1.png +0 -0
- package/test/graphic_references/12_navigation_board/controlled/overriding_initial_node_id.png +0 -0
- package/test/graphic_references/12_navigation_board/controlled/regular_navigation_0.png +0 -0
- package/test/graphic_references/12_navigation_board/controlled/regular_navigation_1.png +0 -0
- package/test/graphic_references/12_navigation_board/controlled/regular_navigation_2.png +0 -0
- package/test/graphic_references/12_navigation_board/controlled/regular_navigation_3.png +0 -0
- package/test/graphic_references/12_navigation_board/error/pgn_parsing_error.png +0 -0
- package/test/graphic_references/12_navigation_board/error/wrong_game_type.png +0 -0
- package/test/graphic_references/12_navigation_board/theme/custom_1.png +0 -0
- package/test/graphic_references/12_navigation_board/theme/custom_2.png +0 -0
- package/test/graphic_references/12_navigation_board/theme/large.png +0 -0
- package/test/graphic_references/12_navigation_board/theme/small.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/forbidden_navigation_at_beginning_0.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/forbidden_navigation_at_beginning_1.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/forbidden_navigation_at_beginning_2.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/forbidden_navigation_at_end_0.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/forbidden_navigation_at_end_1.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/forbidden_navigation_at_end_2.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/initial_state_at_end.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/initial_state_at_specific_id.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/initial_state_default.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/initial_state_default_with_game_index.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/initial_state_on_null-move.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/navigation_with_keys_0.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/navigation_with_keys_1.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/navigation_with_keys_2.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/navigation_with_keys_3.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/navigation_with_keys_4.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/regular_flipping_0.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/regular_flipping_1.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/regular_navigation_0.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/regular_navigation_1.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/regular_navigation_2.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/regular_navigation_3.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/regular_navigation_4.png +0 -0
- package/test/graphic_references/12_navigation_board/uncontrolled/regular_navigation_5.png +0 -0
- package/test/graphic_test_app/05_chessboard_graphic/move.tsx +2 -0
- package/test/graphic_test_app/09_chessboard_small_screens/top_bottom_components.tsx +48 -0
- package/test/graphic_test_app/10_movetext_graphic/format_move.tsx +31 -0
- package/test/graphic_test_app/10_movetext_graphic/options.tsx +1 -0
- package/test/graphic_test_app/11_movetext_interaction/select_moves.tsx +1 -0
- package/test/graphic_test_app/12_navigation_board/controlled.tsx +43 -0
- package/test/graphic_test_app/12_navigation_board/error.tsx +34 -0
- package/test/graphic_test_app/12_navigation_board/theme.tsx +38 -0
- package/test/graphic_test_app/12_navigation_board/uncontrolled.tsx +56 -0
|
@@ -24,17 +24,17 @@
|
|
|
24
24
|
|
|
25
25
|
import * as React from 'react';
|
|
26
26
|
|
|
27
|
-
import {
|
|
27
|
+
import { Database, Game } from 'kokopu';
|
|
28
28
|
|
|
29
29
|
import { IllegalArgument } from '../exception';
|
|
30
|
-
import { i18n } from '../i18n';
|
|
31
30
|
import { sanitizeString, sanitizeBoolean, sanitizeOptional } from '../sanitization';
|
|
32
31
|
import { PieceSymbolMapping } from '../types';
|
|
33
32
|
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
33
|
+
import { StaticBoardGraphicProps } from '../chessboard/BoardProperties';
|
|
34
|
+
import { parseGame } from '../errorbox/parsing';
|
|
35
|
+
import { firstNodeId as firstNodeIdImpl, previousNodeId as previousNodeIdImpl, nextNodeId as nextNodeIdImpl, lastNodeId as lastNodeIdImpl } from '../navigationboard/NavigationField';
|
|
36
36
|
import { moveFormatter } from './moveFormatter';
|
|
37
|
-
import { MovetextImpl
|
|
37
|
+
import { MovetextImpl } from './MovetextImpl';
|
|
38
38
|
|
|
39
39
|
import './Movetext.css';
|
|
40
40
|
|
|
@@ -48,7 +48,7 @@ export type MoveSelectEventOrigin = 'key-first' | 'key-previous' | 'key-next' |
|
|
|
48
48
|
export interface MovetextProps {
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Displayed
|
|
51
|
+
* Displayed game. Can be a [kokopu.Game](https://kokopu.yo35.org/docs/current/classes/Game.html) object,
|
|
52
52
|
* a [kokopu.Database](https://kokopu.yo35.org/docs/current/classes/Database.html) object,
|
|
53
53
|
* or a [PGN string](https://en.wikipedia.org/wiki/Portable_Game_Notation).
|
|
54
54
|
*/
|
|
@@ -64,14 +64,8 @@ export interface MovetextProps {
|
|
|
64
64
|
/**
|
|
65
65
|
* Options applicable to the diagrams in the comments. See [Chessboard](#/Components/Chessboard) for more details about each option.
|
|
66
66
|
*/
|
|
67
|
-
diagramOptions: {
|
|
68
|
-
flipped?:
|
|
69
|
-
squareSize?: ChessboardProps['squareSize'],
|
|
70
|
-
coordinateVisible?: ChessboardProps['coordinateVisible'],
|
|
71
|
-
turnVisible?: ChessboardProps['turnVisible'],
|
|
72
|
-
colorset?: ChessboardProps['colorset'],
|
|
73
|
-
pieceset?: ChessboardProps['pieceset'],
|
|
74
|
-
smallScreenLimits?: ChessboardProps['smallScreenLimits'],
|
|
67
|
+
diagramOptions: Partial<StaticBoardGraphicProps> & {
|
|
68
|
+
flipped?: boolean,
|
|
75
69
|
};
|
|
76
70
|
|
|
77
71
|
/**
|
|
@@ -90,7 +84,7 @@ export interface MovetextProps {
|
|
|
90
84
|
headerVisible: boolean;
|
|
91
85
|
|
|
92
86
|
/**
|
|
93
|
-
* ID of the selected move (or `'start'` for the beginning of the main variation).
|
|
87
|
+
* ID of the selected move (or `'start'`/`'end'` for the beginning/end of the main variation).
|
|
94
88
|
* Use [kokopu.Node#id](https://kokopu.yo35.org/docs/current/classes/Node.html#id) to get the ID of a game move.
|
|
95
89
|
*/
|
|
96
90
|
selection?: string;
|
|
@@ -138,14 +132,9 @@ export class Movetext extends React.Component<MovetextProps> {
|
|
|
138
132
|
render() {
|
|
139
133
|
|
|
140
134
|
// Validate the game and game-index attributes.
|
|
141
|
-
const info = parseGame(this.props.game, this.props.gameIndex);
|
|
135
|
+
const info = parseGame(this.props.game, this.props.gameIndex, 'Movetext');
|
|
142
136
|
if (info.error) {
|
|
143
|
-
return
|
|
144
|
-
<ErrorBox
|
|
145
|
-
title={i18n.INVALID_PGN_ERROR_TITLE} message={info.pgnException.message} text={info.pgnException.pgn}
|
|
146
|
-
errorIndex={info.pgnException.index} lineNumber={info.pgnException.lineNumber}
|
|
147
|
-
/>
|
|
148
|
-
);
|
|
137
|
+
return info.errorBox;
|
|
149
138
|
}
|
|
150
139
|
|
|
151
140
|
// Validate the appearance attributes.
|
|
@@ -187,96 +176,42 @@ export class Movetext extends React.Component<MovetextProps> {
|
|
|
187
176
|
*/
|
|
188
177
|
focus() {
|
|
189
178
|
const movetextImpl = this.implRef.current;
|
|
179
|
+
// istanbul ignore else
|
|
190
180
|
if (movetextImpl) {
|
|
191
181
|
movetextImpl.focus();
|
|
192
182
|
}
|
|
193
183
|
}
|
|
194
184
|
|
|
195
185
|
/**
|
|
196
|
-
*
|
|
197
|
-
* If the given selection corresponds already at the main variation, `undefined` is returned.
|
|
198
|
-
*
|
|
199
|
-
* This corresponds to the operation performed when the user presses key "home" on a `Movetext` component.
|
|
200
|
-
*
|
|
201
|
-
* @param game - Considered chess game.
|
|
202
|
-
* @param selection - ID of the selected move (or `'start'` for the beginning of the main variation).
|
|
203
|
-
* @public
|
|
186
|
+
* @deprecated
|
|
204
187
|
*/
|
|
188
|
+
// istanbul ignore next
|
|
205
189
|
static firstNodeId(game: Game, selection: string): string | undefined {
|
|
206
190
|
return firstNodeIdImpl(game, selection);
|
|
207
191
|
}
|
|
208
192
|
|
|
209
193
|
/**
|
|
210
|
-
*
|
|
211
|
-
* If no such node exists, `undefined` is returned.
|
|
212
|
-
*
|
|
213
|
-
* This corresponds to the operation performed when the user presses key "arrow left" on a `Movetext` component.
|
|
214
|
-
*
|
|
215
|
-
* @param game - Considered chess game.
|
|
216
|
-
* @param selection - ID of the selected move (or `'start'` for the beginning of the main variation).
|
|
217
|
-
* @public
|
|
194
|
+
* @deprecated
|
|
218
195
|
*/
|
|
196
|
+
// istanbul ignore next
|
|
219
197
|
static previousNodeId(game: Game, selection: string): string | undefined {
|
|
220
198
|
return previousNodeIdImpl(game, selection);
|
|
221
199
|
}
|
|
222
200
|
|
|
223
201
|
/**
|
|
224
|
-
*
|
|
225
|
-
* If no such node exists, `undefined` is returned.
|
|
226
|
-
*
|
|
227
|
-
* This corresponds to the operation performed when the user presses key "arrow right" on a `Movetext` component.
|
|
228
|
-
*
|
|
229
|
-
* @param game - Considered chess game.
|
|
230
|
-
* @param selection - ID of the selected move (or `'start'` for the beginning of the main variation).
|
|
231
|
-
* @public
|
|
202
|
+
* @deprecated
|
|
232
203
|
*/
|
|
204
|
+
// istanbul ignore next
|
|
233
205
|
static nextNodeId(game: Game, selection: string): string | undefined {
|
|
234
206
|
return nextNodeIdImpl(game, selection);
|
|
235
207
|
}
|
|
236
208
|
|
|
237
209
|
/**
|
|
238
|
-
*
|
|
239
|
-
* If the selection is already at the end its variation, `undefined` is returned.
|
|
240
|
-
*
|
|
241
|
-
* This corresponds to the operation performed when the user presses key "end" on a `Movetext` component.
|
|
242
|
-
*
|
|
243
|
-
* @param game - Considered chess game.
|
|
244
|
-
* @param selection - ID of the selected move (or `'start'` for the beginning of the main variation).
|
|
245
|
-
* @public
|
|
210
|
+
* @deprecated
|
|
246
211
|
*/
|
|
212
|
+
// istanbul ignore next
|
|
247
213
|
static lastNodeId(game: Game, selection: string): string | undefined {
|
|
248
214
|
return lastNodeIdImpl(game, selection);
|
|
249
215
|
}
|
|
250
216
|
|
|
251
217
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Try to interpret the given object as a chess game.
|
|
256
|
-
*/
|
|
257
|
-
function parseGame(game: Game | Database | string, gameIndex: number): { error: true, pgnException: kokopuException.InvalidPGN } | { error: false, game: Game } {
|
|
258
|
-
if (game instanceof Game) {
|
|
259
|
-
return { error: false, game: game };
|
|
260
|
-
}
|
|
261
|
-
else if (game instanceof Database || typeof game === 'string') {
|
|
262
|
-
if (!Number.isInteger(gameIndex) || gameIndex < 0) {
|
|
263
|
-
throw new IllegalArgument('Movetext', 'gameIndex');
|
|
264
|
-
}
|
|
265
|
-
try {
|
|
266
|
-
const result = game instanceof Database ? game.game(gameIndex) : pgnRead(game, gameIndex);
|
|
267
|
-
return { error: false, game: result };
|
|
268
|
-
}
|
|
269
|
-
catch (e) {
|
|
270
|
-
// istanbul ignore else
|
|
271
|
-
if (e instanceof kokopuException.InvalidPGN) {
|
|
272
|
-
return { error: true, pgnException: e };
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
throw e;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
throw new IllegalArgument('Movetext', 'game');
|
|
281
|
-
}
|
|
282
|
-
}
|
|
@@ -29,7 +29,9 @@ import { Game, Color, GameResult, Node as GameNode, Variation, nagSymbol } from
|
|
|
29
29
|
import { i18n } from '../i18n';
|
|
30
30
|
import { fillPlaceholder } from '../util';
|
|
31
31
|
|
|
32
|
-
import {
|
|
32
|
+
import { StaticBoardGraphicProps } from '../chessboard/BoardProperties';
|
|
33
|
+
import { Chessboard } from '../chessboard/Chessboard';
|
|
34
|
+
import { NavigationField, firstNodeId, previousNodeId, nextNodeId, lastNodeId } from '../navigationboard/NavigationField';
|
|
33
35
|
import { htmlFilter } from './htmlFilter';
|
|
34
36
|
|
|
35
37
|
import './Movetext.css';
|
|
@@ -39,14 +41,8 @@ interface MovetextImplProps {
|
|
|
39
41
|
|
|
40
42
|
game: Game;
|
|
41
43
|
|
|
42
|
-
diagramOptions: {
|
|
43
|
-
flipped?:
|
|
44
|
-
squareSize?: ChessboardProps['squareSize'],
|
|
45
|
-
coordinateVisible?: ChessboardProps['coordinateVisible'],
|
|
46
|
-
turnVisible?: ChessboardProps['turnVisible'],
|
|
47
|
-
colorset?: ChessboardProps['colorset'],
|
|
48
|
-
pieceset?: ChessboardProps['pieceset'],
|
|
49
|
-
smallScreenLimits?: ChessboardProps['smallScreenLimits'],
|
|
44
|
+
diagramOptions: Partial<StaticBoardGraphicProps> & {
|
|
45
|
+
flipped?: boolean,
|
|
50
46
|
};
|
|
51
47
|
moveFormatter: (notation: string) => React.ReactNode;
|
|
52
48
|
diagramVisible: boolean;
|
|
@@ -66,14 +62,14 @@ interface MovetextImplProps {
|
|
|
66
62
|
*/
|
|
67
63
|
export class MovetextImpl extends React.Component<MovetextImplProps> {
|
|
68
64
|
|
|
69
|
-
private
|
|
65
|
+
private navigationFieldRef: React.RefObject<NavigationField> = React.createRef();
|
|
70
66
|
|
|
71
67
|
render() {
|
|
72
68
|
return (
|
|
73
69
|
<div className="kokopu-movetext">
|
|
74
70
|
{this.renderHeaders()}
|
|
75
71
|
{this.renderBody()}
|
|
76
|
-
{this.
|
|
72
|
+
{this.renderNavigationField()}
|
|
77
73
|
</div>
|
|
78
74
|
);
|
|
79
75
|
}
|
|
@@ -155,15 +151,17 @@ export class MovetextImpl extends React.Component<MovetextImplProps> {
|
|
|
155
151
|
return <div className="kokopu-header-annotator" key="annotator">{filter(fillPlaceholder(i18n.ANNOTATED_BY, annotator))}</div>;
|
|
156
152
|
}
|
|
157
153
|
|
|
158
|
-
private
|
|
154
|
+
private renderNavigationField() {
|
|
159
155
|
if (this.props.interactionMode !== 'selectMove') {
|
|
160
156
|
return undefined;
|
|
161
157
|
}
|
|
162
|
-
return
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
158
|
+
return <NavigationField ref={this.navigationFieldRef}
|
|
159
|
+
onFirstPressed={() => this.handleNavigationPressed(firstNodeId, 'key-first')}
|
|
160
|
+
onPreviousPressed={() => this.handleNavigationPressed(previousNodeId, 'key-previous')}
|
|
161
|
+
onNextPressed={() => this.handleNavigationPressed(nextNodeId, 'key-next')}
|
|
162
|
+
onLastPressed={() => this.handleNavigationPressed(lastNodeId, 'key-last')}
|
|
163
|
+
onExitPressed={() => this.handleExitPressed()}
|
|
164
|
+
/>;
|
|
167
165
|
}
|
|
168
166
|
|
|
169
167
|
private renderBody() {
|
|
@@ -315,15 +313,16 @@ export class MovetextImpl extends React.Component<MovetextImplProps> {
|
|
|
315
313
|
for (let textSegment of comment.split('[#]')) {
|
|
316
314
|
if (!isFirstTextSegment) {
|
|
317
315
|
const position = node instanceof Variation ? node.initialPosition() : node.position();
|
|
318
|
-
const diagram = <Chessboard
|
|
316
|
+
const diagram = <Chessboard
|
|
317
|
+
position={position}
|
|
319
318
|
squareMarkers={node.tag('csl')} arrowMarkers={node.tag('cal')} textMarkers={node.tag('ctl')}
|
|
320
319
|
flipped={this.props.diagramOptions.flipped}
|
|
321
320
|
squareSize={this.props.diagramOptions.squareSize}
|
|
322
321
|
coordinateVisible={this.props.diagramOptions.coordinateVisible}
|
|
323
322
|
turnVisible={this.props.diagramOptions.turnVisible}
|
|
324
|
-
smallScreenLimits={this.props.diagramOptions.smallScreenLimits}
|
|
325
323
|
colorset={this.props.diagramOptions.colorset}
|
|
326
324
|
pieceset={this.props.diagramOptions.pieceset}
|
|
325
|
+
smallScreenLimits={this.props.diagramOptions.smallScreenLimits}
|
|
327
326
|
/>;
|
|
328
327
|
segmentElements.push(<div className="kokopu-diagram" key={'diagram-' + (diagramIndex++)}>{diagram}</div>);
|
|
329
328
|
}
|
|
@@ -357,30 +356,15 @@ export class MovetextImpl extends React.Component<MovetextImplProps> {
|
|
|
357
356
|
return comment ? comment : undefined;
|
|
358
357
|
}
|
|
359
358
|
|
|
360
|
-
private
|
|
361
|
-
if (
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
evt.preventDefault();
|
|
365
|
-
if (!this.props.selection) {
|
|
366
|
-
return;
|
|
359
|
+
private handleNavigationPressed(transition: (game: Game, nodeId: string) => string | undefined, evtOrigin: 'key-first' | 'key-previous' | 'key-next' | 'key-last') {
|
|
360
|
+
if (this.props.selection) {
|
|
361
|
+
this.fireMoveSelected(transition(this.props.game, this.props.selection), evtOrigin);
|
|
367
362
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
this.fireMoveSelected(
|
|
373
|
-
}
|
|
374
|
-
else if (evt.key === 'ArrowRight') {
|
|
375
|
-
this.fireMoveSelected(nextNodeIdImpl(this.props.game, this.props.selection), 'key-next');
|
|
376
|
-
}
|
|
377
|
-
else if (evt.key === 'End') {
|
|
378
|
-
this.fireMoveSelected(lastNodeIdImpl(this.props.game, this.props.selection), 'key-last');
|
|
379
|
-
}
|
|
380
|
-
else { // evt.key === 'Escape'
|
|
381
|
-
if (this.props.game.findById(this.props.selection)) {
|
|
382
|
-
this.fireMoveSelected(undefined, 'key-exit');
|
|
383
|
-
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private handleExitPressed() {
|
|
366
|
+
if (this.props.selection && this.props.game.findById(this.props.selection)) {
|
|
367
|
+
this.fireMoveSelected(undefined, 'key-exit');
|
|
384
368
|
}
|
|
385
369
|
}
|
|
386
370
|
|
|
@@ -401,7 +385,8 @@ export class MovetextImpl extends React.Component<MovetextImplProps> {
|
|
|
401
385
|
* Set the focus to the current component.
|
|
402
386
|
*/
|
|
403
387
|
focus(): void {
|
|
404
|
-
const target = this.
|
|
388
|
+
const target = this.navigationFieldRef.current;
|
|
389
|
+
// istanbul ignore else
|
|
405
390
|
if (target) {
|
|
406
391
|
target.focus();
|
|
407
392
|
}
|
|
@@ -410,78 +395,6 @@ export class MovetextImpl extends React.Component<MovetextImplProps> {
|
|
|
410
395
|
}
|
|
411
396
|
|
|
412
397
|
|
|
413
|
-
/**
|
|
414
|
-
* See {@link Movetext.firstNodeId}.
|
|
415
|
-
*/
|
|
416
|
-
export function firstNodeIdImpl(game: Game, selection: string): string | undefined {
|
|
417
|
-
if (!game.findById(selection)) {
|
|
418
|
-
return undefined;
|
|
419
|
-
}
|
|
420
|
-
return selection === 'start' ? undefined : 'start';
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* See {@link Movetext.previousNodeId}.
|
|
426
|
-
*/
|
|
427
|
-
export function previousNodeIdImpl(game: Game, selection: string): string | undefined {
|
|
428
|
-
let currentNode = game.findById(selection);
|
|
429
|
-
if (!currentNode) {
|
|
430
|
-
return undefined;
|
|
431
|
-
}
|
|
432
|
-
if (currentNode instanceof Variation) {
|
|
433
|
-
currentNode = currentNode.parentNode();
|
|
434
|
-
if (!currentNode) {
|
|
435
|
-
return undefined; // This case corresponds to the first variation being selected initially.
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
while (currentNode) {
|
|
439
|
-
const previousNode = currentNode.previous();
|
|
440
|
-
if (previousNode) {
|
|
441
|
-
return previousNode.id();
|
|
442
|
-
}
|
|
443
|
-
currentNode = currentNode.parentVariation().parentNode();
|
|
444
|
-
}
|
|
445
|
-
return 'start';
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* See {@link Movetext.nextNodeId}.
|
|
451
|
-
*/
|
|
452
|
-
export function nextNodeIdImpl(game: Game, selection: string): string | undefined {
|
|
453
|
-
const currentNode = game.findById(selection);
|
|
454
|
-
if (!currentNode) {
|
|
455
|
-
return undefined;
|
|
456
|
-
}
|
|
457
|
-
const nextNode = currentNode instanceof Variation ? currentNode.first() : currentNode.next();
|
|
458
|
-
return nextNode ? nextNode.id() : undefined;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* See {@link Movetext.lastNodeId}.
|
|
464
|
-
*/
|
|
465
|
-
export function lastNodeIdImpl(game: Game, selection: string): string | undefined {
|
|
466
|
-
let currentNode = game.findById(selection);
|
|
467
|
-
if (!currentNode) {
|
|
468
|
-
return undefined;
|
|
469
|
-
}
|
|
470
|
-
currentNode = currentNode instanceof Variation ? currentNode.first() : currentNode.next();
|
|
471
|
-
if (!currentNode) { // Ensure that the input node is not already the last one.
|
|
472
|
-
return undefined;
|
|
473
|
-
}
|
|
474
|
-
let currentNodeNotNull = currentNode;
|
|
475
|
-
while (true) {
|
|
476
|
-
const nextNode = currentNodeNotNull.next();
|
|
477
|
-
if (!nextNode) {
|
|
478
|
-
return currentNodeNotNull.id();
|
|
479
|
-
}
|
|
480
|
-
currentNodeNotNull = nextNode;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
|
|
485
398
|
/**
|
|
486
399
|
* Example: `'hello world'` is turned into `'Hello world'`.
|
|
487
400
|
*/
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* -------------------------------------------------------------------------- *
|
|
3
|
+
* *
|
|
4
|
+
* Kokopu-React - A React-based library of chess-related components. *
|
|
5
|
+
* <https://www.npmjs.com/package/kokopu-react> *
|
|
6
|
+
* Copyright (C) 2021-2024 Yoann Le Montagner <yo35 -at- melix.net> *
|
|
7
|
+
* *
|
|
8
|
+
* Kokopu-React is free software: you can redistribute it and/or *
|
|
9
|
+
* modify it under the terms of the GNU Lesser General Public License *
|
|
10
|
+
* as published by the Free Software Foundation, either version 3 of *
|
|
11
|
+
* the License, or (at your option) any later version. *
|
|
12
|
+
* *
|
|
13
|
+
* Kokopu-React is distributed in the hope that it will be useful, *
|
|
14
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
15
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
16
|
+
* GNU Lesser General Public License for more details. *
|
|
17
|
+
* *
|
|
18
|
+
* You should have received a copy of the GNU Lesser General *
|
|
19
|
+
* Public License along with this program. If not, see *
|
|
20
|
+
* <http://www.gnu.org/licenses/>. *
|
|
21
|
+
* *
|
|
22
|
+
* -------------------------------------------------------------------------- */
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
import * as React from 'react';
|
|
26
|
+
|
|
27
|
+
import { Database, Game, Node as GameNode, Variation } from 'kokopu';
|
|
28
|
+
|
|
29
|
+
import { sanitizeBoolean, sanitizeOptional, sanitizeString } from '../sanitization';
|
|
30
|
+
|
|
31
|
+
import { DynamicBoardGraphicProps, defaultDynamicBoardProps } from '../chessboard/BoardProperties';
|
|
32
|
+
import { Chessboard } from '../chessboard/Chessboard';
|
|
33
|
+
import { parseGame } from '../errorbox/parsing';
|
|
34
|
+
import { NavigationField, firstNodeId, previousNodeId, nextNodeId, lastNodeId } from '../navigationboard/NavigationField';
|
|
35
|
+
import { NavigationToolbar } from './NavigationToolbar';
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
export interface NavigationBoardProps extends DynamicBoardGraphicProps {
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Displayed game. Can be a [kokopu.Game](https://kokopu.yo35.org/docs/current/classes/Game.html) object,
|
|
42
|
+
* a [kokopu.Database](https://kokopu.yo35.org/docs/current/classes/Database.html) object,
|
|
43
|
+
* or a [PGN string](https://en.wikipedia.org/wiki/Portable_Game_Notation).
|
|
44
|
+
*/
|
|
45
|
+
game: Game | Database | string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Index of the game to display (only if attribute `game` is a [kokopu.Database](https://kokopu.yo35.org/docs/current/classes/Database.html)
|
|
49
|
+
* or a [PGN string](https://en.wikipedia.org/wiki/Portable_Game_Notation)): `0` for the first game of the database/PGN, `1` for the second one, etc.
|
|
50
|
+
* If omitted, the first game of the database/PGN is displayed.
|
|
51
|
+
*/
|
|
52
|
+
gameIndex: number;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* ID of the move initially selected (or `'start'`/`'end'` for the beginning/end of the main variation) when the component is uncontrolled.
|
|
56
|
+
* Use [kokopu.Node#id](https://kokopu.yo35.org/docs/current/classes/Node.html#id) to get the ID of a game move.
|
|
57
|
+
* Ignored if the `nodeId` attribute is provided.
|
|
58
|
+
*/
|
|
59
|
+
initialNodeId: string;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* ID of the selected move (or `'start'`/`'end'` for the beginning/end of the main variation).
|
|
63
|
+
* Use [kokopu.Node#id](https://kokopu.yo35.org/docs/current/classes/Node.html#id) to get the ID of a game move.
|
|
64
|
+
* If provided (i.e. if the component is controlled), the attribute `onNodeIdChanged` must be provided as well.
|
|
65
|
+
*/
|
|
66
|
+
nodeId?: string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Callback invoked in controlled-component mode, when the user changes the selected move.
|
|
70
|
+
*
|
|
71
|
+
* @param nodeId - ID of the selected move (as returned by [kokopu.Node#id](https://kokopu.yo35.org/docs/current/classes/Node.html#id)),
|
|
72
|
+
* or `'start'` for the beginning of the main variation.
|
|
73
|
+
*/
|
|
74
|
+
onNodeIdChanged?: (nodeId: string) => void;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether the board is initially flipped (i.e. seen from Black's point of view) or not, when the flip state is uncontrolled.
|
|
78
|
+
* Ignored if the `flipped` attribute is provided.
|
|
79
|
+
*/
|
|
80
|
+
initialFlipped: boolean;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether the board is flipped (i.e. seen from Black's point of view) or not.
|
|
84
|
+
* If provided (i.e. if the flip state is controlled), the attribute `onFlippedChanged` must be provided as well.
|
|
85
|
+
*/
|
|
86
|
+
flipped?: boolean;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Callback invoked in controlled-flip-state mode, when the user flips the board.
|
|
90
|
+
*
|
|
91
|
+
* @param flipped - New flip state.
|
|
92
|
+
*/
|
|
93
|
+
onFlippedChanged?: (flipped: boolean) => void;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether the flip button is visible or not in the toolbar.
|
|
97
|
+
*/
|
|
98
|
+
flipButtonVisible: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
interface NavigationBoardState {
|
|
103
|
+
nodeIdAsUncontrolled: string;
|
|
104
|
+
flippedAsUncontrolled: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Component displaying the positions occurring in a chess game, with navigation buttons to browse these positions.
|
|
110
|
+
*/
|
|
111
|
+
export class NavigationBoard extends React.Component<NavigationBoardProps, NavigationBoardState> {
|
|
112
|
+
|
|
113
|
+
static defaultProps = {
|
|
114
|
+
...defaultDynamicBoardProps(),
|
|
115
|
+
game: new Game(),
|
|
116
|
+
gameIndex: 0,
|
|
117
|
+
initialNodeId: 'start',
|
|
118
|
+
initialFlipped: false,
|
|
119
|
+
flipButtonVisible: true,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
private navigationFieldRef: React.RefObject<NavigationField> = React.createRef();
|
|
123
|
+
|
|
124
|
+
constructor(props: NavigationBoardProps) {
|
|
125
|
+
super(props);
|
|
126
|
+
this.state = {
|
|
127
|
+
nodeIdAsUncontrolled: sanitizeString(props.initialNodeId),
|
|
128
|
+
flippedAsUncontrolled: sanitizeBoolean(props.initialFlipped),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
render() {
|
|
133
|
+
|
|
134
|
+
// Validate the game and game-index attributes.
|
|
135
|
+
const info = parseGame(this.props.game, this.props.gameIndex, 'NavigationBoard');
|
|
136
|
+
if (info.error) {
|
|
137
|
+
return info.errorBox;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Fetch the current node/variation.
|
|
141
|
+
const currentNodeId = sanitizeOptional(this.props.nodeId, sanitizeString) ?? this.state.nodeIdAsUncontrolled;
|
|
142
|
+
const currentNode = info.game.findById(currentNodeId) ?? info.game.mainVariation();
|
|
143
|
+
|
|
144
|
+
// Flip state.
|
|
145
|
+
const flipped = sanitizeOptional(this.props.flipped, sanitizeBoolean) ?? this.state.flippedAsUncontrolled;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="kokopu-navigationBoard">
|
|
149
|
+
{this.renderBoard(info.game, currentNode, flipped)}
|
|
150
|
+
{this.renderNavigationField(info.game, currentNode.id())}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private renderBoard(game: Game, node: GameNode | Variation, flipped: boolean) {
|
|
156
|
+
const position = node instanceof GameNode ? node.positionBefore() : node.initialPosition();
|
|
157
|
+
const move = node instanceof GameNode ? node.notation() : undefined;
|
|
158
|
+
return <Chessboard
|
|
159
|
+
position={position} move={move} flipped={flipped}
|
|
160
|
+
squareMarkers={node.tag('csl')} arrowMarkers={node.tag('cal')} textMarkers={node.tag('ctl')}
|
|
161
|
+
squareSize={this.props.squareSize}
|
|
162
|
+
coordinateVisible={this.props.coordinateVisible}
|
|
163
|
+
turnVisible={this.props.turnVisible}
|
|
164
|
+
colorset={this.props.colorset}
|
|
165
|
+
pieceset={this.props.pieceset}
|
|
166
|
+
smallScreenLimits={this.props.smallScreenLimits}
|
|
167
|
+
moveArrowVisible={this.props.moveArrowVisible}
|
|
168
|
+
moveArrowColor={this.props.moveArrowColor}
|
|
169
|
+
animated={this.props.animated}
|
|
170
|
+
bottomComponent={({ squareSize }) => this.renderToolbar(game, node.id(), squareSize)}
|
|
171
|
+
/>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private renderNavigationField(game: Game, currentNodeId: string) {
|
|
175
|
+
return <NavigationField ref={this.navigationFieldRef}
|
|
176
|
+
onFirstPressed={() => this.handleNavigationButtonClicked(firstNodeId(game, currentNodeId))}
|
|
177
|
+
onPreviousPressed={() => this.handleNavigationButtonClicked(previousNodeId(game, currentNodeId))}
|
|
178
|
+
onNextPressed={() => this.handleNavigationButtonClicked(nextNodeId(game, currentNodeId))}
|
|
179
|
+
onLastPressed={() => this.handleNavigationButtonClicked(lastNodeId(game, currentNodeId))}
|
|
180
|
+
/>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private renderToolbar(game: Game, currentNodeId: string, squareSize: number) {
|
|
184
|
+
const flipButtonVisible = sanitizeBoolean(this.props.flipButtonVisible);
|
|
185
|
+
return <NavigationToolbar flipButtonVisible={flipButtonVisible} squareSize={squareSize}
|
|
186
|
+
onFirstClicked={() => this.handleNavigationButtonClicked(firstNodeId(game, currentNodeId))}
|
|
187
|
+
onPreviousClicked={() => this.handleNavigationButtonClicked(previousNodeId(game, currentNodeId))}
|
|
188
|
+
onNextClicked={() => this.handleNavigationButtonClicked(nextNodeId(game, currentNodeId))}
|
|
189
|
+
onLastClicked={() => this.handleNavigationButtonClicked(lastNodeId(game, currentNodeId))}
|
|
190
|
+
onFlipClicked={() => this.handleFlipButtonClicked()}
|
|
191
|
+
/>;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private handleNavigationButtonClicked(targetNodeId: string | undefined) {
|
|
195
|
+
this.focus();
|
|
196
|
+
if (targetNodeId === undefined) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (this.props.nodeId === undefined) { // uncontrolled-component behavior
|
|
201
|
+
this.setState({ nodeIdAsUncontrolled: targetNodeId });
|
|
202
|
+
}
|
|
203
|
+
else if (this.props.onNodeIdChanged) { // controlled-component behavior
|
|
204
|
+
this.props.onNodeIdChanged(targetNodeId);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private handleFlipButtonClicked() {
|
|
209
|
+
this.focus();
|
|
210
|
+
if (this.props.flipped === undefined) { // uncontrolled flip state
|
|
211
|
+
this.setState({ flippedAsUncontrolled: !this.state.flippedAsUncontrolled });
|
|
212
|
+
}
|
|
213
|
+
else if (this.props.onFlippedChanged) { // controlled flip state
|
|
214
|
+
this.props.onFlippedChanged(!this.props.flipped);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Set the focus to the current component.
|
|
220
|
+
*
|
|
221
|
+
* @public
|
|
222
|
+
*/
|
|
223
|
+
focus(): void {
|
|
224
|
+
const target = this.navigationFieldRef.current;
|
|
225
|
+
// istanbul ignore else
|
|
226
|
+
if (target) {
|
|
227
|
+
target.focus();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* -------------------------------------------------------------------------- *
|
|
3
|
+
* *
|
|
4
|
+
* Kokopu-React - A React-based library of chess-related components. *
|
|
5
|
+
* <https://www.npmjs.com/package/kokopu-react> *
|
|
6
|
+
* Copyright (C) 2021-2024 Yoann Le Montagner <yo35 -at- melix.net> *
|
|
7
|
+
* *
|
|
8
|
+
* Kokopu-React is free software: you can redistribute it and/or *
|
|
9
|
+
* modify it under the terms of the GNU Lesser General Public License *
|
|
10
|
+
* as published by the Free Software Foundation, either version 3 of *
|
|
11
|
+
* the License, or (at your option) any later version. *
|
|
12
|
+
* *
|
|
13
|
+
* Kokopu-React is distributed in the hope that it will be useful, *
|
|
14
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
15
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
16
|
+
* GNU Lesser General Public License for more details. *
|
|
17
|
+
* *
|
|
18
|
+
* You should have received a copy of the GNU Lesser General *
|
|
19
|
+
* Public License along with this program. If not, see *
|
|
20
|
+
* <http://www.gnu.org/licenses/>. *
|
|
21
|
+
* *
|
|
22
|
+
* -------------------------------------------------------------------------- */
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
.kokopu-focusFieldContainer {
|
|
26
|
+
position: fixed;
|
|
27
|
+
bottom: 0px;
|
|
28
|
+
right: 0px;
|
|
29
|
+
width: 0px;
|
|
30
|
+
height: 0px;
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.kokopu-focusField {
|
|
35
|
+
position: absolute; /* see #1 */
|
|
36
|
+
}
|