kokopu-react 1.0.2 → 1.3.1
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/.nycrc.yml +13 -0
- package/CHANGELOG.md +18 -0
- package/README.md +3 -0
- package/dist/lib/ArrowMarkerIcon.js +1 -1
- package/dist/lib/Chessboard.js +267 -89
- package/dist/lib/{impl/ErrorBox.js → ErrorBox.js} +6 -7
- package/dist/lib/css/chessboard.css +4 -0
- package/dist/lib/i18n.js +35 -0
- package/dist/lib/index.js +16 -0
- package/doc_src/{demo/.eslintrc.yml → .eslintrc.yml} +0 -0
- package/doc_src/demo/PageChessboardBase.js +35 -40
- package/doc_src/demo/PageChessboardEdition.js +70 -70
- package/doc_src/demo/PageChessboardMove.js +20 -27
- package/doc_src/demo/PageChessboardSmallScreens.js +15 -16
- package/doc_src/examples/ErrorBox.md +3 -0
- package/doc_src/theming/LogoRenderer.js +50 -0
- package/doc_src/theming/kokopu-react-logo.png +0 -0
- package/doc_src/theming/theming.css +29 -0
- package/graphic_test_app/02_chessboard_simple.js +1 -0
- package/graphic_test_app/04_chessboard_annotations.js +1 -1
- package/graphic_test_app/05_chessboard_move.js +1 -0
- package/graphic_test_app/07_chessboard_click_squares.js +36 -0
- package/graphic_test_app/08_chessboard_move_pieces.js +36 -0
- package/graphic_test_app/09_chessboard_edit_arrows.js +36 -0
- package/graphic_test_app/10_chessboard_play_moves.js +39 -0
- package/graphic_test_app/11_chessboard_play_promotions.js +35 -0
- package/graphic_test_app/common/test_app.js +14 -0
- package/graphics/logo-small.svg +14 -0
- package/graphics/logo.svg +26 -0
- package/package.json +26 -20
- package/scripts/test_graphic/docker-compose.yml +1 -1
- package/scripts/test_graphic/webpack-config.js +6 -1
- package/scripts/test_headless.webpack-config.js +6 -1
- package/src/Chessboard.js +156 -40
- package/src/{impl/ErrorBox.js → ErrorBox.js} +4 -5
- package/src/css/chessboard.css +4 -0
- package/src/i18n.js +30 -0
- package/src/index.js +2 -0
- package/test/4_chessboard_graphic.js +2 -2
- package/test/5_chessboard_interaction.js +135 -0
- package/test/6_chessboard_play_moves.js +107 -0
- package/test/common/graphic.js +75 -15
- package/test/references/02_chessboard_simple/2.png +0 -0
- package/test/references/02_chessboard_simple/5.png +0 -0
- package/test/references/03_chessboard_flipped/2.png +0 -0
- package/test/references/05_chessboard_move/8.png +0 -0
- package/test/references/08_chessboard_move_pieces/after_move.png +0 -0
- package/test/references/08_chessboard_move_pieces/empty_square.png +0 -0
- package/test/references/08_chessboard_move_pieces/null_vector.png +0 -0
- package/test/references/08_chessboard_move_pieces/out_of_board.png +0 -0
- package/test/references/08_chessboard_move_pieces/over_arrow_marker.png +0 -0
- package/test/references/08_chessboard_move_pieces/over_empty.png +0 -0
- package/test/references/08_chessboard_move_pieces/over_non_empty_1.png +0 -0
- package/test/references/08_chessboard_move_pieces/over_non_empty_2.png +0 -0
- package/test/references/08_chessboard_move_pieces/over_square_marker.png +0 -0
- package/test/references/08_chessboard_move_pieces/over_text_marker.png +0 -0
- package/test/references/09_chessboard_edit_arrows/base_1.png +0 -0
- package/test/references/09_chessboard_edit_arrows/base_2.png +0 -0
- package/test/references/09_chessboard_edit_arrows/edit_color_not_set.png +0 -0
- package/test/references/09_chessboard_edit_arrows/null_vector.png +0 -0
- package/test/references/09_chessboard_edit_arrows/out_of_board.png +0 -0
- package/test/references/09_chessboard_edit_arrows/over_arrow_marker.png +0 -0
- package/test/references/09_chessboard_edit_arrows/over_square_marker.png +0 -0
- package/test/references/10_chessboard_play_moves/castling_move.png +0 -0
- package/test/references/10_chessboard_play_moves/chess960_ambiguous_king_move.png +0 -0
- package/test/references/10_chessboard_play_moves/chess960_castling_move_1.png +0 -0
- package/test/references/10_chessboard_play_moves/chess960_castling_move_2.png +0 -0
- package/test/references/10_chessboard_play_moves/chess960_non_kxr_castling.png +0 -0
- package/test/references/10_chessboard_play_moves/illegal_move.png +0 -0
- package/test/references/10_chessboard_play_moves/illegal_position.png +0 -0
- package/test/references/10_chessboard_play_moves/null_vector.png +0 -0
- package/test/references/10_chessboard_play_moves/out_of_board.png +0 -0
- package/test/references/10_chessboard_play_moves/regular_move_1.png +0 -0
- package/test/references/10_chessboard_play_moves/regular_move_2.png +0 -0
- package/test/references/10_chessboard_play_moves/wrong_color.png +0 -0
- package/test/references/11_chessboard_play_promotions/antichess_promotion.png +0 -0
- package/test/references/11_chessboard_play_promotions/cancel_promotion.png +0 -0
- package/test/references/11_chessboard_play_promotions/regular_promotion_1.png +0 -0
- package/test/references/11_chessboard_play_promotions/regular_promotion_2.png +0 -0
package/src/Chessboard.js
CHANGED
|
@@ -29,7 +29,8 @@ import kokopu from 'kokopu';
|
|
|
29
29
|
import colorsets from './impl/colorsets';
|
|
30
30
|
import piecesets from './impl/piecesets';
|
|
31
31
|
import ArrowTip from './impl/ArrowTip';
|
|
32
|
-
import ErrorBox from './
|
|
32
|
+
import ErrorBox from './ErrorBox';
|
|
33
|
+
import i18n from './i18n';
|
|
33
34
|
import { parseSquareMarkers, parseTextMarkers, parseArrowMarkers } from './markers';
|
|
34
35
|
import { MIN_SQUARE_SIZE, MAX_SQUARE_SIZE, sanitizeInteger, generateRandomId, isValidSquare, isValidVector, isValidColor, isValidSymbol } from './impl/util';
|
|
35
36
|
|
|
@@ -59,8 +60,10 @@ export default class Chessboard extends React.Component {
|
|
|
59
60
|
constructor(props) {
|
|
60
61
|
super(props);
|
|
61
62
|
this.state = {
|
|
63
|
+
inhibitedSquare: '-',
|
|
62
64
|
draggedSquare: '-',
|
|
63
65
|
hoveredSquare: '-',
|
|
66
|
+
promotionDrawer: false,
|
|
64
67
|
windowWidth: window.innerWidth,
|
|
65
68
|
};
|
|
66
69
|
this.arrowTipIdSuffix = generateRandomId();
|
|
@@ -78,23 +81,14 @@ export default class Chessboard extends React.Component {
|
|
|
78
81
|
render() {
|
|
79
82
|
|
|
80
83
|
// Compute the current position.
|
|
81
|
-
let
|
|
82
|
-
if (
|
|
83
|
-
return <ErrorBox title=
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
let positionBefore = null;
|
|
88
|
-
if (this.props.move) {
|
|
89
|
-
parseInfo = parseMove(position, this.props.move);
|
|
90
|
-
if (parseInfo.error) {
|
|
91
|
-
return <ErrorBox title="Invalid move notation." message={parseInfo.message}></ErrorBox>;
|
|
92
|
-
}
|
|
93
|
-
move = parseInfo.move;
|
|
94
|
-
positionBefore = position;
|
|
95
|
-
position = new kokopu.Position(position);
|
|
96
|
-
position.play(move);
|
|
84
|
+
let info = this.getPositionAndMoveInfo();
|
|
85
|
+
if (info.positionError) {
|
|
86
|
+
return <ErrorBox title={i18n.INVALID_FEN_ERROR_TITLE} message={info.message}></ErrorBox>;
|
|
87
|
+
}
|
|
88
|
+
else if (info.moveError) {
|
|
89
|
+
return <ErrorBox title={i18n.INVALID_NOTATION_ERROR_TITLE} message={info.message}></ErrorBox>;
|
|
97
90
|
}
|
|
91
|
+
let { position, move, positionBefore } = info;
|
|
98
92
|
|
|
99
93
|
// Compute the attributes.
|
|
100
94
|
let squareSize = this.getSquareSize();
|
|
@@ -199,6 +193,7 @@ export default class Chessboard extends React.Component {
|
|
|
199
193
|
{this.renderArrowMarkers(arm, squareSize, colorset)}
|
|
200
194
|
{this.renderMoveArrow(move, 1, squareSize, colorset)}
|
|
201
195
|
{handles}
|
|
196
|
+
{this.renderPromotionDrawer(position, squareSize, colorset, pieceset)}
|
|
202
197
|
{this.renderDraggedPiece(position, squareSize, pieceset)}
|
|
203
198
|
{this.renderDraggedArrow(squareSize, colorset)}
|
|
204
199
|
{this.renderTurnFlag(position.turn(), squareSize, pieceset)}
|
|
@@ -223,7 +218,7 @@ export default class Chessboard extends React.Component {
|
|
|
223
218
|
|
|
224
219
|
renderPiece(position, squareSize, pieceset, sq) {
|
|
225
220
|
let cp = position.square(sq);
|
|
226
|
-
if (cp === '-' ||
|
|
221
|
+
if (cp === '-' || this.state.inhibitedSquare === sq) {
|
|
227
222
|
return undefined;
|
|
228
223
|
}
|
|
229
224
|
let { x, y } = this.getSquareCoordinates(squareSize, sq);
|
|
@@ -253,21 +248,18 @@ export default class Chessboard extends React.Component {
|
|
|
253
248
|
}
|
|
254
249
|
|
|
255
250
|
renderDraggedPiece(position, squareSize, pieceset) {
|
|
256
|
-
if (!this.
|
|
251
|
+
if (!(this.isMovePieceModeEnabled() || this.isPlayMoveModeEnabled()) || this.state.draggedSquare === '-') {
|
|
257
252
|
return undefined;
|
|
258
253
|
}
|
|
259
254
|
let { x, y } = this.getSquareCoordinates(squareSize, this.state.draggedSquare);
|
|
255
|
+
x = Math.min(Math.max(x + this.state.dragPosition.x, 0), 7 * squareSize);
|
|
256
|
+
y = Math.min(Math.max(y + this.state.dragPosition.y, 0), 7 * squareSize);
|
|
260
257
|
let cp = position.square(this.state.draggedSquare);
|
|
261
|
-
return
|
|
262
|
-
<image
|
|
263
|
-
className="kokopu-pieceDraggable kokopu-dragging"
|
|
264
|
-
x={x + this.state.dragPosition.x} y={y + this.state.dragPosition.y} width={squareSize} height={squareSize} href={pieceset[cp]}
|
|
265
|
-
/>
|
|
266
|
-
);
|
|
258
|
+
return <image className="kokopu-pieceDraggable kokopu-dragging" x={x} y={y} width={squareSize} height={squareSize} href={pieceset[cp]} />;
|
|
267
259
|
}
|
|
268
260
|
|
|
269
261
|
renderDraggedArrow(squareSize, colorset) {
|
|
270
|
-
if (!this.
|
|
262
|
+
if (!this.isEditArrowModeEnabled() || this.state.draggedSquare === '-') {
|
|
271
263
|
return undefined;
|
|
272
264
|
}
|
|
273
265
|
let strokeWidth = squareSize * STROKE_THICKNESS_FACTOR;
|
|
@@ -285,15 +277,39 @@ export default class Chessboard extends React.Component {
|
|
|
285
277
|
);
|
|
286
278
|
}
|
|
287
279
|
|
|
280
|
+
renderPromotionDrawer(position, squareSize, colorset, pieceset) {
|
|
281
|
+
if (!this.state.promotionDrawer) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
let { x, y } = this.getSquareCoordinates(squareSize, this.state.promotionDrawer.origin);
|
|
285
|
+
let inverted = position.turn() === (this.props.flipped ? 'w' : 'b'); // false==top-to-bottom true==bottom-to-top
|
|
286
|
+
let y0 = inverted ? y - squareSize * (this.state.promotionDrawer.buttons.length - 1) : y;
|
|
287
|
+
let buttons = this.state.promotionDrawer.buttons.map((p, i) => {
|
|
288
|
+
let cp = position.turn() + p;
|
|
289
|
+
return <image key={'drawer-piece-' + p} className="kokopu-clickable" x={x} y={y + i * (inverted ? -squareSize : squareSize)}
|
|
290
|
+
width={squareSize} height={squareSize} href={pieceset[cp]} onClick={() => this.handleDrawerButtonClicked(p)} />;
|
|
291
|
+
});
|
|
292
|
+
return (
|
|
293
|
+
<>
|
|
294
|
+
<rect className="kokopu-handle" x={0} y={0} width={squareSize * 8} height={squareSize * 8} onClick={() => this.handleDrawerCancelButtonClicked()} />
|
|
295
|
+
<rect x={x} y={y0} width={squareSize} height={squareSize * this.state.promotionDrawer.buttons.length} fill={colorset.b} />
|
|
296
|
+
<rect x={x} y={y0} width={squareSize} height={squareSize * this.state.promotionDrawer.buttons.length} className="kokopu-drawerMask" fill={colorset.w} />
|
|
297
|
+
{buttons}
|
|
298
|
+
</>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
288
302
|
renderSquareHandle(position, squareSize, sq) {
|
|
289
303
|
let { x, y } = this.getSquareCoordinates(squareSize, sq);
|
|
290
|
-
|
|
304
|
+
let dragEnabledForMovePieces = this.isMovePieceModeEnabled() && position.square(sq) !== '-';
|
|
305
|
+
let dragEnabledForEditArrows = this.isEditArrowModeEnabled();
|
|
306
|
+
let dragEnabledForPlayMoves = this.isPlayMoveModeEnabled() && position.isLegal() && position.square(sq).startsWith(position.turn()) && !this.state.promotionDrawer;
|
|
307
|
+
if (dragEnabledForMovePieces || dragEnabledForEditArrows || dragEnabledForPlayMoves) {
|
|
291
308
|
let dragPosition = this.state.draggedSquare === sq ? this.state.dragPosition : { x: 0, y: 0 };
|
|
292
|
-
let
|
|
293
|
-
let classNames = [ 'kokopu-handle', this.isPieceDragModeEnabled() ? 'kokopu-pieceDraggable' : 'kokopu-arrowDraggable' ];
|
|
309
|
+
let classNames = [ 'kokopu-handle', this.isEditArrowModeEnabled() ? 'kokopu-arrowDraggable' : 'kokopu-pieceDraggable' ];
|
|
294
310
|
return (
|
|
295
311
|
<Draggable
|
|
296
|
-
key={'handle-' + sq} position={dragPosition}
|
|
312
|
+
key={'handle-' + sq} position={dragPosition}
|
|
297
313
|
onStart={evt => this.handleDragStart(sq, evt)}
|
|
298
314
|
onDrag={(_, dragData) => this.handleDrag(sq, dragData)}
|
|
299
315
|
onStop={(_, dragData) => this.handleDragStop(sq, dragData)}
|
|
@@ -420,6 +436,7 @@ export default class Chessboard extends React.Component {
|
|
|
420
436
|
handleDragStart(sq, evt) {
|
|
421
437
|
let squareBoundary = evt.target.getBoundingClientRect();
|
|
422
438
|
this.setState({
|
|
439
|
+
inhibitedSquare: this.isMovePieceModeEnabled() || this.isPlayMoveModeEnabled() ? sq : '-',
|
|
423
440
|
draggedSquare: sq,
|
|
424
441
|
hoveredSquare: sq,
|
|
425
442
|
cursorOffset: { x: evt.clientX - squareBoundary.left, y: evt.clientY - squareBoundary.top },
|
|
@@ -432,9 +449,7 @@ export default class Chessboard extends React.Component {
|
|
|
432
449
|
let { x, y } = this.getSquareCoordinates(squareSize, sq);
|
|
433
450
|
let targetSq = this.getSquareAt(squareSize, x + dragData.x + this.state.cursorOffset.x, y + dragData.y + this.state.cursorOffset.y);
|
|
434
451
|
this.setState({
|
|
435
|
-
draggedSquare: sq,
|
|
436
452
|
hoveredSquare: targetSq,
|
|
437
|
-
cursorOffset: this.state.cursorOffset,
|
|
438
453
|
dragPosition: { x: dragData.x, y: dragData.y },
|
|
439
454
|
});
|
|
440
455
|
}
|
|
@@ -444,18 +459,70 @@ export default class Chessboard extends React.Component {
|
|
|
444
459
|
let { x, y } = this.getSquareCoordinates(squareSize, sq);
|
|
445
460
|
let targetSq = this.getSquareAt(squareSize, x + dragData.x + this.state.cursorOffset.x, y + dragData.y + this.state.cursorOffset.y);
|
|
446
461
|
this.setState({
|
|
462
|
+
inhibitedSquare: '-',
|
|
447
463
|
draggedSquare: '-',
|
|
448
464
|
hoveredSquare: '-',
|
|
449
465
|
});
|
|
450
|
-
if (sq === targetSq) {
|
|
466
|
+
if (sq === targetSq || targetSq === '-') {
|
|
451
467
|
return;
|
|
452
468
|
}
|
|
453
|
-
if (this.
|
|
469
|
+
if (this.isMovePieceModeEnabled() && this.props.onPieceMoved) {
|
|
454
470
|
this.props.onPieceMoved(sq, targetSq);
|
|
455
471
|
}
|
|
456
|
-
else if (this.
|
|
472
|
+
else if (this.isEditArrowModeEnabled() && this.props.onArrowEdited) {
|
|
457
473
|
this.props.onArrowEdited(sq, targetSq);
|
|
458
474
|
}
|
|
475
|
+
else if (this.isPlayMoveModeEnabled()) {
|
|
476
|
+
let { position } = this.getPositionAndMoveInfo();
|
|
477
|
+
let info = position.isMoveLegal(sq, targetSq);
|
|
478
|
+
if (!info) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
switch (info.status) {
|
|
482
|
+
|
|
483
|
+
// Regular move.
|
|
484
|
+
case 'regular':
|
|
485
|
+
if (this.props.onMovePlayed) {
|
|
486
|
+
this.props.onMovePlayed(position.notation(info()));
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
|
|
490
|
+
// Promotion move.
|
|
491
|
+
case 'promotion':
|
|
492
|
+
this.setState({
|
|
493
|
+
inhibitedSquare: sq,
|
|
494
|
+
promotionDrawer: {
|
|
495
|
+
origin: targetSq,
|
|
496
|
+
buttons: position.variant() === 'antichess' ? [ 'q', 'r', 'b', 'n', 'k' ] : [ 'q', 'r', 'b', 'n' ],
|
|
497
|
+
builder: piece => position.notation(info(piece)),
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
break;
|
|
501
|
+
|
|
502
|
+
// Other cases are not supposed to happen.
|
|
503
|
+
// istanbul ignore next
|
|
504
|
+
default:
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
handleDrawerCancelButtonClicked() {
|
|
511
|
+
this.setState({
|
|
512
|
+
inhibitedSquare: '-',
|
|
513
|
+
promotionDrawer: false,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
handleDrawerButtonClicked(piece) {
|
|
518
|
+
let builder = this.state.promotionDrawer.builder;
|
|
519
|
+
this.setState({
|
|
520
|
+
inhibitedSquare: '-',
|
|
521
|
+
promotionDrawer: false,
|
|
522
|
+
});
|
|
523
|
+
if (this.props.onMovePlayed) {
|
|
524
|
+
this.props.onMovePlayed(builder(piece));
|
|
525
|
+
}
|
|
459
526
|
}
|
|
460
527
|
|
|
461
528
|
handleSquareClicked(sq) {
|
|
@@ -467,14 +534,21 @@ export default class Chessboard extends React.Component {
|
|
|
467
534
|
/**
|
|
468
535
|
* Whether the "move piece" mode is enabled or not.
|
|
469
536
|
*/
|
|
470
|
-
|
|
537
|
+
isMovePieceModeEnabled() {
|
|
471
538
|
return this.props.interactionMode === 'movePieces';
|
|
472
539
|
}
|
|
473
540
|
|
|
541
|
+
/**
|
|
542
|
+
* Whether the "play move" mode is enabled or not.
|
|
543
|
+
*/
|
|
544
|
+
isPlayMoveModeEnabled() {
|
|
545
|
+
return this.props.interactionMode === 'playMoves';
|
|
546
|
+
}
|
|
547
|
+
|
|
474
548
|
/**
|
|
475
549
|
* Whether the "edit arrow" mode is enabled or not.
|
|
476
550
|
*/
|
|
477
|
-
|
|
551
|
+
isEditArrowModeEnabled() {
|
|
478
552
|
return this.props.interactionMode === 'editArrows' && isValidColor(this.props.editedArrowColor);
|
|
479
553
|
}
|
|
480
554
|
|
|
@@ -505,6 +579,34 @@ export default class Chessboard extends React.Component {
|
|
|
505
579
|
return { x: x, y: y };
|
|
506
580
|
}
|
|
507
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Return information regarding the current displayed position and move, or parsing error message if something went wrong.
|
|
584
|
+
*/
|
|
585
|
+
getPositionAndMoveInfo() {
|
|
586
|
+
|
|
587
|
+
// Parse the position.
|
|
588
|
+
let positionInfo = parsePosition(this.props.position);
|
|
589
|
+
if (positionInfo.error) {
|
|
590
|
+
return { positionError: true, moveError: true, message: positionInfo.message };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Nothing else to do if no move is defined.
|
|
594
|
+
if (!this.props.move) {
|
|
595
|
+
return { positionError: false, moveError: false, position: positionInfo.position };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Parse the move.
|
|
599
|
+
let moveInfo = parseMove(positionInfo.position, this.props.move);
|
|
600
|
+
if (moveInfo.error) {
|
|
601
|
+
return { positionError: false, moveError: true, message: moveInfo.message, positionBefore: positionInfo.position };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Compute the position after the move and return the result.
|
|
605
|
+
let positionAfter = new kokopu.Position(positionInfo.position);
|
|
606
|
+
positionAfter.play(moveInfo.move);
|
|
607
|
+
return { positionError: false, moveError: false, positionBefore: positionInfo.position, move: moveInfo.move, position: positionAfter };
|
|
608
|
+
}
|
|
609
|
+
|
|
508
610
|
/**
|
|
509
611
|
* Return the square at the given location.
|
|
510
612
|
*/
|
|
@@ -736,8 +838,13 @@ Chessboard.propTypes = {
|
|
|
736
838
|
|
|
737
839
|
/**
|
|
738
840
|
* Type of action allowed with the mouse on the chessboard. If undefined, then the user cannot interact with the component.
|
|
841
|
+
*
|
|
842
|
+
* - `'movePieces'` allows the user to drag & drop the chess pieces from one square to another (regardless of the chess rules),
|
|
843
|
+
* - `'clickSquares'` allows the user to click on squares,
|
|
844
|
+
* - `'editArrows'` allows the user to draw arrow markers from one square to another (warning: attribute `editedArrowColor` must be set),
|
|
845
|
+
* - `'playMoves'` allows the user to play legal chess moves (thus no interaction is possible if the displayed position is not legal).
|
|
739
846
|
*/
|
|
740
|
-
interactionMode: PropTypes.oneOf([ 'movePieces', 'clickSquares', 'editArrows' ]),
|
|
847
|
+
interactionMode: PropTypes.oneOf([ 'movePieces', 'clickSquares', 'editArrows', 'playMoves' ]),
|
|
741
848
|
|
|
742
849
|
/**
|
|
743
850
|
* Color of the edited arrow (only used if `interactionMode` is set to `'editArrows'`).
|
|
@@ -766,6 +873,13 @@ Chessboard.propTypes = {
|
|
|
766
873
|
* @param {string} to Target square (e.g. `'f3'`).
|
|
767
874
|
*/
|
|
768
875
|
onArrowEdited: PropTypes.func,
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Callback invoked when a move is played (only if `interactionMode` is set to `'playMoves'`).
|
|
879
|
+
*
|
|
880
|
+
* @param {string} move SAN notation representing the played move (e.g. `Nxe5`).
|
|
881
|
+
*/
|
|
882
|
+
onMovePlayed: PropTypes.func,
|
|
769
883
|
};
|
|
770
884
|
|
|
771
885
|
|
|
@@ -815,6 +929,7 @@ function parsePosition(position) {
|
|
|
815
929
|
return { error: false, position: new kokopu.Position(position) };
|
|
816
930
|
}
|
|
817
931
|
catch (e) {
|
|
932
|
+
// istanbul ignore else
|
|
818
933
|
if (e instanceof kokopu.exception.InvalidFEN) {
|
|
819
934
|
return { error: true, message: e.message };
|
|
820
935
|
}
|
|
@@ -824,7 +939,7 @@ function parsePosition(position) {
|
|
|
824
939
|
}
|
|
825
940
|
}
|
|
826
941
|
else {
|
|
827
|
-
return { error: true, message:
|
|
942
|
+
return { error: true, message: i18n.INVALID_POSITION_ATTRIBUTE_ERROR_MESSAGE };
|
|
828
943
|
}
|
|
829
944
|
}
|
|
830
945
|
|
|
@@ -841,6 +956,7 @@ function parseMove(position, move) {
|
|
|
841
956
|
return { error: false, move: position.notation(move) };
|
|
842
957
|
}
|
|
843
958
|
catch (e) {
|
|
959
|
+
// istanbul ignore else
|
|
844
960
|
if (e instanceof kokopu.exception.InvalidNotation) {
|
|
845
961
|
return { error: true, message: e.message };
|
|
846
962
|
}
|
|
@@ -850,7 +966,7 @@ function parseMove(position, move) {
|
|
|
850
966
|
}
|
|
851
967
|
}
|
|
852
968
|
else {
|
|
853
|
-
return { error: true, message:
|
|
969
|
+
return { error: true, message: i18n.INVALID_MOVE_ATTRIBUTE_ERROR_MESSAGE };
|
|
854
970
|
}
|
|
855
971
|
}
|
|
856
972
|
|
|
@@ -23,18 +23,17 @@
|
|
|
23
23
|
import PropTypes from 'prop-types';
|
|
24
24
|
import React from 'react';
|
|
25
25
|
|
|
26
|
-
import '
|
|
26
|
+
import './css/error_box.css';
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Display an error message.
|
|
31
31
|
*/
|
|
32
32
|
export default function ErrorBox(props) {
|
|
33
|
-
let message = 'message' in props ? <div className="kokopu-errorMessage">{props.message}</div> : undefined;
|
|
34
33
|
return (
|
|
35
34
|
<div className="kokopu-errorBox">
|
|
36
35
|
<div className="kokopu-errorTitle">{props.title}</div>
|
|
37
|
-
{message}
|
|
36
|
+
<div className="kokopu-errorMessage">{props.message}</div>
|
|
38
37
|
</div>
|
|
39
38
|
);
|
|
40
39
|
}
|
|
@@ -47,7 +46,7 @@ ErrorBox.propTypes = {
|
|
|
47
46
|
title: PropTypes.string.isRequired,
|
|
48
47
|
|
|
49
48
|
/**
|
|
50
|
-
*
|
|
49
|
+
* Additional message providing details about the error.
|
|
51
50
|
*/
|
|
52
|
-
message: PropTypes.string,
|
|
51
|
+
message: PropTypes.string.isRequired,
|
|
53
52
|
};
|
package/src/css/chessboard.css
CHANGED
package/src/i18n.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/******************************************************************************
|
|
2
|
+
* *
|
|
3
|
+
* This file is part of Kokopu-React, a JavaScript chess library. *
|
|
4
|
+
* Copyright (C) 2021 Yoann Le Montagner <yo35 -at- melix.net> *
|
|
5
|
+
* *
|
|
6
|
+
* This program is free software: you can redistribute it and/or *
|
|
7
|
+
* modify it under the terms of the GNU Lesser General Public License *
|
|
8
|
+
* as published by the Free Software Foundation, either version 3 of *
|
|
9
|
+
* the License, or (at your option) any later version. *
|
|
10
|
+
* *
|
|
11
|
+
* This program is distributed in the hope that it will be useful, *
|
|
12
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
13
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
14
|
+
* GNU Lesser General Public License for more details. *
|
|
15
|
+
* *
|
|
16
|
+
* You should have received a copy of the GNU Lesser General *
|
|
17
|
+
* Public License along with this program. If not, see *
|
|
18
|
+
* <http://www.gnu.org/licenses/>. *
|
|
19
|
+
* *
|
|
20
|
+
******************************************************************************/
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const i18n = {
|
|
24
|
+
INVALID_FEN_ERROR_TITLE: 'Invalid FEN string.',
|
|
25
|
+
INVALID_NOTATION_ERROR_TITLE: 'Invalid move notation.',
|
|
26
|
+
INVALID_POSITION_ATTRIBUTE_ERROR_MESSAGE: 'Invalid "position" attribute.',
|
|
27
|
+
INVALID_MOVE_ATTRIBUTE_ERROR_MESSAGE: 'Invalid "move" attribute.',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default i18n;
|
package/src/index.js
CHANGED
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
export { flattenSquareMarkers, parseSquareMarkers, flattenTextMarkers, parseTextMarkers, flattenArrowMarkers, parseArrowMarkers } from './markers';
|
|
24
|
+
export { default as i18n } from './i18n';
|
|
24
25
|
export { default as Chessboard } from './Chessboard';
|
|
26
|
+
export { default as ErrorBox } from './ErrorBox';
|
|
25
27
|
export { default as SquareMarkerIcon } from './SquareMarkerIcon';
|
|
26
28
|
export { default as TextMarkerIcon } from './TextMarkerIcon';
|
|
27
29
|
export { default as ArrowMarkerIcon } from './ArrowMarkerIcon';
|
|
@@ -35,12 +35,12 @@ describe('Chessboard graphic', function() {
|
|
|
35
35
|
await closeBrowser(browserContext);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
itChecksScreenshots(browserContext, '02_chessboard_simple', [ 'default', 'empty', 'invalid', 'from FEN', 'from Kokopu object' ]);
|
|
38
|
+
itChecksScreenshots(browserContext, '02_chessboard_simple', [ 'default', 'empty', 'invalid', 'from FEN', 'from Kokopu object', 'wrong type' ]);
|
|
39
39
|
itChecksScreenshots(browserContext, '03_chessboard_flipped', [ 'default', 'empty', 'invalid', 'from FEN', 'from Kokopu object' ]);
|
|
40
40
|
itChecksScreenshots(browserContext, '04_chessboard_annotations', [ 'with coordinates', 'with coordinates & flip', 'without coordinates', 'without coordinates & flip',
|
|
41
41
|
'overlap', 'overlap & flip' ]);
|
|
42
42
|
itChecksScreenshots(browserContext, '05_chessboard_move', [ 'default', 'no move arrow', 'invalid', 'with forced move arrow & flip', 'capture', 'castling move',
|
|
43
|
-
'en-passant', 'promotion' ]);
|
|
43
|
+
'en-passant', 'promotion', 'wrong type' ]);
|
|
44
44
|
itChecksScreenshots(browserContext, '06_chessboard_theme', [ 'default', 'large', 'small', 'custom 1', 'custom 2', 'custom 3',
|
|
45
45
|
'custom 4', 'custom 5', 'custom 6' ]);
|
|
46
46
|
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/******************************************************************************
|
|
2
|
+
* *
|
|
3
|
+
* This file is part of Kokopu-React, a JavaScript chess library. *
|
|
4
|
+
* Copyright (C) 2021 Yoann Le Montagner <yo35 -at- melix.net> *
|
|
5
|
+
* *
|
|
6
|
+
* This program is free software: you can redistribute it and/or *
|
|
7
|
+
* modify it under the terms of the GNU Lesser General Public License *
|
|
8
|
+
* as published by the Free Software Foundation, either version 3 of *
|
|
9
|
+
* the License, or (at your option) any later version. *
|
|
10
|
+
* *
|
|
11
|
+
* This program is distributed in the hope that it will be useful, *
|
|
12
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
13
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
14
|
+
* GNU Lesser General Public License for more details. *
|
|
15
|
+
* *
|
|
16
|
+
* You should have received a copy of the GNU Lesser General *
|
|
17
|
+
* Public License along with this program. If not, see *
|
|
18
|
+
* <http://www.gnu.org/licenses/>. *
|
|
19
|
+
* *
|
|
20
|
+
******************************************************************************/
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const { openBrowser, closeBrowser, itCustom, setSandbox, compareSandbox, takeScreenshot, compareScreenshot } = require('./common/graphic');
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
describe('Chessboard interaction', function() {
|
|
27
|
+
|
|
28
|
+
const browserContext = {};
|
|
29
|
+
|
|
30
|
+
before(async function() {
|
|
31
|
+
await openBrowser(this, browserContext);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
after(async function() {
|
|
35
|
+
await closeBrowser(browserContext);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function itCheckClickSquare(itemIndex, label, targets) {
|
|
39
|
+
itCustom(browserContext, '07_chessboard_click_squares', itemIndex, label, async function(element) {
|
|
40
|
+
let actions = browserContext.driver.actions({ async: true });
|
|
41
|
+
let area = await element.getRect();
|
|
42
|
+
for (let i = 0; i < targets.length; ++i) {
|
|
43
|
+
let target = targets[i];
|
|
44
|
+
await actions.move({ x: area.x + target.x, y: area.y + target.y }).click().perform();
|
|
45
|
+
await compareSandbox(browserContext, target.expectedText);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
itCheckClickSquare(0, 'default', [
|
|
51
|
+
{ x: 225, y: 75, expectedText: 'square clicked: e7' },
|
|
52
|
+
{ x: 25, y: 375, expectedText: 'square clicked: a1' },
|
|
53
|
+
{ x: 325, y: 175, expectedText: 'square clicked: g5' },
|
|
54
|
+
]);
|
|
55
|
+
itCheckClickSquare(1, 'with flip', [
|
|
56
|
+
{ x: 325, y: 175, expectedText: 'square clicked: b4' },
|
|
57
|
+
]);
|
|
58
|
+
itCheckClickSquare(2, 'over annotations', [
|
|
59
|
+
{ x: 25, y: 25, expectedText: 'square clicked: a8' },
|
|
60
|
+
{ x: 75, y: 125, expectedText: 'square clicked: b6' },
|
|
61
|
+
{ x: 175, y: 125, expectedText: 'square clicked: d6' },
|
|
62
|
+
{ x: 75, y: 225, expectedText: 'square clicked: b4' },
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
function itCheckMovePiece(itemIndex, label, xFrom, yFrom, xTo, yTo, imageBaseName, expectedText) {
|
|
66
|
+
itCustom(browserContext, '08_chessboard_move_pieces', itemIndex, label, async function(element) {
|
|
67
|
+
let actions = browserContext.driver.actions({ async: true });
|
|
68
|
+
let area = await element.getRect();
|
|
69
|
+
await actions.move({ x: area.x + xFrom, y: area.y + yFrom }).press().move({ x: area.x + xTo, y: area.y + yTo }).perform();
|
|
70
|
+
await takeScreenshot(browserContext, imageBaseName, element);
|
|
71
|
+
await actions.release().perform();
|
|
72
|
+
await compareScreenshot(browserContext, imageBaseName);
|
|
73
|
+
await compareSandbox(browserContext, expectedText);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
itCheckMovePiece(0, 'over empty', 275, 385, 225, 175, 'over_empty', 'piece moved: f1 -> e5');
|
|
78
|
+
itCheckMovePiece(0, 'over non-empty 1', 280, 365, 75, 75, 'over_non_empty_1', 'piece moved: f1 -> b7');
|
|
79
|
+
itCheckMovePiece(0, 'over non-empty 2', 130, 75, 225, 375, 'over_non_empty_2', 'piece moved: c7 -> e1');
|
|
80
|
+
itCheckMovePiece(1, 'over square marker', 10, 325, 275, 175, 'over_square_marker', 'piece moved: h7 -> c4');
|
|
81
|
+
itCheckMovePiece(1, 'over text marker', 225, 25, 10, 135, 'over_text_marker', 'piece moved: d1 -> h3');
|
|
82
|
+
itCheckMovePiece(1, 'over arrow marker', 325, 25, 315, 260, 'over_arrow_marker', 'piece moved: b1 -> b6');
|
|
83
|
+
itCheckMovePiece(2, 'after move', 225, 225, 75, 260, 'after_move', 'piece moved: e4 -> b3');
|
|
84
|
+
|
|
85
|
+
function itCheckNonMovePiece(itemIndex, label, xFrom, yFrom, xTo, yTo, imageBaseName) {
|
|
86
|
+
itCustom(browserContext, '08_chessboard_move_pieces', itemIndex, label, async function(element) {
|
|
87
|
+
await setSandbox(browserContext, imageBaseName); // can be any value as long as it is unique among other test-cases
|
|
88
|
+
let actions = browserContext.driver.actions({ async: true });
|
|
89
|
+
let area = await element.getRect();
|
|
90
|
+
await actions.move({ x: area.x + xFrom, y: area.y + yFrom }).press().move({ x: area.x + xTo, y: area.y + yTo }).perform();
|
|
91
|
+
await takeScreenshot(browserContext, imageBaseName, element);
|
|
92
|
+
await actions.release().perform();
|
|
93
|
+
await compareScreenshot(browserContext, imageBaseName);
|
|
94
|
+
await compareSandbox(browserContext, imageBaseName);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
itCheckNonMovePiece(0, 'move empty square', 175, 225, 275, 75, 'empty_square');
|
|
99
|
+
itCheckNonMovePiece(0, 'from == to', 75, 375, 80, 360, 'null_vector');
|
|
100
|
+
itCheckNonMovePiece(0, 'out of board', 175, 25, 500, 210, 'out_of_board');
|
|
101
|
+
|
|
102
|
+
function itCheckEditArrow(itemIndex, label, xFrom, yFrom, xTo, yTo, imageBaseName, expectedText) {
|
|
103
|
+
itCustom(browserContext, '09_chessboard_edit_arrows', itemIndex, label, async function(element) {
|
|
104
|
+
let actions = browserContext.driver.actions({ async: true });
|
|
105
|
+
let area = await element.getRect();
|
|
106
|
+
await actions.move({ x: area.x + xFrom, y: area.y + yFrom }).press().move({ x: area.x + xTo, y: area.y + yTo }).perform();
|
|
107
|
+
await takeScreenshot(browserContext, imageBaseName, element);
|
|
108
|
+
await actions.release().perform();
|
|
109
|
+
await compareScreenshot(browserContext, imageBaseName);
|
|
110
|
+
await compareSandbox(browserContext, expectedText);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
itCheckEditArrow(0, 'base 1', 325, 275, 110, 140, 'base_1', 'arrow edited: g3 -> c6');
|
|
115
|
+
itCheckEditArrow(0, 'base 2', 260, 10, 175, 375, 'base_2', 'arrow edited: f8 -> d1');
|
|
116
|
+
itCheckEditArrow(1, 'over square marker', 275, 125, 275, 230, 'over_square_marker', 'arrow edited: c3 -> c5');
|
|
117
|
+
itCheckEditArrow(1, 'over arrow marker', 40, 110, 125, 290, 'over_arrow_marker', 'arrow edited: h3 -> f6');
|
|
118
|
+
|
|
119
|
+
function itCheckNonEditArrow(itemIndex, label, xFrom, yFrom, xTo, yTo, imageBaseName) {
|
|
120
|
+
itCustom(browserContext, '09_chessboard_edit_arrows', itemIndex, label, async function(element) {
|
|
121
|
+
await setSandbox(browserContext, imageBaseName); // can be any value as long as it is unique among other test-cases
|
|
122
|
+
let actions = browserContext.driver.actions({ async: true });
|
|
123
|
+
let area = await element.getRect();
|
|
124
|
+
await actions.move({ x: area.x + xFrom, y: area.y + yFrom }).press().move({ x: area.x + xTo, y: area.y + yTo }).perform();
|
|
125
|
+
await takeScreenshot(browserContext, imageBaseName, element);
|
|
126
|
+
await actions.release().perform();
|
|
127
|
+
await compareScreenshot(browserContext, imageBaseName);
|
|
128
|
+
await compareSandbox(browserContext, imageBaseName);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
itCheckNonEditArrow(2, 'edit color not set', 125, 175, 325, 225, 'edit_color_not_set');
|
|
133
|
+
itCheckNonEditArrow(0, 'from == to', 275, 225, 290, 210, 'null_vector');
|
|
134
|
+
itCheckNonEditArrow(0, 'out of board', 175, 225, 500, 280, 'out_of_board');
|
|
135
|
+
});
|