You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1818 lines
53 KiB

// chessboard.js v1.0.0
// https://github.com/oakmac/chessboardjs/
//
// Copyright (c) 2019, Chris Oakman
// Released under the MIT license
// https://github.com/oakmac/chessboardjs/blob/master/LICENSE.md
// start anonymous scope
;(function () {
'use strict'
var $ = window['jQuery']
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
var COLUMNS = 'abcdefgh'.split('')
var DEFAULT_DRAG_THROTTLE_RATE = 20
var ELLIPSIS = '…'
var MINIMUM_JQUERY_VERSION = '1.8.3'
var RUN_ASSERTS = false
var START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'
var START_POSITION = fenToObj(START_FEN)
// default animation speeds
var DEFAULT_APPEAR_SPEED = 200
var DEFAULT_MOVE_SPEED = 200
var DEFAULT_SNAPBACK_SPEED = 60
var DEFAULT_SNAP_SPEED = 30
var DEFAULT_TRASH_SPEED = 100
// use unique class names to prevent clashing with anything else on the page
// and simplify selectors
// NOTE: these should never change
var CSS = {}
CSS['alpha'] = 'alpha-d2270'
CSS['black'] = 'black-3c85d'
CSS['board'] = 'board-b72b1'
CSS['chessboard'] = 'chessboard-63f37'
CSS['clearfix'] = 'clearfix-7da63'
CSS['highlight1'] = 'highlight1-32417'
CSS['highlight2'] = 'highlight2-9c5d2'
CSS['notation'] = 'notation-322f9'
CSS['numeric'] = 'numeric-fc462'
CSS['piece'] = 'piece-417db'
CSS['row'] = 'row-5277c'
CSS['sparePieces'] = 'spare-pieces-7492f'
CSS['sparePiecesBottom'] = 'spare-pieces-bottom-ae20f'
CSS['sparePiecesTop'] = 'spare-pieces-top-4028b'
CSS['square'] = 'square-55d63'
CSS['white'] = 'white-1e1d7'
// ---------------------------------------------------------------------------
// Misc Util Functions
// ---------------------------------------------------------------------------
function throttle (f, interval, scope) {
var timeout = 0
var shouldFire = false
var args = []
var handleTimeout = function () {
timeout = 0
if (shouldFire) {
shouldFire = false
fire()
}
}
var fire = function () {
timeout = window.setTimeout(handleTimeout, interval)
f.apply(scope, args)
}
return function (_args) {
args = arguments
if (!timeout) {
fire()
} else {
shouldFire = true
}
}
}
// function debounce (f, interval, scope) {
// var timeout = 0
// return function (_args) {
// window.clearTimeout(timeout)
// var args = arguments
// timeout = window.setTimeout(function () {
// f.apply(scope, args)
// }, interval)
// }
// }
function uuid () {
return 'xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx'.replace(/x/g, function (c) {
var r = (Math.random() * 16) | 0
return r.toString(16)
})
}
function deepCopy (thing) {
return JSON.parse(JSON.stringify(thing))
}
function parseSemVer (version) {
var tmp = version.split('.')
return {
major: parseInt(tmp[0], 10),
minor: parseInt(tmp[1], 10),
patch: parseInt(tmp[2], 10)
}
}
// returns true if version is >= minimum
function validSemanticVersion (version, minimum) {
version = parseSemVer(version)
minimum = parseSemVer(minimum)
var versionNum = (version.major * 100000 * 100000) +
(version.minor * 100000) +
version.patch
var minimumNum = (minimum.major * 100000 * 100000) +
(minimum.minor * 100000) +
minimum.patch
return versionNum >= minimumNum
}
function interpolateTemplate (str, obj) {
for (var key in obj) {
if (!obj.hasOwnProperty(key)) continue
var keyTemplateStr = '{' + key + '}'
var value = obj[key]
while (str.indexOf(keyTemplateStr) !== -1) {
str = str.replace(keyTemplateStr, value)
}
}
return str
}
if (RUN_ASSERTS) {
console.assert(interpolateTemplate('abc', {a: 'x'}) === 'abc')
console.assert(interpolateTemplate('{a}bc', {}) === '{a}bc')
console.assert(interpolateTemplate('{a}bc', {p: 'q'}) === '{a}bc')
console.assert(interpolateTemplate('{a}bc', {a: 'x'}) === 'xbc')
console.assert(interpolateTemplate('{a}bc{a}bc', {a: 'x'}) === 'xbcxbc')
console.assert(interpolateTemplate('{a}{a}{b}', {a: 'x', b: 'y'}) === 'xxy')
}
// ---------------------------------------------------------------------------
// Predicates
// ---------------------------------------------------------------------------
function isString (s) {
return typeof s === 'string'
}
function isFunction (f) {
return typeof f === 'function'
}
function isInteger (n) {
return typeof n === 'number' &&
isFinite(n) &&
Math.floor(n) === n
}
function validAnimationSpeed (speed) {
if (speed === 'fast' || speed === 'slow') return true
if (!isInteger(speed)) return false
return speed >= 0
}
function validThrottleRate (rate) {
return isInteger(rate) &&
rate >= 1
}
function validMove (move) {
// move should be a string
if (!isString(move)) return false
// move should be in the form of "e2-e4", "f6-d5"
var squares = move.split('-')
if (squares.length !== 2) return false
return validSquare(squares[0]) && validSquare(squares[1])
}
function validSquare (square) {
return isString(square) && square.search(/^[a-h][1-8]$/) !== -1
}
if (RUN_ASSERTS) {
console.assert(validSquare('a1'))
console.assert(validSquare('e2'))
console.assert(!validSquare('D2'))
console.assert(!validSquare('g9'))
console.assert(!validSquare('a'))
console.assert(!validSquare(true))
console.assert(!validSquare(null))
console.assert(!validSquare({}))
}
function validPieceCode (code) {
return isString(code) && code.search(/^[bw][KQRNBP]$/) !== -1
}
if (RUN_ASSERTS) {
console.assert(validPieceCode('bP'))
console.assert(validPieceCode('bK'))
console.assert(validPieceCode('wK'))
console.assert(validPieceCode('wR'))
console.assert(!validPieceCode('WR'))
console.assert(!validPieceCode('Wr'))
console.assert(!validPieceCode('a'))
console.assert(!validPieceCode(true))
console.assert(!validPieceCode(null))
console.assert(!validPieceCode({}))
}
function validFen (fen) {
if (!isString(fen)) return false
// cut off any move, castling, etc info from the end
// we're only interested in position information
fen = fen.replace(/ .+$/, '')
// expand the empty square numbers to just 1s
fen = expandFenEmptySquares(fen)
// FEN should be 8 sections separated by slashes
var chunks = fen.split('/')
if (chunks.length !== 8) return false
// check each section
for (var i = 0; i < 8; i++) {
if (chunks[i].length !== 8 ||
chunks[i].search(/[^kqrnbpKQRNBP1]/) !== -1) {
return false
}
}
return true
}
if (RUN_ASSERTS) {
console.assert(validFen(START_FEN))
console.assert(validFen('8/8/8/8/8/8/8/8'))
console.assert(validFen('r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R'))
console.assert(validFen('3r3r/1p4pp/2nb1k2/pP3p2/8/PB2PN2/p4PPP/R4RK1 b - - 0 1'))
console.assert(!validFen('3r3z/1p4pp/2nb1k2/pP3p2/8/PB2PN2/p4PPP/R4RK1 b - - 0 1'))
console.assert(!validFen('anbqkbnr/8/8/8/8/8/PPPPPPPP/8'))
console.assert(!validFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/'))
console.assert(!validFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBN'))
console.assert(!validFen('888888/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'))
console.assert(!validFen('888888/pppppppp/74/8/8/8/PPPPPPPP/RNBQKBNR'))
console.assert(!validFen({}))
}
function validPositionObject (pos) {
if (!$.isPlainObject(pos)) return false
for (var i in pos) {
if (!pos.hasOwnProperty(i)) continue
if (!validSquare(i) || !validPieceCode(pos[i])) {
return false
}
}
return true
}
if (RUN_ASSERTS) {
console.assert(validPositionObject(START_POSITION))
console.assert(validPositionObject({}))
console.assert(validPositionObject({e2: 'wP'}))
console.assert(validPositionObject({e2: 'wP', d2: 'wP'}))
console.assert(!validPositionObject({e2: 'BP'}))
console.assert(!validPositionObject({y2: 'wP'}))
console.assert(!validPositionObject(null))
console.assert(!validPositionObject('start'))
console.assert(!validPositionObject(START_FEN))
}
function isTouchDevice () {
return 'ontouchstart' in document.documentElement
}
function validJQueryVersion () {
return typeof window.$ &&
$.fn &&
$.fn.jquery &&
validSemanticVersion($.fn.jquery, MINIMUM_JQUERY_VERSION)
}
// ---------------------------------------------------------------------------
// Chess Util Functions
// ---------------------------------------------------------------------------
// convert FEN piece code to bP, wK, etc
function fenToPieceCode (piece) {
// black piece
if (piece.toLowerCase() === piece) {
return 'b' + piece.toUpperCase()
}
// white piece
return 'w' + piece.toUpperCase()
}
// convert bP, wK, etc code to FEN structure
function pieceCodeToFen (piece) {
var pieceCodeLetters = piece.split('')
// white piece
if (pieceCodeLetters[0] === 'w') {
return pieceCodeLetters[1].toUpperCase()
}
// black piece
return pieceCodeLetters[1].toLowerCase()
}
// convert FEN string to position object
// returns false if the FEN string is invalid
function fenToObj (fen) {
if (!validFen(fen)) return false
// cut off any move, castling, etc info from the end
// we're only interested in position information
fen = fen.replace(/ .+$/, '')
var rows = fen.split('/')
var position = {}
var currentRow = 8
for (var i = 0; i < 8; i++) {
var row = rows[i].split('')
var colIdx = 0
// loop through each character in the FEN section
for (var j = 0; j < row.length; j++) {
// number / empty squares
if (row[j].search(/[1-8]/) !== -1) {
var numEmptySquares = parseInt(row[j], 10)
colIdx = colIdx + numEmptySquares
} else {
// piece
var square = COLUMNS[colIdx] + currentRow
position[square] = fenToPieceCode(row[j])
colIdx = colIdx + 1
}
}
currentRow = currentRow - 1
}
return position
}
// position object to FEN string
// returns false if the obj is not a valid position object
function objToFen (obj) {
if (!validPositionObject(obj)) return false
var fen = ''
var currentRow = 8
for (var i = 0; i < 8; i++) {
for (var j = 0; j < 8; j++) {
var square = COLUMNS[j] + currentRow
// piece exists
if (obj.hasOwnProperty(square)) {
fen = fen + pieceCodeToFen(obj[square])
} else {
// empty space
fen = fen + '1'
}
}
if (i !== 7) {
fen = fen + '/'
}
currentRow = currentRow - 1
}
// squeeze the empty numbers together
fen = squeezeFenEmptySquares(fen)
return fen
}
if (RUN_ASSERTS) {
console.assert(objToFen(START_POSITION) === START_FEN)
console.assert(objToFen({}) === '8/8/8/8/8/8/8/8')
console.assert(objToFen({a2: 'wP', 'b2': 'bP'}) === '8/8/8/8/8/8/Pp6/8')
}
function squeezeFenEmptySquares (fen) {
return fen.replace(/11111111/g, '8')
.replace(/1111111/g, '7')
.replace(/111111/g, '6')
.replace(/11111/g, '5')
.replace(/1111/g, '4')
.replace(/111/g, '3')
.replace(/11/g, '2')
}
function expandFenEmptySquares (fen) {
return fen.replace(/8/g, '11111111')
.replace(/7/g, '1111111')
.replace(/6/g, '111111')
.replace(/5/g, '11111')
.replace(/4/g, '1111')
.replace(/3/g, '111')
.replace(/2/g, '11')
}
// returns the distance between two squares
function squareDistance (squareA, squareB) {
var squareAArray = squareA.split('')
var squareAx = COLUMNS.indexOf(squareAArray[0]) + 1
var squareAy = parseInt(squareAArray[1], 10)
var squareBArray = squareB.split('')
var squareBx = COLUMNS.indexOf(squareBArray[0]) + 1
var squareBy = parseInt(squareBArray[1], 10)
var xDelta = Math.abs(squareAx - squareBx)
var yDelta = Math.abs(squareAy - squareBy)
if (xDelta >= yDelta) return xDelta
return yDelta
}
// returns the square of the closest instance of piece
// returns false if no instance of piece is found in position
function findClosestPiece (position, piece, square) {
// create array of closest squares from square
var closestSquares = createRadius(square)
// search through the position in order of distance for the piece
for (var i = 0; i < closestSquares.length; i++) {
var s = closestSquares[i]
if (position.hasOwnProperty(s) && position[s] === piece) {
return s
}
}
return false
}
// returns an array of closest squares from square
function createRadius (square) {
var squares = []
// calculate distance of all squares
for (var i = 0; i < 8; i++) {
for (var j = 0; j < 8; j++) {
var s = COLUMNS[i] + (j + 1)
// skip the square we're starting from
if (square === s) continue
squares.push({
square: s,
distance: squareDistance(square, s)
})
}
}
// sort by distance
squares.sort(function (a, b) {
return a.distance - b.distance
})
// just return the square code
var surroundingSquares = []
for (i = 0; i < squares.length; i++) {
surroundingSquares.push(squares[i].square)
}
return surroundingSquares
}
// given a position and a set of moves, return a new position
// with the moves executed
function calculatePositionFromMoves (position, moves) {
var newPosition = deepCopy(position)
for (var i in moves) {
if (!moves.hasOwnProperty(i)) continue
// skip the move if the position doesn't have a piece on the source square
if (!newPosition.hasOwnProperty(i)) continue
var piece = newPosition[i]
delete newPosition[i]
newPosition[moves[i]] = piece
}
return newPosition
}
// TODO: add some asserts here for calculatePositionFromMoves
// ---------------------------------------------------------------------------
// HTML
// ---------------------------------------------------------------------------
function buildContainerHTML (hasSparePieces) {
var html = '<div class="{chessboard}">'
if (hasSparePieces) {
html += '<div class="{sparePieces} {sparePiecesTop}"></div>'
}
html += '<div class="{board}"></div>'
if (hasSparePieces) {
html += '<div class="{sparePieces} {sparePiecesBottom}"></div>'
}
html += '</div>'
return interpolateTemplate(html, CSS)
}
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
function expandConfigArgumentShorthand (config) {
if (config === 'start') {
config = {position: deepCopy(START_POSITION)}
} else if (validFen(config)) {
config = {position: fenToObj(config)}
} else if (validPositionObject(config)) {
config = {position: deepCopy(config)}
}
// config must be an object
if (!$.isPlainObject(config)) config = {}
return config
}
// validate config / set default options
function expandConfig (config) {
// default for orientation is white
if (config.orientation !== 'black') config.orientation = 'white'
// default for showNotation is true
if (config.showNotation !== false) config.showNotation = true
// default for draggable is false
if (config.draggable !== true) config.draggable = false
// default for dropOffBoard is 'snapback'
if (config.dropOffBoard !== 'trash') config.dropOffBoard = 'snapback'
// default for sparePieces is false
if (config.sparePieces !== true) config.sparePieces = false
// draggable must be true if sparePieces is enabled
if (config.sparePieces) config.draggable = true
// default piece theme is wikipedia
if (!config.hasOwnProperty('pieceTheme') ||
(!isString(config.pieceTheme) && !isFunction(config.pieceTheme))) {
config.pieceTheme = 'img/chesspieces/wikipedia/{piece}.png'
}
// animation speeds
if (!validAnimationSpeed(config.appearSpeed)) config.appearSpeed = DEFAULT_APPEAR_SPEED
if (!validAnimationSpeed(config.moveSpeed)) config.moveSpeed = DEFAULT_MOVE_SPEED
if (!validAnimationSpeed(config.snapbackSpeed)) config.snapbackSpeed = DEFAULT_SNAPBACK_SPEED
if (!validAnimationSpeed(config.snapSpeed)) config.snapSpeed = DEFAULT_SNAP_SPEED
if (!validAnimationSpeed(config.trashSpeed)) config.trashSpeed = DEFAULT_TRASH_SPEED
// throttle rate
if (!validThrottleRate(config.dragThrottleRate)) config.dragThrottleRate = DEFAULT_DRAG_THROTTLE_RATE
return config
}
// ---------------------------------------------------------------------------
// Dependencies
// ---------------------------------------------------------------------------
// check for a compatible version of jQuery
function checkJQuery () {
if (!validJQueryVersion()) {
var errorMsg = 'Chessboard Error 1005: Unable to find a valid version of jQuery. ' +
'Please include jQuery ' + MINIMUM_JQUERY_VERSION + ' or higher on the page' +
'\n\n' +
'Exiting' + ELLIPSIS
window.alert(errorMsg)
return false
}
return true
}
// return either boolean false or the $container element
function checkContainerArg (containerElOrString) {
if (containerElOrString === '') {
var errorMsg1 = 'Chessboard Error 1001: ' +
'The first argument to Chessboard() cannot be an empty string.' +
'\n\n' +
'Exiting' + ELLIPSIS
window.alert(errorMsg1)
return false
}
// convert containerEl to query selector if it is a string
if (isString(containerElOrString) &&
containerElOrString.charAt(0) !== '#') {
containerElOrString = '#' + containerElOrString
}
// containerEl must be something that becomes a jQuery collection of size 1
var $container = $(containerElOrString)
if ($container.length !== 1) {
var errorMsg2 = 'Chessboard Error 1003: ' +
'The first argument to Chessboard() must be the ID of a DOM node, ' +
'an ID query selector, or a single DOM node.' +
'\n\n' +
'Exiting' + ELLIPSIS
window.alert(errorMsg2)
return false
}
return $container
}
// ---------------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------------
function constructor (containerElOrString, config) {
// first things first: check basic dependencies
if (!checkJQuery()) return null
var $container = checkContainerArg(containerElOrString)
if (!$container) return null
// ensure the config object is what we expect
config = expandConfigArgumentShorthand(config)
config = expandConfig(config)
// DOM elements
var $board = null
var $draggedPiece = null
var $sparePiecesTop = null
var $sparePiecesBottom = null
// constructor return object
var widget = {}
// -------------------------------------------------------------------------
// Stateful
// -------------------------------------------------------------------------
var boardBorderSize = 2
var currentOrientation = 'white'
var currentPosition = {}
var draggedPiece = null
var draggedPieceLocation = null
var draggedPieceSource = null
var isDragging = false
var sparePiecesElsIds = {}
var squareElsIds = {}
var squareElsOffsets = {}
var squareSize = 16
// -------------------------------------------------------------------------
// Validation / Errors
// -------------------------------------------------------------------------
function error (code, msg, obj) {
// do nothing if showErrors is not set
if (
config.hasOwnProperty('showErrors') !== true ||
config.showErrors === false
) {
return
}
var errorText = 'Chessboard Error ' + code + ': ' + msg
// print to console
if (
config.showErrors === 'console' &&
typeof console === 'object' &&
typeof console.log === 'function'
) {
console.log(errorText)
if (arguments.length >= 2) {
console.log(obj)
}
return
}
// alert errors
if (config.showErrors === 'alert') {
if (obj) {
errorText += '\n\n' + JSON.stringify(obj)
}
window.alert(errorText)
return
}
// custom function
if (isFunction(config.showErrors)) {
config.showErrors(code, msg, obj)
}
}
function setInitialState () {
currentOrientation = config.orientation
// make sure position is valid
if (config.hasOwnProperty('position')) {
if (config.position === 'start') {
currentPosition = deepCopy(START_POSITION)
} else if (validFen(config.position)) {
currentPosition = fenToObj(config.position)
} else if (validPositionObject(config.position)) {
currentPosition = deepCopy(config.position)
} else {
error(
7263,
'Invalid value passed to config.position.',
config.position
)
}
}
}
// -------------------------------------------------------------------------
// DOM Misc
// -------------------------------------------------------------------------
// calculates square size based on the width of the container
// got a little CSS black magic here, so let me explain:
// get the width of the container element (could be anything), reduce by 1 for
// fudge factor, and then keep reducing until we find an exact mod 8 for
// our square size
function calculateSquareSize () {
var containerWidth = parseInt($container.width(), 10)
// defensive, prevent infinite loop
if (!containerWidth || containerWidth <= 0) {
return 0
}
// pad one pixel
var boardWidth = containerWidth - 1
while (boardWidth % 8 !== 0 && boardWidth > 0) {
boardWidth = boardWidth - 1
}
return boardWidth / 8
}
// create random IDs for elements
function createElIds () {
// squares on the board
for (var i = 0; i < COLUMNS.length; i++) {
for (var j = 1; j <= 8; j++) {
var square = COLUMNS[i] + j
squareElsIds[square] = square + '-' + uuid()
}
}
// spare pieces
var pieces = 'KQRNBP'.split('')
for (i = 0; i < pieces.length; i++) {
var whitePiece = 'w' + pieces[i]
var blackPiece = 'b' + pieces[i]
sparePiecesElsIds[whitePiece] = whitePiece + '-' + uuid()
sparePiecesElsIds[blackPiece] = blackPiece + '-' + uuid()
}
}
// -------------------------------------------------------------------------
// Markup Building
// -------------------------------------------------------------------------
function buildBoardHTML (orientation) {
if (orientation !== 'black') {
orientation = 'white'
}
var html = ''
// algebraic notation / orientation
var alpha = deepCopy(COLUMNS)
var row = 8
if (orientation === 'black') {
alpha.reverse()
row = 1
}
var squareColor = 'white'
for (var i = 0; i < 8; i++) {
html += '<div class="{row}">'
for (var j = 0; j < 8; j++) {
var square = alpha[j] + row
html += '<div class="{square} ' + CSS[squareColor] + ' ' +
'square-' + square + '" ' +
'style="width:' + squareSize + 'px;height:' + squareSize + 'px;" ' +
'id="' + squareElsIds[square] + '" ' +
'data-square="' + square + '">'
if (config.showNotation) {
// alpha notation
if ((orientation === 'white' && row === 1) ||
(orientation === 'black' && row === 8)) {
html += '<div class="{notation} {alpha}">' + alpha[j] + '</div>'
}
// numeric notation
if (j === 0) {
html += '<div class="{notation} {numeric}">' + row + '</div>'
}
}
html += '</div>' // end .square
squareColor = (squareColor === 'white') ? 'black' : 'white'
}
html += '<div class="{clearfix}"></div></div>'
squareColor = (squareColor === 'white') ? 'black' : 'white'
if (orientation === 'white') {
row = row - 1
} else {
row = row + 1
}
}
return interpolateTemplate(html, CSS)
}
function buildPieceImgSrc (piece) {
if (isFunction(config.pieceTheme)) {
return config.pieceTheme(piece)
}
if (isString(config.pieceTheme)) {
return interpolateTemplate(config.pieceTheme, {piece: piece})
}
// NOTE: this should never happen
error(8272, 'Unable to build image source for config.pieceTheme.')
return ''
}
function buildPieceHTML (piece, hidden, id) {
var html = '<img src="' + buildPieceImgSrc(piece) + '" '
if (isString(id) && id !== '') {
html += 'id="' + id + '" '
}
html += 'alt="" ' +
'class="{piece}" ' +
'data-piece="' + piece + '" ' +
'style="width:' + squareSize + 'px;' + 'height:' + squareSize + 'px;'
if (hidden) {
html += 'display:none;'
}
html += '" />'
return interpolateTemplate(html, CSS)
}
function buildSparePiecesHTML (color) {
var pieces = ['wK', 'wQ', 'wR', 'wB', 'wN', 'wP']
if (color === 'black') {
pieces = ['bK', 'bQ', 'bR', 'bB', 'bN', 'bP']
}
var html = ''
for (var i = 0; i < pieces.length; i++) {
html += buildPieceHTML(pieces[i], false, sparePiecesElsIds[pieces[i]])
}
return html
}
// -------------------------------------------------------------------------
// Animations
// -------------------------------------------------------------------------
function animateSquareToSquare (src, dest, piece, completeFn) {
// get information about the source and destination squares
var $srcSquare = $('#' + squareElsIds[src])
var srcSquarePosition = $srcSquare.offset()
var $destSquare = $('#' + squareElsIds[dest])
var destSquarePosition = $destSquare.offset()
// create the animated piece and absolutely position it
// over the source square
var animatedPieceId = uuid()
$('body').append(buildPieceHTML(piece, true, animatedPieceId))
var $animatedPiece = $('#' + animatedPieceId)
$animatedPiece.css({
display: '',
position: 'absolute',
top: srcSquarePosition.top,
left: srcSquarePosition.left
})
// remove original piece from source square
$srcSquare.find('.' + CSS.piece).remove()
function onFinishAnimation1 () {
// add the "real" piece to the destination square
$destSquare.append(buildPieceHTML(piece))
// remove the animated piece
$animatedPiece.remove()
// run complete function
if (isFunction(completeFn)) {
completeFn()
}
}
// animate the piece to the destination square
var opts = {
duration: config.moveSpeed,
complete: onFinishAnimation1
}
$animatedPiece.animate(destSquarePosition, opts)
}
function animateSparePieceToSquare (piece, dest, completeFn) {
var srcOffset = $('#' + sparePiecesElsIds[piece]).offset()
var $destSquare = $('#' + squareElsIds[dest])
var destOffset = $destSquare.offset()
// create the animate piece
var pieceId = uuid()
$('body').append(buildPieceHTML(piece, true, pieceId))
var $animatedPiece = $('#' + pieceId)
$animatedPiece.css({
display: '',
position: 'absolute',
left: srcOffset.left,
top: srcOffset.top
})
// on complete
function onFinishAnimation2 () {
// add the "real" piece to the destination square
$destSquare.find('.' + CSS.piece).remove()
$destSquare.append(buildPieceHTML(piece))
// remove the animated piece
$animatedPiece.remove()
// run complete function
if (isFunction(completeFn)) {
completeFn()
}
}
// animate the piece to the destination square
var opts = {
duration: config.moveSpeed,
complete: onFinishAnimation2
}
$animatedPiece.animate(destOffset, opts)
}
// execute an array of animations
function doAnimations (animations, oldPos, newPos) {
if (animations.length === 0) return
var numFinished = 0
function onFinishAnimation3 () {
// exit if all the animations aren't finished
numFinished = numFinished + 1
if (numFinished !== animations.length) return
drawPositionInstant()
// run their onMoveEnd function
if (isFunction(config.onMoveEnd)) {
config.onMoveEnd(deepCopy(oldPos), deepCopy(newPos))
}
}
for (var i = 0; i < animations.length; i++) {
var animation = animations[i]
// clear a piece
if (animation.type === 'clear') {
$('#' + squareElsIds[animation.square] + ' .' + CSS.piece)
.fadeOut(config.trashSpeed, onFinishAnimation3)
// add a piece with no spare pieces - fade the piece onto the square
} else if (animation.type === 'add' && !config.sparePieces) {
$('#' + squareElsIds[animation.square])
.append(buildPieceHTML(animation.piece, true))
.find('.' + CSS.piece)
.fadeIn(config.appearSpeed, onFinishAnimation3)
// add a piece with spare pieces - animate from the spares
} else if (animation.type === 'add' && config.sparePieces) {
animateSparePieceToSquare(animation.piece, animation.square, onFinishAnimation3)
// move a piece from squareA to squareB
} else if (animation.type === 'move') {
animateSquareToSquare(animation.source, animation.destination, animation.piece, onFinishAnimation3)
}
}
}
// calculate an array of animations that need to happen in order to get
// from pos1 to pos2
function calculateAnimations (pos1, pos2) {
// make copies of both
pos1 = deepCopy(pos1)
pos2 = deepCopy(pos2)
var animations = []
var squaresMovedTo = {}
// remove pieces that are the same in both positions
for (var i in pos2) {
if (!pos2.hasOwnProperty(i)) continue
if (pos1.hasOwnProperty(i) && pos1[i] === pos2[i]) {
delete pos1[i]
delete pos2[i]
}
}
// find all the "move" animations
for (i in pos2) {
if (!pos2.hasOwnProperty(i)) continue
var closestPiece = findClosestPiece(pos1, pos2[i], i)
if (closestPiece) {
animations.push({
type: 'move',
source: closestPiece,
destination: i,
piece: pos2[i]
})
delete pos1[closestPiece]
delete pos2[i]
squaresMovedTo[i] = true
}
}
// "add" animations
for (i in pos2) {
if (!pos2.hasOwnProperty(i)) continue
animations.push({
type: 'add',
square: i,
piece: pos2[i]
})
delete pos2[i]
}
// "clear" animations
for (i in pos1) {
if (!pos1.hasOwnProperty(i)) continue
// do not clear a piece if it is on a square that is the result
// of a "move", ie: a piece capture
if (squaresMovedTo.hasOwnProperty(i)) continue
animations.push({
type: 'clear',
square: i,
piece: pos1[i]
})
delete pos1[i]
}
return animations
}
// -------------------------------------------------------------------------
// Control Flow
// -------------------------------------------------------------------------
function drawPositionInstant () {
// clear the board
$board.find('.' + CSS.piece).remove()
// add the pieces
for (var i in currentPosition) {
if (!currentPosition.hasOwnProperty(i)) continue
$('#' + squareElsIds[i]).append(buildPieceHTML(currentPosition[i]))
}
}
function drawBoard () {
$board.html(buildBoardHTML(currentOrientation, squareSize, config.showNotation))
drawPositionInstant()
if (config.sparePieces) {
if (currentOrientation === 'white') {
$sparePiecesTop.html(buildSparePiecesHTML('black'))
$sparePiecesBottom.html(buildSparePiecesHTML('white'))
} else {
$sparePiecesTop.html(buildSparePiecesHTML('white'))
$sparePiecesBottom.html(buildSparePiecesHTML('black'))
}
}
}
function setCurrentPosition (position) {
var oldPos = deepCopy(currentPosition)
var newPos = deepCopy(position)
var oldFen = objToFen(oldPos)
var newFen = objToFen(newPos)
// do nothing if no change in position
if (oldFen === newFen) return
// run their onChange function
if (isFunction(config.onChange)) {
config.onChange(oldPos, newPos)
}
// update state
currentPosition = position
}
function isXYOnSquare (x, y) {
for (var i in squareElsOffsets) {
if (!squareElsOffsets.hasOwnProperty(i)) continue
var s = squareElsOffsets[i]
if (x >= s.left &&
x < s.left + squareSize &&
y >= s.top &&
y < s.top + squareSize) {
return i
}
}
return 'offboard'
}
// records the XY coords of every square into memory
function captureSquareOffsets () {
squareElsOffsets = {}
for (var i in squareElsIds) {
if (!squareElsIds.hasOwnProperty(i)) continue
squareElsOffsets[i] = $('#' + squareElsIds[i]).offset()
}
}
function removeSquareHighlights () {
$board
.find('.' + CSS.square)
.removeClass(CSS.highlight1 + ' ' + CSS.highlight2)
}
function snapbackDraggedPiece () {
// there is no "snapback" for spare pieces
if (draggedPieceSource === 'spare') {
trashDraggedPiece()
return
}
removeSquareHighlights()
// animation complete
function complete () {
drawPositionInstant()
$draggedPiece.css('display', 'none')
// run their onSnapbackEnd function
if (isFunction(config.onSnapbackEnd)) {
config.onSnapbackEnd(
draggedPiece,
draggedPieceSource,
deepCopy(currentPosition),
currentOrientation
)
}
}
// get source square position
var sourceSquarePosition = $('#' + squareElsIds[draggedPieceSource]).offset()
// animate the piece to the target square
var opts = {
duration: config.snapbackSpeed,
complete: complete
}
$draggedPiece.animate(sourceSquarePosition, opts)
// set state
isDragging = false
}
function trashDraggedPiece () {
removeSquareHighlights()
// remove the source piece
var newPosition = deepCopy(currentPosition)
delete newPosition[draggedPieceSource]
setCurrentPosition(newPosition)
// redraw the position
drawPositionInstant()
// hide the dragged piece
$draggedPiece.fadeOut(config.trashSpeed)
// set state
isDragging = false
}
function dropDraggedPieceOnSquare (square) {
removeSquareHighlights()
// update position
var newPosition = deepCopy(currentPosition)
delete newPosition[draggedPieceSource]
newPosition[square] = draggedPiece
setCurrentPosition(newPosition)
// get target square information
var targetSquarePosition = $('#' + squareElsIds[square]).offset()
// animation complete
function onAnimationComplete () {
drawPositionInstant()
$draggedPiece.css('display', 'none')
// execute their onSnapEnd function
if (isFunction(config.onSnapEnd)) {
config.onSnapEnd(draggedPieceSource, square, draggedPiece)
}
}
// snap the piece to the target square
var opts = {
duration: config.snapSpeed,
complete: onAnimationComplete
}
$draggedPiece.animate(targetSquarePosition, opts)
// set state
isDragging = false
}
function beginDraggingPiece (source, piece, x, y) {
// run their custom onDragStart function
// their custom onDragStart function can cancel drag start
if (isFunction(config.onDragStart) &&
config.onDragStart(source, piece, deepCopy(currentPosition), currentOrientation) === false) {
return
}
// set state
isDragging = true
draggedPiece = piece
draggedPieceSource = source
// if the piece came from spare pieces, location is offboard
if (source === 'spare') {
draggedPieceLocation = 'offboard'
} else {
draggedPieceLocation = source
}
// capture the x, y coords of all squares in memory
captureSquareOffsets()
// create the dragged piece
$draggedPiece.attr('src', buildPieceImgSrc(piece)).css({
display: '',
position: 'absolute',
left: x - squareSize / 2,
top: y - squareSize / 2
})
if (source !== 'spare') {
// highlight the source square and hide the piece
$('#' + squareElsIds[source])
.addClass(CSS.highlight1)
.find('.' + CSS.piece)
.css('display', 'none')
}
}
function updateDraggedPiece (x, y) {
// put the dragged piece over the mouse cursor
$draggedPiece.css({
left: x - squareSize / 2,
top: y - squareSize / 2
})
// get location
var location = isXYOnSquare(x, y)
// do nothing if the location has not changed
if (location === draggedPieceLocation) return
// remove highlight from previous square
if (validSquare(draggedPieceLocation)) {
$('#' + squareElsIds[draggedPieceLocation]).removeClass(CSS.highlight2)
}
// add highlight to new square
if (validSquare(location)) {
$('#' + squareElsIds[location]).addClass(CSS.highlight2)
}
// run onDragMove
if (isFunction(config.onDragMove)) {
config.onDragMove(
location,
draggedPieceLocation,
draggedPieceSource,
draggedPiece,
deepCopy(currentPosition),
currentOrientation
)
}
// update state
draggedPieceLocation = location
}
function stopDraggedPiece (location) {
// determine what the action should be
var action = 'drop'
if (location === 'offboard' && config.dropOffBoard === 'snapback') {
action = 'snapback'
}
if (location === 'offboard' && config.dropOffBoard === 'trash') {
action = 'trash'
}
// run their onDrop function, which can potentially change the drop action
if (isFunction(config.onDrop)) {
var newPosition = deepCopy(currentPosition)
// source piece is a spare piece and position is off the board
// if (draggedPieceSource === 'spare' && location === 'offboard') {...}
// position has not changed; do nothing
// source piece is a spare piece and position is on the board
if (draggedPieceSource === 'spare' && validSquare(location)) {
// add the piece to the board
newPosition[location] = draggedPiece
}
// source piece was on the board and position is off the board
if (validSquare(draggedPieceSource) && location === 'offboard') {
// remove the piece from the board
delete newPosition[draggedPieceSource]
}
// source piece was on the board and position is on the board
if (validSquare(draggedPieceSource) && validSquare(location)) {
// move the piece
delete newPosition[draggedPieceSource]
newPosition[location] = draggedPiece
}
var oldPosition = deepCopy(currentPosition)
var result = config.onDrop(
draggedPieceSource,
location,
draggedPiece,
newPosition,
oldPosition,
currentOrientation
)
if (result === 'snapback' || result === 'trash') {
action = result
}
}
// do it!
if (action === 'snapback') {
snapbackDraggedPiece()
} else if (action === 'trash') {
trashDraggedPiece()
} else if (action === 'drop') {
dropDraggedPieceOnSquare(location)
}
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
// clear the board
widget.clear = function (useAnimation) {
widget.position({}, useAnimation)
}
// remove the widget from the page
widget.destroy = function () {
// remove markup
$container.html('')
$draggedPiece.remove()
// remove event handlers
$container.unbind()
}
// shorthand method to get the current FEN
widget.fen = function () {
return widget.position('fen')
}
// flip orientation
widget.flip = function () {
return widget.orientation('flip')
}
// move pieces
// TODO: this method should be variadic as well as accept an array of moves
widget.move = function () {
// no need to throw an error here; just do nothing
// TODO: this should return the current position
if (arguments.length === 0) return
var useAnimation = true
// collect the moves into an object
var moves = {}
for (var i = 0; i < arguments.length; i++) {
// any "false" to this function means no animations
if (arguments[i] === false) {
useAnimation = false
continue
}
// skip invalid arguments
if (!validMove(arguments[i])) {
error(2826, 'Invalid move passed to the move method.', arguments[i])
continue
}
var tmp = arguments[i].split('-')
moves[tmp[0]] = tmp[1]
}
// calculate position from moves
var newPos = calculatePositionFromMoves(currentPosition, moves)
// update the board
widget.position(newPos, useAnimation)
// return the new position object
return newPos
}
widget.orientation = function (arg) {
// no arguments, return the current orientation
if (arguments.length === 0) {
return currentOrientation
}
// set to white or black
if (arg === 'white' || arg === 'black') {
currentOrientation = arg
drawBoard()
return currentOrientation
}
// flip orientation
if (arg === 'flip') {
currentOrientation = currentOrientation === 'white' ? 'black' : 'white'
drawBoard()
return currentOrientation
}
error(5482, 'Invalid value passed to the orientation method.', arg)
}
widget.position = function (position, useAnimation) {
// no arguments, return the current position
if (arguments.length === 0) {
return deepCopy(currentPosition)
}
// get position as FEN
if (isString(position) && position.toLowerCase() === 'fen') {
return objToFen(currentPosition)
}
// start position
if (isString(position) && position.toLowerCase() === 'start') {
position = deepCopy(START_POSITION)
}
// convert FEN to position object
if (validFen(position)) {
position = fenToObj(position)
}
// validate position object
if (!validPositionObject(position)) {
error(6482, 'Invalid value passed to the position method.', position)
return
}
// default for useAnimations is true
if (useAnimation !== false) useAnimation = true
if (useAnimation) {
// start the animations
var animations = calculateAnimations(currentPosition, position)
doAnimations(animations, currentPosition, position)
// set the new position
setCurrentPosition(position)
} else {
// instant update
setCurrentPosition(position)
drawPositionInstant()
}
}
widget.resize = function () {
// calulate the new square size
squareSize = calculateSquareSize()
// set board width
$board.css('width', squareSize * 8 + 'px')
// set drag piece size
$draggedPiece.css({
height: squareSize,
width: squareSize
})
// spare pieces
if (config.sparePieces) {
$container
.find('.' + CSS.sparePieces)
.css('paddingLeft', squareSize + boardBorderSize + 'px')
}
// redraw the board
drawBoard()
}
// set the starting position
widget.start = function (useAnimation) {
widget.position('start', useAnimation)
}
// -------------------------------------------------------------------------
// Browser Events
// -------------------------------------------------------------------------
function stopDefault (evt) {
evt.preventDefault()
}
function mousedownSquare (evt) {
// do nothing if we're not draggable
if (!config.draggable) return
// do nothing if there is no piece on this square
var square = $(this).attr('data-square')
if (!validSquare(square)) return
if (!currentPosition.hasOwnProperty(square)) return
beginDraggingPiece(square, currentPosition[square], evt.pageX, evt.pageY)
}
function touchstartSquare (e) {
// do nothing if we're not draggable
if (!config.draggable) return
// do nothing if there is no piece on this square
var square = $(this).attr('data-square')
if (!validSquare(square)) return
if (!currentPosition.hasOwnProperty(square)) return
e = e.originalEvent
beginDraggingPiece(
square,
currentPosition[square],
e.changedTouches[0].pageX,
e.changedTouches[0].pageY
)
}
function mousedownSparePiece (evt) {
// do nothing if sparePieces is not enabled
if (!config.sparePieces) return
var piece = $(this).attr('data-piece')
beginDraggingPiece('spare', piece, evt.pageX, evt.pageY)
}
function touchstartSparePiece (e) {
// do nothing if sparePieces is not enabled
if (!config.sparePieces) return
var piece = $(this).attr('data-piece')
e = e.originalEvent
beginDraggingPiece(
'spare',
piece,
e.changedTouches[0].pageX,
e.changedTouches[0].pageY
)
}
function mousemoveWindow (evt) {
if (isDragging) {
updateDraggedPiece(evt.pageX, evt.pageY)
}
}
var throttledMousemoveWindow = throttle(mousemoveWindow, config.dragThrottleRate)
function touchmoveWindow (evt) {
// do nothing if we are not dragging a piece
if (!isDragging) return
// prevent screen from scrolling
evt.preventDefault()
updateDraggedPiece(evt.originalEvent.changedTouches[0].pageX,
evt.originalEvent.changedTouches[0].pageY)
}
var throttledTouchmoveWindow = throttle(touchmoveWindow, config.dragThrottleRate)
function mouseupWindow (evt) {
// do nothing if we are not dragging a piece
if (!isDragging) return
// get the location
var location = isXYOnSquare(evt.pageX, evt.pageY)
stopDraggedPiece(location)
}
function touchendWindow (evt) {
// do nothing if we are not dragging a piece
if (!isDragging) return
// get the location
var location = isXYOnSquare(evt.originalEvent.changedTouches[0].pageX,
evt.originalEvent.changedTouches[0].pageY)
stopDraggedPiece(location)
}
function mouseenterSquare (evt) {
// do not fire this event if we are dragging a piece
// NOTE: this should never happen, but it's a safeguard
if (isDragging) return
// exit if they did not provide a onMouseoverSquare function
if (!isFunction(config.onMouseoverSquare)) return
// get the square
var square = $(evt.currentTarget).attr('data-square')
// NOTE: this should never happen; defensive
if (!validSquare(square)) return
// get the piece on this square
var piece = false
if (currentPosition.hasOwnProperty(square)) {
piece = currentPosition[square]
}
// execute their function
config.onMouseoverSquare(square, piece, deepCopy(currentPosition), currentOrientation)
}
function mouseleaveSquare (evt) {
// do not fire this event if we are dragging a piece
// NOTE: this should never happen, but it's a safeguard
if (isDragging) return
// exit if they did not provide an onMouseoutSquare function
if (!isFunction(config.onMouseoutSquare)) return
// get the square
var square = $(evt.currentTarget).attr('data-square')
// NOTE: this should never happen; defensive
if (!validSquare(square)) return
// get the piece on this square
var piece = false
if (currentPosition.hasOwnProperty(square)) {
piece = currentPosition[square]
}
// execute their function
config.onMouseoutSquare(square, piece, deepCopy(currentPosition), currentOrientation)
}
// -------------------------------------------------------------------------
// Initialization
// -------------------------------------------------------------------------
function addEvents () {
// prevent "image drag"
$('body').on('mousedown mousemove', '.' + CSS.piece, stopDefault)
// mouse drag pieces
$board.on('mousedown', '.' + CSS.square, mousedownSquare)
$container.on('mousedown', '.' + CSS.sparePieces + ' .' + CSS.piece, mousedownSparePiece)
// mouse enter / leave square
$board
.on('mouseenter', '.' + CSS.square, mouseenterSquare)
.on('mouseleave', '.' + CSS.square, mouseleaveSquare)
// piece drag
var $window = $(window)
$window
.on('mousemove', throttledMousemoveWindow)
.on('mouseup', mouseupWindow)
// touch drag pieces
if (isTouchDevice()) {
$board.on('touchstart', '.' + CSS.square, touchstartSquare)
$container.on('touchstart', '.' + CSS.sparePieces + ' .' + CSS.piece, touchstartSparePiece)
$window
.on('touchmove', throttledTouchmoveWindow)
.on('touchend', touchendWindow)
}
}
function initDOM () {
// create unique IDs for all the elements we will create
createElIds()
// build board and save it in memory
$container.html(buildContainerHTML(config.sparePieces))
$board = $container.find('.' + CSS.board)
if (config.sparePieces) {
$sparePiecesTop = $container.find('.' + CSS.sparePiecesTop)
$sparePiecesBottom = $container.find('.' + CSS.sparePiecesBottom)
}
// create the drag piece
var draggedPieceId = uuid()
$('body').append(buildPieceHTML('wP', true, draggedPieceId))
$draggedPiece = $('#' + draggedPieceId)
// TODO: need to remove this dragged piece element if the board is no
// longer in the DOM
// get the border size
boardBorderSize = parseInt($board.css('borderLeftWidth'), 10)
// set the size and draw the board
widget.resize()
}
// -------------------------------------------------------------------------
// Initialization
// -------------------------------------------------------------------------
setInitialState()
initDOM()
addEvents()
// return the widget object
return widget
} // end constructor
// TODO: do module exports here
window['Chessboard'] = constructor
// support legacy ChessBoard name
window['ChessBoard'] = window['Chessboard']
// expose util functions
window['Chessboard']['fenToObj'] = fenToObj
window['Chessboard']['objToFen'] = objToFen
})() // end anonymous wrapper