3121 lines
127 KiB
JavaScript
3121 lines
127 KiB
JavaScript
/**
|
||
* This code is a TypeScript port of code in https://github.com/addaleax/munkres-js.
|
||
*
|
||
* Some changes include porting to TypeScript, simplifying some loop logic, and
|
||
* other formatting and name changes.
|
||
*
|
||
* Original copyright details:
|
||
*
|
||
* Copyright 2014 Anna Henningsen (Conversion to JS)
|
||
* Copyright 2008 Brian M. Clapper
|
||
*
|
||
* Original Copyright and License
|
||
* ==============================
|
||
*
|
||
* Copyright 2008-2016 Brian M. Clapper
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
const STAR = 1;
|
||
const PRIME = 2;
|
||
class Munkres {
|
||
constructor(costMatrix, padValue) {
|
||
Object.defineProperty(this, "C", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "n", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "originalRows", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "originalCols", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "marked", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "rowCovered", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "colCovered", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "Z0Row", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: 0
|
||
});
|
||
Object.defineProperty(this, "Z0Col", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: 0
|
||
});
|
||
const maxNumColumns = costMatrix.reduce((acc, row) => Math.max(acc, row.length), 0);
|
||
this.n = Math.max(costMatrix.length, maxNumColumns);
|
||
this.originalRows = costMatrix.length;
|
||
this.originalCols = maxNumColumns;
|
||
this.C = [];
|
||
for (let i = 0; i < this.n; i++) {
|
||
const row = costMatrix[i] === undefined ? [] : costMatrix[i].slice();
|
||
while (row.length < this.n) {
|
||
row.push(padValue || 0);
|
||
}
|
||
this.C.push(row);
|
||
}
|
||
this.marked = this._makeMatrix(this.n, 0);
|
||
this.rowCovered = Array(this.n).fill(false);
|
||
this.colCovered = Array(this.n).fill(false);
|
||
}
|
||
/**
|
||
* Compute the indices for the lowest-cost pairings between rows and columns
|
||
* in the database. Returns a list of (row, column) tuples that can be used
|
||
* to traverse the matrix.
|
||
*
|
||
* **WARNING**: This code handles square and rectangular matrices.
|
||
* It does *not* handle irregular matrices.
|
||
*/
|
||
compute() {
|
||
let step = 1;
|
||
const steps = {
|
||
1: this._step1,
|
||
2: this._step2,
|
||
3: this._step3,
|
||
4: this._step4,
|
||
5: this._step5,
|
||
6: this._step6,
|
||
};
|
||
while (step < 7) {
|
||
const func = steps[step];
|
||
step = func.apply(this);
|
||
}
|
||
const results = [];
|
||
for (let i = 0; i < this.originalRows; i++) {
|
||
for (let j = 0; j < this.originalCols; j++) {
|
||
if (this.marked[i][j] == STAR) {
|
||
results.push([i, j]);
|
||
}
|
||
}
|
||
}
|
||
return results;
|
||
}
|
||
/**
|
||
* Create an n×n matrix, populating it with the specific value.
|
||
*/
|
||
_makeMatrix(n, val) {
|
||
const matrix = [];
|
||
for (let i = 0; i < n; i++) {
|
||
const row = [];
|
||
for (let j = 0; j < n; j++) {
|
||
row.push(val);
|
||
}
|
||
matrix.push(row);
|
||
}
|
||
return matrix;
|
||
}
|
||
/**
|
||
* Produce at least one zero in each row by subtracting the smallest
|
||
* element of each row from every element in a row. Go to Step 2.
|
||
*/
|
||
_step1() {
|
||
for (let i = 0; i < this.n; i++) {
|
||
const minval = Math.min(...this.C[i]);
|
||
for (let j = 0; j < this.n; j++) {
|
||
this.C[i][j] -= minval;
|
||
}
|
||
}
|
||
return 2;
|
||
}
|
||
/**
|
||
* Assign as many tasks as possible:
|
||
* 1. Find a zero in the matrix, and star it. Temporarily mark row and column.
|
||
* 2. Find the next zero that is not in an already marked row and column.
|
||
* 3. Repeat 1.
|
||
* Go to Step 3.
|
||
*/
|
||
_step2() {
|
||
for (let i = 0; i < this.n; i++) {
|
||
for (let j = 0; j < this.n; j++) {
|
||
if (this.C[i][j] === 0 && !this.rowCovered[i] && !this.colCovered[j]) {
|
||
this.marked[i][j] = STAR;
|
||
this.rowCovered[i] = true;
|
||
this.colCovered[j] = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
this._clearCovers();
|
||
return 3;
|
||
}
|
||
/**
|
||
* Cover each column containing an assignment (starred zero). If K columns
|
||
* are covered, the starred zeros describe a complete set of unique
|
||
* assignments. In this case, go to DONE, otherwise, go to Step 4.
|
||
*/
|
||
_step3() {
|
||
let count = 0;
|
||
for (let i = 0; i < this.n; i++) {
|
||
for (let j = 0; j < this.n; j++) {
|
||
if (this.marked[i][j] === STAR && !this.colCovered[j]) {
|
||
this.colCovered[j] = true;
|
||
count++;
|
||
}
|
||
}
|
||
}
|
||
return count >= this.n ? 7 : 4;
|
||
}
|
||
/**
|
||
* Find an uncovered zero and prime it. If there is no starred zero
|
||
* on that row, go to Step 6. If there is a starred zero on that row,
|
||
* cover the row, and uncover the column containing the starred
|
||
* zero. Continue doing this, until we find an uncovered zero with no
|
||
* starred zero on the same row. Go to Step 5.
|
||
*/
|
||
_step4() {
|
||
let colWithStar = -1;
|
||
for (;;) {
|
||
const [row, col] = this._findFirstUncoveredZero();
|
||
if (row < 0) {
|
||
return 6;
|
||
}
|
||
this.marked[row][col] = PRIME;
|
||
colWithStar = this._findStarInRow(row);
|
||
if (colWithStar >= 0) {
|
||
this.rowCovered[row] = true;
|
||
this.colCovered[colWithStar] = false;
|
||
}
|
||
else {
|
||
this.Z0Row = row;
|
||
this.Z0Col = col;
|
||
return 5;
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Construct a series of alternating primed and starred zeros as
|
||
* follows. Let Z0 represent the uncovered primed zero found in Step 4.
|
||
* Let Z1 denote the starred zero in the column of Z0 (if any).
|
||
* Let Z2 denote the primed zero in the row of Z1 (there will always
|
||
* be one). Continue until the series terminates at a primed zero
|
||
* that has no starred zero in its column. Unstar each starred zero
|
||
* of the series, star each primed zero of the series, erase all
|
||
* primes and uncover every line in the matrix. Return to Step 3
|
||
*/
|
||
_step5() {
|
||
let count = 0;
|
||
const path = [[this.Z0Row, this.Z0Col]];
|
||
for (;;) {
|
||
const row = this._findStarInCol(path[count][1]);
|
||
if (row < 0) {
|
||
break;
|
||
}
|
||
path.push([row, path[count][1]]);
|
||
count++;
|
||
const col = this._findPrimeInRow(path[count][0]);
|
||
path.push([path[count][0], col]);
|
||
count++;
|
||
}
|
||
for (let i = 0; i <= count; i++) {
|
||
const [row, col] = path[i];
|
||
// Element at row, col is either starred or primed.
|
||
// if star -> unstar
|
||
// if prime -> star
|
||
this.marked[row][col] = this.marked[row][col] == STAR ? 0 : STAR;
|
||
}
|
||
this._clearCovers();
|
||
this._erasePrimes();
|
||
return 3;
|
||
}
|
||
/**
|
||
* From the uncovered elements, find the smallest element.
|
||
* Add that value to every element of each covered row, and subtract it
|
||
* from every element of each uncovered column. Return to Step 4 without
|
||
* altering any stars, primes, or covered lines.
|
||
*/
|
||
_step6() {
|
||
const minval = this._findSmallestUncovered();
|
||
for (let i = 0; i < this.n; i++) {
|
||
for (let j = 0; j < this.n; j++) {
|
||
if (this.rowCovered[i]) {
|
||
this.C[i][j] += minval;
|
||
}
|
||
if (!this.colCovered[j]) {
|
||
this.C[i][j] -= minval;
|
||
}
|
||
}
|
||
}
|
||
return 4;
|
||
}
|
||
/**
|
||
* Clear all covered matrix cells.
|
||
*/
|
||
_clearCovers() {
|
||
for (let i = 0; i < this.n; i++) {
|
||
this.rowCovered[i] = false;
|
||
this.colCovered[i] = false;
|
||
}
|
||
}
|
||
/**
|
||
* Erase all prime markings.
|
||
*/
|
||
_erasePrimes() {
|
||
for (let i = 0; i < this.n; i++) {
|
||
for (let j = 0; j < this.n; j++)
|
||
if (this.marked[i][j] === PRIME) {
|
||
this.marked[i][j] = 0;
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Find the first uncovered element with value 0. If none found, return [-1, -1].
|
||
*/
|
||
_findFirstUncoveredZero() {
|
||
for (let i = 0; i < this.n; i++)
|
||
for (let j = 0; j < this.n; j++)
|
||
if (this.C[i][j] === 0 && !this.rowCovered[i] && !this.colCovered[j])
|
||
return [i, j];
|
||
return [-1, -1];
|
||
}
|
||
/**
|
||
* Find the first starred element in the specified row. Returns
|
||
* the column index, or -1 if no starred element was found.
|
||
*/
|
||
_findStarInRow(row) {
|
||
for (let j = 0; j < this.n; j++) {
|
||
if (this.marked[row][j] == STAR) {
|
||
return j;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
/**
|
||
* Find the first starred element in the specified column. Returns
|
||
* the row index, or -1 if no starred element was found.
|
||
*/
|
||
_findStarInCol(col) {
|
||
for (let i = 0; i < this.n; i++) {
|
||
if (this.marked[i][col] == STAR) {
|
||
return i;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
/**
|
||
* Find the first prime element in the specified row. Returns the column
|
||
* index, or -1 if no prime element was found.
|
||
*/
|
||
_findPrimeInRow(row) {
|
||
for (let j = 0; j < this.n; j++) {
|
||
if (this.marked[row][j] == PRIME) {
|
||
return j;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
/**
|
||
* Find the smallest uncovered value in the matrix.
|
||
*/
|
||
_findSmallestUncovered() {
|
||
let minval = Number.MAX_SAFE_INTEGER;
|
||
for (let i = 0; i < this.n; i++) {
|
||
for (let j = 0; j < this.n; j++) {
|
||
if (!this.rowCovered[i] &&
|
||
!this.colCovered[j] &&
|
||
minval > this.C[i][j]) {
|
||
minval = this.C[i][j];
|
||
}
|
||
}
|
||
}
|
||
return minval;
|
||
}
|
||
}
|
||
function munkres(costMatrix, padValue) {
|
||
const m = new Munkres(costMatrix, padValue);
|
||
return m.compute();
|
||
}
|
||
|
||
/**
|
||
* Collection of 0x88-based methods to represent chessboard state.
|
||
*
|
||
* https://www.chessprogramming.org/0x88
|
||
*/
|
||
const SIDE_COLORS = ["white", "black"];
|
||
// prettier-ignore
|
||
const SQUARES_MAP = {
|
||
a8: 0, b8: 1, c8: 2, d8: 3, e8: 4, f8: 5, g8: 6, h8: 7,
|
||
a7: 16, b7: 17, c7: 18, d7: 19, e7: 20, f7: 21, g7: 22, h7: 23,
|
||
a6: 32, b6: 33, c6: 34, d6: 35, e6: 36, f6: 37, g6: 38, h6: 39,
|
||
a5: 48, b5: 49, c5: 50, d5: 51, e5: 52, f5: 53, g5: 54, h5: 55,
|
||
a4: 64, b4: 65, c4: 66, d4: 67, e4: 68, f4: 69, g4: 70, h4: 71,
|
||
a3: 80, b3: 81, c3: 82, d3: 83, e3: 84, f3: 85, g3: 86, h3: 87,
|
||
a2: 96, b2: 97, c2: 98, d2: 99, e2: 100, f2: 101, g2: 102, h2: 103,
|
||
a1: 112, b1: 113, c1: 114, d1: 115, e1: 116, f1: 117, g1: 118, h1: 119
|
||
};
|
||
const SQUARES = Object.keys(SQUARES_MAP);
|
||
// prettier-ignore
|
||
const SQUARE_DISTANCE_TABLE = [
|
||
14, 13, 12, 11, 10, 9, 8, 7, 8, 9, 10, 11, 12, 13, 14, 0,
|
||
13, 12, 11, 10, 9, 8, 7, 6, 7, 8, 9, 10, 11, 12, 13, 0,
|
||
12, 11, 10, 9, 8, 7, 6, 5, 6, 7, 8, 9, 10, 11, 12, 0,
|
||
11, 10, 9, 8, 7, 6, 5, 4, 5, 6, 7, 8, 9, 10, 11, 0,
|
||
10, 9, 8, 7, 6, 5, 4, 3, 4, 5, 6, 7, 8, 9, 10, 0,
|
||
9, 8, 7, 6, 5, 4, 3, 2, 3, 4, 5, 6, 7, 8, 9, 0,
|
||
8, 7, 6, 5, 4, 3, 2, 1, 2, 3, 4, 5, 6, 7, 8, 0,
|
||
7, 6, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 6, 7, 0,
|
||
8, 7, 6, 5, 4, 3, 2, 1, 2, 3, 4, 5, 6, 7, 8, 0,
|
||
9, 8, 7, 6, 5, 4, 3, 2, 3, 4, 5, 6, 7, 8, 9, 0,
|
||
10, 9, 8, 7, 6, 5, 4, 3, 4, 5, 6, 7, 8, 9, 10, 0,
|
||
11, 10, 9, 8, 7, 6, 5, 4, 5, 6, 7, 8, 9, 10, 11, 0,
|
||
12, 11, 10, 9, 8, 7, 6, 5, 6, 7, 8, 9, 10, 11, 12, 0,
|
||
13, 12, 11, 10, 9, 8, 7, 6, 7, 8, 9, 10, 11, 12, 13, 0,
|
||
14, 13, 12, 11, 10, 9, 8, 7, 8, 9, 10, 11, 12, 13, 14, 0,
|
||
];
|
||
const REVERSE_SQUARES_MAP = SQUARES.reduce((acc, key) => {
|
||
acc[SQUARES_MAP[key]] = key;
|
||
return acc;
|
||
}, {});
|
||
const FEN_PIECE_TYPE_MAP = {
|
||
p: "pawn",
|
||
n: "knight",
|
||
b: "bishop",
|
||
r: "rook",
|
||
q: "queen",
|
||
k: "king",
|
||
};
|
||
const REVERSE_FEN_PIECE_TYPE_MAP = Object.keys(FEN_PIECE_TYPE_MAP).reduce((acc, key) => {
|
||
acc[FEN_PIECE_TYPE_MAP[key]] = key;
|
||
return acc;
|
||
}, {});
|
||
/**
|
||
* Parse a FEN string and return an object that maps squares to pieces.
|
||
*
|
||
* Also accepts the special string "initial" or "start" to represent
|
||
* standard game starting position.
|
||
*
|
||
* Note that only the first part of the FEN string (piece placement) is
|
||
* parsed; any additional components are ignored.
|
||
*
|
||
* @param fen the FEN string
|
||
* @returns an object where key is of type Square (string) and value is
|
||
* of type Piece
|
||
*/
|
||
function getPosition(fen) {
|
||
if (fen === "initial" || fen === "start") {
|
||
fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR";
|
||
}
|
||
const parts = fen.split(" ");
|
||
const ranks = parts[0].split("/");
|
||
if (ranks.length !== 8) {
|
||
return undefined;
|
||
}
|
||
const position = {};
|
||
for (let i = 0; i < 8; i++) {
|
||
const rank = 8 - i;
|
||
let fileOffset = 0;
|
||
for (let j = 0; j < ranks[i].length; j++) {
|
||
const pieceLetter = ranks[i][j].toLowerCase();
|
||
if (pieceLetter in FEN_PIECE_TYPE_MAP) {
|
||
const square = (String.fromCharCode(97 + fileOffset) + rank);
|
||
position[square] = {
|
||
pieceType: FEN_PIECE_TYPE_MAP[pieceLetter],
|
||
color: pieceLetter === ranks[i][j] ? "black" : "white",
|
||
};
|
||
fileOffset += 1;
|
||
}
|
||
else {
|
||
const emptySpaces = parseInt(ranks[i][j]);
|
||
if (isNaN(emptySpaces) || emptySpaces === 0 || emptySpaces > 8) {
|
||
return undefined;
|
||
}
|
||
else {
|
||
fileOffset += emptySpaces;
|
||
}
|
||
}
|
||
}
|
||
if (fileOffset !== 8) {
|
||
return undefined;
|
||
}
|
||
}
|
||
return position;
|
||
}
|
||
/**
|
||
* Get FEN string corresponding to Position object. Note that this only returns
|
||
* the first (piece placement) component of the FEN string.
|
||
*/
|
||
function getFen(position) {
|
||
const rankSpecs = [];
|
||
for (let i = 0; i < 8; i++) {
|
||
let rankSpec = "";
|
||
let gap = 0;
|
||
for (let j = 0; j < 8; j++) {
|
||
const square = REVERSE_SQUARES_MAP[16 * i + j];
|
||
const piece = position[square];
|
||
if (piece !== undefined) {
|
||
const pieceStr = REVERSE_FEN_PIECE_TYPE_MAP[piece.pieceType];
|
||
if (gap > 0) {
|
||
rankSpec += gap;
|
||
}
|
||
rankSpec += piece.color === "white" ? pieceStr.toUpperCase() : pieceStr;
|
||
gap = 0;
|
||
}
|
||
else {
|
||
gap += 1;
|
||
}
|
||
}
|
||
if (gap > 0) {
|
||
rankSpec += gap;
|
||
}
|
||
rankSpecs.push(rankSpec);
|
||
}
|
||
return rankSpecs.join("/");
|
||
}
|
||
/**
|
||
* Return square identifier for visual index in a grid, depending on
|
||
* orientation. If `orientation` is "white", then a8 is on the top
|
||
* left (0) and h8 is on the bottom right (63):
|
||
*
|
||
* a8 ...... .
|
||
* . ...... .
|
||
* . ...... h1
|
||
*
|
||
* otherwise h1 is on the top left:
|
||
*
|
||
* h1 ...... .
|
||
* . ...... .
|
||
* . ...... a8
|
||
*
|
||
* https://www.chessprogramming.org/0x88#Coordinate_Transformation
|
||
*/
|
||
function getSquare(visualIndex, orientation) {
|
||
const idx = visualIndex + (visualIndex & ~0x7);
|
||
return REVERSE_SQUARES_MAP[orientation === "black" ? 0x77 - idx : idx];
|
||
}
|
||
/**
|
||
* Get the "visual" index for `square` depending on `orientation`.
|
||
* If `orientation` is "white", then a8 is on the top left (0) and h8 is
|
||
* on the bottom right (63):
|
||
*
|
||
* a8 ...... .
|
||
* . ...... .
|
||
* . ...... h1
|
||
*
|
||
* otherwise h1 is on the top left:
|
||
*
|
||
* h1 ...... .
|
||
* . ...... .
|
||
* . ...... a8
|
||
*
|
||
* https://www.chessprogramming.org/0x88#Coordinate_Transformation
|
||
*
|
||
* @param square square to convert to visual index.
|
||
* @param orientation what side is at the bottom ("white" = a1 on bottom left)
|
||
* @returns a visual index for the square in question.
|
||
*/
|
||
function getVisualIndex(square, orientation) {
|
||
const idx = SQUARES_MAP[square];
|
||
const orientedIdx = orientation === "black" ? 0x77 - idx : idx;
|
||
return (orientedIdx + (orientedIdx & 0x7)) >> 1;
|
||
}
|
||
/**
|
||
* Like `getVisualIndex`, but returns a row and column combination.
|
||
*
|
||
* @param square square to convert to visual row and column.
|
||
* @param orientation what side is at the bottom ("white" = a1 on bottom left)
|
||
* @returns an array containing [row, column] for the square in question.
|
||
*/
|
||
function getVisualRowColumn(square, orientation) {
|
||
const idx = getVisualIndex(square, orientation);
|
||
return [idx >> 3, idx & 0x7];
|
||
}
|
||
/**
|
||
* https://www.chessprogramming.org/Color_of_a_Square#By_Anti-Diagonal_Index
|
||
*/
|
||
function getSquareColor(square) {
|
||
const idx0x88 = SQUARES_MAP[square];
|
||
const idx = (idx0x88 + (idx0x88 & 0x7)) >> 1;
|
||
return ((idx * 9) & 8) === 0 ? "light" : "dark";
|
||
}
|
||
/**
|
||
* Type guard to check if `key` (string) is a valid chess square.
|
||
*/
|
||
function keyIsSquare(key) {
|
||
return key !== undefined && key in SQUARES_MAP;
|
||
}
|
||
/**
|
||
* Deep equality check for two Piece objects.
|
||
*/
|
||
function pieceEqual(a, b) {
|
||
return ((a === undefined && b === undefined) ||
|
||
(a !== undefined &&
|
||
b !== undefined &&
|
||
a.color === b.color &&
|
||
a.pieceType === b.pieceType));
|
||
}
|
||
/**
|
||
* Type guard for string values that need to conform to a `Side` definition.
|
||
*/
|
||
function isSide(s) {
|
||
return SIDE_COLORS.includes(s);
|
||
}
|
||
/**
|
||
* Deep equality check for Position objects.
|
||
*/
|
||
function positionsEqual(a, b) {
|
||
return SQUARES.every((square) => pieceEqual(a[square], b[square]));
|
||
}
|
||
function calcPositionDiff(oldPosition, newPosition) {
|
||
// Limit old and new positions only to squares that are different
|
||
const oldPositionLimited = { ...oldPosition };
|
||
const newPositionLimited = { ...newPosition };
|
||
Object.keys(newPosition).forEach((k) => {
|
||
const square = k;
|
||
if (pieceEqual(newPosition[square], oldPosition[square])) {
|
||
delete oldPositionLimited[square];
|
||
delete newPositionLimited[square];
|
||
}
|
||
});
|
||
const added = [];
|
||
const removed = [];
|
||
const moved = [];
|
||
function groupByPiece(position) {
|
||
return Object.entries(position).reduce((groups, [square, piece]) => {
|
||
const key = `${piece.color}_${piece.pieceType}`;
|
||
if (!(key in groups)) {
|
||
groups[key] = { squares: [], piece };
|
||
}
|
||
groups[key].squares.push(square);
|
||
return groups;
|
||
}, {});
|
||
}
|
||
function matchSquares(oldSquares, newSquares) {
|
||
const costMatrix = [];
|
||
for (let i = 0; i < oldSquares.length; i++) {
|
||
const row = [];
|
||
for (let j = 0; j < newSquares.length; j++) {
|
||
row.push(squareDistance(oldSquares[i], newSquares[j]));
|
||
}
|
||
costMatrix.push(row);
|
||
}
|
||
const matches = munkres(costMatrix, 15);
|
||
const moved = [];
|
||
const added = [];
|
||
const removed = [];
|
||
const oldSquaresCopy = oldSquares.slice();
|
||
const newSquaresCopy = newSquares.slice();
|
||
for (const [i, j] of matches || []) {
|
||
moved.push({
|
||
oldSquare: oldSquaresCopy[i],
|
||
newSquare: newSquaresCopy[j],
|
||
});
|
||
delete oldSquaresCopy[i];
|
||
delete newSquaresCopy[j];
|
||
}
|
||
oldSquaresCopy
|
||
.filter((s) => s !== undefined)
|
||
.forEach((s) => {
|
||
removed.push(s);
|
||
});
|
||
newSquaresCopy
|
||
.filter((s) => s !== undefined)
|
||
.forEach((s) => {
|
||
added.push(s);
|
||
});
|
||
return { moved, added, removed };
|
||
}
|
||
const oldPositionGrouped = groupByPiece(oldPositionLimited);
|
||
const newPositionGrouped = groupByPiece(newPositionLimited);
|
||
Object.entries(newPositionGrouped).forEach(([k, pieceSquares]) => {
|
||
if (k in oldPositionGrouped) {
|
||
const matches = matchSquares(oldPositionGrouped[k].squares, pieceSquares.squares);
|
||
matches.moved.forEach(({ oldSquare, newSquare }) => {
|
||
moved.push({ piece: pieceSquares.piece, oldSquare, newSquare });
|
||
});
|
||
matches.added.forEach((square) => {
|
||
added.push({ piece: pieceSquares.piece, square });
|
||
});
|
||
matches.removed.forEach((square) => {
|
||
removed.push({ piece: pieceSquares.piece, square });
|
||
});
|
||
delete oldPositionGrouped[k];
|
||
}
|
||
else {
|
||
// Piece only in new position - it is added
|
||
pieceSquares.squares.forEach((square) => {
|
||
added.push({ piece: pieceSquares.piece, square });
|
||
});
|
||
}
|
||
});
|
||
// All pieces in old position now are removed
|
||
Object.entries(oldPositionGrouped).forEach(([, pieceSquares]) => {
|
||
pieceSquares.squares.forEach((square) => {
|
||
removed.push({ piece: pieceSquares.piece, square });
|
||
});
|
||
});
|
||
return { added, removed, moved };
|
||
}
|
||
function squareDistance(a, b) {
|
||
return SQUARE_DISTANCE_TABLE[SQUARES_MAP[a] - SQUARES_MAP[b] + 0x77];
|
||
}
|
||
|
||
/**
|
||
* Convenience functions for creating and removing DOM elements.
|
||
*/
|
||
/**
|
||
* Make HTML element, with optional `attributes`, `data` key/values and `classes`
|
||
* specified through `options.
|
||
*/
|
||
function makeHTMLElement(tag, options) {
|
||
return addOptionsToElement(document.createElement(tag), options);
|
||
}
|
||
/**
|
||
* Make SVG element, with optional `attributes`, `data` key/values and `classes`
|
||
* specified through `options.
|
||
*/
|
||
function makeSVGElement(tag, options) {
|
||
return addOptionsToElement(document.createElementNS("http://www.w3.org/2000/svg", tag), options);
|
||
}
|
||
function addOptionsToElement(e, options) {
|
||
if (options !== undefined) {
|
||
for (const key in options.attributes) {
|
||
e.setAttribute(key, options.attributes[key]);
|
||
}
|
||
for (const key in options.data) {
|
||
e.dataset[key] = options.data[key];
|
||
}
|
||
if (options.classes) {
|
||
e.classList.add(...options.classes);
|
||
}
|
||
}
|
||
return e;
|
||
}
|
||
|
||
/**
|
||
* Wrapper for Typescript `never` type to be used in exhaustive type checks.
|
||
*/
|
||
// istanbul ignore next
|
||
function assertUnreachable(x) {
|
||
throw new Error(`Unreachable code reached with value ${x}`);
|
||
}
|
||
|
||
/**
|
||
* Visual representation of a chessboard piece.
|
||
*/
|
||
class BoardPiece {
|
||
constructor(container, config) {
|
||
Object.defineProperty(this, "piece", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "animationFinished", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_element", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_parentElement", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_explicitPosition", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
this.piece = config.piece;
|
||
this._parentElement = container;
|
||
this._element = makeHTMLElement("span", {
|
||
attributes: {
|
||
role: "presentation",
|
||
"aria-hidden": "true",
|
||
part: `piece-${BoardPiece.PIECE_CLASS_MAP[this.piece.color][this.piece.pieceType]}`,
|
||
},
|
||
classes: [
|
||
"piece",
|
||
BoardPiece.PIECE_CLASS_MAP[this.piece.color][this.piece.pieceType],
|
||
],
|
||
});
|
||
if (config.animation !== undefined) {
|
||
this._setAnimation(config.animation);
|
||
}
|
||
if (config.secondary) {
|
||
this._element.classList.add("secondary");
|
||
}
|
||
container.appendChild(this._element);
|
||
}
|
||
/**
|
||
* Remove piece for square it is contained on, along with any animation
|
||
* listeners.
|
||
*/
|
||
remove(animationDurationMs) {
|
||
if (animationDurationMs) {
|
||
this._setAnimation({ type: "fade-out", durationMs: animationDurationMs });
|
||
}
|
||
else {
|
||
this._parentElement.removeChild(this._element);
|
||
}
|
||
}
|
||
/**
|
||
* Set explicit offset for piece relative to default location in square.
|
||
*/
|
||
setExplicitPosition(explicitPosition) {
|
||
this._explicitPosition = explicitPosition;
|
||
const coords = this._getTranslateValues(explicitPosition);
|
||
if (coords) {
|
||
this._element.style.transform = `translate(${coords.x}, ${coords.y})`;
|
||
}
|
||
const scaleValue = getComputedStyle(this._element).getPropertyValue(BoardPiece.PIECE_DRAG_SCALE_PROP);
|
||
if (scaleValue) {
|
||
this._element.style.transform += ` scale(${scaleValue})`;
|
||
}
|
||
}
|
||
/**
|
||
* Reset any explicit position set on the piece. If `transition` is true, then
|
||
* the change is accompanied with a transition.
|
||
*/
|
||
resetPosition(animateDurationMs) {
|
||
if (animateDurationMs && this._explicitPosition) {
|
||
this._setAnimation({
|
||
type: "slide-in",
|
||
from: this._explicitPosition,
|
||
durationMs: animateDurationMs,
|
||
});
|
||
}
|
||
this._element.style.removeProperty("transform");
|
||
this._explicitPosition = undefined;
|
||
}
|
||
/**
|
||
* Return explicit position of piece on square, if any.
|
||
*/
|
||
get explicitPosition() {
|
||
return this._explicitPosition;
|
||
}
|
||
/**
|
||
* Finish any animations, if in progress.
|
||
*/
|
||
finishAnimations() {
|
||
this._element.getAnimations().forEach((a) => {
|
||
a.finish();
|
||
});
|
||
}
|
||
_getTranslateValues(explicitPosition) {
|
||
if (explicitPosition.type === "coordinates") {
|
||
const squareDims = this._parentElement.getBoundingClientRect();
|
||
const deltaX = explicitPosition.x - squareDims.left - squareDims.width / 2;
|
||
const deltaY = explicitPosition.y - squareDims.top - (3 * squareDims.height) / 4;
|
||
if (deltaX !== 0 || deltaY !== 0) {
|
||
return { x: `${deltaX}px`, y: `${deltaY}px` };
|
||
}
|
||
}
|
||
else {
|
||
if (explicitPosition.deltaCols !== 0 ||
|
||
explicitPosition.deltaRows !== 0) {
|
||
return {
|
||
x: `${explicitPosition.deltaCols * 100}%`,
|
||
y: `${explicitPosition.deltaRows * 100}%`,
|
||
};
|
||
}
|
||
}
|
||
return undefined;
|
||
}
|
||
_setAnimation(animationSpec) {
|
||
let keyframes;
|
||
let onfinish;
|
||
// Always have exactly one animation running at a time.
|
||
this.finishAnimations();
|
||
switch (animationSpec.type) {
|
||
case "slide-in":
|
||
{
|
||
const coords = this._getTranslateValues(animationSpec.from);
|
||
if (coords) {
|
||
keyframes = [
|
||
{ transform: `translate(${coords.x}, ${coords.y})` },
|
||
{ transform: "none" },
|
||
];
|
||
this._element.classList.add("moving");
|
||
}
|
||
onfinish = () => {
|
||
this._element.classList.remove("moving");
|
||
};
|
||
}
|
||
break;
|
||
case "fade-in":
|
||
keyframes = [{ opacity: 0 }, { opacity: 1 }];
|
||
break;
|
||
case "fade-out":
|
||
{
|
||
keyframes = [{ opacity: 1 }, { opacity: 0 }];
|
||
const elementCopy = this._element;
|
||
onfinish = () => {
|
||
this._parentElement.removeChild(elementCopy);
|
||
};
|
||
}
|
||
break;
|
||
default:
|
||
assertUnreachable(animationSpec);
|
||
}
|
||
if (keyframes !== undefined &&
|
||
typeof this._element.animate === "function") {
|
||
const animation = this._element.animate(keyframes, {
|
||
duration: Math.max(0, animationSpec.durationMs),
|
||
});
|
||
this.animationFinished = new Promise((resolve) => {
|
||
animation.onfinish = () => {
|
||
if (onfinish !== undefined) {
|
||
onfinish();
|
||
}
|
||
this.animationFinished = undefined;
|
||
resolve();
|
||
};
|
||
});
|
||
}
|
||
else if (onfinish !== undefined) {
|
||
onfinish();
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Map of piece to background image CSS class name.
|
||
*/
|
||
Object.defineProperty(BoardPiece, "PIECE_CLASS_MAP", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: {
|
||
white: {
|
||
queen: "wq",
|
||
king: "wk",
|
||
knight: "wn",
|
||
pawn: "wp",
|
||
bishop: "wb",
|
||
rook: "wr",
|
||
},
|
||
black: {
|
||
queen: "bq",
|
||
king: "bk",
|
||
knight: "bn",
|
||
pawn: "bp",
|
||
bishop: "bb",
|
||
rook: "br",
|
||
},
|
||
}
|
||
});
|
||
/**
|
||
* CSS custom property for scale applied to piece while draggging.
|
||
* This is overridden per input method within CSS styles.
|
||
*/
|
||
Object.defineProperty(BoardPiece, "PIECE_DRAG_SCALE_PROP", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: "--p-piece-drag-scale"
|
||
});
|
||
|
||
/**
|
||
* Visual representation of a chessboard square, along with attributes
|
||
* that aid in interactivity (ARIA role, labels etc).
|
||
*/
|
||
class BoardSquare {
|
||
constructor(container, label) {
|
||
Object.defineProperty(this, "_tdElement", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_contentElement", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_slotWrapper", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_slotElement", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_label", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_interactive", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: false
|
||
});
|
||
Object.defineProperty(this, "_tabbable", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: false
|
||
});
|
||
Object.defineProperty(this, "_moveable", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: false
|
||
});
|
||
Object.defineProperty(this, "_boardPiece", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_secondaryBoardPiece", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_hasContent", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_hover", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: false
|
||
});
|
||
Object.defineProperty(this, "_markedTarget", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: false
|
||
});
|
||
Object.defineProperty(this, "_moveState", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
this._tdElement = makeHTMLElement("td", { attributes: { role: "cell" } });
|
||
this._label = label;
|
||
this._contentElement = makeHTMLElement("div", { classes: ["content"] });
|
||
this._slotWrapper = makeHTMLElement("div", {
|
||
classes: ["slot"],
|
||
attributes: { role: "presentation" },
|
||
});
|
||
this._slotElement = document.createElement("slot");
|
||
this._slotWrapper.appendChild(this._slotElement);
|
||
this._contentElement.appendChild(this._slotWrapper);
|
||
this._updateLabelVisuals();
|
||
this._tdElement.appendChild(this._contentElement);
|
||
container.appendChild(this._tdElement);
|
||
}
|
||
/**
|
||
* Label associated with the square (depends on orientation of square
|
||
* on the board).
|
||
*/
|
||
get label() {
|
||
return this._label;
|
||
}
|
||
set label(value) {
|
||
this._label = value;
|
||
this._updateLabelVisuals();
|
||
}
|
||
/**
|
||
* Whether the square is used in an interactive grid. Decides whether
|
||
* the square should get visual attributes like tabindex, labels etc.
|
||
*/
|
||
get interactive() {
|
||
return this._interactive;
|
||
}
|
||
set interactive(value) {
|
||
this._interactive = value;
|
||
this._moveState = undefined;
|
||
// Aria roles
|
||
this._tdElement.setAttribute("role", value ? "gridcell" : "cell");
|
||
if (value) {
|
||
this._contentElement.setAttribute("role", "button");
|
||
}
|
||
else {
|
||
this._contentElement.removeAttribute("role");
|
||
}
|
||
this._updateTabIndex();
|
||
this._updateMoveStateVisuals();
|
||
this._updateLabelVisuals();
|
||
}
|
||
/**
|
||
* Whether this square can be tabbed to by the user (tabindex = 0). By default,
|
||
* all chessboard squares are focusable but not user-tabbable (tabindex = -1).
|
||
*/
|
||
get tabbable() {
|
||
return this._tabbable;
|
||
}
|
||
set tabbable(value) {
|
||
this._tabbable = value;
|
||
this._updateTabIndex();
|
||
}
|
||
/**
|
||
* Whether this square should be marked as containing any slotted content.
|
||
*/
|
||
get hasContent() {
|
||
return !!this._hasContent;
|
||
}
|
||
set hasContent(value) {
|
||
this._hasContent = value;
|
||
this._contentElement.classList.toggle("has-content", value);
|
||
}
|
||
/**
|
||
* Whether the piece on this square is moveable through user interaction.
|
||
* To be set to true, a piece must actually exist on the square.
|
||
*/
|
||
get moveable() {
|
||
return this._moveable;
|
||
}
|
||
set moveable(value) {
|
||
if (!value || this._boardPiece) {
|
||
this._moveable = value;
|
||
this._updateMoveStateVisuals();
|
||
this._updateLabelVisuals();
|
||
}
|
||
}
|
||
/**
|
||
* Whether this square is a valid move target. These are highlighted
|
||
* when move is in progress, indicating squares that we can move to.
|
||
*/
|
||
get moveTarget() {
|
||
return this._moveState === "move-target";
|
||
}
|
||
set moveTarget(value) {
|
||
this._moveState = value ? "move-target" : "move-nontarget";
|
||
this._updateMoveStateVisuals();
|
||
this._updateLabelVisuals();
|
||
}
|
||
removeMoveState() {
|
||
this._moveState = undefined;
|
||
this._updateMoveStateVisuals();
|
||
this._updateLabelVisuals();
|
||
}
|
||
/**
|
||
* Whether this square is currently a "hover" target: the equivalent of a
|
||
* :hover pseudoclass while mousing over a target square, but for drag
|
||
* and keyboard moves.
|
||
*/
|
||
get hover() {
|
||
return this._hover;
|
||
}
|
||
set hover(value) {
|
||
this._hover = value;
|
||
this._contentElement.classList.toggle("hover", value);
|
||
}
|
||
/**
|
||
* Whether this square is currently a marked destination of a move. This
|
||
* is usually shown with a marker or other indicator on the square.
|
||
*/
|
||
get markedTarget() {
|
||
return this._markedTarget;
|
||
}
|
||
set markedTarget(value) {
|
||
this._markedTarget = value;
|
||
this._contentElement.classList.toggle("marked-target", value);
|
||
}
|
||
/**
|
||
* Rendered width of element (in integer), used in making drag threshold calculations.
|
||
*/
|
||
get width() {
|
||
return this._contentElement.clientWidth;
|
||
}
|
||
/**
|
||
* Get explicit position of primary piece, if set.
|
||
*/
|
||
get explicitPiecePosition() {
|
||
return this._boardPiece?.explicitPosition;
|
||
}
|
||
/**
|
||
* Focus element associated with square.
|
||
*/
|
||
focus() {
|
||
this._contentElement.focus();
|
||
}
|
||
/**
|
||
* Blur element associated with square.
|
||
*/
|
||
blur() {
|
||
this._contentElement.blur();
|
||
}
|
||
/**
|
||
* Return BoardPiece on this square, if it exists.
|
||
*/
|
||
get boardPiece() {
|
||
return this._boardPiece;
|
||
}
|
||
/**
|
||
* Set primary piece associated with the square. This piece is rendered either
|
||
* directly onto the square (default) or optionally, animating in from an
|
||
* explicit position `animateFromPosition`.
|
||
*
|
||
* If the piece being set is the same as the one already present on the
|
||
* square, and the new piece is not animating in from anywhere, this will
|
||
* be a no-op since the position of the two pieces would otherwise be exactly
|
||
* the same.
|
||
*/
|
||
setPiece(piece, moveable, animation) {
|
||
if (!pieceEqual(this._boardPiece?.piece, piece) || animation) {
|
||
this.clearPiece(animation?.durationMs);
|
||
this._boardPiece = new BoardPiece(this._contentElement, {
|
||
piece,
|
||
animation,
|
||
});
|
||
this.moveable = moveable;
|
||
this._updateSquareAfterPieceChange();
|
||
}
|
||
}
|
||
clearPiece(animationDurationMs) {
|
||
if (this._boardPiece !== undefined) {
|
||
this.moveable = false;
|
||
this._boardPiece.remove(animationDurationMs);
|
||
this._boardPiece = undefined;
|
||
this._updateSquareAfterPieceChange();
|
||
}
|
||
}
|
||
/**
|
||
* Optionally, squares may have a secondary piece, such as a ghost piece shown
|
||
* while dragging. The secondary piece is always shown *behind* the primary
|
||
* piece in the DOM.
|
||
*/
|
||
toggleSecondaryPiece(show) {
|
||
if (show && !this._secondaryBoardPiece && this._boardPiece) {
|
||
this._secondaryBoardPiece = new BoardPiece(this._contentElement, {
|
||
piece: this._boardPiece.piece,
|
||
secondary: true,
|
||
});
|
||
}
|
||
if (!show) {
|
||
if (this._secondaryBoardPiece !== undefined) {
|
||
this._secondaryBoardPiece.remove();
|
||
}
|
||
this._secondaryBoardPiece = undefined;
|
||
}
|
||
}
|
||
/**
|
||
* Mark this square as being interacted with.
|
||
*/
|
||
startInteraction() {
|
||
if (this._boardPiece !== undefined && this.moveable) {
|
||
this._moveState = "move-start";
|
||
this._updateMoveStateVisuals();
|
||
this._updateLabelVisuals();
|
||
this._boardPiece.finishAnimations();
|
||
}
|
||
}
|
||
/**
|
||
* Set piece to explicit pixel location. Ignore if square has no piece.
|
||
*/
|
||
displacePiece(x, y) {
|
||
this._boardPiece?.setExplicitPosition({ type: "coordinates", x, y });
|
||
}
|
||
/**
|
||
* Set piece back to original location. Ignore if square has no piece.
|
||
*/
|
||
resetPiecePosition(animateDurationMs) {
|
||
this._boardPiece?.resetPosition(animateDurationMs);
|
||
}
|
||
/**
|
||
* Cancel ongoing interaction and reset position.
|
||
*/
|
||
cancelInteraction(animateDurationMs) {
|
||
this._moveState = undefined;
|
||
this._updateMoveStateVisuals();
|
||
this._updateLabelVisuals();
|
||
this.resetPiecePosition(animateDurationMs);
|
||
}
|
||
_updateLabelVisuals() {
|
||
this._contentElement.dataset.square = this.label;
|
||
this._contentElement.dataset.squareColor = getSquareColor(this.label);
|
||
const labelParts = [
|
||
this._boardPiece
|
||
? `${this.label}, ${this._boardPiece.piece.color} ${this._boardPiece.piece.pieceType}`
|
||
: `${this.label}`,
|
||
];
|
||
if (this._moveState === "move-start") {
|
||
labelParts.push("start of move");
|
||
}
|
||
if (this._moveState === "move-target") {
|
||
labelParts.push("target square");
|
||
}
|
||
this._contentElement.setAttribute("aria-label", labelParts.join(", "));
|
||
this._slotElement.name = this.label;
|
||
}
|
||
_updateTabIndex() {
|
||
if (this.interactive) {
|
||
this._contentElement.tabIndex = this.tabbable ? 0 : -1;
|
||
}
|
||
else {
|
||
this._contentElement.removeAttribute("tabindex");
|
||
}
|
||
}
|
||
_updateMoveStateVisuals() {
|
||
this._updateInteractiveCssClass("moveable", this.moveable && !this._moveState);
|
||
this._updateInteractiveCssClass("move-start", this._moveState === "move-start");
|
||
this._updateInteractiveCssClass("move-target", this._moveState === "move-target");
|
||
this._contentElement.setAttribute("aria-disabled", (!this._moveState && !this.moveable).toString());
|
||
}
|
||
_updateInteractiveCssClass(name, value) {
|
||
this._contentElement.classList.toggle(name, this.interactive && value);
|
||
}
|
||
_updateSquareAfterPieceChange() {
|
||
this._contentElement.classList.toggle("has-piece", !!this._boardPiece);
|
||
// Always cancel ongoing interactions when piece changes
|
||
this._moveState = undefined;
|
||
this._updateMoveStateVisuals();
|
||
// Ensure secondary piece is toggled off if piece is changed
|
||
this.toggleSecondaryPiece(false);
|
||
// Update label
|
||
this._updateLabelVisuals();
|
||
}
|
||
}
|
||
|
||
class Board {
|
||
/**
|
||
* Creates a set of elements representing chessboard squares, as well
|
||
* as managing and displaying pieces rendered on the squares.
|
||
*/
|
||
constructor(initValues, dispatchEvent, shadowRef) {
|
||
Object.defineProperty(this, "_table", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_boardSquares", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_dispatchEvent", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_shadowRef", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_orientation", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_turn", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_interactive", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_position", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_boardState", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_tabbableSquare", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_defaultTabbableSquare", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
/**
|
||
* Certain move "finishing" logic is included in `pointerup` (e.g. drags). To
|
||
* prevent re-handling this in the `click` handler, we prevent handling of click
|
||
* events for a certain period after pointerup.
|
||
*/
|
||
Object.defineProperty(this, "_preventClickHandling", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
// Event handlers
|
||
Object.defineProperty(this, "_pointerDownHandler", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_pointerUpHandler", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_pointerMoveHandler", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_clickHandler", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_focusInHandler", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_keyDownHandler", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
/**
|
||
* Duration (in milliseconds) for all animations.
|
||
*/
|
||
Object.defineProperty(this, "animationDurationMs", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_slotChangeHandler", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: (e) => {
|
||
if (Board._isSlotElement(e.target) && keyIsSquare(e.target.name)) {
|
||
this._getBoardSquare(e.target.name).hasContent =
|
||
e.target.assignedElements().length > 0;
|
||
}
|
||
}
|
||
});
|
||
Object.defineProperty(this, "_transitionHandler", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: (e) => {
|
||
// Delete transition-property style at the end of all transitions
|
||
if (e.target && e.target.style !== undefined) {
|
||
const style = e.target.style;
|
||
style.removeProperty("transition-property");
|
||
}
|
||
}
|
||
});
|
||
this._boardSquares = new Array(64);
|
||
this._orientation = initValues.orientation;
|
||
this.animationDurationMs = initValues.animationDurationMs;
|
||
this._interactive = false;
|
||
this._position = {};
|
||
this._boardState = { id: "default" };
|
||
this._dispatchEvent = dispatchEvent;
|
||
this._shadowRef = shadowRef;
|
||
// Bottom left corner
|
||
this._defaultTabbableSquare = getSquare(56, initValues.orientation);
|
||
this._table = makeHTMLElement("table", {
|
||
attributes: {
|
||
role: "table",
|
||
"aria-label": "Chess board",
|
||
},
|
||
classes: ["board"],
|
||
});
|
||
for (let i = 0; i < 8; i++) {
|
||
const row = makeHTMLElement("tr", {
|
||
attributes: { role: "row" },
|
||
});
|
||
for (let j = 0; j < 8; j++) {
|
||
const idx = 8 * i + j;
|
||
const square = getSquare(idx, this.orientation);
|
||
this._boardSquares[idx] = new BoardSquare(row, square);
|
||
}
|
||
this._table.appendChild(row);
|
||
}
|
||
this._getBoardSquare(this._defaultTabbableSquare).tabbable = true;
|
||
this._pointerDownHandler = this._makeEventHandler(this._handlePointerDown);
|
||
this._pointerUpHandler = this._makeEventHandler(this._handlePointerUp);
|
||
this._pointerMoveHandler = this._makeEventHandler(this._handlePointerMove);
|
||
this._clickHandler = this._makeEventHandler(this._handleClick);
|
||
this._keyDownHandler = this._makeEventHandler(this._handleKeyDown);
|
||
this._focusInHandler = this._makeEventHandler(this._handleFocusIn);
|
||
this._table.addEventListener("pointerdown", this._pointerDownHandler);
|
||
this._table.addEventListener("click", this._clickHandler);
|
||
this._table.addEventListener("focusin", this._focusInHandler);
|
||
this._table.addEventListener("keydown", this._keyDownHandler);
|
||
this._table.addEventListener("slotchange", this._slotChangeHandler);
|
||
this._table.addEventListener("transitionend", this._transitionHandler);
|
||
this._table.addEventListener("transitioncancel", this._transitionHandler);
|
||
}
|
||
/**
|
||
* Add event listeners that operate outside shadow DOM (pointer up and move).
|
||
* These listeners should be unbound when the element is removed from the DOM.
|
||
*/
|
||
addGlobalListeners() {
|
||
document.addEventListener("pointerup", this._pointerUpHandler);
|
||
document.addEventListener("pointermove", this._pointerMoveHandler);
|
||
}
|
||
/**
|
||
* Removes global listeners for pointer up and move.
|
||
*/
|
||
removeGlobalListeners() {
|
||
document.removeEventListener("pointerup", this._pointerUpHandler);
|
||
document.removeEventListener("pointermove", this._pointerMoveHandler);
|
||
}
|
||
/**
|
||
* HTML element associated with board.
|
||
*/
|
||
get element() {
|
||
return this._table;
|
||
}
|
||
/**
|
||
* What side's perspective to render squares from (what color appears on
|
||
* the bottom as viewed on the screen).
|
||
*/
|
||
get orientation() {
|
||
return this._orientation;
|
||
}
|
||
set orientation(value) {
|
||
this._cancelMove(false);
|
||
this._orientation = value;
|
||
this._refreshDefaultTabbableSquare();
|
||
for (let i = 0; i < 64; i++) {
|
||
const square = getSquare(i, value);
|
||
const piece = this._position[square];
|
||
this._boardSquares[i].label = square;
|
||
this._boardSquares[i].tabbable = this.tabbableSquare === square;
|
||
if (piece) {
|
||
this._boardSquares[i].setPiece(piece, this._pieceMoveable(piece));
|
||
}
|
||
else {
|
||
this._boardSquares[i].clearPiece();
|
||
}
|
||
}
|
||
// Switch focused square, if any, on orientation change
|
||
if (this._focusedSquare) {
|
||
this._focusTabbableSquare();
|
||
}
|
||
}
|
||
/**
|
||
* Whether the grid is interactive. This determines the roles and attributes,
|
||
* like tabindex, associated with the grid.
|
||
*/
|
||
get interactive() {
|
||
return this._interactive;
|
||
}
|
||
set interactive(value) {
|
||
this._cancelMove(false);
|
||
this._interactive = value;
|
||
this._blurTabbableSquare();
|
||
this._table.setAttribute("role", value ? "grid" : "table");
|
||
this._boardSquares.forEach((s) => {
|
||
s.interactive = value;
|
||
});
|
||
this._resetBoardStateAndMoves();
|
||
}
|
||
get turn() {
|
||
return this._turn;
|
||
}
|
||
/**
|
||
* What side is allowed to move pieces. This may be undefined, in which
|
||
* pieces from either side can be moved around.
|
||
*/
|
||
set turn(value) {
|
||
this._cancelMove(false);
|
||
this._turn = value;
|
||
for (let idx = 0; idx < 64; idx++) {
|
||
const square = getSquare(idx, this.orientation);
|
||
const piece = this._position[square];
|
||
this._boardSquares[idx].moveable = !piece || this._pieceMoveable(piece);
|
||
}
|
||
}
|
||
/**
|
||
* Current `Position` object of board.
|
||
*/
|
||
get position() {
|
||
return this._position;
|
||
}
|
||
set position(value) {
|
||
if (!positionsEqual(this._position, value)) {
|
||
this._cancelMove(false);
|
||
const diff = calcPositionDiff(this._position, value);
|
||
this._position = { ...value };
|
||
diff.moved.forEach(({ oldSquare }) => {
|
||
// Remove all copies of moved piece from starting squares, without animation
|
||
this._getBoardSquare(oldSquare).clearPiece();
|
||
});
|
||
diff.removed.forEach(({ square }) => {
|
||
this._getBoardSquare(square).clearPiece(this.animationDurationMs);
|
||
});
|
||
diff.moved.forEach(({ piece, oldSquare, newSquare }) => {
|
||
// Render moved piece at location of old square, and animate in to new square
|
||
const startingPosition = this._getStartingPositionForMove(oldSquare, newSquare);
|
||
this._getBoardSquare(newSquare).setPiece(piece, this._pieceMoveable(piece), {
|
||
type: "slide-in",
|
||
from: startingPosition,
|
||
durationMs: this.animationDurationMs,
|
||
});
|
||
});
|
||
diff.added.forEach(({ piece, square }) => {
|
||
this._getBoardSquare(square).setPiece(piece, this._pieceMoveable(piece), {
|
||
type: "fade-in",
|
||
durationMs: this.animationDurationMs,
|
||
});
|
||
});
|
||
// Default tabbable square might change with position change
|
||
this._refreshDefaultTabbableSquare();
|
||
}
|
||
}
|
||
/**
|
||
* Square that is considered "tabbable", if any. Keyboard navigation
|
||
* on the board uses a roving tabindex, which means that only one square is
|
||
* "tabbable" at a time (the rest are navigable using up and down keys on
|
||
* the keyboard).
|
||
*/
|
||
get tabbableSquare() {
|
||
return this._tabbableSquare || this._defaultTabbableSquare;
|
||
}
|
||
set tabbableSquare(value) {
|
||
if (this.tabbableSquare !== value) {
|
||
// Unset previous tabbable square so that tabindex is changed to -1
|
||
this._getBoardSquare(this.tabbableSquare).tabbable = false;
|
||
this._getBoardSquare(value).tabbable = true;
|
||
this._tabbableSquare = value;
|
||
}
|
||
}
|
||
/**
|
||
* Start a move on the board at `square`, optionally with specified targets
|
||
* at `targetSquares`.
|
||
*/
|
||
startMove(square, targetSquares) {
|
||
if (this._interactable(square)) {
|
||
this._setBoardState({
|
||
id: "awaiting-second-touch",
|
||
startSquare: square,
|
||
});
|
||
this._startInteraction(square, targetSquares);
|
||
}
|
||
}
|
||
/**
|
||
* Cancels in-progress moves, if any.
|
||
*/
|
||
cancelMove() {
|
||
this._cancelMove(false);
|
||
}
|
||
get _focusedSquare() {
|
||
return Board._extractSquareData(this._shadowRef.activeElement);
|
||
}
|
||
_startInteraction(square, forceTargetSquares) {
|
||
const piece = this._position[square];
|
||
if (piece) {
|
||
let targetsLimited = false;
|
||
const targetSquares = [];
|
||
if (forceTargetSquares !== undefined) {
|
||
targetsLimited = true;
|
||
forceTargetSquares.forEach((s) => {
|
||
if (keyIsSquare(s)) {
|
||
targetSquares.push(s);
|
||
}
|
||
});
|
||
}
|
||
else {
|
||
this._dispatchEvent(new CustomEvent("movestart", {
|
||
bubbles: true,
|
||
detail: {
|
||
from: square,
|
||
piece,
|
||
setTargets: (squares) => {
|
||
targetsLimited = true;
|
||
for (const s of squares) {
|
||
if (keyIsSquare(s)) {
|
||
targetSquares.push(s);
|
||
}
|
||
}
|
||
},
|
||
},
|
||
}));
|
||
}
|
||
this._getBoardSquare(square).startInteraction();
|
||
this.tabbableSquare = square;
|
||
this._boardSquares.forEach((s) => {
|
||
if (s.label !== square) {
|
||
s.moveTarget = !targetsLimited || targetSquares.includes(s.label);
|
||
s.markedTarget = targetsLimited && s.moveTarget;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
_finishMove(to, animate) {
|
||
if (this._boardState.startSquare) {
|
||
const from = this._boardState.startSquare;
|
||
const piece = this._position[from];
|
||
if (piece !== undefined) {
|
||
const endEvent = new CustomEvent("moveend", {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
detail: { from, to, piece },
|
||
});
|
||
this._dispatchEvent(endEvent);
|
||
if (endEvent.defaultPrevented) {
|
||
return false;
|
||
}
|
||
const startingPosition = this._getStartingPositionForMove(from, to);
|
||
this._getBoardSquare(from).clearPiece();
|
||
this._getBoardSquare(to).setPiece(piece, this._pieceMoveable(piece),
|
||
// Animate transition only when piece is displaced to a specific location
|
||
animate
|
||
? {
|
||
type: "slide-in",
|
||
from: startingPosition,
|
||
durationMs: this.animationDurationMs,
|
||
}
|
||
: undefined);
|
||
// Tabbable square always updates to target square
|
||
this.tabbableSquare = to;
|
||
this._position[to] = this._position[from];
|
||
delete this._position[from];
|
||
const finishedEvent = new CustomEvent("movefinished", {
|
||
bubbles: true,
|
||
detail: { from, to, piece },
|
||
});
|
||
if (animate) {
|
||
this._getBoardSquare(to).boardPiece?.animationFinished?.then(() => {
|
||
this._dispatchEvent(finishedEvent);
|
||
});
|
||
}
|
||
else {
|
||
this._dispatchEvent(finishedEvent);
|
||
}
|
||
}
|
||
this._resetBoardStateAndMoves();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
_userCancelMove(animate) {
|
||
if (this._boardState.startSquare) {
|
||
const e = new CustomEvent("movecancel", {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
detail: {
|
||
from: this._boardState.startSquare,
|
||
piece: this._position[this._boardState.startSquare],
|
||
},
|
||
});
|
||
this._dispatchEvent(e);
|
||
if (!e.defaultPrevented) {
|
||
this._cancelMove(animate);
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
_cancelMove(animate) {
|
||
if (this._boardState.startSquare) {
|
||
const square = this._getBoardSquare(this._boardState.startSquare);
|
||
square.cancelInteraction(animate ? this.animationDurationMs : undefined);
|
||
}
|
||
this._resetBoardStateAndMoves();
|
||
}
|
||
_focusTabbableSquare() {
|
||
if (this.tabbableSquare) {
|
||
this._getBoardSquare(this.tabbableSquare).focus();
|
||
}
|
||
}
|
||
_blurTabbableSquare() {
|
||
if (this.tabbableSquare) {
|
||
this._getBoardSquare(this.tabbableSquare).blur();
|
||
}
|
||
}
|
||
_resetBoardStateAndMoves() {
|
||
this._boardSquares.forEach((s) => {
|
||
s.removeMoveState();
|
||
s.markedTarget = false;
|
||
});
|
||
this._setBoardState({
|
||
id: this.interactive ? "awaiting-input" : "default",
|
||
});
|
||
}
|
||
_pieceMoveable(piece) {
|
||
return !this.turn || piece.color === this.turn;
|
||
}
|
||
_interactable(square) {
|
||
const piece = this._position[square];
|
||
return !!piece && this._pieceMoveable(piece);
|
||
}
|
||
_isValidMove(from, to) {
|
||
return from !== to && this._getBoardSquare(to).moveTarget;
|
||
}
|
||
_getBoardSquare(square) {
|
||
return this._boardSquares[getVisualIndex(square, this.orientation)];
|
||
}
|
||
/**
|
||
* Compute an explicit position to apply to a piece that is being moved
|
||
* from `from` to `to`. This can either be the explicit piece position,
|
||
* if already set, for that piece, or it is computed as the offset or
|
||
* difference in rows and columns between the two squares.
|
||
*/
|
||
_getStartingPositionForMove(from, to) {
|
||
const [fromRow, fromCol] = getVisualRowColumn(from, this.orientation);
|
||
const [toRow, toCol] = getVisualRowColumn(to, this.orientation);
|
||
return (this._getBoardSquare(from).explicitPiecePosition || {
|
||
type: "squareOffset",
|
||
deltaRows: fromRow - toRow,
|
||
deltaCols: fromCol - toCol,
|
||
});
|
||
}
|
||
/**
|
||
* When no tabbable square has been explicitly set (usually, when user has
|
||
* not yet tabbed into or interacted with the board, we want to calculate
|
||
* the tabbable square dynamically. It is either:
|
||
* - the first occupied square from the player's orientation (i.e. from
|
||
* bottom left of board), or
|
||
* - the bottom left square of the board.
|
||
*/
|
||
_refreshDefaultTabbableSquare() {
|
||
const oldDefaultSquare = this._defaultTabbableSquare;
|
||
let pieceFound = false;
|
||
if (Object.keys(this._position).length > 0) {
|
||
for (let row = 7; row >= 0 && !pieceFound; row--) {
|
||
for (let col = 0; col <= 7 && !pieceFound; col++) {
|
||
const square = getSquare(8 * row + col, this.orientation);
|
||
if (this._position[square]) {
|
||
this._defaultTabbableSquare = square;
|
||
pieceFound = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (!pieceFound) {
|
||
this._defaultTabbableSquare = getSquare(56, this.orientation);
|
||
}
|
||
// If tabbable square is set to default and has changed, then
|
||
// update the two squares accordingly.
|
||
if (this._tabbableSquare === undefined &&
|
||
oldDefaultSquare !== this._defaultTabbableSquare) {
|
||
this._getBoardSquare(oldDefaultSquare).tabbable = false;
|
||
this._getBoardSquare(this._defaultTabbableSquare).tabbable = true;
|
||
}
|
||
}
|
||
_setBoardState(state) {
|
||
const oldState = this._boardState;
|
||
this._boardState = state;
|
||
if (this._boardState.id !== oldState.id) {
|
||
this._table.classList.toggle("dragging", this._isDragState());
|
||
}
|
||
if (this._boardState.highlightedSquare !== oldState.highlightedSquare) {
|
||
if (oldState.highlightedSquare) {
|
||
this._getBoardSquare(oldState.highlightedSquare).hover = false;
|
||
}
|
||
if (this._boardState.highlightedSquare) {
|
||
this._getBoardSquare(this._boardState.highlightedSquare).hover = true;
|
||
}
|
||
}
|
||
}
|
||
_handlePointerDown(square, e) {
|
||
// We will control focus entirely ourselves
|
||
e.preventDefault();
|
||
// Primary clicks only
|
||
if (e.button !== 0) {
|
||
return;
|
||
}
|
||
switch (this._boardState.id) {
|
||
case "awaiting-input":
|
||
if (square && this._interactable(square)) {
|
||
this._setBoardState({
|
||
id: "touching-first-square",
|
||
startSquare: square,
|
||
touchStartX: e.clientX,
|
||
touchStartY: e.clientY,
|
||
});
|
||
this._startInteraction(square);
|
||
this._getBoardSquare(square).toggleSecondaryPiece(true);
|
||
}
|
||
break;
|
||
case "awaiting-second-touch":
|
||
case "moving-piece-kb":
|
||
if (this._boardState.startSquare === square) {
|
||
// Second pointerdown on the same square *may* be a cancel, but could
|
||
// also be a misclick/readjustment in order to begin dragging. Wait
|
||
// till corresponding pointerup event in order to cancel.
|
||
this._setBoardState({
|
||
id: "canceling-second-touch",
|
||
startSquare: square,
|
||
touchStartX: e.clientX,
|
||
touchStartY: e.clientY,
|
||
});
|
||
// Show secondary piece while pointer is down
|
||
this._getBoardSquare(square).toggleSecondaryPiece(true);
|
||
}
|
||
else if (square) {
|
||
this._setBoardState({
|
||
id: "touching-second-square",
|
||
startSquare: this._boardState.startSquare,
|
||
});
|
||
}
|
||
break;
|
||
case "dragging":
|
||
case "dragging-outside":
|
||
case "canceling-second-touch":
|
||
case "touching-first-square":
|
||
case "touching-second-square":
|
||
// Noop: pointer is already down while dragging or touching square
|
||
break;
|
||
case "default":
|
||
break;
|
||
// istanbul ignore next
|
||
default:
|
||
assertUnreachable(this._boardState);
|
||
}
|
||
}
|
||
_handlePointerUp(square) {
|
||
let newFocusedSquare = square;
|
||
switch (this._boardState.id) {
|
||
case "touching-first-square":
|
||
this._getBoardSquare(this._boardState.startSquare).toggleSecondaryPiece(false);
|
||
this._setBoardState({
|
||
id: "awaiting-second-touch",
|
||
startSquare: this._boardState.startSquare,
|
||
});
|
||
newFocusedSquare = this._boardState.startSquare;
|
||
break;
|
||
case "canceling-second-touch":
|
||
// User cancels by clicking on the same square.
|
||
if (!this._userCancelMove(false)) {
|
||
this._setBoardState({
|
||
id: "awaiting-second-touch",
|
||
startSquare: this._boardState.startSquare,
|
||
});
|
||
}
|
||
newFocusedSquare = this._boardState.startSquare;
|
||
break;
|
||
case "dragging":
|
||
case "dragging-outside":
|
||
case "touching-second-square":
|
||
{
|
||
this._getBoardSquare(this._boardState.startSquare).toggleSecondaryPiece(false);
|
||
let done = false;
|
||
if (square &&
|
||
this._isValidMove(this._boardState.startSquare, square)) {
|
||
done = this._finishMove(square, !this._isDragState());
|
||
if (!done) {
|
||
newFocusedSquare = this._boardState.startSquare;
|
||
}
|
||
}
|
||
else {
|
||
newFocusedSquare = this._boardState.startSquare;
|
||
done = this._userCancelMove(square !== this._boardState.startSquare);
|
||
}
|
||
if (!done) {
|
||
this._setBoardState({
|
||
id: "awaiting-second-touch",
|
||
startSquare: this._boardState.startSquare,
|
||
});
|
||
this._getBoardSquare(this._boardState.startSquare).resetPiecePosition(this.animationDurationMs);
|
||
}
|
||
}
|
||
break;
|
||
case "awaiting-input":
|
||
case "moving-piece-kb":
|
||
case "awaiting-second-touch":
|
||
// noop: Either we are in a non-mouse state or we are delegating to click
|
||
break;
|
||
case "default":
|
||
break;
|
||
// istanbul ignore next
|
||
default:
|
||
assertUnreachable(this._boardState);
|
||
}
|
||
// If board currently has focus, move focus to newly clicked square.
|
||
if (this._focusedSquare && newFocusedSquare) {
|
||
this.tabbableSquare = newFocusedSquare;
|
||
this._focusTabbableSquare();
|
||
}
|
||
// Prevent click handling for a certain duration
|
||
this._preventClickHandling = true;
|
||
setTimeout(() => {
|
||
this._preventClickHandling = false;
|
||
}, Board.POINTERUP_CLICK_PREVENT_DURATION_MS);
|
||
}
|
||
_handlePointerMove(square, e) {
|
||
switch (this._boardState.id) {
|
||
case "canceling-second-touch":
|
||
case "touching-first-square":
|
||
{
|
||
const delta = Math.sqrt((e.clientX - this._boardState.touchStartX) ** 2 +
|
||
(e.clientY - this._boardState.touchStartY) ** 2);
|
||
const squareWidth = this._getBoardSquare(this._boardState.startSquare).width;
|
||
const threshold = Math.max(Board.DRAG_THRESHOLD_MIN_PIXELS, Board.DRAG_THRESHOLD_SQUARE_WIDTH_FRACTION * squareWidth);
|
||
// Consider a "dragging" action to be when we have moved the pointer a sufficient
|
||
// threshold, or we are now in a different square from where we started.
|
||
if (delta > threshold || square !== this._boardState.startSquare) {
|
||
this._getBoardSquare(this._boardState.startSquare).displacePiece(e.clientX, e.clientY);
|
||
if (square) {
|
||
this._setBoardState({
|
||
id: "dragging",
|
||
startSquare: this._boardState.startSquare,
|
||
highlightedSquare: this._isValidMove(this._boardState.startSquare, square)
|
||
? square
|
||
: undefined,
|
||
});
|
||
}
|
||
else {
|
||
this._setBoardState({
|
||
id: "dragging-outside",
|
||
startSquare: this._boardState.startSquare,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
case "dragging":
|
||
case "dragging-outside":
|
||
this._getBoardSquare(this._boardState.startSquare).displacePiece(e.clientX, e.clientY);
|
||
if (square && square !== this._boardState.highlightedSquare) {
|
||
this._setBoardState({
|
||
id: "dragging",
|
||
startSquare: this._boardState.startSquare,
|
||
highlightedSquare: this._isValidMove(this._boardState.startSquare, square)
|
||
? square
|
||
: undefined,
|
||
});
|
||
}
|
||
else if (!square && this._boardState.id !== "dragging-outside") {
|
||
this._setBoardState({
|
||
id: "dragging-outside",
|
||
startSquare: this._boardState.startSquare,
|
||
});
|
||
}
|
||
break;
|
||
case "awaiting-input":
|
||
case "awaiting-second-touch":
|
||
case "default":
|
||
case "moving-piece-kb":
|
||
case "touching-second-square":
|
||
break;
|
||
// istanbul ignore next
|
||
default:
|
||
assertUnreachable(this._boardState);
|
||
}
|
||
}
|
||
_handleClick(square) {
|
||
if (this._preventClickHandling) {
|
||
return;
|
||
}
|
||
switch (this._boardState.id) {
|
||
case "awaiting-input":
|
||
if (square && this._interactable(square)) {
|
||
this._setBoardState({
|
||
id: "awaiting-second-touch",
|
||
startSquare: square,
|
||
});
|
||
this._startInteraction(square);
|
||
}
|
||
break;
|
||
case "awaiting-second-touch":
|
||
case "moving-piece-kb":
|
||
{
|
||
const done = square && this._isValidMove(this._boardState.startSquare, square)
|
||
? this._finishMove(square, true)
|
||
: this._userCancelMove(square !== this._boardState.startSquare);
|
||
if (!done) {
|
||
this._setBoardState({
|
||
id: "awaiting-second-touch",
|
||
startSquare: this._boardState.startSquare,
|
||
});
|
||
this._getBoardSquare(this._boardState.startSquare).resetPiecePosition(this.animationDurationMs);
|
||
}
|
||
}
|
||
break;
|
||
case "touching-first-square":
|
||
case "touching-second-square":
|
||
case "canceling-second-touch":
|
||
case "dragging":
|
||
case "dragging-outside":
|
||
case "default":
|
||
break;
|
||
// istanbul ignore next
|
||
default:
|
||
assertUnreachable(this._boardState);
|
||
}
|
||
// If board currently has focus, move focus to newly clicked square.
|
||
if (this._focusedSquare && square) {
|
||
this.tabbableSquare = square;
|
||
this._focusTabbableSquare();
|
||
}
|
||
}
|
||
_handleFocusIn(square) {
|
||
if (square) {
|
||
if (
|
||
// Some browsers (Safari) focus on board squares that are not tabbable
|
||
// (tabindex = -1). If that happens, update tabbable square manually.
|
||
square !== this.tabbableSquare ||
|
||
// Assign tabbable square if none is explicitly assigned yet.
|
||
this._tabbableSquare === undefined) {
|
||
this.tabbableSquare = square;
|
||
}
|
||
}
|
||
}
|
||
_handleKeyDown(square, e) {
|
||
if (e.key === "Enter" || e.key === " ") {
|
||
e.preventDefault();
|
||
switch (this._boardState.id) {
|
||
case "awaiting-input":
|
||
if (square && this._interactable(square)) {
|
||
this._setBoardState({
|
||
id: "moving-piece-kb",
|
||
startSquare: square,
|
||
highlightedSquare: undefined,
|
||
});
|
||
this._startInteraction(square);
|
||
}
|
||
break;
|
||
case "moving-piece-kb":
|
||
case "awaiting-second-touch":
|
||
// Only move if enter was inside squares area and if start
|
||
// and end square are not the same.
|
||
if (square &&
|
||
this._isValidMove(this._boardState.startSquare, square)) {
|
||
this._finishMove(square, true);
|
||
}
|
||
else {
|
||
this._userCancelMove(false);
|
||
}
|
||
break;
|
||
case "dragging":
|
||
case "dragging-outside":
|
||
case "touching-first-square":
|
||
case "touching-second-square":
|
||
case "canceling-second-touch":
|
||
// Noop: don't handle keypresses in active pointer states
|
||
break;
|
||
case "default":
|
||
break;
|
||
// istanbul ignore next
|
||
default:
|
||
assertUnreachable(this._boardState);
|
||
}
|
||
}
|
||
else {
|
||
const currentIdx = getVisualIndex(this.tabbableSquare, this.orientation);
|
||
const currentRow = currentIdx >> 3;
|
||
const currentCol = currentIdx & 0x7;
|
||
let newIdx = currentIdx;
|
||
let keyHandled = false;
|
||
switch (e.key) {
|
||
case "ArrowRight":
|
||
case "Right":
|
||
newIdx = 8 * currentRow + Math.min(7, currentCol + 1);
|
||
keyHandled = true;
|
||
break;
|
||
case "ArrowLeft":
|
||
case "Left":
|
||
newIdx = 8 * currentRow + Math.max(0, currentCol - 1);
|
||
keyHandled = true;
|
||
break;
|
||
case "ArrowDown":
|
||
case "Down":
|
||
newIdx = 8 * Math.min(7, currentRow + 1) + currentCol;
|
||
keyHandled = true;
|
||
break;
|
||
case "ArrowUp":
|
||
case "Up":
|
||
newIdx = 8 * Math.max(0, currentRow - 1) + currentCol;
|
||
keyHandled = true;
|
||
break;
|
||
case "Home":
|
||
newIdx = e.ctrlKey ? 0 : 8 * currentRow;
|
||
keyHandled = true;
|
||
break;
|
||
case "End":
|
||
newIdx = e.ctrlKey ? 63 : 8 * currentRow + 7;
|
||
keyHandled = true;
|
||
break;
|
||
case "PageUp":
|
||
newIdx = currentCol;
|
||
keyHandled = true;
|
||
break;
|
||
case "PageDown":
|
||
newIdx = 56 + currentCol;
|
||
keyHandled = true;
|
||
break;
|
||
}
|
||
if (keyHandled) {
|
||
// Prevent native browser scrolling via any of the
|
||
// navigation keys since the focus below will auto-scroll
|
||
e.preventDefault();
|
||
}
|
||
if (newIdx !== currentIdx) {
|
||
this.tabbableSquare = getSquare(newIdx, this.orientation);
|
||
this._focusTabbableSquare();
|
||
// If we are currently in a non-keyboard friendly state, we should
|
||
// still transition to one since we started keyboard navigation.
|
||
switch (this._boardState.id) {
|
||
case "moving-piece-kb":
|
||
case "awaiting-second-touch":
|
||
this._setBoardState({
|
||
id: "moving-piece-kb",
|
||
startSquare: this._boardState.startSquare,
|
||
highlightedSquare: this._boardState.startSquare !== this.tabbableSquare
|
||
? this._tabbableSquare
|
||
: undefined,
|
||
});
|
||
break;
|
||
case "awaiting-input":
|
||
case "touching-first-square":
|
||
case "touching-second-square":
|
||
case "canceling-second-touch":
|
||
case "dragging":
|
||
case "dragging-outside":
|
||
break;
|
||
case "default":
|
||
break;
|
||
// istanbul ignore next
|
||
default:
|
||
assertUnreachable(this._boardState);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Convenience wrapper to make pointer, blur, or keyboard event handler for
|
||
* square elements. Attempts to extract square label from the element in
|
||
* question, then passes square label and current event to `callback`.
|
||
*/
|
||
_makeEventHandler(callback) {
|
||
const boundCallback = callback.bind(this);
|
||
return (e) => {
|
||
// For mouse events, use client X and Y location to find target reliably.
|
||
const square = Board._isMouseEvent(e)
|
||
? this._shadowRef
|
||
.elementsFromPoint(e.clientX, e.clientY)
|
||
.map((e) => Board._extractSquareData(e))
|
||
.find((e) => !!e)
|
||
: Board._extractSquareData(e.target);
|
||
boundCallback(square, e);
|
||
};
|
||
}
|
||
_isDragState() {
|
||
return ["dragging", "dragging-outside"].includes(this._boardState.id);
|
||
}
|
||
static _extractSquareData(target) {
|
||
if (!!target && !!target.dataset) {
|
||
const dataset = target.dataset;
|
||
return keyIsSquare(dataset.square) ? dataset.square : undefined;
|
||
}
|
||
return undefined;
|
||
}
|
||
static _isMouseEvent(e) {
|
||
return e.clientX !== undefined;
|
||
}
|
||
static _isSlotElement(e) {
|
||
return !!e && e.assignedElements !== undefined;
|
||
}
|
||
}
|
||
/**
|
||
* Fraction of square width that pointer must be moved to be
|
||
* considered a "drag" action.
|
||
*/
|
||
Object.defineProperty(Board, "DRAG_THRESHOLD_SQUARE_WIDTH_FRACTION", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: 0.1
|
||
});
|
||
/**
|
||
* Minimum number of pixels to enable dragging.
|
||
*/
|
||
Object.defineProperty(Board, "DRAG_THRESHOLD_MIN_PIXELS", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: 2
|
||
});
|
||
/**
|
||
* Amount of time (in ms) to suppress click handling after a pointerup event.
|
||
*/
|
||
Object.defineProperty(Board, "POINTERUP_CLICK_PREVENT_DURATION_MS", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: 250
|
||
});
|
||
|
||
var css_248z = ":host{--square-color-dark:#4c946a;--square-color-light:#e0ddcc;--square-color-dark-hover:#1cc45f;--square-color-light-hover:#fde968;--square-color-dark-active:#19c257;--square-color-light-active:#fadd4c;--outline-color-dark-active:rgba(33,237,94,.95);--outline-color-light-active:hsla(66,97%,72%,.95);--outline-color-focus:rgba(248,140,32,.9);--outline-blur-radius:3px;--outline-spread-radius:4px;--coords-font-size:0.7rem;--coords-font-family:sans-serif;--outer-gutter-width:4%;--inner-border-width:1px;--coords-inside-coord-padding-left:0.5%;--coords-inside-coord-padding-right:0.5%;--move-target-marker-color-dark-square:rgba(8,38,20,.9);--move-target-marker-color-light-square:rgba(8,38,20,.9);--move-target-marker-radius:24%;--move-target-marker-radius-occupied:82%;--ghost-piece-opacity:0.35;--piece-drag-z-index:9999;--piece-drag-coarse-scale:2.4;--piece-padding:3%;--arrow-color-primary:rgba(255,170,0,.8);--arrow-color-secondary:rgba(248,85,63,.8);display:block}:host([hidden]){display:none}.board{border:var(--inner-border-width) solid var(--inner-border-color,var(--square-color-dark));border-collapse:collapse;box-sizing:border-box;table-layout:fixed;touch-action:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%}.board>tr>td{padding:12.5% 0 0;position:relative}[data-square]{background-color:var(--p-square-color);bottom:0;color:var(--p-label-color);font-family:var(--coords-font-family);font-size:var(--coords-font-size);height:100%;left:0;position:absolute;right:0;top:0;width:100%}[data-square]:focus{box-shadow:inset 0 0 var(--outline-blur-radius) var(--outline-spread-radius) var(--outline-color-focus);outline:none}[data-square].marked-target{background:radial-gradient(var(--p-move-target-marker-color) var(--move-target-marker-radius),var(--p-square-color) calc(var(--move-target-marker-radius) + 1px))}[data-square].has-content.marked-target,[data-square].has-piece.marked-target{background:radial-gradient(var(--p-square-color) var(--move-target-marker-radius-occupied),var(--p-move-target-marker-color) calc(var(--move-target-marker-radius-occupied) + 1px))}[data-square].move-start{--p-square-color:var(--p-square-color-active)}[data-square].move-start:not(:focus){box-shadow:inset 0 0 var(--outline-blur-radius) var(--outline-spread-radius) var(--p-outline-color-active)}@media (hover:hover){[data-square]:is(.moveable,.move-target):hover{--p-square-color:var(--p-square-color-hover)}}[data-square].hover{--p-square-color:var(--p-square-color-hover)}table:not(.dragging) [data-square]:is(.moveable,.move-start,.move-target){cursor:pointer}table.dragging{cursor:grab}.wrapper{position:relative}.coords{display:none;font-family:var(--coords-font-family);font-size:var(--coords-font-size);pointer-events:none;position:absolute;touch-action:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.coord{box-sizing:border-box;display:flex}.coords.file>.coord{width:12.5%}.coords.rank{flex-direction:column}.coords.rank>.coord{height:12.5%}.wrapper.outside{background-color:var(--square-color-light);padding:var(--outer-gutter-width)}.wrapper.outside>.coords{color:var(--square-color-dark);display:flex}.wrapper.outside>.coords>.coord{align-items:center;justify-content:center}.wrapper.outside>.coords.file{bottom:0;height:var(--outer-gutter-width);left:var(--outer-gutter-width);right:var(--outer-gutter-width);width:calc(100% - var(--outer-gutter-width)*2)}.wrapper.outside>.coords.rank{bottom:var(--outer-gutter-width);height:calc(100% - var(--outer-gutter-width)*2);left:0;top:var(--outer-gutter-width);width:var(--outer-gutter-width)}.wrapper.inside>.coords{bottom:0;display:flex;height:100%;left:0;right:0;top:0;width:100%}.wrapper.inside>.coords>.coord.light{color:var(--square-color-dark)}.wrapper.inside>.coords>.coord.dark{color:var(--square-color-light)}.wrapper.inside>.coords.file>.coord{align-items:flex-end;justify-content:flex-end;padding-right:var(--coords-inside-coord-padding-right)}.wrapper.inside>.coords.rank>.coord{padding-left:var(--coords-inside-coord-padding-left)}[data-square-color=dark]{--p-square-color:var(--square-color-dark);--p-label-color:var(--square-color-light);--p-square-color-hover:var(--square-color-dark-hover);--p-move-target-marker-color:var(--move-target-marker-color-dark-square);--p-square-color-active:var(--square-color-dark-active);--p-outline-color-active:var(--outline-color-dark-active)}[data-square-color=light]{--p-square-color:var(--square-color-light);--p-label-color:var(--square-color-dark);--p-square-color-hover:var(--square-color-light-hover);--p-move-target-marker-color:var(--move-target-marker-color-light-square);--p-square-color-active:var(--square-color-light-active);--p-outline-color-active:var(--outline-color-light-active)}[data-square] .piece,[data-square] .slot{bottom:0;height:100%;left:0;pointer-events:none;position:absolute;right:0;top:0;width:100%}[data-square] .piece{background-origin:content-box;background-repeat:no-repeat;background-size:cover;box-sizing:border-box;padding:var(--piece-padding);z-index:10}[data-square] .piece.moving{z-index:15}[data-square] .piece.secondary{opacity:var(--ghost-piece-opacity);z-index:5}[data-square].move-start .piece:not(.secondary){z-index:var(--piece-drag-z-index)}@media (pointer:coarse){[data-square] .piece{--p-piece-drag-scale:var(--piece-drag-coarse-scale)}}.bb{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='opacity:1;fill:none;fill-rule:evenodd;fill-opacity:1;stroke:%23000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1'%3E%3Cg style='fill:%23000;stroke:%23000;stroke-linecap:butt'%3E%3Cpath d='M9 36.6c3.39-.97 10.11.43 13.5-2 3.39 2.43 10.11 1.03 13.5 2 0 0 1.65.54 3 2-.68.97-1.65.99-3 .5-3.39-.97-10.11.46-13.5-1-3.39 1.46-10.11.03-13.5 1-1.35.49-2.32.47-3-.5 1.35-1.46 3-2 3-2z'/%3E%3Cpath d='M15 32.6c2.5 2.5 12.5 2.5 15 0 .5-1.5 0-2 0-2 0-2.5-2.5-4-2.5-4 5.5-1.5 6-11.5-5-15.5-11 4-10.5 14-5 15.5 0 0-2.5 1.5-2.5 4 0 0-.5.5 0 2z'/%3E%3Cpath d='M25 8.6a2.5 2.5 0 1 1-5 0 2.5 2.5 0 1 1 5 0z'/%3E%3C/g%3E%3Cpath d='M17.5 26h10M15 30h15m-7.5-14.5v5M20 18h5' style='fill:none;stroke:%23fff;stroke-linejoin:miter' transform='translate(0 .6)'/%3E%3C/g%3E%3C/svg%3E\")}.bk{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='fill:none;fill-opacity:1;fill-rule:evenodd;stroke:%23000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1'%3E%3Cpath d='M22.5 11.63V6' style='fill:none;stroke:%23000;stroke-linejoin:miter'/%3E%3Cpath d='M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5' style='fill:%23000;fill-opacity:1;stroke-linecap:butt;stroke-linejoin:miter'/%3E%3Cpath d='M12.5 37c5.5 3.5 14.5 3.5 20 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-2.5-7.5-12-10.5-16-4-3 6 6 10.5 6 10.5v7' style='fill:%23000;stroke:%23000'/%3E%3Cpath d='M20 8h5' style='fill:none;stroke:%23000;stroke-linejoin:miter'/%3E%3Cpath d='M32 29.5s8.5-4 6.03-9.65C34.15 14 25 18 22.5 24.5v2.1-2.1C20 18 10.85 14 6.97 19.85 4.5 25.5 13 29.5 13 29.5' style='fill:none;stroke:%23fff'/%3E%3Cpath d='M12.5 30c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0' style='fill:none;stroke:%23fff'/%3E%3C/g%3E%3C/svg%3E\")}.bn{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='opacity:1;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:%23000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1'%3E%3Cpath d='M22 10c10.5 1 16.5 8 16 29H15c0-9 10-6.5 8-21' style='fill:%23000;stroke:%23000' transform='translate(0 .3)'/%3E%3Cpath d='M24 18c.38 2.91-5.55 7.37-8 9-3 2-2.82 4.34-5 4-1.042-.94 1.41-3.04 0-3-1 0 .19 1.23-1 2-1 0-4.003 1-4-4 0-2 6-12 6-12s1.89-1.9 2-3.5c-.73-.994-.5-2-.5-3 1-1 3 2.5 3 2.5h2s.78-1.992 2.5-3c1 0 1 3 1 3' style='fill:%23000;stroke:%23000' transform='translate(0 .3)'/%3E%3Cpath d='M9.5 25.5a.5.5 0 1 1-1 0 .5.5 0 1 1 1 0z' style='fill:%23fff;stroke:%23fff' transform='translate(0 .3)'/%3E%3Cpath d='M15 15.5a.5 1.5 0 1 1-1 0 .5 1.5 0 1 1 1 0z' transform='rotate(30 13.94 15.65)' style='fill:%23fff;stroke:%23fff'/%3E%3Cpath d='m24.55 10.4-.45 1.45.5.15c3.15 1 5.65 2.49 7.9 6.75S35.75 29.06 35.25 39l-.05.5h2.25l.05-.5c.5-10.06-.88-16.85-3.25-21.34-2.37-4.49-5.79-6.64-9.19-7.16l-.51-.1z' style='fill:%23fff;stroke:none' transform='translate(0 .3)' stroke='none'/%3E%3C/g%3E%3C/svg%3E\")}.bp{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cpath d='M22.5 9c-2.21 0-4 1.79-4 4 0 .89.29 1.71.78 2.38C17.33 16.5 16 18.59 16 21c0 2.03.94 3.84 2.41 5.03-3 1.06-7.41 5.55-7.41 13.47h23c0-7.92-4.41-12.41-7.41-13.47 1.47-1.19 2.41-3 2.41-5.03 0-2.41-1.33-4.5-3.28-5.62.49-.67.78-1.49.78-2.38 0-2.21-1.79-4-4-4z' style='opacity:1;fill:%23000;fill-opacity:1;fill-rule:nonzero;stroke:%23000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1'/%3E%3C/svg%3E\")}.bq{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='fill:%23000;stroke:%23000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round'%3E%3Cpath d='M9 26c8.5-1.5 21-1.5 27 0l2.5-12.5L31 25l-.3-14.1-5.2 13.6-3-14.5-3 14.5-5.2-13.6L14 25 6.5 13.5 9 26z' style='stroke-linecap:butt;fill:%23000'/%3E%3Cpath d='M9 26c0 2 1.5 2 2.5 4 1 1.5 1 1 .5 3.5-1.5 1-1 2.5-1 2.5-1.5 1.5 0 2.5 0 2.5 6.5 1 16.5 1 23 0 0 0 1.5-1 0-2.5 0 0 .5-1.5-1-2.5-.5-2.5-.5-2 .5-3.5 1-2 2.5-2 2.5-4-8.5-1.5-18.5-1.5-27 0z'/%3E%3Cpath d='M11.5 30c3.5-1 18.5-1 22 0M12 33.5c6-1 15-1 21 0'/%3E%3Ccircle cx='6' cy='12' r='2'/%3E%3Ccircle cx='14' cy='9' r='2'/%3E%3Ccircle cx='22.5' cy='8' r='2'/%3E%3Ccircle cx='31' cy='9' r='2'/%3E%3Ccircle cx='39' cy='12' r='2'/%3E%3Cpath d='M11 38.5a35 35 1 0 0 23 0' style='fill:none;stroke:%23000;stroke-linecap:butt'/%3E%3Cpath d='M11 29a35 35 1 0 1 23 0m-21.5 2.5h20m-21 3a35 35 1 0 0 22 0m-23 3a35 35 1 0 0 24 0' style='fill:none;stroke:%23fff'/%3E%3C/g%3E%3C/svg%3E\")}.br{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='opacity:1;fill:%23000;fill-opacity:1;fill-rule:evenodd;stroke:%23000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1'%3E%3Cpath d='M9 39h27v-3H9v3zm3.5-7 1.5-2.5h17l1.5 2.5h-20zm-.5 4v-4h21v4H12z' style='stroke-linecap:butt' transform='translate(0 .3)'/%3E%3Cpath d='M14 29.5v-13h17v13H14z' style='stroke-linecap:butt;stroke-linejoin:miter' transform='translate(0 .3)'/%3E%3Cpath d='M14 16.5 11 14h23l-3 2.5H14zM11 14V9h4v2h5V9h5v2h5V9h4v5H11z' style='stroke-linecap:butt' transform='translate(0 .3)'/%3E%3Cpath d='M12 35.5h21m-20-4h19m-18-2h17m-17-13h17M11 14h23' style='fill:none;stroke:%23fff;stroke-width:1;stroke-linejoin:miter' transform='translate(0 .3)'/%3E%3C/g%3E%3C/svg%3E\")}.wb{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='opacity:1;fill:none;fill-rule:evenodd;fill-opacity:1;stroke:%23000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' transform='translate(0 .6)'%3E%26gt;%3Cg style='fill:%23fff;stroke:%23000;stroke-linecap:butt'%3E%3Cpath d='M9 36c3.39-.97 10.11.43 13.5-2 3.39 2.43 10.11 1.03 13.5 2 0 0 1.65.54 3 2-.68.97-1.65.99-3 .5-3.39-.97-10.11.46-13.5-1-3.39 1.46-10.11.03-13.5 1-1.35.49-2.32.47-3-.5 1.35-1.46 3-2 3-2z'/%3E%3Cpath d='M15 32c2.5 2.5 12.5 2.5 15 0 .5-1.5 0-2 0-2 0-2.5-2.5-4-2.5-4 5.5-1.5 6-11.5-5-15.5-11 4-10.5 14-5 15.5 0 0-2.5 1.5-2.5 4 0 0-.5.5 0 2z'/%3E%3Cpath d='M25 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 1 1 5 0z'/%3E%3C/g%3E%3Cpath d='M17.5 26h10M15 30h15m-7.5-14.5v5M20 18h5' style='fill:none;stroke:%23000;stroke-linejoin:miter'/%3E%3C/g%3E%3C/svg%3E\")}.wk{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='fill:none;fill-opacity:1;fill-rule:evenodd;stroke:%23000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1'%3E%3Cpath d='M22.5 11.63V6M20 8h5' style='fill:none;stroke:%23000;stroke-linejoin:miter'/%3E%3Cpath d='M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5' style='fill:%23fff;stroke:%23000;stroke-linecap:butt;stroke-linejoin:miter'/%3E%3Cpath d='M12.5 37c5.5 3.5 14.5 3.5 20 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-2.5-7.5-12-10.5-16-4-3 6 6 10.5 6 10.5v7' style='fill:%23fff;stroke:%23000'/%3E%3Cpath d='M12.5 30c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0' style='fill:none;stroke:%23000'/%3E%3C/g%3E%3C/svg%3E\")}.wn{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='opacity:1;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:%23000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1'%3E%3Cpath d='M22 10c10.5 1 16.5 8 16 29H15c0-9 10-6.5 8-21' style='fill:%23fff;stroke:%23000' transform='translate(0 .3)'/%3E%3Cpath d='M24 18c.38 2.91-5.55 7.37-8 9-3 2-2.82 4.34-5 4-1.042-.94 1.41-3.04 0-3-1 0 .19 1.23-1 2-1 0-4.003 1-4-4 0-2 6-12 6-12s1.89-1.9 2-3.5c-.73-.994-.5-2-.5-3 1-1 3 2.5 3 2.5h2s.78-1.992 2.5-3c1 0 1 3 1 3' style='fill:%23fff;stroke:%23000' transform='translate(0 .3)'/%3E%3Cpath d='M9.5 25.5a.5.5 0 1 1-1 0 .5.5 0 1 1 1 0z' style='fill:%23000;stroke:%23000' transform='translate(0 .3)'/%3E%3Cpath d='M15 15.5a.5 1.5 0 1 1-1 0 .5 1.5 0 1 1 1 0z' transform='rotate(30 13.94 15.65)' style='fill:%23000;stroke:%23000'/%3E%3C/g%3E%3C/svg%3E\")}.wp{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cpath d='M22.5 9c-2.21 0-4 1.79-4 4 0 .89.29 1.71.78 2.38C17.33 16.5 16 18.59 16 21c0 2.03.94 3.84 2.41 5.03-3 1.06-7.41 5.55-7.41 13.47h23c0-7.92-4.41-12.41-7.41-13.47 1.47-1.19 2.41-3 2.41-5.03 0-2.41-1.33-4.5-3.28-5.62.49-.67.78-1.49.78-2.38 0-2.21-1.79-4-4-4z' style='opacity:1;fill:%23fff;fill-opacity:1;fill-rule:nonzero;stroke:%23000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1'/%3E%3C/svg%3E\")}.wq{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='fill:%23fff;stroke:%23000;stroke-width:1.5;stroke-linejoin:round'%3E%3Cpath d='M9 26c8.5-1.5 21-1.5 27 0l2.5-12.5L31 25l-.3-14.1-5.2 13.6-3-14.5-3 14.5-5.2-13.6L14 25 6.5 13.5 9 26z'/%3E%3Cpath d='M9 26c0 2 1.5 2 2.5 4 1 1.5 1 1 .5 3.5-1.5 1-1 2.5-1 2.5-1.5 1.5 0 2.5 0 2.5 6.5 1 16.5 1 23 0 0 0 1.5-1 0-2.5 0 0 .5-1.5-1-2.5-.5-2.5-.5-2 .5-3.5 1-2 2.5-2 2.5-4-8.5-1.5-18.5-1.5-27 0z'/%3E%3Cpath d='M11.5 30c3.5-1 18.5-1 22 0M12 33.5c6-1 15-1 21 0' style='fill:none'/%3E%3Ccircle cx='6' cy='12' r='2'/%3E%3Ccircle cx='14' cy='9' r='2'/%3E%3Ccircle cx='22.5' cy='8' r='2'/%3E%3Ccircle cx='31' cy='9' r='2'/%3E%3Ccircle cx='39' cy='12' r='2'/%3E%3C/g%3E%3C/svg%3E\")}.wr{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='opacity:1;fill:%23fff;fill-opacity:1;fill-rule:evenodd;stroke:%23000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1'%3E%3Cpath d='M9 39h27v-3H9v3zm3-3v-4h21v4H12zm-1-22V9h4v2h5V9h5v2h5V9h4v5' style='stroke-linecap:butt' transform='translate(0 .3)'/%3E%3Cpath d='m34 14.3-3 3H14l-3-3'/%3E%3Cpath d='M31 17v12.5H14V17' style='stroke-linecap:butt;stroke-linejoin:miter' transform='translate(0 .3)'/%3E%3Cpath d='m31 29.8 1.5 2.5h-20l1.5-2.5'/%3E%3Cpath d='M11 14h23' style='fill:none;stroke:%23000;stroke-linejoin:miter' transform='translate(0 .3)'/%3E%3C/g%3E%3C/svg%3E\")}.arrows{border:var(--inner-border-width) solid transparent;bottom:0;box-sizing:border-box;height:100%;left:0;pointer-events:none;position:absolute;right:0;top:0;touch-action:none;width:100%;z-index:20}.arrow-primary{color:var(--arrow-color-primary)}.arrow-secondary{color:var(--arrow-color-secondary)}";
|
||
|
||
const COORDINATES_PLACEMENTS = ["inside", "outside", "hidden"];
|
||
class Coordinates {
|
||
constructor(props) {
|
||
Object.defineProperty(this, "element", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_coordElements", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_orientation", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_direction", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
this.element = makeHTMLElement("div", {
|
||
attributes: {
|
||
role: "presentation",
|
||
"aria-hidden": "true",
|
||
},
|
||
classes: ["coords", props.direction],
|
||
});
|
||
this._direction = props.direction;
|
||
this._orientation = props.orientation;
|
||
this._coordElements = new Array(8);
|
||
const evenSquareColor = props.direction === "file" ? "dark" : "light";
|
||
const oddSquareColor = props.direction === "file" ? "light" : "dark";
|
||
for (let i = 0; i < 8; i++) {
|
||
const color = i % 2 === 0 ? evenSquareColor : oddSquareColor;
|
||
const textElement = makeHTMLElement("div", { classes: ["coord", color] });
|
||
this._coordElements[i] = textElement;
|
||
this.element.appendChild(textElement);
|
||
}
|
||
this._updateCoordsText();
|
||
}
|
||
/**
|
||
* Orientation of the board; this determines labels for ranks and files.
|
||
*/
|
||
get orientation() {
|
||
return this._orientation;
|
||
}
|
||
set orientation(value) {
|
||
this._orientation = value;
|
||
this._updateCoordsText();
|
||
}
|
||
_updateCoordsText() {
|
||
for (let i = 0; i < 8; i++) {
|
||
if (this._direction === "file") {
|
||
this._coordElements[i].textContent = String.fromCharCode("a".charCodeAt(0) + (this.orientation === "white" ? i : 7 - i));
|
||
}
|
||
else {
|
||
this._coordElements[i].textContent = `${this.orientation === "white" ? 8 - i : i + 1}`;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Type guard for string values that need to conform to a
|
||
* `CoordinatesPlacement` definition.
|
||
*/
|
||
function isCoordinatesPlacement(value) {
|
||
return COORDINATES_PLACEMENTS.includes(value);
|
||
}
|
||
|
||
class Arrows {
|
||
constructor(orientation) {
|
||
Object.defineProperty(this, "element", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_defs", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_group", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_orientation", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_arrows", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_arrowElements", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: new Map()
|
||
});
|
||
Object.defineProperty(this, "_markerElements", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: new Map()
|
||
});
|
||
this.element = makeSVGElement("svg", {
|
||
attributes: {
|
||
viewBox: "0 0 80 80",
|
||
},
|
||
classes: ["arrows"],
|
||
});
|
||
this._orientation = orientation;
|
||
this._defs = makeSVGElement("defs");
|
||
this.element.appendChild(this._defs);
|
||
this._group = makeSVGElement("g");
|
||
this.element.appendChild(this._group);
|
||
}
|
||
get arrows() {
|
||
return this._arrows;
|
||
}
|
||
set arrows(arrows) {
|
||
const validArrows = arrows?.filter((a) => a.from !== a.to);
|
||
// Update brushes
|
||
const brushes = validArrows
|
||
? new Set(validArrows.map((a) => Arrows._escapedBrushName(a.brush)))
|
||
: new Set();
|
||
const oldBrushes = new Set(this._markerElements.keys());
|
||
oldBrushes.forEach((key) => {
|
||
if (!brushes.has(key)) {
|
||
const marker = this._markerElements.get(key);
|
||
if (marker) {
|
||
this._defs.removeChild(marker);
|
||
this._markerElements.delete(key);
|
||
}
|
||
}
|
||
});
|
||
brushes.forEach((key) => {
|
||
if (!oldBrushes.has(key)) {
|
||
const marker = Arrows._makeMarker(key);
|
||
this._defs.appendChild(marker);
|
||
this._markerElements.set(key, marker);
|
||
}
|
||
});
|
||
// Update arrows
|
||
const oldHashes = new Set(this._arrowElements.keys());
|
||
const newHashes = validArrows
|
||
? new Set(validArrows.map((arrow) => Arrows._arrowHash(arrow)))
|
||
: new Set();
|
||
oldHashes.forEach((hash) => {
|
||
if (!newHashes.has(hash)) {
|
||
const element = this._arrowElements.get(hash);
|
||
if (element) {
|
||
this._group.removeChild(element);
|
||
this._arrowElements.delete(hash);
|
||
}
|
||
}
|
||
});
|
||
validArrows?.forEach((arrow) => {
|
||
const hash = Arrows._arrowHash(arrow);
|
||
if (!this._arrowElements.has(hash)) {
|
||
const element = this._makeArrow(arrow);
|
||
this._arrowElements.set(hash, element);
|
||
this._group.appendChild(element);
|
||
}
|
||
});
|
||
this._arrows = validArrows ? [...validArrows] : undefined;
|
||
}
|
||
/**
|
||
* Orientation of the board; this determines direction to draw arrows.
|
||
*/
|
||
get orientation() {
|
||
return this._orientation;
|
||
}
|
||
set orientation(value) {
|
||
if (value !== this._orientation) {
|
||
this._orientation = value;
|
||
this._arrows?.forEach((arrow) => {
|
||
const hash = Arrows._arrowHash(arrow);
|
||
const element = this._arrowElements.get(hash);
|
||
if (element) {
|
||
this._group.removeChild(element);
|
||
}
|
||
const newElement = this._makeArrow(arrow);
|
||
this._group.appendChild(newElement);
|
||
this._arrowElements.set(hash, newElement);
|
||
});
|
||
}
|
||
}
|
||
_makeArrow(arrow) {
|
||
const strokeWidth = Arrows._getSvgStrokeWidth(arrow.weight || Arrows._DEFAULT_ARROW_WEIGHT);
|
||
const fromRowCol = getVisualRowColumn(arrow.from, this.orientation);
|
||
const toRowCol = getVisualRowColumn(arrow.to, this.orientation);
|
||
const coords = {
|
||
x1: fromRowCol[1] * 10 + 5,
|
||
y1: fromRowCol[0] * 10 + 5,
|
||
x2: toRowCol[1] * 10 + 5,
|
||
y2: toRowCol[0] * 10 + 5,
|
||
};
|
||
const endOffset = Arrows._computeXYProjections(strokeWidth * Arrows._ARROW_LENGTH, coords);
|
||
const startOffset = Arrows._computeXYProjections(Arrows._ARROW_START_MARGIN, coords);
|
||
const escapedBrushName = Arrows._escapedBrushName(arrow.brush || Arrows._DEFAULT_BRUSH_NAME);
|
||
const className = Arrows._makeArrowClass(escapedBrushName);
|
||
const line = makeSVGElement("line", {
|
||
attributes: {
|
||
x1: `${coords.x1 + startOffset.x}`,
|
||
y1: `${coords.y1 + startOffset.y}`,
|
||
x2: `${coords.x2 - endOffset.x}`,
|
||
y2: `${coords.y2 - endOffset.y}`,
|
||
stroke: "currentColor",
|
||
"stroke-width": `${strokeWidth}`,
|
||
"marker-end": `url(#${Arrows._makeArrowHeadId(escapedBrushName)})`,
|
||
part: className,
|
||
},
|
||
classes: [className],
|
||
});
|
||
return line;
|
||
}
|
||
static _makeMarker(escapedBrushName) {
|
||
const marker = makeSVGElement("marker", {
|
||
attributes: {
|
||
id: Arrows._makeArrowHeadId(escapedBrushName),
|
||
refX: "0",
|
||
refY: `${Arrows._ARROW_WIDTH / 2}`,
|
||
orient: "auto",
|
||
markerWidth: `${Arrows._ARROW_LENGTH}`,
|
||
markerHeight: `${Arrows._ARROW_WIDTH}`,
|
||
},
|
||
});
|
||
const className = Arrows._makeArrowClass(escapedBrushName);
|
||
const polygon = makeSVGElement("polygon", {
|
||
attributes: {
|
||
fill: "currentColor",
|
||
points: `0,0 ${Arrows._ARROW_LENGTH},${Arrows._ARROW_WIDTH / 2} 0,${Arrows._ARROW_WIDTH}`,
|
||
part: className,
|
||
},
|
||
classes: [className],
|
||
});
|
||
marker.appendChild(polygon);
|
||
return marker;
|
||
}
|
||
static _getSvgStrokeWidth(weight) {
|
||
switch (weight) {
|
||
case "bold":
|
||
return 2.5;
|
||
case "light":
|
||
return 1;
|
||
case "normal":
|
||
default:
|
||
return 1.8;
|
||
}
|
||
}
|
||
static _escapedBrushName(brush) {
|
||
return CSS.escape(brush || Arrows._DEFAULT_BRUSH_NAME);
|
||
}
|
||
static _makeArrowHeadId(escapedBrushName) {
|
||
return `arrowhead-${escapedBrushName}`;
|
||
}
|
||
static _makeArrowClass(escapedBrushName) {
|
||
return `arrow-${escapedBrushName}`;
|
||
}
|
||
static _computeXYProjections(length, arrow) {
|
||
const angle = Math.atan2(arrow.y2 - arrow.y1, arrow.x2 - arrow.x1);
|
||
return { x: length * Math.cos(angle), y: length * Math.sin(angle) };
|
||
}
|
||
static _arrowHash(arrow) {
|
||
return `${arrow.from}_${arrow.to}_${arrow.brush || Arrows._DEFAULT_BRUSH_NAME}_${arrow.weight || Arrows._DEFAULT_ARROW_WEIGHT}`;
|
||
}
|
||
}
|
||
/**
|
||
* Length of arrow from base to tip, in terms of line "stroke width" units.
|
||
*/
|
||
Object.defineProperty(Arrows, "_ARROW_LENGTH", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: 2.4
|
||
});
|
||
/**
|
||
* Width of arrow base, in terms of line "stroke width" units.
|
||
*/
|
||
Object.defineProperty(Arrows, "_ARROW_WIDTH", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: 2
|
||
});
|
||
/**
|
||
* Margin applied at start of line, along direction of arrow. In CSS viewport units.
|
||
*/
|
||
Object.defineProperty(Arrows, "_ARROW_START_MARGIN", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: 2.7
|
||
});
|
||
/**
|
||
* Default brush name when none is specified for an arrow.
|
||
*/
|
||
Object.defineProperty(Arrows, "_DEFAULT_BRUSH_NAME", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: "primary"
|
||
});
|
||
/**
|
||
* Default arrow weight when none is specified.
|
||
*/
|
||
Object.defineProperty(Arrows, "_DEFAULT_ARROW_WEIGHT", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: "normal"
|
||
});
|
||
|
||
/**
|
||
* A component that displays a chess board, with optional interactivity. Allows
|
||
* click, drag and keyboard-based moves.
|
||
*
|
||
* @fires movestart - Fired when the user initiates a move by clicking, dragging or
|
||
* via the keyboard.
|
||
*
|
||
* The event has a `detail` object with the `from` and
|
||
* `piece` values for the move. It also has a function, `setTargets(squares)`,
|
||
* that the caller can invoke with an array of square labels. This limits the
|
||
* set of targets that the piece can be moved to. Note that calling this
|
||
* function with an empty list will still allow the piece to be dragged around,
|
||
* but no square will accept the piece and thus it will always return to the
|
||
* starting square.
|
||
*
|
||
* @fires moveend - Fired when user is completing a move. This move can be prevented
|
||
* from completing by calling `preventDefault()` on the event. If that is called,
|
||
* the move itself remains in progress. The event has a `detail` object with `from`
|
||
* and `to` set to the square labels of the move, and `piece` containing information
|
||
* about the piece that was moved.
|
||
*
|
||
* @fires movefinished - Fired after a move is completed _and_ animations are resolved.
|
||
* The event has a `detail` object with `from` and `to` set to the square labels
|
||
* of the move, and `piece` containing information about the piece that was moved.
|
||
*
|
||
* The `movefinished` event is the best time to update board position in response to
|
||
* a move. For example, after a king is moved for castling, the rook can be subsequently
|
||
* moved by updating the board position in `movefinished` by setting the `position`
|
||
* property.
|
||
*
|
||
* @fires movecancel - Fired as a move is being canceled by the user. The event
|
||
* is *itself* cancelable, ie. a caller can call `preventDefault()` on the event
|
||
* to prevent the move from being canceled. Any pieces being dragged will be returned
|
||
* to the start square, but the move will remain in progress.
|
||
*
|
||
* The event has a `detail` object with `from` set to the square label where
|
||
* the move was started, and `piece` containing information about the piece that was
|
||
* moved.
|
||
*
|
||
* @cssprop [--square-color-dark=hsl(145deg 32% 44%)] - Color for dark squares.
|
||
* @cssprop [--square-color-light=hsl(51deg 24% 84%)] - Color for light squares.
|
||
*
|
||
* @cssprop [--square-color-dark-hover=hsl(144deg 75% 44%)] - Hover color
|
||
* for a dark square. Applied when mouse is hovering over an interactable square
|
||
* or a square has keyboard focus during a move.
|
||
* @cssprop [--square-color-light-hover=hsl(52deg 98% 70%)] - Hover color
|
||
* for a dark square. Applied when mouse is hovering over an interactable square
|
||
* or a square has keyboard focus during a move.
|
||
*
|
||
* @cssprop [--square-color-dark-active=hsl(142deg 77% 43%)] - Color applied to
|
||
* dark square when it is involved in (the starting point) of a move. By default
|
||
* this color is similar to, but slightly different from, `--square-color-dark-hover`.
|
||
* @cssprop [--square-color-light-active=hsl(50deg 95% 64%)] - Color applied to
|
||
* light square when it is involved in (the starting point) of a move. By default
|
||
* this color is similar to, but slightly different from, `--square-color-light-hover`.
|
||
*
|
||
* @cssprop [--outline-color-dark-active=hsl(138deg 85% 53% / 95%)] - Color of
|
||
* outline applied to dark square when it is the starting point of a move.
|
||
* It is applied in addition to `--square-color-dark-active`, and is visible
|
||
* when the square does not have focus.
|
||
* @cssprop [--outline-color-light-active=hsl(66deg 97% 72% / 95%)] - Color of
|
||
* outline applied to light square when it is the starting point of a move.
|
||
* It is applied in addition to `--square-color-light-active`, and is visible
|
||
* when the square does not have focus.
|
||
* @cssprop [--outline-color-focus=hsl(30deg 94% 55% / 90%)] - Color of outline applied to square when it has focus.
|
||
*
|
||
* @cssprop [--outer-gutter-width=4%] - When the `coordinates` property is `outside`,
|
||
* this CSS property controls the width of the gutter outside the board where coords are shown.
|
||
* @cssprop [--inner-border-width=1px] - Width of the inside border drawn around the board.
|
||
* @cssprop [--inner-border-color=var(--square-color-dark)] - Color of the inside border drawn
|
||
* around the board.
|
||
*
|
||
* @cssprop [--move-target-marker-color-dark-square=hsl(144deg 64% 9% / 90%)] -
|
||
* Color of marker shown on dark square when it is an eligible move target.
|
||
* @cssprop [--move-target-marker-color-light-square=hsl(144deg 64% 9% / 90%)] -
|
||
* Color of marker shown on light square when it is an eligible move target.
|
||
*
|
||
* @cssprop [--move-target-marker-radius=24%] - Radius of marker on a move target
|
||
* square.
|
||
* @cssprop [--move-target-marker-radius-occupied=82%] - Radius of marker on
|
||
* a move target square that is occupied (by a piece or custom content).
|
||
*
|
||
* @cssprop [--outline-blur-radius=3px] - Blur radius of all outlines applied to square.
|
||
* @cssprop [--outline-spread-radius=4px] - Spread radius of all outlines applied to square.
|
||
*
|
||
* @cssprop [--coords-font-size=0.7rem] - Font size of coord labels shown on board.
|
||
* @cssprop [--coords-font-family=sans-serif] - Font family of coord labels shown on board.
|
||
* @cssprop [--coords-inside-coord-padding-left=0.5%] - Left padding applied to coordinates
|
||
* when shown inside the board. Percentage values are relative to the width of the board.
|
||
* @cssprop [--coords-inside-coord-padding-right=0.5%] - Right padding applied to coordinates
|
||
* when shown inside the board. Percentage values are relative to the width of the board.
|
||
*
|
||
* @cssprop [--ghost-piece-opacity=0.35] - Opacity of ghost piece shown while dragging.
|
||
* Set to 0 to hide ghost piece altogether.
|
||
* @cssprop [--piece-drag-z-index=9999] - Z-index applied to piece while being dragged.
|
||
* @cssprop [--piece-drag-coarse-scale=2.4] - Amount to scale up a piece when doing a
|
||
* coarse (touch) drag. On mobile devices, pieces will be scaled up in size to
|
||
* make them easier to see.
|
||
* @cssprop [--piece-padding=3%] - Padding applied to square when piece is placed in it.
|
||
*
|
||
* @cssprop [--arrow-color-primary=hsl(40deg 100% 50% / 80%)] - Color applied to arrow
|
||
* with brush `primary`.
|
||
* @cssprop [--arrow-color-secondary=hsl(7deg 93% 61% / 80%)] - Color applied to arrow
|
||
* with brush `secondary`.
|
||
*
|
||
* @slot a1,a2,...,h8 - Slots that allow placement of custom content -- SVGs, text, or
|
||
* any other annotation -- on the corresponding square.
|
||
*
|
||
* @csspart piece-<b|w><b|r|p|n|k|q> - CSS parts for each of the piece classes. The part
|
||
* name is of the form `piece-xy`, where `x` corresponds to the color of the piece --
|
||
* either `w` for white or `b` for black, and `y` is the piece type -- one of `p` (pawn),
|
||
* `r` (rook), `n` (knight), `b` (bishop), `k` (king), `q` (queen). Thus, `piece-wr`
|
||
* would be the CSS part corresponding to the white rook.
|
||
*
|
||
* The CSS parts can be used to set custom CSS for the pieces (such as changing the image
|
||
* for a piece by changing the `background-image` property).
|
||
*
|
||
* @csspart arrow-<brush_name> - CSS parts for any arrow brushes configured using the
|
||
* `brush` field on an arrow specification (see the `arrows` property for more details).
|
||
*/
|
||
class GChessBoardElement extends HTMLElement {
|
||
static get observedAttributes() {
|
||
return [
|
||
"orientation",
|
||
"turn",
|
||
"interactive",
|
||
"fen",
|
||
"coordinates",
|
||
"animation-duration",
|
||
];
|
||
}
|
||
constructor() {
|
||
super();
|
||
Object.defineProperty(this, "_shadow", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_style", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_wrapper", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_board", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_fileCoords", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_rankCoords", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
Object.defineProperty(this, "_arrows", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: void 0
|
||
});
|
||
this._shadow = this.attachShadow({ mode: "open" });
|
||
this._style = document.createElement("style");
|
||
this._style.textContent = css_248z;
|
||
this._shadow.appendChild(this._style);
|
||
this._wrapper = makeHTMLElement("div", {
|
||
classes: ["wrapper", GChessBoardElement._DEFAULT_COORDS_PLACEMENT],
|
||
});
|
||
this._shadow.appendChild(this._wrapper);
|
||
this._board = new Board({
|
||
orientation: GChessBoardElement._DEFAULT_SIDE,
|
||
animationDurationMs: GChessBoardElement._DEFAULT_ANIMATION_DURATION_MS,
|
||
}, (e) => this.dispatchEvent(e), this._shadow);
|
||
this._wrapper.appendChild(this._board.element);
|
||
this._fileCoords = new Coordinates({
|
||
direction: "file",
|
||
placement: GChessBoardElement._DEFAULT_COORDS_PLACEMENT,
|
||
orientation: GChessBoardElement._DEFAULT_SIDE,
|
||
});
|
||
this._rankCoords = new Coordinates({
|
||
direction: "rank",
|
||
placement: GChessBoardElement._DEFAULT_COORDS_PLACEMENT,
|
||
orientation: GChessBoardElement._DEFAULT_SIDE,
|
||
});
|
||
this._wrapper.appendChild(this._fileCoords.element);
|
||
this._wrapper.appendChild(this._rankCoords.element);
|
||
this._arrows = new Arrows(GChessBoardElement._DEFAULT_SIDE);
|
||
this._wrapper.appendChild(this._arrows.element);
|
||
}
|
||
connectedCallback() {
|
||
this._board.addGlobalListeners();
|
||
}
|
||
disconnectedCallback() {
|
||
this._board.removeGlobalListeners();
|
||
}
|
||
attributeChangedCallback(name, _, newValue) {
|
||
switch (name) {
|
||
case "interactive":
|
||
this._board.interactive = this.interactive;
|
||
break;
|
||
case "coordinates":
|
||
this._wrapper.classList.toggle("outside", this.coordinates === "outside");
|
||
this._wrapper.classList.toggle("inside", this.coordinates === "inside");
|
||
break;
|
||
case "orientation":
|
||
this._board.orientation = this.orientation;
|
||
this._fileCoords.orientation = this.orientation;
|
||
this._rankCoords.orientation = this.orientation;
|
||
this._arrows.orientation = this.orientation;
|
||
break;
|
||
case "turn":
|
||
this._board.turn = this.turn;
|
||
break;
|
||
case "fen":
|
||
if (newValue !== null) {
|
||
this.fen = newValue;
|
||
}
|
||
else {
|
||
this.position = {};
|
||
}
|
||
break;
|
||
case "animation-duration":
|
||
this._board.animationDurationMs = this.animationDuration;
|
||
break;
|
||
default:
|
||
assertUnreachable(name);
|
||
}
|
||
}
|
||
/**
|
||
* What side's perspective to render squares from (what color appears on
|
||
* the bottom as viewed on the screen). Either `"white"` or `"black"`.
|
||
*
|
||
* @attr [orientation=white]
|
||
*/
|
||
get orientation() {
|
||
return this._parseRestrictedStringAttributeWithDefault("orientation", isSide, GChessBoardElement._DEFAULT_SIDE);
|
||
}
|
||
set orientation(value) {
|
||
this.setAttribute("orientation", value);
|
||
}
|
||
/**
|
||
* What side is allowed to move pieces. Either `"white`, `"black"`, or
|
||
* unset. When unset, pieces from either side can be moved around.
|
||
*
|
||
* @attr
|
||
*/
|
||
get turn() {
|
||
return this._parseRestrictedStringAttribute("turn", isSide);
|
||
}
|
||
set turn(value) {
|
||
if (value) {
|
||
this.setAttribute("turn", value);
|
||
}
|
||
else {
|
||
this.removeAttribute("turn");
|
||
}
|
||
}
|
||
/**
|
||
* Whether the squares are interactive, i.e. user can interact with squares,
|
||
* move pieces etc. By default, this is false; i.e a board is only for displaying
|
||
* a position.
|
||
*
|
||
* @attr [interactive=false]
|
||
*/
|
||
get interactive() {
|
||
return this._hasBooleanAttribute("interactive");
|
||
}
|
||
set interactive(interactive) {
|
||
this._setBooleanAttribute("interactive", interactive);
|
||
}
|
||
/**
|
||
* A map-like object representing the board position, where object keys are square
|
||
* labels, and values are `Piece` objects. Note that changes to this property are
|
||
* mirrored in the value of the `fen` property of the element, but **not** the
|
||
* corresponding attribute. All changes to position are animated, using the duration
|
||
* specified by the `animationDuration` property.
|
||
*
|
||
* Example:
|
||
*
|
||
* ```js
|
||
* board.position = {
|
||
* a2: {
|
||
* pieceType: "king",
|
||
* color: "white"
|
||
* },
|
||
* g4: {
|
||
* pieceType: "knight",
|
||
* color: "black"
|
||
* },
|
||
* };
|
||
* ```
|
||
*/
|
||
get position() {
|
||
return this._board.position;
|
||
}
|
||
set position(value) {
|
||
this._board.position = { ...value };
|
||
}
|
||
/**
|
||
* FEN string representing the board position. Note that changes to the corresponding
|
||
* `fen` _property_ will **not** reflect onto the "fen" _attribute_ of the element.
|
||
* In other words, to get the latest FEN string for the board position, use the `fen`
|
||
* _property_.
|
||
*
|
||
* Accepts the special string `"start"` as shorthand for the starting position
|
||
* of a chess game. An empty string represents an empty board. Invalid FEN values
|
||
* are ignored with an error.
|
||
*
|
||
* Note that a FEN string normally contains 6 components, separated by slashes,
|
||
* but only the first component (the "piece placement" component) is used by this
|
||
* attribute.
|
||
*
|
||
* @attr
|
||
*/
|
||
get fen() {
|
||
return getFen(this._board.position);
|
||
}
|
||
set fen(value) {
|
||
const position = getPosition(value);
|
||
if (position !== undefined) {
|
||
this.position = position;
|
||
}
|
||
else {
|
||
throw new Error(`Invalid FEN position: ${value}`);
|
||
}
|
||
}
|
||
/**
|
||
* How to display coordinates for squares. Could be `"inside"` the board (default),
|
||
* `"outside"`, or `"hidden"`.
|
||
*
|
||
* @attr [coordinates=inside]
|
||
*/
|
||
get coordinates() {
|
||
return this._parseRestrictedStringAttributeWithDefault("coordinates", isCoordinatesPlacement, GChessBoardElement._DEFAULT_COORDS_PLACEMENT);
|
||
}
|
||
set coordinates(value) {
|
||
this.setAttribute("coordinates", value);
|
||
}
|
||
/**
|
||
* Duration, in milliseconds, of animation when adding/removing/moving pieces.
|
||
*
|
||
* @attr [animation-duration=200]
|
||
*/
|
||
get animationDuration() {
|
||
return this._parseNumberAttribute("animation-duration", GChessBoardElement._DEFAULT_ANIMATION_DURATION_MS);
|
||
}
|
||
set animationDuration(value) {
|
||
this._setNumberAttribute("animation-duration", value);
|
||
}
|
||
/**
|
||
* Set of arrows to draw on the board. This is an array of objects specifying
|
||
* arrow characteristics, with the following properties: (1) `from` and `to`
|
||
* corresponding to the start and end squares for the arrow, (2) optional
|
||
* `weight` for the line (values: `"light"`, `"normal"`, `"bold"`), and
|
||
* (3) `brush`, which is a string that will be used to make a CSS part
|
||
* where one can customize the color, opacity, and other styles of the
|
||
* arrow. For example, a value for `brush` of `"foo"` will apply a
|
||
* CSS part named `arrow-foo` to the arrow.
|
||
*
|
||
* Note: because the value of `brush` becomes part of a CSS part name, it
|
||
* should be usable as a valid CSS identifier.
|
||
*
|
||
* In addition to allowing arbitrary part names, arrows support a few
|
||
* out-of-the-box brush names, `primary` and `secondary`, which colors
|
||
* defined with CSS custom properties `--arrow-color-primary` and
|
||
* `--arrow-color-secondary`.
|
||
*
|
||
* Example:
|
||
*
|
||
* ```js
|
||
* board.arrows = [
|
||
* { from: "e2", to: "e4" },
|
||
* {
|
||
* from: "g1",
|
||
* to: "f3",
|
||
* brush: "foo"
|
||
* },
|
||
* {
|
||
* from: "c7",
|
||
* to: "c5",
|
||
* brush: "secondary"
|
||
* },
|
||
* ];
|
||
*/
|
||
get arrows() {
|
||
return this._arrows.arrows;
|
||
}
|
||
set arrows(arrows) {
|
||
this._arrows.arrows = arrows;
|
||
}
|
||
addEventListener(type, listener, options) {
|
||
super.addEventListener(type, listener, options);
|
||
}
|
||
removeEventListener(type, listener, options) {
|
||
super.removeEventListener(type, listener, options);
|
||
}
|
||
/**
|
||
* Start a move on the board at `square`, optionally with specified targets
|
||
* at `targetSquares`.
|
||
*/
|
||
startMove(square, targetSquares) {
|
||
this._board.startMove(square, targetSquares);
|
||
}
|
||
/**
|
||
* Imperatively cancel any in-progress moves.
|
||
*/
|
||
cancelMove() {
|
||
this._board.cancelMove();
|
||
}
|
||
_hasBooleanAttribute(name) {
|
||
return (this.hasAttribute(name) &&
|
||
this.getAttribute(name)?.toLowerCase() !== "false");
|
||
}
|
||
_setBooleanAttribute(name, value) {
|
||
if (value) {
|
||
this.setAttribute(name, "");
|
||
}
|
||
else {
|
||
this.removeAttribute(name);
|
||
}
|
||
}
|
||
_setNumberAttribute(name, value) {
|
||
this.setAttribute(name, value.toString());
|
||
}
|
||
_parseRestrictedStringAttribute(name, guard) {
|
||
const value = this.getAttribute(name);
|
||
return guard(value) ? value : undefined;
|
||
}
|
||
_parseRestrictedStringAttributeWithDefault(name, guard, defaultValue) {
|
||
const parsed = this._parseRestrictedStringAttribute(name, guard);
|
||
return parsed !== undefined ? parsed : defaultValue;
|
||
}
|
||
_parseNumberAttribute(name, defaultValue) {
|
||
const value = this.getAttribute(name);
|
||
return value === null || Number.isNaN(Number(value))
|
||
? defaultValue
|
||
: Number(value);
|
||
}
|
||
}
|
||
Object.defineProperty(GChessBoardElement, "_DEFAULT_SIDE", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: "white"
|
||
});
|
||
Object.defineProperty(GChessBoardElement, "_DEFAULT_ANIMATION_DURATION_MS", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: 200
|
||
});
|
||
Object.defineProperty(GChessBoardElement, "_DEFAULT_COORDS_PLACEMENT", {
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true,
|
||
value: "inside"
|
||
});
|
||
|
||
customElements.define("g-chess-board", GChessBoardElement);
|
||
|
||
export { GChessBoardElement };
|
||
//# sourceMappingURL=index.es.js.map
|