UNPKG

@noe_rls/cm-chessboard

Version:

A JavaScript chessboard which is lightweight, ES6 module based, responsive, SVG rendered and without dependencies.

501 lines (454 loc) 18.9 kB
/** * Author and copyright: Stefan Haack (https://shaack.com) * Repository: https://github.com/shaack/cm-chessboard * License: MIT, see file 'LICENSE' */ import {ChessboardMoveInput} from "./ChessboardMoveInput.js" import {COLOR, INPUT_EVENT_TYPE, BORDER_TYPE} from "./Chessboard.js" import {ChessboardPiecesAnimation} from "./ChessboardPiecesAnimation.js" export const SQUARE_COORDINATES = [ "a1", "b1", "c1", "d1", "e1", "f1", "g1", "h1", "a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2", "a3", "b3", "c3", "d3", "e3", "f3", "g3", "h3", "a4", "b4", "c4", "d4", "e4", "f4", "g4", "h4", "a5", "b5", "c5", "d5", "e5", "f5", "g5", "h5", "a6", "b6", "c6", "d6", "e6", "f6", "g6", "h6", "a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7", "a8", "b8", "c8", "d8", "e8", "f8", "g8", "h8" ] export class ChessboardView { constructor(chessboard, callbackAfterCreation) { this.animationRunning = false this.currentAnimation = undefined this.chessboard = chessboard this.moveInput = new ChessboardMoveInput(this, this.moveStartCallback.bind(this), this.moveDoneCallback.bind(this), this.moveCanceledCallback.bind(this) ) this.animationQueue = [] if (chessboard.props.sprite.cache) { this.cacheSprite() } if (chessboard.props.responsive) { // noinspection JSUnresolvedVariable if (typeof ResizeObserver !== "undefined") { // noinspection JSUnresolvedFunction this.resizeObserver = new ResizeObserver(() => { this.handleResize() }) this.resizeObserver.observe(this.chessboard.element) } else { this.resizeListener = this.handleResize.bind(this) window.addEventListener("resize", this.resizeListener) } } this.pointerDownListener = this.pointerDownHandler.bind(this) this.chessboard.element.addEventListener("mousedown", this.pointerDownListener) this.chessboard.element.addEventListener("touchstart", this.pointerDownListener) this.createSvgAndGroups() this.updateMetrics() callbackAfterCreation(this) if (chessboard.props.responsive) { this.handleResize() } } pointerDownHandler(e) { this.moveInput.onPointerDown(e) } destroy() { this.moveInput.destroy() if (this.resizeObserver) { this.resizeObserver.unobserve(this.chessboard.element); } if (this.resizeListener) { window.removeEventListener("resize", this.resizeListener) } this.chessboard.element.removeEventListener("mousedown", this.pointerDownListener) this.chessboard.element.removeEventListener("touchstart", this.pointerDownListener) Svg.removeElement(this.svg) this.animationQueue = [] if (this.currentAnimation) { cancelAnimationFrame(this.currentAnimation.frameHandle) } } // Sprite // cacheSprite() { const wrapperId = "chessboardSpriteCache" if (!document.getElementById(wrapperId)) { const wrapper = document.createElement("div") wrapper.style.display = "none" wrapper.id = wrapperId document.body.appendChild(wrapper) const xhr = new XMLHttpRequest() xhr.open("GET", this.chessboard.props.sprite.url, true) xhr.onload = function () { wrapper.insertAdjacentHTML('afterbegin', xhr.response) } xhr.send() } } createSvgAndGroups() { if (this.svg) { Svg.removeElement(this.svg) } this.svg = Svg.createSvg(this.chessboard.element) let cssClass = this.chessboard.props.style.cssClass ? this.chessboard.props.style.cssClass : "default" this.svg.setAttribute("class", "cm-chessboard border-type-" + this.chessboard.props.style.borderType + " " + cssClass) this.updateMetrics() this.boardGroup = Svg.addElement(this.svg, "g", {class: "board"}) this.coordinatesGroup = Svg.addElement(this.svg, "g", {class: "coordinates"}) this.markersGroup = Svg.addElement(this.svg, "g", {class: "markers"}) this.piecesGroup = Svg.addElement(this.svg, "g", {class: "pieces"}) } updateMetrics() { this.width = this.chessboard.element.clientWidth this.height = this.chessboard.element.clientHeight if (this.chessboard.props.style.borderType === BORDER_TYPE.frame) { this.borderSize = this.width / 25 } else if (this.chessboard.props.style.borderType === BORDER_TYPE.thin) { this.borderSize = this.width / 320 } else { this.borderSize = 0 } this.innerWidth = this.width - 2 * this.borderSize this.innerHeight = this.height - 2 * this.borderSize this.squareWidth = this.innerWidth / 8 this.squareHeight = this.innerHeight / 8 this.scalingX = this.squareWidth / this.chessboard.props.sprite.size this.scalingY = this.squareHeight / this.chessboard.props.sprite.size this.pieceXTranslate = (this.squareWidth / 2 - this.chessboard.props.sprite.size * this.scalingY / 2) } handleResize() { if (this.chessboard.props.style.aspectRatio) { this.chessboard.element.style.height = (this.chessboard.element.clientWidth * this.chessboard.props.style.aspectRatio) + "px" } if (this.chessboard.element.clientWidth !== this.width || this.chessboard.element.clientHeight !== this.height) { this.updateMetrics() this.redraw() } this.svg.setAttribute("width", "100%") // safari bugfix this.svg.setAttribute("height", "100%") } redraw() { this.drawBoard() this.drawCoordinates() this.drawMarkers() this.setCursor() this.drawPieces(this.chessboard.state.squares) } // Board // drawBoard() { while (this.boardGroup.firstChild) { this.boardGroup.removeChild(this.boardGroup.lastChild) } if (this.chessboard.props.style.borderType !== BORDER_TYPE.none) { let boardBorder = Svg.addElement(this.boardGroup, "rect", {width: this.width, height: this.height}) boardBorder.setAttribute("class", "border") if (this.chessboard.props.style.borderType === BORDER_TYPE.frame) { const innerPos = this.borderSize let borderInner = Svg.addElement(this.boardGroup, "rect", { x: innerPos, y: innerPos, width: this.width - innerPos * 2, height: this.height - innerPos * 2 }) borderInner.setAttribute("class", "border-inner") } } for (let i = 0; i < 64; i++) { const index = this.chessboard.state.orientation === COLOR.white ? i : 63 - i const squareColor = ((9 * index) & 8) === 0 ? 'black' : 'white' const fieldClass = `square ${squareColor}` const point = this.squareIndexToPoint(index) const squareRect = Svg.addElement(this.boardGroup, "rect", { x: point.x, y: point.y, width: this.squareWidth, height: this.squareHeight }) squareRect.setAttribute("class", fieldClass) squareRect.setAttribute("data-index", "" + index) } } drawCoordinates() { if (!this.chessboard.props.style.showCoordinates) { return } while (this.coordinatesGroup.firstChild) { this.coordinatesGroup.removeChild(this.coordinatesGroup.lastChild) } const inline = this.chessboard.props.style.borderType !== BORDER_TYPE.frame for (let file = 0; file < 8; file++) { let x = this.borderSize + (17 + this.chessboard.props.sprite.size * file) * this.scalingX let y = this.height - this.scalingY * 3.5 let cssClass = "coordinate file" if (inline) { x = x + this.scalingX * 15.5 cssClass += file % 2 ? " white" : " black" } const textElement = Svg.addElement(this.coordinatesGroup, "text", { class: cssClass, x: x, y: y, style: `font-size: ${this.scalingY * 10}px` }) if (this.chessboard.state.orientation === COLOR.white) { textElement.textContent = String.fromCharCode(97 + file) } else { textElement.textContent = String.fromCharCode(104 - file) } } for (let rank = 0; rank < 8; rank++) { let x = (this.borderSize / 3.7) let y = this.borderSize + 25 * this.scalingY + rank * this.squareHeight let cssClass = "coordinate rank" if (inline) { cssClass += rank % 2 ? " black" : " white" if (this.chessboard.props.style.borderType === BORDER_TYPE.frame) { x = x + this.scalingX * 10 y = y - this.scalingY * 15 } else { x = x + this.scalingX * 2 y = y - this.scalingY * 15 } } const textElement = Svg.addElement(this.coordinatesGroup, "text", { class: cssClass, x: x, y: y, style: `font-size: ${this.scalingY * 10}px` }) if (this.chessboard.state.orientation === COLOR.white) { textElement.textContent = "" + (8 - rank) } else { textElement.textContent = "" + (1 + rank) } } } // Pieces // drawPieces(squares = this.chessboard.state.squares) { const childNodes = Array.from(this.piecesGroup.childNodes) for (let i = 0; i < 64; i++) { const pieceName = squares[i] if (pieceName) { this.drawPiece(i, pieceName) } } for (const childNode of childNodes) { this.piecesGroup.removeChild(childNode) } } drawPiece(index, pieceName) { const pieceGroup = Svg.addElement(this.piecesGroup, "g") pieceGroup.setAttribute("data-piece", pieceName) pieceGroup.setAttribute("data-index", index) const point = this.squareIndexToPoint(index) const transform = (this.svg.createSVGTransform()) transform.setTranslate(point.x, point.y) pieceGroup.transform.baseVal.appendItem(transform) const spriteUrl = this.chessboard.props.sprite.cache ? "" : this.chessboard.props.sprite.url const pieceUse = Svg.addElement(pieceGroup, "use", { href: `${spriteUrl}#${pieceName}`, class: "piece" }) // center on square const transformTranslate = (this.svg.createSVGTransform()) transformTranslate.setTranslate(this.pieceXTranslate, 0) pieceUse.transform.baseVal.appendItem(transformTranslate) // scale const transformScale = (this.svg.createSVGTransform()) transformScale.setScale(this.scalingY, this.scalingY) pieceUse.transform.baseVal.appendItem(transformScale) return pieceGroup } setPieceVisibility(index, visible = true) { const piece = this.getPiece(index) if (visible) { piece.setAttribute("visibility", "visible") } else { piece.setAttribute("visibility", "hidden") } } getPiece(index) { return this.piecesGroup.querySelector(`g[data-index='${index}']`) } // Markers // drawMarkers() { while (this.markersGroup.firstChild) { this.markersGroup.removeChild(this.markersGroup.firstChild) } this.chessboard.state.markers.forEach((marker) => { this.drawMarker(marker) } ) } drawMarker(marker) { const markerGroup = Svg.addElement(this.markersGroup, "g") markerGroup.setAttribute("data-index", marker.index) const point = this.squareIndexToPoint(marker.index) const transform = (this.svg.createSVGTransform()) transform.setTranslate(point.x, point.y) markerGroup.transform.baseVal.appendItem(transform) const spriteUrl = this.chessboard.props.sprite.cache ? "" : this.chessboard.props.sprite.url const markerUse = Svg.addElement(markerGroup, "use", {href: `${spriteUrl}#${marker.type.slice}`, class: "marker " + marker.type.class}) const transformScale = (this.svg.createSVGTransform()) transformScale.setScale(this.scalingX, this.scalingY) markerUse.transform.baseVal.appendItem(transformScale) return markerGroup } // animation queue // animatePieces(fromSquares, toSquares, callback) { this.animationQueue.push({fromSquares: fromSquares, toSquares: toSquares, callback: callback}) if (!this.animationRunning) { this.nextPieceAnimationInQueue() } } nextPieceAnimationInQueue() { const nextAnimation = this.animationQueue.shift() if (nextAnimation !== undefined) { this.animationRunning = true this.drawPieces(nextAnimation.fromSquares); this.currentAnimation = new ChessboardPiecesAnimation(this, nextAnimation.fromSquares, nextAnimation.toSquares, this.chessboard.props.animationDuration / (this.animationQueue.length + 1), () => { if (!this.moveInput.draggablePiece) { this.animationRunning = false this.nextPieceAnimationInQueue() if (nextAnimation.callback) { nextAnimation.callback() } } else { this.animationRunning = false this.nextPieceAnimationInQueue() if (nextAnimation.callback) { nextAnimation.callback() } } }) } } // enable and disable move input // enableMoveInput(eventHandler, color = undefined) { if (color === COLOR.white) { this.chessboard.state.inputWhiteEnabled = true } else if (color === COLOR.black) { this.chessboard.state.inputBlackEnabled = true } else { this.chessboard.state.inputWhiteEnabled = true this.chessboard.state.inputBlackEnabled = true } this.chessboard.state.inputEnabled = true this.moveInputCallback = eventHandler this.setCursor() } disableMoveInput() { this.chessboard.state.inputWhiteEnabled = false this.chessboard.state.inputBlackEnabled = false this.chessboard.state.inputEnabled = false this.moveInputCallback = undefined this.setCursor() } // callbacks // moveStartCallback(index) { if (this.moveInputCallback) { return this.moveInputCallback({ chessboard: this.chessboard, type: INPUT_EVENT_TYPE.moveStart, square: SQUARE_COORDINATES[index] }) } else { return true } } moveDoneCallback(fromIndex, toIndex) { if (this.moveInputCallback) { return this.moveInputCallback({ chessboard: this.chessboard, type: INPUT_EVENT_TYPE.moveDone, squareFrom: SQUARE_COORDINATES[fromIndex], squareTo: SQUARE_COORDINATES[toIndex] }) } else { return true } } moveCanceledCallback(reason, fromIndex, toIndex) { if (this.moveInputCallback) { this.moveInputCallback({ chessboard: this.chessboard, type: INPUT_EVENT_TYPE.moveCanceled, reason: reason, squareFrom: SQUARE_COORDINATES[fromIndex], squareTo: toIndex ? SQUARE_COORDINATES[toIndex] : undefined }) } } // Helpers // setCursor() { if (this.chessboard.state) { // fix https://github.com/shaack/cm-chessboard/issues/47 if (this.chessboard.state.inputWhiteEnabled || this.chessboard.state.inputBlackEnabled || this.chessboard.state.squareSelectEnabled) { this.boardGroup.setAttribute("class", "board input-enabled") } else { this.boardGroup.setAttribute("class", "board") } } } squareIndexToPoint(index) { let x, y if (this.chessboard.state.orientation === COLOR.white) { x = this.borderSize + (index % 8) * this.squareWidth y = this.borderSize + (7 - Math.floor(index / 8)) * this.squareHeight } else { x = this.borderSize + (7 - index % 8) * this.squareWidth y = this.borderSize + (Math.floor(index / 8)) * this.squareHeight } return {x: x, y: y} } } const SVG_NAMESPACE = "http://www.w3.org/2000/svg" export class Svg { /** * create the Svg in the HTML DOM * @param containerElement * @returns {Element} */ static createSvg(containerElement = undefined) { let svg = document.createElementNS(SVG_NAMESPACE, "svg") if (containerElement) { svg.setAttribute("width", "100%") svg.setAttribute("height", "100%") containerElement.appendChild(svg) } return svg } /** * Add an Element to a SVG DOM * @param parent * @param name * @param attributes * @returns {Element} */ static addElement(parent, name, attributes) { let element = document.createElementNS(SVG_NAMESPACE, name) if (name === "use") { attributes["xlink:href"] = attributes["href"] // fix for safari } for (let attribute in attributes) { if (attributes.hasOwnProperty(attribute)) { if (attribute.indexOf(":") !== -1) { const value = attribute.split(":") element.setAttributeNS("http://www.w3.org/1999/" + value[0], value[1], attributes[attribute]) } else { element.setAttribute(attribute, attributes[attribute]) } } } parent.appendChild(element) return element } /** * Remove an Element from a SVG DOM * @param element */ static removeElement(element) { element.parentNode.removeChild(element) } }