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
@@ -24,401 +24,348 @@
24
24
 
25
25
  import * as React from 'react';
26
26
 
27
- import { exception as kokopuException, MoveDescriptor, Position, Square, GameVariant, isSquare, isSquareCouple, isGameVariant } from 'kokopu';
27
+ import { MoveDescriptor, Position, Square, isSquare, isSquareCouple } from 'kokopu';
28
28
 
29
29
  import { IllegalArgument } from '../exception';
30
- import { i18n } from '../i18n';
31
30
  import { sanitizeString, sanitizeBoolean, sanitizePartialObject, sanitizeInteger, sanitizeBoundedInteger, sanitizeOptional } from '../sanitization';
32
31
  import { MIN_SQUARE_SIZE, MAX_SQUARE_SIZE, Colorset, Pieceset, AnnotationColor, AnnotationSymbol, SquareMarkerSet, TextMarkerSet, ArrowMarkerSet,
33
- isAnnotationColor, isAnnotationSymbol, parseSquareMarkers, parseTextMarkers, parseArrowMarkers } from '../types';
32
+ isAnnotationColor, isAnnotationSymbol, parseSquareMarkers, parseTextMarkers, parseArrowMarkers } from '../types';
34
33
 
35
- import { colorsets, DEFAULT_COLORSET } from './colorsets';
36
- import { piecesets, DEFAULT_PIECESET } from './piecesets';
34
+ import { StaticBoardGraphicProps, DynamicBoardGraphicProps, SmallScreenLimit, DEFAULT_SQUARE_SIZE, defaultDynamicBoardProps } from './BoardProperties';
35
+ import { colorsets } from './colorsets';
36
+ import { piecesets } from './piecesets';
37
37
  import { ChessboardImpl, chessboardSize } from './ChessboardImpl';
38
- import { ErrorBox } from '../errorbox/ErrorBox';
39
-
40
-
41
- /**
42
- * Define a limit applicable to the parameters of a {@link Chessboard} on small-screen devices.
43
- */
44
- export interface SmallScreenLimit {
45
- width: number;
46
- squareSize?: number;
47
- coordinateVisible?: boolean;
48
- turnVisible?: boolean;
49
- }
50
-
51
-
52
- export interface ChessboardProps {
53
-
54
- /**
55
- * Displayed position. Can be a [kokopu.Position](https://kokopu.yo35.org/docs/current/classes/Position.html) object,
56
- * a [FEN string](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation),
57
- * `'start'` (usual starting position), or `'empty'` (empty board).
58
- *
59
- * Optionally, the FEN string can be prefixed with `'variant:'`, `variant` corresponding to one of the
60
- * [game variant](https://kokopu.yo35.org/docs/current/types/GameVariant.html) supported by Kokopu. For instance:
61
- * `'chess960:nrkbqrbn/pppppppp/8/8/8/8/PPPPPPPP/NRKBQRBN w KQkq - 0 1'`.
62
- */
63
- position: Position | string;
64
-
65
- /**
66
- * Displayed move (optional), defined either as a [kokopu.MoveDescriptor](https://kokopu.yo35.org/docs/current/classes/MoveDescriptor.html) object
67
- * or as a [SAN string](https://en.wikipedia.org/wiki/Algebraic_notation_(chess)) (e.g. `'Nf3'`). In both cases, it must represent
68
- * a legal move in position defined in attribute `position`.
69
- */
70
- move?: MoveDescriptor | string;
71
-
72
- /**
73
- * Square markers, defined as a {@link SquareMarkerSet} (e.g. `{ e4: 'g', d5: 'r' }`) or as a comma-separated CSL string (e.g. `'Rd5,Ge4'`).
74
- */
75
- squareMarkers?: SquareMarkerSet | string;
76
-
77
- /**
78
- * Text markers, defined as a {@link TextMarkerSet} (e.g. `{ e4: { symbol: 'A', color: 'g' }, d5: { symbol: 'z', color: 'r' }}`)
79
- * or as a comma-separated CTL string (e.g. `'Rzd5,GAe4'`).
80
- */
81
- textMarkers?: TextMarkerSet | string;
82
-
83
- /**
84
- * Arrow markers, defined as a {@link ArrowMarkerSet} (e.g. `{ e2e4: 'g', g8f6: 'r', g8h6: 'y' }`)
85
- * or as a comma-separated CAL string (e.g. `'Ge2e4,Rg8f6,Yg8h6'`).
86
- */
87
- arrowMarkers?: ArrowMarkerSet | string;
88
-
89
- /**
90
- * Whether the board is flipped (i.e. seen from Black's point of view) or not.
91
- */
92
- flipped: boolean;
93
-
94
- /**
95
- * Size of the squares (in pixels). Must be an integer between `Chessboard.minSquareSize()` and `Chessboard.maxSquareSize()`.
96
- */
97
- squareSize: number;
98
-
99
- /**
100
- * Whether the row and column coordinates are visible or not.
101
- */
102
- coordinateVisible: boolean;
103
-
104
- /**
105
- * Whether the turn flag is visible or not.
106
- */
107
- turnVisible: boolean;
108
-
109
- /**
110
- * Whether moves are highlighted with an arrow or not.
111
- */
112
- moveArrowVisible: boolean;
113
-
114
- /**
115
- * Color of the move arrow.
116
- */
117
- moveArrowColor: AnnotationColor;
118
-
119
- /**
120
- * Whether moves are animated or not.
121
- */
122
- animated: boolean;
123
-
124
- /**
125
- * Color theme ID. Must be a property of `Chessboard.colorsets()`.
126
- */
127
- colorset: string;
128
-
129
- /**
130
- * Piece theme ID. Must be a property of `Chessboard.piecesets()`.
131
- */
132
- pieceset: string;
133
-
134
- /**
135
- * Limits applicable on small-screen devices. For instance, if set to
136
- * ```
137
- * [
138
- * { width: 768, squareSize: 40 },
139
- * { width: 375, squareSize: 24, coordinateVisible: false }
140
- * ]
141
- * ```
142
- * then on screens with resolution ≤768 pixels, the square size will be limited to 40 (whatever the value
143
- * of the `squareSize` attribute); in addition, on screens with resolution ≤375 pixels, the square size
144
- * will be limited to 24 and the row and column coordinates will always be hidden (whatever the value of the
145
- * `coordinateVisible` attribute).
146
- */
147
- smallScreenLimits?: SmallScreenLimit[];
148
-
149
- /**
150
- * Type of action allowed with the mouse on the chessboard. If undefined, then the user cannot interact with the component.
151
- *
152
- * - `'movePieces'` allows the user to drag & drop the chess pieces from one square to another (regardless of the chess rules),
153
- * - `'clickSquares'` allows the user to click on squares,
154
- * - `'editArrows'` allows the user to draw arrow markers from one square to another (warning: attribute `editedArrowColor` must be set),
155
- * - `'playMoves'` allows the user to play legal chess moves (thus no interaction is possible if the displayed position is not legal).
156
- */
157
- interactionMode?: 'movePieces' | 'clickSquares' | 'editArrows' | 'playMoves',
158
-
159
- /**
160
- * Color of the edited arrow. Mandatory if `interactionMode === 'editArrows'`, ignored otherwise.
161
- */
162
- editedArrowColor?: AnnotationColor;
163
-
164
- /**
165
- * Callback invoked when a piece is moved through drag&drop (only if `interactionMode` is set to `'movePieces'`).
166
- */
167
- onPieceMoved?: (from: Square, to: Square) => void;
168
-
169
- /**
170
- * Callback invoked when the user clicks on a square (only if `interactionMode` is set to `'clickSquares'`).
171
- */
172
- onSquareClicked?: (square: Square) => void;
173
-
174
- /**
175
- * Callback invoked when an arrow is dragged (only if `interactionMode` is set to `'editArrows'`).
176
- */
177
- onArrowEdited?: (from: Square, to: Square) => void;
178
-
179
- /**
180
- * Callback invoked when a move is played (only if `interactionMode` is set to `'playMoves'`).
181
- */
182
- onMovePlayed?: (move: string) => void;
38
+ import { parsePosition, parseMove } from '../errorbox/parsing';
39
+
40
+
41
+ export interface ChessboardProps extends DynamicBoardGraphicProps {
42
+
43
+ /**
44
+ * Displayed position. Can be a [kokopu.Position](https://kokopu.yo35.org/docs/current/classes/Position.html) object,
45
+ * a [FEN string](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation),
46
+ * `'start'` (usual starting position), or `'empty'` (empty board).
47
+ *
48
+ * Optionally, the FEN string can be prefixed with `'variant:'`, `variant` corresponding to one of the
49
+ * [game variant](https://kokopu.yo35.org/docs/current/types/GameVariant.html) supported by Kokopu. For instance:
50
+ * `'chess960:nrkbqrbn/pppppppp/8/8/8/8/PPPPPPPP/NRKBQRBN w KQkq - 0 1'`.
51
+ */
52
+ position: Position | string;
53
+
54
+ /**
55
+ * Displayed move (optional), defined either as a [kokopu.MoveDescriptor](https://kokopu.yo35.org/docs/current/classes/MoveDescriptor.html) object
56
+ * or as a [SAN string](https://en.wikipedia.org/wiki/Algebraic_notation_(chess)) (e.g. `'Nf3'`). Use `'--'` for a null-move.
57
+ * In all cases, the move must be a legal move in position defined in attribute `position`.
58
+ */
59
+ move?: MoveDescriptor | string;
60
+
61
+ /**
62
+ * Square markers, defined as a {@link SquareMarkerSet} (e.g. `{ e4: 'g', d5: 'r' }`) or as a comma-separated CSL string (e.g. `'Rd5,Ge4'`).
63
+ */
64
+ squareMarkers?: SquareMarkerSet | string;
65
+
66
+ /**
67
+ * Text markers, defined as a {@link TextMarkerSet} (e.g. `{ e4: { symbol: 'A', color: 'g' }, d5: { symbol: 'z', color: 'r' }}`)
68
+ * or as a comma-separated CTL string (e.g. `'Rzd5,GAe4'`).
69
+ */
70
+ textMarkers?: TextMarkerSet | string;
71
+
72
+ /**
73
+ * Arrow markers, defined as a {@link ArrowMarkerSet} (e.g. `{ e2e4: 'g', g8f6: 'r', g8h6: 'y' }`)
74
+ * or as a comma-separated CAL string (e.g. `'Ge2e4,Rg8f6,Yg8h6'`).
75
+ */
76
+ arrowMarkers?: ArrowMarkerSet | string;
77
+
78
+ /**
79
+ * Whether the board is flipped (i.e. seen from Black's point of view) or not.
80
+ */
81
+ flipped: boolean;
82
+
83
+ /**
84
+ * Type of action allowed with the mouse on the chessboard. If undefined, then the user cannot interact with the component.
85
+ *
86
+ * - `'movePieces'` allows the user to drag & drop the chess pieces from one square to another (regardless of the chess rules),
87
+ * - `'clickSquares'` allows the user to click on squares,
88
+ * - `'editArrows'` allows the user to draw arrow markers from one square to another (warning: attribute `editedArrowColor` must be set),
89
+ * - `'playMoves'` allows the user to play legal chess moves (thus no interaction is possible if the displayed position is not legal).
90
+ */
91
+ interactionMode?: 'movePieces' | 'clickSquares' | 'editArrows' | 'playMoves',
92
+
93
+ /**
94
+ * Color of the edited arrow. Mandatory if `interactionMode === 'editArrows'`, ignored otherwise.
95
+ */
96
+ editedArrowColor?: AnnotationColor;
97
+
98
+ /**
99
+ * Callback invoked when a piece is moved through drag&drop (only if `interactionMode` is set to `'movePieces'`).
100
+ */
101
+ onPieceMoved?: (from: Square, to: Square) => void;
102
+
103
+ /**
104
+ * Callback invoked when the user clicks on a square (only if `interactionMode` is set to `'clickSquares'`).
105
+ */
106
+ onSquareClicked?: (square: Square) => void;
107
+
108
+ /**
109
+ * Callback invoked when an arrow is dragged (only if `interactionMode` is set to `'editArrows'`).
110
+ */
111
+ onArrowEdited?: (from: Square, to: Square) => void;
112
+
113
+ /**
114
+ * Callback invoked when a move is played (only if `interactionMode` is set to `'playMoves'`).
115
+ */
116
+ onMovePlayed?: (move: string) => void;
117
+
118
+ /**
119
+ * Optional component, to be rendered above the chessboard, and customized with the same square-size / coordinate-visible / turn-visible
120
+ * parameter values as actually used for the chessboard (which may be different from what is defined in props because of small-screen limits).
121
+ */
122
+ topComponent?: (attr: Pick<StaticBoardGraphicProps, 'squareSize' | 'coordinateVisible' | 'turnVisible'>) => JSX.Element;
123
+
124
+ /**
125
+ * Optional component, to be rendered below the chessboard, and customized with the same square-size / coordinate-visible / turn-visible
126
+ * parameter values as actually used for the chessboard (which may be different from what is defined in props because of small-screen limits).
127
+ */
128
+ bottomComponent?: (attr: Pick<StaticBoardGraphicProps, 'squareSize' | 'coordinateVisible' | 'turnVisible'>) => JSX.Element;
183
129
  }
184
130
 
185
131
 
186
132
  interface ChessboardState {
187
- windowWidth: number;
133
+ windowWidth: number;
188
134
  }
189
135
 
190
136
 
191
- const DEFAULT_SQUARE_SIZE = 40;
192
-
193
-
194
137
  /**
195
138
  * SVG image representing a chessboard diagram. Optionally, the user may interact with the board (move pieces, click on squares...).
196
139
  * Annotations such as square markers or arrows can also be added to the board.
197
140
  */
198
141
  export class Chessboard extends React.Component<ChessboardProps, ChessboardState> {
199
142
 
200
- static defaultProps: Partial<ChessboardProps> = {
201
- position: 'start',
202
- flipped: false,
203
- squareSize: DEFAULT_SQUARE_SIZE,
204
- coordinateVisible: true,
205
- turnVisible: true,
206
- moveArrowVisible: true,
207
- moveArrowColor: 'b',
208
- animated: false,
209
- colorset: DEFAULT_COLORSET,
210
- pieceset: DEFAULT_PIECESET,
211
- };
212
-
213
- private windowResizeListener = () => this.handleWindowResize();
214
-
215
- constructor(props: ChessboardProps) {
216
- super(props);
217
- this.state = {
218
- windowWidth: window.innerWidth,
219
- };
220
- }
221
-
222
- componentDidMount() {
223
- window.addEventListener('resize', this.windowResizeListener);
224
- }
225
-
226
- componentWillUnmount() {
227
- window.removeEventListener('resize', this.windowResizeListener);
228
- }
229
-
230
- private handleWindowResize() {
231
- this.setState({ windowWidth: window.innerWidth });
232
- }
233
-
234
- render() {
235
-
236
- // Validate the position and move attributes.
237
- const positionInfo = parsePosition(this.props.position);
238
- if (positionInfo.error) {
239
- return <ErrorBox title={i18n.INVALID_FEN_ERROR_TITLE} message={positionInfo.message} />;
240
- }
241
- const position = positionInfo.position;
242
- const moveInfo = parseMove(position, this.props.move);
243
- if (moveInfo.error) {
244
- return <ErrorBox title={i18n.INVALID_NOTATION_ERROR_TITLE} message={moveInfo.message} />;
245
- }
246
- const move = moveInfo.move;
247
-
248
- // Validate the markers
249
- const sqm = parseMarkers(this.props.squareMarkers, 'squareMarkers', parseSquareMarkers, isSquare, isAnnotationColor);
250
- const txtm = parseMarkers(this.props.textMarkers, 'textMarkers', parseTextMarkers, isSquare, isTextMarkerElement);
251
- const arm = parseMarkers(this.props.arrowMarkers, 'arrowMarkers', parseArrowMarkers, isSquareCouple, isAnnotationColor);
252
-
253
- // Validate the appearance attributes.
254
- const flipped = sanitizeBoolean(this.props.flipped);
255
- const squareSize = sanitizeBoundedInteger(this.props.squareSize, MIN_SQUARE_SIZE, MAX_SQUARE_SIZE, () => new IllegalArgument('Chessboard', 'squareSize'));
256
- const coordinateVisible = sanitizeBoolean(this.props.coordinateVisible);
257
- const turnVisible = sanitizeBoolean(this.props.turnVisible);
258
- const moveArrowVisible = sanitizeBoolean(this.props.moveArrowVisible);
259
- if (!isAnnotationColor(this.props.moveArrowColor)) {
260
- throw new IllegalArgument('Chessboard', 'moveArrowColor');
261
- }
262
- const animated = sanitizeBoolean(this.props.animated);
263
- const colorset = colorsets[this.props.colorset];
264
- if (!colorset) {
265
- throw new IllegalArgument('Chessboard', 'colorset');
266
- }
267
- const pieceset = piecesets[this.props.pieceset];
268
- if (!pieceset) {
269
- throw new IllegalArgument('Chessboard', 'pieceset');
270
- }
271
-
272
- // Validate and enforce the small-screen limits.
273
- const smallScreenLimits = sanitizeSmallScreenLimits(this.props.smallScreenLimits, () => new IllegalArgument('Chessboard', 'smallScreenLimits'));
274
- const actualSquareSize = computeSquareSizeForSmallScreens(squareSize, smallScreenLimits, this.state.windowWidth);
275
- const actualCoordinateVisible = computeCoordinateVisibleForSmallScreens(coordinateVisible, smallScreenLimits, this.state.windowWidth);
276
- const actualTurnVisible = computeTurnVisibleForSmallScreens(turnVisible, smallScreenLimits, this.state.windowWidth);
277
-
278
- // Validate the interaction attributes and the callbacks.
279
- const interactionMode = this.getInteractionModeAndValidateEditedArrowColor();
280
- const { onPieceMoved, onSquareClicked, onArrowEdited, onMovePlayed } = this.props;
281
-
282
- // Build a key so that a new `ChessboardImpl` component is instantiated each time the position or the move changes
283
- // (mandatory to ensure that the move animation works properly and that the internal state of the component remains consistent).
284
- const key = `${position.variant()}|${position.fen()}|${move ? position.notation(move) : ''}`;
285
-
286
- return (
287
- <ChessboardImpl key={key}
288
- position={position} move={move} squareMarkers={sqm} textMarkers={txtm} arrowMarkers={arm} flipped={flipped}
289
- squareSize={actualSquareSize} coordinateVisible={actualCoordinateVisible} turnVisible={actualTurnVisible}
290
- moveArrowVisible={moveArrowVisible} moveArrowColor={this.props.moveArrowColor} animated={animated}
291
- colorset={colorset} pieceset={pieceset}
292
- interactionMode={interactionMode} editedArrowColor={this.props.editedArrowColor}
293
- onPieceMoved={onPieceMoved} onSquareClicked={onSquareClicked} onArrowEdited={onArrowEdited} onMovePlayed={onMovePlayed}
294
- />
295
- );
296
- }
297
-
298
- private getInteractionModeAndValidateEditedArrowColor() {
299
- const interactionMode = sanitizeOptional(this.props.interactionMode, sanitizeString);
300
- if (interactionMode === undefined || interactionMode === 'movePieces' || interactionMode === 'clickSquares' || interactionMode === 'playMoves') {
301
- return interactionMode;
302
- }
303
- else if (interactionMode === 'editArrows') {
304
- if (!isAnnotationColor(this.props.editedArrowColor)) {
305
- throw new IllegalArgument('Chessboard', 'editedArrowColor');
306
- }
307
- return interactionMode;
308
- }
309
- else {
310
- throw new IllegalArgument('Chessboard', 'interactionMode');
311
- }
312
- }
313
-
314
- /**
315
- * Return the size of the chessboard, assuming it is built with the given attributes.
316
- */
317
- static size(attr?: Partial<Pick<ChessboardProps, 'squareSize' | 'coordinateVisible' | 'turnVisible' | 'smallScreenLimits'>>): { width: number, height: number } {
318
- let { squareSize, coordinateVisible, turnVisible, smallScreenLimits } = sanitizePartialObject(attr, () => new IllegalArgument('Chessboard.size()', 'attr'));
319
-
320
- // Sanitize the arguments.
321
- squareSize = squareSize === undefined ? DEFAULT_SQUARE_SIZE :
322
- sanitizeBoundedInteger(squareSize, MIN_SQUARE_SIZE, MAX_SQUARE_SIZE, () => new IllegalArgument('Chessboard.size()', 'squareSize'));
323
- coordinateVisible = coordinateVisible === undefined ? true : sanitizeBoolean(coordinateVisible);
324
- turnVisible = turnVisible === undefined ? true : sanitizeBoolean(turnVisible);
325
- smallScreenLimits = sanitizeSmallScreenLimits(smallScreenLimits, () => new IllegalArgument('Chessboard.size()', 'smallScreenLimits'));
326
-
327
- // Enforce small-screen limits, if any.
328
- if (typeof window !== 'undefined') {
329
- squareSize = computeSquareSizeForSmallScreens(squareSize, smallScreenLimits, window.innerWidth);
330
- coordinateVisible = computeCoordinateVisibleForSmallScreens(coordinateVisible, smallScreenLimits, window.innerWidth);
331
- turnVisible = computeTurnVisibleForSmallScreens(turnVisible, smallScreenLimits, window.innerWidth);
332
- }
333
-
334
- // Compute the dimensions.
335
- return chessboardSize(squareSize, coordinateVisible, turnVisible);
336
- }
337
-
338
- /**
339
- * Return the maximum square size that would allow the chessboard to fit in a rectangle of size `width x height`.
340
- */
341
- static adaptSquareSize(width: number, height: number, attr?: Partial<Pick<ChessboardProps, 'coordinateVisible' | 'turnVisible' | 'smallScreenLimits'>>): number {
342
- let { coordinateVisible, turnVisible, smallScreenLimits } = sanitizePartialObject(attr, () => new IllegalArgument('Chessboard.size()', 'attr'));
343
-
344
- // Sanitize the arguments.
345
- width = sanitizeInteger(width, () => new IllegalArgument('Chessboard.adaptSquareSize()', 'width'));
346
- height = sanitizeInteger(height, () => new IllegalArgument('Chessboard.adaptSquareSize()', 'height'));
347
- coordinateVisible = coordinateVisible === undefined ? true : sanitizeBoolean(coordinateVisible);
348
- turnVisible = turnVisible === undefined ? true : sanitizeBoolean(turnVisible);
349
- smallScreenLimits = sanitizeSmallScreenLimits(smallScreenLimits, () => new IllegalArgument('Chessboard.adaptSquareSize()', 'smallScreenLimits'));
350
-
351
- // Enforce small-screen limits, if any.
352
- let maxSquareSize = MAX_SQUARE_SIZE;
353
- if (typeof window !== 'undefined') {
354
- maxSquareSize = computeSquareSizeForSmallScreens(maxSquareSize, smallScreenLimits, window.innerWidth);
355
- coordinateVisible = computeCoordinateVisibleForSmallScreens(coordinateVisible, smallScreenLimits, window.innerWidth);
356
- turnVisible = computeTurnVisibleForSmallScreens(turnVisible, smallScreenLimits, window.innerWidth);
357
- }
358
-
359
- function isAdapted(squareSize: number) {
360
- const { width: actualWidth, height: actualHeight } = chessboardSize(squareSize, coordinateVisible!, turnVisible!);
361
- return actualWidth <= width && actualHeight <= height;
362
- }
363
-
364
- // Check min/max bounds.
365
- if (isAdapted(maxSquareSize)) {
366
- return maxSquareSize;
367
- }
368
- else if (!isAdapted(MIN_SQUARE_SIZE)) {
369
- return MIN_SQUARE_SIZE;
370
- }
371
-
372
- // Dichotomic search, between a (inclusive) and b (exclusive).
373
- let a = MIN_SQUARE_SIZE;
374
- let b = maxSquareSize;
375
- while (a + 1 < b) {
376
- const mid = Math.floor((a + b) / 2);
377
- if (isAdapted(mid)) {
378
- a = mid;
379
- }
380
- else {
381
- b = mid;
382
- }
383
- }
384
- return a;
385
- }
386
-
387
- /**
388
- * Minimum square size (inclusive).
389
- *
390
- * @public
391
- */
392
- static minSquareSize(): number {
393
- return MIN_SQUARE_SIZE;
394
- }
395
-
396
- /**
397
- * Maximum square size (inclusive).
398
- *
399
- * @public
400
- */
401
- static maxSquareSize(): number {
402
- return MAX_SQUARE_SIZE;
403
- }
404
-
405
- /**
406
- * Available colorsets for theming.
407
- *
408
- * @public
409
- */
410
- static colorsets(): Record<string, Colorset> {
411
- return colorsets;
412
- }
413
-
414
- /**
415
- * Available piecesets for theming.
416
- *
417
- * @public
418
- */
419
- static piecesets(): Record<string, Pieceset> {
420
- return piecesets;
421
- }
143
+ static defaultProps: Partial<ChessboardProps> = {
144
+ ...defaultDynamicBoardProps(),
145
+ position: 'start',
146
+ flipped: false,
147
+ };
148
+
149
+ private windowResizeListener = () => this.handleWindowResize();
150
+
151
+ constructor(props: ChessboardProps) {
152
+ super(props);
153
+ this.state = {
154
+ windowWidth: window.innerWidth,
155
+ };
156
+ }
157
+
158
+ componentDidMount() {
159
+ window.addEventListener('resize', this.windowResizeListener);
160
+ }
161
+
162
+ componentWillUnmount() {
163
+ window.removeEventListener('resize', this.windowResizeListener);
164
+ }
165
+
166
+ private handleWindowResize() {
167
+ this.setState({ windowWidth: window.innerWidth });
168
+ }
169
+
170
+ render() {
171
+
172
+ // Validate the position and move attributes.
173
+ const positionInfo = parsePosition(this.props.position, 'Chessboard');
174
+ if (positionInfo.error) {
175
+ return positionInfo.errorBox;
176
+ }
177
+ let position = positionInfo.position;
178
+ const moveInfo = parseMove(position, this.props.move, 'Chessboard');
179
+ if (moveInfo.error) {
180
+ return moveInfo.errorBox;
181
+ }
182
+ const move = moveInfo.type === 'regular' ? moveInfo.move : undefined;
183
+ if (moveInfo.type === 'null-move') {
184
+ position = new Position(position);
185
+ position.playNullMove();
186
+ }
187
+
188
+ // Validate the markers
189
+ const sqm = parseMarkers(this.props.squareMarkers, 'squareMarkers', parseSquareMarkers, isSquare, isAnnotationColor);
190
+ const txtm = parseMarkers(this.props.textMarkers, 'textMarkers', parseTextMarkers, isSquare, isTextMarkerElement);
191
+ const arm = parseMarkers(this.props.arrowMarkers, 'arrowMarkers', parseArrowMarkers, isSquareCouple, isAnnotationColor);
192
+
193
+ // Validate the appearance attributes.
194
+ const flipped = sanitizeBoolean(this.props.flipped);
195
+ const squareSize = sanitizeBoundedInteger(this.props.squareSize, MIN_SQUARE_SIZE, MAX_SQUARE_SIZE, () => new IllegalArgument('Chessboard', 'squareSize'));
196
+ const coordinateVisible = sanitizeBoolean(this.props.coordinateVisible);
197
+ const turnVisible = sanitizeBoolean(this.props.turnVisible);
198
+ const moveArrowVisible = sanitizeBoolean(this.props.moveArrowVisible);
199
+ if (!isAnnotationColor(this.props.moveArrowColor)) {
200
+ throw new IllegalArgument('Chessboard', 'moveArrowColor');
201
+ }
202
+ const animated = sanitizeBoolean(this.props.animated);
203
+ const colorset = colorsets[this.props.colorset];
204
+ if (!colorset) {
205
+ throw new IllegalArgument('Chessboard', 'colorset');
206
+ }
207
+ const pieceset = piecesets[this.props.pieceset];
208
+ if (!pieceset) {
209
+ throw new IllegalArgument('Chessboard', 'pieceset');
210
+ }
211
+
212
+ // Validate and enforce the small-screen limits.
213
+ const smallScreenLimits = sanitizeSmallScreenLimits(this.props.smallScreenLimits, () => new IllegalArgument('Chessboard', 'smallScreenLimits'));
214
+ const actualSquareSize = computeSquareSizeForSmallScreens(squareSize, smallScreenLimits, this.state.windowWidth);
215
+ const actualCoordinateVisible = computeCoordinateVisibleForSmallScreens(coordinateVisible, smallScreenLimits, this.state.windowWidth);
216
+ const actualTurnVisible = computeTurnVisibleForSmallScreens(turnVisible, smallScreenLimits, this.state.windowWidth);
217
+ const auxilliaryComponentAttr = {
218
+ squareSize: actualSquareSize,
219
+ coordinateVisible: actualCoordinateVisible,
220
+ turnVisible: actualTurnVisible,
221
+ };
222
+
223
+ // Validate the interaction attributes and the callbacks.
224
+ const interactionMode = this.getInteractionModeAndValidateEditedArrowColor();
225
+ const { onPieceMoved, onSquareClicked, onArrowEdited, onMovePlayed } = this.props;
226
+
227
+ // Build a key so that a new `ChessboardImpl` component is instantiated each time the position or the move changes
228
+ // (mandatory to ensure that the move animation works properly and that the internal state of the component remains consistent).
229
+ const key = `${position.variant()}|${position.fen()}|${move ? position.notation(move) : ''}`;
230
+
231
+ return (<>
232
+ {this.props.topComponent ? this.props.topComponent(auxilliaryComponentAttr) : undefined}
233
+ <ChessboardImpl key={key}
234
+ position={position} move={move} squareMarkers={sqm} textMarkers={txtm} arrowMarkers={arm} flipped={flipped}
235
+ squareSize={actualSquareSize} coordinateVisible={actualCoordinateVisible} turnVisible={actualTurnVisible}
236
+ moveArrowVisible={moveArrowVisible} moveArrowColor={this.props.moveArrowColor} animated={animated}
237
+ colorset={colorset} pieceset={pieceset}
238
+ interactionMode={interactionMode} editedArrowColor={this.props.editedArrowColor}
239
+ onPieceMoved={onPieceMoved} onSquareClicked={onSquareClicked} onArrowEdited={onArrowEdited} onMovePlayed={onMovePlayed}
240
+ />
241
+ {this.props.bottomComponent ? this.props.bottomComponent(auxilliaryComponentAttr) : undefined}
242
+ </>);
243
+ }
244
+
245
+ private getInteractionModeAndValidateEditedArrowColor() {
246
+ const interactionMode = sanitizeOptional(this.props.interactionMode, sanitizeString);
247
+ if (interactionMode === undefined || interactionMode === 'movePieces' || interactionMode === 'clickSquares' || interactionMode === 'playMoves') {
248
+ return interactionMode;
249
+ }
250
+ else if (interactionMode === 'editArrows') {
251
+ if (!isAnnotationColor(this.props.editedArrowColor)) {
252
+ throw new IllegalArgument('Chessboard', 'editedArrowColor');
253
+ }
254
+ return interactionMode;
255
+ }
256
+ else {
257
+ throw new IllegalArgument('Chessboard', 'interactionMode');
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Return the size of the chessboard, assuming it is built with the given attributes.
263
+ */
264
+ static size(attr?: Partial<Pick<ChessboardProps, 'squareSize' | 'coordinateVisible' | 'turnVisible' | 'smallScreenLimits'>>): { width: number, height: number } {
265
+ let { squareSize, coordinateVisible, turnVisible, smallScreenLimits } = sanitizePartialObject(attr, () => new IllegalArgument('Chessboard.size()', 'attr'));
266
+
267
+ // Sanitize the arguments.
268
+ squareSize = squareSize === undefined ? DEFAULT_SQUARE_SIZE :
269
+ sanitizeBoundedInteger(squareSize, MIN_SQUARE_SIZE, MAX_SQUARE_SIZE, () => new IllegalArgument('Chessboard.size()', 'squareSize'));
270
+ coordinateVisible = coordinateVisible === undefined ? true : sanitizeBoolean(coordinateVisible);
271
+ turnVisible = turnVisible === undefined ? true : sanitizeBoolean(turnVisible);
272
+ smallScreenLimits = sanitizeSmallScreenLimits(smallScreenLimits, () => new IllegalArgument('Chessboard.size()', 'smallScreenLimits'));
273
+
274
+ // Enforce small-screen limits, if any.
275
+ if (typeof window !== 'undefined') {
276
+ squareSize = computeSquareSizeForSmallScreens(squareSize, smallScreenLimits, window.innerWidth);
277
+ coordinateVisible = computeCoordinateVisibleForSmallScreens(coordinateVisible, smallScreenLimits, window.innerWidth);
278
+ turnVisible = computeTurnVisibleForSmallScreens(turnVisible, smallScreenLimits, window.innerWidth);
279
+ }
280
+
281
+ // Compute the dimensions.
282
+ return chessboardSize(squareSize, coordinateVisible, turnVisible);
283
+ }
284
+
285
+ /**
286
+ * Return the maximum square size that would allow the chessboard to fit in a rectangle of size `width x height`.
287
+ */
288
+ static adaptSquareSize(width: number, height: number, attr?: Partial<Pick<ChessboardProps, 'coordinateVisible' | 'turnVisible' | 'smallScreenLimits'>>): number {
289
+ let { coordinateVisible, turnVisible, smallScreenLimits } = sanitizePartialObject(attr, () => new IllegalArgument('Chessboard.adaptSquareSize()', 'attr'));
290
+
291
+ // Sanitize the arguments.
292
+ width = sanitizeInteger(width, () => new IllegalArgument('Chessboard.adaptSquareSize()', 'width'));
293
+ height = sanitizeInteger(height, () => new IllegalArgument('Chessboard.adaptSquareSize()', 'height'));
294
+ coordinateVisible = coordinateVisible === undefined ? true : sanitizeBoolean(coordinateVisible);
295
+ turnVisible = turnVisible === undefined ? true : sanitizeBoolean(turnVisible);
296
+ smallScreenLimits = sanitizeSmallScreenLimits(smallScreenLimits, () => new IllegalArgument('Chessboard.adaptSquareSize()', 'smallScreenLimits'));
297
+
298
+ // Enforce small-screen limits, if any.
299
+ let maxSquareSize = MAX_SQUARE_SIZE;
300
+ if (typeof window !== 'undefined') {
301
+ maxSquareSize = computeSquareSizeForSmallScreens(maxSquareSize, smallScreenLimits, window.innerWidth);
302
+ coordinateVisible = computeCoordinateVisibleForSmallScreens(coordinateVisible, smallScreenLimits, window.innerWidth);
303
+ turnVisible = computeTurnVisibleForSmallScreens(turnVisible, smallScreenLimits, window.innerWidth);
304
+ }
305
+
306
+ function isAdapted(squareSize: number) {
307
+ const { width: actualWidth, height: actualHeight } = chessboardSize(squareSize, coordinateVisible!, turnVisible!);
308
+ return actualWidth <= width && actualHeight <= height;
309
+ }
310
+
311
+ // Check min/max bounds.
312
+ if (isAdapted(maxSquareSize)) {
313
+ return maxSquareSize;
314
+ }
315
+ else if (!isAdapted(MIN_SQUARE_SIZE)) {
316
+ return MIN_SQUARE_SIZE;
317
+ }
318
+
319
+ // Dichotomic search, between a (inclusive) and b (exclusive).
320
+ let a = MIN_SQUARE_SIZE;
321
+ let b = maxSquareSize;
322
+ while (a + 1 < b) {
323
+ const mid = Math.floor((a + b) / 2);
324
+ if (isAdapted(mid)) {
325
+ a = mid;
326
+ }
327
+ else {
328
+ b = mid;
329
+ }
330
+ }
331
+ return a;
332
+ }
333
+
334
+ /**
335
+ * Minimum square size (inclusive).
336
+ *
337
+ * @public
338
+ */
339
+ static minSquareSize(): number {
340
+ return MIN_SQUARE_SIZE;
341
+ }
342
+
343
+ /**
344
+ * Maximum square size (inclusive).
345
+ *
346
+ * @public
347
+ */
348
+ static maxSquareSize(): number {
349
+ return MAX_SQUARE_SIZE;
350
+ }
351
+
352
+ /**
353
+ * Available colorsets for theming.
354
+ *
355
+ * @public
356
+ */
357
+ static colorsets(): Record<string, Colorset> {
358
+ return colorsets;
359
+ }
360
+
361
+ /**
362
+ * Available piecesets for theming.
363
+ *
364
+ * @public
365
+ */
366
+ static piecesets(): Record<string, Pieceset> {
367
+ return piecesets;
368
+ }
422
369
  }
423
370
 
424
371
 
@@ -426,8 +373,8 @@ export class Chessboard extends React.Component<ChessboardProps, ChessboardState
426
373
  * Compute the actual square size taking into account the given small-screen limits and window width.
427
374
  */
428
375
  function computeSquareSizeForSmallScreens(squareSize: number, smallScreenLimits: SmallScreenLimit[], windowWidth: number) {
429
- const maximumSquareSize = computeSmallScreenLimits(smallScreenLimits, windowWidth, limit => limit.squareSize);
430
- return maximumSquareSize === undefined ? squareSize : Math.min(squareSize, maximumSquareSize);
376
+ const maximumSquareSize = computeSmallScreenLimits(smallScreenLimits, windowWidth, limit => limit.squareSize);
377
+ return maximumSquareSize === undefined ? squareSize : Math.min(squareSize, maximumSquareSize);
431
378
  }
432
379
 
433
380
 
@@ -435,8 +382,8 @@ function computeSquareSizeForSmallScreens(squareSize: number, smallScreenLimits:
435
382
  * Compute the actual coordinate visibility attribute taking into account the given small-screen limits and window width.
436
383
  */
437
384
  function computeCoordinateVisibleForSmallScreens(coordinateVisible: boolean, smallScreenLimits: SmallScreenLimit[], windowWidth: number) {
438
- const maximumCoordinateVisible = computeSmallScreenLimits(smallScreenLimits, windowWidth, limit => limit.coordinateVisible);
439
- return maximumCoordinateVisible === undefined ? coordinateVisible : coordinateVisible && maximumCoordinateVisible;
385
+ const maximumCoordinateVisible = computeSmallScreenLimits(smallScreenLimits, windowWidth, limit => limit.coordinateVisible);
386
+ return maximumCoordinateVisible === undefined ? coordinateVisible : coordinateVisible && maximumCoordinateVisible;
440
387
  }
441
388
 
442
389
 
@@ -444,8 +391,8 @@ function computeCoordinateVisibleForSmallScreens(coordinateVisible: boolean, sma
444
391
  * Compute the actual turn flag visibility attribute taking into account the given small-screen limits and window width.
445
392
  */
446
393
  function computeTurnVisibleForSmallScreens(turnVisible: boolean, smallScreenLimits: SmallScreenLimit[], windowWidth: number) {
447
- const maximumTurnVisible = computeSmallScreenLimits(smallScreenLimits, windowWidth, limit => limit.turnVisible);
448
- return maximumTurnVisible === undefined ? turnVisible : turnVisible && maximumTurnVisible;
394
+ const maximumTurnVisible = computeSmallScreenLimits(smallScreenLimits, windowWidth, limit => limit.turnVisible);
395
+ return maximumTurnVisible === undefined ? turnVisible : turnVisible && maximumTurnVisible;
449
396
  }
450
397
 
451
398
 
@@ -453,99 +400,23 @@ function computeTurnVisibleForSmallScreens(turnVisible: boolean, smallScreenLimi
453
400
  * Compute the limit applicable to the given window width.
454
401
  */
455
402
  function computeSmallScreenLimits<T>(smallScreenLimits: SmallScreenLimit[], windowWidth: number, getter: (limit: SmallScreenLimit) => T | undefined): T | undefined {
456
- let bestWidth = Infinity;
457
- let bestAttributeValue: T | undefined = undefined;
458
- for (const limit of smallScreenLimits) {
459
-
460
- // Discard the limits not applicable to the current window width (`windowWidth <= limit.width` must hold)
461
- // and those for which the target attribute is not defined.
462
- if (windowWidth > limit.width || getter(limit) === undefined) {
463
- continue;
464
- }
465
-
466
- // Find the limit with the smallest width among those applicable.
467
- if (limit.width < bestWidth) {
468
- bestWidth = limit.width;
469
- bestAttributeValue = getter(limit);
470
- }
471
- }
472
- return bestAttributeValue;
473
- }
474
-
475
-
476
- /**
477
- * Try to interpret the given object as a chess position.
478
- */
479
- function parsePosition(position: Position | string): { error: true, message: string } | { error: false, position: Position } {
480
- if (position instanceof Position) {
481
- return { error: false, position: position };
482
- }
483
- else if (position === 'start' || position === 'empty') {
484
- return { error: false, position: new Position(position) };
485
- }
486
- else if (typeof position === 'string') {
487
- try {
488
- const { variant, fen } = splitGameVariantAndFEN(position);
489
- const result = new Position(variant, 'empty');
490
- result.fen(fen);
491
- return { error: false, position: result };
492
- }
493
- catch (e) {
494
- // istanbul ignore else
495
- if (e instanceof kokopuException.InvalidFEN) {
496
- return { error: true, message: e.message };
497
- }
498
- else {
499
- throw e;
500
- }
501
- }
502
- }
503
- else {
504
- throw new IllegalArgument('Chessboard', 'position');
505
- }
506
- }
507
-
508
-
509
- /**
510
- * Look for an optional 'chess-variant:' prefix in the position attribute.
511
- */
512
- function splitGameVariantAndFEN(position: string): { variant: GameVariant, fen: string } {
513
- const separatorIndex = position.indexOf(':');
514
- if (separatorIndex < 0) {
515
- return { variant: 'regular', fen: position };
516
- }
517
- const variant = position.substring(0, separatorIndex);
518
- return isGameVariant(variant) ? { variant: variant, fen: position.substring(separatorIndex + 1) } : { variant: 'regular', fen: position };
519
- }
520
-
521
-
522
- /**
523
- * Try to interpret the given object `move` as a move descriptor based on the given position.
524
- */
525
- function parseMove(position: Position, move: MoveDescriptor | string | undefined): { error: true, message: string } | { error: false, move: MoveDescriptor | undefined } {
526
- if (move === undefined || move === null) {
527
- return { error: false, move: undefined };
528
- }
529
- else if (move instanceof MoveDescriptor) {
530
- return { error: false, move: move };
531
- }
532
- else if (typeof move === 'string') {
533
- try {
534
- return { error: false, move: position.notation(move) };
535
- }
536
- catch (e) {
537
- // istanbul ignore else
538
- if (e instanceof kokopuException.InvalidNotation) {
539
- return { error: true, message: e.message };
540
- }
541
- else {
542
- throw e;
543
- }
544
- }
545
- }
546
- else {
547
- throw new IllegalArgument('Chessboard', 'move');
548
- }
403
+ let bestWidth = Infinity;
404
+ let bestAttributeValue: T | undefined = undefined;
405
+ for (const limit of smallScreenLimits) {
406
+
407
+ // Discard the limits not applicable to the current window width (`windowWidth <= limit.width` must hold)
408
+ // and those for which the target attribute is not defined.
409
+ if (windowWidth > limit.width || getter(limit) === undefined) {
410
+ continue;
411
+ }
412
+
413
+ // Find the limit with the smallest width among those applicable.
414
+ if (limit.width < bestWidth) {
415
+ bestWidth = limit.width;
416
+ bestAttributeValue = getter(limit);
417
+ }
418
+ }
419
+ return bestAttributeValue;
549
420
  }
550
421
 
551
422
 
@@ -553,34 +424,34 @@ function parseMove(position: Position, move: MoveDescriptor | string | undefined
553
424
  * Try to interpret the given object as a list of markers.
554
425
  */
555
426
  function parseMarkers<K extends string, V>(markers: Partial<Record<K, V>> | string | undefined, argumentName: string, parse: (m: string) => Partial<Record<K, V>>,
556
- isValidKey: (k: unknown) => k is K, isValidValue: (v: unknown) => v is V): Partial<Record<K, V>> | undefined {
557
-
558
- if (markers === undefined || markers === null) {
559
- return undefined;
560
- }
561
- else if (typeof markers === 'string') {
562
- return parse(markers);
563
- }
564
- else if (typeof markers === 'object') {
565
- const result: Partial<Record<K, V>> = {};
566
- Object.entries(markers).forEach(([ key, value ]) => {
567
- if (isValidKey(key) && isValidValue(value)) {
568
- result[key] = value;
569
- }
570
- });
571
- return result;
572
- }
573
- else {
574
- throw new IllegalArgument('Chessboard', argumentName);
575
- }
427
+ isValidKey: (k: unknown) => k is K, isValidValue: (v: unknown) => v is V): Partial<Record<K, V>> | undefined {
428
+
429
+ if (markers === undefined || markers === null) {
430
+ return undefined;
431
+ }
432
+ else if (typeof markers === 'string') {
433
+ return parse(markers);
434
+ }
435
+ else if (typeof markers === 'object') {
436
+ const result: Partial<Record<K, V>> = {};
437
+ Object.entries(markers).forEach(([ key, value ]) => {
438
+ if (isValidKey(key) && isValidValue(value)) {
439
+ result[key] = value;
440
+ }
441
+ });
442
+ return result;
443
+ }
444
+ else {
445
+ throw new IllegalArgument('Chessboard', argumentName);
446
+ }
576
447
  }
577
448
 
578
449
 
579
450
  function isTextMarkerElement(value: unknown): value is { symbol: AnnotationSymbol, color: AnnotationColor } {
580
- if (typeof value !== 'object' || value === null) {
581
- return false;
582
- }
583
- return isAnnotationSymbol((value as Record<string, unknown>).symbol) && isAnnotationColor((value as Record<string, unknown>).color);
451
+ if (typeof value !== 'object' || value === null) {
452
+ return false;
453
+ }
454
+ return isAnnotationSymbol((value as Record<string, unknown>).symbol) && isAnnotationColor((value as Record<string, unknown>).color);
584
455
  }
585
456
 
586
457
 
@@ -588,23 +459,23 @@ function isTextMarkerElement(value: unknown): value is { symbol: AnnotationSymbo
588
459
  * Sanitization method for the small-screen limits parameter.
589
460
  */
590
461
  function sanitizeSmallScreenLimits(smallScreenLimits: SmallScreenLimit[] | undefined, exceptionBuilder: () => IllegalArgument): SmallScreenLimit[] {
591
- if (smallScreenLimits === undefined || smallScreenLimits === null) {
592
- return [];
593
- }
594
- else if (!Array.isArray(smallScreenLimits)) {
595
- throw exceptionBuilder();
596
- }
597
- else {
598
- return smallScreenLimits.map(smallScreenLimit => {
599
- if (typeof smallScreenLimit !== 'object' || smallScreenLimit === null) {
600
- throw exceptionBuilder();
601
- }
602
- return {
603
- width: sanitizeInteger(smallScreenLimit.width, exceptionBuilder),
604
- squareSize: sanitizeOptional(smallScreenLimit.squareSize, val => sanitizeBoundedInteger(val, MIN_SQUARE_SIZE, MAX_SQUARE_SIZE, exceptionBuilder)),
605
- coordinateVisible: sanitizeOptional(smallScreenLimit.coordinateVisible, sanitizeBoolean),
606
- turnVisible: sanitizeOptional(smallScreenLimit.turnVisible, sanitizeBoolean)
607
- };
608
- });
609
- }
462
+ if (smallScreenLimits === undefined || smallScreenLimits === null) {
463
+ return [];
464
+ }
465
+ else if (!Array.isArray(smallScreenLimits)) {
466
+ throw exceptionBuilder();
467
+ }
468
+ else {
469
+ return smallScreenLimits.map(smallScreenLimit => {
470
+ if (typeof smallScreenLimit !== 'object' || smallScreenLimit === null) {
471
+ throw exceptionBuilder();
472
+ }
473
+ return {
474
+ width: sanitizeInteger(smallScreenLimit.width, exceptionBuilder),
475
+ squareSize: sanitizeOptional(smallScreenLimit.squareSize, val => sanitizeBoundedInteger(val, MIN_SQUARE_SIZE, MAX_SQUARE_SIZE, exceptionBuilder)),
476
+ coordinateVisible: sanitizeOptional(smallScreenLimit.coordinateVisible, sanitizeBoolean),
477
+ turnVisible: sanitizeOptional(smallScreenLimit.turnVisible, sanitizeBoolean)
478
+ };
479
+ });
480
+ }
610
481
  }