]> git.sesse.net Git - remoteglot/blob - www/js/chessboard-0.3.0.js
Remove some trash code that we do not use.
[remoteglot] / www / js / chessboard-0.3.0.js
1 /*!
2  * chessboard.js v0.3.0+asn
3  *
4  * Copyright 2013 Chris Oakman
5  * Portions copyright 2022 Steinar H. Gunderson
6  * Released under the MIT license
7  * http://chessboardjs.com/license
8  *
9  * Date: 10 Aug 2013
10  */
11
12 // start anonymous scope
13 ;(function() {
14 'use strict';
15
16 //------------------------------------------------------------------------------
17 // Chess Util Functions
18 //------------------------------------------------------------------------------
19 var COLUMNS = 'abcdefgh'.split('');
20
21 function validMove(move) {
22   // move should be a string
23   if (typeof move !== 'string') return false;
24
25   // move should be in the form of "e2-e4", "f6-d5"
26   var tmp = move.split('-');
27   if (tmp.length !== 2) return false;
28
29   return (validSquare(tmp[0]) === true && validSquare(tmp[1]) === true);
30 }
31
32 function validSquare(square) {
33   if (typeof square !== 'string') return false;
34   return (square.search(/^[a-h][1-8]$/) !== -1);
35 }
36
37 function validPieceCode(code) {
38   if (typeof code !== 'string') return false;
39   return (code.search(/^[bw][KQRNBP]$/) !== -1);
40 }
41
42 // TODO: this whole function could probably be replaced with a single regex
43 function validFen(fen) {
44   if (typeof fen !== 'string') return false;
45
46   // cut off any move, castling, etc info from the end
47   // we're only interested in position information
48   fen = fen.replace(/ .+$/, '');
49
50   // FEN should be 8 sections separated by slashes
51   var chunks = fen.split('/');
52   if (chunks.length !== 8) return false;
53
54   // check the piece sections
55   for (var i = 0; i < 8; i++) {
56     if (chunks[i] === '' ||
57         chunks[i].length > 8 ||
58         chunks[i].search(/[^kqrbnpKQRNBP1-8]/) !== -1) {
59       return false;
60     }
61   }
62
63   return true;
64 }
65
66 function validPositionObject(pos) {
67   if (typeof pos !== 'object') return false;
68
69   for (var i in pos) {
70     if (pos.hasOwnProperty(i) !== true) continue;
71
72     if (validSquare(i) !== true || validPieceCode(pos[i]) !== true) {
73       return false;
74     }
75   }
76
77   return true;
78 }
79
80 // convert FEN piece code to bP, wK, etc
81 function fenToPieceCode(piece) {
82   // black piece
83   if (piece.toLowerCase() === piece) {
84     return 'b' + piece.toUpperCase();
85   }
86
87   // white piece
88   return 'w' + piece.toUpperCase();
89 }
90
91 // convert bP, wK, etc code to FEN structure
92 function pieceCodeToFen(piece) {
93   var tmp = piece.split('');
94
95   // white piece
96   if (tmp[0] === 'w') {
97     return tmp[1].toUpperCase();
98   }
99
100   // black piece
101   return tmp[1].toLowerCase();
102 }
103
104 // convert FEN string to position object
105 // returns false if the FEN string is invalid
106 function fenToObj(fen) {
107   if (validFen(fen) !== true) {
108     return false;
109   }
110
111   // cut off any move, castling, etc info from the end
112   // we're only interested in position information
113   fen = fen.replace(/ .+$/, '');
114
115   var rows = fen.split('/');
116   var position = {};
117
118   var currentRow = 8;
119   for (var i = 0; i < 8; i++) {
120     var row = rows[i].split('');
121     var colIndex = 0;
122
123     // loop through each character in the FEN section
124     for (var j = 0; j < row.length; j++) {
125       // number / empty squares
126       if (row[j].search(/[1-8]/) !== -1) {
127         var emptySquares = parseInt(row[j], 10);
128         colIndex += emptySquares;
129       }
130       // piece
131       else {
132         var square = COLUMNS[colIndex] + currentRow;
133         position[square] = fenToPieceCode(row[j]);
134         colIndex++;
135       }
136     }
137
138     currentRow--;
139   }
140
141   return position;
142 }
143
144 // position object to FEN string
145 // returns false if the obj is not a valid position object
146 function objToFen(obj) {
147   if (validPositionObject(obj) !== true) {
148     return false;
149   }
150
151   var fen = '';
152
153   var currentRow = 8;
154   for (var i = 0; i < 8; i++) {
155     for (var j = 0; j < 8; j++) {
156       var square = COLUMNS[j] + currentRow;
157
158       // piece exists
159       if (obj.hasOwnProperty(square) === true) {
160         fen += pieceCodeToFen(obj[square]);
161       }
162
163       // empty space
164       else {
165         fen += '1';
166       }
167     }
168
169     if (i !== 7) {
170       fen += '/';
171     }
172
173     currentRow--;
174   }
175
176   // squeeze the numbers together
177   // haha, I love this solution...
178   fen = fen.replace(/11111111/g, '8');
179   fen = fen.replace(/1111111/g, '7');
180   fen = fen.replace(/111111/g, '6');
181   fen = fen.replace(/11111/g, '5');
182   fen = fen.replace(/1111/g, '4');
183   fen = fen.replace(/111/g, '3');
184   fen = fen.replace(/11/g, '2');
185
186   return fen;
187 }
188
189 /** @struct */
190 var cfg;
191
192 /** @constructor */
193 window.ChessBoard = function(containerElOrId, cfg) {
194 'use strict';
195
196 cfg = cfg || {};
197
198 //------------------------------------------------------------------------------
199 // Constants
200 //------------------------------------------------------------------------------
201
202 var START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR',
203   START_POSITION = fenToObj(START_FEN);
204
205 // use unique class names to prevent clashing with anything else on the page
206 // and simplify selectors
207 var CSS = {
208   alpha: 'alpha-d2270',
209   board: 'board-b72b1',
210   chessboard: 'chessboard-63f37',
211   clearfix: 'clearfix-7da63',
212   highlight1: 'highlight1-32417',
213   highlight2: 'highlight2-9c5d2',
214   notation: 'notation-322f9',
215   numeric: 'numeric-fc462',
216   piece: 'piece-417db',
217   row: 'row-5277c',
218   square: 'square-55d63'
219 };
220 var CSSColor = {};
221 CSSColor['white'] = 'white-1e1d7';
222 CSSColor['black'] = 'black-3c85d';
223
224 //------------------------------------------------------------------------------
225 // Module Scope Variables
226 //------------------------------------------------------------------------------
227
228 // DOM elements
229 var containerEl,
230   boardEl,
231   draggedPieceEl;
232
233 // constructor return object
234 var widget = {};
235
236 //------------------------------------------------------------------------------
237 // Stateful
238 //------------------------------------------------------------------------------
239
240 var BOARD_BORDER_SIZE = 2,
241   CURRENT_ORIENTATION = 'white',
242   CURRENT_POSITION = {},
243   SQUARE_SIZE,
244   DRAGGED_PIECE,
245   DRAGGED_PIECE_LOCATION,
246   DRAGGED_PIECE_SOURCE,
247   DRAGGING_A_PIECE = false,
248   SQUARE_ELS_IDS = {},
249   SQUARE_ELS_OFFSETS;
250
251 //------------------------------------------------------------------------------
252 // JS Util Functions
253 //------------------------------------------------------------------------------
254
255 let id_counter = 0;
256 function createId() {
257   return 'chesspiece-id-' + (id_counter++);
258 }
259
260 function deepCopy(thing) {
261   return JSON.parse(JSON.stringify(thing));
262 }
263
264 //------------------------------------------------------------------------------
265 // Validation / Errors
266 //------------------------------------------------------------------------------
267
268 /**
269  * @param {!number} code
270  * @param {!string} msg
271  * @param {Object=} obj
272  */
273 function error(code, msg, obj) {
274   // do nothing if showErrors is not set
275   if (cfg.hasOwnProperty('showErrors') !== true ||
276       cfg.showErrors === false) {
277     return;
278   }
279
280   var errorText = 'ChessBoard Error ' + code + ': ' + msg;
281
282   // print to console
283   if (cfg.showErrors === 'console' &&
284       typeof console === 'object' &&
285       typeof console.log === 'function') {
286     console.log(errorText);
287     if (arguments.length >= 2) {
288       console.log(obj);
289     }
290     return;
291   }
292
293   // alert errors
294   if (cfg.showErrors === 'alert') {
295     if (obj) {
296       errorText += '\n\n' + JSON.stringify(obj);
297     }
298     window.alert(errorText);
299     return;
300   }
301
302   // custom function
303   if (typeof cfg.showErrors === 'function') {
304     cfg.showErrors(code, msg, obj);
305   }
306 }
307
308 // check dependencies
309 function checkDeps() {
310   // if containerId is a string, it must be the ID of a DOM node
311   if (typeof containerElOrId === 'string') {
312     // cannot be empty
313     if (containerElOrId === '') {
314       window.alert('ChessBoard Error 1001: ' +
315         'The first argument to ChessBoard() cannot be an empty string.' +
316         '\n\nExiting...');
317       return false;
318     }
319
320     // make sure the container element exists in the DOM
321     var el = document.getElementById(containerElOrId);
322     if (! el) {
323       window.alert('ChessBoard Error 1002: Element with id "' +
324         containerElOrId + '" does not exist in the DOM.' +
325         '\n\nExiting...');
326       return false;
327     }
328
329     // set the containerEl
330     containerEl = el;
331   }
332
333   // else it must be a DOM node
334   else {
335     containerEl = containerElOrId;
336   }
337
338   return true;
339 }
340
341 function validAnimationSpeed(speed) {
342   if (speed === 'fast' || speed === 'slow') {
343     return true;
344   }
345
346   if ((parseInt(speed, 10) + '') !== (speed + '')) {
347     return false;
348   }
349
350   return (speed >= 0);
351 }
352
353 // validate config / set default options
354 function expandConfig() {
355   if (typeof cfg === 'string' || validPositionObject(cfg) === true) {
356     cfg = {
357       position: cfg
358     };
359   }
360
361   // default for orientation is white
362   if (cfg.orientation !== 'black') {
363     cfg.orientation = 'white';
364   }
365   CURRENT_ORIENTATION = cfg.orientation;
366
367   // default for showNotation is true
368   if (cfg.showNotation !== false) {
369     cfg.showNotation = true;
370   }
371
372   // default for draggable is false
373   if (cfg.draggable !== true) {
374     cfg.draggable = false;
375   }
376
377   // default piece theme is wikipedia
378   if (cfg.hasOwnProperty('pieceTheme') !== true ||
379       (typeof cfg.pieceTheme !== 'string' &&
380        typeof cfg.pieceTheme !== 'function')) {
381     cfg.pieceTheme = 'img/chesspieces/wikipedia/{piece}.png';
382   }
383
384   // animation speeds
385   if (cfg.hasOwnProperty('appearSpeed') !== true ||
386       validAnimationSpeed(cfg.appearSpeed) !== true) {
387     cfg.appearSpeed = 200;
388   }
389   if (cfg.hasOwnProperty('moveSpeed') !== true ||
390       validAnimationSpeed(cfg.moveSpeed) !== true) {
391     cfg.moveSpeed = 200;
392   }
393   if (cfg.hasOwnProperty('snapbackSpeed') !== true ||
394       validAnimationSpeed(cfg.snapbackSpeed) !== true) {
395     cfg.snapbackSpeed = 50;
396   }
397   if (cfg.hasOwnProperty('snapSpeed') !== true ||
398       validAnimationSpeed(cfg.snapSpeed) !== true) {
399     cfg.snapSpeed = 25;
400   }
401   if (cfg.hasOwnProperty('trashSpeed') !== true ||
402       validAnimationSpeed(cfg.trashSpeed) !== true) {
403     cfg.trashSpeed = 100;
404   }
405
406   // make sure position is valid
407   if (cfg.hasOwnProperty('position') === true) {
408     if (cfg.position === 'start') {
409       CURRENT_POSITION = deepCopy(START_POSITION);
410     }
411
412     else if (validFen(cfg.position) === true) {
413       CURRENT_POSITION = fenToObj(cfg.position);
414     }
415
416     else if (validPositionObject(cfg.position) === true) {
417       CURRENT_POSITION = deepCopy(cfg.position);
418     }
419
420     else {
421       error(7263, 'Invalid value passed to config.position.', cfg.position);
422     }
423   }
424
425   return true;
426 }
427
428 //------------------------------------------------------------------------------
429 // DOM Misc
430 //------------------------------------------------------------------------------
431
432 // calculates square size based on the width of the container
433 // got a little CSS black magic here, so let me explain:
434 // get the width of the container element (could be anything), reduce by 1 for
435 // fudge factor, and then keep reducing until we find an exact mod 8 for
436 // our square size
437 function calculateSquareSize() {
438   var containerWidth = parseInt(getComputedStyle(containerEl).width, 10);
439
440   // defensive, prevent infinite loop
441   if (! containerWidth || containerWidth <= 0) {
442     return 0;
443   }
444
445   // pad one pixel
446   var boardWidth = containerWidth - 1;
447
448   while (boardWidth % 8 !== 0 && boardWidth > 0) {
449     boardWidth--;
450   }
451
452   return (boardWidth / 8);
453 }
454
455 // create random IDs for elements
456 function createElIds() {
457   // squares on the board
458   for (var i = 0; i < COLUMNS.length; i++) {
459     for (var j = 1; j <= 8; j++) {
460       var square = COLUMNS[i] + j;
461       SQUARE_ELS_IDS[square] = square + '-' + createId();
462     }
463   }
464 }
465
466 //------------------------------------------------------------------------------
467 // Markup Building
468 //------------------------------------------------------------------------------
469
470 function buildBoardContainer() {
471   var html = '<div class="' + CSS.chessboard + '">';
472
473   html += '<div class="' + CSS.board + '"></div>';
474
475   html += '</div>';
476
477   return html;
478 }
479
480 /*
481 var buildSquare = function(color, size, id) {
482   var html = '<div class="' + CSS.square + ' ' + CSSColor[color] + '" ' +
483   'style="width: ' + size + 'px; height: ' + size + 'px" ' +
484   'id="' + id + '">';
485
486   if (cfg.showNotation === true) {
487
488   }
489
490   html += '</div>';
491
492   return html;
493 };
494 */
495
496 function buildBoard(orientation) {
497   if (orientation !== 'black') {
498     orientation = 'white';
499   }
500
501   var html = '';
502
503   // algebraic notation / orientation
504   var alpha = deepCopy(COLUMNS);
505   var row = 8;
506   if (orientation === 'black') {
507     alpha.reverse();
508     row = 1;
509   }
510
511   var squareColor = 'white';
512   for (var i = 0; i < 8; i++) {
513     html += '<div class="' + CSS.row + '">';
514     for (var j = 0; j < 8; j++) {
515       var square = alpha[j] + row;
516
517       html += '<div class="' + CSS.square + ' ' + CSSColor[squareColor] + ' ' +
518         'square-' + square + '" ' +
519         'style="width: ' + SQUARE_SIZE + 'px; height: ' + SQUARE_SIZE + 'px" ' +
520         'id="' + SQUARE_ELS_IDS[square] + '" ' +
521         'data-square="' + square + '">';
522
523       if (cfg.showNotation === true) {
524         // alpha notation
525         if ((orientation === 'white' && row === 1) ||
526             (orientation === 'black' && row === 8)) {
527           html += '<div class="' + CSS.notation + ' ' + CSS.alpha + '">' +
528             alpha[j] + '</div>';
529         }
530
531         // numeric notation
532         if (j === 0) {
533           html += '<div class="' + CSS.notation + ' ' + CSS.numeric + '">' +
534             row + '</div>';
535         }
536       }
537
538       html += '</div>'; // end .square
539
540       squareColor = (squareColor === 'white' ? 'black' : 'white');
541     }
542     html += '<div class="' + CSS.clearfix + '"></div></div>';
543
544     squareColor = (squareColor === 'white' ? 'black' : 'white');
545
546     if (orientation === 'white') {
547       row--;
548     }
549     else {
550       row++;
551     }
552   }
553
554   return html;
555 }
556
557 function buildPieceImgSrc(piece) {
558   if (typeof cfg.pieceTheme === 'function') {
559     return cfg.pieceTheme(piece);
560   }
561
562   if (typeof cfg.pieceTheme === 'string') {
563     return cfg.pieceTheme.replace(/{piece}/g, piece);
564   }
565
566   // NOTE: this should never happen
567   error(8272, 'Unable to build image source for cfg.pieceTheme.');
568   return '';
569 }
570
571 /**
572  * @param {!string} piece
573  * @param {boolean=} hidden
574  * @param {!string=} id
575  */
576 function buildPiece(piece, hidden, id) {
577   let img = document.createElement('img');
578   img.src = buildPieceImgSrc(piece);
579   if (id && typeof id === 'string') {
580     img.setAttribute('id', id);
581   }
582   img.setAttribute('alt', '');
583   img.classList.add(CSS.piece);
584   img.setAttribute('data-piece', piece);
585   img.style.width = SQUARE_SIZE + 'px';
586   img.style.height = SQUARE_SIZE + 'px';
587   if (hidden === true) {
588     img.style.display = 'none';
589   }
590   return img;
591 }
592
593 //------------------------------------------------------------------------------
594 // Animations
595 //------------------------------------------------------------------------------
596
597 function offset(el) {  // From https://youmightnotneedjquery.com/.
598   let box = el.getBoundingClientRect();
599   let docElem = document.documentElement;
600   return {
601     top: box.top + window.pageYOffset - docElem.clientTop,
602     left: box.left + window.pageXOffset - docElem.clientLeft
603   };
604 }
605
606 function animateSquareToSquare(src, dest, piece, completeFn) {
607   // get information about the source and destination squares
608   var srcSquareEl = document.getElementById(SQUARE_ELS_IDS[src]);
609   var srcSquarePosition = offset(srcSquareEl);
610   var destSquareEl = document.getElementById(SQUARE_ELS_IDS[dest]);
611   var destSquarePosition = offset(destSquareEl);
612
613   // create the animated piece and absolutely position it
614   // over the source square
615   var animatedPieceId = createId();
616   document.body.append(buildPiece(piece, true, animatedPieceId));
617   var animatedPieceEl = document.getElementById(animatedPieceId);
618   animatedPieceEl.style.display = null;
619   animatedPieceEl.style.position = 'absolute';
620   animatedPieceEl.style.top = srcSquarePosition.top + 'px';
621   animatedPieceEl.style.left = srcSquarePosition.left + 'px';
622
623   // remove original piece(s) from source square
624   // TODO: multiple pieces should never really happen, but it will if we are moving
625   // while another animation still isn't done
626   srcSquareEl.querySelectorAll('.' + CSS.piece).forEach((piece) => piece.remove());
627
628   // on complete
629   var complete = function() {
630     // add the "real" piece to the destination square
631     destSquareEl.append(buildPiece(piece));
632
633     // remove the animated piece
634     animatedPieceEl.remove();
635
636     // run complete function
637     if (typeof completeFn === 'function') {
638       completeFn();
639     }
640   };
641
642   // animate the piece to the destination square
643   animatedPieceEl.addEventListener('transitionend', complete, {once: true});
644   requestAnimationFrame(() => {
645     animatedPieceEl.style.transitionProperty = 'top, left';
646     animatedPieceEl.style.transitionDuration = cfg.moveSpeed + 'ms';
647     animatedPieceEl.style.top = destSquarePosition.top + 'px';
648     animatedPieceEl.style.left = destSquarePosition.left + 'px';
649   });
650 }
651
652 function fadeIn(pieces, onFinish) {
653   pieces.forEach((piece) => {
654     piece.style.opacity = 0;
655     piece.style.display = null;
656     piece.addEventListener('transitionend', onFinish, {once: true});
657   });
658   requestAnimationFrame(() => {
659     pieces.forEach((piece) => {
660       piece.style.transitionProperty = 'opacity';
661       piece.style.transitionDuration = cfg.appearSpeed + 'ms';
662       piece.style.opacity = 1;
663     });
664   });
665 }
666
667 function fadeOut(pieces, onFinish) {
668   pieces.forEach((piece) => {
669     piece.style.opacity = 1;
670     piece.style.display = null;
671     piece.addEventListener('transitionend', onFinish, {once: true});
672   });
673   requestAnimationFrame(() => {
674     pieces.forEach((piece) => {
675       piece.style.transitionProperty = 'opacity';
676       piece.style.transitionDuration = cfg.trashSpeed + 'ms';
677       piece.style.opacity = 0;
678     });
679   });
680 }
681
682 // execute an array of animations
683 function doAnimations(a, oldPos, newPos) {
684   var numFinished = 0;
685   function onFinish(e) {
686     if (e && e.target) {
687       e.target.transitionProperty = null;
688     }
689
690     numFinished++;
691
692     // exit if all the animations aren't finished
693     if (numFinished !== a.length) return;
694
695     drawPositionInstant();
696
697     // run their onMoveEnd function
698     if (cfg.hasOwnProperty('onMoveEnd') === true &&
699       typeof cfg.onMoveEnd === 'function') {
700       cfg.onMoveEnd(deepCopy(oldPos), deepCopy(newPos));
701     }
702   }
703
704   requestAnimationFrame(() => {  // Firefox workaround.
705     let fadeout_pieces = [];
706     let fadein_pieces = [];
707
708     for (var i = 0; i < a.length; i++) {
709       // clear a piece
710       if (a[i].type === 'clear') {
711         document.getElementById(SQUARE_ELS_IDS[a[i].square]).querySelectorAll('.' + CSS.piece).forEach(
712           (piece) => fadeout_pieces.push(piece)
713         );
714       }
715
716       // add a piece
717       if (a[i].type === 'add') {
718         let square = document.getElementById(SQUARE_ELS_IDS[a[i].square]);
719         square.append(buildPiece(a[i].piece, true));
720         let piece = square.querySelector('.' + CSS.piece);
721         fadein_pieces.push(piece);
722       }
723
724       // move a piece
725       if (a[i].type === 'move') {
726         animateSquareToSquare(a[i].source, a[i].destination, a[i].piece,
727           onFinish);
728       }
729     }
730
731     // TODO: Batch moves as well, not just fade in/out.
732     // (We batch them because requestAnimationFrame seemingly costs real time.)
733     if (fadeout_pieces.length > 0) {
734       fadeOut(fadeout_pieces, onFinish);
735     }
736     if (fadein_pieces.length > 0) {
737       fadeIn(fadein_pieces, onFinish);
738     }
739   });
740 }
741
742 // returns the distance between two squares
743 function squareDistance(s1, s2) {
744   s1 = s1.split('');
745   var s1x = COLUMNS.indexOf(s1[0]) + 1;
746   var s1y = parseInt(s1[1], 10);
747
748   s2 = s2.split('');
749   var s2x = COLUMNS.indexOf(s2[0]) + 1;
750   var s2y = parseInt(s2[1], 10);
751
752   var xDelta = Math.abs(s1x - s2x);
753   var yDelta = Math.abs(s1y - s2y);
754
755   if (xDelta >= yDelta) return xDelta;
756   return yDelta;
757 }
758
759 // returns the square of the closest instance of piece
760 // returns false if no instance of piece is found in position
761 function findClosestPiece(position, piece, square) {
762   let best_square = false;
763   let best_dist = 1e9;
764   for (var i = 0; i < COLUMNS.length; i++) {
765     for (var j = 1; j <= 8; j++) {
766       let other_square = COLUMNS[i] + j;
767
768       if (position[other_square] === piece && square != other_square) {
769         let dist = squareDistance(square, other_square);
770         if (dist < best_dist) {
771           best_square = other_square;
772           best_dist = dist;
773         }
774       }
775     }
776   }
777
778   return best_square;
779 }
780
781 // calculate an array of animations that need to happen in order to get
782 // from pos1 to pos2
783 function calculateAnimations(pos1, pos2) {
784   // make copies of both
785   pos1 = deepCopy(pos1);
786   pos2 = deepCopy(pos2);
787
788   var animations = [];
789   var squaresMovedTo = {};
790
791   // remove pieces that are the same in both positions
792   for (var i in pos2) {
793     if (pos2.hasOwnProperty(i) !== true) continue;
794
795     if (pos1.hasOwnProperty(i) === true && pos1[i] === pos2[i]) {
796       delete pos1[i];
797       delete pos2[i];
798     }
799   }
800
801   // find all the "move" animations
802   for (var i in pos2) {
803     if (pos2.hasOwnProperty(i) !== true) continue;
804
805     var closestPiece = findClosestPiece(pos1, pos2[i], i);
806     if (closestPiece !== false) {
807       animations.push({
808         type: 'move',
809         source: closestPiece,
810         destination: i,
811         piece: pos2[i]
812       });
813
814       delete pos1[closestPiece];
815       delete pos2[i];
816       squaresMovedTo[i] = true;
817     }
818   }
819
820   // add pieces to pos2
821   for (var i in pos2) {
822     if (pos2.hasOwnProperty(i) !== true) continue;
823
824     animations.push({
825       type: 'add',
826       square: i,
827       piece: pos2[i]
828     })
829
830     delete pos2[i];
831   }
832
833   // clear pieces from pos1
834   for (var i in pos1) {
835     if (pos1.hasOwnProperty(i) !== true) continue;
836
837     // do not clear a piece if it is on a square that is the result
838     // of a "move", ie: a piece capture
839     if (squaresMovedTo.hasOwnProperty(i) === true) continue;
840
841     animations.push({
842       type: 'clear',
843       square: i,
844       piece: pos1[i]
845     });
846
847     delete pos1[i];
848   }
849
850   return animations;
851 }
852
853 //------------------------------------------------------------------------------
854 // Control Flow
855 //------------------------------------------------------------------------------
856
857 function drawPositionInstant() {
858   // clear the board
859   boardEl.querySelectorAll('.' + CSS.piece).forEach((piece) => piece.remove());
860
861   // add the pieces
862   for (var i in CURRENT_POSITION) {
863     if (CURRENT_POSITION.hasOwnProperty(i) !== true) continue;
864
865     document.getElementById(SQUARE_ELS_IDS[i]).append(buildPiece(CURRENT_POSITION[i]));
866   }
867 }
868
869 function drawBoard() {
870   boardEl.innerHTML = buildBoard(CURRENT_ORIENTATION);
871   drawPositionInstant();
872 }
873
874 // given a position and a set of moves, return a new position
875 // with the moves executed
876 function calculatePositionFromMoves(position, moves) {
877   position = deepCopy(position);
878
879   for (var i in moves) {
880     if (moves.hasOwnProperty(i) !== true) continue;
881
882     // skip the move if the position doesn't have a piece on the source square
883     if (position.hasOwnProperty(i) !== true) continue;
884
885     var piece = position[i];
886     delete position[i];
887     position[moves[i]] = piece;
888   }
889
890   return position;
891 }
892
893 function setCurrentPosition(position) {
894   var oldPos = deepCopy(CURRENT_POSITION);
895   var newPos = deepCopy(position);
896   var oldFen = objToFen(oldPos);
897   var newFen = objToFen(newPos);
898
899   // do nothing if no change in position
900   if (oldFen === newFen) return;
901
902   // run their onChange function
903   if (cfg.hasOwnProperty('onChange') === true &&
904     typeof cfg.onChange === 'function') {
905     cfg.onChange(oldPos, newPos);
906   }
907
908   // update state
909   CURRENT_POSITION = position;
910 }
911
912 function isXYOnSquare(x, y) {
913   for (var i in SQUARE_ELS_OFFSETS) {
914     if (SQUARE_ELS_OFFSETS.hasOwnProperty(i) !== true) continue;
915
916     var s = SQUARE_ELS_OFFSETS[i];
917     if (x >= s.left && x < s.left + SQUARE_SIZE &&
918         y >= s.top && y < s.top + SQUARE_SIZE) {
919       return i;
920     }
921   }
922
923   return 'offboard';
924 }
925
926 // records the XY coords of every square into memory
927 function captureSquareOffsets() {
928   SQUARE_ELS_OFFSETS = {};
929
930   for (var i in SQUARE_ELS_IDS) {
931     if (SQUARE_ELS_IDS.hasOwnProperty(i) !== true) continue;
932
933     SQUARE_ELS_OFFSETS[i] = offset(document.getElementById(SQUARE_ELS_IDS[i]));
934   }
935 }
936
937 function removeSquareHighlights() {
938   boardEl.querySelectorAll('.' + CSS.square).forEach((piece) => {
939     piece.classList.remove(CSS.highlight1);
940     piece.classList.remove(CSS.highlight2);
941   });
942 }
943
944 function snapbackDraggedPiece() {
945   removeSquareHighlights();
946
947   // animation complete
948   function complete() {
949     drawPositionInstant();
950     draggedPieceEl.style.display = 'none';
951
952     // run their onSnapbackEnd function
953     if (cfg.hasOwnProperty('onSnapbackEnd') === true &&
954       typeof cfg.onSnapbackEnd === 'function') {
955       cfg.onSnapbackEnd(DRAGGED_PIECE, DRAGGED_PIECE_SOURCE,
956         deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION);
957     }
958   }
959
960   // get source square position
961   var sourceSquarePosition =
962     offset(document.getElementById(SQUARE_ELS_IDS[DRAGGED_PIECE_SOURCE]));
963
964   // animate the piece to the target square
965   draggedPieceEl.addEventListener('transitionend', complete, {once: true});
966   requestAnimationFrame(() => {
967     draggedPieceEl.style.transitionProperty = 'top, left';
968     draggedPieceEl.style.transitionDuration = cfg.snapbackSpeed + 'ms';
969     draggedPieceEl.style.top = sourceSquarePosition.top + 'px';
970     draggedPieceEl.style.left = sourceSquarePosition.left + 'px';
971   });
972
973   // set state
974   DRAGGING_A_PIECE = false;
975 }
976
977 function dropDraggedPieceOnSquare(square) {
978   removeSquareHighlights();
979
980   // update position
981   var newPosition = deepCopy(CURRENT_POSITION);
982   delete newPosition[DRAGGED_PIECE_SOURCE];
983   newPosition[square] = DRAGGED_PIECE;
984   setCurrentPosition(newPosition);
985
986   // get target square information
987   var targetSquarePosition = offset(document.getElementById(SQUARE_ELS_IDS[square]));
988
989   // animation complete
990   var complete = function() {
991     drawPositionInstant();
992     draggedPieceEl.style.display = 'none';
993
994     // execute their onSnapEnd function
995     if (cfg.hasOwnProperty('onSnapEnd') === true &&
996       typeof cfg.onSnapEnd === 'function') {
997       requestAnimationFrame(() => {  // HACK: so that we don't add event handlers from the callback...
998         cfg.onSnapEnd(DRAGGED_PIECE_SOURCE, square, DRAGGED_PIECE);
999       });
1000     }
1001   };
1002
1003   // snap the piece to the target square
1004   draggedPieceEl.addEventListener('transitionend', complete, {once: true});
1005   requestAnimationFrame(() => {
1006     draggedPieceEl.style.transitionProperty = 'top, left';
1007     draggedPieceEl.style.transitionDuration = cfg.snapSpeed + 'ms';
1008     draggedPieceEl.style.top = targetSquarePosition.top + 'px';
1009     draggedPieceEl.style.left = targetSquarePosition.left + 'px';
1010   });
1011
1012   // set state
1013   DRAGGING_A_PIECE = false;
1014 }
1015
1016 function beginDraggingPiece(source, piece, x, y) {
1017   // run their custom onDragStart function
1018   // their custom onDragStart function can cancel drag start
1019   if (typeof cfg.onDragStart === 'function' &&
1020       cfg.onDragStart(source, piece,
1021         deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION) === false) {
1022     return;
1023   }
1024
1025   // set state
1026   DRAGGING_A_PIECE = true;
1027   DRAGGED_PIECE = piece;
1028   DRAGGED_PIECE_SOURCE = source;
1029   DRAGGED_PIECE_LOCATION = source;
1030
1031   // capture the x, y coords of all squares in memory
1032   captureSquareOffsets();
1033
1034   // create the dragged piece
1035   draggedPieceEl.setAttribute('src', buildPieceImgSrc(piece));
1036   draggedPieceEl.style.display = null;
1037   draggedPieceEl.style.position = 'absolute';
1038   draggedPieceEl.style.left = (x - (SQUARE_SIZE / 2)) + 'px';
1039   draggedPieceEl.style.top = (y - (SQUARE_SIZE / 2)) + 'px';
1040
1041   // highlight the source square and hide the piece
1042   let square = document.getElementById(SQUARE_ELS_IDS[source]);
1043   square.classList.add(CSS.highlight1);
1044   square.querySelector('.' + CSS.piece).style.display = 'none';
1045 }
1046
1047 function updateDraggedPiece(x, y) {
1048   // put the dragged piece over the mouse cursor
1049   draggedPieceEl.style.left = (x - (SQUARE_SIZE / 2)) + 'px';
1050   draggedPieceEl.style.top = (y - (SQUARE_SIZE / 2)) + 'px';
1051
1052   // get location
1053   var location = isXYOnSquare(x, y);
1054
1055   // do nothing if the location has not changed
1056   if (location === DRAGGED_PIECE_LOCATION) return;
1057
1058   // remove highlight from previous square
1059   if (validSquare(DRAGGED_PIECE_LOCATION) === true) {
1060     document.getElementById(SQUARE_ELS_IDS[DRAGGED_PIECE_LOCATION])
1061       .classList.remove(CSS.highlight2);
1062   }
1063
1064   // add highlight to new square
1065   if (validSquare(location) === true) {
1066     document.getElementById(SQUARE_ELS_IDS[location]).classList.add(CSS.highlight2);
1067   }
1068
1069   // run onDragMove
1070   if (typeof cfg.onDragMove === 'function') {
1071     cfg.onDragMove(location, DRAGGED_PIECE_LOCATION,
1072       DRAGGED_PIECE_SOURCE, DRAGGED_PIECE,
1073       deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION);
1074   }
1075
1076   // update state
1077   DRAGGED_PIECE_LOCATION = location;
1078 }
1079
1080 function stopDraggedPiece(location) {
1081   // determine what the action should be
1082   var action = 'drop';
1083   if (location === 'offboard' && cfg.dropOffBoard === 'snapback') {
1084     action = 'snapback';
1085   }
1086
1087   // run their onDrop function, which can potentially change the drop action
1088   if (cfg.hasOwnProperty('onDrop') === true &&
1089     typeof cfg.onDrop === 'function') {
1090     var newPosition = deepCopy(CURRENT_POSITION);
1091
1092     // source piece was on the board and position is off the board
1093     if (validSquare(DRAGGED_PIECE_SOURCE) === true && location === 'offboard') {
1094       // remove the piece from the board
1095       delete newPosition[DRAGGED_PIECE_SOURCE];
1096     }
1097
1098     // source piece was on the board and position is on the board
1099     if (validSquare(DRAGGED_PIECE_SOURCE) === true &&
1100       validSquare(location) === true) {
1101       // move the piece
1102       delete newPosition[DRAGGED_PIECE_SOURCE];
1103       newPosition[location] = DRAGGED_PIECE;
1104     }
1105
1106     var oldPosition = deepCopy(CURRENT_POSITION);
1107
1108     var result = cfg.onDrop(DRAGGED_PIECE_SOURCE, location, DRAGGED_PIECE,
1109       newPosition, oldPosition, CURRENT_ORIENTATION);
1110     if (result === 'snapback') {
1111       action = result;
1112     }
1113   }
1114
1115   // do it!
1116   if (action === 'snapback') {
1117     snapbackDraggedPiece();
1118   }
1119   else if (action === 'drop') {
1120     dropDraggedPieceOnSquare(location);
1121   }
1122 }
1123
1124 //------------------------------------------------------------------------------
1125 // Public Methods
1126 //------------------------------------------------------------------------------
1127
1128 // clear the board
1129 widget.clear = function(useAnimation) {
1130   widget.position({}, useAnimation);
1131 };
1132
1133 /*
1134 // get or set config properties
1135 // TODO: write this, GitHub Issue #1
1136 widget.config = function(arg1, arg2) {
1137   // get the current config
1138   if (arguments.length === 0) {
1139     return deepCopy(cfg);
1140   }
1141 };
1142 */
1143
1144 // remove the widget from the page
1145 widget.destroy = function() {
1146   // remove markup
1147   containerEl.innerHTML = '';
1148   draggedPieceEl.remove();
1149 };
1150
1151 // shorthand method to get the current FEN
1152 widget.fen = function() {
1153   return widget.position('fen');
1154 };
1155
1156 // flip orientation
1157 widget.flip = function() {
1158   widget.orientation('flip');
1159 };
1160
1161 /*
1162 // TODO: write this, GitHub Issue #5
1163 widget.highlight = function() {
1164
1165 };
1166 */
1167
1168 // move pieces
1169 widget.move = function() {
1170   // no need to throw an error here; just do nothing
1171   if (arguments.length === 0) return;
1172
1173   var useAnimation = true;
1174
1175   // collect the moves into an object
1176   var moves = {};
1177   for (var i = 0; i < arguments.length; i++) {
1178     // any "false" to this function means no animations
1179     if (arguments[i] === false) {
1180       useAnimation = false;
1181       continue;
1182     }
1183
1184     // skip invalid arguments
1185     if (validMove(arguments[i]) !== true) {
1186       error(2826, 'Invalid move passed to the move method.', arguments[i]);
1187       continue;
1188     }
1189
1190     var tmp = arguments[i].split('-');
1191     moves[tmp[0]] = tmp[1];
1192   }
1193
1194   // calculate position from moves
1195   var newPos = calculatePositionFromMoves(CURRENT_POSITION, moves);
1196
1197   // update the board
1198   widget.position(newPos, useAnimation);
1199
1200   // return the new position object
1201   return newPos;
1202 };
1203
1204 widget.orientation = function(arg) {
1205   // no arguments, return the current orientation
1206   if (arguments.length === 0) {
1207     return CURRENT_ORIENTATION;
1208   }
1209
1210   // set to white or black
1211   if (arg === 'white' || arg === 'black') {
1212     CURRENT_ORIENTATION = arg;
1213     drawBoard();
1214     return;
1215   }
1216
1217   // flip orientation
1218   if (arg === 'flip') {
1219     CURRENT_ORIENTATION = (CURRENT_ORIENTATION === 'white') ? 'black' : 'white';
1220     drawBoard();
1221     return;
1222   }
1223
1224   error(5482, 'Invalid value passed to the orientation method.', arg);
1225 };
1226
1227 /**
1228  * @param {!string|!Object} position
1229  * @param {boolean=} useAnimation
1230  */
1231 widget.position = function(position, useAnimation) {
1232   // no arguments, return the current position
1233   if (arguments.length === 0) {
1234     return deepCopy(CURRENT_POSITION);
1235   }
1236
1237   // get position as FEN
1238   if (typeof position === 'string' && position.toLowerCase() === 'fen') {
1239     return objToFen(CURRENT_POSITION);
1240   }
1241
1242   // default for useAnimations is true
1243   if (useAnimation !== false) {
1244     useAnimation = true;
1245   }
1246
1247   // start position
1248   if (typeof position === 'string' && position.toLowerCase() === 'start') {
1249     position = deepCopy(START_POSITION);
1250   }
1251
1252   // convert FEN to position object
1253   if (validFen(position) === true) {
1254     position = fenToObj(position);
1255   }
1256
1257   // validate position object
1258   if (validPositionObject(position) !== true) {
1259     error(6482, 'Invalid value passed to the position method.', position);
1260     return;
1261   }
1262
1263   if (useAnimation === true) {
1264     // start the animations
1265     doAnimations(calculateAnimations(CURRENT_POSITION, position),
1266       CURRENT_POSITION, position);
1267
1268     // set the new position
1269     setCurrentPosition(position);
1270   }
1271   // instant update
1272   else {
1273     setCurrentPosition(position);
1274     drawPositionInstant();
1275   }
1276 };
1277
1278 widget.resize = function() {
1279   // calulate the new square size
1280   SQUARE_SIZE = calculateSquareSize();
1281
1282   // set board width
1283   boardEl.style.width = (SQUARE_SIZE * 8) + 'px';
1284
1285   // set drag piece size
1286   if (draggedPieceEl !== null) {
1287     draggedPieceEl.style.height = SQUARE_SIZE + 'px';
1288     draggedPieceEl.style.width = SQUARE_SIZE + 'px';
1289   }
1290
1291   // redraw the board
1292   drawBoard();
1293 };
1294
1295 // set the starting position
1296 widget.start = function(useAnimation) {
1297   widget.position('start', useAnimation);
1298 };
1299
1300 //------------------------------------------------------------------------------
1301 // Browser Events
1302 //------------------------------------------------------------------------------
1303
1304 function isTouchDevice() {
1305   return ('ontouchstart' in document.documentElement);
1306 }
1307
1308 function mousedownSquare(e) {
1309   let target = e.target.closest('.' + CSS.square);
1310   if (!target) {
1311     return;
1312   }
1313
1314   // do nothing if we're not draggable
1315   if (cfg.draggable !== true) return;
1316
1317   var square = target.getAttribute('data-square');
1318
1319   // no piece on this square
1320   if (validSquare(square) !== true ||
1321       CURRENT_POSITION.hasOwnProperty(square) !== true) {
1322     return;
1323   }
1324
1325   beginDraggingPiece(square, CURRENT_POSITION[square], e.pageX, e.pageY);
1326 }
1327
1328 function touchstartSquare(e) {
1329   let target = e.target.closest('.' + CSS.square);
1330   if (!target) {
1331     return;
1332   }
1333
1334   // do nothing if we're not draggable
1335   if (cfg.draggable !== true) return;
1336
1337   var square = target.getAttribute('data-square');
1338
1339   // no piece on this square
1340   if (validSquare(square) !== true ||
1341       CURRENT_POSITION.hasOwnProperty(square) !== true) {
1342     return;
1343   }
1344
1345   beginDraggingPiece(square, CURRENT_POSITION[square],
1346     e.changedTouches[0].pageX, e.changedTouches[0].pageY);
1347 }
1348
1349 function mousemoveWindow(e) {
1350   // do nothing if we are not dragging a piece
1351   if (DRAGGING_A_PIECE !== true) return;
1352
1353   updateDraggedPiece(e.pageX, e.pageY);
1354 }
1355
1356 function touchmoveWindow(e) {
1357   // do nothing if we are not dragging a piece
1358   if (DRAGGING_A_PIECE !== true) return;
1359
1360   // prevent screen from scrolling
1361   e.preventDefault();
1362
1363   updateDraggedPiece(e.changedTouches[0].pageX,
1364     e.changedTouches[0].pageY);
1365 }
1366
1367 function mouseupWindow(e) {
1368   // do nothing if we are not dragging a piece
1369   if (DRAGGING_A_PIECE !== true) return;
1370
1371   // get the location
1372   var location = isXYOnSquare(e.pageX, e.pageY);
1373
1374   stopDraggedPiece(location);
1375 }
1376
1377 function touchendWindow(e) {
1378   // do nothing if we are not dragging a piece
1379   if (DRAGGING_A_PIECE !== true) return;
1380
1381   // get the location
1382   var location = isXYOnSquare(e.changedTouches[0].pageX,
1383     e.changedTouches[0].pageY);
1384
1385   stopDraggedPiece(location);
1386 }
1387
1388 function mouseenterSquare(e) {
1389   let target = e.target.closest('.' + CSS.square);
1390   if (!target) {
1391     return;
1392   }
1393
1394   // do not fire this event if we are dragging a piece
1395   // NOTE: this should never happen, but it's a safeguard
1396   if (DRAGGING_A_PIECE !== false) return;
1397
1398   if (cfg.hasOwnProperty('onMouseoverSquare') !== true ||
1399     typeof cfg.onMouseoverSquare !== 'function') return;
1400
1401   // get the square
1402   var square = target.getAttribute('data-square');
1403
1404   // NOTE: this should never happen; defensive
1405   if (validSquare(square) !== true) return;
1406
1407   // get the piece on this square
1408   var piece = false;
1409   if (CURRENT_POSITION.hasOwnProperty(square) === true) {
1410     piece = CURRENT_POSITION[square];
1411   }
1412
1413   // execute their function
1414   cfg.onMouseoverSquare(square, piece, deepCopy(CURRENT_POSITION),
1415     CURRENT_ORIENTATION);
1416 }
1417
1418 function mouseleaveSquare(e) {
1419   let target = e.target.closest('.' + CSS.square);
1420   if (!target) {
1421     return;
1422   }
1423
1424   // do not fire this event if we are dragging a piece
1425   // NOTE: this should never happen, but it's a safeguard
1426   if (DRAGGING_A_PIECE !== false) return;
1427
1428   if (cfg.hasOwnProperty('onMouseoutSquare') !== true ||
1429     typeof cfg.onMouseoutSquare !== 'function') return;
1430
1431   // get the square
1432   var square = target.getAttribute('data-square');
1433
1434   // NOTE: this should never happen; defensive
1435   if (validSquare(square) !== true) return;
1436
1437   // get the piece on this square
1438   var piece = false;
1439   if (CURRENT_POSITION.hasOwnProperty(square) === true) {
1440     piece = CURRENT_POSITION[square];
1441   }
1442
1443   // execute their function
1444   cfg.onMouseoutSquare(square, piece, deepCopy(CURRENT_POSITION),
1445     CURRENT_ORIENTATION);
1446 }
1447
1448 //------------------------------------------------------------------------------
1449 // Initialization
1450 //------------------------------------------------------------------------------
1451
1452 function addEvents() {
1453   // prevent browser "image drag"
1454   let stopDefault = (e) => {
1455     if (e.target.matches('.' + CSS.piece)) {
1456       e.preventDefault();
1457     }
1458   };
1459   document.body.addEventListener('mousedown', stopDefault);
1460   document.body.addEventListener('mousemove', stopDefault);
1461
1462   // mouse drag pieces
1463   boardEl.addEventListener('mousedown', mousedownSquare);
1464
1465   // mouse enter / leave square
1466   boardEl.addEventListener('mouseenter', mouseenterSquare);
1467   boardEl.addEventListener('mouseleave', mouseleaveSquare);
1468
1469   window.addEventListener('mousemove', mousemoveWindow);
1470   window.addEventListener('mouseup', mouseupWindow);
1471
1472   // touch drag pieces
1473   if (isTouchDevice() === true) {
1474     boardEl.addEventListener('touchstart', touchstartSquare);
1475     window.addEventListener('touchmove', touchmoveWindow);
1476     window.addEventListener('touchend', touchendWindow);
1477   }
1478 }
1479
1480 function initDom() {
1481   // build board and save it in memory
1482   containerEl.innerHTML = buildBoardContainer();
1483   boardEl = containerEl.querySelector('.' + CSS.board);
1484
1485   // create the drag piece
1486   var draggedPieceId = createId();
1487   document.body.append(buildPiece('wP', true, draggedPieceId));
1488   draggedPieceEl = document.getElementById(draggedPieceId);
1489
1490   // get the border size
1491   BOARD_BORDER_SIZE = parseInt(boardEl.style.borderLeftWidth, 10);
1492
1493   // set the size and draw the board
1494   widget.resize();
1495 }
1496
1497 function init() {
1498   if (checkDeps() !== true ||
1499       expandConfig() !== true) return;
1500
1501   // create unique IDs for all the elements we will create
1502   createElIds();
1503
1504   initDom();
1505   addEvents();
1506 }
1507
1508 // go time
1509 init();
1510
1511 // return the widget object
1512 return widget;
1513
1514 }; // end window.ChessBoard
1515
1516 // expose util functions
1517 window.ChessBoard.fenToObj = fenToObj;
1518 window.ChessBoard.objToFen = objToFen;
1519
1520 })(); // end anonymous wrapper