UNPKG

@noe_rls/cm-chessboard

Version:

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

397 lines (361 loc) 17.7 kB
/** * Author and copyright: Stefan Haack (https://shaack.com) * Repository: https://github.com/shaack/cm-chessboard * License: MIT, see file 'LICENSE' */ import {SQUARE_COORDINATES, Svg} from "./ChessboardView.js" const STATE = { waitForInputStart: 0, pieceClickedThreshold: 1, clickTo: 2, secondClickThreshold: 3, dragTo: 4, clickDragTo: 5, moveDone: 6, reset: 7 } export const MOVE_CANCELED_REASON = { secondClick: "secondClick", movedOutOfBoard: "movedOutOfBoard", draggedBack: "draggedBack", clickedAnotherPiece: "clickedAnotherPiece" } const DRAG_THRESHOLD = 4 export class ChessboardMoveInput { constructor(view, moveStartCallback, moveDoneCallback, moveCanceledCallback) { this.view = view this.chessboard = view.chessboard this.moveStartCallback = moveStartCallback this.moveDoneCallback = moveDoneCallback this.moveCanceledCallback = moveCanceledCallback this.setMoveInputState(STATE.waitForInputStart) } setMoveInputState(newState, params = undefined) { // console.log("setMoveInputState", Object.keys(STATE)[this.moveInputState], "=>", Object.keys(STATE)[newState]); const prevState = this.moveInputState this.moveInputState = newState switch (newState) { case STATE.waitForInputStart: break case STATE.pieceClickedThreshold: if (STATE.waitForInputStart !== prevState && STATE.clickTo !== prevState) { throw new Error("moveInputState") } if (this.pointerMoveListener) { removeEventListener(this.pointerMoveListener.type, this.pointerMoveListener) this.pointerMoveListener = undefined } if (this.pointerUpListener) { removeEventListener(this.pointerUpListener.type, this.pointerUpListener) this.pointerUpListener = undefined } this.startIndex = params.index this.endIndex = undefined this.movedPiece = params.piece this.updateStartEndMarkers() this.startPoint = params.point if (!this.pointerMoveListener && !this.pointerUpListener) { if (params.type === "mousedown") { this.pointerMoveListener = this.onPointerMove.bind(this) this.pointerMoveListener.type = "mousemove" addEventListener("mousemove", this.pointerMoveListener) this.pointerUpListener = this.onPointerUp.bind(this) this.pointerUpListener.type = "mouseup" addEventListener("mouseup", this.pointerUpListener) } else if (params.type === "touchstart") { this.pointerMoveListener = this.onPointerMove.bind(this) this.pointerMoveListener.type = "touchmove" addEventListener("touchmove", this.pointerMoveListener) this.pointerUpListener = this.onPointerUp.bind(this) this.pointerUpListener.type = "touchend" addEventListener("touchend", this.pointerUpListener) } else { throw Error("event type") } } else { throw Error("_pointerMoveListener or _pointerUpListener") } break case STATE.clickTo: if (this.draggablePiece) { Svg.removeElement(this.draggablePiece) this.draggablePiece = undefined } if (prevState === STATE.dragTo) { this.view.setPieceVisibility(params.index) } break case STATE.secondClickThreshold: if (STATE.clickTo !== prevState) { throw new Error("moveInputState") } this.startPoint = params.point break case STATE.dragTo: if (STATE.pieceClickedThreshold !== prevState) { throw new Error("moveInputState") } if (this.view.chessboard.state.inputEnabled) { this.view.setPieceVisibility(params.index, false) this.createDraggablePiece(params.piece) } break case STATE.clickDragTo: if (STATE.secondClickThreshold !== prevState) { throw new Error("moveInputState") } if (this.view.chessboard.state.inputEnabled) { this.view.setPieceVisibility(params.index, false) this.createDraggablePiece(params.piece) } break case STATE.moveDone: if ([STATE.dragTo, STATE.clickTo, STATE.clickDragTo].indexOf(prevState) === -1) { throw new Error("moveInputState") } this.endIndex = params.index if (this.endIndex && this.moveDoneCallback(this.startIndex, this.endIndex)) { const prevSquares = this.chessboard.state.squares.slice(0) this.chessboard.state.setPiece(this.startIndex, undefined) this.chessboard.state.setPiece(this.endIndex, this.movedPiece) if (prevState === STATE.clickTo) { this.updateStartEndMarkers() this.view.animatePieces(prevSquares, this.chessboard.state.squares.slice(0), () => { this.setMoveInputState(STATE.reset) }) } else { this.view.drawPieces(this.chessboard.state.squares) this.setMoveInputState(STATE.reset) } } else { this.view.drawPieces() this.setMoveInputState(STATE.reset) } break case STATE.reset: if (this.startIndex && !this.endIndex && this.movedPiece) { this.chessboard.state.setPiece(this.startIndex, this.movedPiece) } this.startIndex = undefined this.endIndex = undefined this.movedPiece = undefined this.updateStartEndMarkers() if (this.draggablePiece) { Svg.removeElement(this.draggablePiece) this.draggablePiece = undefined } if (this.pointerMoveListener) { removeEventListener(this.pointerMoveListener.type, this.pointerMoveListener) this.pointerMoveListener = undefined } if (this.pointerUpListener) { removeEventListener(this.pointerUpListener.type, this.pointerUpListener) this.pointerUpListener = undefined } this.setMoveInputState(STATE.waitForInputStart) break default: throw Error(`moveInputState ${newState}`) } } createDraggablePiece(pieceName) { if (this.draggablePiece) { throw Error("draggablePiece exists") } this.draggablePiece = Svg.createSvg(document.body) this.draggablePiece.classList.add("cm-chessboard-draggable-piece") this.draggablePiece.setAttribute("width", this.view.squareWidth) this.draggablePiece.setAttribute("height", this.view.squareHeight) this.draggablePiece.setAttribute("style", "pointer-events: none") this.draggablePiece.name = pieceName const spriteUrl = this.chessboard.props.sprite.cache ? "" : this.chessboard.props.sprite.url const piece = Svg.addElement(this.draggablePiece, "use", { href: `${spriteUrl}#${pieceName}` }) const scaling = this.view.squareHeight / this.chessboard.props.sprite.size const transformScale = (this.draggablePiece.createSVGTransform()) transformScale.setScale(scaling, scaling) piece.transform.baseVal.appendItem(transformScale) } moveDraggablePiece(x, y) { this.draggablePiece.setAttribute("style", `pointer-events: none; position: absolute; left: ${x - (this.view.squareHeight / 2)}px; top: ${y - (this.view.squareHeight / 2)}px`) } onPointerDown(e) { if (e.type === "mousedown" && e.button === 0 || e.type === "touchstart") { const index = e.target.getAttribute("data-index") const pieceElement = this.view.getPiece(index) let pieceName, color if (pieceElement) { pieceName = pieceElement.getAttribute("data-piece") color = pieceName ? pieceName.substr(0, 1) : undefined // allow scrolling, if not pointed on draggable piece if (color === "w" && this.chessboard.state.inputWhiteEnabled || color === "b" && this.chessboard.state.inputBlackEnabled) { e.preventDefault() } } if (index) { // pointer on square if (this.moveInputState !== STATE.waitForInputStart || this.chessboard.state.inputWhiteEnabled && color === "w" || this.chessboard.state.inputBlackEnabled && color === "b") { let point if (e.type === "mousedown") { point = {x: e.clientX, y: e.clientY} } else if (e.type === "touchstart") { point = {x: e.touches[0].clientX, y: e.touches[0].clientY} } if (this.moveInputState === STATE.waitForInputStart && pieceName && this.moveStartCallback(index)) { this.setMoveInputState(STATE.pieceClickedThreshold, { index: index, piece: pieceName, point: point, type: e.type }) } else if (this.moveInputState === STATE.clickTo) { if (index === this.startIndex) { this.setMoveInputState(STATE.secondClickThreshold, { index: index, piece: pieceName, point: point, type: e.type }) } else { const pieceName = this.chessboard.getPiece(SQUARE_COORDINATES[index]) const pieceColor = pieceName ? pieceName.substr(0, 1) : undefined const startPieceName = this.chessboard.getPiece(SQUARE_COORDINATES[this.startIndex]) const startPieceColor = startPieceName ? startPieceName.substr(0, 1) : undefined if (color && startPieceColor === pieceColor) { // https://github.com/shaack/cm-chessboard/issues/40 this.moveCanceledCallback(MOVE_CANCELED_REASON.clickedAnotherPiece, this.startIndex, index) if (this.moveStartCallback(index)) { this.setMoveInputState(STATE.pieceClickedThreshold, { index: index, piece: pieceName, point: point, type: e.type }) } else { this.setMoveInputState(STATE.reset) } } else { this.setMoveInputState(STATE.moveDone, {index: index}) } } } } } } } onPointerMove(e) { let pageX, pageY, clientX, clientY, target if (e.type === "mousemove") { clientX = e.clientX clientY = e.clientY pageX = e.pageX pageY = e.pageY target = e.target } else if (e.type === "touchmove") { clientX = e.touches[0].clientX clientY = e.touches[0].clientY pageX = e.touches[0].pageX pageY = e.touches[0].pageY target = document.elementFromPoint(clientX, clientY) } if (this.moveInputState === STATE.pieceClickedThreshold || this.moveInputState === STATE.secondClickThreshold) { if (Math.abs(this.startPoint.x - clientX) > DRAG_THRESHOLD || Math.abs(this.startPoint.y - clientY) > DRAG_THRESHOLD) { if (this.moveInputState === STATE.secondClickThreshold) { this.setMoveInputState(STATE.clickDragTo, {index: this.startIndex, piece: this.movedPiece}) } else { this.setMoveInputState(STATE.dragTo, {index: this.startIndex, piece: this.movedPiece}) } if (this.view.chessboard.state.inputEnabled) { this.moveDraggablePiece(pageX, pageY) } } } else if (this.moveInputState === STATE.dragTo || this.moveInputState === STATE.clickDragTo || this.moveInputState === STATE.clickTo) { if (target && target.getAttribute && target.parentElement === this.view.boardGroup) { const index = target.getAttribute("data-index") if (index !== this.startIndex && index !== this.endIndex) { this.endIndex = index this.updateStartEndMarkers() } else if (index === this.startIndex && this.endIndex !== undefined) { this.endIndex = undefined this.updateStartEndMarkers() } } else { if (this.endIndex !== undefined) { this.endIndex = undefined this.updateStartEndMarkers() } } if (this.view.chessboard.state.inputEnabled && (this.moveInputState === STATE.dragTo || this.moveInputState === STATE.clickDragTo)) { this.moveDraggablePiece(pageX, pageY) } } } onPointerUp(e) { let target if (e.type === "mouseup") { target = e.target } else if (e.type === "touchend") { target = document.elementFromPoint(e.changedTouches[0].clientX, e.changedTouches[0].clientY) } if (target && target.getAttribute) { const index = target.getAttribute("data-index") if (index) { if (this.moveInputState === STATE.dragTo || this.moveInputState === STATE.clickDragTo) { if (this.startIndex === index) { if (this.moveInputState === STATE.clickDragTo) { this.chessboard.state.setPiece(this.startIndex, this.movedPiece) this.view.setPieceVisibility(this.startIndex) this.moveCanceledCallback(MOVE_CANCELED_REASON.draggedBack, index, index) this.setMoveInputState(STATE.reset) } else { this.setMoveInputState(STATE.clickTo, {index: index}) } } else { this.setMoveInputState(STATE.moveDone, {index: index}) } } else if (this.moveInputState === STATE.pieceClickedThreshold) { this.setMoveInputState(STATE.clickTo, {index: index}) } else if (this.moveInputState === STATE.secondClickThreshold) { this.setMoveInputState(STATE.reset) this.moveCanceledCallback(MOVE_CANCELED_REASON.secondClick, index, index) } } else { this.view.drawPieces() const moveStartIndex = this.startIndex this.setMoveInputState(STATE.reset) this.moveCanceledCallback(MOVE_CANCELED_REASON.movedOutOfBoard, moveStartIndex, undefined) } } else { this.view.drawPieces() this.setMoveInputState(STATE.reset) } } updateStartEndMarkers() { if(this.chessboard.props.style.moveFromMarker) { this.chessboard.state.removeMarkers(undefined, this.chessboard.props.style.moveFromMarker) } if(this.chessboard.props.style.moveToMarker) { this.chessboard.state.removeMarkers(undefined, this.chessboard.props.style.moveToMarker) } if (this.chessboard.props.style.moveFromMarker) { if (this.startIndex) { this.chessboard.state.addMarker(this.startIndex, this.chessboard.props.style.moveFromMarker) } } if (this.chessboard.props.style.moveToMarker) { if (this.endIndex) { this.chessboard.state.addMarker(this.endIndex, this.chessboard.props.style.moveToMarker) } } this.view.drawMarkers() } reset() { this.setMoveInputState(STATE.reset) } destroy() { this.reset() } }