kokopu-react 1.5.1 → 1.5.4

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,24 @@
1
1
  ChangeLog
2
2
  =========
3
3
 
4
+ 1.5.4 (May 1, 2022)
5
+ -------------------
6
+ * Provide method `Chessboard#size(..)` to retrieve the width/height of a `Chessboard` component instance.
7
+
8
+ 1.5.3 (April 30, 2022)
9
+ ----------------------
10
+ * Add method `Movetext#focus()`.
11
+ * Expose static methods `Movetext#firstNodeId(..)`, `Movetext#previousNodeId(..)`, `Movetext#nextNodeId(..)`
12
+ and `Movetext#lastNodeId(..)`.
13
+
14
+ 1.5.2 (April 29, 2022)
15
+ ----------------------
16
+ * Prevent default actions when handling key events in component `Movetext`.
17
+
18
+ 1.5.1 (April 29, 2022)
19
+ ----------------------
20
+ * Add argument `evtOrigin` in `onMoveSelected(..)` callback in component `Movetext`.
21
+
4
22
  1.5.0 (April 27, 2022)
5
23
  ----------------------
6
24
  * Add component `Movetext` to display chess games.
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,43 +522,33 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
522
522
  }
523
523
  }, {
524
524
  key: "handleKeyDownInFocusField",
525
- value: function handleKeyDownInFocusField(evt) {
525
+ value: function handleKeyDownInFocusField(evt, game) {
526
+ if (evt.key !== 'Home' && evt.key !== 'ArrowLeft' && evt.key !== 'ArrowRight' && evt.key !== 'End') {
527
+ return;
528
+ }
529
+
530
+ evt.preventDefault();
531
+
526
532
  if (!this.props.selection) {
527
533
  return;
528
534
  }
529
535
 
530
- var game = parseGame(this.props.game, this.props.gameIndex).game;
531
- var nodeId = false;
536
+ var nodeId = undefined;
532
537
  var evtOrigin = '';
533
538
 
534
- if (this.props.selection === 'start') {
535
- if (evt.key === 'ArrowRight') {
536
- nodeId = getNextNodeId(game.mainVariation(), true);
537
- evtOrigin = 'key-next';
538
- } else if (evt.key === 'End') {
539
- nodeId = getLastNodeId(game.mainVariation(), true);
540
- evtOrigin = 'key-last';
541
- }
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';
542
548
  } else {
543
- var currentNode = game.findById(this.props.selection);
544
-
545
- if (!currentNode) {
546
- return;
547
- }
548
-
549
- if (evt.key === 'Home') {
550
- nodeId = 'start';
551
- evtOrigin = 'key-first';
552
- } else if (evt.key === 'ArrowLeft') {
553
- nodeId = getPreviousNodeId(currentNode);
554
- evtOrigin = 'key-previous';
555
- } else if (evt.key === 'ArrowRight') {
556
- nodeId = getNextNodeId(currentNode, false);
557
- evtOrigin = 'key-next';
558
- } else if (evt.key === 'End') {
559
- nodeId = getLastNodeId(currentNode, false);
560
- evtOrigin = 'key-last';
561
- }
549
+ // evt.key === 'End'
550
+ nodeId = Movetext.lastNodeId(game, this.props.selection);
551
+ evtOrigin = 'key-last';
562
552
  }
563
553
 
564
554
  if (nodeId && this.props.onMoveSelected) {
@@ -568,7 +558,7 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
568
558
  }, {
569
559
  key: "handleNodeClicked",
570
560
  value: function handleNodeClicked(nodeId) {
571
- this.focusFieldRef.current.focus();
561
+ this.focus();
572
562
 
573
563
  if (this.props.onMoveSelected) {
574
564
  this.props.onMoveSelected(nodeId === this.props.selection ? undefined : nodeId, 'click');
@@ -608,6 +598,139 @@ var Movetext = /*#__PURE__*/function (_React$Component) {
608
598
  };
609
599
  }
610
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
+ }
611
734
  }]);
612
735
 
613
736
  return Movetext;
@@ -775,41 +898,6 @@ function figurineNotation(text, fontName) {
775
898
 
776
899
  return result;
777
900
  }
778
-
779
- function getPreviousNodeId(currentNode) {
780
- var previousNode = currentNode.previous();
781
-
782
- if (previousNode) {
783
- return previousNode.id();
784
- } else {
785
- var parentNode = currentNode.parentVariation().parentNode();
786
- return parentNode ? getPreviousNodeId(parentNode) : 'start';
787
- }
788
- }
789
-
790
- function getNextNodeId(currentNode, isVariation) {
791
- var nextNode = isVariation ? currentNode.first() : currentNode.next();
792
- return nextNode ? nextNode.id() : false;
793
- }
794
-
795
- function getLastNodeId(currentNode, isVariation) {
796
- currentNode = isVariation ? currentNode.first() : currentNode.next();
797
-
798
- if (!currentNode) {
799
- // Ensure that the input node is not already the last one.
800
- return false;
801
- }
802
-
803
- while (true) {
804
- var nextNode = currentNode.next();
805
-
806
- if (!nextNode) {
807
- return currentNode.id();
808
- }
809
-
810
- currentNode = nextNode;
811
- }
812
- }
813
901
  /**
814
902
  * Try to interpret the given object as a chess game.
815
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.1",
4
- "description": "A React-based library to create and display chessboard components.",
3
+ "version": "1.5.4",
4
+ "description": "A React-based library to create and display chessboard and chess-related components.",
5
5
  "keywords": [
6
6
  "chess",
7
7
  "react",
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,44 +330,31 @@ export default class Movetext extends React.Component {
330
330
  return comment ? comment : undefined;
331
331
  }
332
332
 
333
- handleKeyDownInFocusField(evt) {
333
+ handleKeyDownInFocusField(evt, game) {
334
+ if (evt.key !== 'Home' && evt.key !== 'ArrowLeft' && evt.key !== 'ArrowRight' && evt.key !== 'End') {
335
+ return;
336
+ }
337
+ evt.preventDefault();
334
338
  if (!this.props.selection) {
335
339
  return;
336
340
  }
337
- let game = parseGame(this.props.game, this.props.gameIndex).game;
338
- let nodeId = false;
341
+ let nodeId = undefined;
339
342
  let evtOrigin = '';
340
- if (this.props.selection === 'start') {
341
- if (evt.key === 'ArrowRight') {
342
- nodeId = getNextNodeId(game.mainVariation(), true);
343
- evtOrigin = 'key-next';
344
- }
345
- else if (evt.key === 'End') {
346
- nodeId = getLastNodeId(game.mainVariation(), true);
347
- evtOrigin = 'key-last';
348
- }
343
+ if (evt.key === 'Home') {
344
+ nodeId = Movetext.firstNodeId(game, this.props.selection);
345
+ evtOrigin = 'key-first';
349
346
  }
350
- else {
351
- let currentNode = game.findById(this.props.selection);
352
- if (!currentNode) {
353
- return;
354
- }
355
- if (evt.key === 'Home') {
356
- nodeId = 'start';
357
- evtOrigin = 'key-first';
358
- }
359
- else if (evt.key === 'ArrowLeft') {
360
- nodeId = getPreviousNodeId(currentNode);
361
- evtOrigin = 'key-previous';
362
- }
363
- else if (evt.key === 'ArrowRight') {
364
- nodeId = getNextNodeId(currentNode, false);
365
- evtOrigin = 'key-next';
366
- }
367
- else if (evt.key === 'End') {
368
- nodeId = getLastNodeId(currentNode, false);
369
- evtOrigin = 'key-last';
370
- }
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';
371
358
  }
372
359
  if (nodeId && this.props.onMoveSelected) {
373
360
  this.props.onMoveSelected(nodeId, evtOrigin);
@@ -375,7 +362,7 @@ export default class Movetext extends React.Component {
375
362
  }
376
363
 
377
364
  handleNodeClicked(nodeId) {
378
- this.focusFieldRef.current.focus();
365
+ this.focus();
379
366
  if (this.props.onMoveSelected) {
380
367
  this.props.onMoveSelected(nodeId === this.props.selection ? undefined : nodeId, 'click');
381
368
  }
@@ -400,6 +387,114 @@ export default class Movetext extends React.Component {
400
387
  return notation => notation;
401
388
  }
402
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
+ }
403
498
  }
404
499
 
405
500
 
@@ -566,39 +661,6 @@ function figurineNotation(text, fontName) {
566
661
  }
567
662
 
568
663
 
569
- function getPreviousNodeId(currentNode) {
570
- let previousNode = currentNode.previous();
571
- if (previousNode) {
572
- return previousNode.id();
573
- }
574
- else {
575
- let parentNode = currentNode.parentVariation().parentNode();
576
- return parentNode ? getPreviousNodeId(parentNode) : 'start';
577
- }
578
- }
579
-
580
-
581
- function getNextNodeId(currentNode, isVariation) {
582
- let nextNode = isVariation ? currentNode.first() : currentNode.next();
583
- return nextNode ? nextNode.id() : false;
584
- }
585
-
586
-
587
- function getLastNodeId(currentNode, isVariation) {
588
- currentNode = isVariation ? currentNode.first() : currentNode.next();
589
- if (!currentNode) { // Ensure that the input node is not already the last one.
590
- return false;
591
- }
592
- while (true) {
593
- let nextNode = currentNode.next();
594
- if (!nextNode) {
595
- return currentNode.id();
596
- }
597
- currentNode = nextNode;
598
- }
599
- }
600
-
601
-
602
664
  /**
603
665
  * Try to interpret the given object as a chess game.
604
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
  });