PageSourceSearch

https://agadmator-library.github.io/js/chessboard-1.0.0.js

js agadmator-library.github.io collected 2026-10-03 08:33:19 UTC 53,261 bytes, 1,817 lines download raw bytes

1// chessboard.js v1.0.0
2// https://github.com/oakmac/chessboardjs/
3//
4// Copyright (c) 2019, Chris Oakman
5// Released under the MIT license
6// https://github.com/oakmac/chessboardjs/blob/master/LICENSE.md
7
8// start anonymous scope
9;(function () {
10  'use strict'
11
12  var $ = window['jQuery']
13
14  // ---------------------------------------------------------------------------
15  // Constants
16  // ---------------------------------------------------------------------------
17
18  var COLUMNS = 'abcdefgh'.split('')
19  var DEFAULT_DRAG_THROTTLE_RATE = 20
20  var ELLIPSIS = '…'
21  var MINIMUM_JQUERY_VERSION = '1.8.3'
22  var RUN_ASSERTS = false
23  var START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'
24  var START_POSITION = fenToObj(START_FEN)
25
26  // default animation speeds
27  var DEFAULT_APPEAR_SPEED = 200
28  var DEFAULT_MOVE_SPEED = 200
29  var DEFAULT_SNAPBACK_SPEED = 60
30  var DEFAULT_SNAP_SPEED = 30
31  var DEFAULT_TRASH_SPEED = 100
32
33  // use unique class names to prevent clashing with anything else on the page
34  // and simplify selectors
35  // NOTE: these should never change
36  var CSS = {}
37  CSS['alpha'] = 'alpha-d2270'
38  CSS['black'] = 'black-3c85d'
39  CSS['board'] = 'board-b72b1'
40  CSS['chessboard'] = 'chessboard-63f37'
41  CSS['clearfix'] = 'clearfix-7da63'
42  CSS['highlight1'] = 'highlight1-32417'
43  CSS['highlight2'] = 'highlight2-9c5d2'
44  CSS['notation'] = 'notation-322f9'
45  CSS['numeric'] = 'numeric-fc462'
46  CSS['piece'] = 'piece-417db'
47  CSS['row'] = 'row-5277c'
48  CSS['sparePieces'] = 'spare-pieces-7492f'
49  CSS['sparePiecesBottom'] = 'spare-pieces-bottom-ae20f'
50  CSS['sparePiecesTop'] = 'spare-pieces-top-4028b'
51  CSS['square'] = 'square-55d63'
52  CSS['white'] = 'white-1e1d7'
53
54  // ---------------------------------------------------------------------------
55  // Misc Util Functions
56  // ---------------------------------------------------------------------------
57
58  function throttle (f, interval, scope) {
59    var timeout = 0
60    var shouldFire = false
61    var args = []
62
63    var handleTimeout = function () {
64      timeout = 0
65      if (shouldFire) {
66        shouldFire = false
67        fire()
68      }
69    }
70
71    var fire = function () {
72      timeout = window.setTimeout(handleTimeout, interval)
73      f.apply(scope, args)
74    }
75
76    return function (_args) {
77      args = arguments
78      if (!timeout) {
79        fire()
80      } else {
81        shouldFire = true
82      }
83    }
84  }
85
86  // function debounce (f, interval, scope) {
87  //   var timeout = 0
88  //   return function (_args) {
89  //     window.clearTimeout(timeout)
90  //     var args = arguments
91  //     timeout = window.setTimeout(function () {
92  //       f.apply(scope, args)
93  //     }, interval)
94  //   }
95  // }
96
97  function uuid () {
98    return 'xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx'.replace(/x/g, function (c) {
99      var r = (Math.random() * 16) | 0
100      return r.toString(16)
101    })
102  }
103
104  function deepCopy (thing) {
105    return JSON.parse(JSON.stringify(thing))
106  }
107
108  function parseSemVer (version) {
109    var tmp = version.split('.')
110    return {
111      major: parseInt(tmp[0], 10),
112      minor: parseInt(tmp[1], 10),
113      patch: parseInt(tmp[2], 10)
114    }
115  }
116
117  // returns true if version is >= minimum
118  function validSemanticVersion (version, minimum) {
119    version = parseSemVer(version)
120    minimum = parseSemVer(minimum)
121
122    var versionNum = (version.major * 100000 * 100000) +
123                     (version.minor * 100000) +
124                     version.patch
125    var minimumNum = (minimum.major * 100000 * 100000) +
126                     (minimum.minor * 100000) +
127                     minimum.patch
128
129    return versionNum >= minimumNum
130  }
131
132  function interpolateTemplate (str, obj) {
133    for (var key in obj) {
134      if (!obj.hasOwnProperty(key)) continue
135      var keyTemplateStr = '{' + key + '}'
136      var value = obj[key]
137      while (str.indexOf(keyTemplateStr) !== -1) {
138        str = str.replace(keyTemplateStr, value)
139      }
140    }
141    return str
142  }
143
144  if (RUN_ASSERTS) {
145    console.assert(interpolateTemplate('abc', {a: 'x'}) === 'abc')
146    console.assert(interpolateTemplate('{a}bc', {}) === '{a}bc')
147    console.assert(interpolateTemplate('{a}bc', {p: 'q'}) === '{a}bc')
148    console.assert(interpolateTemplate('{a}bc', {a: 'x'}) === 'xbc')
149    console.assert(interpolateTemplate('{a}bc{a}bc', {a: 'x'}) === 'xbcxbc')
150    console.assert(interpolateTemplate('{a}{a}{b}', {a: 'x', b: 'y'}) === 'xxy')
151  }
152
153  // ---------------------------------------------------------------------------
154  // Predicates
155  // ---------------------------------------------------------------------------
156
157  function isString (s) {
158    return typeof s === 'string'
159  }
160
161  function isFunction (f) {
162    return typeof f === 'function'
163  }
164
165  function isInteger (n) {
166    return typeof n === 'number' &&
167           isFinite(n) &&
168           Math.floor(n) === n
169  }
170
171  function validAnimationSpeed (speed) {
172    if (speed === 'fast' || speed === 'slow') return true
173    if (!isInteger(speed)) return false
174    return speed >= 0
175  }
176
177  function validThrottleRate (rate) {
178    return isInteger(rate) &&
179           rate >= 1
180  }
181
182  function validMove (move) {
183    // move should be a string
184    if (!isString(move)) return false
185
186    // move should be in the form of "e2-e4", "f6-d5"
187    var squares = move.split('-')
188    if (squares.length !== 2) return false
189
190    return validSquare(squares[0]) && validSquare(squares[1])
191  }
192
193  function validSquare (square) {
194    return isString(square) && square.search(/^[a-h][1-8]$/) !== -1
195  }
196
197  if (RUN_ASSERTS) {
198    console.assert(validSquare('a1'))
199    console.assert(validSquare('e2'))
200    console.assert(!validSquare('D2'))
201    console.assert(!validSquare('g9'))
202    console.assert(!validSquare('a'))
203    console.assert(!validSquare(true))
204    console.assert(!validSquare(null))
205    console.assert(!validSquare({}))
206  }
207
208  function validPieceCode (code) {
209    return isString(code) && code.search(/^[bw][KQRNBP]$/) !== -1
210  }
211
212  if (RUN_ASSERTS) {
213    console.assert(validPieceCode('bP'))
214    console.assert(validPieceCode('bK'))
215    console.assert(validPieceCode('wK'))
216    console.assert(validPieceCode('wR'))
217    console.assert(!validPieceCode('WR'))
218    console.assert(!validPieceCode('Wr'))
219    console.assert(!validPieceCode('a'))
220    console.assert(!validPieceCode(true))
221    console.assert(!validPieceCode(null))
222    console.assert(!validPieceCode({}))
223  }
224
225  function validFen (fen) {
226    if (!isString(fen)) return false
227
228    // cut off any move, castling, etc info from the end
229    // we're only interested in position information
230    fen = fen.replace(/ .+$/, '')
231
232    // expand the empty square numbers to just 1s
233    fen = expandFenEmptySquares(fen)
234
235    // FEN should be 8 sections separated by slashes
236    var chunks = fen.split('/')
237    if (chunks.length !== 8) return false
238
239    // check each section
240    for (var i = 0; i < 8; i++) {
241      if (chunks[i].length !== 8 ||
242          chunks[i].search(/[^kqrnbpKQRNBP1]/) !== -1) {
243        return false
244      }
245    }
246
247    return true
248  }
249
250  if (RUN_ASSERTS) {
251    console.assert(validFen(START_FEN))
252    console.assert(validFen('8/8/8/8/8/8/8/8'))
253    console.assert(validFen('r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R'))
254    console.assert(validFen('3r3r/1p4pp/2nb1k2/pP3p2/8/PB2PN2/p4PPP/R4RK1 b - - 0 1'))
255    console.assert(!validFen('3r3z/1p4pp/2nb1k2/pP3p2/8/PB2PN2/p4PPP/R4RK1 b - - 0 1'))
256    console.assert(!validFen('anbqkbnr/8/8/8/8/8/PPPPPPPP/8'))
257    console.assert(!validFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/'))
258    console.assert(!validFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBN'))
259    console.assert(!validFen('888888/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'))
260    console.assert(!validFen('888888/pppppppp/74/8/8/8/PPPPPPPP/RNBQKBNR'))
261    console.assert(!validFen({}))
262  }
263
264  function validPositionObject (pos) {
265    if (!$.isPlainObject(pos)) return false
266
267    for (var i in pos) {
268      if (!pos.hasOwnProperty(i)) continue
269
270      if (!validSquare(i) || !validPieceCode(pos[i])) {
271        return false
272      }
273    }
274
275    return true
276  }
277
278  if (RUN_ASSERTS) {
279    console.assert(validPositionObject(START_POSITION))
280    console.assert(validPositionObject({}))
281    console.assert(validPositionObject({e2: 'wP'}))
282    console.assert(validPositionObject({e2: 'wP', d2: 'wP'}))
283    console.assert(!validPositionObject({e2: 'BP'}))
284    console.assert(!validPositionObject({y2: 'wP'}))
285    console.assert(!validPositionObject(null))
286    console.assert(!validPositionObject('start'))
287    console.assert(!validPositionObject(START_FEN))
288  }
289
290  function isTouchDevice () {
291    return 'ontouchstart' in document.documentElement
292  }
293
294  function validJQueryVersion () {
295    return typeof window.$ &&
296           $.fn &&
297           $.fn.jquery &&
298           validSemanticVersion($.fn.jquery, MINIMUM_JQUERY_VERSION)
299  }
300
301  // ---------------------------------------------------------------------------
302  // Chess Util Functions
303  // ---------------------------------------------------------------------------
304
305  // convert FEN piece code to bP, wK, etc
306  function fenToPieceCode (piece) {
307    // black piece
308    if (piece.toLowerCase() === piece) {
309      return 'b' + piece.toUpperCase()
310    }
311
312    // white piece
313    return 'w' + piece.toUpperCase()
314  }
315
316  // convert bP, wK, etc code to FEN structure
317  function pieceCodeToFen (piece) {
318    var pieceCodeLetters = piece.split('')
319
320    // white piece
321    if (pieceCodeLetters[0] === 'w') {
322      return pieceCodeLetters[1].toUpperCase()
323    }
324
325    // black piece
326    return pieceCodeLetters[1].toLowerCase()
327  }
328
329  // convert FEN string to position object
330  // returns false if the FEN string is invalid
331  function fenToObj (fen) {
332    if (!validFen(fen)) return false
333
334    // cut off any move, castling, etc info from the end
335    // we're only interested in position information
336    fen = fen.replace(/ .+$/, '')
337
338    var rows = fen.split('/')
339    var position = {}
340
341    var currentRow = 8
342    for (var i = 0; i < 8; i++) {
343      var row = rows[i].split('')
344      var colIdx = 0
345
346      // loop through each character in the FEN section
347      for (var j = 0; j < row.length; j++) {
348        // number / empty squares
349        if (row[j].search(/[1-8]/) !== -1) {
350          var numEmptySquares = parseInt(row[j], 10)
351          colIdx = colIdx + numEmptySquares
352        } else {
353          // piece
354          var square = COLUMNS[colIdx] + currentRow
355          position[square] = fenToPieceCode(row[j])
356          colIdx = colIdx + 1
357        }
358      }
359
360      currentRow = currentRow - 1
361    }
362
363    return position
364  }
365
366  // position object to FEN string
367  // returns false if the obj is not a valid position object
368  function objToFen (obj) {
369    if (!validPositionObject(obj)) return false
370
371    var fen = ''
372
373    var currentRow = 8
374    for (var i = 0; i < 8; i++) {
375      for (var j = 0; j < 8; j++) {
376        var square = COLUMNS[j] + currentRow
377
378        // piece exists
379        if (obj.hasOwnProperty(square)) {
380          fen = fen + pieceCodeToFen(obj[square])
381        } else {
382          // empty space
383          fen = fen + '1'
384        }
385      }
386
387      if (i !== 7) {
388        fen = fen + '/'
389      }
390
391      currentRow = currentRow - 1
392    }
393
394    // squeeze the empty numbers together
395    fen = squeezeFenEmptySquares(fen)
396
397    return fen
398  }
399
400  if (RUN_ASSERTS) {
401    console.assert(objToFen(START_POSITION) === START_FEN)
402    console.assert(objToFen({}) === '8/8/8/8/8/8/8/8')
403    console.assert(objToFen({a2: 'wP', 'b2': 'bP'}) === '8/8/8/8/8/8/Pp6/8')
404  }
405
406  function squeezeFenEmptySquares (fen) {
407    return fen.replace(/11111111/g, '8')
408      .replace(/1111111/g, '7')
409      .replace(/111111/g, '6')
410      .replace(/11111/g, '5')
411      .replace(/1111/g, '4')
412      .replace(/111/g, '3')
413      .replace(/11/g, '2')
414  }
415
416  function expandFenEmptySquares (fen) {
417    return fen.replace(/8/g, '11111111')
418      .replace(/7/g, '1111111')
419      .replace(/6/g, '111111')
420      .replace(/5/g, '11111')
421      .replace(/4/g, '1111')
422      .replace(/3/g, '111')
423      .replace(/2/g, '11')
424  }
425
426  // returns the distance between two squares
427  function squareDistance (squareA, squareB) {
428    var squareAArray = squareA.split('')
429    var squareAx = COLUMNS.indexOf(squareAArray[0]) + 1
430    var squareAy = parseInt(squareAArray[1], 10)
431
432    var squareBArray = squareB.split('')
433    var squareBx = COLUMNS.indexOf(squareBArray[0]) + 1
434    var squareBy = parseInt(squareBArray[1], 10)
435
436    var xDelta = Math.abs(squareAx - squareBx)
437    var yDelta = Math.abs(squareAy - squareBy)
438
439    if (xDelta >= yDelta) return xDelta
440    return yDelta
441  }
442
443  // returns the square of the closest instance of piece
444  // returns false if no instance of piece is found in position
445  function findClosestPiece (position, piece, square) {
446    // create array of closest squares from square
447    var closestSquares = createRadius(square)
448
449    // search through the position in order of distance for the piece
450    for (var i = 0; i < closestSquares.length; i++) {
451      var s = closestSquares[i]
452
453      if (position.hasOwnProperty(s) && position[s] === piece) {
454        return s
455      }
456    }
457
458    return false
459  }
460
461  // returns an array of closest squares from square
462  function createRadius (square) {
463    var squares = []
464
465    // calculate distance of all squares
466    for (var i = 0; i < 8; i++) {
467      for (var j = 0; j < 8; j++) {
468        var s = COLUMNS[i] + (j + 1)
469
470        // skip the square we're starting from
471        if (square === s) continue
472
473        squares.push({
474          square: s,
475          distance: squareDistance(square, s)
476        })
477      }
478    }
479
480    // sort by distance
481    squares.sort(function (a, b) {
482      return a.distance - b.distance
483    })
484
485    // just return the square code
486    var surroundingSquares = []
487    for (i = 0; i < squares.length; i++) {
488      surroundingSquares.push(squares[i].square)
489    }
490
491    return surroundingSquares
492  }
493
494  // given a position and a set of moves, return a new position
495  // with the moves executed
496  function calculatePositionFromMoves (position, moves) {
497    var newPosition = deepCopy(position)
498
499    for (var i in moves) {
500      if (!moves.hasOwnProperty(i)) continue
501
502      // skip the move if the position doesn't have a piece on the source square
503      if (!newPosition.hasOwnProperty(i)) continue
504
505      var piece = newPosition[i]
506      delete newPosition[i]
507      newPosition[moves[i]] = piece
508    }
509
510    return newPosition
511  }
512
513  // TODO: add some asserts here for calculatePositionFromMoves
514
515  // ---------------------------------------------------------------------------
516  // HTML
517  // ---------------------------------------------------------------------------
518
519  function buildContainerHTML (hasSparePieces) {
520    var html = '<div class="{chessboard}">'
521
522    if (hasSparePieces) {
523      html += '<div class="{sparePieces} {sparePiecesTop}"></div>'
524    }
525
526    html += '<div class="{board}"></div>'
527
528    if (hasSparePieces) {
529      html += '<div class="{sparePieces} {sparePiecesBottom}"></div>'
530    }
531
532    html += '</div>'
533
534    return interpolateTemplate(html, CSS)
535  }
536
537  // ---------------------------------------------------------------------------
538  // Config
539  // ---------------------------------------------------------------------------
540
541  function expandConfigArgumentShorthand (config) {
542    if (config === 'start') {
543      config = {position: deepCopy(START_POSITION)}
544    } else if (validFen(config)) {
545      config = {position: fenToObj(config)}
546    } else if (validPositionObject(config)) {
547      config = {position: deepCopy(config)}
548    }
549
550    // config must be an object
551    if (!$.isPlainObject(config)) config = {}
552
553    return config
554  }
555
556  // validate config / set default options
557  function expandConfig (config) {
558    // default for orientation is white
559    if (config.orientation !== 'black') config.orientation = 'white'
560
561    // default for showNotation is true
562    if (config.showNotation !== false) config.showNotation = true
563
564    // default for draggable is false
565    if (config.draggable !== true) config.draggable = false
566
567    // default for dropOffBoard is 'snapback'
568    if (config.dropOffBoard !== 'trash') config.dropOffBoard = 'snapback'
569
570    // default for sparePieces is false
571    if (config.sparePieces !== true) config.sparePieces = false
572
573    // draggable must be true if sparePieces is enabled
574    if (config.sparePieces) config.draggable = true
575
576    // default piece theme is wikipedia
577    if (!config.hasOwnProperty('pieceTheme') ||
578        (!isString(config.pieceTheme) && !isFunction(config.pieceTheme))) {
579      config.pieceTheme = 'img/chesspieces/wikipedia/{piece}.png'
580    }
581
582    // animation speeds
583    if (!validAnimationSpeed(config.appearSpeed)) config.appearSpeed = DEFAULT_APPEAR_SPEED
584    if (!validAnimationSpeed(config.moveSpeed)) config.moveSpeed = DEFAULT_MOVE_SPEED
585    if (!validAnimationSpeed(config.snapbackSpeed)) config.snapbackSpeed = DEFAULT_SNAPBACK_SPEED
586    if (!validAnimationSpeed(config.snapSpeed)) config.snapSpeed = DEFAULT_SNAP_SPEED
587    if (!validAnimationSpeed(config.trashSpeed)) config.trashSpeed = DEFAULT_TRASH_SPEED
588
589    // throttle rate
590    if (!validThrottleRate(config.dragThrottleRate)) config.dragThrottleRate = DEFAULT_DRAG_THROTTLE_RATE
591
592    return config
593  }
594
595  // ---------------------------------------------------------------------------
596  // Dependencies
597  // ---------------------------------------------------------------------------
598
599  // check for a compatible version of jQuery
600  function checkJQuery () {
601    if (!validJQueryVersion()) {
602      var errorMsg = 'Chessboard Error 1005: Unable to find a valid version of jQuery. ' +
603        'Please include jQuery ' + MINIMUM_JQUERY_VERSION + ' or higher on the page' +
604        '\n\n' +
605        'Exiting' + ELLIPSIS
606      window.alert(errorMsg)
607      return false
608    }
609
610    return true
611  }
612
613  // return either boolean false or the $container element
614  function checkContainerArg (containerElOrString) {
615    if (containerElOrString === '') {
616      var errorMsg1 = 'Chessboard Error 1001: ' +
617        'The first argument to Chessboard() cannot be an empty string.' +
618        '\n\n' +
619        'Exiting' + ELLIPSIS
620      window.alert(errorMsg1)
621      return false
622    }
623
624    // convert containerEl to query selector if it is a string
625    if (isString(containerElOrString) &&
626        containerElOrString.charAt(0) !== '#') {
627      containerElOrString = '#' + containerElOrString
628    }
629
630    // containerEl must be something that becomes a jQuery collection of size 1
631    var $container = $(containerElOrString)
632    if ($container.length !== 1) {
633      var errorMsg2 = 'Chessboard Error 1003: ' +
634        'The first argument to Chessboard() must be the ID of a DOM node, ' +
635        'an ID query selector, or a single DOM node.' +
636        '\n\n' +
637        'Exiting' + ELLIPSIS
638      window.alert(errorMsg2)
639      return false
640    }
641
642    return $container
643  }
644
645  // ---------------------------------------------------------------------------
646  // Constructor
647  // ---------------------------------------------------------------------------
648
649  function constructor (containerElOrString, config) {
650    // first things first: check basic dependencies
651    if (!checkJQuery()) return null
652    var $container = checkContainerArg(containerElOrString)
653    if (!$container) return null
654
655    // ensure the config object is what we expect
656    config = expandConfigArgumentShorthand(config)
657    config = expandConfig(config)
658
659    // DOM elements
660    var $board = null
661    var $draggedPiece = null
662    var $sparePiecesTop = null
663    var $sparePiecesBottom = null
664
665    // constructor return object
666    var widget = {}
667
668    // -------------------------------------------------------------------------
669    // Stateful
670    // -------------------------------------------------------------------------
671
672    var boardBorderSize = 2
673    var currentOrientation = 'white'
674    var currentPosition = {}
675    var draggedPiece = null
676    var draggedPieceLocation = null
677    var draggedPieceSource = null
678    var isDragging = false
679    var sparePiecesElsIds = {}
680    var squareElsIds = {}
681    var squareElsOffsets = {}
682    var squareSize = 16
683
684    // -------------------------------------------------------------------------
685    // Validation / Errors
686    // -------------------------------------------------------------------------
687
688    function error (code, msg, obj) {
689      // do nothing if showErrors is not set
690      if (
691        config.hasOwnProperty('showErrors') !== true ||
692          config.showErrors === false
693      ) {
694        return
695      }
696
697      var errorText = 'Chessboard Error ' + code + ': ' + msg
698
699      // print to console
700      if (
701        config.showErrors === 'console' &&
702          typeof console === 'object' &&
703          typeof console.log === 'function'
704      ) {
705        console.log(errorText)
706        if (arguments.length >= 2) {
707          console.log(obj)
708        }
709        return
710      }
711
712      // alert errors
713      if (config.showErrors === 'alert') {
714        if (obj) {
715          errorText += '\n\n' + JSON.stringify(obj)
716        }
717        window.alert(errorText)
718        return
719      }
720
721      // custom function
722      if (isFunction(config.showErrors)) {
723        config.showErrors(code, msg, obj)
724      }
725    }
726
727    function setInitialState () {
728      currentOrientation = config.orientation
729
730      // make sure position is valid
731      if (config.hasOwnProperty('position')) {
732        if (config.position === 'start') {
733          currentPosition = deepCopy(START_POSITION)
734        } else if (validFen(config.position)) {
735          currentPosition = fenToObj(config.position)
736        } else if (validPositionObject(config.position)) {
737          currentPosition = deepCopy(config.position)
738        } else {
739          error(
740            7263,
741            'Invalid value passed to config.position.',
742            config.position
743          )
744        }
745      }
746    }
747
748    // -------------------------------------------------------------------------
749    // DOM Misc
750    // -------------------------------------------------------------------------
751
752    // calculates square size based on the width of the container
753    // got a little CSS black magic here, so let me explain:
754    // get the width of the container element (could be anything), reduce by 1 for
755    // fudge factor, and then keep reducing until we find an exact mod 8 for
756    // our square size
757    function calculateSquareSize () {
758      var containerWidth = parseInt($container.width(), 10)
759
760      // defensive, prevent infinite loop
761      if (!containerWidth || containerWidth <= 0) {
762        return 0
763      }
764
765      // pad one pixel
766      var boardWidth = containerWidth - 1
767
768      while (boardWidth % 8 !== 0 && boardWidth > 0) {
769        boardWidth = boardWidth - 1
770      }
771
772      return boardWidth / 8
773    }
774
775    // create random IDs for elements
776    function createElIds () {
777      // squares on the board
778      for (var i = 0; i < COLUMNS.length; i++) {
779        for (var j = 1; j <= 8; j++) {
780          var square = COLUMNS[i] + j
781          squareElsIds[square] = square + '-' + uuid()
782        }
783      }
784
785      // spare pieces
786      var pieces = 'KQRNBP'.split('')
787      for (i = 0; i < pieces.length; i++) {
788        var whitePiece = 'w' + pieces[i]
789        var blackPiece = 'b' + pieces[i]
790        sparePiecesElsIds[whitePiece] = whitePiece + '-' + uuid()
791        sparePiecesElsIds[blackPiece] = blackPiece + '-' + uuid()
792      }
793    }
794
795    // -------------------------------------------------------------------------
796    // Markup Building
797    // -------------------------------------------------------------------------
798
799    function buildBoardHTML (orientation) {
800      if (orientation !== 'black') {
801        orientation = 'white'
802      }
803
804      var html = ''
805
806      // algebraic notation / orientation
807      var alpha = deepCopy(COLUMNS)
808      var row = 8
809      if (orientation === 'black') {
810        alpha.reverse()
811        row = 1
812      }
813
814      var squareColor = 'white'
815      for (var i = 0; i < 8; i++) {
816        html += '<div class="{row}">'
817        for (var j = 0; j < 8; j++) {
818          var square = alpha[j] + row
819
820          html += '<div class="{square} ' + CSS[squareColor] + ' ' +
821            'square-' + square + '" ' +
822            'style="width:' + squareSize + 'px;height:' + squareSize + 'px;" ' +
823            'id="' + squareElsIds[square] + '" ' +
824            'data-square="' + square + '">'
825
826          if (config.showNotation) {
827            // alpha notation
828            if ((orientation === 'white' && row === 1) ||
829                (orientation === 'black' && row === 8)) {
830              html += '<div class="{notation} {alpha}">' + alpha[j] + '</div>'
831            }
832
833            // numeric notation
834            if (j === 0) {
835              html += '<div class="{notation} {numeric}">' + row + '</div>'
836            }
837          }
838
839          html += '</div>' // end .square
840
841          squareColor = (squareColor === 'white') ? 'black' : 'white'
842        }
843        html += '<div class="{clearfix}"></div></div>'
844
845        squareColor = (squareColor === 'white') ? 'black' : 'white'
846
847        if (orientation === 'white') {
848          row = row - 1
849        } else {
850          row = row + 1
851        }
852      }
853
854      return interpolateTemplate(html, CSS)
855    }
856
857    function buildPieceImgSrc (piece) {
858      if (isFunction(config.pieceTheme)) {
859        return config.pieceTheme(piece)
860      }
861
862      if (isString(config.pieceTheme)) {
863        return interpolateTemplate(config.pieceTheme, {piece: piece})
864      }
865
866      // NOTE: this should never happen
867      error(8272, 'Unable to build image source for config.pieceTheme.')
868      return ''
869    }
870
871    function buildPieceHTML (piece, hidden, id) {
872      var html = '<img src="' + buildPieceImgSrc(piece) + '" '
873      if (isString(id) && id !== '') {
874        html += 'id="' + id + '" '
875      }
876      html += 'alt="" ' +
877        'class="{piece}" ' +
878        'data-piece="' + piece + '" ' +
879        'style="width:' + squareSize + 'px;' + 'height:' + squareSize + 'px;'
880
881      if (hidden) {
882        html += 'display:none;'
883      }
884
885      html += '" />'
886
887      return interpolateTemplate(html, CSS)
888    }
889
890    function buildSparePiecesHTML (color) {
891      var pieces = ['wK', 'wQ', 'wR', 'wB', 'wN', 'wP']
892      if (color === 'black') {
893        pieces = ['bK', 'bQ', 'bR', 'bB', 'bN', 'bP']
894      }
895
896      var html = ''
897      for (var i = 0; i < pieces.length; i++) {
898        html += buildPieceHTML(pieces[i], false, sparePiecesElsIds[pieces[i]])
899      }
900
901      return html
902    }
903
904    // -------------------------------------------------------------------------
905    // Animations
906    // -------------------------------------------------------------------------
907
908    function animateSquareToSquare (src, dest, piece, completeFn) {
909      // get information about the source and destination squares
910      var $srcSquare = $('#' + squareElsIds[src])
911      var srcSquarePosition = $srcSquare.offset()
912      var $destSquare = $('#' + squareElsIds[dest])
913      var destSquarePosition = $destSquare.offset()
914
915      // create the animated piece and absolutely position it
916      // over the source square
917      var animatedPieceId = uuid()
918      $('body').append(buildPieceHTML(piece, true, animatedPieceId))
919      var $animatedPiece = $('#' + animatedPieceId)
920      $animatedPiece.css({
921        display: '',
922        position: 'absolute',
923        top: srcSquarePosition.top,
924        left: srcSquarePosition.left
925      })
926
927      // remove original piece from source square
928      $srcSquare.find('.' + CSS.piece).remove()
929
930      function onFinishAnimation1 () {
931        // add the "real" piece to the destination square
932        $destSquare.append(buildPieceHTML(piece))
933
934        // remove the animated piece
935        $animatedPiece.remove()
936
937        // run complete function
938        if (isFunction(completeFn)) {
939          completeFn()
940        }
941      }
942
943      // animate the piece to the destination square
944      var opts = {
945        duration: config.moveSpeed,
946        complete: onFinishAnimation1
947      }
948      $animatedPiece.animate(destSquarePosition, opts)
949    }
950
951    function animateSparePieceToSquare (piece, dest, completeFn) {
952      var srcOffset = $('#' + sparePiecesElsIds[piece]).offset()
953      var $destSquare = $('#' + squareElsIds[dest])
954      var destOffset = $destSquare.offset()
955
956      // create the animate piece
957      var pieceId = uuid()
958      $('body').append(buildPieceHTML(piece, true, pieceId))
959      var $animatedPiece = $('#' + pieceId)
960      $animatedPiece.css({
961        display: '',
962        position: 'absolute',
963        left: srcOffset.left,
964        top: srcOffset.top
965      })
966
967      // on complete
968      function onFinishAnimation2 () {
969        // add the "real" piece to the destination square
970        $destSquare.find('.' + CSS.piece).remove()
971        $destSquare.append(buildPieceHTML(piece))
972
973        // remove the animated piece
974        $animatedPiece.remove()
975
976        // run complete function
977        if (isFunction(completeFn)) {
978          completeFn()
979        }
980      }
981
982      // animate the piece to the destination square
983      var opts = {
984        duration: config.moveSpeed,
985        complete: onFinishAnimation2
986      }
987      $animatedPiece.animate(destOffset, opts)
988    }
989
990    // execute an array of animations
991    function doAnimations (animations, oldPos, newPos) {
992      if (animations.length === 0) return
993
994      var numFinished = 0
995      function onFinishAnimation3 () {
996        // exit if all the animations aren't finished
997        numFinished = numFinished + 1
998        if (numFinished !== animations.length) return
999
1000        drawPositionInstant()
1001
1002        // run their onMoveEnd function
1003        if (isFunction(config.onMoveEnd)) {
1004          config.onMoveEnd(deepCopy(oldPos), deepCopy(newPos))
1005        }
1006      }
1007
1008      for (var i = 0; i < animations.length; i++) {
1009        var animation = animations[i]
1010
1011        // clear a piece
1012        if (animation.type === 'clear') {
1013          $('#' + squareElsIds[animation.square] + ' .' + CSS.piece)
1014            .fadeOut(config.trashSpeed, onFinishAnimation3)
1015
1016        // add a piece with no spare pieces - fade the piece onto the square
1017        } else if (animation.type === 'add' && !config.sparePieces) {
1018          $('#' + squareElsIds[animation.square])
1019            .append(buildPieceHTML(animation.piece, true))
1020            .find('.' + CSS.piece)
1021            .fadeIn(config.appearSpeed, onFinishAnimation3)
1022
1023        // add a piece with spare pieces - animate from the spares
1024        } else if (animation.type === 'add' && config.sparePieces) {
1025          animateSparePieceToSquare(animation.piece, animation.square, onFinishAnimation3)
1026
1027        // move a piece from squareA to squareB
1028        } else if (animation.type === 'move') {
1029          animateSquareToSquare(animation.source, animation.destination, animation.piece, onFinishAnimation3)
1030        }
1031      }
1032    }
1033
1034    // calculate an array of animations that need to happen in order to get
1035    // from pos1 to pos2
1036    function calculateAnimations (pos1, pos2) {
1037      // make copies of both
1038      pos1 = deepCopy(pos1)
1039      pos2 = deepCopy(pos2)
1040
1041      var animations = []
1042      var squaresMovedTo = {}
1043
1044      // remove pieces that are the same in both positions
1045      for (var i in pos2) {
1046        if (!pos2.hasOwnProperty(i)) continue
1047
1048        if (pos1.hasOwnProperty(i) && pos1[i] === pos2[i]) {
1049          delete pos1[i]
1050          delete pos2[i]
1051        }
1052      }
1053
1054      // find all the "move" animations
1055      for (i in pos2) {
1056        if (!pos2.hasOwnProperty(i)) continue
1057
1058        var closestPiece = findClosestPiece(pos1, pos2[i], i)
1059        if (closestPiece) {
1060          animations.push({
1061            type: 'move',
1062            source: closestPiece,
1063            destination: i,
1064            piece: pos2[i]
1065          })
1066
1067          delete pos1[closestPiece]
1068          delete pos2[i]
1069          squaresMovedTo[i] = true
1070        }
1071      }
1072
1073      // "add" animations
1074      for (i in pos2) {
1075        if (!pos2.hasOwnProperty(i)) continue
1076
1077        animations.push({
1078          type: 'add',
1079          square: i,
1080          piece: pos2[i]
1081        })
1082
1083        delete pos2[i]
1084      }
1085
1086      // "clear" animations
1087      for (i in pos1) {
1088        if (!pos1.hasOwnProperty(i)) continue
1089
1090        // do not clear a piece if it is on a square that is the result
1091        // of a "move", ie: a piece capture
1092        if (squaresMovedTo.hasOwnProperty(i)) continue
1093
1094        animations.push({
1095          type: 'clear',
1096          square: i,
1097          piece: pos1[i]
1098        })
1099
1100        delete pos1[i]
1101      }
1102
1103      return animations
1104    }
1105
1106    // -------------------------------------------------------------------------
1107    // Control Flow
1108    // -------------------------------------------------------------------------
1109
1110    function drawPositionInstant () {
1111      // clear the board
1112      $board.find('.' + CSS.piece).remove()
1113
1114      // add the pieces
1115      for (var i in currentPosition) {
1116        if (!currentPosition.hasOwnProperty(i)) continue
1117
1118        $('#' + squareElsIds[i]).append(buildPieceHTML(currentPosition[i]))
1119      }
1120    }
1121
1122    function drawBoard () {
1123      $board.html(buildBoardHTML(currentOrientation, squareSize, config.showNotation))
1124      drawPositionInstant()
1125
1126      if (config.sparePieces) {
1127        if (currentOrientation === 'white') {
1128          $sparePiecesTop.html(buildSparePiecesHTML('black'))
1129          $sparePiecesBottom.html(buildSparePiecesHTML('white'))
1130        } else {
1131          $sparePiecesTop.html(buildSparePiecesHTML('white'))
1132          $sparePiecesBottom.html(buildSparePiecesHTML('black'))
1133        }
1134      }
1135    }
1136
1137    function setCurrentPosition (position) {
1138      var oldPos = deepCopy(currentPosition)
1139      var newPos = deepCopy(position)
1140      var oldFen = objToFen(oldPos)
1141      var newFen = objToFen(newPos)
1142
1143      // do nothing if no change in position
1144      if (oldFen === newFen) return
1145
1146      // run their onChange function
1147      if (isFunction(config.onChange)) {
1148        config.onChange(oldPos, newPos)
1149      }
1150
1151      // update state
1152      currentPosition = position
1153    }
1154
1155    function isXYOnSquare (x, y) {
1156      for (var i in squareElsOffsets) {
1157        if (!squareElsOffsets.hasOwnProperty(i)) continue
1158
1159        var s = squareElsOffsets[i]
1160        if (x >= s.left &&
1161            x < s.left + squareSize &&
1162            y >= s.top &&
1163            y < s.top + squareSize) {
1164          return i
1165        }
1166      }
1167
1168      return 'offboard'
1169    }
1170
1171    // records the XY coords of every square into memory
1172    function captureSquareOffsets () {
1173      squareElsOffsets = {}
1174
1175      for (var i in squareElsIds) {
1176        if (!squareElsIds.hasOwnProperty(i)) continue
1177
1178        squareElsOffsets[i] = $('#' + squareElsIds[i]).offset()
1179      }
1180    }
1181
1182    function removeSquareHighlights () {
1183      $board
1184        .find('.' + CSS.square)
1185        .removeClass(CSS.highlight1 + ' ' + CSS.highlight2)
1186    }
1187
1188    function snapbackDraggedPiece () {
1189      // there is no "snapback" for spare pieces
1190      if (draggedPieceSource === 'spare') {
1191        trashDraggedPiece()
1192        return
1193      }
1194
1195      removeSquareHighlights()
1196
1197      // animation complete
1198      function complete () {
1199        drawPositionInstant()
1200        $draggedPiece.css('display', 'none')
1201
1202        // run their onSnapbackEnd function
1203        if (isFunction(config.onSnapbackEnd)) {
1204          config.onSnapbackEnd(
1205            draggedPiece,
1206            draggedPieceSource,
1207            deepCopy(currentPosition),
1208            currentOrientation
1209          )
1210        }
1211      }
1212
1213      // get source square position
1214      var sourceSquarePosition = $('#' + squareElsIds[draggedPieceSource]).offset()
1215
1216      // animate the piece to the target square
1217      var opts = {
1218        duration: config.snapbackSpeed,
1219        complete: complete
1220      }
1221      $draggedPiece.animate(sourceSquarePosition, opts)
1222
1223      // set state
1224      isDragging = false
1225    }
1226
1227    function trashDraggedPiece () {
1228      removeSquareHighlights()
1229
1230      // remove the source piece
1231      var newPosition = deepCopy(currentPosition)
1232      delete newPosition[draggedPieceSource]
1233      setCurrentPosition(newPosition)
1234
1235      // redraw the position
1236      drawPositionInstant()
1237
1238      // hide the dragged piece
1239      $draggedPiece.fadeOut(config.trashSpeed)
1240
1241      // set state
1242      isDragging = false
1243    }
1244
1245    function dropDraggedPieceOnSquare (square) {
1246      removeSquareHighlights()
1247
1248      // update position
1249      var newPosition = deepCopy(currentPosition)
1250      delete newPosition[draggedPieceSource]
1251      newPosition[square] = draggedPiece
1252      setCurrentPosition(newPosition)
1253
1254      // get target square information
1255      var targetSquarePosition = $('#' + squareElsIds[square]).offset()
1256
1257      // animation complete
1258      function onAnimationComplete () {
1259        drawPositionInstant()
1260        $draggedPiece.css('display', 'none')
1261
1262        // execute their onSnapEnd function
1263        if (isFunction(config.onSnapEnd)) {
1264          config.onSnapEnd(draggedPieceSource, square, draggedPiece)
1265        }
1266      }
1267
1268      // snap the piece to the target square
1269      var opts = {
1270        duration: config.snapSpeed,
1271        complete: onAnimationComplete
1272      }
1273      $draggedPiece.animate(targetSquarePosition, opts)
1274
1275      // set state
1276      isDragging = false
1277    }
1278
1279    function beginDraggingPiece (source, piece, x, y) {
1280      // run their custom onDragStart function
1281      // their custom onDragStart function can cancel drag start
1282      if (isFunction(config.onDragStart) &&
1283          config.onDragStart(source, piece, deepCopy(currentPosition), currentOrientation) === false) {
1284        return
1285      }
1286
1287      // set state
1288      isDragging = true
1289      draggedPiece = piece
1290      draggedPieceSource = source
1291
1292      // if the piece came from spare pieces, location is offboard
1293      if (source === 'spare') {
1294        draggedPieceLocation = 'offboard'
1295      } else {
1296        draggedPieceLocation = source
1297      }
1298
1299      // capture the x, y coords of all squares in memory
1300      captureSquareOffsets()
1301
1302      // create the dragged piece
1303      $draggedPiece.attr('src', buildPieceImgSrc(piece)).css({
1304        display: '',
1305        position: 'absolute',
1306        left: x - squareSize / 2,
1307        top: y - squareSize / 2
1308      })
1309
1310      if (source !== 'spare') {
1311        // highlight the source square and hide the piece
1312        $('#' + squareElsIds[source])
1313          .addClass(CSS.highlight1)
1314          .find('.' + CSS.piece)
1315          .css('display', 'none')
1316      }
1317    }
1318
1319    function updateDraggedPiece (x, y) {
1320      // put the dragged piece over the mouse cursor
1321      $draggedPiece.css({
1322        left: x - squareSize / 2,
1323        top: y - squareSize / 2
1324      })
1325
1326      // get location
1327      var location = isXYOnSquare(x, y)
1328
1329      // do nothing if the location has not changed
1330      if (location === draggedPieceLocation) return
1331
1332      // remove highlight from previous square
1333      if (validSquare(draggedPieceLocation)) {
1334        $('#' + squareElsIds[draggedPieceLocation]).removeClass(CSS.highlight2)
1335      }
1336
1337      // add highlight to new square
1338      if (validSquare(location)) {
1339        $('#' + squareElsIds[location]).addClass(CSS.highlight2)
1340      }
1341
1342      // run onDragMove
1343      if (isFunction(config.onDragMove)) {
1344        config.onDragMove(
1345          location,
1346          draggedPieceLocation,
1347          draggedPieceSource,
1348          draggedPiece,
1349          deepCopy(currentPosition),
1350          currentOrientation
1351        )
1352      }
1353
1354      // update state
1355      draggedPieceLocation = location
1356    }
1357
1358    function stopDraggedPiece (location) {
1359      // determine what the action should be
1360      var action = 'drop'
1361      if (location === 'offboard' && config.dropOffBoard === 'snapback') {
1362        action = 'snapback'
1363      }
1364      if (location === 'offboard' && config.dropOffBoard === 'trash') {
1365        action = 'trash'
1366      }
1367
1368      // run their onDrop function, which can potentially change the drop action
1369      if (isFunction(config.onDrop)) {
1370        var newPosition = deepCopy(currentPosition)
1371
1372        // source piece is a spare piece and position is off the board
1373        // if (draggedPieceSource === 'spare' && location === 'offboard') {...}
1374        // position has not changed; do nothing
1375
1376        // source piece is a spare piece and position is on the board
1377        if (draggedPieceSource === 'spare' && validSquare(location)) {
1378          // add the piece to the board
1379          newPosition[location] = draggedPiece
1380        }
1381
1382        // source piece was on the board and position is off the board
1383        if (validSquare(draggedPieceSource) && location === 'offboard') {
1384          // remove the piece from the board
1385          delete newPosition[draggedPieceSource]
1386        }
1387
1388        // source piece was on the board and position is on the board
1389        if (validSquare(draggedPieceSource) && validSquare(location)) {
1390          // move the piece
1391          delete newPosition[draggedPieceSource]
1392          newPosition[location] = draggedPiece
1393        }
1394
1395        var oldPosition = deepCopy(currentPosition)
1396
1397        var result = config.onDrop(
1398          draggedPieceSource,
1399          location,
1400          draggedPiece,
1401          newPosition,
1402          oldPosition,
1403          currentOrientation
1404        )
1405        if (result === 'snapback' || result === 'trash') {
1406          action = result
1407        }
1408      }
1409
1410      // do it!
1411      if (action === 'snapback') {
1412        snapbackDraggedPiece()
1413      } else if (action === 'trash') {
1414        trashDraggedPiece()
1415      } else if (action === 'drop') {
1416        dropDraggedPieceOnSquare(location)
1417      }
1418    }
1419
1420    // -------------------------------------------------------------------------
1421    // Public Methods
1422    // -------------------------------------------------------------------------
1423
1424    // clear the board
1425    widget.clear = function (useAnimation) {
1426      widget.position({}, useAnimation)
1427    }
1428
1429    // remove the widget from the page
1430    widget.destroy = function () {
1431      // remove markup
1432      $container.html('')
1433      $draggedPiece.remove()
1434
1435      // remove event handlers
1436      $container.unbind()
1437    }
1438
1439    // shorthand method to get the current FEN
1440    widget.fen = function () {
1441      return widget.position('fen')
1442    }
1443
1444    // flip orientation
1445    widget.flip = function () {
1446      return widget.orientation('flip')
1447    }
1448
1449    // move pieces
1450    // TODO: this method should be variadic as well as accept an array of moves
1451    widget.move = function () {
1452      // no need to throw an error here; just do nothing
1453      // TODO: this should return the current position
1454      if (arguments.length === 0) return
1455
1456      var useAnimation = true
1457
1458      // collect the moves into an object
1459      var moves = {}
1460      for (var i = 0; i < arguments.length; i++) {
1461        // any "false" to this function means no animations
1462        if (arguments[i] === false) {
1463          useAnimation = false
1464          continue
1465        }
1466
1467        // skip invalid arguments
1468        if (!validMove(arguments[i])) {
1469          error(2826, 'Invalid move passed to the move method.', arguments[i])
1470          continue
1471        }
1472
1473        var tmp = arguments[i].split('-')
1474        moves[tmp[0]] = tmp[1]
1475      }
1476
1477      // calculate position from moves
1478      var newPos = calculatePositionFromMoves(currentPosition, moves)
1479
1480      // update the board
1481      widget.position(newPos, useAnimation)
1482
1483      // return the new position object
1484      return newPos
1485    }
1486
1487    widget.orientation = function (arg) {
1488      // no arguments, return the current orientation
1489      if (arguments.length === 0) {
1490        return currentOrientation
1491      }
1492
1493      // set to white or black
1494      if (arg === 'white' || arg === 'black') {
1495        currentOrientation = arg
1496        drawBoard()
1497        return currentOrientation
1498      }
1499
1500      // flip orientation
1501      if (arg === 'flip') {
1502        currentOrientation = currentOrientation === 'white' ? 'black' : 'white'
1503        drawBoard()
1504        return currentOrientation
1505      }
1506
1507      error(5482, 'Invalid value passed to the orientation method.', arg)
1508    }
1509
1510    widget.position = function (position, useAnimation) {
1511      // no arguments, return the current position
1512      if (arguments.length === 0) {
1513        return deepCopy(currentPosition)
1514      }
1515
1516      // get position as FEN
1517      if (isString(position) && position.toLowerCase() === 'fen') {
1518        return objToFen(currentPosition)
1519      }
1520
1521      // start position
1522      if (isString(position) && position.toLowerCase() === 'start') {
1523        position = deepCopy(START_POSITION)
1524      }
1525
1526      // convert FEN to position object
1527      if (validFen(position)) {
1528        position = fenToObj(position)
1529      }
1530
1531      // validate position object
1532      if (!validPositionObject(position)) {
1533        error(6482, 'Invalid value passed to the position method.', position)
1534        return
1535      }
1536
1537      // default for useAnimations is true
1538      if (useAnimation !== false) useAnimation = true
1539
1540      if (useAnimation) {
1541        // start the animations
1542        var animations = calculateAnimations(currentPosition, position)
1543        doAnimations(animations, currentPosition, position)
1544
1545        // set the new position
1546        setCurrentPosition(position)
1547      } else {
1548        // instant update
1549        setCurrentPosition(position)
1550        drawPositionInstant()
1551      }
1552    }
1553
1554    widget.resize = function () {
1555      // calulate the new square size
1556      squareSize = calculateSquareSize()
1557
1558      // set board width
1559      $board.css('width', squareSize * 8 + 'px')
1560
1561      // set drag piece size
1562      $draggedPiece.css({
1563        height: squareSize,
1564        width: squareSize
1565      })
1566
1567      // spare pieces
1568      if (config.sparePieces) {
1569        $container
1570          .find('.' + CSS.sparePieces)
1571          .css('paddingLeft', squareSize + boardBorderSize + 'px')
1572      }
1573
1574      // redraw the board
1575      drawBoard()
1576    }
1577
1578    // set the starting position
1579    widget.start = function (useAnimation) {
1580      widget.position('start', useAnimation)
1581    }
1582
1583    // -------------------------------------------------------------------------
1584    // Browser Events
1585    // -------------------------------------------------------------------------
1586
1587    function stopDefault (evt) {
1588      evt.preventDefault()
1589    }
1590
1591    function mousedownSquare (evt) {
1592      // do nothing if we're not draggable
1593      if (!config.draggable) return
1594
1595      // do nothing if there is no piece on this square
1596      var square = $(this).attr('data-square')
1597      if (!validSquare(square)) return
1598      if (!currentPosition.hasOwnProperty(square)) return
1599
1600      beginDraggingPiece(square, currentPosition[square], evt.pageX, evt.pageY)
1601    }
1602
1603    function touchstartSquare (e) {
1604      // do nothing if we're not draggable
1605      if (!config.draggable) return
1606
1607      // do nothing if there is no piece on this square
1608      var square = $(this).attr('data-square')
1609      if (!validSquare(square)) return
1610      if (!currentPosition.hasOwnProperty(square)) return
1611
1612      e = e.originalEvent
1613      beginDraggingPiece(
1614        square,
1615        currentPosition[square],
1616        e.changedTouches[0].pageX,
1617        e.changedTouches[0].pageY
1618      )
1619    }
1620
1621    function mousedownSparePiece (evt) {
1622      // do nothing if sparePieces is not enabled
1623      if (!config.sparePieces) return
1624
1625      var piece = $(this).attr('data-piece')
1626
1627      beginDraggingPiece('spare', piece, evt.pageX, evt.pageY)
1628    }
1629
1630    function touchstartSparePiece (e) {
1631      // do nothing if sparePieces is not enabled
1632      if (!config.sparePieces) return
1633
1634      var piece = $(this).attr('data-piece')
1635
1636      e = e.originalEvent
1637      beginDraggingPiece(
1638        'spare',
1639        piece,
1640        e.changedTouches[0].pageX,
1641        e.changedTouches[0].pageY
1642      )
1643    }
1644
1645    function mousemoveWindow (evt) {
1646      if (isDragging) {
1647        updateDraggedPiece(evt.pageX, evt.pageY)
1648      }
1649    }
1650
1651    var throttledMousemoveWindow = throttle(mousemoveWindow, config.dragThrottleRate)
1652
1653    function touchmoveWindow (evt) {
1654      // do nothing if we are not dragging a piece
1655      if (!isDragging) return
1656
1657      // prevent screen from scrolling
1658      evt.preventDefault()
1659
1660      updateDraggedPiece(evt.originalEvent.changedTouches[0].pageX,
1661        evt.originalEvent.changedTouches[0].pageY)
1662    }
1663
1664    var throttledTouchmoveWindow = throttle(touchmoveWindow, config.dragThrottleRate)
1665
1666    function mouseupWindow (evt) {
1667      // do nothing if we are not dragging a piece
1668      if (!isDragging) return
1669
1670      // get the location
1671      var location = isXYOnSquare(evt.pageX, evt.pageY)
1672
1673      stopDraggedPiece(location)
1674    }
1675
1676    function touchendWindow (evt) {
1677      // do nothing if we are not dragging a piece
1678      if (!isDragging) return
1679
1680      // get the location
1681      var location = isXYOnSquare(evt.originalEvent.changedTouches[0].pageX,
1682        evt.originalEvent.changedTouches[0].pageY)
1683
1684      stopDraggedPiece(location)
1685    }
1686
1687    function mouseenterSquare (evt) {
1688      // do not fire this event if we are dragging a piece
1689      // NOTE: this should never happen, but it's a safeguard
1690      if (isDragging) return
1691
1692      // exit if they did not provide a onMouseoverSquare function
1693      if (!isFunction(config.onMouseoverSquare)) return
1694
1695      // get the square
1696      var square = $(evt.currentTarget).attr('data-square')
1697
1698      // NOTE: this should never happen; defensive
1699      if (!validSquare(square)) return
1700
1701      // get the piece on this square
1702      var piece = false
1703      if (currentPosition.hasOwnProperty(square)) {
1704        piece = currentPosition[square]
1705      }
1706
1707      // execute their function
1708      config.onMouseoverSquare(square, piece, deepCopy(currentPosition), currentOrientation)
1709    }
1710
1711    function mouseleaveSquare (evt) {
1712      // do not fire this event if we are dragging a piece
1713      // NOTE: this should never happen, but it's a safeguard
1714      if (isDragging) return
1715
1716      // exit if they did not provide an onMouseoutSquare function
1717      if (!isFunction(config.onMouseoutSquare)) return
1718
1719      // get the square
1720      var square = $(evt.currentTarget).attr('data-square')
1721
1722      // NOTE: this should never happen; defensive
1723      if (!validSquare(square)) return
1724
1725      // get the piece on this square
1726      var piece = false
1727      if (currentPosition.hasOwnProperty(square)) {
1728        piece = currentPosition[square]
1729      }
1730
1731      // execute their function
1732      config.onMouseoutSquare(square, piece, deepCopy(currentPosition), currentOrientation)
1733    }
1734
1735    // -------------------------------------------------------------------------
1736    // Initialization
1737    // -------------------------------------------------------------------------
1738
1739    function addEvents () {
1740      // prevent "image drag"
1741      $('body').on('mousedown mousemove', '.' + CSS.piece, stopDefault)
1742
1743      // mouse drag pieces
1744      $board.on('mousedown', '.' + CSS.square, mousedownSquare)
1745      $container.on('mousedown', '.' + CSS.sparePieces + ' .' + CSS.piece, mousedownSparePiece)
1746
1747      // mouse enter / leave square
1748      $board
1749        .on('mouseenter', '.' + CSS.square, mouseenterSquare)
1750        .on('mouseleave', '.' + CSS.square, mouseleaveSquare)
1751
1752      // piece drag
1753      var $window = $(window)
1754      $window
1755        .on('mousemove', throttledMousemoveWindow)
1756        .on('mouseup', mouseupWindow)
1757
1758      // touch drag pieces
1759      if (isTouchDevice()) {
1760        $board.on('touchstart', '.' + CSS.square, touchstartSquare)
1761        $container.on('touchstart', '.' + CSS.sparePieces + ' .' + CSS.piece, touchstartSparePiece)
1762        $window
1763          .on('touchmove', throttledTouchmoveWindow)
1764          .on('touchend', touchendWindow)
1765      }
1766    }
1767
1768    function initDOM () {
1769      // create unique IDs for all the elements we will create
1770      createElIds()
1771
1772      // build board and save it in memory
1773      $container.html(buildContainerHTML(config.sparePieces))
1774      $board = $container.find('.' + CSS.board)
1775
1776      if (config.sparePieces) {
1777        $sparePiecesTop = $container.find('.' + CSS.sparePiecesTop)
1778        $sparePiecesBottom = $container.find('.' + CSS.sparePiecesBottom)
1779      }
1780
1781      // create the drag piece
1782      var draggedPieceId = uuid()
1783      $('body').append(buildPieceHTML('wP', true, draggedPieceId)
1783)
1784      $draggedPiece = $('#' + draggedPieceId)
1785
1786      // TODO: need to remove this dragged piece element if the board is no
1787      // longer in the DOM
1788
1789      // get the border size
1790      boardBorderSize = parseInt($board.css('borderLeftWidth'), 10)
1791
1792      // set the size and draw the board
1793      widget.resize()
1794    }
1795
1796    // -------------------------------------------------------------------------
1797    // Initialization
1798    // -------------------------------------------------------------------------
1799
1800    setInitialState()
1801    initDOM()
1802    addEvents()
1803
1804    // return the widget object
1805    return widget
1806  } // end constructor
1807
1808  // TODO: do module exports here
1809  window['Chessboard'] = constructor
1810
1811  // support legacy ChessBoard name
1812  window['ChessBoard'] = window['Chessboard']
1813
1814  // expose util functions
1815  window['Chessboard']['fenToObj'] = fenToObj
1816  window['Chessboard']['objToFen'] = objToFen
1817})() // end anonymous wrapper

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.