kokopu-react 3.1.0 → 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 (234) hide show
  1. package/.eslintrc.yml +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/dist/lib/chessboard/BoardProperties.js.map +1 -1
  4. package/dist/lib/chessboard/Chessboard.css +13 -13
  5. package/dist/lib/chessboard/Chessboard.js.map +1 -1
  6. package/dist/lib/chessboard/ChessboardImpl.js.map +1 -1
  7. package/dist/lib/chessboard/Draggable.js.map +1 -1
  8. package/dist/lib/chessboard/Motion.js.map +1 -1
  9. package/dist/lib/chessboard/colorsets.js.map +1 -1
  10. package/dist/lib/chessboard/piecesets.js.map +1 -1
  11. package/dist/lib/errorbox/ErrorBox.css +18 -18
  12. package/dist/lib/errorbox/ErrorBox.js.map +1 -1
  13. package/dist/lib/errorbox/parsing.js.map +1 -1
  14. package/dist/lib/exception.js.map +1 -1
  15. package/dist/lib/i18n.d.ts +1 -0
  16. package/dist/lib/i18n.js +1 -0
  17. package/dist/lib/i18n.js.map +1 -1
  18. package/dist/lib/icons/AnnotationSymbolShape.css +6 -6
  19. package/dist/lib/icons/AnnotationSymbolShape.js.map +1 -1
  20. package/dist/lib/icons/ArrowMarkerIcon.js.map +1 -1
  21. package/dist/lib/icons/ArrowTip.js.map +1 -1
  22. package/dist/lib/icons/ChessPieceIcon.js.map +1 -1
  23. package/dist/lib/icons/SquareMarkerIcon.js.map +1 -1
  24. package/dist/lib/icons/TextMarkerIcon.js.map +1 -1
  25. package/dist/lib/icons/arrow.css +1 -1
  26. package/dist/lib/index.d.ts +1 -0
  27. package/dist/lib/index.js +1 -0
  28. package/dist/lib/index.js.map +1 -1
  29. package/dist/lib/movetext/Movetext.css +27 -27
  30. package/dist/lib/movetext/Movetext.js.map +1 -1
  31. package/dist/lib/movetext/MovetextImpl.js.map +1 -1
  32. package/dist/lib/movetext/fonts.css +7 -7
  33. package/dist/lib/movetext/htmlFilter.js.map +1 -1
  34. package/dist/lib/movetext/moveFormatter.js.map +1 -1
  35. package/dist/lib/navigationboard/NavigationBoard.d.ts +39 -1
  36. package/dist/lib/navigationboard/NavigationBoard.js +94 -11
  37. package/dist/lib/navigationboard/NavigationBoard.js.map +1 -1
  38. package/dist/lib/navigationboard/NavigationButton.d.ts +54 -0
  39. package/dist/lib/navigationboard/NavigationButton.js +35 -0
  40. package/dist/lib/navigationboard/NavigationButton.js.map +1 -0
  41. package/dist/lib/navigationboard/NavigationField.css +7 -7
  42. package/dist/lib/navigationboard/NavigationField.js.map +1 -1
  43. package/dist/lib/navigationboard/NavigationToolbar.css +16 -13
  44. package/dist/lib/navigationboard/NavigationToolbar.d.ts +3 -10
  45. package/dist/lib/navigationboard/NavigationToolbar.js +31 -82
  46. package/dist/lib/navigationboard/NavigationToolbar.js.map +1 -1
  47. package/dist/lib/navigationboard/iconPaths.d.ts +29 -0
  48. package/dist/lib/navigationboard/iconPaths.js +91 -0
  49. package/dist/lib/navigationboard/iconPaths.js.map +1 -0
  50. package/dist/lib/sanitization.js.map +1 -1
  51. package/dist/lib/types.js.map +1 -1
  52. package/dist/lib/util.js.map +1 -1
  53. package/doc_src/demo/PageChessboardBase.tsx +115 -115
  54. package/doc_src/demo/PageChessboardInteraction.tsx +303 -303
  55. package/doc_src/demo/PageChessboardMove.tsx +173 -173
  56. package/doc_src/demo/PageChessboardSmallScreens.tsx +94 -94
  57. package/doc_src/demo/PageMovetextBase.tsx +180 -180
  58. package/doc_src/demo/PageMovetextInteraction.tsx +123 -123
  59. package/doc_src/demo/PageNavigationBoardBase.tsx +144 -137
  60. package/doc_src/demo/PageNavigationBoardUncontrolledVsControlled.tsx +69 -69
  61. package/doc_src/demo/decs.d.ts +4 -4
  62. package/doc_src/demo/demo.css +12 -12
  63. package/doc_src/demo/util.ts +1 -1
  64. package/doc_src/examples/NavigationBoard.md +1 -1
  65. package/doc_src/theming/LogoRenderer.js +13 -13
  66. package/doc_src/theming/theming.css +5 -5
  67. package/package.json +95 -95
  68. package/scripts/doc.styleguide.config.js +131 -131
  69. package/scripts/doc.tsconfig.json +5 -5
  70. package/scripts/docker-compose.yml +1 -1
  71. package/scripts/test-graphic.tsconfig.json +5 -5
  72. package/scripts/test-graphic.webpack.config.js +57 -57
  73. package/scripts/test-headless.webpack.config.js +29 -29
  74. package/src/chessboard/BoardProperties.ts +68 -68
  75. package/src/chessboard/Chessboard.css +13 -13
  76. package/src/chessboard/Chessboard.tsx +380 -380
  77. package/src/chessboard/ChessboardImpl.tsx +557 -557
  78. package/src/chessboard/Draggable.tsx +185 -185
  79. package/src/chessboard/Motion.tsx +67 -67
  80. package/src/chessboard/colorsets.ts +66 -66
  81. package/src/chessboard/piecesets.ts +84 -84
  82. package/src/decs.d.ts +2 -2
  83. package/src/errorbox/ErrorBox.css +18 -18
  84. package/src/errorbox/ErrorBox.tsx +75 -75
  85. package/src/errorbox/parsing.tsx +95 -95
  86. package/src/exception.ts +17 -17
  87. package/src/i18n.ts +1 -0
  88. package/src/icons/AnnotationSymbolShape.css +6 -6
  89. package/src/icons/AnnotationSymbolShape.tsx +31 -31
  90. package/src/icons/ArrowMarkerIcon.tsx +34 -34
  91. package/src/icons/ArrowTip.tsx +14 -14
  92. package/src/icons/ChessPieceIcon.tsx +45 -45
  93. package/src/icons/SquareMarkerIcon.tsx +20 -20
  94. package/src/icons/TextMarkerIcon.tsx +29 -29
  95. package/src/icons/arrow.css +1 -1
  96. package/src/index.ts +3 -2
  97. package/src/movetext/Movetext.css +27 -27
  98. package/src/movetext/Movetext.tsx +158 -158
  99. package/src/movetext/MovetextImpl.tsx +345 -345
  100. package/src/movetext/fonts.css +7 -7
  101. package/src/movetext/htmlFilter.tsx +129 -129
  102. package/src/movetext/moveFormatter.tsx +37 -37
  103. package/src/navigationboard/NavigationBoard.tsx +297 -178
  104. package/src/navigationboard/NavigationButton.ts +72 -0
  105. package/src/navigationboard/NavigationField.css +7 -7
  106. package/src/navigationboard/NavigationField.tsx +90 -90
  107. package/src/navigationboard/NavigationToolbar.css +16 -13
  108. package/src/navigationboard/NavigationToolbar.tsx +60 -117
  109. package/src/navigationboard/iconPaths.ts +98 -0
  110. package/src/sanitization.ts +17 -17
  111. package/src/types.ts +116 -116
  112. package/src/util.ts +11 -11
  113. package/test/01_types.js +167 -167
  114. package/test/02_chessboard_util.js +114 -114
  115. package/test/03_error_box_graphic.js +16 -16
  116. package/test/04_icons_graphic.js +32 -32
  117. package/test/05_chessboard_graphic.js +60 -60
  118. package/test/06_chessboard_animation.js +36 -36
  119. package/test/07_chessboard_interaction.js +101 -101
  120. package/test/08_chessboard_play_moves.js +62 -62
  121. package/test/09_chessboard_small_screens.js +32 -32
  122. package/test/10_movetext_graphic.js +32 -32
  123. package/test/11_movetext_interaction.js +100 -100
  124. package/test/12_navigation_board_base.js +151 -0
  125. package/test/13_navigation_board_graphic.js +68 -0
  126. package/test/14_navigation_board_autoplay.js +100 -0
  127. package/test/common/graphic.js +106 -97
  128. package/test/graphic_references/12_navigation_board_base/controlled/default.png +0 -0
  129. package/test/graphic_references/{12_navigation_board/controlled/forbidden_navigation_1.png → 12_navigation_board_base/controlled/forbidden_navigation_0.png} +0 -0
  130. package/test/graphic_references/{12_navigation_board/controlled/invalid_node_id.png → 12_navigation_board_base/controlled/forbidden_navigation_1.png} +0 -0
  131. package/test/graphic_references/{12_navigation_board/controlled/forbidden_navigation_0.png → 12_navigation_board_base/controlled/invalid_node_id.png} +0 -0
  132. package/test/graphic_references/12_navigation_board_base/controlled/no_callback_defined_0.png +0 -0
  133. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/controlled/no_callback_defined_1.png +0 -0
  134. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/controlled/overriding_initial_node_id.png +0 -0
  135. package/test/graphic_references/{12_navigation_board/controlled/regular_navigation_1.png → 12_navigation_board_base/controlled/regular_navigation_0.png} +0 -0
  136. package/test/graphic_references/{12_navigation_board/uncontrolled/initial_state_at_specific_id.png → 12_navigation_board_base/controlled/regular_navigation_1.png} +0 -0
  137. package/test/graphic_references/{12_navigation_board/controlled/regular_navigation_3.png → 12_navigation_board_base/controlled/regular_navigation_2.png} +0 -0
  138. package/test/graphic_references/{12_navigation_board/controlled/regular_navigation_0.png → 12_navigation_board_base/controlled/regular_navigation_3.png} +0 -0
  139. package/test/graphic_references/{12_navigation_board/uncontrolled/forbidden_navigation_at_beginning_2.png → 12_navigation_board_base/uncontrolled/forbidden_navigation_at_beginning_0.png} +0 -0
  140. package/test/graphic_references/{12_navigation_board/uncontrolled/forbidden_navigation_at_beginning_0.png → 12_navigation_board_base/uncontrolled/forbidden_navigation_at_beginning_1.png} +0 -0
  141. package/test/graphic_references/{12_navigation_board/uncontrolled/forbidden_navigation_at_beginning_1.png → 12_navigation_board_base/uncontrolled/forbidden_navigation_at_beginning_2.png} +0 -0
  142. package/test/graphic_references/{12_navigation_board/controlled/no_callback_defined_0.png → 12_navigation_board_base/uncontrolled/forbidden_navigation_at_end_0.png} +0 -0
  143. package/test/graphic_references/12_navigation_board_base/uncontrolled/forbidden_navigation_at_end_1.png +0 -0
  144. package/test/graphic_references/12_navigation_board_base/uncontrolled/forbidden_navigation_at_end_2.png +0 -0
  145. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/uncontrolled/initial_state_at_end.png +0 -0
  146. package/test/graphic_references/12_navigation_board_base/uncontrolled/initial_state_at_specific_id.png +0 -0
  147. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/uncontrolled/initial_state_default.png +0 -0
  148. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/uncontrolled/initial_state_default_with_game_index.png +0 -0
  149. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/uncontrolled/initial_state_on_null-move.png +0 -0
  150. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/uncontrolled/navigation_with_keys_0.png +0 -0
  151. package/test/graphic_references/12_navigation_board_base/uncontrolled/navigation_with_keys_1.png +0 -0
  152. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/uncontrolled/navigation_with_keys_2.png +0 -0
  153. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/uncontrolled/navigation_with_keys_3.png +0 -0
  154. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/uncontrolled/navigation_with_keys_4.png +0 -0
  155. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/uncontrolled/regular_flipping_0.png +0 -0
  156. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/uncontrolled/regular_flipping_1.png +0 -0
  157. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/uncontrolled/regular_navigation_0.png +0 -0
  158. package/test/graphic_references/{12_navigation_board/controlled/default.png → 12_navigation_board_base/uncontrolled/regular_navigation_1.png} +0 -0
  159. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/uncontrolled/regular_navigation_2.png +0 -0
  160. package/test/graphic_references/12_navigation_board_base/uncontrolled/regular_navigation_3.png +0 -0
  161. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/uncontrolled/regular_navigation_4.png +0 -0
  162. package/test/graphic_references/{12_navigation_board → 12_navigation_board_base}/uncontrolled/regular_navigation_5.png +0 -0
  163. package/test/graphic_references/13_navigation_board_graphic/additional_buttons/error_1.png +0 -0
  164. package/test/graphic_references/13_navigation_board_graphic/additional_buttons/error_2.png +0 -0
  165. package/test/graphic_references/13_navigation_board_graphic/additional_buttons/multiple_buttons.png +0 -0
  166. package/test/graphic_references/{12_navigation_board/uncontrolled/regular_navigation_3.png → 13_navigation_board_graphic/additional_buttons/single_button.png} +0 -0
  167. package/test/graphic_references/{12_navigation_board → 13_navigation_board_graphic}/theme/custom_1.png +0 -0
  168. package/test/graphic_references/{12_navigation_board → 13_navigation_board_graphic}/theme/custom_2.png +0 -0
  169. package/test/graphic_references/{12_navigation_board → 13_navigation_board_graphic}/theme/large.png +0 -0
  170. package/test/graphic_references/13_navigation_board_graphic/theme/small.png +0 -0
  171. package/test/graphic_references/14_navigation_board_autoplay/controlled/after_1_move_acknowledging_is-playing_changes.png +0 -0
  172. package/test/graphic_references/14_navigation_board_autoplay/controlled/after_1_move_not_acknowledging_is-playing_changes.png +0 -0
  173. package/test/graphic_references/14_navigation_board_autoplay/controlled/after_2_moves_acknowledging_node_changes.png +0 -0
  174. package/test/graphic_references/14_navigation_board_autoplay/controlled/after_2_moves_not_acknowledging_node_changes.png +0 -0
  175. package/test/graphic_references/{12_navigation_board/uncontrolled/forbidden_navigation_at_end_1.png → 14_navigation_board_autoplay/controlled/after_reaching_the_end.png} +0 -0
  176. package/test/graphic_references/{12_navigation_board/uncontrolled/forbidden_navigation_at_end_2.png → 14_navigation_board_autoplay/controlled/already_at_the_end.png} +0 -0
  177. package/test/graphic_references/14_navigation_board_autoplay/controlled/initial_state_acknowledging_is-playing_changes.png +0 -0
  178. package/test/graphic_references/14_navigation_board_autoplay/controlled/initial_state_acknowledging_node_changes.png +0 -0
  179. package/test/graphic_references/{12_navigation_board/uncontrolled/forbidden_navigation_at_end_0.png → 14_navigation_board_autoplay/controlled/initial_state_at_the_end.png} +0 -0
  180. package/test/graphic_references/14_navigation_board_autoplay/controlled/initial_state_close_to_end.png +0 -0
  181. package/test/graphic_references/14_navigation_board_autoplay/controlled/initial_state_not_acknowledging_is-playing_changes.png +0 -0
  182. package/test/graphic_references/14_navigation_board_autoplay/controlled/initial_state_not_acknowledging_node_changes.png +0 -0
  183. package/test/graphic_references/14_navigation_board_autoplay/uncontrolled/after_2_moves_from_beginning_after_stop.png +0 -0
  184. package/test/graphic_references/14_navigation_board_autoplay/uncontrolled/after_2_moves_from_beginning_before_stop.png +0 -0
  185. package/test/graphic_references/14_navigation_board_autoplay/uncontrolled/after_reaching_the_end_after_stop.png +0 -0
  186. package/test/graphic_references/14_navigation_board_autoplay/uncontrolled/after_reaching_the_end_before_stop.png +0 -0
  187. package/test/graphic_references/14_navigation_board_autoplay/uncontrolled/initial_state_close_to_end.png +0 -0
  188. package/test/graphic_references/{12_navigation_board/uncontrolled/navigation_with_keys_1.png → 14_navigation_board_autoplay/uncontrolled/initial_state_default.png} +0 -0
  189. package/test/graphic_test_app/03_error_box_graphic/base.tsx +14 -14
  190. package/test/graphic_test_app/03_error_box_graphic/localization.tsx +1 -1
  191. package/test/graphic_test_app/04_icons_graphic/chess_piece.tsx +7 -7
  192. package/test/graphic_test_app/04_icons_graphic/marker.tsx +15 -15
  193. package/test/graphic_test_app/05_chessboard_graphic/annotations.tsx +7 -7
  194. package/test/graphic_test_app/05_chessboard_graphic/base.tsx +6 -6
  195. package/test/graphic_test_app/05_chessboard_graphic/chess_variants.tsx +7 -7
  196. package/test/graphic_test_app/05_chessboard_graphic/flipped.tsx +5 -5
  197. package/test/graphic_test_app/05_chessboard_graphic/move.tsx +12 -12
  198. package/test/graphic_test_app/05_chessboard_graphic/theme.tsx +12 -12
  199. package/test/graphic_test_app/06_chessboard_animation/base_1.tsx +6 -6
  200. package/test/graphic_test_app/06_chessboard_animation/base_2.tsx +6 -6
  201. package/test/graphic_test_app/06_chessboard_animation/base_3.tsx +6 -6
  202. package/test/graphic_test_app/06_chessboard_animation/on_redraw.tsx +16 -16
  203. package/test/graphic_test_app/07_chessboard_interaction/click_squares.tsx +5 -5
  204. package/test/graphic_test_app/07_chessboard_interaction/edit_arrows.tsx +4 -4
  205. package/test/graphic_test_app/07_chessboard_interaction/error.tsx +2 -2
  206. package/test/graphic_test_app/07_chessboard_interaction/move_pieces.tsx +5 -5
  207. package/test/graphic_test_app/08_chessboard_play_moves/base.tsx +8 -8
  208. package/test/graphic_test_app/08_chessboard_play_moves/promotions.tsx +4 -4
  209. package/test/graphic_test_app/09_chessboard_small_screens/base.tsx +9 -9
  210. package/test/graphic_test_app/09_chessboard_small_screens/top_bottom_components.tsx +9 -9
  211. package/test/graphic_test_app/10_movetext_graphic/base.tsx +4 -4
  212. package/test/graphic_test_app/10_movetext_graphic/error.tsx +6 -6
  213. package/test/graphic_test_app/10_movetext_graphic/format_move.tsx +1 -1
  214. package/test/graphic_test_app/10_movetext_graphic/html.tsx +5 -5
  215. package/test/graphic_test_app/10_movetext_graphic/options.tsx +10 -10
  216. package/test/graphic_test_app/11_movetext_interaction/error.tsx +1 -1
  217. package/test/graphic_test_app/11_movetext_interaction/select_moves.tsx +11 -11
  218. package/test/graphic_test_app/{12_navigation_board → 12_navigation_board_base}/controlled.tsx +5 -5
  219. package/test/graphic_test_app/{12_navigation_board → 12_navigation_board_base}/uncontrolled.tsx +7 -7
  220. package/test/graphic_test_app/13_navigation_board_graphic/additional_buttons.tsx +49 -0
  221. package/test/graphic_test_app/{12_navigation_board → 13_navigation_board_graphic}/error.tsx +2 -2
  222. package/test/graphic_test_app/{12_navigation_board → 13_navigation_board_graphic}/theme.tsx +4 -4
  223. package/test/graphic_test_app/14_navigation_board_autoplay/controlled.tsx +73 -0
  224. package/test/graphic_test_app/14_navigation_board_autoplay/uncontrolled.tsx +34 -0
  225. package/test/graphic_test_app/common/decs.d.ts +2 -2
  226. package/test/graphic_test_app/common/test_app.css +15 -15
  227. package/test/graphic_test_app/common/test_app.tsx +48 -48
  228. package/tsconfig.json +13 -13
  229. package/test/12_navigation_board.js +0 -168
  230. package/test/graphic_references/12_navigation_board/controlled/regular_navigation_2.png +0 -0
  231. package/test/graphic_references/12_navigation_board/theme/small.png +0 -0
  232. package/test/graphic_references/12_navigation_board/uncontrolled/regular_navigation_1.png +0 -0
  233. /package/test/graphic_references/{12_navigation_board → 13_navigation_board_graphic}/error/pgn_parsing_error.png +0 -0
  234. /package/test/graphic_references/{12_navigation_board → 13_navigation_board_graphic}/error/wrong_game_type.png +0 -0
@@ -26,82 +26,119 @@ import * as React from 'react';
26
26
 
27
27
  import { Database, Game, Node as GameNode, Variation } from 'kokopu';
28
28
 
29
+ import { IllegalArgument } from '../exception';
30
+ import { i18n } from '../i18n';
29
31
  import { sanitizeBoolean, sanitizeOptional, sanitizeString } from '../sanitization';
30
32
 
31
33
  import { DynamicBoardGraphicProps, defaultDynamicBoardProps } from '../chessboard/BoardProperties';
32
34
  import { Chessboard } from '../chessboard/Chessboard';
33
35
  import { parseGame } from '../errorbox/parsing';
34
36
  import { NavigationField, firstNodeId, previousNodeId, nextNodeId, lastNodeId } from '../navigationboard/NavigationField';
37
+ import { GO_FIRST_ICON_PATH, GO_PREVIOUS_ICON_PATH, GO_NEXT_ICON_PATH, GO_LAST_ICON_PATH, PLAY_ICON_PATH, STOP_ICON_PATH, FLIP_ICON_PATH } from './iconPaths';
38
+ import { NavigationButton, NavigationButtonList, isNavigationButton } from './NavigationButton';
35
39
  import { NavigationToolbar } from './NavigationToolbar';
36
40
 
37
41
 
42
+ const INTER_MOVE_DURATION = 1000;
43
+
44
+
38
45
  export interface NavigationBoardProps extends DynamicBoardGraphicProps {
39
46
 
40
- /**
41
- * Displayed game. Can be a [kokopu.Game](https://kokopu.yo35.org/docs/current/classes/Game.html) object,
42
- * a [kokopu.Database](https://kokopu.yo35.org/docs/current/classes/Database.html) object,
43
- * or a [PGN string](https://en.wikipedia.org/wiki/Portable_Game_Notation).
44
- */
45
- game: Game | Database | string;
46
-
47
- /**
48
- * Index of the game to display (only if attribute `game` is a [kokopu.Database](https://kokopu.yo35.org/docs/current/classes/Database.html)
49
- * or a [PGN string](https://en.wikipedia.org/wiki/Portable_Game_Notation)): `0` for the first game of the database/PGN, `1` for the second one, etc.
50
- * If omitted, the first game of the database/PGN is displayed.
51
- */
52
- gameIndex: number;
53
-
54
- /**
55
- * ID of the move initially selected (or `'start'`/`'end'` for the beginning/end of the main variation) when the component is uncontrolled.
56
- * Use [kokopu.Node#id](https://kokopu.yo35.org/docs/current/classes/Node.html#id) to get the ID of a game move.
57
- * Ignored if the `nodeId` attribute is provided.
58
- */
59
- initialNodeId: string;
60
-
61
- /**
62
- * ID of the selected move (or `'start'`/`'end'` for the beginning/end of the main variation).
63
- * Use [kokopu.Node#id](https://kokopu.yo35.org/docs/current/classes/Node.html#id) to get the ID of a game move.
64
- * If provided (i.e. if the component is controlled), the attribute `onNodeIdChanged` must be provided as well.
65
- */
66
- nodeId?: string;
67
-
68
- /**
69
- * Callback invoked in controlled-component mode, when the user changes the selected move.
70
- *
71
- * @param nodeId - ID of the selected move (as returned by [kokopu.Node#id](https://kokopu.yo35.org/docs/current/classes/Node.html#id)),
72
- * or `'start'` for the beginning of the main variation.
73
- */
74
- onNodeIdChanged?: (nodeId: string) => void;
75
-
76
- /**
77
- * Whether the board is initially flipped (i.e. seen from Black's point of view) or not, when the flip state is uncontrolled.
78
- * Ignored if the `flipped` attribute is provided.
79
- */
80
- initialFlipped: boolean;
81
-
82
- /**
83
- * Whether the board is flipped (i.e. seen from Black's point of view) or not.
84
- * If provided (i.e. if the flip state is controlled), the attribute `onFlippedChanged` must be provided as well.
85
- */
86
- flipped?: boolean;
87
-
88
- /**
89
- * Callback invoked in controlled-flip-state mode, when the user flips the board.
90
- *
91
- * @param flipped - New flip state.
92
- */
93
- onFlippedChanged?: (flipped: boolean) => void;
94
-
95
- /**
96
- * Whether the flip button is visible or not in the toolbar.
97
- */
98
- flipButtonVisible: boolean;
47
+ /**
48
+ * Displayed game. Can be a [kokopu.Game](https://kokopu.yo35.org/docs/current/classes/Game.html) object,
49
+ * a [kokopu.Database](https://kokopu.yo35.org/docs/current/classes/Database.html) object,
50
+ * or a [PGN string](https://en.wikipedia.org/wiki/Portable_Game_Notation).
51
+ */
52
+ game: Game | Database | string;
53
+
54
+ /**
55
+ * Index of the game to display (only if attribute `game` is a [kokopu.Database](https://kokopu.yo35.org/docs/current/classes/Database.html)
56
+ * or a [PGN string](https://en.wikipedia.org/wiki/Portable_Game_Notation)): `0` for the first game of the database/PGN, `1` for the second one, etc.
57
+ * If omitted, the first game of the database/PGN is displayed.
58
+ */
59
+ gameIndex: number;
60
+
61
+ /**
62
+ * ID of the move initially selected (or `'start'`/`'end'` for the beginning/end of the main variation) when the component is uncontrolled.
63
+ * Use [kokopu.Node#id](https://kokopu.yo35.org/docs/current/classes/Node.html#id) to get the ID of a game move.
64
+ * Ignored if the `nodeId` attribute is provided.
65
+ */
66
+ initialNodeId: string;
67
+
68
+ /**
69
+ * ID of the selected move (or `'start'`/`'end'` for the beginning/end of the main variation).
70
+ * Use [kokopu.Node#id](https://kokopu.yo35.org/docs/current/classes/Node.html#id) to get the ID of a game move.
71
+ * If provided (i.e. if the component is controlled), the attribute `onNodeIdChanged` must be provided as well.
72
+ */
73
+ nodeId?: string;
74
+
75
+ /**
76
+ * Callback invoked in controlled-component mode, when the user changes the selected move.
77
+ *
78
+ * @param nodeId - ID of the selected move (as returned by [kokopu.Node#id](https://kokopu.yo35.org/docs/current/classes/Node.html#id)),
79
+ * or `'start'` for the beginning of the main variation.
80
+ */
81
+ onNodeIdChanged?: (nodeId: string) => void;
82
+
83
+ /**
84
+ * Whether auto-play is initially enabled or not.
85
+ * Ignored if the `isPlaying` attribute is provided.
86
+ */
87
+ initialIsPlaying: boolean;
88
+
89
+ /**
90
+ * Whether auto-play is enabled or not.
91
+ * If provided (i.e. if the is-playing state is controlled), the attribute `onIsPlayingChanged` must be provided as well.
92
+ */
93
+ isPlaying?: boolean;
94
+
95
+ /**
96
+ * Callback invoked in controlled-is-playing-state mode, when the user clicks on the play/stop button.
97
+ *
98
+ * @param isPlaying - New is-playing state.
99
+ */
100
+ onIsPlayingChanged?: (isPlaying: boolean) => void;
101
+
102
+ /**
103
+ * Whether the board is initially flipped (i.e. seen from Black's point of view) or not, when the flip state is uncontrolled.
104
+ * Ignored if the `flipped` attribute is provided.
105
+ */
106
+ initialFlipped: boolean;
107
+
108
+ /**
109
+ * Whether the board is flipped (i.e. seen from Black's point of view) or not.
110
+ * If provided (i.e. if the flip state is controlled), the attribute `onFlippedChanged` must be provided as well.
111
+ */
112
+ flipped?: boolean;
113
+
114
+ /**
115
+ * Callback invoked in controlled-flip-state mode, when the user flips the board.
116
+ *
117
+ * @param flipped - New flip state.
118
+ */
119
+ onFlippedChanged?: (flipped: boolean) => void;
120
+
121
+ /**
122
+ * Whether the play/stop button is visible or not in the toolbar.
123
+ */
124
+ playButtonVisible: boolean;
125
+
126
+ /**
127
+ * Whether the flip button is visible or not in the toolbar.
128
+ */
129
+ flipButtonVisible: boolean;
130
+
131
+ /**
132
+ * Additional buttons to be added to the toolbar.
133
+ */
134
+ additionalButtons: NavigationButton | NavigationButtonList;
99
135
  }
100
136
 
101
137
 
102
138
  interface NavigationBoardState {
103
- nodeIdAsUncontrolled: string;
104
- flippedAsUncontrolled: boolean;
139
+ nodeIdAsUncontrolled: string;
140
+ isPlayingAsUncontrolled: boolean;
141
+ flippedAsUncontrolled: boolean;
105
142
  }
106
143
 
107
144
 
@@ -110,122 +147,204 @@ interface NavigationBoardState {
110
147
  */
111
148
  export class NavigationBoard extends React.Component<NavigationBoardProps, NavigationBoardState> {
112
149
 
113
- static defaultProps = {
114
- ...defaultDynamicBoardProps(),
115
- game: new Game(),
116
- gameIndex: 0,
117
- initialNodeId: 'start',
118
- initialFlipped: false,
119
- flipButtonVisible: true,
120
- };
121
-
122
- private navigationFieldRef: React.RefObject<NavigationField> = React.createRef();
123
-
124
- constructor(props: NavigationBoardProps) {
125
- super(props);
126
- this.state = {
127
- nodeIdAsUncontrolled: sanitizeString(props.initialNodeId),
128
- flippedAsUncontrolled: sanitizeBoolean(props.initialFlipped),
129
- };
130
- }
131
-
132
- render() {
133
-
134
- // Validate the game and game-index attributes.
135
- const info = parseGame(this.props.game, this.props.gameIndex, 'NavigationBoard');
136
- if (info.error) {
137
- return info.errorBox;
138
- }
139
-
140
- // Fetch the current node/variation.
141
- const currentNodeId = sanitizeOptional(this.props.nodeId, sanitizeString) ?? this.state.nodeIdAsUncontrolled;
142
- const currentNode = info.game.findById(currentNodeId) ?? info.game.mainVariation();
143
-
144
- // Flip state.
145
- const flipped = sanitizeOptional(this.props.flipped, sanitizeBoolean) ?? this.state.flippedAsUncontrolled;
146
-
147
- return (
148
- <div className="kokopu-navigationBoard">
149
- {this.renderBoard(info.game, currentNode, flipped)}
150
- {this.renderNavigationField(info.game, currentNode.id())}
151
- </div>
152
- );
153
- }
154
-
155
- private renderBoard(game: Game, node: GameNode | Variation, flipped: boolean) {
156
- const position = node instanceof GameNode ? node.positionBefore() : node.initialPosition();
157
- const move = node instanceof GameNode ? node.notation() : undefined;
158
- return <Chessboard
159
- position={position} move={move} flipped={flipped}
160
- squareMarkers={node.tag('csl')} arrowMarkers={node.tag('cal')} textMarkers={node.tag('ctl')}
161
- squareSize={this.props.squareSize}
162
- coordinateVisible={this.props.coordinateVisible}
163
- turnVisible={this.props.turnVisible}
164
- colorset={this.props.colorset}
165
- pieceset={this.props.pieceset}
166
- smallScreenLimits={this.props.smallScreenLimits}
167
- moveArrowVisible={this.props.moveArrowVisible}
168
- moveArrowColor={this.props.moveArrowColor}
169
- animated={this.props.animated}
170
- bottomComponent={({ squareSize }) => this.renderToolbar(game, node.id(), squareSize)}
171
- />;
172
- }
173
-
174
- private renderNavigationField(game: Game, currentNodeId: string) {
175
- return <NavigationField ref={this.navigationFieldRef}
176
- onFirstPressed={() => this.handleNavigationButtonClicked(firstNodeId(game, currentNodeId))}
177
- onPreviousPressed={() => this.handleNavigationButtonClicked(previousNodeId(game, currentNodeId))}
178
- onNextPressed={() => this.handleNavigationButtonClicked(nextNodeId(game, currentNodeId))}
179
- onLastPressed={() => this.handleNavigationButtonClicked(lastNodeId(game, currentNodeId))}
180
- />;
181
- }
182
-
183
- private renderToolbar(game: Game, currentNodeId: string, squareSize: number) {
184
- const flipButtonVisible = sanitizeBoolean(this.props.flipButtonVisible);
185
- return <NavigationToolbar flipButtonVisible={flipButtonVisible} squareSize={squareSize}
186
- onFirstClicked={() => this.handleNavigationButtonClicked(firstNodeId(game, currentNodeId))}
187
- onPreviousClicked={() => this.handleNavigationButtonClicked(previousNodeId(game, currentNodeId))}
188
- onNextClicked={() => this.handleNavigationButtonClicked(nextNodeId(game, currentNodeId))}
189
- onLastClicked={() => this.handleNavigationButtonClicked(lastNodeId(game, currentNodeId))}
190
- onFlipClicked={() => this.handleFlipButtonClicked()}
191
- />;
192
- }
193
-
194
- private handleNavigationButtonClicked(targetNodeId: string | undefined) {
195
- this.focus();
196
- if (targetNodeId === undefined) {
197
- return;
198
- }
199
-
200
- if (this.props.nodeId === undefined) { // uncontrolled-component behavior
201
- this.setState({ nodeIdAsUncontrolled: targetNodeId });
202
- }
203
- else if (this.props.onNodeIdChanged) { // controlled-component behavior
204
- this.props.onNodeIdChanged(targetNodeId);
205
- }
206
- }
207
-
208
- private handleFlipButtonClicked() {
209
- this.focus();
210
- if (this.props.flipped === undefined) { // uncontrolled flip state
211
- this.setState({ flippedAsUncontrolled: !this.state.flippedAsUncontrolled });
212
- }
213
- else if (this.props.onFlippedChanged) { // controlled flip state
214
- this.props.onFlippedChanged(!this.props.flipped);
215
- }
216
- }
217
-
218
- /**
219
- * Set the focus to the current component.
220
- *
221
- * @public
222
- */
223
- focus(): void {
224
- const target = this.navigationFieldRef.current;
225
- // istanbul ignore else
226
- if (target) {
227
- target.focus();
228
- }
229
- }
150
+ static defaultProps = {
151
+ ...defaultDynamicBoardProps(),
152
+ game: new Game(),
153
+ gameIndex: 0,
154
+ initialNodeId: 'start',
155
+ initialIsPlaying: false,
156
+ initialFlipped: false,
157
+ playButtonVisible: false,
158
+ flipButtonVisible: true,
159
+ additionalButtons: [],
160
+ };
161
+
162
+ private navigationFieldRef: React.RefObject<NavigationField> = React.createRef();
163
+ private timeoutId?: number;
164
+
165
+ constructor(props: NavigationBoardProps) {
166
+ super(props);
167
+ this.state = {
168
+ nodeIdAsUncontrolled: sanitizeString(props.initialNodeId),
169
+ isPlayingAsUncontrolled: sanitizeBoolean(props.initialIsPlaying),
170
+ flippedAsUncontrolled: sanitizeBoolean(props.initialFlipped),
171
+ };
172
+ }
173
+
174
+ componentWillUnmount() {
175
+ this.cancelCurrentTimeout();
176
+ }
177
+
178
+ render() {
179
+ this.cancelCurrentTimeout();
180
+
181
+ // Validate the game and game-index attributes.
182
+ const info = parseGame(this.props.game, this.props.gameIndex, 'NavigationBoard');
183
+ if (info.error) {
184
+ return info.errorBox;
185
+ }
186
+
187
+ // Fetch the current node/variation.
188
+ const currentNodeId = sanitizeOptional(this.props.nodeId, sanitizeString) ?? this.state.nodeIdAsUncontrolled;
189
+ const currentNode = info.game.findById(currentNodeId) ?? info.game.mainVariation();
190
+
191
+ // State flags.
192
+ const isPlaying = sanitizeOptional(this.props.isPlaying, sanitizeBoolean) ?? this.state.isPlayingAsUncontrolled;
193
+ const flipped = sanitizeOptional(this.props.flipped, sanitizeBoolean) ?? this.state.flippedAsUncontrolled;
194
+
195
+ return (
196
+ <div className="kokopu-navigationBoard">
197
+ {this.renderBoard(info.game, currentNode, isPlaying, flipped)}
198
+ {this.renderNavigationField(info.game, currentNode.id())}
199
+ </div>
200
+ );
201
+ }
202
+
203
+ private renderBoard(game: Game, node: GameNode | Variation, isPlaying: boolean, flipped: boolean) {
204
+ const position = node instanceof GameNode ? node.positionBefore() : node.initialPosition();
205
+ const move = node instanceof GameNode ? node.notation() : undefined;
206
+ return <Chessboard
207
+ position={position} move={move} flipped={flipped}
208
+ squareMarkers={node.tag('csl')} arrowMarkers={node.tag('cal')} textMarkers={node.tag('ctl')}
209
+ squareSize={this.props.squareSize}
210
+ coordinateVisible={this.props.coordinateVisible}
211
+ turnVisible={this.props.turnVisible}
212
+ colorset={this.props.colorset}
213
+ pieceset={this.props.pieceset}
214
+ smallScreenLimits={this.props.smallScreenLimits}
215
+ moveArrowVisible={this.props.moveArrowVisible}
216
+ moveArrowColor={this.props.moveArrowColor}
217
+ animated={this.props.animated}
218
+ bottomComponent={({ squareSize }) => this.renderToolbar(game, node, squareSize, isPlaying)}
219
+ />;
220
+ }
221
+
222
+ private renderNavigationField(game: Game, currentNodeId: string) {
223
+ return <NavigationField ref={this.navigationFieldRef}
224
+ onFirstPressed={() => this.handleNavClicked(firstNodeId(game, currentNodeId))}
225
+ onPreviousPressed={() => this.handleNavClicked(previousNodeId(game, currentNodeId))}
226
+ onNextPressed={() => this.handleNavClicked(nextNodeId(game, currentNodeId))}
227
+ onLastPressed={() => this.handleNavClicked(lastNodeId(game, currentNodeId))}
228
+ />;
229
+ }
230
+
231
+ private renderToolbar(game: Game, node: GameNode | Variation, squareSize: number, isPlaying: boolean) {
232
+ const buttons: NavigationButtonList = [];
233
+
234
+ // Core navigation buttons
235
+ const currentNodeId = node.id();
236
+ const hasPrevious = currentNodeId !== 'start';
237
+ const hasNext = (node instanceof Variation ? node.first() : node.next()) !== undefined;
238
+ buttons.push({ iconPath: GO_FIRST_ICON_PATH, tooltip: i18n.TOOLTIP_GO_FIRST, enabled: hasPrevious, onClick: () => this.handleNavClicked(firstNodeId(game, currentNodeId)) });
239
+ buttons.push({ iconPath: GO_PREVIOUS_ICON_PATH, tooltip: i18n.TOOLTIP_GO_PREVIOUS, enabled: hasPrevious, onClick: () => this.handleNavClicked(previousNodeId(game, currentNodeId)) });
240
+ if (sanitizeBoolean(this.props.playButtonVisible)) {
241
+ buttons.push({ iconPath: isPlaying ? STOP_ICON_PATH : PLAY_ICON_PATH, tooltip: i18n.TOOLTIP_PLAY_STOP, enabled: hasNext, onClick: () => this.handlePlayStopClicked(!isPlaying) });
242
+ }
243
+ buttons.push({ iconPath: GO_NEXT_ICON_PATH, tooltip: i18n.TOOLTIP_GO_NEXT, enabled: hasNext, onClick: () => this.handleNavClicked(nextNodeId(game, currentNodeId)) });
244
+ buttons.push({ iconPath: GO_LAST_ICON_PATH, tooltip: i18n.TOOLTIP_GO_LAST, enabled: hasNext, onClick: () => this.handleNavClicked(lastNodeId(game, currentNodeId)) });
245
+ buttons.push('spacer');
246
+ if (sanitizeBoolean(this.props.flipButtonVisible)) {
247
+ buttons.push({ iconPath: FLIP_ICON_PATH, tooltip: i18n.TOOLTIP_FLIP, onClick: () => this.handleFlipButtonClicked() });
248
+ }
249
+ buttons.push('spacer');
250
+
251
+ // Schedule the next transition if auto-play is enabled.
252
+ if (isPlaying) {
253
+ if (hasNext) {
254
+ this.timeoutId = window.setTimeout(() => this.handleNavClicked(nextNodeId(game, currentNodeId), false), INTER_MOVE_DURATION);
255
+ }
256
+ else {
257
+ // ... or stop the auto-play if at the end of the game.
258
+ this.timeoutId = window.setTimeout(() => this.handlePlayStopClicked(false, false), 0);
259
+ }
260
+ }
261
+
262
+ // Additional buttons.
263
+ const additionalButtons = sanitizeNavigationButtonList(this.props.additionalButtons, () => new IllegalArgument('NavigationBoard', 'additionalButtons'));
264
+ for (const button of additionalButtons) {
265
+ buttons.push(button);
266
+ }
267
+
268
+ return <NavigationToolbar squareSize={squareSize} buttons={buttons} />;
269
+ }
270
+
271
+ private handleNavClicked(targetNodeId: string | undefined, forceFocus = true) {
272
+ if (forceFocus) {
273
+ this.focus();
274
+ }
275
+ if (targetNodeId === undefined) {
276
+ return;
277
+ }
278
+
279
+ if (this.props.nodeId === undefined) { // uncontrolled-component behavior
280
+ this.setState({ nodeIdAsUncontrolled: targetNodeId });
281
+ }
282
+ else if (this.props.onNodeIdChanged) { // controlled-component behavior
283
+ this.props.onNodeIdChanged(targetNodeId);
284
+ }
285
+ }
286
+
287
+ private handlePlayStopClicked(targetIsPlaying: boolean, forceFocus = true) {
288
+ if (forceFocus) {
289
+ this.focus();
290
+ }
291
+ if (this.props.isPlaying === undefined) { // uncontrolled is-playing state
292
+ this.setState({ isPlayingAsUncontrolled: targetIsPlaying });
293
+ }
294
+ else if (this.props.onIsPlayingChanged) { // controlled is-playing state
295
+ this.props.onIsPlayingChanged(targetIsPlaying);
296
+ }
297
+ }
298
+
299
+ private handleFlipButtonClicked() {
300
+ this.focus();
301
+ if (this.props.flipped === undefined) { // uncontrolled flip state
302
+ this.setState({ flippedAsUncontrolled: !this.state.flippedAsUncontrolled });
303
+ }
304
+ else if (this.props.onFlippedChanged) { // controlled flip state
305
+ this.props.onFlippedChanged(!this.props.flipped);
306
+ }
307
+ }
308
+
309
+ private cancelCurrentTimeout() {
310
+ if (this.timeoutId !== undefined) {
311
+ window.clearTimeout(this.timeoutId);
312
+ this.timeoutId = undefined;
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Set the focus to the current component.
318
+ *
319
+ * @public
320
+ */
321
+ focus(): void {
322
+ const target = this.navigationFieldRef.current;
323
+ // istanbul ignore else
324
+ if (target) {
325
+ target.focus();
326
+ }
327
+ }
328
+
329
+ }
230
330
 
331
+
332
+ /**
333
+ * Sanitization method for the additional button parameter.
334
+ */
335
+ function sanitizeNavigationButtonList(type: NavigationButton | NavigationButtonList, exceptionBuilder: () => IllegalArgument): NavigationButtonList {
336
+ if (Array.isArray(type)) {
337
+ return type.map(t => {
338
+ if (t !== 'spacer' && !isNavigationButton(t)) {
339
+ throw exceptionBuilder();
340
+ }
341
+ return t;
342
+ });
343
+ }
344
+ else if (isNavigationButton(type)) {
345
+ return [ type ];
346
+ }
347
+ else {
348
+ throw exceptionBuilder();
349
+ }
231
350
  }
@@ -0,0 +1,72 @@
1
+ /*!
2
+ * -------------------------------------------------------------------------- *
3
+ * *
4
+ * Kokopu-React - A React-based library of chess-related components. *
5
+ * <https://www.npmjs.com/package/kokopu-react> *
6
+ * Copyright (C) 2021-2024 Yoann Le Montagner <yo35 -at- melix.net> *
7
+ * *
8
+ * Kokopu-React is free software: you can redistribute it and/or *
9
+ * modify it under the terms of the GNU Lesser General Public License *
10
+ * as published by the Free Software Foundation, either version 3 of *
11
+ * the License, or (at your option) any later version. *
12
+ * *
13
+ * Kokopu-React is distributed in the hope that it will be useful, *
14
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
15
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
16
+ * GNU Lesser General Public License for more details. *
17
+ * *
18
+ * You should have received a copy of the GNU Lesser General *
19
+ * Public License along with this program. If not, see *
20
+ * <http://www.gnu.org/licenses/>. *
21
+ * *
22
+ * -------------------------------------------------------------------------- */
23
+
24
+
25
+ /**
26
+ * Descriptor for additional buttons to be added to the toolbar of {@link NavigationBoard}.
27
+ */
28
+ export type NavigationButton = {
29
+
30
+ /**
31
+ * Button icon, defined as a SVG path (see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands for more details
32
+ * on how to define a SVG path).
33
+ *
34
+ * The path is rendered within 32 x 32 viewbox (defined as `viewbox="0 0 32 32"`). It must be be a closed path, and must fit within the viewbox incircle.
35
+ */
36
+ iconPath: string;
37
+
38
+ /**
39
+ * Human-readable tooltip.
40
+ */
41
+ tooltip?: string;
42
+
43
+ /**
44
+ * Whether or not the button is enabled (true by default).
45
+ */
46
+ enabled?: boolean;
47
+
48
+ /**
49
+ * Callback invoked when the user clicks on the button.
50
+ */
51
+ onClick?: () => void;
52
+ };
53
+
54
+
55
+ /**
56
+ * List of additional buttons to be added to the toolbar of {@link NavigationBoard}, together with optional spacers in-between.
57
+ */
58
+ export type NavigationButtonList = (NavigationButton | 'spacer')[];
59
+
60
+
61
+ /**
62
+ * Whether the given object is a {@link NavigationButton} or not.
63
+ */
64
+ export function isNavigationButton(button: unknown): button is NavigationButton {
65
+ if (typeof button !== 'object' || button === null) {
66
+ return false;
67
+ }
68
+ return typeof (button as NavigationButton).iconPath === 'string' &&
69
+ ((button as NavigationButton).tooltip === undefined || typeof (button as NavigationButton).tooltip === 'string') &&
70
+ ((button as NavigationButton).enabled === undefined || typeof (button as NavigationButton).enabled === 'boolean') &&
71
+ ((button as NavigationButton).onClick === undefined || typeof (button as NavigationButton).onClick === 'function');
72
+ }
@@ -23,14 +23,14 @@
23
23
 
24
24
 
25
25
  .kokopu-focusFieldContainer {
26
- position: fixed;
27
- bottom: 0px;
28
- right: 0px;
29
- width: 0px;
30
- height: 0px;
31
- overflow: hidden;
26
+ position: fixed;
27
+ bottom: 0px;
28
+ right: 0px;
29
+ width: 0px;
30
+ height: 0px;
31
+ overflow: hidden;
32
32
  }
33
33
 
34
34
  .kokopu-focusField {
35
- position: absolute; /* see #1 */
35
+ position: absolute; /* see #1 */
36
36
  }