diff --git a/lib/chessboard.css b/lib/chessboard.css index 27ffdccb..3696561b 100644 --- a/lib/chessboard.css +++ b/lib/chessboard.css @@ -1,15 +1,15 @@ -/*! chessboard.js v@VERSION | (c) 2019 Chris Oakman | MIT License chessboardjs.com/license */ +/*! chessboard.js v@VERSION | (c) 2019 Chris Oakman | MIT License chessboardjs.com/license */ -.clearfix-7da63 { +.clearfix-7da63 { clear: both; } -.board-b72b1 { - border: 2px solid #404040; - box-sizing: content-box; +.board-b72b1 { + border: 2px solid #404040; + box-sizing: content-box; } -.square-55d63 { +.square-55d63 { float: left; position: relative; @@ -22,33 +22,50 @@ user-select: none; } -.white-1e1d7 { +.white-1e1d7 { background-color: #f0d9b5; color: #b58863; } -.black-3c85d { +.black-3c85d { background-color: #b58863; color: #f0d9b5; } - -.highlight1-32417, .highlight2-9c5d2 { + +.highlight1-32417, .highlight2-9c5d2 { box-shadow: inset 0 0 3px 3px yellow; } - + .notation-322f9 { cursor: default; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; position: absolute; -} - +} + .alpha-d2270 { bottom: 1px; right: 3px; -} - +} + .numeric-fc462 { top: 2px; left: 2px; -} +} + +.piece-wrapper { + position: relative; + display: inline-block; +} + +.spare-count { + border: 1px solid #ccc; + padding: 0 4px; + border-radius: 37%; + font-size: 12px; + position: absolute; + z-index: 1; + background: #fff; + right: 0; + bottom: 1px; +} diff --git a/lib/chessboard.js b/lib/chessboard.js index ed27db18..f066aeff 100644 --- a/lib/chessboard.js +++ b/lib/chessboard.js @@ -4,9 +4,9 @@ // Copyright (c) 2019, Chris Oakman // Released under the MIT license // https://github.com/oakmac/chessboardjs/blob/master/LICENSE.md - -// start anonymous scope -;(function () { + +// start anonymous scope +;(function () { 'use strict' var $ = window['jQuery'] @@ -179,20 +179,20 @@ rate >= 1 } - function validMove (move) { - // move should be a string + function validMove (move) { + // move should be a string if (!isString(move)) return false - - // move should be in the form of "e2-e4", "f6-d5" + + // 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')) @@ -207,7 +207,7 @@ function validPieceCode (code) { return isString(code) && code.search(/^[bw][KQRNBP]$/) !== -1 - } + } if (RUN_ASSERTS) { console.assert(validPieceCode('bP')) @@ -224,28 +224,28 @@ 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(/ .+$/, '') + + // 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('/') + // 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 - } + return false + } + } + + return true + } if (RUN_ASSERTS) { console.assert(validFen(START_FEN)) @@ -263,17 +263,17 @@ 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 - } + + 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)) @@ -326,76 +326,76 @@ return pieceCodeLetters[1].toLowerCase() } - // convert FEN string to position object - // returns false if the FEN string is invalid - function fenToObj (fen) { + // 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('') + + // 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) { + + // 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 + // piece var square = COLUMNS[colIdx] + currentRow - position[square] = fenToPieceCode(row[j]) + position[square] = fenToPieceCode(row[j]) colIdx = colIdx + 1 - } - } + } + } currentRow = currentRow - 1 - } - - return position + } + + return position } - // position object to FEN string - // returns false if the obj is not a valid position object - function objToFen (obj) { + // 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 + + 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 + } else { + // empty space fen = fen + '1' - } - } - + } + } + if (i !== 7) { fen = fen + '/' } - + currentRow = currentRow - 1 - } - + } + // squeeze the empty numbers together fen = squeezeFenEmptySquares(fen) - return fen - } + return fen + } if (RUN_ASSERTS) { console.assert(objToFen(START_POSITION) === START_FEN) @@ -520,13 +520,13 @@ var html = '
' if (hasSparePieces) { - html += '
' + html += '
' } html += '
' if (hasSparePieces) { - html += '
' + html += '
' } html += '
' @@ -576,7 +576,7 @@ // default piece theme is wikipedia if (!config.hasOwnProperty('pieceTheme') || (!isString(config.pieceTheme) && !isFunction(config.pieceTheme))) { - config.pieceTheme = 'img/chesspieces/wikipedia/{piece}.png' + config.pieceTheme = '../images/{piece}.png' } // animation speeds @@ -663,8 +663,8 @@ var $sparePiecesBottom = null // constructor return object - var widget = {} - + var widget = {} + // ------------------------------------------------------------------------- // Stateful // ------------------------------------------------------------------------- @@ -677,6 +677,7 @@ var draggedPieceSource = null var isDragging = false var sparePiecesElsIds = {} + var sparePiecesCountElsIds = {} var squareElsIds = {} var squareElsOffsets = {} var squareSize = 16 @@ -684,44 +685,64 @@ // ------------------------------------------------------------------------- // Validation / Errors // ------------------------------------------------------------------------- - - function error (code, msg, obj) { - // do nothing if showErrors is not set - if ( + + function validSpareCounts(counts) { + if (typeof counts !== 'object' || Object.getOwnPropertyNames(counts).length === 0) { + return false + } + + for (var i in counts) { + if (isNaN(counts[i])) { + return false + } + } + ['w', 'b'].forEach(function(color) { + ['K', 'Q', 'R', 'B', 'N', 'P'].forEach(function(kind) { + if (!counts.hasOwnProperty(color + kind)) { + counts[color + kind] = 0; + } + }); + }); + return true + } + + function error (code, msg, obj) { + // do nothing if showErrors is not set + if ( config.hasOwnProperty('showErrors') !== true || config.showErrors === false - ) { - return - } - + ) { + return + } + var errorText = 'Chessboard Error ' + code + ': ' + msg - - // print to console - if ( + + // 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 + 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 - } - + 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 () { @@ -743,55 +764,81 @@ ) } } + + //spare piece counts + if (config.hasOwnProperty('spareCounts') === true) { + if (config.spareCounts === 'default') { + config.spareCounts = { + 'wK': 0, + 'wQ': 0, + 'wR': 0, + 'wB': 0, + 'wN': 0, + 'wP': 0, + 'bK': 0, + 'bQ': 0, + 'bR': 0, + 'bB': 0, + 'bN': 0, + 'bP': 0 + } + } else if (!validSpareCounts(config.spareCounts)) { + config.spareCounts = false; + } + } else { + config.spareCounts = false; + } } // ------------------------------------------------------------------------- // 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 () { + function calculateSquareSize () { var containerWidth = parseInt($container.width(), 10) - + // defensive, prevent infinite loop if (!containerWidth || containerWidth <= 0) { - return 0 - } - + return 0 + } + // pad one pixel - var boardWidth = containerWidth - 1 - + var boardWidth = containerWidth - 1 + while (boardWidth % 8 !== 0 && boardWidth > 0) { boardWidth = boardWidth - 1 - } - - return boardWidth / 8 - } - + } + + 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 + 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] + var whitePiece = 'w' + pieces[i] + var blackPiece = 'b' + pieces[i] sparePiecesElsIds[whitePiece] = whitePiece + '-' + uuid() sparePiecesElsIds[blackPiece] = blackPiece + '-' + uuid() - } - } - + sparePiecesCountElsIds[whitePiece] = 'count' + whitePiece + ' - ' + uuid() + sparePiecesCountElsIds[blackPiece] = 'count' + blackPiece + ' - ' + uuid() + } + } + // ------------------------------------------------------------------------- // Markup Building // ------------------------------------------------------------------------- @@ -853,158 +900,173 @@ return interpolateTemplate(html, CSS) } - - function buildPieceImgSrc (piece) { + + 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 = '' + config.spareCounts[piece] + '' + } + + function buildPieceHTML (piece, hidden, id, is_spare) { + var html = '
' - - return interpolateTemplate(html, CSS) - } - + if (is_spare) { + html += '" />' + spareCountHtml(piece, hidden) + } else { + 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 - } - + 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]], true) + } + + return html + } + // ------------------------------------------------------------------------- // Animations // ------------------------------------------------------------------------- - - function animateSquareToSquare (src, dest, piece, completeFn) { + + 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() + var animatedPieceId = uuid() $('body').append(buildPieceHTML(piece, true, animatedPieceId)) var $animatedPiece = $('#' + animatedPieceId) $animatedPiece.css({ - display: '', - position: 'absolute', - top: srcSquarePosition.top, - left: srcSquarePosition.left - }) - + 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() - } - } - + completeFn() + } + } + // animate the piece to the destination square - var opts = { + var opts = { duration: config.moveSpeed, complete: onFinishAnimation1 - } + } $animatedPiece.animate(destSquarePosition, opts) - } - - function animateSparePieceToSquare (piece, dest, completeFn) { + } + + 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() + var pieceId = uuid() $('body').append(buildPieceHTML(piece, true, pieceId)) var $animatedPiece = $('#' + pieceId) $animatedPiece.css({ - display: '', - position: 'absolute', - left: srcOffset.left, - top: srcOffset.top - }) - + 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() - } - } - + completeFn() + } + } + // animate the piece to the destination square - var opts = { + 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 + + 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] @@ -1027,178 +1089,178 @@ // 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) { + function calculateAnimations (pos1, pos2) { // make copies of both - pos1 = deepCopy(pos1) - pos2 = deepCopy(pos2) - - var animations = [] - var squaresMovedTo = {} - + 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] - } - } - + 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 (!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 - } + 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] - } + + 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 - } - + + animations.push({ + type: 'clear', + square: i, + piece: pos1[i] + }) + + delete pos1[i] + } + + return animations + } + // ------------------------------------------------------------------------- // Control Flow // ------------------------------------------------------------------------- - function drawPositionInstant () { + 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 () { + } + } + + function drawBoard () { $board.html(buildBoardHTML(currentOrientation, squareSize, config.showNotation)) - drawPositionInstant() - + drawPositionInstant() + if (config.sparePieces) { if (currentOrientation === 'white') { $sparePiecesTop.html(buildSparePiecesHTML('black')) $sparePiecesBottom.html(buildSparePiecesHTML('white')) - } else { + } else { $sparePiecesTop.html(buildSparePiecesHTML('white')) $sparePiecesBottom.html(buildSparePiecesHTML('black')) - } - } - } + } + } + } - function setCurrentPosition (position) { + function setCurrentPosition (position) { var oldPos = deepCopy(currentPosition) - var newPos = deepCopy(position) - var oldFen = objToFen(oldPos) - var newFen = objToFen(newPos) - + var newPos = deepCopy(position) + var oldFen = objToFen(oldPos) + var newFen = objToFen(newPos) + // do nothing if no change in position - if (oldFen === newFen) return - + if (oldFen === newFen) return + // run their onChange function if (isFunction(config.onChange)) { config.onChange(oldPos, newPos) - } - + } + // update state currentPosition = position - } - - function isXYOnSquare (x, y) { + } + + 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 && y < s.top + squareSize) { - return i - } - } - - return 'offboard' - } - + return i + } + } + + return 'offboard' + } + // records the XY coords of every square into memory - function captureSquareOffsets () { + function captureSquareOffsets () { squareElsOffsets = {} - + for (var i in squareElsIds) { if (!squareElsIds.hasOwnProperty(i)) continue - + squareElsOffsets[i] = $('#' + squareElsIds[i]).offset() - } - } - - function removeSquareHighlights () { + } + } + + function removeSquareHighlights () { $board .find('.' + CSS.square) .removeClass(CSS.highlight1 + ' ' + CSS.highlight2) - } - - function snapbackDraggedPiece () { + } + + function snapbackDraggedPiece () { // there is no "snapback" for spare pieces if (draggedPieceSource === 'spare') { - trashDraggedPiece() - return - } - - removeSquareHighlights() - + trashDraggedPiece() + return + } + + removeSquareHighlights() + // animation complete - function complete () { - drawPositionInstant() + function complete () { + drawPositionInstant() $draggedPiece.css('display', 'none') - + // run their onSnapbackEnd function if (isFunction(config.onSnapbackEnd)) { config.onSnapbackEnd( @@ -1206,313 +1268,318 @@ draggedPieceSource, deepCopy(currentPosition), currentOrientation - ) - } - } - + ) + } + } + // get source square position var sourceSquarePosition = $('#' + squareElsIds[draggedPieceSource]).offset() - + // animate the piece to the target square - var opts = { + var opts = { duration: config.snapbackSpeed, - complete: complete - } + complete: complete + } $draggedPiece.animate(sourceSquarePosition, opts) - + // set state isDragging = false - } - - function trashDraggedPiece () { - removeSquareHighlights() - + } + + function trashDraggedPiece () { + removeSquareHighlights() + // remove the source piece var newPosition = deepCopy(currentPosition) delete newPosition[draggedPieceSource] - setCurrentPosition(newPosition) - + setCurrentPosition(newPosition) + // redraw the position - drawPositionInstant() - + drawPositionInstant() + // hide the dragged piece $draggedPiece.fadeOut(config.trashSpeed) - + // set state isDragging = false - } - - function dropDraggedPieceOnSquare (square) { - removeSquareHighlights() - + } + + function dropDraggedPieceOnSquare (square) { + removeSquareHighlights() + // update position var newPosition = deepCopy(currentPosition) delete newPosition[draggedPieceSource] newPosition[square] = draggedPiece - setCurrentPosition(newPosition) - + setCurrentPosition(newPosition) + // get target square information var targetSquarePosition = $('#' + squareElsIds[square]).offset() - + // animation complete function onAnimationComplete () { - drawPositionInstant() + 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 = { + var opts = { duration: config.snapSpeed, complete: onAnimationComplete - } + } $draggedPiece.animate(targetSquarePosition, opts) - + // set state isDragging = false - } - - function beginDraggingPiece (source, piece, x, y) { + } + + 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 - } - + config.onDragStart(source, piece, deepCopy(currentPosition), currentOrientation) === false || + (source === 'spare' && (typeof config.spareCounts === 'object' && config.spareCounts[piece] < 1))) { + return + } + // set state isDragging = true draggedPiece = piece draggedPieceSource = source - + // if the piece came from spare pieces, location is offboard - if (source === 'spare') { + if (source === 'spare') { draggedPieceLocation = 'offboard' - } else { + } else { draggedPieceLocation = source - } - + } + // capture the x, y coords of all squares in memory - captureSquareOffsets() - + captureSquareOffsets() + // create the dragged piece $draggedPiece.attr('src', buildPieceImgSrc(piece)).css({ - display: '', - position: 'absolute', + display: '', + position: 'fixed', left: x - squareSize / 2, top: y - squareSize / 2 - }) - - if (source !== 'spare') { + }) + + 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) { + } + } + + 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) - + 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, + location, draggedPieceLocation, draggedPieceSource, draggedPiece, deepCopy(currentPosition), currentOrientation - ) - } - + ) + } + // update state draggedPieceLocation = location - } - - function stopDraggedPiece (location) { + } + + function stopDraggedPiece (location) { // determine what the action should be - var action = 'drop' + var action = 'drop' if (location === 'offboard' && config.dropOffBoard === 'snapback') { - action = 'snapback' - } + action = 'snapback' + } if (location === 'offboard' && config.dropOffBoard === 'trash') { - action = '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, + location, draggedPiece, - newPosition, - oldPosition, + newPosition, + oldPosition, currentOrientation - ) - if (result === 'snapback' || result === 'trash') { - action = result - } - } - + ) + if (result === 'snapback' || result === 'trash') { + action = result + } + } + // do it! - if (action === 'snapback') { - snapbackDraggedPiece() - } else if (action === 'trash') { - trashDraggedPiece() - } else if (action === 'drop') { - dropDraggedPieceOnSquare(location) - } - } - + if (action === 'snapback') { + snapbackDraggedPiece() + } else if (action === 'trash') { + trashDraggedPiece() + } else if (action === 'drop') { + dropDraggedPieceOnSquare(location) + } + if (config.spareCounts && draggedPieceSource === 'spare' && action === 'drop') { + config.spareCounts[draggedPiece] -= 1; + var elem = document.getElementById(sparePiecesCountElsIds[draggedPiece]).innerText = config.spareCounts[draggedPiece]; + } + } + // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- - + // clear the board widget.clear = function (useAnimation) { - widget.position({}, 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') - } - + widget.fen = function () { + return widget.position('fen') + } + // flip orientation widget.flip = function () { - return widget.orientation('flip') + return widget.orientation('flip') } // move pieces // TODO: this method should be variadic as well as accept an array of moves - widget.move = function () { + 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 - + if (arguments.length === 0) return + + var useAnimation = true + // collect the moves into an object - var moves = {} - for (var i = 0; i < arguments.length; i++) { + 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 - } - + 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] - } - + 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) - + widget.position(newPos, useAnimation) + // return the new position object - return newPos - } - - widget.orientation = function (arg) { + return newPos + } + + widget.orientation = function (arg) { // no arguments, return the current orientation - if (arguments.length === 0) { + if (arguments.length === 0) { return currentOrientation - } - + } + // set to white or black - if (arg === 'white' || arg === 'black') { + if (arg === 'white' || arg === 'black') { currentOrientation = arg - drawBoard() + drawBoard() return currentOrientation - } - + } + // flip orientation - if (arg === 'flip') { + if (arg === 'flip') { currentOrientation = currentOrientation === 'white' ? 'black' : 'white' - drawBoard() + drawBoard() return currentOrientation - } - - error(5482, 'Invalid value passed to the orientation method.', arg) + } + + 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) { + if (arguments.length === 0) { return deepCopy(currentPosition) - } - + } + // get position as FEN if (isString(position) && position.toLowerCase() === 'fen') { return objToFen(currentPosition) @@ -1520,19 +1587,19 @@ // start position if (isString(position) && position.toLowerCase() === 'start') { - position = deepCopy(START_POSITION) - } - + position = deepCopy(START_POSITION) + } + // convert FEN to position object if (validFen(position)) { - position = fenToObj(position) - } - + position = fenToObj(position) + } + // validate position object if (!validPositionObject(position)) { - error(6482, 'Invalid value passed to the position method.', position) - return - } + error(6482, 'Invalid value passed to the position method.', position) + return + } // default for useAnimations is true if (useAnimation !== false) useAnimation = true @@ -1541,43 +1608,41 @@ // start the animations var animations = calculateAnimations(currentPosition, position) doAnimations(animations, currentPosition, position) - + // set the new position - setCurrentPosition(position) - } else { + setCurrentPosition(position) + } else { // instant update - setCurrentPosition(position) - drawPositionInstant() - } + 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') - } - + $container.css('paddingLeft', ($board.clientWidth - $container.offsetWidth) / 2 + 'px'); + } + // redraw the board - drawBoard() + drawBoard() } // set the starting position widget.start = function (useAnimation) { - widget.position('start', useAnimation) + widget.position('start', useAnimation) } // ------------------------------------------------------------------------- @@ -1586,8 +1651,8 @@ function stopDefault (evt) { evt.preventDefault() - } - + } + function mousedownSquare (evt) { // do nothing if we're not draggable if (!config.draggable) return @@ -1600,48 +1665,48 @@ beginDraggingPiece(square, currentPosition[square], evt.pageX, evt.pageY) } - function touchstartSquare (e) { + 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, + + e = e.originalEvent + beginDraggingPiece( + square, currentPosition[square], - e.changedTouches[0].pageX, - e.changedTouches[0].pageY - ) - } - + 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') - + + var piece = $(this).attr('data-piece') + beginDraggingPiece('spare', piece, evt.pageX, evt.pageY) - } - - function touchstartSparePiece (e) { + } + + 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 - ) - } - + + 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) @@ -1653,37 +1718,37 @@ 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) - } - + + 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) - } - + + 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 @@ -1691,23 +1756,23 @@ // 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 + 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 @@ -1715,35 +1780,35 @@ // 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 + var piece = false if (currentPosition.hasOwnProperty(square)) { piece = currentPosition[square] - } - + } + // execute their function config.onMouseoutSquare(square, piece, deepCopy(currentPosition), currentOrientation) - } - + } + // ------------------------------------------------------------------------- // Initialization // ------------------------------------------------------------------------- - function addEvents () { + 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) @@ -1754,7 +1819,7 @@ $window .on('mousemove', throttledMousemoveWindow) .on('mouseup', mouseupWindow) - + // touch drag pieces if (isTouchDevice()) { $board.on('touchstart', '.' + CSS.square, touchstartSquare) @@ -1762,24 +1827,24 @@ $window .on('touchmove', throttledTouchmoveWindow) .on('touchend', touchendWindow) - } - } - + } + } + function initDOM () { // create unique IDs for all the elements we will create - createElIds() - + 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() + var draggedPieceId = uuid() $('body').append(buildPieceHTML('wP', true, draggedPieceId)) $draggedPiece = $('#' + draggedPieceId) @@ -1788,10 +1853,10 @@ // get the border size boardBorderSize = parseInt($board.css('borderLeftWidth'), 10) - + // set the size and draw the board - widget.resize() - } + widget.resize() + } // ------------------------------------------------------------------------- // Initialization @@ -1811,10 +1876,10 @@ // support legacy ChessBoard name window['ChessBoard'] = window['Chessboard'] - // expose util functions + // expose util functions window['Chessboard']['fenToObj'] = fenToObj window['Chessboard']['objToFen'] = objToFen -})() // end anonymous wrapper +})() // end anonymous wrapper /* export Chessboard object if using node or any other CommonJS compatible * environment */