kokopu-react 1.5.2 → 1.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,21 @@
1
1
  ChangeLog
2
2
  =========
3
3
 
4
+ 1.5.5 (May 18, 2022)
5
+ --------------------
6
+ * Upgrade dependencies.
7
+ * Remove dependency with security issue.
8
+
9
+ 1.5.4 (May 1, 2022)
10
+ -------------------
11
+ * Provide method `Chessboard#size(..)` to retrieve the width/height of a `Chessboard` component instance.
12
+
13
+ 1.5.3 (April 30, 2022)
14
+ ----------------------
15
+ * Add method `Movetext#focus()`.
16
+ * Expose static methods `Movetext#firstNodeId(..)`, `Movetext#previousNodeId(..)`, `Movetext#nextNodeId(..)`
17
+ and `Movetext#lastNodeId(..)`.
18
+
4
19
  1.5.2 (April 29, 2022)
5
20
  ----------------------
6
21
  * Prevent default actions when handling key events in component `Movetext`.
package/README.md CHANGED
@@ -3,7 +3,7 @@ Kokopu-React
3
3
 
4
4
  <img align="right" width="96" height="96" src="graphics/logo.svg" />
5
5
 
6
- Kokopu-React is a [React](https://reactjs.org/)-based library to create and display chessboard components.
6
+ Kokopu-React is a [React](https://reactjs.org/)-based library to create and display chessboard and chess-related components.
7
7
  Kokopu-React is built on top of [Kokopu](https://www.npmjs.com/package/kokopu), a headless library that
8
8
  implements all the chess logic (game rules, parsing of [FEN](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation)
9
9
  and [PGN](https://en.wikipedia.org/wiki/Portable_Game_Notation) formats...).
@@ -984,6 +984,46 @@ var Chessboard = /*#__PURE__*/function (_React$Component) {
984
984
  value: function getArrowTipId(color) {
985
985
  return 'kokopu-arrowTip-' + color + '-' + this.arrowTipIdSuffix;
986
986
  }
987
+ /**
988
+ * Return the size of the chessboard, assuming it is built with the given attributes.
989
+ *
990
+ * @param {number} squareSize
991
+ * @param {boolean} coordinateVisible
992
+ * @param {{width:number, squareSize:number, coordinateVisible:boolean}[]} smallScreenLimits
993
+ * @returns {{width:number, height:number}}
994
+ * @public
995
+ */
996
+
997
+ }], [{
998
+ key: "size",
999
+ value: function size(squareSize, coordinateVisible, smallScreenLimits) {
1000
+ // Enforce small-screen limits, if any.
1001
+ if (typeof window !== 'undefined') {
1002
+ var squareSizeLimit = computeSmallScreenLimits('squareSize', smallScreenLimits, window.innerWidth);
1003
+ var coordinateVisibleLimit = computeSmallScreenLimits('coordinateVisible', smallScreenLimits, window.innerWidth);
1004
+
1005
+ if (!isNaN(squareSizeLimit)) {
1006
+ squareSize = (0, _util.sanitizeInteger)(squareSizeLimit, _util.MIN_SQUARE_SIZE, squareSize);
1007
+ }
1008
+
1009
+ coordinateVisible = coordinateVisible && (coordinateVisibleLimit === undefined || coordinateVisibleLimit);
1010
+ } // Compute the dimensions.
1011
+
1012
+
1013
+ var width = 9 * squareSize + Math.round(TURN_FLAG_SPACING_FACTOR * squareSize);
1014
+ var height = 8 * squareSize;
1015
+
1016
+ if (coordinateVisible) {
1017
+ var fontSize = computeCoordinateFontSize(squareSize);
1018
+ width += Math.round(RANK_COORDINATE_WIDTH_FACTOR * fontSize);
1019
+ height += Math.round(FILE_COORDINATE_HEIGHT_FACTOR * fontSize);
1020
+ }
1021
+
1022
+ return {
1023
+ width: width,
1024
+ height: height
1025
+ };
1026
+ }
987
1027
  /**
988
1028
  * Return the maximum square size that would allow the chessboard to fit in a rectangle of size `width x height`.
989
1029
  *
@@ -995,7 +1035,7 @@ var Chessboard = /*#__PURE__*/function (_React$Component) {
995
1035
  * @public
996
1036
  */
997
1037
 
998
- }], [{
1038
+ }, {
999
1039
  key: "adaptSquareSize",
1000
1040
  value: function adaptSquareSize(width, height, coordinateVisible, smallScreenLimits) {
1001
1041
  var maxSquareSize = _util.MAX_SQUARE_SIZE; // Enforce small-screen limits, if any.
@@ -96,7 +96,7 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
96
96
 
97
97
  return /*#__PURE__*/_react["default"].createElement("div", {
98
98
  className: "kokopu-movetext"
99
- }, this.renderHeaders(info.game), this.renderBody(info.game), this.renderFocusField());
99
+ }, this.renderHeaders(info.game), this.renderBody(info.game), this.renderFocusField(info.game));
100
100
  }
101
101
  }, {
102
102
  key: "renderHeaders",
@@ -233,7 +233,7 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
233
233
  }
234
234
  }, {
235
235
  key: "renderFocusField",
236
- value: function renderFocusField() {
236
+ value: function renderFocusField(game) {
237
237
  var _this2 = this;
238
238
 
239
239
  if (this.props.interactionMode !== 'selectMove') {
@@ -247,7 +247,7 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
247
247
  href: "#",
248
248
  ref: this.focusFieldRef,
249
249
  onKeyDown: function onKeyDown(evt) {
250
- return _this2.handleKeyDownInFocusField(evt);
250
+ return _this2.handleKeyDownInFocusField(evt, game);
251
251
  }
252
252
  }));
253
253
  }
@@ -522,47 +522,33 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
522
522
  }
523
523
  }, {
524
524
  key: "handleKeyDownInFocusField",
525
- value: function handleKeyDownInFocusField(evt) {
526
- if (evt.key === 'Home' || evt.key === 'ArrowLeft' || evt.key === 'ArrowRight' || evt.key === 'End') {
527
- evt.preventDefault();
525
+ value: function handleKeyDownInFocusField(evt, game) {
526
+ if (evt.key !== 'Home' && evt.key !== 'ArrowLeft' && evt.key !== 'ArrowRight' && evt.key !== 'End') {
527
+ return;
528
528
  }
529
529
 
530
+ evt.preventDefault();
531
+
530
532
  if (!this.props.selection) {
531
533
  return;
532
534
  }
533
535
 
534
- var game = parseGame(this.props.game, this.props.gameIndex).game;
535
- var nodeId = false;
536
+ var nodeId = undefined;
536
537
  var evtOrigin = '';
537
538
 
538
- if (this.props.selection === 'start') {
539
- if (evt.key === 'ArrowRight') {
540
- nodeId = getNextNodeId(game.mainVariation(), true);
541
- evtOrigin = 'key-next';
542
- } else if (evt.key === 'End') {
543
- nodeId = getLastNodeId(game.mainVariation(), true);
544
- evtOrigin = 'key-last';
545
- }
539
+ if (evt.key === 'Home') {
540
+ nodeId = Movetext.firstNodeId(game, this.props.selection);
541
+ evtOrigin = 'key-first';
542
+ } else if (evt.key === 'ArrowLeft') {
543
+ nodeId = Movetext.previousNodeId(game, this.props.selection);
544
+ evtOrigin = 'key-previous';
545
+ } else if (evt.key === 'ArrowRight') {
546
+ nodeId = Movetext.nextNodeId(game, this.props.selection);
547
+ evtOrigin = 'key-next';
546
548
  } else {
547
- var currentNode = game.findById(this.props.selection);
548
-
549
- if (!currentNode) {
550
- return;
551
- }
552
-
553
- if (evt.key === 'Home') {
554
- nodeId = 'start';
555
- evtOrigin = 'key-first';
556
- } else if (evt.key === 'ArrowLeft') {
557
- nodeId = getPreviousNodeId(currentNode);
558
- evtOrigin = 'key-previous';
559
- } else if (evt.key === 'ArrowRight') {
560
- nodeId = getNextNodeId(currentNode, false);
561
- evtOrigin = 'key-next';
562
- } else if (evt.key === 'End') {
563
- nodeId = getLastNodeId(currentNode, false);
564
- evtOrigin = 'key-last';
565
- }
549
+ // evt.key === 'End'
550
+ nodeId = Movetext.lastNodeId(game, this.props.selection);
551
+ evtOrigin = 'key-last';
566
552
  }
567
553
 
568
554
  if (nodeId && this.props.onMoveSelected) {
@@ -572,7 +558,7 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
572
558
  }, {
573
559
  key: "handleNodeClicked",
574
560
  value: function handleNodeClicked(nodeId) {
575
- this.focusFieldRef.current.focus();
561
+ this.focus();
576
562
 
577
563
  if (this.props.onMoveSelected) {
578
564
  this.props.onMoveSelected(nodeId === this.props.selection ? undefined : nodeId, 'click');
@@ -612,6 +598,139 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
612
598
  };
613
599
  }
614
600
  }
601
+ /**
602
+ * Set the focus to the current component.
603
+ *
604
+ * @public
605
+ */
606
+
607
+ }, {
608
+ key: "focus",
609
+ value: function focus() {
610
+ this.focusFieldRef.current.focus();
611
+ }
612
+ /**
613
+ * Return the ID of the main variation in the given chess game.
614
+ * If the given selection corresponds already at the main variation, `undefined` is returned.
615
+ *
616
+ * This corresponds to the operation performed when the user presses key "home" on a `Movetext` component.
617
+ *
618
+ * @param {kokopu.Game} game Considered chess game.
619
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
620
+ * @returns {string?}
621
+ * @public
622
+ */
623
+
624
+ }], [{
625
+ key: "firstNodeId",
626
+ value: function firstNodeId(game, selection) {
627
+ if (!game.findById(selection)) {
628
+ return undefined;
629
+ }
630
+
631
+ return selection === 'start' ? undefined : 'start';
632
+ }
633
+ /**
634
+ * Return the ID of the node immediately preceding the given selection in the given chess game.
635
+ * If no such node exists, `undefined` is returned.
636
+ *
637
+ * This corresponds to the operation performed when the user presses key "arrow left" on a `Movetext` component.
638
+ *
639
+ * @param {kokopu.Game} game Considered chess game.
640
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
641
+ * @returns {string?}
642
+ * @public
643
+ */
644
+
645
+ }, {
646
+ key: "previousNodeId",
647
+ value: function previousNodeId(game, selection) {
648
+ var currentNode = game.findById(selection);
649
+
650
+ if (!currentNode) {
651
+ return undefined;
652
+ }
653
+
654
+ if (selection === 'start') {
655
+ return undefined;
656
+ } else if (selection.endsWith('start')) {
657
+ currentNode = currentNode.parentNode(); // `.parentNode()` returns always non-null ref here
658
+ }
659
+
660
+ while (currentNode) {
661
+ var previousNode = currentNode.previous();
662
+
663
+ if (previousNode) {
664
+ return previousNode.id();
665
+ }
666
+
667
+ currentNode = currentNode.parentVariation().parentNode();
668
+ }
669
+
670
+ return 'start';
671
+ }
672
+ /**
673
+ * Return the ID of the node immediately following the given selection in the given chess game.
674
+ * If no such node exists, `undefined` is returned.
675
+ *
676
+ * This corresponds to the operation performed when the user presses key "arrow right" on a `Movetext` component.
677
+ *
678
+ * @param {kokopu.Game} game Considered chess game.
679
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
680
+ * @returns {string?}
681
+ * @public
682
+ */
683
+
684
+ }, {
685
+ key: "nextNodeId",
686
+ value: function nextNodeId(game, selection) {
687
+ var currentNode = game.findById(selection);
688
+
689
+ if (!currentNode) {
690
+ return undefined;
691
+ }
692
+
693
+ var nextNode = selection.endsWith('start') ? currentNode.first() : currentNode.next();
694
+ return nextNode ? nextNode.id() : undefined;
695
+ }
696
+ /**
697
+ * Return the ID of the node at then end of the variation in which lies the given selection in the given chess game.
698
+ * If the selection is already at the end its variation, `undefined` is returned.
699
+ *
700
+ * This corresponds to the operation performed when the user presses key "end" on a `Movetext` component.
701
+ *
702
+ * @param {kokopu.Game} game Considered chess game.
703
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
704
+ * @returns {string?}
705
+ * @public
706
+ */
707
+
708
+ }, {
709
+ key: "lastNodeId",
710
+ value: function lastNodeId(game, selection) {
711
+ var currentNode = game.findById(selection);
712
+
713
+ if (!currentNode) {
714
+ return undefined;
715
+ }
716
+
717
+ currentNode = selection.endsWith('start') ? currentNode.first() : currentNode.next();
718
+
719
+ if (!currentNode) {
720
+ // Ensure that the input node is not already the last one.
721
+ return undefined;
722
+ }
723
+
724
+ while (true) {
725
+ var nextNode = currentNode.next();
726
+
727
+ if (!nextNode) {
728
+ return currentNode.id();
729
+ }
730
+
731
+ currentNode = nextNode;
732
+ }
733
+ }
615
734
  }]);
616
735
 
617
736
  return Movetext;
@@ -779,41 +898,6 @@ function figurineNotation(text, fontName) {
779
898
 
780
899
  return result;
781
900
  }
782
-
783
- function getPreviousNodeId(currentNode) {
784
- var previousNode = currentNode.previous();
785
-
786
- if (previousNode) {
787
- return previousNode.id();
788
- } else {
789
- var parentNode = currentNode.parentVariation().parentNode();
790
- return parentNode ? getPreviousNodeId(parentNode) : 'start';
791
- }
792
- }
793
-
794
- function getNextNodeId(currentNode, isVariation) {
795
- var nextNode = isVariation ? currentNode.first() : currentNode.next();
796
- return nextNode ? nextNode.id() : false;
797
- }
798
-
799
- function getLastNodeId(currentNode, isVariation) {
800
- currentNode = isVariation ? currentNode.first() : currentNode.next();
801
-
802
- if (!currentNode) {
803
- // Ensure that the input node is not already the last one.
804
- return false;
805
- }
806
-
807
- while (true) {
808
- var nextNode = currentNode.next();
809
-
810
- if (!nextNode) {
811
- return currentNode.id();
812
- }
813
-
814
- currentNode = nextNode;
815
- }
816
- }
817
901
  /**
818
902
  * Try to interpret the given object as a chess game.
819
903
  */
package/doc_src/home.md CHANGED
@@ -1,4 +1,4 @@
1
- Kokopu-React is a [React](https://reactjs.org/)-based library to create and display chessboard components.
1
+ Kokopu-React is a [React](https://reactjs.org/)-based library to create and display chessboard and chess-related components.
2
2
  Kokopu-React is built on top of [Kokopu](https://www.npmjs.com/package/kokopu), a headless library that
3
3
  implements all the chess logic (game rules, parsing of [FEN](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation)
4
4
  and [PGN](https://en.wikipedia.org/wiki/Portable_Game_Notation) formats...).
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "kokopu-react",
3
- "version": "1.5.2",
4
- "description": "A React-based library to create and display chessboard components.",
3
+ "version": "1.5.5",
4
+ "description": "A React-based library to create and display chessboard and chess-related components.",
5
5
  "keywords": [
6
6
  "chess",
7
7
  "react",
@@ -29,41 +29,40 @@
29
29
  ".nycrc.yml"
30
30
  ],
31
31
  "dependencies": {
32
- "htmlparser2": "^7.2.0",
33
- "kokopu": "^2.7.0",
32
+ "htmlparser2": "^8.0.1",
33
+ "kokopu": "^2.8.0",
34
34
  "prop-types": "^15.8.1",
35
35
  "react": "^17.0.2",
36
36
  "react-draggable": "^4.4.5",
37
37
  "react-motion": "^0.5.2"
38
38
  },
39
39
  "devDependencies": {
40
- "@babel/cli": "^7.17.6",
41
- "@babel/core": "^7.17.9",
42
- "@babel/preset-env": "^7.16.11",
43
- "@babel/preset-react": "^7.16.7",
40
+ "@babel/cli": "^7.17.10",
41
+ "@babel/core": "^7.17.12",
42
+ "@babel/preset-env": "^7.17.12",
43
+ "@babel/preset-react": "^7.17.12",
44
44
  "@emotion/react": "^11.9.0",
45
45
  "@emotion/styled": "^11.8.1",
46
- "@mui/material": "^5.6.3",
46
+ "@mui/material": "^5.8.0",
47
47
  "babel-loader": "^8.2.5",
48
48
  "babel-plugin-istanbul": "^6.1.1",
49
- "copy-webpack-plugin": "^10.2.4",
49
+ "copy-webpack-plugin": "^11.0.0",
50
50
  "coveralls": "^3.1.1",
51
51
  "css-loader": "^6.7.1",
52
- "eslint": "^8.14.0",
52
+ "eslint": "^8.15.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",
56
- "mocha": "^9.2.2",
56
+ "mocha": "^10.0.0",
57
57
  "null-loader": "^4.0.1",
58
58
  "nyc": "^15.1.0",
59
- "prompt": "^1.3.0",
60
59
  "react-dom": "^17.0.2",
61
60
  "react-styleguidist": "^11.2.0",
62
61
  "selenium-webdriver": "^4.1.2",
63
- "ssh2-sftp-client": "^8.0.0",
62
+ "ssh2-sftp-client": "^8.1.0",
64
63
  "style-loader": "^3.3.1",
65
64
  "unit.js": "^2.1.1",
66
- "webpack": "^5.72.0",
65
+ "webpack": "^5.72.1",
67
66
  "webpack-cli": "^4.9.2"
68
67
  },
69
68
  "scripts": {
package/src/Chessboard.js CHANGED
@@ -623,6 +623,38 @@ export default class Chessboard extends React.Component {
623
623
  return 'kokopu-arrowTip-' + color + '-' + this.arrowTipIdSuffix;
624
624
  }
625
625
 
626
+ /**
627
+ * Return the size of the chessboard, assuming it is built with the given attributes.
628
+ *
629
+ * @param {number} squareSize
630
+ * @param {boolean} coordinateVisible
631
+ * @param {{width:number, squareSize:number, coordinateVisible:boolean}[]} smallScreenLimits
632
+ * @returns {{width:number, height:number}}
633
+ * @public
634
+ */
635
+ static size(squareSize, coordinateVisible, smallScreenLimits) {
636
+
637
+ // Enforce small-screen limits, if any.
638
+ if (typeof window !== 'undefined') {
639
+ let squareSizeLimit = computeSmallScreenLimits('squareSize', smallScreenLimits, window.innerWidth);
640
+ let coordinateVisibleLimit = computeSmallScreenLimits('coordinateVisible', smallScreenLimits, window.innerWidth);
641
+ if (!isNaN(squareSizeLimit)) {
642
+ squareSize = sanitizeInteger(squareSizeLimit, MIN_SQUARE_SIZE, squareSize);
643
+ }
644
+ coordinateVisible = coordinateVisible && (coordinateVisibleLimit === undefined || coordinateVisibleLimit);
645
+ }
646
+
647
+ // Compute the dimensions.
648
+ let width = 9 * squareSize + Math.round(TURN_FLAG_SPACING_FACTOR * squareSize);
649
+ let height = 8 * squareSize;
650
+ if (coordinateVisible) {
651
+ let fontSize = computeCoordinateFontSize(squareSize);
652
+ width += Math.round(RANK_COORDINATE_WIDTH_FACTOR * fontSize);
653
+ height += Math.round(FILE_COORDINATE_HEIGHT_FACTOR * fontSize);
654
+ }
655
+ return { width: width, height: height };
656
+ }
657
+
626
658
  /**
627
659
  * Return the maximum square size that would allow the chessboard to fit in a rectangle of size `width x height`.
628
660
  *
package/src/Movetext.js CHANGED
@@ -52,7 +52,7 @@ export default class Movetext extends React.Component {
52
52
  <div className="kokopu-movetext">
53
53
  {this.renderHeaders(info.game)}
54
54
  {this.renderBody(info.game)}
55
- {this.renderFocusField()}
55
+ {this.renderFocusField(info.game)}
56
56
  </div>
57
57
  );
58
58
  }
@@ -132,13 +132,13 @@ export default class Movetext extends React.Component {
132
132
  return <div className="kokopu-header-annotator" key="annotator">{sanitizeHtml(annotator)}</div>;
133
133
  }
134
134
 
135
- renderFocusField() {
135
+ renderFocusField(game) {
136
136
  if (this.props.interactionMode !== 'selectMove') {
137
137
  return undefined;
138
138
  }
139
139
  return (
140
140
  <div className="kokopu-focusFieldContainer">
141
- <a className="kokopu-focusField" href="#" ref={this.focusFieldRef} onKeyDown={evt => this.handleKeyDownInFocusField(evt)}></a>
141
+ <a className="kokopu-focusField" href="#" ref={this.focusFieldRef} onKeyDown={evt => this.handleKeyDownInFocusField(evt, game)}></a>
142
142
  </div>
143
143
  );
144
144
  }
@@ -330,47 +330,31 @@ export default class Movetext extends React.Component {
330
330
  return comment ? comment : undefined;
331
331
  }
332
332
 
333
- handleKeyDownInFocusField(evt) {
334
- if (evt.key === 'Home' || evt.key === 'ArrowLeft' || evt.key === 'ArrowRight' || evt.key === 'End') {
335
- evt.preventDefault();
333
+ handleKeyDownInFocusField(evt, game) {
334
+ if (evt.key !== 'Home' && evt.key !== 'ArrowLeft' && evt.key !== 'ArrowRight' && evt.key !== 'End') {
335
+ return;
336
336
  }
337
+ evt.preventDefault();
337
338
  if (!this.props.selection) {
338
339
  return;
339
340
  }
340
- let game = parseGame(this.props.game, this.props.gameIndex).game;
341
- let nodeId = false;
341
+ let nodeId = undefined;
342
342
  let evtOrigin = '';
343
- if (this.props.selection === 'start') {
344
- if (evt.key === 'ArrowRight') {
345
- nodeId = getNextNodeId(game.mainVariation(), true);
346
- evtOrigin = 'key-next';
347
- }
348
- else if (evt.key === 'End') {
349
- nodeId = getLastNodeId(game.mainVariation(), true);
350
- evtOrigin = 'key-last';
351
- }
343
+ if (evt.key === 'Home') {
344
+ nodeId = Movetext.firstNodeId(game, this.props.selection);
345
+ evtOrigin = 'key-first';
352
346
  }
353
- else {
354
- let currentNode = game.findById(this.props.selection);
355
- if (!currentNode) {
356
- return;
357
- }
358
- if (evt.key === 'Home') {
359
- nodeId = 'start';
360
- evtOrigin = 'key-first';
361
- }
362
- else if (evt.key === 'ArrowLeft') {
363
- nodeId = getPreviousNodeId(currentNode);
364
- evtOrigin = 'key-previous';
365
- }
366
- else if (evt.key === 'ArrowRight') {
367
- nodeId = getNextNodeId(currentNode, false);
368
- evtOrigin = 'key-next';
369
- }
370
- else if (evt.key === 'End') {
371
- nodeId = getLastNodeId(currentNode, false);
372
- evtOrigin = 'key-last';
373
- }
347
+ else if (evt.key === 'ArrowLeft') {
348
+ nodeId = Movetext.previousNodeId(game, this.props.selection);
349
+ evtOrigin = 'key-previous';
350
+ }
351
+ else if (evt.key === 'ArrowRight') {
352
+ nodeId = Movetext.nextNodeId(game, this.props.selection);
353
+ evtOrigin = 'key-next';
354
+ }
355
+ else { // evt.key === 'End'
356
+ nodeId = Movetext.lastNodeId(game, this.props.selection);
357
+ evtOrigin = 'key-last';
374
358
  }
375
359
  if (nodeId && this.props.onMoveSelected) {
376
360
  this.props.onMoveSelected(nodeId, evtOrigin);
@@ -378,7 +362,7 @@ export default class Movetext extends React.Component {
378
362
  }
379
363
 
380
364
  handleNodeClicked(nodeId) {
381
- this.focusFieldRef.current.focus();
365
+ this.focus();
382
366
  if (this.props.onMoveSelected) {
383
367
  this.props.onMoveSelected(nodeId === this.props.selection ? undefined : nodeId, 'click');
384
368
  }
@@ -403,6 +387,114 @@ export default class Movetext extends React.Component {
403
387
  return notation => notation;
404
388
  }
405
389
  }
390
+
391
+ /**
392
+ * Set the focus to the current component.
393
+ *
394
+ * @public
395
+ */
396
+ focus() {
397
+ this.focusFieldRef.current.focus();
398
+ }
399
+
400
+ /**
401
+ * Return the ID of the main variation in the given chess game.
402
+ * If the given selection corresponds already at the main variation, `undefined` is returned.
403
+ *
404
+ * This corresponds to the operation performed when the user presses key "home" on a `Movetext` component.
405
+ *
406
+ * @param {kokopu.Game} game Considered chess game.
407
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
408
+ * @returns {string?}
409
+ * @public
410
+ */
411
+ static firstNodeId(game, selection) {
412
+ if (!game.findById(selection)) {
413
+ return undefined;
414
+ }
415
+ return selection === 'start' ? undefined : 'start';
416
+ }
417
+
418
+ /**
419
+ * Return the ID of the node immediately preceding the given selection in the given chess game.
420
+ * If no such node exists, `undefined` is returned.
421
+ *
422
+ * This corresponds to the operation performed when the user presses key "arrow left" on a `Movetext` component.
423
+ *
424
+ * @param {kokopu.Game} game Considered chess game.
425
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
426
+ * @returns {string?}
427
+ * @public
428
+ */
429
+ static previousNodeId(game, selection) {
430
+ let currentNode = game.findById(selection);
431
+ if (!currentNode) {
432
+ return undefined;
433
+ }
434
+ if (selection === 'start') {
435
+ return undefined;
436
+ }
437
+ else if (selection.endsWith('start')) {
438
+ currentNode = currentNode.parentNode(); // `.parentNode()` returns always non-null ref here
439
+ }
440
+ while (currentNode) {
441
+ let previousNode = currentNode.previous();
442
+ if (previousNode) {
443
+ return previousNode.id();
444
+ }
445
+ currentNode = currentNode.parentVariation().parentNode();
446
+ }
447
+ return 'start';
448
+ }
449
+
450
+ /**
451
+ * Return the ID of the node immediately following the given selection in the given chess game.
452
+ * If no such node exists, `undefined` is returned.
453
+ *
454
+ * This corresponds to the operation performed when the user presses key "arrow right" on a `Movetext` component.
455
+ *
456
+ * @param {kokopu.Game} game Considered chess game.
457
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
458
+ * @returns {string?}
459
+ * @public
460
+ */
461
+ static nextNodeId(game, selection) {
462
+ let currentNode = game.findById(selection);
463
+ if (!currentNode) {
464
+ return undefined;
465
+ }
466
+ let nextNode = selection.endsWith('start') ? currentNode.first() : currentNode.next();
467
+ return nextNode ? nextNode.id() : undefined;
468
+ }
469
+
470
+ /**
471
+ * Return the ID of the node at then end of the variation in which lies the given selection in the given chess game.
472
+ * If the selection is already at the end its variation, `undefined` is returned.
473
+ *
474
+ * This corresponds to the operation performed when the user presses key "end" on a `Movetext` component.
475
+ *
476
+ * @param {kokopu.Game} game Considered chess game.
477
+ * @param {string} selection ID of the selected move (or `'start'` for the beginning of the main variation).
478
+ * @returns {string?}
479
+ * @public
480
+ */
481
+ static lastNodeId(game, selection) {
482
+ let currentNode = game.findById(selection);
483
+ if (!currentNode) {
484
+ return undefined;
485
+ }
486
+ currentNode = selection.endsWith('start') ? currentNode.first() : currentNode.next();
487
+ if (!currentNode) { // Ensure that the input node is not already the last one.
488
+ return undefined;
489
+ }
490
+ while (true) {
491
+ let nextNode = currentNode.next();
492
+ if (!nextNode) {
493
+ return currentNode.id();
494
+ }
495
+ currentNode = nextNode;
496
+ }
497
+ }
406
498
  }
407
499
 
408
500
 
@@ -569,39 +661,6 @@ function figurineNotation(text, fontName) {
569
661
  }
570
662
 
571
663
 
572
- function getPreviousNodeId(currentNode) {
573
- let previousNode = currentNode.previous();
574
- if (previousNode) {
575
- return previousNode.id();
576
- }
577
- else {
578
- let parentNode = currentNode.parentVariation().parentNode();
579
- return parentNode ? getPreviousNodeId(parentNode) : 'start';
580
- }
581
- }
582
-
583
-
584
- function getNextNodeId(currentNode, isVariation) {
585
- let nextNode = isVariation ? currentNode.first() : currentNode.next();
586
- return nextNode ? nextNode.id() : false;
587
- }
588
-
589
-
590
- function getLastNodeId(currentNode, isVariation) {
591
- currentNode = isVariation ? currentNode.first() : currentNode.next();
592
- if (!currentNode) { // Ensure that the input node is not already the last one.
593
- return false;
594
- }
595
- while (true) {
596
- let nextNode = currentNode.next();
597
- if (!nextNode) {
598
- return currentNode.id();
599
- }
600
- currentNode = nextNode;
601
- }
602
- }
603
-
604
-
605
664
  /**
606
665
  * Try to interpret the given object as a chess game.
607
666
  */
@@ -24,19 +24,33 @@ let { Chessboard } = require('../build/test_headless/index');
24
24
  let test = require('unit.js');
25
25
 
26
26
 
27
+ function testAdaptSquareSize(expectedSquareSize, width, height, coordinateVisible, smallScreenLimits) {
28
+ test.value(Chessboard.adaptSquareSize(width, height, coordinateVisible, smallScreenLimits)).is(expectedSquareSize);
29
+
30
+ let actualSize = Chessboard.size(expectedSquareSize, coordinateVisible, smallScreenLimits);
31
+ test.value(actualSize.width <= width && actualSize.height <= height).isTrue();
32
+ }
33
+
34
+ function testAdaptSquareSizeWithIncrement(expectedSquareSize, width, height, coordinateVisible, smallScreenLimits) {
35
+ testAdaptSquareSize(expectedSquareSize, width, height, coordinateVisible, smallScreenLimits);
36
+
37
+ let actualSizeIncremented = Chessboard.size(expectedSquareSize + 1, coordinateVisible, smallScreenLimits);
38
+ test.value(actualSizeIncremented.width > width || actualSizeIncremented.height > height).isTrue();
39
+ }
40
+
27
41
  describe('Adapt square-size', () => {
28
42
 
29
43
  it('Very small', () => { test.value(Chessboard.adaptSquareSize(10, 10, false)).is(Chessboard.minSquareSize()); });
30
44
  it('Very large', () => { test.value(Chessboard.adaptSquareSize(9999, 9999, true)).is(Chessboard.maxSquareSize()); });
31
45
 
32
- it('Size 185x300 with coordinates', () => { test.value(Chessboard.adaptSquareSize(185, 300, true)).is(19); });
33
- it('Size 185x300 without coordinates', () => { test.value(Chessboard.adaptSquareSize(185, 300, false)).is(20); });
34
- it('Size 300x200 with coordinates', () => { test.value(Chessboard.adaptSquareSize(300, 200, true)).is(23); });
35
- it('Size 300x200 without coordinates', () => { test.value(Chessboard.adaptSquareSize(300, 200, false)).is(25); });
36
- it('Size 375x500 with coordinates', () => { test.value(Chessboard.adaptSquareSize(375, 500, true)).is(40); });
37
- it('Size 375x500 without coordinates', () => { test.value(Chessboard.adaptSquareSize(375, 500, false)).is(41); });
38
- it('Size 600x450 with coordinates', () => { test.value(Chessboard.adaptSquareSize(600, 450, true)).is(54); });
39
- it('Size 600x450 without coordinates', () => { test.value(Chessboard.adaptSquareSize(600, 450, false)).is(56); });
46
+ it('Size 185x300 with coordinates', () => testAdaptSquareSizeWithIncrement(19, 185, 300, true));
47
+ it('Size 185x300 without coordinates', () => testAdaptSquareSizeWithIncrement(20, 185, 300, false));
48
+ it('Size 300x200 with coordinates', () => testAdaptSquareSizeWithIncrement(23, 300, 200, true));
49
+ it('Size 300x200 without coordinates', () => testAdaptSquareSizeWithIncrement(25, 300, 200, false));
50
+ it('Size 375x500 with coordinates', () => testAdaptSquareSizeWithIncrement(40, 375, 500, true));
51
+ it('Size 375x500 without coordinates', () => testAdaptSquareSizeWithIncrement(41, 375, 500, false));
52
+ it('Size 600x450 with coordinates', () => testAdaptSquareSizeWithIncrement(54, 600, 450, true));
53
+ it('Size 600x450 without coordinates', () => testAdaptSquareSizeWithIncrement(56, 600, 450, false));
40
54
  });
41
55
 
42
56
  describe('Adapt square-size with small-screen limits', () => {
@@ -58,18 +72,18 @@ describe('Adapt square-size with small-screen limits', () => {
58
72
 
59
73
  it('Window-limited', () => {
60
74
  window.innerWidth = 640;
61
- test.value(Chessboard.adaptSquareSize(9999, 9999, true, limits)).is(32);
75
+ testAdaptSquareSize(32, 9999, 9999, true, limits);
62
76
  });
63
77
  it('Available-space-limited 1', () => {
64
78
  window.innerWidth = 800;
65
- test.value(Chessboard.adaptSquareSize(375, 500, true, limits)).is(40);
79
+ testAdaptSquareSize(40, 375, 500, true, limits);
66
80
  });
67
81
  it('Available-space-limited 2', () => {
68
82
  window.innerWidth = 800;
69
- test.value(Chessboard.adaptSquareSize(375, 500, false, limits)).is(41);
83
+ testAdaptSquareSize(41, 375, 500, false, limits);
70
84
  });
71
85
  it('Force hidden coordinates', () => {
72
86
  window.innerWidth = 600;
73
- test.value(Chessboard.adaptSquareSize(185, 300, true, limits)).is(20);
87
+ testAdaptSquareSize(20, 185, 300, true, limits);
74
88
  });
75
89
  });
@@ -90,6 +90,10 @@ describe('Movetext interaction', function() {
90
90
  await setSandbox(browserContext, '');
91
91
  await focusFieldElement.sendKeys(Key.END);
92
92
  await compareSandbox(browserContext, expectedOnGoLast);
93
+
94
+ await setSandbox(browserContext, '');
95
+ await focusFieldElement.sendKeys(Key.ARROW_UP);
96
+ await compareSandbox(browserContext, '');
93
97
  });
94
98
  }
95
99
 
@@ -100,4 +104,5 @@ describe('Movetext interaction', function() {
100
104
  itCheckKeyboardActions(5, 'key on first selected', tpl('start', 'key-first'), tpl('start', 'key-previous'), tpl('1b', 'key-next'), tpl('6b', 'key-last'));
101
105
  itCheckKeyboardActions(6, 'key on last selected', tpl('start', 'key-first'), tpl('6w', 'key-previous'), '', '');
102
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'));
103
108
  });