kokopu-react 1.5.0 → 1.5.3

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 (91) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/lib/Movetext.js +168 -65
  3. package/doc_src/demo/PageMovetextInteraction.js +10 -6
  4. package/graphic_test_app/16_movetext_interaction.js +3 -2
  5. package/package.json +5 -5
  6. package/scripts/test_graphic/docker-compose.yml +1 -1
  7. package/src/Chessboard.js +2 -2
  8. package/src/Movetext.js +142 -68
  9. package/test/8_movetext_interaction.js +17 -8
  10. package/test/references/01_marker_icons/0.png +0 -0
  11. package/test/references/01_marker_icons/1.png +0 -0
  12. package/test/references/01_marker_icons/10.png +0 -0
  13. package/test/references/01_marker_icons/2.png +0 -0
  14. package/test/references/01_marker_icons/3.png +0 -0
  15. package/test/references/01_marker_icons/4.png +0 -0
  16. package/test/references/01_marker_icons/5.png +0 -0
  17. package/test/references/01_marker_icons/6.png +0 -0
  18. package/test/references/01_marker_icons/7.png +0 -0
  19. package/test/references/01_marker_icons/8.png +0 -0
  20. package/test/references/01_marker_icons/9.png +0 -0
  21. package/test/references/02_chessboard_simple/0.png +0 -0
  22. package/test/references/02_chessboard_simple/1.png +0 -0
  23. package/test/references/02_chessboard_simple/2.png +0 -0
  24. package/test/references/02_chessboard_simple/3.png +0 -0
  25. package/test/references/02_chessboard_simple/4.png +0 -0
  26. package/test/references/02_chessboard_simple/5.png +0 -0
  27. package/test/references/03_chessboard_flipped/0.png +0 -0
  28. package/test/references/03_chessboard_flipped/1.png +0 -0
  29. package/test/references/03_chessboard_flipped/2.png +0 -0
  30. package/test/references/03_chessboard_flipped/3.png +0 -0
  31. package/test/references/03_chessboard_flipped/4.png +0 -0
  32. package/test/references/04_chessboard_annotations/0.png +0 -0
  33. package/test/references/04_chessboard_annotations/1.png +0 -0
  34. package/test/references/04_chessboard_annotations/2.png +0 -0
  35. package/test/references/04_chessboard_annotations/3.png +0 -0
  36. package/test/references/04_chessboard_annotations/4.png +0 -0
  37. package/test/references/04_chessboard_annotations/5.png +0 -0
  38. package/test/references/05_chessboard_move/0.png +0 -0
  39. package/test/references/05_chessboard_move/1.png +0 -0
  40. package/test/references/05_chessboard_move/2.png +0 -0
  41. package/test/references/05_chessboard_move/3.png +0 -0
  42. package/test/references/05_chessboard_move/4.png +0 -0
  43. package/test/references/05_chessboard_move/5.png +0 -0
  44. package/test/references/05_chessboard_move/6.png +0 -0
  45. package/test/references/05_chessboard_move/7.png +0 -0
  46. package/test/references/05_chessboard_move/8.png +0 -0
  47. package/test/references/06_chessboard_theme/0.png +0 -0
  48. package/test/references/06_chessboard_theme/1.png +0 -0
  49. package/test/references/06_chessboard_theme/2.png +0 -0
  50. package/test/references/06_chessboard_theme/3.png +0 -0
  51. package/test/references/06_chessboard_theme/4.png +0 -0
  52. package/test/references/06_chessboard_theme/5.png +0 -0
  53. package/test/references/06_chessboard_theme/6.png +0 -0
  54. package/test/references/06_chessboard_theme/7.png +0 -0
  55. package/test/references/06_chessboard_theme/8.png +0 -0
  56. package/test/references/08_chessboard_move_pieces/after_move.png +0 -0
  57. package/test/references/08_chessboard_move_pieces/empty_square.png +0 -0
  58. package/test/references/08_chessboard_move_pieces/null_vector.png +0 -0
  59. package/test/references/08_chessboard_move_pieces/out_of_board.png +0 -0
  60. package/test/references/08_chessboard_move_pieces/over_arrow_marker.png +0 -0
  61. package/test/references/08_chessboard_move_pieces/over_empty.png +0 -0
  62. package/test/references/08_chessboard_move_pieces/over_non_empty_1.png +0 -0
  63. package/test/references/08_chessboard_move_pieces/over_non_empty_2.png +0 -0
  64. package/test/references/08_chessboard_move_pieces/over_square_marker.png +0 -0
  65. package/test/references/08_chessboard_move_pieces/over_text_marker.png +0 -0
  66. package/test/references/09_chessboard_edit_arrows/base_1.png +0 -0
  67. package/test/references/09_chessboard_edit_arrows/base_2.png +0 -0
  68. package/test/references/09_chessboard_edit_arrows/edit_color_not_set.png +0 -0
  69. package/test/references/09_chessboard_edit_arrows/null_vector.png +0 -0
  70. package/test/references/09_chessboard_edit_arrows/out_of_board.png +0 -0
  71. package/test/references/09_chessboard_edit_arrows/over_arrow_marker.png +0 -0
  72. package/test/references/09_chessboard_edit_arrows/over_square_marker.png +0 -0
  73. package/test/references/10_chessboard_play_moves/castling_move.png +0 -0
  74. package/test/references/10_chessboard_play_moves/chess960_ambiguous_king_move.png +0 -0
  75. package/test/references/10_chessboard_play_moves/chess960_castling_move_1.png +0 -0
  76. package/test/references/10_chessboard_play_moves/chess960_castling_move_2.png +0 -0
  77. package/test/references/10_chessboard_play_moves/chess960_non_kxr_castling.png +0 -0
  78. package/test/references/10_chessboard_play_moves/illegal_move.png +0 -0
  79. package/test/references/10_chessboard_play_moves/illegal_position.png +0 -0
  80. package/test/references/10_chessboard_play_moves/null_vector.png +0 -0
  81. package/test/references/10_chessboard_play_moves/out_of_board.png +0 -0
  82. package/test/references/10_chessboard_play_moves/regular_move_1.png +0 -0
  83. package/test/references/10_chessboard_play_moves/regular_move_2.png +0 -0
  84. package/test/references/10_chessboard_play_moves/wrong_color.png +0 -0
  85. package/test/references/11_chessboard_play_promotions/antichess_promotion.png +0 -0
  86. package/test/references/11_chessboard_play_promotions/cancel_promotion.png +0 -0
  87. package/test/references/11_chessboard_play_promotions/regular_promotion_1.png +0 -0
  88. package/test/references/11_chessboard_play_promotions/regular_promotion_2.png +0 -0
  89. package/test/references/12_movetext_simple/2.png +0 -0
  90. package/test/references/12_movetext_simple/3.png +0 -0
  91. package/test/references/15_movetext_options/2.png +0 -0
package/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
1
1
  ChangeLog
2
2
  =========
3
3
 
4
+ 1.5.3 (April 30, 2022)
5
+ ----------------------
6
+ * Add method `Movetext#focus()`.
7
+ * Expose static methods `Movetext#firstNodeId(..)`, `Movetext#previousNodeId(..)`, `Movetext#nextNodeId(..)`
8
+ and `Movetext#lastNodeId(..)`.
9
+
10
+ 1.5.2 (April 29, 2022)
11
+ ----------------------
12
+ * Prevent default actions when handling key events in component `Movetext`.
13
+
14
+ 1.5.1 (April 29, 2022)
15
+ ----------------------
16
+ * Add argument `evtOrigin` in `onMoveSelected(..)` callback in component `Movetext`.
17
+
4
18
  1.5.0 (April 27, 2022)
5
19
  ----------------------
6
20
  * Add component `Movetext` to display chess games.
@@ -94,10 +94,9 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
94
94
  });
95
95
  }
96
96
 
97
- var game = info.game;
98
97
  return /*#__PURE__*/_react["default"].createElement("div", {
99
98
  className: "kokopu-movetext"
100
- }, this.renderHeaders(game), this.renderBody(game), this.renderFocusField());
99
+ }, this.renderHeaders(info.game), this.renderBody(info.game), this.renderFocusField());
101
100
  }
102
101
  }, {
103
102
  key: "renderHeaders",
@@ -524,48 +523,48 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
524
523
  }, {
525
524
  key: "handleKeyDownInFocusField",
526
525
  value: function handleKeyDownInFocusField(evt) {
527
- if (!this.props.selection) {
526
+ if (evt.key !== 'Home' && evt.key !== 'ArrowLeft' && evt.key !== 'ArrowRight' && evt.key !== 'End') {
528
527
  return;
529
528
  }
530
529
 
531
- var game = parseGame(this.props.game, this.props.gameIndex).game;
532
- var nodeId = false;
533
-
534
- if (this.props.selection === 'start') {
535
- if (evt.key === 'ArrowRight') {
536
- nodeId = getNextNodeId(game.mainVariation(), true);
537
- } else if (evt.key === 'End') {
538
- nodeId = getLastNodeId(game.mainVariation(), true);
539
- }
540
- } else {
541
- var currentNode = game.findById(this.props.selection);
530
+ evt.preventDefault();
542
531
 
543
- if (!currentNode) {
544
- return;
545
- }
532
+ if (!this.props.selection) {
533
+ return;
534
+ }
546
535
 
547
- if (evt.key === 'Home') {
548
- nodeId = 'start';
549
- } else if (evt.key === 'ArrowLeft') {
550
- nodeId = getPreviousNodeId(currentNode);
551
- } else if (evt.key === 'ArrowRight') {
552
- nodeId = getNextNodeId(currentNode, false);
553
- } else if (evt.key === 'End') {
554
- nodeId = getLastNodeId(currentNode, false);
555
- }
536
+ var _parseGame = parseGame(this.props.game, this.props.gameIndex),
537
+ game = _parseGame.game;
538
+
539
+ var nodeId = undefined;
540
+ var evtOrigin = '';
541
+
542
+ if (evt.key === 'Home') {
543
+ nodeId = Movetext.firstNodeId(game, this.props.selection);
544
+ evtOrigin = 'key-first';
545
+ } else if (evt.key === 'ArrowLeft') {
546
+ nodeId = Movetext.previousNodeId(game, this.props.selection);
547
+ evtOrigin = 'key-previous';
548
+ } else if (evt.key === 'ArrowRight') {
549
+ nodeId = Movetext.nextNodeId(game, this.props.selection);
550
+ evtOrigin = 'key-next';
551
+ } else {
552
+ // evt.key === 'End'
553
+ nodeId = Movetext.lastNodeId(game, this.props.selection);
554
+ evtOrigin = 'key-last';
556
555
  }
557
556
 
558
557
  if (nodeId && this.props.onMoveSelected) {
559
- this.props.onMoveSelected(nodeId);
558
+ this.props.onMoveSelected(nodeId, evtOrigin);
560
559
  }
561
560
  }
562
561
  }, {
563
562
  key: "handleNodeClicked",
564
563
  value: function handleNodeClicked(nodeId) {
565
- this.focusFieldRef.current.focus();
564
+ this.focus();
566
565
 
567
566
  if (this.props.onMoveSelected) {
568
- this.props.onMoveSelected(nodeId === this.props.selection ? undefined : nodeId);
567
+ this.props.onMoveSelected(nodeId === this.props.selection ? undefined : nodeId, 'click');
569
568
  }
570
569
  }
571
570
  /**
@@ -602,6 +601,139 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
602
601
  };
603
602
  }
604
603
  }
604
+ /**
605
+ * Set the focus to the current component.
606
+ *
607
+ * @public
608
+ */
609
+
610
+ }, {
611
+ key: "focus",
612
+ value: function focus() {
613
+ this.focusFieldRef.current.focus();
614
+ }
615
+ /**
616
+ * Return the ID of the main variation in the given chess game.
617
+ * If the given selection corresponds already at the main variation, `undefined` is returned.
618
+ *
619
+ * This corresponds to the operation performed when the user presses key "home" on a `Movetext` component.
620
+ *
621
+ * @param {kokopu.Game} game Considered chess game.
622
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
623
+ * @returns {string?}
624
+ * @public
625
+ */
626
+
627
+ }], [{
628
+ key: "firstNodeId",
629
+ value: function firstNodeId(game, selection) {
630
+ if (!game.findById(selection)) {
631
+ return undefined;
632
+ }
633
+
634
+ return selection === 'start' ? undefined : 'start';
635
+ }
636
+ /**
637
+ * Return the ID of the node immediately preceding the given selection in the given chess game.
638
+ * If no such node exists, `undefined` is returned.
639
+ *
640
+ * This corresponds to the operation performed when the user presses key "arrow left" on a `Movetext` component.
641
+ *
642
+ * @param {kokopu.Game} game Considered chess game.
643
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
644
+ * @returns {string?}
645
+ * @public
646
+ */
647
+
648
+ }, {
649
+ key: "previousNodeId",
650
+ value: function previousNodeId(game, selection) {
651
+ var currentNode = game.findById(selection);
652
+
653
+ if (!currentNode) {
654
+ return undefined;
655
+ }
656
+
657
+ if (selection === 'start') {
658
+ return undefined;
659
+ } else if (selection.endsWith('start')) {
660
+ currentNode = currentNode.parentNode(); // `.parentNode()` returns always non-null ref here
661
+ }
662
+
663
+ while (currentNode) {
664
+ var previousNode = currentNode.previous();
665
+
666
+ if (previousNode) {
667
+ return previousNode.id();
668
+ }
669
+
670
+ currentNode = currentNode.parentVariation().parentNode();
671
+ }
672
+
673
+ return 'start';
674
+ }
675
+ /**
676
+ * Return the ID of the node immediately following the given selection in the given chess game.
677
+ * If no such node exists, `undefined` is returned.
678
+ *
679
+ * This corresponds to the operation performed when the user presses key "arrow right" on a `Movetext` component.
680
+ *
681
+ * @param {kokopu.Game} game Considered chess game.
682
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
683
+ * @returns {string?}
684
+ * @public
685
+ */
686
+
687
+ }, {
688
+ key: "nextNodeId",
689
+ value: function nextNodeId(game, selection) {
690
+ var currentNode = game.findById(selection);
691
+
692
+ if (!currentNode) {
693
+ return undefined;
694
+ }
695
+
696
+ var nextNode = selection.endsWith('start') ? currentNode.first() : currentNode.next();
697
+ return nextNode ? nextNode.id() : undefined;
698
+ }
699
+ /**
700
+ * Return the ID of the node at then end of the variation in which lies the given selection in the given chess game.
701
+ * If the selection is already at the end its variation, `undefined` is returned.
702
+ *
703
+ * This corresponds to the operation performed when the user presses key "end" on a `Movetext` component.
704
+ *
705
+ * @param {kokopu.Game} game Considered chess game.
706
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
707
+ * @returns {string?}
708
+ * @public
709
+ */
710
+
711
+ }, {
712
+ key: "lastNodeId",
713
+ value: function lastNodeId(game, selection) {
714
+ var currentNode = game.findById(selection);
715
+
716
+ if (!currentNode) {
717
+ return undefined;
718
+ }
719
+
720
+ currentNode = selection.endsWith('start') ? currentNode.first() : currentNode.next();
721
+
722
+ if (!currentNode) {
723
+ // Ensure that the input node is not already the last one.
724
+ return undefined;
725
+ }
726
+
727
+ while (true) {
728
+ var nextNode = currentNode.next();
729
+
730
+ if (!nextNode) {
731
+ return currentNode.id();
732
+ }
733
+
734
+ currentNode = nextNode;
735
+ }
736
+ }
605
737
  }]);
606
738
 
607
739
  return Movetext;
@@ -675,10 +807,16 @@ Movetext.propTypes = {
675
807
  interactionMode: _propTypes["default"].oneOf(['selectMove']),
676
808
 
677
809
  /**
678
- * Callback invoked when the user clicks on a move (only if `interactionMode` is set to `'selectMove'`).
810
+ * Callback invoked when the user selects a move (only if `interactionMode` is set to `'selectMove'`).
679
811
  *
680
812
  * @param {string?} nodeId ID of the selected move (as returned by [kokopu.Node#id](https://kokopu.yo35.org/docs/Node.html#id)),
681
813
  * `'start'` for the beginning of the main variation, or `undefined` if the user unselects the previously selected move.
814
+ * @param {string} evtOrigin Origin of the event. Can be:
815
+ * - `'key-first'`: the event has been triggered by the "go-to-first-move" key (aka. the home key),
816
+ * - `'key-previous'`: the event has been triggered by the "go-to-previous-move" key (aka. the arrow left key),
817
+ * - `'key-next'`: the event has been triggered by the "go-to-next-move" key (aka. the arrow right key),
818
+ * - `'key-last'`: the event has been triggered by the "go-to-last-move" key (aka. the end key),
819
+ * - `'click'`: the event has been triggered by a mouse click on a move.
682
820
  */
683
821
  onMoveSelected: _propTypes["default"].func
684
822
  };
@@ -763,41 +901,6 @@ function figurineNotation(text, fontName) {
763
901
 
764
902
  return result;
765
903
  }
766
-
767
- function getPreviousNodeId(currentNode) {
768
- var previousNode = currentNode.previous();
769
-
770
- if (previousNode) {
771
- return previousNode.id();
772
- } else {
773
- var parentNode = currentNode.parentVariation().parentNode();
774
- return parentNode ? getPreviousNodeId(parentNode) : 'start';
775
- }
776
- }
777
-
778
- function getNextNodeId(currentNode, isVariation) {
779
- var nextNode = isVariation ? currentNode.first() : currentNode.next();
780
- return nextNode ? nextNode.id() : false;
781
- }
782
-
783
- function getLastNodeId(currentNode, isVariation) {
784
- currentNode = isVariation ? currentNode.first() : currentNode.next();
785
-
786
- if (!currentNode) {
787
- // Ensure that the input node is not already the last one.
788
- return false;
789
- }
790
-
791
- while (true) {
792
- var nextNode = currentNode.next();
793
-
794
- if (!nextNode) {
795
- return currentNode.id();
796
- }
797
-
798
- currentNode = nextNode;
799
- }
800
- }
801
904
  /**
802
905
  * Try to interpret the given object as a chess game.
803
906
  */
@@ -50,6 +50,7 @@ export default class Page extends React.Component {
50
50
  selection: '28b',
51
51
  interactionMode: 'selectMove',
52
52
  withPopup: true,
53
+ withMove: false,
53
54
  };
54
55
  }
55
56
 
@@ -91,7 +92,7 @@ export default class Page extends React.Component {
91
92
  pieceSymbols="figurines"
92
93
  diagramVisible={false}
93
94
  interactionMode={this.state.interactionMode}
94
- onMoveSelected={nodeId => this.handleMoveSelected(nodeId)}
95
+ onMoveSelected={(nodeId, evtOrigin) => this.handleMoveSelected(nodeId, evtOrigin)}
95
96
  />
96
97
  </Box>
97
98
  );
@@ -117,14 +118,14 @@ export default class Page extends React.Component {
117
118
 
118
119
  renderNavigationBoard() {
119
120
  let button = <Button size="small" onClick={() => this.handlePopupToggled()}>{this.state.withPopup ? 'Reduce' : 'Open'}</Button>;
120
- let { position, csl, cal } = this.getCurrentPositionAndAnnotations();
121
- let content = this.state.withPopup ? <Stack><Chessboard position={position} squareMarkers={csl} arrowMarkers={cal} />{button}</Stack> : button;
121
+ let { position, move, csl, cal } = this.getCurrentPositionAndAnnotations();
122
+ let content = this.state.withPopup ? <Stack><Chessboard position={position} move={move} squareMarkers={csl} arrowMarkers={cal} animated={true} />{button}</Stack> : button;
122
123
  return <Paper className="kokopu-fixedPopup" elevation={3}>{content}</Paper>;
123
124
  }
124
125
 
125
- handleMoveSelected(nodeId) {
126
+ handleMoveSelected(nodeId, evtOrigin) {
126
127
  if (nodeId) {
127
- this.set('selection', nodeId);
128
+ this.set('selection', nodeId, 'withMove', evtOrigin === 'key-next');
128
129
  }
129
130
  }
130
131
 
@@ -149,7 +150,10 @@ export default class Page extends React.Component {
149
150
  }
150
151
  else {
151
152
  let node = game.findById(this.state.selection);
152
- return { position: node.position(), csl: node.tag('csl'), cal: node.tag('cal') };
153
+ let result = this.state.withMove ? { position: node.positionBefore(), move: node.notation() } : { position: node.position() };
154
+ result.csl = node.tag('csl');
155
+ result.cal = node.tag('cal');
156
+ return result;
153
157
  }
154
158
  }
155
159
  }
@@ -26,8 +26,8 @@ import { Movetext } from '../src/index';
26
26
 
27
27
  import pgn from './common/games.pgn';
28
28
 
29
- function onMoveSelected(nodeId) {
30
- setSandbox(`move selected: ${nodeId}`);
29
+ function onMoveSelected(nodeId, evtOrigin) {
30
+ setSandbox(`move selected: ${nodeId}\norigin: ${evtOrigin}`);
31
31
  }
32
32
 
33
33
  testApp([ /* eslint-disable react/jsx-key */
@@ -39,4 +39,5 @@ testApp([ /* eslint-disable react/jsx-key */
39
39
  <Movetext game={pgn} gameIndex={7} headerVisible={false} onMoveSelected={onMoveSelected} interactionMode="selectMove" selection="1w" />,
40
40
  <Movetext game={pgn} gameIndex={7} headerVisible={false} onMoveSelected={onMoveSelected} interactionMode="selectMove" selection="6b" />,
41
41
  <Movetext game={pgn} gameIndex={7} headerVisible={false} onMoveSelected={onMoveSelected} interactionMode="selectMove" selection="invalid-id" />,
42
+ <Movetext game={pgn} gameIndex={8} headerVisible={false} onMoveSelected={onMoveSelected} interactionMode="selectMove" selection="1b-v0-start" />,
42
43
  ], 'width-600'); /* eslint-enable react/jsx-key */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kokopu-react",
3
- "version": "1.5.0",
3
+ "version": "1.5.3",
4
4
  "description": "A React-based library to create and display chessboard components.",
5
5
  "keywords": [
6
6
  "chess",
@@ -33,7 +33,7 @@
33
33
  "kokopu": "^2.7.0",
34
34
  "prop-types": "^15.8.1",
35
35
  "react": "^17.0.2",
36
- "react-draggable": "^4.4.4",
36
+ "react-draggable": "^4.4.5",
37
37
  "react-motion": "^0.5.2"
38
38
  },
39
39
  "devDependencies": {
@@ -43,13 +43,13 @@
43
43
  "@babel/preset-react": "^7.16.7",
44
44
  "@emotion/react": "^11.9.0",
45
45
  "@emotion/styled": "^11.8.1",
46
- "@mui/material": "^5.6.2",
46
+ "@mui/material": "^5.6.3",
47
47
  "babel-loader": "^8.2.5",
48
48
  "babel-plugin-istanbul": "^6.1.1",
49
49
  "copy-webpack-plugin": "^10.2.4",
50
50
  "coveralls": "^3.1.1",
51
51
  "css-loader": "^6.7.1",
52
- "eslint": "^8.13.0",
52
+ "eslint": "^8.14.0",
53
53
  "eslint-plugin-react": "^7.29.4",
54
54
  "html-webpack-plugin": "^5.5.0",
55
55
  "img-diff-js": "^0.5.2",
@@ -59,7 +59,7 @@
59
59
  "prompt": "^1.3.0",
60
60
  "react-dom": "^17.0.2",
61
61
  "react-styleguidist": "^11.2.0",
62
- "selenium-webdriver": "^4.1.1",
62
+ "selenium-webdriver": "^4.1.2",
63
63
  "ssh2-sftp-client": "^8.0.0",
64
64
  "style-loader": "^3.3.1",
65
65
  "unit.js": "^2.1.1",
@@ -2,7 +2,7 @@ version: "3.7"
2
2
 
3
3
  services:
4
4
  web-client:
5
- image: selenium/standalone-firefox:4.1.1-20220121
5
+ image: selenium/standalone-firefox:4.1.4-20220427
6
6
  shm_size: '2gb'
7
7
  ports:
8
8
  - 4444:4444 # Driver (http://localhost:4444)
package/src/Chessboard.js CHANGED
@@ -83,10 +83,10 @@ export default class Chessboard extends React.Component {
83
83
  // Compute the current position.
84
84
  let info = this.getPositionAndMoveInfo();
85
85
  if (info.positionError) {
86
- return <ErrorBox title={i18n.INVALID_FEN_ERROR_TITLE} message={info.message}></ErrorBox>;
86
+ return <ErrorBox title={i18n.INVALID_FEN_ERROR_TITLE} message={info.message} />;
87
87
  }
88
88
  else if (info.moveError) {
89
- return <ErrorBox title={i18n.INVALID_NOTATION_ERROR_TITLE} message={info.message}></ErrorBox>;
89
+ return <ErrorBox title={i18n.INVALID_NOTATION_ERROR_TITLE} message={info.message} />;
90
90
  }
91
91
  let { position, move, positionBefore } = info;
92
92
 
package/src/Movetext.js CHANGED
@@ -46,14 +46,12 @@ export default class Movetext extends React.Component {
46
46
  render() {
47
47
  let info = parseGame(this.props.game, this.props.gameIndex);
48
48
  if (info.error) {
49
- return <ErrorBox title={i18n.INVALID_PGN_ERROR_TITLE} message={info.message} text={info.text} errorIndex={info.errorIndex} lineNumber={info.lineNumber}></ErrorBox>;
49
+ return <ErrorBox title={i18n.INVALID_PGN_ERROR_TITLE} message={info.message} text={info.text} errorIndex={info.errorIndex} lineNumber={info.lineNumber} />;
50
50
  }
51
- let game = info.game;
52
-
53
51
  return (
54
52
  <div className="kokopu-movetext">
55
- {this.renderHeaders(game)}
56
- {this.renderBody(game)}
53
+ {this.renderHeaders(info.game)}
54
+ {this.renderBody(info.game)}
57
55
  {this.renderFocusField()}
58
56
  </div>
59
57
  );
@@ -333,46 +331,41 @@ export default class Movetext extends React.Component {
333
331
  }
334
332
 
335
333
  handleKeyDownInFocusField(evt) {
334
+ if (evt.key !== 'Home' && evt.key !== 'ArrowLeft' && evt.key !== 'ArrowRight' && evt.key !== 'End') {
335
+ return;
336
+ }
337
+ evt.preventDefault();
336
338
  if (!this.props.selection) {
337
339
  return;
338
340
  }
339
- let game = parseGame(this.props.game, this.props.gameIndex).game;
340
- let nodeId = false;
341
- if (this.props.selection === 'start') {
342
- if (evt.key === 'ArrowRight') {
343
- nodeId = getNextNodeId(game.mainVariation(), true);
344
- }
345
- else if (evt.key === 'End') {
346
- nodeId = getLastNodeId(game.mainVariation(), true);
347
- }
341
+ let { game } = parseGame(this.props.game, this.props.gameIndex);
342
+ let nodeId = undefined;
343
+ let evtOrigin = '';
344
+ if (evt.key === 'Home') {
345
+ nodeId = Movetext.firstNodeId(game, this.props.selection);
346
+ evtOrigin = 'key-first';
348
347
  }
349
- else {
350
- let currentNode = game.findById(this.props.selection);
351
- if (!currentNode) {
352
- return;
353
- }
354
- if (evt.key === 'Home') {
355
- nodeId = 'start';
356
- }
357
- else if (evt.key === 'ArrowLeft') {
358
- nodeId = getPreviousNodeId(currentNode);
359
- }
360
- else if (evt.key === 'ArrowRight') {
361
- nodeId = getNextNodeId(currentNode, false);
362
- }
363
- else if (evt.key === 'End') {
364
- nodeId = getLastNodeId(currentNode, false);
365
- }
348
+ else if (evt.key === 'ArrowLeft') {
349
+ nodeId = Movetext.previousNodeId(game, this.props.selection);
350
+ evtOrigin = 'key-previous';
351
+ }
352
+ else if (evt.key === 'ArrowRight') {
353
+ nodeId = Movetext.nextNodeId(game, this.props.selection);
354
+ evtOrigin = 'key-next';
355
+ }
356
+ else { // evt.key === 'End'
357
+ nodeId = Movetext.lastNodeId(game, this.props.selection);
358
+ evtOrigin = 'key-last';
366
359
  }
367
360
  if (nodeId && this.props.onMoveSelected) {
368
- this.props.onMoveSelected(nodeId);
361
+ this.props.onMoveSelected(nodeId, evtOrigin);
369
362
  }
370
363
  }
371
364
 
372
365
  handleNodeClicked(nodeId) {
373
- this.focusFieldRef.current.focus();
366
+ this.focus();
374
367
  if (this.props.onMoveSelected) {
375
- this.props.onMoveSelected(nodeId === this.props.selection ? undefined : nodeId);
368
+ this.props.onMoveSelected(nodeId === this.props.selection ? undefined : nodeId, 'click');
376
369
  }
377
370
  }
378
371
 
@@ -395,6 +388,114 @@ export default class Movetext extends React.Component {
395
388
  return notation => notation;
396
389
  }
397
390
  }
391
+
392
+ /**
393
+ * Set the focus to the current component.
394
+ *
395
+ * @public
396
+ */
397
+ focus() {
398
+ this.focusFieldRef.current.focus();
399
+ }
400
+
401
+ /**
402
+ * Return the ID of the main variation in the given chess game.
403
+ * If the given selection corresponds already at the main variation, `undefined` is returned.
404
+ *
405
+ * This corresponds to the operation performed when the user presses key "home" on a `Movetext` component.
406
+ *
407
+ * @param {kokopu.Game} game Considered chess game.
408
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
409
+ * @returns {string?}
410
+ * @public
411
+ */
412
+ static firstNodeId(game, selection) {
413
+ if (!game.findById(selection)) {
414
+ return undefined;
415
+ }
416
+ return selection === 'start' ? undefined : 'start';
417
+ }
418
+
419
+ /**
420
+ * Return the ID of the node immediately preceding the given selection in the given chess game.
421
+ * If no such node exists, `undefined` is returned.
422
+ *
423
+ * This corresponds to the operation performed when the user presses key "arrow left" on a `Movetext` component.
424
+ *
425
+ * @param {kokopu.Game} game Considered chess game.
426
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
427
+ * @returns {string?}
428
+ * @public
429
+ */
430
+ static previousNodeId(game, selection) {
431
+ let currentNode = game.findById(selection);
432
+ if (!currentNode) {
433
+ return undefined;
434
+ }
435
+ if (selection === 'start') {
436
+ return undefined;
437
+ }
438
+ else if (selection.endsWith('start')) {
439
+ currentNode = currentNode.parentNode(); // `.parentNode()` returns always non-null ref here
440
+ }
441
+ while (currentNode) {
442
+ let previousNode = currentNode.previous();
443
+ if (previousNode) {
444
+ return previousNode.id();
445
+ }
446
+ currentNode = currentNode.parentVariation().parentNode();
447
+ }
448
+ return 'start';
449
+ }
450
+
451
+ /**
452
+ * Return the ID of the node immediately following the given selection in the given chess game.
453
+ * If no such node exists, `undefined` is returned.
454
+ *
455
+ * This corresponds to the operation performed when the user presses key "arrow right" on a `Movetext` component.
456
+ *
457
+ * @param {kokopu.Game} game Considered chess game.
458
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
459
+ * @returns {string?}
460
+ * @public
461
+ */
462
+ static nextNodeId(game, selection) {
463
+ let currentNode = game.findById(selection);
464
+ if (!currentNode) {
465
+ return undefined;
466
+ }
467
+ let nextNode = selection.endsWith('start') ? currentNode.first() : currentNode.next();
468
+ return nextNode ? nextNode.id() : undefined;
469
+ }
470
+
471
+ /**
472
+ * Return the ID of the node at then end of the variation in which lies the given selection in the given chess game.
473
+ * If the selection is already at the end its variation, `undefined` is returned.
474
+ *
475
+ * This corresponds to the operation performed when the user presses key "end" on a `Movetext` component.
476
+ *
477
+ * @param {kokopu.Game} game Considered chess game.
478
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
479
+ * @returns {string?}
480
+ * @public
481
+ */
482
+ static lastNodeId(game, selection) {
483
+ let currentNode = game.findById(selection);
484
+ if (!currentNode) {
485
+ return undefined;
486
+ }
487
+ currentNode = selection.endsWith('start') ? currentNode.first() : currentNode.next();
488
+ if (!currentNode) { // Ensure that the input node is not already the last one.
489
+ return undefined;
490
+ }
491
+ while (true) {
492
+ let nextNode = currentNode.next();
493
+ if (!nextNode) {
494
+ return currentNode.id();
495
+ }
496
+ currentNode = nextNode;
497
+ }
498
+ }
398
499
  }
399
500
 
400
501
 
@@ -473,10 +574,16 @@ Movetext.propTypes = {
473
574
  interactionMode: PropTypes.oneOf([ 'selectMove' ]),
474
575
 
475
576
  /**
476
- * Callback invoked when the user clicks on a move (only if `interactionMode` is set to `'selectMove'`).
577
+ * Callback invoked when the user selects a move (only if `interactionMode` is set to `'selectMove'`).
477
578
  *
478
579
  * @param {string?} nodeId ID of the selected move (as returned by [kokopu.Node#id](https://kokopu.yo35.org/docs/Node.html#id)),
479
580
  * `'start'` for the beginning of the main variation, or `undefined` if the user unselects the previously selected move.
581
+ * @param {string} evtOrigin Origin of the event. Can be:
582
+ * - `'key-first'`: the event has been triggered by the "go-to-first-move" key (aka. the home key),
583
+ * - `'key-previous'`: the event has been triggered by the "go-to-previous-move" key (aka. the arrow left key),
584
+ * - `'key-next'`: the event has been triggered by the "go-to-next-move" key (aka. the arrow right key),
585
+ * - `'key-last'`: the event has been triggered by the "go-to-last-move" key (aka. the end key),
586
+ * - `'click'`: the event has been triggered by a mouse click on a move.
480
587
  */
481
588
  onMoveSelected: PropTypes.func,
482
589
  };
@@ -555,39 +662,6 @@ function figurineNotation(text, fontName) {
555
662
  }
556
663
 
557
664
 
558
- function getPreviousNodeId(currentNode) {
559
- let previousNode = currentNode.previous();
560
- if (previousNode) {
561
- return previousNode.id();
562
- }
563
- else {
564
- let parentNode = currentNode.parentVariation().parentNode();
565
- return parentNode ? getPreviousNodeId(parentNode) : 'start';
566
- }
567
- }
568
-
569
-
570
- function getNextNodeId(currentNode, isVariation) {
571
- let nextNode = isVariation ? currentNode.first() : currentNode.next();
572
- return nextNode ? nextNode.id() : false;
573
- }
574
-
575
-
576
- function getLastNodeId(currentNode, isVariation) {
577
- currentNode = isVariation ? currentNode.first() : currentNode.next();
578
- if (!currentNode) { // Ensure that the input node is not already the last one.
579
- return false;
580
- }
581
- while (true) {
582
- let nextNode = currentNode.next();
583
- if (!nextNode) {
584
- return currentNode.id();
585
- }
586
- currentNode = nextNode;
587
- }
588
- }
589
-
590
-
591
665
  /**
592
666
  * Try to interpret the given object as a chess game.
593
667
  */
@@ -53,18 +53,22 @@ describe('Movetext interaction', function() {
53
53
  });
54
54
  }
55
55
 
56
+ function tpl(nodeId, evtOrigin) {
57
+ return `move selected: ${nodeId}\norigin: ${evtOrigin}`;
58
+ }
59
+
56
60
  itCheckClickMove(0, 'click on disabled', [
57
61
  { searchedText: 'Nf6', expectedText: '' },
58
62
  { searchedText: 'Qc2', expectedText: '' },
59
63
  ]);
60
64
 
61
65
  itCheckClickMove(1, 'click on unselected', [
62
- { searchedText: 'cxd5', expectedText: 'move selected: 5w' },
66
+ { searchedText: 'cxd5', expectedText: tpl('5w', 'click') },
63
67
  ]);
64
68
 
65
69
  itCheckClickMove(2, 'click on with variations 1', [
66
- { searchedText: 'Bb5', expectedText: 'move selected: undefined' },
67
- { searchedText: 'Nc6', expectedText: 'move selected: 1b-v0-2b' },
70
+ { searchedText: 'Bb5', expectedText: tpl(undefined, 'click') },
71
+ { searchedText: 'Nc6', expectedText: tpl('1b-v0-2b', 'click') },
68
72
  ]);
69
73
 
70
74
  function itCheckKeyboardActions(itemIndex, label, expectedOnGoFirst, expectedOnGoPrevious, expectedOnGoNext, expectedOnGoLast) {
@@ -86,14 +90,19 @@ describe('Movetext interaction', function() {
86
90
  await setSandbox(browserContext, '');
87
91
  await focusFieldElement.sendKeys(Key.END);
88
92
  await compareSandbox(browserContext, expectedOnGoLast);
93
+
94
+ await setSandbox(browserContext, '');
95
+ await focusFieldElement.sendKeys(Key.ARROW_UP);
96
+ await compareSandbox(browserContext, '');
89
97
  });
90
98
  }
91
99
 
92
100
  itCheckKeyboardActions(1, 'key on unselected', '', '', '', '');
93
- itCheckKeyboardActions(2, 'key on with variations 1', 'move selected: start', 'move selected: 1b-v0-2b', 'move selected: 1b-v0-3w-v0-3b', 'move selected: 1b-v0-3w-v0-4w');
94
- itCheckKeyboardActions(3, 'key on with variations 2', 'move selected: start', 'move selected: 1b-v0-2w', 'move selected: 1b-v0-3w', 'move selected: 1b-v0-3w');
95
- itCheckKeyboardActions(4, 'key on start selected', '', '', 'move selected: 1w', 'move selected: 6b');
96
- itCheckKeyboardActions(5, 'key on first selected', 'move selected: start', 'move selected: start', 'move selected: 1b', 'move selected: 6b');
97
- itCheckKeyboardActions(6, 'key on last selected', 'move selected: start', 'move selected: 6w', '', '');
101
+ itCheckKeyboardActions(2, 'key on with variations 1', tpl('start', 'key-first'), tpl('1b-v0-2b', 'key-previous'), tpl('1b-v0-3w-v0-3b', 'key-next'), tpl('1b-v0-3w-v0-4w', 'key-last'));
102
+ itCheckKeyboardActions(3, 'key on with variations 2', tpl('start', 'key-first'), tpl('1b-v0-2w', 'key-previous'), tpl('1b-v0-3w', 'key-next'), tpl('1b-v0-3w', 'key-last'));
103
+ itCheckKeyboardActions(4, 'key on start selected', '', '', tpl('1w', 'key-next'), tpl('6b', 'key-last'));
104
+ itCheckKeyboardActions(5, 'key on first selected', tpl('start', 'key-first'), tpl('start', 'key-previous'), tpl('1b', 'key-next'), tpl('6b', 'key-last'));
105
+ itCheckKeyboardActions(6, 'key on last selected', tpl('start', 'key-first'), tpl('6w', 'key-previous'), '', '');
98
106
  itCheckKeyboardActions(7, 'key on invalid selection', '', '', '', '');
107
+ itCheckKeyboardActions(8, 'key on sub-variation selected', tpl('start', 'key-first'), tpl('1w', 'key-previous'), tpl('1b-v0-1b', 'key-next'), tpl('1b-v0-3w', 'key-last'));
99
108
  });