kokopu-react 3.0.2 → 3.2.0

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 (253) hide show
  1. package/.eslintrc.yml +1 -1
  2. package/CHANGELOG.md +13 -1
  3. package/dist/lib/chessboard/BoardProperties.d.ts +91 -0
  4. package/dist/lib/chessboard/BoardProperties.js +43 -0
  5. package/dist/lib/chessboard/BoardProperties.js.map +1 -0
  6. package/dist/lib/chessboard/Chessboard.css +13 -13
  7. package/dist/lib/chessboard/Chessboard.d.ts +14 -58
  8. package/dist/lib/chessboard/Chessboard.js +26 -92
  9. package/dist/lib/chessboard/Chessboard.js.map +1 -1
  10. package/dist/lib/chessboard/ChessboardImpl.js.map +1 -1
  11. package/dist/lib/chessboard/Draggable.js.map +1 -1
  12. package/dist/lib/chessboard/Motion.js.map +1 -1
  13. package/dist/lib/chessboard/colorsets.js.map +1 -1
  14. package/dist/lib/chessboard/piecesets.js.map +1 -1
  15. package/dist/lib/errorbox/ErrorBox.css +18 -18
  16. package/dist/lib/errorbox/ErrorBox.js.map +1 -1
  17. package/dist/lib/errorbox/parsing.d.ts +57 -0
  18. package/dist/lib/errorbox/parsing.js +140 -0
  19. package/dist/lib/errorbox/parsing.js.map +1 -0
  20. package/dist/lib/exception.js.map +1 -1
  21. package/dist/lib/i18n.d.ts +6 -0
  22. package/dist/lib/i18n.js +7 -0
  23. package/dist/lib/i18n.js.map +1 -1
  24. package/dist/lib/icons/AnnotationSymbolShape.css +6 -6
  25. package/dist/lib/icons/AnnotationSymbolShape.js.map +1 -1
  26. package/dist/lib/icons/ArrowMarkerIcon.js.map +1 -1
  27. package/dist/lib/icons/ArrowTip.js.map +1 -1
  28. package/dist/lib/icons/ChessPieceIcon.js.map +1 -1
  29. package/dist/lib/icons/SquareMarkerIcon.js.map +1 -1
  30. package/dist/lib/icons/TextMarkerIcon.js.map +1 -1
  31. package/dist/lib/icons/arrow.css +1 -1
  32. package/dist/lib/index.d.ts +5 -1
  33. package/dist/lib/index.js +3 -0
  34. package/dist/lib/index.js.map +1 -1
  35. package/dist/lib/movetext/Movetext.css +27 -40
  36. package/dist/lib/movetext/Movetext.d.ts +9 -43
  37. package/dist/lib/movetext/Movetext.js +15 -67
  38. package/dist/lib/movetext/Movetext.js.map +1 -1
  39. package/dist/lib/movetext/MovetextImpl.d.ts +7 -28
  40. package/dist/lib/movetext/MovetextImpl.js +15 -94
  41. package/dist/lib/movetext/MovetextImpl.js.map +1 -1
  42. package/dist/lib/movetext/fonts.css +7 -7
  43. package/dist/lib/movetext/htmlFilter.js.map +1 -1
  44. package/dist/lib/movetext/moveFormatter.js.map +1 -1
  45. package/dist/lib/navigationboard/NavigationBoard.d.ts +156 -0
  46. package/dist/lib/navigationboard/NavigationBoard.js +195 -0
  47. package/dist/lib/navigationboard/NavigationBoard.js.map +1 -0
  48. package/dist/lib/navigationboard/NavigationButton.d.ts +54 -0
  49. package/dist/lib/navigationboard/NavigationButton.js +35 -0
  50. package/dist/lib/navigationboard/NavigationButton.js.map +1 -0
  51. package/dist/lib/navigationboard/NavigationField.css +36 -0
  52. package/dist/lib/navigationboard/NavigationField.d.ts +81 -0
  53. package/dist/lib/navigationboard/NavigationField.js +154 -0
  54. package/dist/lib/navigationboard/NavigationField.js.map +1 -0
  55. package/dist/lib/navigationboard/NavigationToolbar.css +54 -0
  56. package/dist/lib/navigationboard/NavigationToolbar.d.ts +34 -0
  57. package/dist/lib/navigationboard/NavigationToolbar.js +73 -0
  58. package/dist/lib/navigationboard/NavigationToolbar.js.map +1 -0
  59. package/dist/lib/navigationboard/iconPaths.d.ts +29 -0
  60. package/dist/lib/navigationboard/iconPaths.js +91 -0
  61. package/dist/lib/navigationboard/iconPaths.js.map +1 -0
  62. package/dist/lib/sanitization.js.map +1 -1
  63. package/dist/lib/types.js.map +1 -1
  64. package/dist/lib/util.js.map +1 -1
  65. package/doc_src/demo/PageChessboardBase.tsx +115 -115
  66. package/doc_src/demo/PageChessboardInteraction.tsx +303 -303
  67. package/doc_src/demo/PageChessboardMove.tsx +173 -156
  68. package/doc_src/demo/PageChessboardSmallScreens.tsx +94 -94
  69. package/doc_src/demo/PageMovetextBase.tsx +180 -180
  70. package/doc_src/demo/PageMovetextInteraction.tsx +123 -123
  71. package/doc_src/demo/PageNavigationBoardBase.tsx +201 -0
  72. package/doc_src/demo/PageNavigationBoardUncontrolledVsControlled.tsx +114 -0
  73. package/doc_src/demo/decs.d.ts +4 -4
  74. package/doc_src/demo/demo.css +12 -12
  75. package/doc_src/demo/game-3.pgn +10 -0
  76. package/doc_src/demo/util.ts +1 -1
  77. package/doc_src/examples/Movetext.md +1 -1
  78. package/doc_src/examples/NavigationBoard.md +15 -0
  79. package/doc_src/theming/LogoRenderer.js +13 -13
  80. package/doc_src/theming/theming.css +5 -5
  81. package/package.json +95 -95
  82. package/scripts/doc.styleguide.config.js +133 -123
  83. package/scripts/doc.tsconfig.json +5 -5
  84. package/scripts/docker-compose.yml +1 -1
  85. package/scripts/test-graphic.tsconfig.json +5 -5
  86. package/scripts/test-graphic.webpack.config.js +57 -57
  87. package/scripts/test-headless.webpack.config.js +29 -29
  88. package/src/chessboard/BoardProperties.ts +131 -0
  89. package/src/chessboard/Chessboard.css +13 -13
  90. package/src/chessboard/Chessboard.tsx +389 -518
  91. package/src/chessboard/ChessboardImpl.tsx +557 -557
  92. package/src/chessboard/Draggable.tsx +185 -185
  93. package/src/chessboard/Motion.tsx +67 -67
  94. package/src/chessboard/colorsets.ts +66 -66
  95. package/src/chessboard/piecesets.ts +84 -84
  96. package/src/decs.d.ts +2 -2
  97. package/src/errorbox/ErrorBox.css +18 -18
  98. package/src/errorbox/ErrorBox.tsx +75 -75
  99. package/src/errorbox/parsing.tsx +158 -0
  100. package/src/exception.ts +17 -17
  101. package/src/i18n.ts +8 -0
  102. package/src/icons/AnnotationSymbolShape.css +6 -6
  103. package/src/icons/AnnotationSymbolShape.tsx +31 -31
  104. package/src/icons/ArrowMarkerIcon.tsx +34 -34
  105. package/src/icons/ArrowTip.tsx +14 -14
  106. package/src/icons/ChessPieceIcon.tsx +45 -45
  107. package/src/icons/SquareMarkerIcon.tsx +20 -20
  108. package/src/icons/TextMarkerIcon.tsx +29 -29
  109. package/src/icons/arrow.css +1 -1
  110. package/src/index.ts +8 -3
  111. package/src/movetext/Movetext.css +27 -40
  112. package/src/movetext/Movetext.tsx +163 -228
  113. package/src/movetext/MovetextImpl.tsx +351 -438
  114. package/src/movetext/fonts.css +7 -7
  115. package/src/movetext/htmlFilter.tsx +129 -129
  116. package/src/movetext/moveFormatter.tsx +37 -37
  117. package/src/navigationboard/NavigationBoard.tsx +350 -0
  118. package/src/navigationboard/NavigationButton.ts +72 -0
  119. package/src/navigationboard/NavigationField.css +36 -0
  120. package/src/navigationboard/NavigationField.tsx +185 -0
  121. package/src/navigationboard/NavigationToolbar.css +54 -0
  122. package/src/navigationboard/NavigationToolbar.tsx +117 -0
  123. package/src/navigationboard/iconPaths.ts +98 -0
  124. package/src/sanitization.ts +17 -17
  125. package/src/types.ts +116 -116
  126. package/src/util.ts +11 -11
  127. package/test/01_types.js +167 -167
  128. package/test/02_chessboard_util.js +114 -114
  129. package/test/03_error_box_graphic.js +16 -16
  130. package/test/04_icons_graphic.js +32 -32
  131. package/test/05_chessboard_graphic.js +60 -58
  132. package/test/06_chessboard_animation.js +36 -36
  133. package/test/07_chessboard_interaction.js +101 -101
  134. package/test/08_chessboard_play_moves.js +62 -62
  135. package/test/09_chessboard_small_screens.js +42 -21
  136. package/test/10_movetext_graphic.js +36 -31
  137. package/test/11_movetext_interaction.js +100 -96
  138. package/test/12_navigation_board_base.js +151 -0
  139. package/test/13_navigation_board_graphic.js +68 -0
  140. package/test/14_navigation_board_autoplay.js +100 -0
  141. package/test/common/graphic.js +106 -97
  142. package/test/graphic_references/05_chessboard_graphic/move/illegal_null-move.png +0 -0
  143. package/test/graphic_references/05_chessboard_graphic/move/null-move.png +0 -0
  144. package/test/graphic_references/09_chessboard_small_screens/top_bottom_components/default_bottom_component.png +0 -0
  145. package/test/graphic_references/09_chessboard_small_screens/top_bottom_components/default_top_component.png +0 -0
  146. package/test/graphic_references/09_chessboard_small_screens/top_bottom_components/reduced_bottom_component.png +0 -0
  147. package/test/graphic_references/09_chessboard_small_screens/top_bottom_components/reduced_top_component.png +0 -0
  148. package/test/graphic_references/10_movetext_graphic/format_move/format_text.png +0 -0
  149. package/test/graphic_references/10_movetext_graphic/options/wrong_piece_symbol_value.png +0 -0
  150. package/test/graphic_references/11_movetext_interaction/select_moves/click_on_unselected_without_callback.png +0 -0
  151. package/test/graphic_references/12_navigation_board_base/controlled/default.png +0 -0
  152. package/test/graphic_references/12_navigation_board_base/controlled/forbidden_navigation_0.png +0 -0
  153. package/test/graphic_references/12_navigation_board_base/controlled/forbidden_navigation_1.png +0 -0
  154. package/test/graphic_references/12_navigation_board_base/controlled/invalid_node_id.png +0 -0
  155. package/test/graphic_references/12_navigation_board_base/controlled/no_callback_defined_0.png +0 -0
  156. package/test/graphic_references/12_navigation_board_base/controlled/no_callback_defined_1.png +0 -0
  157. package/test/graphic_references/12_navigation_board_base/controlled/overriding_initial_node_id.png +0 -0
  158. package/test/graphic_references/12_navigation_board_base/controlled/regular_navigation_0.png +0 -0
  159. package/test/graphic_references/12_navigation_board_base/controlled/regular_navigation_1.png +0 -0
  160. package/test/graphic_references/12_navigation_board_base/controlled/regular_navigation_2.png +0 -0
  161. package/test/graphic_references/12_navigation_board_base/controlled/regular_navigation_3.png +0 -0
  162. package/test/graphic_references/12_navigation_board_base/uncontrolled/forbidden_navigation_at_beginning_0.png +0 -0
  163. package/test/graphic_references/12_navigation_board_base/uncontrolled/forbidden_navigation_at_beginning_1.png +0 -0
  164. package/test/graphic_references/12_navigation_board_base/uncontrolled/forbidden_navigation_at_beginning_2.png +0 -0
  165. package/test/graphic_references/12_navigation_board_base/uncontrolled/forbidden_navigation_at_end_0.png +0 -0
  166. package/test/graphic_references/12_navigation_board_base/uncontrolled/forbidden_navigation_at_end_1.png +0 -0
  167. package/test/graphic_references/12_navigation_board_base/uncontrolled/forbidden_navigation_at_end_2.png +0 -0
  168. package/test/graphic_references/12_navigation_board_base/uncontrolled/initial_state_at_end.png +0 -0
  169. package/test/graphic_references/12_navigation_board_base/uncontrolled/initial_state_at_specific_id.png +0 -0
  170. package/test/graphic_references/12_navigation_board_base/uncontrolled/initial_state_default.png +0 -0
  171. package/test/graphic_references/12_navigation_board_base/uncontrolled/initial_state_default_with_game_index.png +0 -0
  172. package/test/graphic_references/12_navigation_board_base/uncontrolled/initial_state_on_null-move.png +0 -0
  173. package/test/graphic_references/12_navigation_board_base/uncontrolled/navigation_with_keys_0.png +0 -0
  174. package/test/graphic_references/12_navigation_board_base/uncontrolled/navigation_with_keys_1.png +0 -0
  175. package/test/graphic_references/12_navigation_board_base/uncontrolled/navigation_with_keys_2.png +0 -0
  176. package/test/graphic_references/12_navigation_board_base/uncontrolled/navigation_with_keys_3.png +0 -0
  177. package/test/graphic_references/12_navigation_board_base/uncontrolled/navigation_with_keys_4.png +0 -0
  178. package/test/graphic_references/12_navigation_board_base/uncontrolled/regular_flipping_0.png +0 -0
  179. package/test/graphic_references/12_navigation_board_base/uncontrolled/regular_flipping_1.png +0 -0
  180. package/test/graphic_references/12_navigation_board_base/uncontrolled/regular_navigation_0.png +0 -0
  181. package/test/graphic_references/12_navigation_board_base/uncontrolled/regular_navigation_1.png +0 -0
  182. package/test/graphic_references/12_navigation_board_base/uncontrolled/regular_navigation_2.png +0 -0
  183. package/test/graphic_references/12_navigation_board_base/uncontrolled/regular_navigation_3.png +0 -0
  184. package/test/graphic_references/12_navigation_board_base/uncontrolled/regular_navigation_4.png +0 -0
  185. package/test/graphic_references/12_navigation_board_base/uncontrolled/regular_navigation_5.png +0 -0
  186. package/test/graphic_references/13_navigation_board_graphic/additional_buttons/error_1.png +0 -0
  187. package/test/graphic_references/13_navigation_board_graphic/additional_buttons/error_2.png +0 -0
  188. package/test/graphic_references/13_navigation_board_graphic/additional_buttons/multiple_buttons.png +0 -0
  189. package/test/graphic_references/13_navigation_board_graphic/additional_buttons/single_button.png +0 -0
  190. package/test/graphic_references/13_navigation_board_graphic/error/pgn_parsing_error.png +0 -0
  191. package/test/graphic_references/13_navigation_board_graphic/error/wrong_game_type.png +0 -0
  192. package/test/graphic_references/13_navigation_board_graphic/theme/custom_1.png +0 -0
  193. package/test/graphic_references/13_navigation_board_graphic/theme/custom_2.png +0 -0
  194. package/test/graphic_references/13_navigation_board_graphic/theme/large.png +0 -0
  195. package/test/graphic_references/13_navigation_board_graphic/theme/small.png +0 -0
  196. package/test/graphic_references/14_navigation_board_autoplay/controlled/after_1_move_acknowledging_is-playing_changes.png +0 -0
  197. package/test/graphic_references/14_navigation_board_autoplay/controlled/after_1_move_not_acknowledging_is-playing_changes.png +0 -0
  198. package/test/graphic_references/14_navigation_board_autoplay/controlled/after_2_moves_acknowledging_node_changes.png +0 -0
  199. package/test/graphic_references/14_navigation_board_autoplay/controlled/after_2_moves_not_acknowledging_node_changes.png +0 -0
  200. package/test/graphic_references/14_navigation_board_autoplay/controlled/after_reaching_the_end.png +0 -0
  201. package/test/graphic_references/14_navigation_board_autoplay/controlled/already_at_the_end.png +0 -0
  202. package/test/graphic_references/14_navigation_board_autoplay/controlled/initial_state_acknowledging_is-playing_changes.png +0 -0
  203. package/test/graphic_references/14_navigation_board_autoplay/controlled/initial_state_acknowledging_node_changes.png +0 -0
  204. package/test/graphic_references/14_navigation_board_autoplay/controlled/initial_state_at_the_end.png +0 -0
  205. package/test/graphic_references/14_navigation_board_autoplay/controlled/initial_state_close_to_end.png +0 -0
  206. package/test/graphic_references/14_navigation_board_autoplay/controlled/initial_state_not_acknowledging_is-playing_changes.png +0 -0
  207. package/test/graphic_references/14_navigation_board_autoplay/controlled/initial_state_not_acknowledging_node_changes.png +0 -0
  208. package/test/graphic_references/14_navigation_board_autoplay/uncontrolled/after_2_moves_from_beginning_after_stop.png +0 -0
  209. package/test/graphic_references/14_navigation_board_autoplay/uncontrolled/after_2_moves_from_beginning_before_stop.png +0 -0
  210. package/test/graphic_references/14_navigation_board_autoplay/uncontrolled/after_reaching_the_end_after_stop.png +0 -0
  211. package/test/graphic_references/14_navigation_board_autoplay/uncontrolled/after_reaching_the_end_before_stop.png +0 -0
  212. package/test/graphic_references/14_navigation_board_autoplay/uncontrolled/initial_state_close_to_end.png +0 -0
  213. package/test/graphic_references/14_navigation_board_autoplay/uncontrolled/initial_state_default.png +0 -0
  214. package/test/graphic_test_app/03_error_box_graphic/base.tsx +14 -14
  215. package/test/graphic_test_app/03_error_box_graphic/localization.tsx +1 -1
  216. package/test/graphic_test_app/04_icons_graphic/chess_piece.tsx +7 -7
  217. package/test/graphic_test_app/04_icons_graphic/marker.tsx +15 -15
  218. package/test/graphic_test_app/05_chessboard_graphic/annotations.tsx +7 -7
  219. package/test/graphic_test_app/05_chessboard_graphic/base.tsx +6 -6
  220. package/test/graphic_test_app/05_chessboard_graphic/chess_variants.tsx +7 -7
  221. package/test/graphic_test_app/05_chessboard_graphic/flipped.tsx +5 -5
  222. package/test/graphic_test_app/05_chessboard_graphic/move.tsx +12 -10
  223. package/test/graphic_test_app/05_chessboard_graphic/theme.tsx +12 -12
  224. package/test/graphic_test_app/06_chessboard_animation/base_1.tsx +6 -6
  225. package/test/graphic_test_app/06_chessboard_animation/base_2.tsx +6 -6
  226. package/test/graphic_test_app/06_chessboard_animation/base_3.tsx +6 -6
  227. package/test/graphic_test_app/06_chessboard_animation/on_redraw.tsx +16 -16
  228. package/test/graphic_test_app/07_chessboard_interaction/click_squares.tsx +5 -5
  229. package/test/graphic_test_app/07_chessboard_interaction/edit_arrows.tsx +4 -4
  230. package/test/graphic_test_app/07_chessboard_interaction/error.tsx +2 -2
  231. package/test/graphic_test_app/07_chessboard_interaction/move_pieces.tsx +5 -5
  232. package/test/graphic_test_app/08_chessboard_play_moves/base.tsx +8 -8
  233. package/test/graphic_test_app/08_chessboard_play_moves/promotions.tsx +4 -4
  234. package/test/graphic_test_app/09_chessboard_small_screens/base.tsx +9 -9
  235. package/test/graphic_test_app/09_chessboard_small_screens/top_bottom_components.tsx +48 -0
  236. package/test/graphic_test_app/10_movetext_graphic/base.tsx +4 -4
  237. package/test/graphic_test_app/10_movetext_graphic/error.tsx +6 -6
  238. package/test/graphic_test_app/10_movetext_graphic/format_move.tsx +31 -0
  239. package/test/graphic_test_app/10_movetext_graphic/html.tsx +5 -5
  240. package/test/graphic_test_app/10_movetext_graphic/options.tsx +10 -9
  241. package/test/graphic_test_app/11_movetext_interaction/error.tsx +1 -1
  242. package/test/graphic_test_app/11_movetext_interaction/select_moves.tsx +11 -10
  243. package/test/graphic_test_app/12_navigation_board_base/controlled.tsx +43 -0
  244. package/test/graphic_test_app/12_navigation_board_base/uncontrolled.tsx +56 -0
  245. package/test/graphic_test_app/13_navigation_board_graphic/additional_buttons.tsx +49 -0
  246. package/test/graphic_test_app/13_navigation_board_graphic/error.tsx +34 -0
  247. package/test/graphic_test_app/13_navigation_board_graphic/theme.tsx +38 -0
  248. package/test/graphic_test_app/14_navigation_board_autoplay/controlled.tsx +73 -0
  249. package/test/graphic_test_app/14_navigation_board_autoplay/uncontrolled.tsx +34 -0
  250. package/test/graphic_test_app/common/decs.d.ts +2 -2
  251. package/test/graphic_test_app/common/test_app.css +15 -15
  252. package/test/graphic_test_app/common/test_app.tsx +48 -48
  253. package/tsconfig.json +13 -13
@@ -29,7 +29,9 @@ import { Game, Color, GameResult, Node as GameNode, Variation, nagSymbol } from
29
29
  import { i18n } from '../i18n';
30
30
  import { fillPlaceholder } from '../util';
31
31
 
32
- import { Chessboard, ChessboardProps } from '../chessboard/Chessboard';
32
+ import { StaticBoardGraphicProps } from '../chessboard/BoardProperties';
33
+ import { Chessboard } from '../chessboard/Chessboard';
34
+ import { NavigationField, firstNodeId, previousNodeId, nextNodeId, lastNodeId } from '../navigationboard/NavigationField';
33
35
  import { htmlFilter } from './htmlFilter';
34
36
 
35
37
  import './Movetext.css';
@@ -37,25 +39,19 @@ import './Movetext.css';
37
39
 
38
40
  interface MovetextImplProps {
39
41
 
40
- game: Game;
41
-
42
- diagramOptions: {
43
- flipped?: ChessboardProps['flipped'],
44
- squareSize?: ChessboardProps['squareSize'],
45
- coordinateVisible?: ChessboardProps['coordinateVisible'],
46
- turnVisible?: ChessboardProps['turnVisible'],
47
- colorset?: ChessboardProps['colorset'],
48
- pieceset?: ChessboardProps['pieceset'],
49
- smallScreenLimits?: ChessboardProps['smallScreenLimits'],
50
- };
51
- moveFormatter: (notation: string) => React.ReactNode;
52
- diagramVisible: boolean;
53
- headerVisible: boolean;
54
-
55
- selection?: string;
56
- interactionMode?: 'selectMove';
57
-
58
- onMoveSelected?: (nodeId: string | undefined, evtOrigin: 'key-first' | 'key-previous' | 'key-next' | 'key-last' | 'key-exit' | 'click') => void;
42
+ game: Game;
43
+
44
+ diagramOptions: Partial<StaticBoardGraphicProps> & {
45
+ flipped?: boolean,
46
+ };
47
+ moveFormatter: (notation: string) => React.ReactNode;
48
+ diagramVisible: boolean;
49
+ headerVisible: boolean;
50
+
51
+ selection?: string;
52
+ interactionMode?: 'selectMove';
53
+
54
+ onMoveSelected?: (nodeId: string | undefined, evtOrigin: 'key-first' | 'key-previous' | 'key-next' | 'key-last' | 'key-exit' | 'click') => void;
59
55
  }
60
56
 
61
57
 
@@ -66,419 +62,336 @@ interface MovetextImplProps {
66
62
  */
67
63
  export class MovetextImpl extends React.Component<MovetextImplProps> {
68
64
 
69
- private focusFieldRef: React.RefObject<HTMLAnchorElement> = React.createRef();
70
-
71
- render() {
72
- return (
73
- <div className="kokopu-movetext">
74
- {this.renderHeaders()}
75
- {this.renderBody()}
76
- {this.renderFocusField()}
77
- </div>
78
- );
79
- }
80
-
81
- private renderHeaders() {
82
- if (!this.props.headerVisible) {
83
- return undefined;
84
- }
85
-
86
- const filter = htmlFilter('header');
87
- const headers: React.ReactNode[] = [];
88
- headers.push(this.renderPlayerHeaders(filter, 'w'));
89
- headers.push(this.renderPlayerHeaders(filter, 'b'));
90
- headers.push(this.renderEventRoundHeaders(filter));
91
- headers.push(this.renderDateSiteHeaders(filter));
92
- headers.push(this.renderAnnotatorHeader(filter));
93
- return headers.some(element => element !== undefined) ? <div className="kokopu-headers">{headers}</div> : undefined;
94
- }
95
-
96
- /**
97
- * Header containing the player-related information (name, rating, title) corresponding to the given color.
98
- */
99
- private renderPlayerHeaders(filter: (text: string) => React.ReactNode, color: Color) {
100
- const playerName = this.props.game.playerName(color);
101
- if (playerName === undefined) {
102
- return undefined;
103
- }
104
- const title = this.props.game.playerTitle(color);
105
- const rating = this.props.game.playerElo(color);
106
-
107
- const classNames = [ 'kokopu-headerGroup-player', color === 'w' ? 'kokopu-headerGroup-whitePlayer' : 'kokopu-headerGroup-blackPlayer' ];
108
- const colorTag = <span className="kokopu-colorTag"></span>;
109
- const playerNameElement = <span className="kokopu-header-playerName">{filter(playerName)}</span>;
110
- const titleElement = title === undefined ? undefined : <span className="kokopu-header-playerTitle">{filter(title)}</span>;
111
- const ratingElement = rating === undefined ? undefined : <span className="kokopu-header-playerRating">{rating}</span>;
112
- const separator = title === undefined || rating === undefined ? undefined : '\u00a0'; // \u00a0 == &nbsp;
113
- const titleRatingGroup = title === undefined && rating === undefined ? undefined :
114
- <span className="kokopu-headerGroup-titleRating">{titleElement}{separator}{ratingElement}</span>;
115
- return <div className={classNames.join(' ')} key={'player-' + color}>{colorTag}{playerNameElement}{titleRatingGroup}</div>;
116
- }
117
-
118
- /**
119
- * Header containing the event-related information: event + round.
120
- */
121
- private renderEventRoundHeaders(filter: (text: string) => React.ReactNode) {
122
- const evt = this.props.game.event();
123
- if (evt === undefined) {
124
- return undefined;
125
- }
126
- const fullRound = this.props.game.fullRound();
127
- const roundElement = fullRound === undefined ? undefined : <span className="kokopu-header-round">{fullRound}</span>;
128
- const evtElement = <span className="kokopu-header-event">{filter(evt)}</span>;
129
- return <div className="kokopu-headerGroup-eventRound" key="event-round">{evtElement}{roundElement}</div>;
130
- }
131
-
132
- /**
133
- * Header containing the date/place information.
134
- */
135
- private renderDateSiteHeaders(filter: (text: string) => React.ReactNode) {
136
- const date = this.props.game.dateAsString();
137
- const site = this.props.game.site();
138
- if (date === undefined && site === undefined) {
139
- return undefined;
140
- }
141
- const dateElement = date === undefined ? undefined : <span className="kokopu-header-date">{capitalizeFirstWord(date)}</span>;
142
- const siteElement = site === undefined ? undefined : <span className="kokopu-header-site">{filter(site)}</span>;
143
- const separator = date === undefined || site === undefined ? undefined : '\u00a0\u2013\u00a0'; // \u00a0 == &nbsp; \u2013 == &ndash;
144
- return <div className="kokopu-headerGroup-dateSite" key="date-site">{dateElement}{separator}{siteElement}</div>;
145
- }
146
-
147
- /**
148
- * Header containing the annotator information.
149
- */
150
- private renderAnnotatorHeader(filter: (text: string) => React.ReactNode) {
151
- const annotator = this.props.game.annotator();
152
- if (annotator === undefined) {
153
- return undefined;
154
- }
155
- return <div className="kokopu-header-annotator" key="annotator">{filter(fillPlaceholder(i18n.ANNOTATED_BY, annotator))}</div>;
156
- }
157
-
158
- private renderFocusField() {
159
- if (this.props.interactionMode !== 'selectMove') {
160
- return undefined;
161
- }
162
- return (
163
- <div className="kokopu-focusFieldContainer">
164
- <a className="kokopu-focusField" href="#" ref={this.focusFieldRef} onKeyDown={evt => this.handleKeyDownInFocusField(evt)}></a>
165
- </div>
166
- );
167
- }
168
-
169
- private renderBody() {
170
- const filter = htmlFilter('comment');
171
- return this.renderVariation(filter, this.props.game.mainVariation(), true, this.props.game.result());
172
- }
173
-
174
- /**
175
- * Render the given variation and its sub-variations, recursively.
176
- */
177
- private renderVariation(filter: (text: string) => React.ReactNode, variation: Variation, isMainVariation: boolean, gameResult?: GameResult) {
178
-
179
- const moveGroups: React.ReactNode[] = []; // ... and also long comments and long sub-variations
180
- let currentMoveGroupElements: React.ReactNode[] = [];
181
-
182
- // Close the current move group, if any.
183
- function closeMoveGroup() {
184
- if (!variation.isLongVariation() || currentMoveGroupElements.length === 0) {
185
- return;
186
- }
187
- moveGroups.push(<div className="kokopu-moveGroup" key={'group-' + moveGroups.length}>{currentMoveGroupElements}</div>);
188
- currentMoveGroupElements = [];
189
- }
190
-
191
- // Write the initial comment, if any.
192
- const variationComment = this.extractComment(variation);
193
- if (variationComment !== undefined) {
194
- if (variation.isLongComment()) {
195
- moveGroups.push(this.renderComment(filter, variation, variationComment));
196
- }
197
- else {
198
- currentMoveGroupElements.push(this.renderComment(filter, variation, variationComment));
199
- }
200
- }
201
-
202
- // Visit all the PGN nodes (one node per move) within the variation.
203
- let forcePrintMoveNumber = true;
204
- let node = variation.first();
205
- while (node !== undefined) {
206
-
207
- // Write the move, including directly related information (i.e. move number + NAGs).
208
- currentMoveGroupElements.push(this.renderMove(node, forcePrintMoveNumber));
209
-
210
- // Write the comment (if any).
211
- const nodeComment = this.extractComment(node);
212
- if (nodeComment !== undefined) {
213
- if (node.isLongComment()) {
214
- closeMoveGroup();
215
- moveGroups.push(this.renderComment(filter, node, nodeComment));
216
- }
217
- else {
218
- currentMoveGroupElements.push(this.renderComment(filter, node, nodeComment));
219
- }
220
- }
221
-
222
- // Write the sub-variations.
223
- let hasNonEmptySubVariations = false;
224
- for (const subVariation of node.variations()) {
225
- const subVariationElement = this.renderVariation(filter, subVariation, false);
226
- if (subVariationElement) {
227
- if (subVariation.isLongVariation()) {
228
- closeMoveGroup();
229
- moveGroups.push(subVariationElement);
230
- }
231
- else {
232
- currentMoveGroupElements.push(subVariationElement);
233
- }
234
- hasNonEmptySubVariations = true;
235
- }
236
- }
237
-
238
- // Back to the current variation, go to the next move.
239
- forcePrintMoveNumber = (nodeComment !== undefined || hasNonEmptySubVariations);
240
- node = node.next();
241
- }
242
-
243
- // Append the game result at the end of the main variation.
244
- if (isMainVariation && gameResult !== '*') {
245
- currentMoveGroupElements.push(<span className="kokopu-gameResult" key="game-result">{formatResult(gameResult!)}</span>);
246
- }
247
-
248
- // Close the last move group, and return the result.
249
- closeMoveGroup();
250
- const elements = variation.isLongVariation() ? moveGroups : currentMoveGroupElements;
251
- if (elements.length === 0) {
252
- return undefined;
253
- }
254
- if (isMainVariation) {
255
- return <div className="kokopu-variation kokopu-mainVariation" key={variation.id()}>{elements}</div>;
256
- }
257
- else if (variation.isLongVariation()) {
258
- return <div className="kokopu-variation kokopu-subVariation" key={variation.id()}>{elements}</div>;
259
- }
260
- else {
261
- return <span className="kokopu-variation kokopu-subVariation" key={variation.id()}>{elements}</span>;
262
- }
263
- }
264
-
265
- /**
266
- * Render the given move, move number, and NAG (if any).
267
- */
268
- private renderMove(node: GameNode, forcePrintMoveNumber: boolean) {
269
-
270
- // Move number
271
- let moveNumber: React.ReactNode = undefined;
272
- if (forcePrintMoveNumber || node.moveColor() === 'w') {
273
- const moveNumberText = node.fullMoveNumber() + (node.moveColor() === 'w' ? '.' : '\u2026');
274
- moveNumber = <span className="kokopu-moveNumber">{moveNumberText}</span>;
275
- }
276
-
277
- // SAN notation.
278
- const notationText = this.props.moveFormatter(node.notation());
279
-
280
- // NAGs
281
- const nagElements = node.nags().map(nag => <span className="kokopu-nag" key={nag}>{nagSymbol(nag)}</span>);
282
-
283
- // Class
284
- const nodeId = node.id();
285
- const moveClassNames = [ 'kokopu-move' ];
286
- let onClick: (() => void) | undefined = undefined;
287
- if (this.props.selection && this.props.selection === nodeId) {
288
- moveClassNames.push('kokopu-selectedMove');
289
- }
290
- if (this.props.interactionMode === 'selectMove') {
291
- moveClassNames.push('kokopu-clickableMove');
292
- onClick = () => this.handleNodeClicked(nodeId);
293
- }
294
-
295
- return (
296
- <span className={moveClassNames.join(' ')} key={node.id()}>
297
- <span className="kokopu-moveContent" onClick={onClick}>
298
- {moveNumber}
299
- <span className="kokopu-moveNotation">{notationText}</span>
300
- {nagElements.length === 0 ? undefined : nagElements}
301
- </span>
302
- </span>
303
- );
304
- }
305
-
306
- /**
307
- * Render the given text comment with its diagrams, if any.
308
- */
309
- private renderComment(filter: (text: string) => React.ReactNode, node: GameNode | Variation, comment: string) {
310
- let content: React.ReactNode;
311
- if (this.props.diagramVisible && comment.includes('[#]')) {
312
- const segmentElements: React.ReactNode[] = [];
313
- let isFirstTextSegment = true;
314
- let diagramIndex = 0;
315
- for (let textSegment of comment.split('[#]')) {
316
- if (!isFirstTextSegment) {
317
- const position = node instanceof Variation ? node.initialPosition() : node.position();
318
- const diagram = <Chessboard position={position}
319
- squareMarkers={node.tag('csl')} arrowMarkers={node.tag('cal')} textMarkers={node.tag('ctl')}
320
- flipped={this.props.diagramOptions.flipped}
321
- squareSize={this.props.diagramOptions.squareSize}
322
- coordinateVisible={this.props.diagramOptions.coordinateVisible}
323
- turnVisible={this.props.diagramOptions.turnVisible}
324
- smallScreenLimits={this.props.diagramOptions.smallScreenLimits}
325
- colorset={this.props.diagramOptions.colorset}
326
- pieceset={this.props.diagramOptions.pieceset}
327
- />;
328
- segmentElements.push(<div className="kokopu-diagram" key={'diagram-' + (diagramIndex++)}>{diagram}</div>);
329
- }
330
- isFirstTextSegment = false;
331
- textSegment = textSegment.trim();
332
- if (textSegment.length !== 0) {
333
- segmentElements.push(filter(textSegment));
334
- }
335
- }
336
- content = segmentElements;
337
- }
338
- else {
339
- content = filter(comment);
340
- }
341
- const key = node.id() + '-comment';
342
- return node.isLongComment() ? <div className="kokopu-comment" key={key}>{content}</div> : <span className="kokopu-comment" key={key}>{content}</span>;
343
- }
344
-
345
- private extractComment(node: GameNode | Variation) {
346
- let comment = node.comment();
347
- if (comment) {
348
-
349
- // Remove the diagrams if necessary.
350
- if (!this.props.diagramVisible) {
351
- comment = comment.replace(/\[#\]/g, ' ');
352
- }
353
-
354
- // Trim and sanitize the space characters.
355
- comment = comment.replace(/\s+/g, ' ').trim();
356
- }
357
- return comment ? comment : undefined;
358
- }
359
-
360
- private handleKeyDownInFocusField(evt: React.KeyboardEvent<HTMLAnchorElement>) {
361
- if (evt.key !== 'Home' && evt.key !== 'ArrowLeft' && evt.key !== 'ArrowRight' && evt.key !== 'End' && evt.key !== 'Escape') {
362
- return;
363
- }
364
- evt.preventDefault();
365
- if (!this.props.selection) {
366
- return;
367
- }
368
- if (evt.key === 'Home') {
369
- this.fireMoveSelected(firstNodeIdImpl(this.props.game, this.props.selection), 'key-first');
370
- }
371
- else if (evt.key === 'ArrowLeft') {
372
- this.fireMoveSelected(previousNodeIdImpl(this.props.game, this.props.selection), 'key-previous');
373
- }
374
- else if (evt.key === 'ArrowRight') {
375
- this.fireMoveSelected(nextNodeIdImpl(this.props.game, this.props.selection), 'key-next');
376
- }
377
- else if (evt.key === 'End') {
378
- this.fireMoveSelected(lastNodeIdImpl(this.props.game, this.props.selection), 'key-last');
379
- }
380
- else { // evt.key === 'Escape'
381
- if (this.props.game.findById(this.props.selection)) {
382
- this.fireMoveSelected(undefined, 'key-exit');
383
- }
384
- }
385
- }
386
-
387
- private fireMoveSelected(nodeId: string | undefined, evtOrigin: 'key-first' | 'key-previous' | 'key-next' | 'key-last' | 'key-exit') {
388
- if ((nodeId || evtOrigin === 'key-exit') && this.props.onMoveSelected) {
389
- this.props.onMoveSelected(nodeId, evtOrigin);
390
- }
391
- }
392
-
393
- private handleNodeClicked(nodeId: string) {
394
- this.focus();
395
- if (this.props.onMoveSelected) {
396
- this.props.onMoveSelected(nodeId === this.props.selection ? undefined : nodeId, 'click');
397
- }
398
- }
399
-
400
- /**
401
- * Set the focus to the current component.
402
- */
403
- focus(): void {
404
- const target = this.focusFieldRef.current;
405
- if (target) {
406
- target.focus();
407
- }
408
- }
409
-
410
- }
411
-
65
+ private navigationFieldRef: React.RefObject<NavigationField> = React.createRef();
66
+
67
+ render() {
68
+ return (
69
+ <div className="kokopu-movetext">
70
+ {this.renderHeaders()}
71
+ {this.renderBody()}
72
+ {this.renderNavigationField()}
73
+ </div>
74
+ );
75
+ }
76
+
77
+ private renderHeaders() {
78
+ if (!this.props.headerVisible) {
79
+ return undefined;
80
+ }
81
+
82
+ const filter = htmlFilter('header');
83
+ const headers: React.ReactNode[] = [];
84
+ headers.push(this.renderPlayerHeaders(filter, 'w'));
85
+ headers.push(this.renderPlayerHeaders(filter, 'b'));
86
+ headers.push(this.renderEventRoundHeaders(filter));
87
+ headers.push(this.renderDateSiteHeaders(filter));
88
+ headers.push(this.renderAnnotatorHeader(filter));
89
+ return headers.some(element => element !== undefined) ? <div className="kokopu-headers">{headers}</div> : undefined;
90
+ }
91
+
92
+ /**
93
+ * Header containing the player-related information (name, rating, title) corresponding to the given color.
94
+ */
95
+ private renderPlayerHeaders(filter: (text: string) => React.ReactNode, color: Color) {
96
+ const playerName = this.props.game.playerName(color);
97
+ if (playerName === undefined) {
98
+ return undefined;
99
+ }
100
+ const title = this.props.game.playerTitle(color);
101
+ const rating = this.props.game.playerElo(color);
102
+
103
+ const classNames = [ 'kokopu-headerGroup-player', color === 'w' ? 'kokopu-headerGroup-whitePlayer' : 'kokopu-headerGroup-blackPlayer' ];
104
+ const colorTag = <span className="kokopu-colorTag"></span>;
105
+ const playerNameElement = <span className="kokopu-header-playerName">{filter(playerName)}</span>;
106
+ const titleElement = title === undefined ? undefined : <span className="kokopu-header-playerTitle">{filter(title)}</span>;
107
+ const ratingElement = rating === undefined ? undefined : <span className="kokopu-header-playerRating">{rating}</span>;
108
+ const separator = title === undefined || rating === undefined ? undefined : '\u00a0'; // \u00a0 == &nbsp;
109
+ const titleRatingGroup = title === undefined && rating === undefined ? undefined :
110
+ <span className="kokopu-headerGroup-titleRating">{titleElement}{separator}{ratingElement}</span>;
111
+ return <div className={classNames.join(' ')} key={'player-' + color}>{colorTag}{playerNameElement}{titleRatingGroup}</div>;
112
+ }
113
+
114
+ /**
115
+ * Header containing the event-related information: event + round.
116
+ */
117
+ private renderEventRoundHeaders(filter: (text: string) => React.ReactNode) {
118
+ const evt = this.props.game.event();
119
+ if (evt === undefined) {
120
+ return undefined;
121
+ }
122
+ const fullRound = this.props.game.fullRound();
123
+ const roundElement = fullRound === undefined ? undefined : <span className="kokopu-header-round">{fullRound}</span>;
124
+ const evtElement = <span className="kokopu-header-event">{filter(evt)}</span>;
125
+ return <div className="kokopu-headerGroup-eventRound" key="event-round">{evtElement}{roundElement}</div>;
126
+ }
127
+
128
+ /**
129
+ * Header containing the date/place information.
130
+ */
131
+ private renderDateSiteHeaders(filter: (text: string) => React.ReactNode) {
132
+ const date = this.props.game.dateAsString();
133
+ const site = this.props.game.site();
134
+ if (date === undefined && site === undefined) {
135
+ return undefined;
136
+ }
137
+ const dateElement = date === undefined ? undefined : <span className="kokopu-header-date">{capitalizeFirstWord(date)}</span>;
138
+ const siteElement = site === undefined ? undefined : <span className="kokopu-header-site">{filter(site)}</span>;
139
+ const separator = date === undefined || site === undefined ? undefined : '\u00a0\u2013\u00a0'; // \u00a0 == &nbsp; \u2013 == &ndash;
140
+ return <div className="kokopu-headerGroup-dateSite" key="date-site">{dateElement}{separator}{siteElement}</div>;
141
+ }
142
+
143
+ /**
144
+ * Header containing the annotator information.
145
+ */
146
+ private renderAnnotatorHeader(filter: (text: string) => React.ReactNode) {
147
+ const annotator = this.props.game.annotator();
148
+ if (annotator === undefined) {
149
+ return undefined;
150
+ }
151
+ return <div className="kokopu-header-annotator" key="annotator">{filter(fillPlaceholder(i18n.ANNOTATED_BY, annotator))}</div>;
152
+ }
153
+
154
+ private renderNavigationField() {
155
+ if (this.props.interactionMode !== 'selectMove') {
156
+ return undefined;
157
+ }
158
+ return <NavigationField ref={this.navigationFieldRef}
159
+ onFirstPressed={() => this.handleNavigationPressed(firstNodeId, 'key-first')}
160
+ onPreviousPressed={() => this.handleNavigationPressed(previousNodeId, 'key-previous')}
161
+ onNextPressed={() => this.handleNavigationPressed(nextNodeId, 'key-next')}
162
+ onLastPressed={() => this.handleNavigationPressed(lastNodeId, 'key-last')}
163
+ onExitPressed={() => this.handleExitPressed()}
164
+ />;
165
+ }
166
+
167
+ private renderBody() {
168
+ const filter = htmlFilter('comment');
169
+ return this.renderVariation(filter, this.props.game.mainVariation(), true, this.props.game.result());
170
+ }
171
+
172
+ /**
173
+ * Render the given variation and its sub-variations, recursively.
174
+ */
175
+ private renderVariation(filter: (text: string) => React.ReactNode, variation: Variation, isMainVariation: boolean, gameResult?: GameResult) {
176
+
177
+ const moveGroups: React.ReactNode[] = []; // ... and also long comments and long sub-variations
178
+ let currentMoveGroupElements: React.ReactNode[] = [];
179
+
180
+ // Close the current move group, if any.
181
+ function closeMoveGroup() {
182
+ if (!variation.isLongVariation() || currentMoveGroupElements.length === 0) {
183
+ return;
184
+ }
185
+ moveGroups.push(<div className="kokopu-moveGroup" key={'group-' + moveGroups.length}>{currentMoveGroupElements}</div>);
186
+ currentMoveGroupElements = [];
187
+ }
188
+
189
+ // Write the initial comment, if any.
190
+ const variationComment = this.extractComment(variation);
191
+ if (variationComment !== undefined) {
192
+ if (variation.isLongComment()) {
193
+ moveGroups.push(this.renderComment(filter, variation, variationComment));
194
+ }
195
+ else {
196
+ currentMoveGroupElements.push(this.renderComment(filter, variation, variationComment));
197
+ }
198
+ }
199
+
200
+ // Visit all the PGN nodes (one node per move) within the variation.
201
+ let forcePrintMoveNumber = true;
202
+ let node = variation.first();
203
+ while (node !== undefined) {
204
+
205
+ // Write the move, including directly related information (i.e. move number + NAGs).
206
+ currentMoveGroupElements.push(this.renderMove(node, forcePrintMoveNumber));
207
+
208
+ // Write the comment (if any).
209
+ const nodeComment = this.extractComment(node);
210
+ if (nodeComment !== undefined) {
211
+ if (node.isLongComment()) {
212
+ closeMoveGroup();
213
+ moveGroups.push(this.renderComment(filter, node, nodeComment));
214
+ }
215
+ else {
216
+ currentMoveGroupElements.push(this.renderComment(filter, node, nodeComment));
217
+ }
218
+ }
219
+
220
+ // Write the sub-variations.
221
+ let hasNonEmptySubVariations = false;
222
+ for (const subVariation of node.variations()) {
223
+ const subVariationElement = this.renderVariation(filter, subVariation, false);
224
+ if (subVariationElement) {
225
+ if (subVariation.isLongVariation()) {
226
+ closeMoveGroup();
227
+ moveGroups.push(subVariationElement);
228
+ }
229
+ else {
230
+ currentMoveGroupElements.push(subVariationElement);
231
+ }
232
+ hasNonEmptySubVariations = true;
233
+ }
234
+ }
235
+
236
+ // Back to the current variation, go to the next move.
237
+ forcePrintMoveNumber = (nodeComment !== undefined || hasNonEmptySubVariations);
238
+ node = node.next();
239
+ }
240
+
241
+ // Append the game result at the end of the main variation.
242
+ if (isMainVariation && gameResult !== '*') {
243
+ currentMoveGroupElements.push(<span className="kokopu-gameResult" key="game-result">{formatResult(gameResult!)}</span>);
244
+ }
245
+
246
+ // Close the last move group, and return the result.
247
+ closeMoveGroup();
248
+ const elements = variation.isLongVariation() ? moveGroups : currentMoveGroupElements;
249
+ if (elements.length === 0) {
250
+ return undefined;
251
+ }
252
+ if (isMainVariation) {
253
+ return <div className="kokopu-variation kokopu-mainVariation" key={variation.id()}>{elements}</div>;
254
+ }
255
+ else if (variation.isLongVariation()) {
256
+ return <div className="kokopu-variation kokopu-subVariation" key={variation.id()}>{elements}</div>;
257
+ }
258
+ else {
259
+ return <span className="kokopu-variation kokopu-subVariation" key={variation.id()}>{elements}</span>;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Render the given move, move number, and NAG (if any).
265
+ */
266
+ private renderMove(node: GameNode, forcePrintMoveNumber: boolean) {
267
+
268
+ // Move number
269
+ let moveNumber: React.ReactNode = undefined;
270
+ if (forcePrintMoveNumber || node.moveColor() === 'w') {
271
+ const moveNumberText = node.fullMoveNumber() + (node.moveColor() === 'w' ? '.' : '\u2026');
272
+ moveNumber = <span className="kokopu-moveNumber">{moveNumberText}</span>;
273
+ }
274
+
275
+ // SAN notation.
276
+ const notationText = this.props.moveFormatter(node.notation());
277
+
278
+ // NAGs
279
+ const nagElements = node.nags().map(nag => <span className="kokopu-nag" key={nag}>{nagSymbol(nag)}</span>);
280
+
281
+ // Class
282
+ const nodeId = node.id();
283
+ const moveClassNames = [ 'kokopu-move' ];
284
+ let onClick: (() => void) | undefined = undefined;
285
+ if (this.props.selection && this.props.selection === nodeId) {
286
+ moveClassNames.push('kokopu-selectedMove');
287
+ }
288
+ if (this.props.interactionMode === 'selectMove') {
289
+ moveClassNames.push('kokopu-clickableMove');
290
+ onClick = () => this.handleNodeClicked(nodeId);
291
+ }
292
+
293
+ return (
294
+ <span className={moveClassNames.join(' ')} key={node.id()}>
295
+ <span className="kokopu-moveContent" onClick={onClick}>
296
+ {moveNumber}
297
+ <span className="kokopu-moveNotation">{notationText}</span>
298
+ {nagElements.length === 0 ? undefined : nagElements}
299
+ </span>
300
+ </span>
301
+ );
302
+ }
303
+
304
+ /**
305
+ * Render the given text comment with its diagrams, if any.
306
+ */
307
+ private renderComment(filter: (text: string) => React.ReactNode, node: GameNode | Variation, comment: string) {
308
+ let content: React.ReactNode;
309
+ if (this.props.diagramVisible && comment.includes('[#]')) {
310
+ const segmentElements: React.ReactNode[] = [];
311
+ let isFirstTextSegment = true;
312
+ let diagramIndex = 0;
313
+ for (let textSegment of comment.split('[#]')) {
314
+ if (!isFirstTextSegment) {
315
+ const position = node instanceof Variation ? node.initialPosition() : node.position();
316
+ const diagram = <Chessboard
317
+ position={position}
318
+ squareMarkers={node.tag('csl')} arrowMarkers={node.tag('cal')} textMarkers={node.tag('ctl')}
319
+ flipped={this.props.diagramOptions.flipped}
320
+ squareSize={this.props.diagramOptions.squareSize}
321
+ coordinateVisible={this.props.diagramOptions.coordinateVisible}
322
+ turnVisible={this.props.diagramOptions.turnVisible}
323
+ colorset={this.props.diagramOptions.colorset}
324
+ pieceset={this.props.diagramOptions.pieceset}
325
+ smallScreenLimits={this.props.diagramOptions.smallScreenLimits}
326
+ />;
327
+ segmentElements.push(<div className="kokopu-diagram" key={'diagram-' + (diagramIndex++)}>{diagram}</div>);
328
+ }
329
+ isFirstTextSegment = false;
330
+ textSegment = textSegment.trim();
331
+ if (textSegment.length !== 0) {
332
+ segmentElements.push(filter(textSegment));
333
+ }
334
+ }
335
+ content = segmentElements;
336
+ }
337
+ else {
338
+ content = filter(comment);
339
+ }
340
+ const key = node.id() + '-comment';
341
+ return node.isLongComment() ? <div className="kokopu-comment" key={key}>{content}</div> : <span className="kokopu-comment" key={key}>{content}</span>;
342
+ }
343
+
344
+ private extractComment(node: GameNode | Variation) {
345
+ let comment = node.comment();
346
+ if (comment) {
347
+
348
+ // Remove the diagrams if necessary.
349
+ if (!this.props.diagramVisible) {
350
+ comment = comment.replace(/\[#\]/g, ' ');
351
+ }
352
+
353
+ // Trim and sanitize the space characters.
354
+ comment = comment.replace(/\s+/g, ' ').trim();
355
+ }
356
+ return comment ? comment : undefined;
357
+ }
358
+
359
+ private handleNavigationPressed(transition: (game: Game, nodeId: string) => string | undefined, evtOrigin: 'key-first' | 'key-previous' | 'key-next' | 'key-last') {
360
+ if (this.props.selection) {
361
+ this.fireMoveSelected(transition(this.props.game, this.props.selection), evtOrigin);
362
+ }
363
+ }
364
+
365
+ private handleExitPressed() {
366
+ if (this.props.selection && this.props.game.findById(this.props.selection)) {
367
+ this.fireMoveSelected(undefined, 'key-exit');
368
+ }
369
+ }
370
+
371
+ private fireMoveSelected(nodeId: string | undefined, evtOrigin: 'key-first' | 'key-previous' | 'key-next' | 'key-last' | 'key-exit') {
372
+ if ((nodeId || evtOrigin === 'key-exit') && this.props.onMoveSelected) {
373
+ this.props.onMoveSelected(nodeId, evtOrigin);
374
+ }
375
+ }
376
+
377
+ private handleNodeClicked(nodeId: string) {
378
+ this.focus();
379
+ if (this.props.onMoveSelected) {
380
+ this.props.onMoveSelected(nodeId === this.props.selection ? undefined : nodeId, 'click');
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Set the focus to the current component.
386
+ */
387
+ focus(): void {
388
+ const target = this.navigationFieldRef.current;
389
+ // istanbul ignore else
390
+ if (target) {
391
+ target.focus();
392
+ }
393
+ }
412
394
 
413
- /**
414
- * See {@link Movetext.firstNodeId}.
415
- */
416
- export function firstNodeIdImpl(game: Game, selection: string): string | undefined {
417
- if (!game.findById(selection)) {
418
- return undefined;
419
- }
420
- return selection === 'start' ? undefined : 'start';
421
- }
422
-
423
-
424
- /**
425
- * See {@link Movetext.previousNodeId}.
426
- */
427
- export function previousNodeIdImpl(game: Game, selection: string): string | undefined {
428
- let currentNode = game.findById(selection);
429
- if (!currentNode) {
430
- return undefined;
431
- }
432
- if (currentNode instanceof Variation) {
433
- currentNode = currentNode.parentNode();
434
- if (!currentNode) {
435
- return undefined; // This case corresponds to the first variation being selected initially.
436
- }
437
- }
438
- while (currentNode) {
439
- const previousNode = currentNode.previous();
440
- if (previousNode) {
441
- return previousNode.id();
442
- }
443
- currentNode = currentNode.parentVariation().parentNode();
444
- }
445
- return 'start';
446
- }
447
-
448
-
449
- /**
450
- * See {@link Movetext.nextNodeId}.
451
- */
452
- export function nextNodeIdImpl(game: Game, selection: string): string | undefined {
453
- const currentNode = game.findById(selection);
454
- if (!currentNode) {
455
- return undefined;
456
- }
457
- const nextNode = currentNode instanceof Variation ? currentNode.first() : currentNode.next();
458
- return nextNode ? nextNode.id() : undefined;
459
- }
460
-
461
-
462
- /**
463
- * See {@link Movetext.lastNodeId}.
464
- */
465
- export function lastNodeIdImpl(game: Game, selection: string): string | undefined {
466
- let currentNode = game.findById(selection);
467
- if (!currentNode) {
468
- return undefined;
469
- }
470
- currentNode = currentNode instanceof Variation ? currentNode.first() : currentNode.next();
471
- if (!currentNode) { // Ensure that the input node is not already the last one.
472
- return undefined;
473
- }
474
- let currentNodeNotNull = currentNode;
475
- while (true) {
476
- const nextNode = currentNodeNotNull.next();
477
- if (!nextNode) {
478
- return currentNodeNotNull.id();
479
- }
480
- currentNodeNotNull = nextNode;
481
- }
482
395
  }
483
396
 
484
397
 
@@ -486,14 +399,14 @@ export function lastNodeIdImpl(game: Game, selection: string): string | undefine
486
399
  * Example: `'hello world'` is turned into `'Hello world'`.
487
400
  */
488
401
  function capitalizeFirstWord(text: string) {
489
- return text.length === 0 ? '' : text.charAt(0).toUpperCase() + text.slice(1);
402
+ return text.length === 0 ? '' : text.charAt(0).toUpperCase() + text.slice(1);
490
403
  }
491
404
 
492
405
 
493
406
  function formatResult(result: GameResult) {
494
- switch (result) {
495
- case '1/2-1/2': return '\u00bd\u2013\u00bd';
496
- case '1-0': return '1\u20130';
497
- case '0-1': return '0\u20131';
498
- }
407
+ switch (result) {
408
+ case '1/2-1/2': return '\u00bd\u2013\u00bd';
409
+ case '1-0': return '1\u20130';
410
+ case '0-1': return '0\u20131';
411
+ }
499
412
  }