elo-worldle/gchessboard.js
2023-04-01 23:11:32 -04:00

3121 lines
127 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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