cm-chessboard 8.11.4 → 8.12.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.
- package/.claude/settings.local.json +2 -1
- package/assets/chessboard.css +24 -0
- package/assets/extensions/promotion-dialog/promotion-dialog.css +25 -18
- package/assets/extensions/promotion-dialog/promotion-dialog.css.map +1 -9
- package/assets/extensions/promotion-dialog/promotion-dialog.scss +12 -0
- package/examples/extensions/accessibility-extension.html +9 -0
- package/examples/extensions/promotion-dialog-extension.html +15 -7
- package/index.html +3 -3
- package/package.json +1 -1
- package/src/extensions/accessibility/Accessibility.js +335 -3
- package/src/extensions/arrows/Arrows.js +15 -2
- package/src/extensions/auto-border-none/AutoBorderNone.js +4 -0
- package/src/extensions/html-layer/HtmlLayer.js +23 -2
- package/src/extensions/markers/Markers.js +46 -12
- package/src/extensions/promotion-dialog/PromotionDialog.js +72 -17
- package/src/model/Position.js +1 -1
- package/src/view/ChessboardView.js +19 -1
- package/test/TestChessboard.js +38 -0
- package/CLAUDE.md +0 -207
package/assets/chessboard.css
CHANGED
|
@@ -32,6 +32,12 @@
|
|
|
32
32
|
white-space: nowrap;
|
|
33
33
|
border: 0; }
|
|
34
34
|
|
|
35
|
+
.cm-chessboard-accessibility button:disabled,
|
|
36
|
+
.cm-chessboard-accessibility button:disabled:hover {
|
|
37
|
+
opacity: 0.5;
|
|
38
|
+
cursor: not-allowed;
|
|
39
|
+
pointer-events: none; }
|
|
40
|
+
|
|
35
41
|
.cm-chessboard.default .board .square.white {
|
|
36
42
|
fill: #ecdab9; }
|
|
37
43
|
|
|
@@ -291,4 +297,22 @@
|
|
|
291
297
|
.cm-chessboard.black-and-white .coordinates .coordinate.white {
|
|
292
298
|
fill: #909090; }
|
|
293
299
|
|
|
300
|
+
/* Keyboard focus indicator for accessibility */
|
|
301
|
+
.cm-chessboard .keyboard-focus-indicator .keyboard-focus {
|
|
302
|
+
fill: none;
|
|
303
|
+
stroke: #0066cc;
|
|
304
|
+
stroke-width: 3px;
|
|
305
|
+
pointer-events: none; }
|
|
306
|
+
|
|
307
|
+
.cm-chessboard .keyboard-focus-indicator .keyboard-from-square {
|
|
308
|
+
fill: rgba(0, 102, 204, 0.3);
|
|
309
|
+
stroke: #0066cc;
|
|
310
|
+
stroke-width: 2px;
|
|
311
|
+
pointer-events: none; }
|
|
312
|
+
|
|
313
|
+
/* High contrast focus indicator */
|
|
314
|
+
.cm-chessboard:focus {
|
|
315
|
+
outline: 2px solid #0066cc;
|
|
316
|
+
outline-offset: 2px; }
|
|
317
|
+
|
|
294
318
|
/*# sourceMappingURL=chessboard.css.map */
|
|
@@ -1,30 +1,37 @@
|
|
|
1
1
|
svg.cm-chessboard .promotion-dialog-group rect, svg.cm-chessboard .promotion-dialog-group g[data-piece] {
|
|
2
|
-
animation: fade-in 0.25s ease-in;
|
|
3
|
-
|
|
2
|
+
animation: fade-in 0.25s ease-in;
|
|
3
|
+
}
|
|
4
4
|
svg.cm-chessboard .promotion-dialog-group .promotion-dialog {
|
|
5
5
|
fill: white;
|
|
6
6
|
fill-opacity: 0.7;
|
|
7
|
-
stroke: rgba(0, 0, 0, 0.4);
|
|
8
|
-
|
|
7
|
+
stroke: rgba(0, 0, 0, 0.4);
|
|
8
|
+
}
|
|
9
|
+
svg.cm-chessboard .promotion-dialog-group .promotion-dialog-button-group {
|
|
10
|
+
outline: none;
|
|
11
|
+
}
|
|
12
|
+
svg.cm-chessboard .promotion-dialog-group .promotion-dialog-button-group:focus .promotion-dialog-button {
|
|
13
|
+
stroke: rgba(0, 0, 0, 0.3);
|
|
14
|
+
stroke-width: 1px;
|
|
15
|
+
}
|
|
9
16
|
svg.cm-chessboard .promotion-dialog-group .promotion-dialog-button {
|
|
10
17
|
fill: transparent;
|
|
11
|
-
cursor: pointer;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
svg.cm-chessboard .promotion-dialog-group .promotion-dialog-button-group:focus .promotion-dialog-button {
|
|
18
|
-
stroke: rgba(0, 0, 0, 0.3);
|
|
19
|
-
stroke-width: 2px; }
|
|
20
|
-
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
outline: none;
|
|
20
|
+
}
|
|
21
|
+
svg.cm-chessboard .promotion-dialog-group .promotion-dialog-button:hover {
|
|
22
|
+
fill: rgba(0, 0, 0, 0.2);
|
|
23
|
+
}
|
|
21
24
|
svg.cm-chessboard .promotion-dialog-group .piece {
|
|
22
|
-
pointer-events: none;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
}
|
|
23
27
|
|
|
24
28
|
@keyframes fade-in {
|
|
25
29
|
0% {
|
|
26
|
-
opacity: 0;
|
|
30
|
+
opacity: 0;
|
|
31
|
+
}
|
|
27
32
|
100% {
|
|
28
|
-
opacity: 1;
|
|
33
|
+
opacity: 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
29
36
|
|
|
30
|
-
/*# sourceMappingURL=promotion-dialog.css.map */
|
|
37
|
+
/*# sourceMappingURL=promotion-dialog.css.map */
|
|
@@ -1,9 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"file": "promotion-dialog.css",
|
|
4
|
-
"sources": [
|
|
5
|
-
"promotion-dialog.scss"
|
|
6
|
-
],
|
|
7
|
-
"names": [],
|
|
8
|
-
"mappings": "AAAA,AAGI,GAHD,AAAA,cAAc,CACf,uBAAuB,CAErB,IAAI,EAHR,GAAG,AAAA,cAAc,CACf,uBAAuB,CAEf,CAAC,CAAA,AAAA,UAAC,AAAA,EAAY;EAClB,SAAS,EAAE,qBAAqB,GACjC;;AALL,AAOI,GAPD,AAAA,cAAc,CACf,uBAAuB,CAMrB,iBAAiB,CAAC;EAChB,IAAI,EAAE,KAAK;EACX,YAAY,EAAE,GAAG;EACjB,MAAM,EAAE,kBAAkB,GAC3B;;AAXL,AAaI,GAbD,AAAA,cAAc,CACf,uBAAuB,CAYrB,wBAAwB,CAAC;EACvB,IAAI,EAAE,WAAW;EACjB,MAAM,EAAE,OAAO,GAKhB;EApBL,AAiBM,GAjBH,AAAA,cAAc,CACf,uBAAuB,CAYrB,wBAAwB,CAIpB,KAAK,CAAC;IACN,IAAI,EAAE,kBAAkB,GACzB;;AAnBP,AAsBI,GAtBD,AAAA,cAAc,CACf,uBAAuB,CAqBrB,MAAM,CAAC;EACL,cAAc,EAAE,IAAI,GACrB;;AAIL,UAAU,CAAV,OAAU;EACR,EAAE;IACA,OAAO,EAAE,CAAC;EAEZ,IAAI;IACF,OAAO,EAAE,CAAC"
|
|
9
|
-
}
|
|
1
|
+
{"version":3,"sourceRoot":"","sources":["promotion-dialog.scss"],"names":[],"mappings":"AAGI;EACE;;AAGF;EACE;EACA;EACA;;AAGF;EACE;;AACA;EACE;EACA;;AAGJ;EACE;EACA;EAMA;;AAJA;EACE;;AAOJ;EACE;;;AAON;EACE;IACE;;EAEF;IACE","file":"promotion-dialog.css"}
|
|
@@ -11,6 +11,13 @@ svg.cm-chessboard {
|
|
|
11
11
|
stroke: rgba(0, 0, 0, 0.4);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
.promotion-dialog-button-group {
|
|
15
|
+
outline: none;
|
|
16
|
+
&:focus .promotion-dialog-button {
|
|
17
|
+
stroke: rgba(0, 0, 0, 0.3);
|
|
18
|
+
stroke-width: 1px; }
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
.promotion-dialog-button {
|
|
15
22
|
fill: transparent;
|
|
16
23
|
cursor: pointer;
|
|
@@ -18,6 +25,9 @@ svg.cm-chessboard {
|
|
|
18
25
|
&:hover {
|
|
19
26
|
fill: rgba(0, 0, 0, 0.2)
|
|
20
27
|
}
|
|
28
|
+
|
|
29
|
+
outline: none;
|
|
30
|
+
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
.piece {
|
|
@@ -26,6 +36,8 @@ svg.cm-chessboard {
|
|
|
26
36
|
}
|
|
27
37
|
}
|
|
28
38
|
|
|
39
|
+
|
|
40
|
+
|
|
29
41
|
@keyframes fade-in {
|
|
30
42
|
0% {
|
|
31
43
|
opacity: 0;
|
|
@@ -26,8 +26,16 @@
|
|
|
26
26
|
<li>Display the chessboard additionally as HTML table</li>
|
|
27
27
|
<li>Display a form to move a piece (from, to, move)</li>
|
|
28
28
|
<li>Display the pieces additionally as List</li>
|
|
29
|
+
<li>Keyboard navigation on the board (arrow keys to navigate, Enter/Space to select, Escape to cancel)</li>
|
|
29
30
|
<li>Display these features visually hidden or not</li>
|
|
30
31
|
</ul>
|
|
32
|
+
<h3>Keyboard Navigation</h3>
|
|
33
|
+
<p>Click the board or press Tab to focus it, then use:</p>
|
|
34
|
+
<ul>
|
|
35
|
+
<li><strong>Arrow keys</strong> - Navigate between squares</li>
|
|
36
|
+
<li><strong>Enter / Space</strong> - Select a piece (first press) or target square (second press)</li>
|
|
37
|
+
<li><strong>Escape</strong> - Cancel the current move</li>
|
|
38
|
+
</ul>
|
|
31
39
|
<h3>The chessboard</h3>
|
|
32
40
|
<div class="board" id="board"></div>
|
|
33
41
|
<button style="margin-bottom: 10px"
|
|
@@ -53,6 +61,7 @@
|
|
|
53
61
|
boardAsTable: true, // display the board additionally as HTML table
|
|
54
62
|
movePieceForm: true, // display a form to move a piece (from, to, move)
|
|
55
63
|
piecesAsList: true, // display the pieces additionally as List
|
|
64
|
+
keyboardMoveInput: true, // enable keyboard navigation on the board with arrow keys
|
|
56
65
|
visuallyHidden: false // hide all those extra outputs visually but keep them accessible for screen readers and braille displays
|
|
57
66
|
}
|
|
58
67
|
|
|
@@ -20,20 +20,28 @@
|
|
|
20
20
|
onclick="window.switchOrientation()">Switch Orientation
|
|
21
21
|
</button>
|
|
22
22
|
<script type="module">
|
|
23
|
-
import {Chessboard, COLOR, INPUT_EVENT_TYPE} from "../../src/Chessboard.js"
|
|
24
|
-
import {PromotionDialog} from "../../src/extensions/promotion-dialog/PromotionDialog.js"
|
|
25
|
-
import {Markers} from "../../src/extensions/markers/Markers.js"
|
|
23
|
+
import {Chessboard, COLOR, INPUT_EVENT_TYPE} from "../../src/Chessboard.js?v=1"
|
|
24
|
+
import {PromotionDialog} from "../../src/extensions/promotion-dialog/PromotionDialog.js?v=1"
|
|
25
|
+
import {Markers} from "../../src/extensions/markers/Markers.js?v=1"
|
|
26
|
+
import {Accessibility} from "../../src/extensions/accessibility/Accessibility.js?v=1"
|
|
26
27
|
|
|
27
|
-
const position = "4k3/1P6/8/8/6r1/8/
|
|
28
|
+
const position = "4k3/1P6/8/8/6r1/8/61p/2R1K3 w - - 0 1"
|
|
28
29
|
const chessboard = new Chessboard(document.getElementById("chessboard"), {
|
|
29
30
|
position: position,
|
|
30
31
|
assetsUrl: "../../assets/",
|
|
31
|
-
extensions: [{class: PromotionDialog}, {class: Markers}
|
|
32
|
+
extensions: [{class: PromotionDialog}, {class: Markers}, {class: Accessibility, props: {
|
|
33
|
+
brailleNotationInAlt: true, // show the braille notation of the game in the alt attribute of the SVG
|
|
34
|
+
boardAsTable: true, // display the board additionally as HTML table
|
|
35
|
+
movePieceForm: true, // display a form to move a piece (from, to, move)
|
|
36
|
+
piecesAsList: true, // display the pieces additionally as List
|
|
37
|
+
visuallyHidden: false // hide all those extra outputs visually but keep them accessible for screen readers and braille displays
|
|
38
|
+
}}]
|
|
32
39
|
})
|
|
33
40
|
chessboard.enableMoveInput((event) => {
|
|
34
41
|
if (event.type === INPUT_EVENT_TYPE.validateMoveInput) {
|
|
35
|
-
if (event.squareTo.charAt(1) === "8" && event.piece.charAt(1) === "p") {
|
|
36
|
-
|
|
42
|
+
if ((event.squareTo.charAt(1) === "8" || event.squareTo.charAt(1) === "1") && event.piece.charAt(1) === "p") {
|
|
43
|
+
const pieceColor = event.piece.charAt(0)
|
|
44
|
+
chessboard.showPromotionDialog(event.squareTo, pieceColor, (result) => {
|
|
37
45
|
console.log("Promotion result", result)
|
|
38
46
|
if (result && result.piece) {
|
|
39
47
|
chessboard.setPiece(result.square, result.piece, true)
|
package/index.html
CHANGED
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
<li><a href="examples/extensions/arrows-extension.html">Arrows extension</a></li>
|
|
43
43
|
</ul>
|
|
44
44
|
</li>
|
|
45
|
-
<li>Draw HTML over the board with <a href="examples/extensions/html-layer-extension.html">HTML Layer extension</a></li>
|
|
46
|
-
<li>Allow displaying a promotion dialog with <a href="examples/extensions/promotion-dialog-extension.html">PromotionDialog extension</a></li>
|
|
47
|
-
<li>Make the board accessible
|
|
45
|
+
<li>Draw HTML over the board with the <a href="examples/extensions/html-layer-extension.html">HTML Layer extension</a></li>
|
|
46
|
+
<li>Allow displaying a promotion dialog with the <a href="examples/extensions/promotion-dialog-extension.html">PromotionDialog extension</a></li>
|
|
47
|
+
<li>Make the board accessible, add screen-reader support with the <a href="examples/extensions/accessibility-extension.html">Accessibility extension</a></li>
|
|
48
48
|
</ul>
|
|
49
49
|
</div>
|
|
50
50
|
<h2>Chessboard</h2>
|
package/package.json
CHANGED
|
@@ -8,6 +8,8 @@ import {COLOR, INPUT_EVENT_TYPE} from "../../Chessboard.js"
|
|
|
8
8
|
import {piecesTranslations, renderPieceTitle} from "./I18n.js"
|
|
9
9
|
import {Utils} from "../../lib/Utils.js"
|
|
10
10
|
|
|
11
|
+
import {Svg} from "../../lib/Svg.js"
|
|
12
|
+
|
|
11
13
|
const translations = {
|
|
12
14
|
de: {
|
|
13
15
|
chessboard: "Schachbrett",
|
|
@@ -21,6 +23,10 @@ const translations = {
|
|
|
21
23
|
input_black_enabled: "Eingabe Schwarz aktiviert",
|
|
22
24
|
input_disabled: "Eingabe deaktiviert",
|
|
23
25
|
pieces: "Figuren",
|
|
26
|
+
empty_square: "leer",
|
|
27
|
+
move_from: "Zug von",
|
|
28
|
+
move_to: "Zug nach",
|
|
29
|
+
move_canceled: "Zug abgebrochen"
|
|
24
30
|
},
|
|
25
31
|
en: {
|
|
26
32
|
chessboard: "Chessboard",
|
|
@@ -33,7 +39,11 @@ const translations = {
|
|
|
33
39
|
input_white_enabled: "Input white enabled",
|
|
34
40
|
input_black_enabled: "Input black enabled",
|
|
35
41
|
input_disabled: "Input disabled",
|
|
36
|
-
pieces: "Pieces"
|
|
42
|
+
pieces: "Pieces",
|
|
43
|
+
empty_square: "empty",
|
|
44
|
+
move_from: "Move from",
|
|
45
|
+
move_to: "Move to",
|
|
46
|
+
move_canceled: "Move canceled"
|
|
37
47
|
}
|
|
38
48
|
}
|
|
39
49
|
|
|
@@ -46,6 +56,7 @@ export class Accessibility extends Extension {
|
|
|
46
56
|
movePieceForm: true, // display a form to move a piece (from, to, move)
|
|
47
57
|
boardAsTable: true, // display the board additionally as HTML table
|
|
48
58
|
piecesAsList: true, // display the pieces additionally as List
|
|
59
|
+
keyboardMoveInput: true, // enable keyboard navigation on the board with arrow keys
|
|
49
60
|
visuallyHidden: true // hide all those extra outputs visually but keep them accessible for screen readers and braille displays
|
|
50
61
|
}
|
|
51
62
|
Object.assign(this.props, props)
|
|
@@ -76,6 +87,9 @@ export class Accessibility extends Extension {
|
|
|
76
87
|
if (this.props.brailleNotationInAlt) {
|
|
77
88
|
this.components.push(new BrailleNotationInAlt(this))
|
|
78
89
|
}
|
|
90
|
+
if (this.props.keyboardMoveInput) {
|
|
91
|
+
this.components.push(new KeyboardMoveInput(this))
|
|
92
|
+
}
|
|
79
93
|
}
|
|
80
94
|
}
|
|
81
95
|
|
|
@@ -135,6 +149,7 @@ class MovePieceForm {
|
|
|
135
149
|
true).then(() => {
|
|
136
150
|
this.inputFrom.value = ""
|
|
137
151
|
this.inputTo.value = ""
|
|
152
|
+
this.updateButtonState()
|
|
138
153
|
})
|
|
139
154
|
}
|
|
140
155
|
})
|
|
@@ -142,6 +157,13 @@ class MovePieceForm {
|
|
|
142
157
|
extension.registerExtensionPoint(EXTENSION_POINT.moveInputToggled, () => {
|
|
143
158
|
this.redraw()
|
|
144
159
|
})
|
|
160
|
+
// Update button state when input values change
|
|
161
|
+
this.inputFrom.addEventListener("input", () => {
|
|
162
|
+
this.updateButtonState()
|
|
163
|
+
})
|
|
164
|
+
this.inputTo.addEventListener("input", () => {
|
|
165
|
+
this.updateButtonState()
|
|
166
|
+
})
|
|
145
167
|
this.keydownListener = (event) => {
|
|
146
168
|
if (event.shiftKey && event.altKey && event.code === 'KeyE') {
|
|
147
169
|
event.preventDefault()
|
|
@@ -152,6 +174,25 @@ class MovePieceForm {
|
|
|
152
174
|
extension.registerExtensionPoint(EXTENSION_POINT.destroy, () => {
|
|
153
175
|
document.removeEventListener("keydown", this.keydownListener)
|
|
154
176
|
})
|
|
177
|
+
// Initial button state
|
|
178
|
+
this.updateButtonState()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
isValidSquare(value) {
|
|
182
|
+
const square = value.trim().toLowerCase()
|
|
183
|
+
if (square.length !== 2) {
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
const file = square.charAt(0)
|
|
187
|
+
const rank = square.charAt(1)
|
|
188
|
+
return file >= 'a' && file <= 'h' && rank >= '1' && rank <= '8'
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
updateButtonState() {
|
|
192
|
+
const inputEnabled = this.chessboard.state.inputWhiteEnabled || this.chessboard.state.inputBlackEnabled
|
|
193
|
+
const isValidFrom = this.isValidSquare(this.inputFrom.value)
|
|
194
|
+
const isValidTo = this.isValidSquare(this.inputTo.value)
|
|
195
|
+
this.moveButton.disabled = !inputEnabled || !isValidFrom || !isValidTo
|
|
155
196
|
}
|
|
156
197
|
|
|
157
198
|
redraw() {
|
|
@@ -159,12 +200,11 @@ class MovePieceForm {
|
|
|
159
200
|
if (this.chessboard.state.inputWhiteEnabled || this.chessboard.state.inputBlackEnabled) {
|
|
160
201
|
this.inputFrom.disabled = false
|
|
161
202
|
this.inputTo.disabled = false
|
|
162
|
-
this.moveButton.disabled = false
|
|
163
203
|
} else {
|
|
164
204
|
this.inputFrom.disabled = true
|
|
165
205
|
this.inputTo.disabled = true
|
|
166
|
-
this.moveButton.disabled = true
|
|
167
206
|
}
|
|
207
|
+
this.updateButtonState()
|
|
168
208
|
}
|
|
169
209
|
}
|
|
170
210
|
}
|
|
@@ -248,3 +288,295 @@ class PiecesAsList {
|
|
|
248
288
|
<ul aria-labelledby="black_${this.chessboard.id}" class="list-inline">${listB}</ul>`
|
|
249
289
|
}
|
|
250
290
|
}
|
|
291
|
+
|
|
292
|
+
class KeyboardMoveInput {
|
|
293
|
+
constructor(extension) {
|
|
294
|
+
this.extension = extension
|
|
295
|
+
this.chessboard = extension.chessboard
|
|
296
|
+
this.t = extension.t
|
|
297
|
+
this.tPieces = extension.tPieces
|
|
298
|
+
|
|
299
|
+
// Current focus position (file 0-7, rank 0-7)
|
|
300
|
+
this.focusedFile = 0 // a-h
|
|
301
|
+
this.focusedRank = 0 // 1-8 (0 = rank 1)
|
|
302
|
+
|
|
303
|
+
// Move selection state
|
|
304
|
+
this.fromSquare = null
|
|
305
|
+
|
|
306
|
+
// Create focus indicator group in SVG
|
|
307
|
+
this.focusIndicatorGroup = Svg.addElement(
|
|
308
|
+
this.chessboard.view.markersTopLayer,
|
|
309
|
+
"g",
|
|
310
|
+
{class: "keyboard-focus-indicator"}
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
// Create live region for screen reader announcements
|
|
314
|
+
this.liveRegion = document.createElement("div")
|
|
315
|
+
this.liveRegion.setAttribute("aria-live", "polite")
|
|
316
|
+
this.liveRegion.setAttribute("aria-atomic", "true")
|
|
317
|
+
this.liveRegion.className = "cm-chessboard-keyboard-live-region visually-hidden"
|
|
318
|
+
this.liveRegion.style.cssText = "position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;"
|
|
319
|
+
this.chessboard.context.appendChild(this.liveRegion)
|
|
320
|
+
|
|
321
|
+
// Make SVG focusable
|
|
322
|
+
this.chessboard.view.svg.setAttribute("tabindex", "0")
|
|
323
|
+
this.chessboard.view.svg.setAttribute("role", "application")
|
|
324
|
+
this.chessboard.view.svg.setAttribute("aria-label", this.t.chessboard)
|
|
325
|
+
|
|
326
|
+
// Bind event handlers
|
|
327
|
+
this.handleKeyDown = this.handleKeyDown.bind(this)
|
|
328
|
+
this.handleFocus = this.handleFocus.bind(this)
|
|
329
|
+
this.handleBlur = this.handleBlur.bind(this)
|
|
330
|
+
|
|
331
|
+
this.chessboard.view.svg.addEventListener("keydown", this.handleKeyDown)
|
|
332
|
+
this.chessboard.view.svg.addEventListener("focus", this.handleFocus)
|
|
333
|
+
this.chessboard.view.svg.addEventListener("blur", this.handleBlur)
|
|
334
|
+
|
|
335
|
+
// Register extension points
|
|
336
|
+
extension.registerExtensionPoint(EXTENSION_POINT.afterRedrawBoard, () => {
|
|
337
|
+
this.redrawFocusIndicator()
|
|
338
|
+
})
|
|
339
|
+
extension.registerExtensionPoint(EXTENSION_POINT.destroy, () => {
|
|
340
|
+
this.destroy()
|
|
341
|
+
})
|
|
342
|
+
extension.registerExtensionPoint(EXTENSION_POINT.moveInputToggled, () => {
|
|
343
|
+
// Reset move selection when input is toggled
|
|
344
|
+
this.fromSquare = null
|
|
345
|
+
this.redrawFocusIndicator()
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
handleFocus() {
|
|
350
|
+
this.redrawFocusIndicator()
|
|
351
|
+
this.announceCurrentSquare()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
handleBlur() {
|
|
355
|
+
this.clearFocusIndicator()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
handleKeyDown(event) {
|
|
359
|
+
const orientation = this.chessboard.state.orientation
|
|
360
|
+
|
|
361
|
+
switch (event.key) {
|
|
362
|
+
case "ArrowRight":
|
|
363
|
+
event.preventDefault()
|
|
364
|
+
if (orientation === COLOR.white) {
|
|
365
|
+
this.focusedFile = Math.min(7, this.focusedFile + 1)
|
|
366
|
+
} else {
|
|
367
|
+
this.focusedFile = Math.max(0, this.focusedFile - 1)
|
|
368
|
+
}
|
|
369
|
+
this.redrawFocusIndicator()
|
|
370
|
+
this.announceCurrentSquare()
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
case "ArrowLeft":
|
|
374
|
+
event.preventDefault()
|
|
375
|
+
if (orientation === COLOR.white) {
|
|
376
|
+
this.focusedFile = Math.max(0, this.focusedFile - 1)
|
|
377
|
+
} else {
|
|
378
|
+
this.focusedFile = Math.min(7, this.focusedFile + 1)
|
|
379
|
+
}
|
|
380
|
+
this.redrawFocusIndicator()
|
|
381
|
+
this.announceCurrentSquare()
|
|
382
|
+
break
|
|
383
|
+
|
|
384
|
+
case "ArrowUp":
|
|
385
|
+
event.preventDefault()
|
|
386
|
+
if (orientation === COLOR.white) {
|
|
387
|
+
this.focusedRank = Math.min(7, this.focusedRank + 1)
|
|
388
|
+
} else {
|
|
389
|
+
this.focusedRank = Math.max(0, this.focusedRank - 1)
|
|
390
|
+
}
|
|
391
|
+
this.redrawFocusIndicator()
|
|
392
|
+
this.announceCurrentSquare()
|
|
393
|
+
break
|
|
394
|
+
|
|
395
|
+
case "ArrowDown":
|
|
396
|
+
event.preventDefault()
|
|
397
|
+
if (orientation === COLOR.white) {
|
|
398
|
+
this.focusedRank = Math.max(0, this.focusedRank - 1)
|
|
399
|
+
} else {
|
|
400
|
+
this.focusedRank = Math.min(7, this.focusedRank + 1)
|
|
401
|
+
}
|
|
402
|
+
this.redrawFocusIndicator()
|
|
403
|
+
this.announceCurrentSquare()
|
|
404
|
+
break
|
|
405
|
+
|
|
406
|
+
case "Enter":
|
|
407
|
+
case " ":
|
|
408
|
+
event.preventDefault()
|
|
409
|
+
this.selectSquare()
|
|
410
|
+
break
|
|
411
|
+
|
|
412
|
+
case "Escape":
|
|
413
|
+
event.preventDefault()
|
|
414
|
+
if (this.fromSquare) {
|
|
415
|
+
this.fromSquare = null
|
|
416
|
+
this.redrawFocusIndicator()
|
|
417
|
+
this.announce(this.t.move_canceled)
|
|
418
|
+
}
|
|
419
|
+
break
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
selectSquare() {
|
|
424
|
+
if (!this.chessboard.state.inputWhiteEnabled && !this.chessboard.state.inputBlackEnabled) {
|
|
425
|
+
return // Input not enabled
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const square = this.getCurrentSquare()
|
|
429
|
+
const piece = this.chessboard.getPiece(square)
|
|
430
|
+
|
|
431
|
+
if (!this.fromSquare) {
|
|
432
|
+
// Selecting "from" square
|
|
433
|
+
if (piece) {
|
|
434
|
+
const pieceColor = piece.charAt(0)
|
|
435
|
+
// Check if the piece color matches enabled input
|
|
436
|
+
if ((pieceColor === "w" && this.chessboard.state.inputWhiteEnabled) ||
|
|
437
|
+
(pieceColor === "b" && this.chessboard.state.inputBlackEnabled)) {
|
|
438
|
+
// Trigger moveInputStarted callback
|
|
439
|
+
const startResult = this.chessboard.state.moveInputCallback({
|
|
440
|
+
chessboard: this.chessboard,
|
|
441
|
+
type: INPUT_EVENT_TYPE.moveInputStarted,
|
|
442
|
+
square: square,
|
|
443
|
+
squareFrom: square,
|
|
444
|
+
piece: piece
|
|
445
|
+
})
|
|
446
|
+
if (startResult) {
|
|
447
|
+
this.fromSquare = square
|
|
448
|
+
this.redrawFocusIndicator()
|
|
449
|
+
this.announce(this.t.move_from + " " + square)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
// Selecting "to" square
|
|
455
|
+
if (square === this.fromSquare) {
|
|
456
|
+
// Clicking same square cancels
|
|
457
|
+
this.fromSquare = null
|
|
458
|
+
this.redrawFocusIndicator()
|
|
459
|
+
this.announce(this.t.move_canceled)
|
|
460
|
+
} else {
|
|
461
|
+
// Try to make the move
|
|
462
|
+
const fromPiece = this.chessboard.getPiece(this.fromSquare)
|
|
463
|
+
const result = this.chessboard.state.moveInputCallback({
|
|
464
|
+
chessboard: this.chessboard,
|
|
465
|
+
type: INPUT_EVENT_TYPE.validateMoveInput,
|
|
466
|
+
squareFrom: this.fromSquare,
|
|
467
|
+
squareTo: square,
|
|
468
|
+
piece: fromPiece
|
|
469
|
+
})
|
|
470
|
+
if (result) {
|
|
471
|
+
const fromSquare = this.fromSquare
|
|
472
|
+
this.fromSquare = null
|
|
473
|
+
this.chessboard.movePiece(fromSquare, square, true).then(() => {
|
|
474
|
+
this.redrawFocusIndicator()
|
|
475
|
+
})
|
|
476
|
+
this.announce(this.t.move_to + " " + square)
|
|
477
|
+
} else {
|
|
478
|
+
// Invalid move - check if clicking on own piece to start new move
|
|
479
|
+
if (piece) {
|
|
480
|
+
const pieceColor = piece.charAt(0)
|
|
481
|
+
if ((pieceColor === "w" && this.chessboard.state.inputWhiteEnabled) ||
|
|
482
|
+
(pieceColor === "b" && this.chessboard.state.inputBlackEnabled)) {
|
|
483
|
+
const startResult = this.chessboard.state.moveInputCallback({
|
|
484
|
+
chessboard: this.chessboard,
|
|
485
|
+
type: INPUT_EVENT_TYPE.moveInputStarted,
|
|
486
|
+
square: square,
|
|
487
|
+
squareFrom: square,
|
|
488
|
+
piece: piece
|
|
489
|
+
})
|
|
490
|
+
if (startResult) {
|
|
491
|
+
this.fromSquare = square
|
|
492
|
+
this.redrawFocusIndicator()
|
|
493
|
+
this.announce(this.t.move_from + " " + square)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
getCurrentSquare() {
|
|
503
|
+
const file = String.fromCharCode(97 + this.focusedFile) // 'a' + file
|
|
504
|
+
const rank = this.focusedRank + 1
|
|
505
|
+
return `${file}${rank}`
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
announceCurrentSquare() {
|
|
509
|
+
const square = this.getCurrentSquare()
|
|
510
|
+
const piece = this.chessboard.getPiece(square)
|
|
511
|
+
let announcement = square
|
|
512
|
+
if (piece) {
|
|
513
|
+
const pieceType = piece.charAt(1)
|
|
514
|
+
const pieceColor = piece.charAt(0)
|
|
515
|
+
announcement += " " + renderPieceTitle(this.extension.lang, pieceType, pieceColor)
|
|
516
|
+
} else {
|
|
517
|
+
announcement += " " + this.t.empty_square
|
|
518
|
+
}
|
|
519
|
+
if (this.fromSquare) {
|
|
520
|
+
announcement += " (" + this.t.move_from + " " + this.fromSquare + ")"
|
|
521
|
+
}
|
|
522
|
+
this.announce(announcement)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
announce(message) {
|
|
526
|
+
this.liveRegion.textContent = ""
|
|
527
|
+
setTimeout(() => {
|
|
528
|
+
this.liveRegion.textContent = message
|
|
529
|
+
}, 50)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
redrawFocusIndicator() {
|
|
533
|
+
this.clearFocusIndicator()
|
|
534
|
+
|
|
535
|
+
// Only show if SVG is focused
|
|
536
|
+
if (document.activeElement !== this.chessboard.view.svg) {
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const square = this.getCurrentSquare()
|
|
541
|
+
const point = this.chessboard.view.squareToPoint(square)
|
|
542
|
+
const squareWidth = this.chessboard.view.squareWidth
|
|
543
|
+
const squareHeight = this.chessboard.view.squareHeight
|
|
544
|
+
|
|
545
|
+
// Draw focus indicator
|
|
546
|
+
Svg.addElement(this.focusIndicatorGroup, "rect", {
|
|
547
|
+
x: point.x,
|
|
548
|
+
y: point.y,
|
|
549
|
+
width: squareWidth,
|
|
550
|
+
height: squareHeight,
|
|
551
|
+
class: "keyboard-focus"
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
// If we have a from square selected, also highlight it
|
|
555
|
+
if (this.fromSquare) {
|
|
556
|
+
const fromPoint = this.chessboard.view.squareToPoint(this.fromSquare)
|
|
557
|
+
Svg.addElement(this.focusIndicatorGroup, "rect", {
|
|
558
|
+
x: fromPoint.x,
|
|
559
|
+
y: fromPoint.y,
|
|
560
|
+
width: squareWidth,
|
|
561
|
+
height: squareHeight,
|
|
562
|
+
class: "keyboard-from-square"
|
|
563
|
+
})
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
clearFocusIndicator() {
|
|
568
|
+
while (this.focusIndicatorGroup.firstChild) {
|
|
569
|
+
this.focusIndicatorGroup.removeChild(this.focusIndicatorGroup.firstChild)
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
destroy() {
|
|
574
|
+
this.chessboard.view.svg.removeEventListener("keydown", this.handleKeyDown)
|
|
575
|
+
this.chessboard.view.svg.removeEventListener("focus", this.handleFocus)
|
|
576
|
+
this.chessboard.view.svg.removeEventListener("blur", this.handleBlur)
|
|
577
|
+
if (this.liveRegion && this.liveRegion.parentNode) {
|
|
578
|
+
this.liveRegion.parentNode.removeChild(this.liveRegion)
|
|
579
|
+
}
|
|
580
|
+
this.clearFocusIndicator()
|
|
581
|
+
}
|
|
582
|
+
}
|
|
@@ -25,6 +25,9 @@ export class Arrows extends Extension {
|
|
|
25
25
|
this.registerExtensionPoint(EXTENSION_POINT.afterRedrawBoard, () => {
|
|
26
26
|
this.onRedrawBoard()
|
|
27
27
|
})
|
|
28
|
+
this.registerExtensionPoint(EXTENSION_POINT.destroy, () => {
|
|
29
|
+
this.onDestroy()
|
|
30
|
+
})
|
|
28
31
|
this.props = {
|
|
29
32
|
sprite: "extensions/arrows/arrows.svg",
|
|
30
33
|
slice: "arrowDefault",
|
|
@@ -43,6 +46,16 @@ export class Arrows extends Extension {
|
|
|
43
46
|
this.arrows = []
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
onDestroy() {
|
|
50
|
+
this.arrows.length = 0
|
|
51
|
+
if (this.arrowGroup && this.arrowGroup.parentNode) {
|
|
52
|
+
this.arrowGroup.parentNode.removeChild(this.arrowGroup)
|
|
53
|
+
}
|
|
54
|
+
delete this.chessboard.addArrow
|
|
55
|
+
delete this.chessboard.getArrows
|
|
56
|
+
delete this.chessboard.removeArrows
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
onRedrawBoard() {
|
|
47
60
|
while (this.arrowGroup.firstChild) {
|
|
48
61
|
this.arrowGroup.removeChild(this.arrowGroup.firstChild)
|
|
@@ -112,7 +125,7 @@ export class Arrows extends Extension {
|
|
|
112
125
|
|
|
113
126
|
addArrow(type, from, to) {
|
|
114
127
|
this.arrows.push(new Arrow(from, to, type))
|
|
115
|
-
this.
|
|
128
|
+
this.onRedrawBoard()
|
|
116
129
|
}
|
|
117
130
|
|
|
118
131
|
getArrows(type = undefined, from = undefined, to = undefined) {
|
|
@@ -127,7 +140,7 @@ export class Arrows extends Extension {
|
|
|
127
140
|
|
|
128
141
|
removeArrows(type = undefined, from = undefined, to = undefined) {
|
|
129
142
|
this.arrows = this.arrows.filter((arrow) => !arrow.matches(from, to, type))
|
|
130
|
-
this.
|
|
143
|
+
this.onRedrawBoard()
|
|
131
144
|
}
|
|
132
145
|
|
|
133
146
|
getSpriteUrl() {
|
|
@@ -8,12 +8,16 @@ import {Extension, EXTENSION_POINT} from "../../model/Extension.js"
|
|
|
8
8
|
export class AutoBorderNone extends Extension {
|
|
9
9
|
constructor(chessboard, props = {}) {
|
|
10
10
|
super(chessboard)
|
|
11
|
+
this.originalBorderType = chessboard.props.style.borderType
|
|
11
12
|
this.props = {
|
|
12
13
|
chessboardBorderType: chessboard.props.style.borderType,
|
|
13
14
|
borderNoneBelow: 540 // pixels width of the board, where the border is set to none
|
|
14
15
|
}
|
|
15
16
|
Object.assign(this.props, props)
|
|
16
17
|
this.registerExtensionPoint(EXTENSION_POINT.beforeRedrawBoard, this.extensionPointBeforeRedrawBoard.bind(this))
|
|
18
|
+
this.registerExtensionPoint(EXTENSION_POINT.destroy, () => {
|
|
19
|
+
this.chessboard.props.style.borderType = this.originalBorderType
|
|
20
|
+
})
|
|
17
21
|
}
|
|
18
22
|
extensionPointBeforeRedrawBoard() {
|
|
19
23
|
let newBorderType
|
|
@@ -3,14 +3,18 @@
|
|
|
3
3
|
* Repository: https://github.com/shaack/cm-chessboard
|
|
4
4
|
* License: MIT, see file 'LICENSE'
|
|
5
5
|
*/
|
|
6
|
-
import {Extension} from "../../model/Extension.js"
|
|
6
|
+
import {Extension, EXTENSION_POINT} from "../../model/Extension.js"
|
|
7
7
|
|
|
8
8
|
export class HtmlLayer extends Extension {
|
|
9
9
|
/** @constructor */
|
|
10
10
|
constructor(chessboard) {
|
|
11
11
|
super(chessboard)
|
|
12
|
+
this.layers = []
|
|
12
13
|
chessboard.addHtmlLayer = this.addHtmlLayer.bind(this)
|
|
13
14
|
chessboard.removeHtmlLayer = this.removeHtmlLayer.bind(this)
|
|
15
|
+
this.registerExtensionPoint(EXTENSION_POINT.destroy, () => {
|
|
16
|
+
this.onDestroy()
|
|
17
|
+
})
|
|
14
18
|
}
|
|
15
19
|
addHtmlLayer(html) {
|
|
16
20
|
const layer = document.createElement("div")
|
|
@@ -23,9 +27,26 @@ export class HtmlLayer extends Extension {
|
|
|
23
27
|
layer.style.bottom = "0"
|
|
24
28
|
layer.style.right = "0"
|
|
25
29
|
layer.innerHTML = html
|
|
30
|
+
this.layers.push(layer)
|
|
26
31
|
return layer
|
|
27
32
|
}
|
|
28
33
|
removeHtmlLayer(layer) {
|
|
29
|
-
this.
|
|
34
|
+
const index = this.layers.indexOf(layer)
|
|
35
|
+
if (index !== -1) {
|
|
36
|
+
this.layers.splice(index, 1)
|
|
37
|
+
}
|
|
38
|
+
if (layer && layer.parentNode === this.chessboard.context) {
|
|
39
|
+
this.chessboard.context.removeChild(layer)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
onDestroy() {
|
|
43
|
+
for (const layer of this.layers) {
|
|
44
|
+
if (layer.parentNode) {
|
|
45
|
+
layer.parentNode.removeChild(layer)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
this.layers.length = 0
|
|
49
|
+
delete this.chessboard.addHtmlLayer
|
|
50
|
+
delete this.chessboard.removeHtmlLayer
|
|
30
51
|
}
|
|
31
52
|
}
|
|
@@ -29,6 +29,9 @@ export class Markers extends Extension {
|
|
|
29
29
|
this.registerExtensionPoint(EXTENSION_POINT.afterRedrawBoard, () => {
|
|
30
30
|
this.onRedrawBoard()
|
|
31
31
|
})
|
|
32
|
+
this.registerExtensionPoint(EXTENSION_POINT.destroy, () => {
|
|
33
|
+
this.onDestroy()
|
|
34
|
+
})
|
|
32
35
|
this.props = {
|
|
33
36
|
autoMarkers: MARKER_TYPE.frame, // set to `null` to disable autoMarkers
|
|
34
37
|
sprite: "extensions/markers/markers.svg" // the sprite file of the markers
|
|
@@ -53,6 +56,21 @@ export class Markers extends Extension {
|
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
onDestroy() {
|
|
60
|
+
this.markers.length = 0
|
|
61
|
+
if (this.markerGroupDown && this.markerGroupDown.parentNode) {
|
|
62
|
+
this.markerGroupDown.parentNode.removeChild(this.markerGroupDown)
|
|
63
|
+
}
|
|
64
|
+
if (this.markerGroupUp && this.markerGroupUp.parentNode) {
|
|
65
|
+
this.markerGroupUp.parentNode.removeChild(this.markerGroupUp)
|
|
66
|
+
}
|
|
67
|
+
delete this.chessboard.addMarker
|
|
68
|
+
delete this.chessboard.getMarkers
|
|
69
|
+
delete this.chessboard.removeMarkers
|
|
70
|
+
delete this.chessboard.addLegalMovesMarkers
|
|
71
|
+
delete this.chessboard.removeLegalMovesMarkers
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
drawAutoMarkers(event) {
|
|
57
75
|
if(event.type !== INPUT_EVENT_TYPE.moveInputFinished) {
|
|
58
76
|
this.removeMarkers(this.props.autoMarkers)
|
|
@@ -86,21 +104,33 @@ export class Markers extends Extension {
|
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
addLegalMovesMarkers(moves) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
this.batchUpdate = true
|
|
108
|
+
try {
|
|
109
|
+
for (const move of moves) {
|
|
110
|
+
if (move.promotion && move.promotion !== "q") {
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
if (this.chessboard.getPiece(move.to)) {
|
|
114
|
+
this.chessboard.addMarker(MARKER_TYPE.bevel, move.to)
|
|
115
|
+
} else {
|
|
116
|
+
this.chessboard.addMarker(MARKER_TYPE.dot, move.to)
|
|
117
|
+
}
|
|
97
118
|
}
|
|
119
|
+
} finally {
|
|
120
|
+
this.batchUpdate = false
|
|
121
|
+
this.onRedrawBoard()
|
|
98
122
|
}
|
|
99
123
|
}
|
|
100
124
|
|
|
101
125
|
removeLegalMovesMarkers() {
|
|
102
|
-
this.
|
|
103
|
-
|
|
126
|
+
this.batchUpdate = true
|
|
127
|
+
try {
|
|
128
|
+
this.chessboard.removeMarkers(MARKER_TYPE.bevel)
|
|
129
|
+
this.chessboard.removeMarkers(MARKER_TYPE.dot)
|
|
130
|
+
} finally {
|
|
131
|
+
this.batchUpdate = false
|
|
132
|
+
this.onRedrawBoard()
|
|
133
|
+
}
|
|
104
134
|
}
|
|
105
135
|
|
|
106
136
|
drawMarker(marker) {
|
|
@@ -130,7 +160,9 @@ export class Markers extends Extension {
|
|
|
130
160
|
return
|
|
131
161
|
}
|
|
132
162
|
this.markers.push(new Marker(square, type))
|
|
133
|
-
this.
|
|
163
|
+
if (!this.batchUpdate) {
|
|
164
|
+
this.onRedrawBoard()
|
|
165
|
+
}
|
|
134
166
|
}
|
|
135
167
|
|
|
136
168
|
getMarkers(type = undefined, square = undefined) {
|
|
@@ -153,7 +185,9 @@ export class Markers extends Extension {
|
|
|
153
185
|
return
|
|
154
186
|
}
|
|
155
187
|
this.markers = this.markers.filter((marker) => !marker.matches(square, type))
|
|
156
|
-
this.
|
|
188
|
+
if (!this.batchUpdate) {
|
|
189
|
+
this.onRedrawBoard()
|
|
190
|
+
}
|
|
157
191
|
}
|
|
158
192
|
|
|
159
193
|
getSpriteUrl() {
|
|
@@ -91,14 +91,16 @@ export class PromotionDialog extends Extension {
|
|
|
91
91
|
this.state.dialogParams.color = color
|
|
92
92
|
this.state.callback = callback
|
|
93
93
|
this.setDisplayState(DISPLAY_STATE.displayRequested)
|
|
94
|
-
setTimeout(() => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
this.showTimeoutId = setTimeout(() => {
|
|
95
|
+
this.showTimeoutId = null
|
|
96
|
+
if (!this.chessboard.view) return // destroyed before timeout fired
|
|
97
|
+
this.chessboard.view.positionsAnimationTask.then(() => {
|
|
98
|
+
if (this.state.displayState !== DISPLAY_STATE.displayRequested) return
|
|
99
|
+
this.setDisplayState(DISPLAY_STATE.shown)
|
|
100
|
+
this.announce(this.t.choosePromotion + ": " +
|
|
101
|
+
this.pieceOrder.map(p => this.t.pieces[p]).join(", "))
|
|
102
|
+
})
|
|
103
|
+
})
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
// public (chessboard.isPromotionDialogShown)
|
|
@@ -144,12 +146,13 @@ export class PromotionDialog extends Extension {
|
|
|
144
146
|
const squareCenterPoint = this.chessboard.view.squareToPoint(this.state.dialogParams.square)
|
|
145
147
|
squareCenterPoint.x = squareCenterPoint.x + squareWidth / 2
|
|
146
148
|
squareCenterPoint.y = squareCenterPoint.y + squareHeight / 2
|
|
147
|
-
|
|
149
|
+
this.turned = false
|
|
148
150
|
const rank = parseInt(this.state.dialogParams.square.charAt(1), 10)
|
|
149
151
|
if (this.chessboard.getOrientation() === COLOR.white && rank < 5 ||
|
|
150
152
|
this.chessboard.getOrientation() === COLOR.black && rank >= 5) {
|
|
151
|
-
turned = true
|
|
153
|
+
this.turned = true
|
|
152
154
|
}
|
|
155
|
+
const turned = this.turned
|
|
153
156
|
const offsetY = turned ? -4 * squareHeight : 0
|
|
154
157
|
const offsetX = squareCenterPoint.x + squareWidth > this.chessboard.view.width ? -squareWidth : 0
|
|
155
158
|
Svg.addElement(this.promotionDialogGroup,
|
|
@@ -247,6 +250,7 @@ export class PromotionDialog extends Extension {
|
|
|
247
250
|
}
|
|
248
251
|
|
|
249
252
|
setDisplayState(displayState) {
|
|
253
|
+
const prevState = this.state.displayState
|
|
250
254
|
this.state.displayState = displayState
|
|
251
255
|
if (displayState === DISPLAY_STATE.shown) {
|
|
252
256
|
this.clickDelegate = Utils.delegate(this.chessboard.view.svg,
|
|
@@ -258,19 +262,27 @@ export class PromotionDialog extends Extension {
|
|
|
258
262
|
// Add keyboard listener
|
|
259
263
|
document.addEventListener("keydown", this.handleKeyDown)
|
|
260
264
|
} else if (displayState === DISPLAY_STATE.hidden) {
|
|
261
|
-
this.clickDelegate
|
|
262
|
-
|
|
265
|
+
if (this.clickDelegate) {
|
|
266
|
+
this.clickDelegate.remove()
|
|
267
|
+
this.clickDelegate = null
|
|
268
|
+
}
|
|
269
|
+
if (this.contextMenuListener && this.chessboard.view) {
|
|
270
|
+
this.chessboard.view.svg.removeEventListener("contextmenu", this.contextMenuListener)
|
|
271
|
+
this.contextMenuListener = null
|
|
272
|
+
}
|
|
263
273
|
// Remove keyboard listener
|
|
264
274
|
document.removeEventListener("keydown", this.handleKeyDown)
|
|
265
|
-
// Restore focus
|
|
266
|
-
if (
|
|
275
|
+
// Restore focus (only if the dialog was actually shown before)
|
|
276
|
+
if (prevState === DISPLAY_STATE.shown &&
|
|
277
|
+
this.previouslyFocusedElement && this.previouslyFocusedElement.focus) {
|
|
267
278
|
this.previouslyFocusedElement.focus()
|
|
268
279
|
}
|
|
269
280
|
}
|
|
270
281
|
this.redrawDialog()
|
|
271
282
|
// Focus first button after redraw when shown
|
|
272
283
|
if (displayState === DISPLAY_STATE.shown) {
|
|
273
|
-
setTimeout(() => {
|
|
284
|
+
this.focusTimeoutId = setTimeout(() => {
|
|
285
|
+
this.focusTimeoutId = null
|
|
274
286
|
this.focusButton(0)
|
|
275
287
|
}, 0)
|
|
276
288
|
}
|
|
@@ -282,12 +294,28 @@ export class PromotionDialog extends Extension {
|
|
|
282
294
|
}
|
|
283
295
|
switch (event.key) {
|
|
284
296
|
case "ArrowDown":
|
|
297
|
+
event.preventDefault()
|
|
298
|
+
if (this.turned) {
|
|
299
|
+
this.focusedIndex = (this.focusedIndex - 1 + 4) % 4
|
|
300
|
+
} else {
|
|
301
|
+
this.focusedIndex = (this.focusedIndex + 1) % 4
|
|
302
|
+
}
|
|
303
|
+
this.focusButton(this.focusedIndex)
|
|
304
|
+
break
|
|
285
305
|
case "ArrowRight":
|
|
286
306
|
event.preventDefault()
|
|
287
307
|
this.focusedIndex = (this.focusedIndex + 1) % 4
|
|
288
308
|
this.focusButton(this.focusedIndex)
|
|
289
309
|
break
|
|
290
310
|
case "ArrowUp":
|
|
311
|
+
event.preventDefault()
|
|
312
|
+
if (this.turned) {
|
|
313
|
+
this.focusedIndex = (this.focusedIndex + 1) % 4
|
|
314
|
+
} else {
|
|
315
|
+
this.focusedIndex = (this.focusedIndex - 1 + 4) % 4
|
|
316
|
+
}
|
|
317
|
+
this.focusButton(this.focusedIndex)
|
|
318
|
+
break
|
|
291
319
|
case "ArrowLeft":
|
|
292
320
|
event.preventDefault()
|
|
293
321
|
this.focusedIndex = (this.focusedIndex - 1 + 4) % 4
|
|
@@ -335,18 +363,45 @@ export class PromotionDialog extends Extension {
|
|
|
335
363
|
}
|
|
336
364
|
|
|
337
365
|
announce(message) {
|
|
366
|
+
if (!this.liveRegion) return
|
|
338
367
|
this.liveRegion.textContent = ""
|
|
368
|
+
if (this.announceTimeoutId) {
|
|
369
|
+
clearTimeout(this.announceTimeoutId)
|
|
370
|
+
}
|
|
339
371
|
// Small delay to ensure screen readers pick up the change
|
|
340
|
-
setTimeout(() => {
|
|
341
|
-
this.
|
|
372
|
+
this.announceTimeoutId = setTimeout(() => {
|
|
373
|
+
this.announceTimeoutId = null
|
|
374
|
+
if (this.liveRegion) {
|
|
375
|
+
this.liveRegion.textContent = message
|
|
376
|
+
}
|
|
342
377
|
}, 50)
|
|
343
378
|
}
|
|
344
379
|
|
|
345
380
|
destroy() {
|
|
381
|
+
// Close the dialog first so its listeners are removed
|
|
382
|
+
if (this.state.displayState === DISPLAY_STATE.shown) {
|
|
383
|
+
this.setDisplayState(DISPLAY_STATE.hidden)
|
|
384
|
+
}
|
|
385
|
+
// Cancel any pending timeouts so callbacks don't fire on a destroyed board
|
|
386
|
+
if (this.showTimeoutId) {
|
|
387
|
+
clearTimeout(this.showTimeoutId)
|
|
388
|
+
this.showTimeoutId = null
|
|
389
|
+
}
|
|
390
|
+
if (this.focusTimeoutId) {
|
|
391
|
+
clearTimeout(this.focusTimeoutId)
|
|
392
|
+
this.focusTimeoutId = null
|
|
393
|
+
}
|
|
394
|
+
if (this.announceTimeoutId) {
|
|
395
|
+
clearTimeout(this.announceTimeoutId)
|
|
396
|
+
this.announceTimeoutId = null
|
|
397
|
+
}
|
|
346
398
|
document.removeEventListener("keydown", this.handleKeyDown)
|
|
347
399
|
if (this.liveRegion && this.liveRegion.parentNode) {
|
|
348
400
|
this.liveRegion.parentNode.removeChild(this.liveRegion)
|
|
401
|
+
this.liveRegion = null
|
|
349
402
|
}
|
|
403
|
+
delete this.chessboard.showPromotionDialog
|
|
404
|
+
delete this.chessboard.isPromotionDialogShown
|
|
350
405
|
}
|
|
351
406
|
|
|
352
407
|
}
|
package/src/model/Position.js
CHANGED
|
@@ -49,7 +49,12 @@ export class ChessboardView {
|
|
|
49
49
|
if (chessboard.props.responsive) {
|
|
50
50
|
if (typeof ResizeObserver !== "undefined") {
|
|
51
51
|
this.resizeObserver = new ResizeObserver(() => {
|
|
52
|
-
|
|
52
|
+
// Defer via setTimeout to avoid "ResizeObserver loop
|
|
53
|
+
// completed with undelivered notifications." The timeout
|
|
54
|
+
// id is tracked so destroy() can cancel a pending call
|
|
55
|
+
// and avoid running handleResize on a destroyed board.
|
|
56
|
+
this.resizeTimeout = setTimeout(() => {
|
|
57
|
+
this.resizeTimeout = null
|
|
53
58
|
this.handleResize()
|
|
54
59
|
})
|
|
55
60
|
})
|
|
@@ -76,6 +81,13 @@ export class ChessboardView {
|
|
|
76
81
|
if (this.resizeObserver) {
|
|
77
82
|
this.resizeObserver.unobserve(this.chessboard.context)
|
|
78
83
|
}
|
|
84
|
+
// Cancel any pending handleResize that the ResizeObserver callback
|
|
85
|
+
// has already scheduled via setTimeout. unobserve() stops new
|
|
86
|
+
// notifications but does not clear timeouts we scheduled ourselves.
|
|
87
|
+
if (this.resizeTimeout) {
|
|
88
|
+
clearTimeout(this.resizeTimeout)
|
|
89
|
+
this.resizeTimeout = null
|
|
90
|
+
}
|
|
79
91
|
if (this.resizeListener) {
|
|
80
92
|
window.removeEventListener("resize", this.resizeListener)
|
|
81
93
|
}
|
|
@@ -145,6 +157,12 @@ export class ChessboardView {
|
|
|
145
157
|
}
|
|
146
158
|
|
|
147
159
|
handleResize() {
|
|
160
|
+
// Skip if the board has already been destroyed. The resizeObserver
|
|
161
|
+
// or window resize listener may fire a callback whose deferred work
|
|
162
|
+
// is still pending when destroy() runs.
|
|
163
|
+
if (!this.chessboard || !this.chessboard.state) {
|
|
164
|
+
return
|
|
165
|
+
}
|
|
148
166
|
this.container.style.width = (this.chessboard.context.clientWidth) + "px"
|
|
149
167
|
this.container.style.height = (this.chessboard.context.clientWidth * this.chessboard.props.style.aspectRatio) + "px"
|
|
150
168
|
if (this.container.clientWidth !== this.width || this.container.clientHeight !== this.height) {
|
package/test/TestChessboard.js
CHANGED
|
@@ -69,4 +69,42 @@ describe("TestChessboard", () => {
|
|
|
69
69
|
chessboard.destroy()
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
+
// Regression for a production crash: the ResizeObserver callback defers
|
|
73
|
+
// handleResize() via setTimeout to avoid "ResizeObserver loop completed"
|
|
74
|
+
// warnings. If destroy() is called between the observer firing and the
|
|
75
|
+
// setTimeout running, handleResize -> redrawBoard hits
|
|
76
|
+
// `this.chessboard.state.invokeExtensionPoints(...)` where `state` is
|
|
77
|
+
// now undefined, throwing TypeError.
|
|
78
|
+
//
|
|
79
|
+
// We invalidate view.width by zeroing it so that the size-changed branch
|
|
80
|
+
// of handleResize is guaranteed to enter updateMetrics + redrawBoard.
|
|
81
|
+
// Otherwise a stable container size would short-circuit the branch and
|
|
82
|
+
// the bug path wouldn't be exercised.
|
|
83
|
+
it("should not crash when destroyed while a resize handler is pending", async () => {
|
|
84
|
+
const chessboard = new Chessboard(document.getElementById("TestBoard"), {
|
|
85
|
+
assetsUrl: "../assets/",
|
|
86
|
+
position: FEN.start
|
|
87
|
+
})
|
|
88
|
+
const view = chessboard.view
|
|
89
|
+
// Force the size-changed branch to trigger on the next handleResize
|
|
90
|
+
view.width = 0
|
|
91
|
+
view.height = 0
|
|
92
|
+
// Simulate what the ResizeObserver callback does: defer handleResize
|
|
93
|
+
// via setTimeout. This path is deterministic.
|
|
94
|
+
const pending = new Promise((resolve) => {
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
try {
|
|
97
|
+
view.handleResize()
|
|
98
|
+
resolve(null)
|
|
99
|
+
} catch (e) {
|
|
100
|
+
resolve(e)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
// Force the race: destroy the board before the setTimeout fires.
|
|
105
|
+
chessboard.destroy()
|
|
106
|
+
const error = await pending
|
|
107
|
+
assert.equal(error, null, error && error.message)
|
|
108
|
+
})
|
|
109
|
+
|
|
72
110
|
})
|
package/CLAUDE.md
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Project Overview
|
|
6
|
-
|
|
7
|
-
cm-chessboard is a lightweight, dependency-free JavaScript chessboard library. It's ES6 module-based, responsive, SVG-rendered, and used in production on chessmail.eu/chessmail.de.
|
|
8
|
-
|
|
9
|
-
**Key Philosophy:** The core is intentionally minimal. Functionality is extended through an extension system rather than adding features to the core.
|
|
10
|
-
|
|
11
|
-
## Development Commands
|
|
12
|
-
|
|
13
|
-
### Running Tests
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
# Tests use the Teevi framework and must be run in a browser
|
|
17
|
-
# Open test/index.html directly to run the test suite
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
### No Build Step
|
|
21
|
-
This project has no build process. It uses native ES6 modules that run directly in modern browsers.
|
|
22
|
-
|
|
23
|
-
## Architecture
|
|
24
|
-
|
|
25
|
-
### Core Components (MVC Pattern)
|
|
26
|
-
|
|
27
|
-
The codebase follows a clean MVC architecture:
|
|
28
|
-
|
|
29
|
-
1. **Model Layer** (`src/model/`)
|
|
30
|
-
- `Position.js` - Board state representation using FEN notation. Contains 64-square array and FEN parsing/serialization
|
|
31
|
-
- `ChessboardState.js` - Manages application state (orientation, input flags, extension points)
|
|
32
|
-
- `Extension.js` - Base class for all extensions. Defines EXTENSION_POINT constants
|
|
33
|
-
|
|
34
|
-
2. **View Layer** (`src/view/`)
|
|
35
|
-
- `ChessboardView.js` - SVG rendering, board drawing, piece rendering, coordinate display
|
|
36
|
-
- `VisualMoveInput.js` - Handles user interaction (drag & drop, click to move)
|
|
37
|
-
- `PositionAnimationsQueue.js` - Queues and executes piece animations
|
|
38
|
-
|
|
39
|
-
3. **Controller** (`src/Chessboard.js`)
|
|
40
|
-
- Main API class that coordinates model and view
|
|
41
|
-
- Entry point for all public methods
|
|
42
|
-
- Instantiates extensions from props
|
|
43
|
-
|
|
44
|
-
4. **Utilities** (`src/lib/`)
|
|
45
|
-
- `Svg.js` - SVG DOM manipulation helpers
|
|
46
|
-
- `Utils.js` - General utilities (URL handling, object merging)
|
|
47
|
-
|
|
48
|
-
### Data Flow
|
|
49
|
-
|
|
50
|
-
1. User calls API method on `Chessboard` instance
|
|
51
|
-
2. `Chessboard` updates `Position` in `ChessboardState`
|
|
52
|
-
3. Extension points are invoked via `state.invokeExtensionPoints()`
|
|
53
|
-
4. Animation is queued in `PositionAnimationsQueue`
|
|
54
|
-
5. `ChessboardView` renders the changes to SVG
|
|
55
|
-
|
|
56
|
-
### Extension System
|
|
57
|
-
|
|
58
|
-
Extensions are the primary way to add functionality. They:
|
|
59
|
-
- Extend the `Extension` base class
|
|
60
|
-
- Register callbacks at extension points using `registerExtensionPoint(EXTENSION_POINT.*, callback)`
|
|
61
|
-
- Can add methods directly to the chessboard instance (e.g., `chessboard.addMarker = this.addMarker.bind(this)`)
|
|
62
|
-
|
|
63
|
-
**Available Extension Points** (defined in `Extension.js`):
|
|
64
|
-
- `positionChanged` - Piece positions changed
|
|
65
|
-
- `boardChanged` - Board orientation changed
|
|
66
|
-
- `boardResized` - Board was resized
|
|
67
|
-
- `moveInputToggled` - Move input enabled/disabled
|
|
68
|
-
- `moveInput` - Move events (started, validating, canceled, finished)
|
|
69
|
-
- `beforeRedrawBoard` / `afterRedrawBoard` - Board redraw lifecycle
|
|
70
|
-
- `animation` - Animation lifecycle hooks
|
|
71
|
-
- `destroy` - Cleanup before board destruction
|
|
72
|
-
|
|
73
|
-
**Included Extensions** (`src/extensions/`):
|
|
74
|
-
- `markers/` - Visual markers on squares (frames, circles, dots)
|
|
75
|
-
- `arrows/` - Drawing arrows between squares
|
|
76
|
-
- `right-click-annotator/` - Right-click UI for annotations (uses markers + arrows)
|
|
77
|
-
- `accessibility/` - Screen reader support, braille notation, keyboard input
|
|
78
|
-
- `promotion-dialog/` - UI dialog for pawn promotion
|
|
79
|
-
- `persistence/` - Save/restore board state to localStorage
|
|
80
|
-
- `html-layer/` - Overlay HTML content on the board
|
|
81
|
-
- `auto-border-none/` - Utility extension
|
|
82
|
-
|
|
83
|
-
## Important Implementation Details
|
|
84
|
-
|
|
85
|
-
### Coordinate System
|
|
86
|
-
- Squares are represented as strings: "a1", "h8", etc.
|
|
87
|
-
- Internally uses 0-63 index (Position.squareToIndex / indexToSquare)
|
|
88
|
-
- Orientation affects rendering but not internal representation
|
|
89
|
-
|
|
90
|
-
### FEN Handling
|
|
91
|
-
- Position class only stores the piece placement (first part of full FEN)
|
|
92
|
-
- Full FEN format is supported in setPosition() but only piece positions are stored
|
|
93
|
-
- Use `FEN.start` and `FEN.empty` constants
|
|
94
|
-
|
|
95
|
-
### Piece Naming
|
|
96
|
-
- Format: `{color}{type}` - e.g., "wp" (white pawn), "bk" (black king)
|
|
97
|
-
- Color: "w" or "b"
|
|
98
|
-
- Type: "p", "n", "b", "r", "q", "k"
|
|
99
|
-
|
|
100
|
-
### SVG Rendering
|
|
101
|
-
- Pieces are rendered from SVG sprites (40x40px tiles)
|
|
102
|
-
- Sprite can be cached in a hidden div for performance
|
|
103
|
-
- Board uses multiple SVG groups as layers (board, coordinates, markers, pieces, etc.)
|
|
104
|
-
- Responsive sizing via ResizeObserver or window resize events
|
|
105
|
-
|
|
106
|
-
### Move Input System
|
|
107
|
-
- Implemented in `VisualMoveInput.js`
|
|
108
|
-
- Supports both drag-and-drop and click-to-move
|
|
109
|
-
- Callback receives event objects with types from `INPUT_EVENT_TYPE`
|
|
110
|
-
- Returns true/false to validate moves
|
|
111
|
-
- Move input can be enabled per color (white/black/both)
|
|
112
|
-
|
|
113
|
-
### Animation Queue
|
|
114
|
-
- All position changes are queued to prevent conflicts
|
|
115
|
-
- Animations return Promises that resolve when complete
|
|
116
|
-
- Queue ensures sequential execution even with rapid API calls
|
|
117
|
-
|
|
118
|
-
## Writing Extensions
|
|
119
|
-
|
|
120
|
-
Template for a new extension:
|
|
121
|
-
|
|
122
|
-
```javascript
|
|
123
|
-
import {Extension, EXTENSION_POINT} from "../../model/Extension.js"
|
|
124
|
-
|
|
125
|
-
export class MyExtension extends Extension {
|
|
126
|
-
constructor(chessboard, props = {}) {
|
|
127
|
-
super(chessboard)
|
|
128
|
-
|
|
129
|
-
// Set default props
|
|
130
|
-
this.props = {
|
|
131
|
-
myOption: true
|
|
132
|
-
}
|
|
133
|
-
Object.assign(this.props, props)
|
|
134
|
-
|
|
135
|
-
// Register extension points
|
|
136
|
-
this.registerExtensionPoint(EXTENSION_POINT.positionChanged, (data) => {
|
|
137
|
-
// React to position changes
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
// Add methods to chessboard instance
|
|
141
|
-
chessboard.myMethod = this.myMethod.bind(this)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
myMethod() {
|
|
145
|
-
// Implementation
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
Enable extension in chessboard props:
|
|
151
|
-
```javascript
|
|
152
|
-
extensions: [{class: MyExtension, props: {myOption: false}}]
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
## Common Patterns
|
|
156
|
-
|
|
157
|
-
### Adding a piece
|
|
158
|
-
```javascript
|
|
159
|
-
board.setPiece("e4", "wn", true) // animated
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### Moving pieces
|
|
163
|
-
```javascript
|
|
164
|
-
board.movePiece("e2", "e4", true) // animated
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### Setting entire position
|
|
168
|
-
```javascript
|
|
169
|
-
board.setPosition(FEN.start, true) // animated
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### Enabling move input
|
|
173
|
-
```javascript
|
|
174
|
-
board.enableMoveInput((event) => {
|
|
175
|
-
if (event.type === INPUT_EVENT_TYPE.validateMoveInput) {
|
|
176
|
-
// Validate the move
|
|
177
|
-
return isLegalMove(event.squareFrom, event.squareTo)
|
|
178
|
-
}
|
|
179
|
-
}, COLOR.white) // Optional: restrict to one side
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
### Creating markers (requires Markers extension)
|
|
183
|
-
```javascript
|
|
184
|
-
board.addMarker(MARKER_TYPE.dot, "e4")
|
|
185
|
-
board.removeMarkers(MARKER_TYPE.frame) // Remove by type
|
|
186
|
-
board.removeMarkers() // Remove all
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
## Assets Structure
|
|
190
|
-
|
|
191
|
-
- `assets/pieces/` - SVG sprite files for piece sets (standard.svg, staunty.svg)
|
|
192
|
-
- `assets/extensions/` - Assets needed by extensions (markers SVG, arrow SVG, etc.)
|
|
193
|
-
- `assets/chessboard.css` - Core styles
|
|
194
|
-
- Extension-specific CSS must be included separately when using extensions
|
|
195
|
-
|
|
196
|
-
## Examples
|
|
197
|
-
|
|
198
|
-
The `examples/` directory contains working examples demonstrating:
|
|
199
|
-
- Simple board creation
|
|
200
|
-
- Move input and validation
|
|
201
|
-
- Different visual styles
|
|
202
|
-
- All included extensions
|
|
203
|
-
- Animation examples
|
|
204
|
-
- Responsive boards
|
|
205
|
-
- Multiple boards on one page
|
|
206
|
-
|
|
207
|
-
Reference examples when implementing new features or debugging issues.
|