cm-chessboard
Version:
A JavaScript chessboard which is lightweight, ES6 module based, responsive, SVG rendered and without dependencies.
420 lines (386 loc) • 19.3 kB
JavaScript
/**
* Author and copyright: Stefan Haack (https://shaack.com)
* Repository: https://github.com/shaack/cm-chessboard
* License: MIT, see file 'LICENSE'
*/
import {Svg} from "../lib/Svg.js"
import {Utils} from "../lib/Utils.js"
const MOVE_INPUT_STATE = {
waitForInputStart: "waitForInputStart",
pieceClickedThreshold: "pieceClickedThreshold",
clickTo: "clickTo",
secondClickThreshold: "secondClickThreshold",
dragTo: "dragTo",
clickDragTo: "clickDragTo",
moveDone: "moveDone",
reset: "reset"
}
export const MOVE_CANCELED_REASON = {
secondClick: "secondClick", // clicked the same piece
secondaryClick: "secondaryClick", // right click while moving
movedOutOfBoard: "movedOutOfBoard",
draggedBack: "draggedBack", // dragged to the start square
clickedAnotherPiece: "clickedAnotherPiece" // of the same color
}
const DRAG_THRESHOLD = 4
export class VisualMoveInput {
constructor(view) {
this.view = view
this.chessboard = view.chessboard
this.moveInputState = null
this.fromSquare = null
this.toSquare = null
this.setMoveInputState(MOVE_INPUT_STATE.waitForInputStart)
}
moveInputStartedCallback(square) {
const result = this.view.moveInputStartedCallback(square)
if (result) {
this.chessboard.state.moveInputProcess = Utils.createTask()
this.chessboard.state.moveInputProcess.then((result) => {
if (this.moveInputState === MOVE_INPUT_STATE.waitForInputStart ||
this.moveInputState === MOVE_INPUT_STATE.moveDone) {
this.view.moveInputFinishedCallback(this.fromSquare, this.toSquare, result)
}
})
}
return result
}
movingOverSquareCallback(fromSquare, toSquare) {
this.view.movingOverSquareCallback(fromSquare, toSquare)
}
validateMoveInputCallback(fromSquare, toSquare) {
const result = this.view.validateMoveInputCallback(fromSquare, toSquare)
this.chessboard.state.moveInputProcess.resolve(result)
return result
}
moveInputCanceledCallback(fromSquare, toSquare, reason) {
this.view.moveInputCanceledCallback(fromSquare, toSquare, reason)
this.chessboard.state.moveInputProcess.resolve()
}
setMoveInputState(newState, params = undefined) {
const prevState = this.moveInputState
this.moveInputState = newState
switch (newState) {
case MOVE_INPUT_STATE.waitForInputStart:
break
case MOVE_INPUT_STATE.pieceClickedThreshold:
if (MOVE_INPUT_STATE.waitForInputStart !== prevState && MOVE_INPUT_STATE.clickTo !== prevState) {
throw new Error("moveInputState")
}
if (this.pointerMoveListener) {
removeEventListener(this.pointerMoveListener.type, this.pointerMoveListener)
this.pointerMoveListener = null
}
if (this.pointerUpListener) {
removeEventListener(this.pointerUpListener.type, this.pointerUpListener)
this.pointerUpListener = null
}
this.fromSquare = params.square
this.toSquare = null
this.movedPiece = params.piece
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("4b74af")
}
if (!this.contextMenuListener) {
this.contextMenuListener = this.onContextMenu.bind(this)
this.chessboard.view.svg.addEventListener("contextmenu", this.contextMenuListener)
}
} else {
throw Error("94ad0c")
}
break
case MOVE_INPUT_STATE.clickTo:
if (this.draggablePiece) {
Svg.removeElement(this.draggablePiece)
this.draggablePiece = null
}
if (prevState === MOVE_INPUT_STATE.dragTo) {
this.view.setPieceVisibility(params.square, true)
}
break
case MOVE_INPUT_STATE.secondClickThreshold:
if (MOVE_INPUT_STATE.clickTo !== prevState) {
throw new Error("moveInputState")
}
this.startPoint = params.point
break
case MOVE_INPUT_STATE.dragTo:
if (MOVE_INPUT_STATE.pieceClickedThreshold !== prevState) {
throw new Error("moveInputState")
}
if (this.view.chessboard.state.inputEnabled()) {
this.view.setPieceVisibility(params.square, false)
this.createDraggablePiece(params.piece)
}
break
case MOVE_INPUT_STATE.clickDragTo:
if (MOVE_INPUT_STATE.secondClickThreshold !== prevState) {
throw new Error("moveInputState")
}
if (this.view.chessboard.state.inputEnabled()) {
this.view.setPieceVisibility(params.square, false)
this.createDraggablePiece(params.piece)
}
break
case MOVE_INPUT_STATE.moveDone:
if ([MOVE_INPUT_STATE.dragTo, MOVE_INPUT_STATE.clickTo, MOVE_INPUT_STATE.clickDragTo].indexOf(prevState) === -1) {
throw new Error("moveInputState")
}
this.toSquare = params.square
if (this.toSquare && this.validateMoveInputCallback(this.fromSquare, this.toSquare)) {
this.chessboard.movePiece(this.fromSquare, this.toSquare, prevState === MOVE_INPUT_STATE.clickTo).then(() => {
if (prevState === MOVE_INPUT_STATE.clickTo) {
this.view.setPieceVisibility(this.toSquare, true)
}
this.setMoveInputState(MOVE_INPUT_STATE.reset)
})
} else {
this.view.setPieceVisibility(this.fromSquare, true)
this.setMoveInputState(MOVE_INPUT_STATE.reset)
}
break
case MOVE_INPUT_STATE.reset:
if (this.fromSquare && !this.toSquare && this.movedPiece) {
this.chessboard.state.position.setPiece(this.fromSquare, this.movedPiece)
}
this.fromSquare = null
this.toSquare = null
this.movedPiece = null
if (this.draggablePiece) {
Svg.removeElement(this.draggablePiece)
this.draggablePiece = null
}
if (this.pointerMoveListener) {
removeEventListener(this.pointerMoveListener.type, this.pointerMoveListener)
this.pointerMoveListener = null
}
if (this.pointerUpListener) {
removeEventListener(this.pointerUpListener.type, this.pointerUpListener)
this.pointerUpListener = null
}
if (this.contextMenuListener) {
removeEventListener("contextmenu", this.contextMenuListener)
this.contextMenuListener = null
}
this.setMoveInputState(MOVE_INPUT_STATE.waitForInputStart)
// set temporarily hidden pieces visible again
const hiddenPieces = this.view.piecesGroup.querySelectorAll("[visibility=hidden]")
for (let i = 0; i < hiddenPieces.length; i++) {
hiddenPieces[i].removeAttribute("visibility")
}
break
default:
throw Error(`260b09: moveInputState ${newState}`)
}
}
createDraggablePiece(pieceName) {
// maybe I should use the existing piece from the board and don't create a new one
if (this.draggablePiece) {
throw Error("draggablePiece already 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.assetsCache ? "" : this.view.getSpriteUrl()
const piece = Svg.addElement(this.draggablePiece, "use", {
href: `${spriteUrl}#${pieceName}`
})
const scaling = this.view.squareHeight / this.chessboard.props.style.pieces.tileSize
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")) {
return
}
const square = e.target.getAttribute("data-square")
if (!square) { // pointer on square
return
}
const pieceName = this.chessboard.getPiece(square)
let color
if (pieceName) {
color = pieceName ? pieceName.substring(0, 1) : null
// allow scrolling, if not pointed on draggable piece
if (color === "w" && this.chessboard.state.inputWhiteEnabled ||
color === "b" && this.chessboard.state.inputBlackEnabled) {
e.preventDefault()
}
}
if (this.moveInputState !== MOVE_INPUT_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 === MOVE_INPUT_STATE.waitForInputStart && pieceName && this.moveInputStartedCallback(square)) {
this.setMoveInputState(MOVE_INPUT_STATE.pieceClickedThreshold, {
square: square,
piece: pieceName,
point: point,
type: e.type
})
} else if (this.moveInputState === MOVE_INPUT_STATE.clickTo) {
if (square === this.fromSquare) {
this.setMoveInputState(MOVE_INPUT_STATE.secondClickThreshold, {
square: square,
piece: pieceName,
point: point,
type: e.type
})
} else {
const pieceName = this.chessboard.getPiece(square)
const pieceColor = pieceName ? pieceName.substring(0, 1) : null
const startPieceName = this.chessboard.getPiece(this.fromSquare)
const startPieceColor = startPieceName ? startPieceName.substring(0, 1) : null
if (color && startPieceColor === pieceColor) {
// added to allow chess960 castling
const result = this.validateMoveInputCallback(this.fromSquare, square)
if(!result) {
this.moveInputCanceledCallback(this.fromSquare, square, MOVE_CANCELED_REASON.clickedAnotherPiece)
if (this.moveInputStartedCallback(square)) {
this.setMoveInputState(MOVE_INPUT_STATE.pieceClickedThreshold, {
square: square,
piece: pieceName,
point: point,
type: e.type
})
} else {
this.setMoveInputState(MOVE_INPUT_STATE.reset)
}
}
} else {
this.setMoveInputState(MOVE_INPUT_STATE.moveDone, {square: square})
}
}
}
}
}
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 === MOVE_INPUT_STATE.pieceClickedThreshold || this.moveInputState === MOVE_INPUT_STATE.secondClickThreshold) {
if (Math.abs(this.startPoint.x - clientX) > DRAG_THRESHOLD || Math.abs(this.startPoint.y - clientY) > DRAG_THRESHOLD) {
if (this.moveInputState === MOVE_INPUT_STATE.secondClickThreshold) {
this.setMoveInputState(MOVE_INPUT_STATE.clickDragTo, {
square: this.fromSquare,
piece: this.movedPiece
})
} else {
this.setMoveInputState(MOVE_INPUT_STATE.dragTo, {square: this.fromSquare, piece: this.movedPiece})
}
if (this.view.chessboard.state.inputEnabled()) {
this.moveDraggablePiece(pageX, pageY)
}
}
} else if (this.moveInputState === MOVE_INPUT_STATE.dragTo || this.moveInputState === MOVE_INPUT_STATE.clickDragTo || this.moveInputState === MOVE_INPUT_STATE.clickTo) {
if (target && target.getAttribute && target.parentElement === this.view.boardGroup) {
const square = target.getAttribute("data-square")
if (square !== this.fromSquare && square !== this.toSquare) {
this.toSquare = square
this.movingOverSquareCallback(this.fromSquare, this.toSquare)
} else if (square === this.fromSquare && this.toSquare !== null) {
this.toSquare = null
this.movingOverSquareCallback(this.fromSquare, null)
}
} else if (this.toSquare !== null) {
this.toSquare = null
this.movingOverSquareCallback(this.fromSquare, null)
}
if (this.view.chessboard.state.inputEnabled() && (this.moveInputState === MOVE_INPUT_STATE.dragTo || this.moveInputState === MOVE_INPUT_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 square = target.getAttribute("data-square")
if (square) {
if (this.moveInputState === MOVE_INPUT_STATE.dragTo || this.moveInputState === MOVE_INPUT_STATE.clickDragTo) {
if (this.fromSquare === square) {
if (this.moveInputState === MOVE_INPUT_STATE.clickDragTo) {
this.chessboard.state.position.setPiece(this.fromSquare, this.movedPiece)
this.view.setPieceVisibility(this.fromSquare)
this.moveInputCanceledCallback(square, null, MOVE_CANCELED_REASON.draggedBack)
this.setMoveInputState(MOVE_INPUT_STATE.reset)
} else {
this.setMoveInputState(MOVE_INPUT_STATE.clickTo, {square: square})
}
} else {
this.setMoveInputState(MOVE_INPUT_STATE.moveDone, {square: square})
}
} else if (this.moveInputState === MOVE_INPUT_STATE.pieceClickedThreshold) {
this.setMoveInputState(MOVE_INPUT_STATE.clickTo, {square: square})
} else if (this.moveInputState === MOVE_INPUT_STATE.secondClickThreshold) {
this.setMoveInputState(MOVE_INPUT_STATE.reset)
this.moveInputCanceledCallback(square, null, MOVE_CANCELED_REASON.secondClick)
}
} else {
this.view.redrawPieces()
const moveStartSquare = this.fromSquare
this.setMoveInputState(MOVE_INPUT_STATE.reset)
this.moveInputCanceledCallback(moveStartSquare, null, MOVE_CANCELED_REASON.movedOutOfBoard)
}
} else {
this.view.redrawPieces()
this.setMoveInputState(MOVE_INPUT_STATE.reset)
}
}
onContextMenu(e) { // while moving
e.preventDefault()
this.view.redrawPieces()
this.setMoveInputState(MOVE_INPUT_STATE.reset)
this.moveInputCanceledCallback(this.fromSquare, null, MOVE_CANCELED_REASON.secondaryClick)
}
isDragging() {
return this.moveInputState === MOVE_INPUT_STATE.dragTo || this.moveInputState === MOVE_INPUT_STATE.clickDragTo
}
destroy() {
this.setMoveInputState(MOVE_INPUT_STATE.reset)
}
}