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.
Files changed (79) hide show
  1. package/.nycrc.yml +13 -0
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +3 -0
  4. package/dist/lib/ArrowMarkerIcon.js +1 -1
  5. package/dist/lib/Chessboard.js +267 -89
  6. package/dist/lib/{impl/ErrorBox.js → ErrorBox.js} +6 -7
  7. package/dist/lib/css/chessboard.css +4 -0
  8. package/dist/lib/i18n.js +35 -0
  9. package/dist/lib/index.js +16 -0
  10. package/doc_src/{demo/.eslintrc.yml → .eslintrc.yml} +0 -0
  11. package/doc_src/demo/PageChessboardBase.js +35 -40
  12. package/doc_src/demo/PageChessboardEdition.js +70 -70
  13. package/doc_src/demo/PageChessboardMove.js +20 -27
  14. package/doc_src/demo/PageChessboardSmallScreens.js +15 -16
  15. package/doc_src/examples/ErrorBox.md +3 -0
  16. package/doc_src/theming/LogoRenderer.js +50 -0
  17. package/doc_src/theming/kokopu-react-logo.png +0 -0
  18. package/doc_src/theming/theming.css +29 -0
  19. package/graphic_test_app/02_chessboard_simple.js +1 -0
  20. package/graphic_test_app/04_chessboard_annotations.js +1 -1
  21. package/graphic_test_app/05_chessboard_move.js +1 -0
  22. package/graphic_test_app/07_chessboard_click_squares.js +36 -0
  23. package/graphic_test_app/08_chessboard_move_pieces.js +36 -0
  24. package/graphic_test_app/09_chessboard_edit_arrows.js +36 -0
  25. package/graphic_test_app/10_chessboard_play_moves.js +39 -0
  26. package/graphic_test_app/11_chessboard_play_promotions.js +35 -0
  27. package/graphic_test_app/common/test_app.js +14 -0
  28. package/graphics/logo-small.svg +14 -0
  29. package/graphics/logo.svg +26 -0
  30. package/package.json +26 -20
  31. package/scripts/test_graphic/docker-compose.yml +1 -1
  32. package/scripts/test_graphic/webpack-config.js +6 -1
  33. package/scripts/test_headless.webpack-config.js +6 -1
  34. package/src/Chessboard.js +156 -40
  35. package/src/{impl/ErrorBox.js → ErrorBox.js} +4 -5
  36. package/src/css/chessboard.css +4 -0
  37. package/src/i18n.js +30 -0
  38. package/src/index.js +2 -0
  39. package/test/4_chessboard_graphic.js +2 -2
  40. package/test/5_chessboard_interaction.js +135 -0
  41. package/test/6_chessboard_play_moves.js +107 -0
  42. package/test/common/graphic.js +75 -15
  43. package/test/references/02_chessboard_simple/2.png +0 -0
  44. package/test/references/02_chessboard_simple/5.png +0 -0
  45. package/test/references/03_chessboard_flipped/2.png +0 -0
  46. package/test/references/05_chessboard_move/8.png +0 -0
  47. package/test/references/08_chessboard_move_pieces/after_move.png +0 -0
  48. package/test/references/08_chessboard_move_pieces/empty_square.png +0 -0
  49. package/test/references/08_chessboard_move_pieces/null_vector.png +0 -0
  50. package/test/references/08_chessboard_move_pieces/out_of_board.png +0 -0
  51. package/test/references/08_chessboard_move_pieces/over_arrow_marker.png +0 -0
  52. package/test/references/08_chessboard_move_pieces/over_empty.png +0 -0
  53. package/test/references/08_chessboard_move_pieces/over_non_empty_1.png +0 -0
  54. package/test/references/08_chessboard_move_pieces/over_non_empty_2.png +0 -0
  55. package/test/references/08_chessboard_move_pieces/over_square_marker.png +0 -0
  56. package/test/references/08_chessboard_move_pieces/over_text_marker.png +0 -0
  57. package/test/references/09_chessboard_edit_arrows/base_1.png +0 -0
  58. package/test/references/09_chessboard_edit_arrows/base_2.png +0 -0
  59. package/test/references/09_chessboard_edit_arrows/edit_color_not_set.png +0 -0
  60. package/test/references/09_chessboard_edit_arrows/null_vector.png +0 -0
  61. package/test/references/09_chessboard_edit_arrows/out_of_board.png +0 -0
  62. package/test/references/09_chessboard_edit_arrows/over_arrow_marker.png +0 -0
  63. package/test/references/09_chessboard_edit_arrows/over_square_marker.png +0 -0
  64. package/test/references/10_chessboard_play_moves/castling_move.png +0 -0
  65. package/test/references/10_chessboard_play_moves/chess960_ambiguous_king_move.png +0 -0
  66. package/test/references/10_chessboard_play_moves/chess960_castling_move_1.png +0 -0
  67. package/test/references/10_chessboard_play_moves/chess960_castling_move_2.png +0 -0
  68. package/test/references/10_chessboard_play_moves/chess960_non_kxr_castling.png +0 -0
  69. package/test/references/10_chessboard_play_moves/illegal_move.png +0 -0
  70. package/test/references/10_chessboard_play_moves/illegal_position.png +0 -0
  71. package/test/references/10_chessboard_play_moves/null_vector.png +0 -0
  72. package/test/references/10_chessboard_play_moves/out_of_board.png +0 -0
  73. package/test/references/10_chessboard_play_moves/regular_move_1.png +0 -0
  74. package/test/references/10_chessboard_play_moves/regular_move_2.png +0 -0
  75. package/test/references/10_chessboard_play_moves/wrong_color.png +0 -0
  76. package/test/references/11_chessboard_play_promotions/antichess_promotion.png +0 -0
  77. package/test/references/11_chessboard_play_promotions/cancel_promotion.png +0 -0
  78. package/test/references/11_chessboard_play_promotions/regular_promotion_1.png +0 -0
  79. 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 './impl/ErrorBox';
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 parseInfo = parsePosition(this.props.position);
82
- if (parseInfo.error) {
83
- return <ErrorBox title="Error while analysing a FEN string." message={parseInfo.message}></ErrorBox>;
84
- }
85
- let position = parseInfo.position;
86
- let move = null;
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 === '-' || (this.isPieceDragModeEnabled() && this.state.draggedSquare === sq)) {
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.isPieceDragModeEnabled() || this.state.draggedSquare === '-') {
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.isArrowDragModeEnabled() || this.state.draggedSquare === '-') {
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
- if ((this.isPieceDragModeEnabled() && position.square(sq) !== '-') || this.isArrowDragModeEnabled()) {
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 bounds = this.isPieceDragModeEnabled() ? { left: -x, top: -y, right: 7 * squareSize - x, bottom: 7 * squareSize - y } : undefined;
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} bounds={bounds}
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.isPieceDragModeEnabled() && this.props.onPieceMoved) {
469
+ if (this.isMovePieceModeEnabled() && this.props.onPieceMoved) {
454
470
  this.props.onPieceMoved(sq, targetSq);
455
471
  }
456
- else if (this.isArrowDragModeEnabled() && this.props.onArrowEdited) {
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
- isPieceDragModeEnabled() {
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
- isArrowDragModeEnabled() {
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: 'Invalid "position" attribute.' };
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: 'Invalid "move" attribute.' };
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 '../css/error_box.css';
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
- * Optional message providing additional details about the error.
49
+ * Additional message providing details about the error.
51
50
  */
52
- message: PropTypes.string,
51
+ message: PropTypes.string.isRequired,
53
52
  };
@@ -48,3 +48,7 @@
48
48
  .kokopu-arrowDraggable {
49
49
  cursor: crosshair;
50
50
  }
51
+
52
+ .kokopu-drawerMask {
53
+ opacity: 0.5;
54
+ }
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
+ });