// 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 = '
' if (hasSparePieces) { html += '
' } html += '
' if (hasSparePieces) { html += '
' } html += '
' 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 += '
' for (var j = 0; j < 8; j++) { var square = alpha[j] + row html += '
' if (config.showNotation) { // alpha notation if ((orientation === 'white' && row === 1) || (orientation === 'black' && row === 8)) { html += '
' + alpha[j] + '
' } // numeric notation if (j === 0) { html += '
' + row + '
' } } html += '
' // end .square squareColor = (squareColor === 'white') ? 'black' : 'white' } html += '
' 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 = '' 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