diff --git a/.gitignore b/.gitignore index f31703fb..bf1fea48 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,8 @@ dist/ node_modules/ +website/examples/ website/*.html -website/css/chessboard.css -website/js/chessboard.js -website/examples/ +.DS_Store + diff --git a/Gulpfile.js b/Gulpfile.js new file mode 100644 index 00000000..d01b8371 --- /dev/null +++ b/Gulpfile.js @@ -0,0 +1,266 @@ +import { writeFile, readFile, rm, mkdir } from 'node:fs/promises'; + +import gulp from 'gulp'; +import kidif from 'kidif'; +import mustache from 'mustache'; +import sass from 'sass'; +import { rollup } from 'rollup'; +import { terser } from 'rollup-plugin-terser'; + +const { version } = JSON.parse(await readFile('./package.json', 'utf-8')); +const banner = `/** @preserve +* chessboard.js v${version} +* https://github.com/oakmac/chessboardjs/ +* +* Copyright (c) 2019, Chris Oakman +* Released under the MIT license +* https://github.com/oakmac/chessboardjs/blob/master/LICENSE.md +*/`; + +export const build = gulp.series(resetDistFolder, gulp.parallel(buildJs, buildCss), buildWebsite); +export const watch = () => gulp.watch(['data', 'examples', 'lib', 'templates', 'website/css', 'website/js'], { ignoreInitial: false }, build); +export default build; + +async function resetDistFolder() { + await rm('./dist', { recursive: true, force: true }); + await mkdir('./dist'); +} + +async function buildJs() { + const bundle = await rollup({ input: './lib/chessboard.js' }); + + const format = /** @type {import('rollup').ModuleFormat} */('iife'); + const output = { banner, format, name: 'ChessboardJS', outro: `Object.assign(window, exports);` }; + + return Promise.all([ + bundle.write({ file: `./dist/chessboard-${version}.js`, ...output }), + bundle.write({ file: `./dist/chessboard-${version}.min.js`, plugins: [terser()], ...output }), + ]); +} + +async function buildCss() { + return Promise.all([ + writeFile(`./dist/chessboard-${version}.css`, banner + '\n' + sass.compile('lib/chessboard.css').css), + writeFile(`./dist/chessboard-${version}.min.css`, banner + '\n' + sass.compile('lib/chessboard.css', { style: 'compressed' }).css), + ]); +} + +async function buildWebsite() { + const chessboardJsScript = ``; + const docs = JSON.parse(await readFile('./data/docs.json', 'utf-8')); + const examples = /** @type {{ id: number, name: string, description: string, group: string, html: string, js: string, chessboardJsScript: string, includeChessJS?: boolean }[]} */(kidif('examples/*.example')); + const groups = ['Basic Usage', 'Config', 'Methods', 'Events', 'Integration']; + const examplesByGroup = /** @type {Record} */({ + [groups.at(0) ?? '']: [], + [groups.at(1) ?? '']: [], + [groups.at(2) ?? '']: [], + [groups.at(3) ?? '']: [], + [groups.at(4) ?? '']: [], + }); + + for (const example of examples) { + example.id = Number(example.id); + example.chessboardJsScript = chessboardJsScript; + + if (!example.id) continue; + else if (example.id >= 5000) { + example.includeChessJS = true; + example.group = groups.at(4) ?? ''; + } + else if (example.id >= 4000) example.group = groups.at(3) ?? ''; + else if (example.id >= 3000) example.group = groups.at(2) ?? ''; + else if (example.id >= 2000) example.group = groups.at(1) ?? ''; + else if (example.id >= 1000) example.group = groups.at(0) ?? ''; + + examplesByGroup[example.group].push(example); + } + + const encoding = 'utf-8'; + const [ + headTemplate, + docsTemplate, + downloadTemplate, + examplesTemplate, + homepageTemplate, + singleExampleTemplate, + licensePageTemplate, + headerTemplate, + footerTemplate, + ] = await Promise.all([ + readFile('templates/_head.mustache', encoding), + readFile('templates/docs.mustache', encoding), + readFile('templates/download.mustache', encoding), + readFile('templates/examples.mustache', encoding), + readFile('templates/homepage.mustache', encoding), + readFile('templates/single-example.mustache', encoding), + readFile('templates/license.mustache', encoding), + readFile('templates/_header.mustache', encoding), + readFile('templates/_footer.mustache', encoding), + ]); + + await rm('./website/examples', { recursive: true, force: true }); + await mkdir('./website/examples'); + + await Promise.all([ + writeFile('website/index.html', mustache.render(homepageTemplate, { + chessboardJsScript, + example2: ` + const board2 = new Chessboard('board2', { + draggable: true, + dropOffBoard: 'trash', + sparePieces: true + }) + document.getElementById('startBtn') + .onclick = () => board2.start() + document.getElementById('clearBtn') + .onclick = () => board2.clear() + `.trim().replace(/^\s{8}/mg, ''), + footer: footerTemplate, + head: mustache.render(headTemplate, { pageTitle: 'Homepage', version }), + version + })), + + writeFile('website/examples.html', mustache.render(examplesTemplate, { + chessboardJsScript, + examplesJavaScript: buildExamplesJS(), + footer: footerTemplate, + head: mustache.render(headTemplate, { pageTitle: 'Examples', version }), + header: mustache.render(headerTemplate, { examplesActive: true, version }), + nav: groups.reduce((html, group, i) => { + const groupNum = i + 1; + html += `
${group}
'); + }, ''), + version + })), + + writeFile('website/docs.html', mustache.render(docsTemplate, { + configTableRows: docs.config.reduce(function (html, prop) { + if (typeof prop === 'string') return html; + + html += ``; // table row + html += `

${prop.name}

${buildTypeHTML(prop.type)}

`; // property and type + html += `

${prop.default || 'n/a'}

`; // default + html += `${buildDescriptionHTML(prop.desc)}`; // description + html += `${buildExamplesCellHTML(prop.examples)}`; // examples + + return html + ''; + }, ''), + errorRows: docs.errors.reduce((html, error) => { + if (typeof error === 'string') return html; + + html += ``; // table row + html += `

${error.id}

`; // id + html += `

${error.desc}

`; // desc + + // more information + if (error.fix) { + html += `${Array.isArray(error.fix) ? error.fix.map(p => `

${p}

`).join('') : `

${error.fix}

`}`; + } else { + html += 'n/a'; + } + + return html + ''; + }, ''), + methodTableRows: docs.methods.reduce((html, method) => { + if (typeof method === 'string') return html; + + const nameNoParens = method.name.replace(/\(.+$/, ''); + + html += method.noId ? '' : ``; // table row + html += `

${method.name}

`; // name + html += Array.isArray(method.args) ? `${method.args.map((arg) => '

' + arg[1] + '

').join('')}` : 'none'; // args + html += `${buildDescriptionHTML(method.desc)}`; // description + html += `${buildExamplesCellHTML(method.examples)}`; // examples + + return html + ''; + }, ''), + footer: footerTemplate, + head: mustache.render(headTemplate, { pageTitle: 'Documentation', version }), + header: mustache.render(headerTemplate, { docsActive: true, version }), + version, + })), + + writeFile('website/download.html', mustache.render(downloadTemplate, { + footer: footerTemplate, + head: mustache.render(headTemplate, { pageTitle: 'Download', version }), + header: mustache.render(headerTemplate, { downloadActive: true, version }), + version + })), + + writeFile('website/license.html', mustache.render(licensePageTemplate, { version })), + + Promise.all(examples.map(example => { + return writeFile(`website/examples/${example.id}.html`, mustache.render(singleExampleTemplate, { version, ...example })); + })), + ]); + + // ----------------------------------------------------------------------------- + // HTML + // ----------------------------------------------------------------------------- + + function buildExamplesJS() { + let txt = 'window.CHESSBOARD_EXAMPLES = {}\n\n'; + + examples.forEach(function (ex) { + txt += 'CHESSBOARD_EXAMPLES["' + ex.id + '"] = {\n' + + ' description: ' + JSON.stringify(ex.description) + ',\n' + + ' html: ' + JSON.stringify(ex.html) + ',\n' + + ' name: ' + JSON.stringify(ex.name) + ',\n' + + ' jsStr: ' + JSON.stringify(ex.js) + ',\n' + + ' jsFn: function () {\n' + ex.js + '\n }\n' + + '};\n\n'; + }); + + return txt; + } + + function buildTypeHTML(type) { + if (!Array.isArray(type)) { + type = [type]; + } + + let html = ''; + for (var i = 0; i < type.length; i++) { + if (i !== 0) { + html += ' or
'; + } + html += type[i]; + } + + return html; + } + + function buildDescriptionHTML(desc) { + if (!Array.isArray(desc)) { + desc = [desc]; + } + + let html = ''; + desc.forEach(function (d) { + html += '

' + d + '

'; + }); + + return html; + } + + function buildExamplesCellHTML(examplesIds) { + if (!Array.isArray(examplesIds)) { + examplesIds = [examplesIds]; + } + + let html = ''; + examplesIds.forEach(function (exampleId) { + const example = examples.find(x => x.id === exampleId); + if (!example) return; + html += '

' + example.name + '

'; + }); + + return html; + } +} diff --git a/README.md b/README.md index 70df69f5..24015102 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # chessboard.js -chessboard.js is a JavaScript chessboard component. It depends on [jQuery] v3.4.1 (or higher). +chessboard.js is a _zero-dependency_ JavaScript chessboard component. Please see [chessboardjs.com] for documentation and examples. @@ -53,7 +53,6 @@ npm run website [MIT License](LICENSE.md) -[jQuery]:https://jquery.com/ [chessboardjs.com]:https://chessboardjs.com [chess.js]:https://github.com/jhlywa/chess.js [Example 5000]:https://chessboardjs.com/examples#5000 diff --git a/data/docs.json b/data/docs.json index a6e1392d..9c0ee9fe 100644 --- a/data/docs.json +++ b/data/docs.json @@ -426,18 +426,13 @@ "errors":[ { "id":1001, - "desc":"The first argument to Chessboard() cannot be an empty string.", - "fix":"The first argument to the Chessboard() constructor should be the id of a DOM element or a reference to a single DOM element." + "desc":"The first argument to new Chessboard() cannot be an empty string.", + "fix":"The first argument to the new Chessboard() constructor should be the id of a DOM element or a reference to a single DOM element." }, { "id":1003, - "desc":"The first argument to Chessboard() must be an ID or a single DOM node.", - "fix":"The first argument to the Chessboard() constructor should be the id of a DOM element or a reference to a single DOM element." -}, -{ - "id":1005, - "desc":"Unable to find a valid version of jQuery. Please include jQuery 1.8.3 or higher on the page.", - "fix":"Chessboard requires jQuery version 1.8.3 or higher." + "desc":"The first argument to new Chessboard() must be an ID or a single DOM node.", + "fix":"The first argument to the new Chessboard() constructor should be the id of a DOM element or a reference to a single DOM element." }, { "id":2826, diff --git a/examples/1000-empty-board.example b/examples/1000-empty-board.example index b065604c..32990899 100644 --- a/examples/1000-empty-board.example +++ b/examples/1000-empty-board.example @@ -2,13 +2,13 @@ 1000 ===== Name -Empty Board - +Empty Board + ===== Description Chessboard.js initializes to an empty board with no second argument. - + ===== HTML
- + ===== JS -var board = Chessboard('myBoard') +var board = new Chessboard('myBoard') diff --git a/examples/1001-start-position.example b/examples/1001-start-position.example index d95b132b..ca9aca98 100644 --- a/examples/1001-start-position.example +++ b/examples/1001-start-position.example @@ -1,15 +1,15 @@ ===== id 1001 -===== Name -Start Position - +===== Name +Start Position + ===== Description Pass 'start' as the second argument to initialize the board to the start position. - + ===== HTML
- + ===== JS -var board = Chessboard('myBoard', 'start') +var board = new Chessboard('myBoard', 'start') diff --git a/examples/1002-fen.example b/examples/1002-fen.example index fa7718a2..e9c2dc41 100644 --- a/examples/1002-fen.example +++ b/examples/1002-fen.example @@ -1,16 +1,16 @@ ===== id 1002 -===== Name -FEN String - +===== Name +FEN String + ===== Description Pass a FEN String as the second argument to initialize the board to a specific position. - + ===== HTML
- + ===== JS var ruyLopez = 'r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R' -var board = Chessboard('myBoard', ruyLopez) +var board = new Chessboard('myBoard', ruyLopez) diff --git a/examples/1003-position-object.example b/examples/1003-position-object.example index 6bfa237e..694fd453 100644 --- a/examples/1003-position-object.example +++ b/examples/1003-position-object.example @@ -1,19 +1,19 @@ ===== id 1003 -===== Name -Position Object - -===== Description -Pass a Position Object as the second argument to initialize the board to a specific position. - -===== HTML -
- -===== JS +===== Name +Position Object + +===== Description +Pass a Position Object as the second argument to initialize the board to a specific position. + +===== HTML +
+ +===== JS var position = { d6: 'bK', d4: 'wP', e4: 'wK' } -var board = Chessboard('myBoard', position) +var board = new Chessboard('myBoard', position) diff --git a/examples/1004-multiple-boards.example b/examples/1004-multiple-boards.example index 54c45e6d..7d5ba8bd 100644 --- a/examples/1004-multiple-boards.example +++ b/examples/1004-multiple-boards.example @@ -1,36 +1,36 @@ ===== id 1004 -===== Name -Multiple Boards - -===== Description -You can have multiple boards on the same page. - +===== Name +Multiple Boards + +===== Description +You can have multiple boards on the same page. + ===== CSS .small-board { display: inline-block; margin-right: 5px; width: 200px; } - + ===== HTML
- -===== JS -var board1 = Chessboard('board1', { + +===== JS +var board1 = new Chessboard('board1', { position: 'start', showNotation: false }) -var board2 = Chessboard('board2', { +var board2 = new Chessboard('board2', { position: 'r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R', showNotation: false }) -var board3 = Chessboard('board3', { +var board3 = new Chessboard('board3', { position: 'r1k4r/p2nb1p1/2b4p/1p1n1p2/2PP4/3Q1NB1/1P3PPP/R5K1', showNotation: false }) diff --git a/examples/2000-config-position.example b/examples/2000-config-position.example index ed5ba568..bfc9d409 100644 --- a/examples/2000-config-position.example +++ b/examples/2000-config-position.example @@ -1,17 +1,17 @@ ===== id 2000 -===== Name -Start Position - -===== Description -Set the position property to 'start' to initialize the board to the start position. - -===== HTML -
- -===== JS -var config = { - position: 'start' +===== Name +Start Position + +===== Description +Set the position property to 'start' to initialize the board to the start position. + +===== HTML +
+ +===== JS +var config = { + position: 'start' } -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) diff --git a/examples/2001-config-orientation.example b/examples/2001-config-orientation.example index 1ce4048b..eae0ac38 100644 --- a/examples/2001-config-orientation.example +++ b/examples/2001-config-orientation.example @@ -1,18 +1,18 @@ ===== id 2001 -===== Name -Orientation - -===== Description -Use the orientation property to set board orientation. - -===== HTML -
- -===== JS -var config = { - orientation: 'black', - position: 'start' +===== Name +Orientation + +===== Description +Use the orientation property to set board orientation. + +===== HTML +
+ +===== JS +var config = { + orientation: 'black', + position: 'start' } -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) diff --git a/examples/2002-config-notation.example b/examples/2002-config-notation.example index a4585bed..b575b8dc 100644 --- a/examples/2002-config-notation.example +++ b/examples/2002-config-notation.example @@ -1,18 +1,18 @@ ===== id 2002 -===== Name -Notation - -===== Description -Use the showNotation property to turn board notation on or off. - -===== HTML -
- -===== JS -var config = { - showNotation: false, - position: 'start' +===== Name +Notation + +===== Description +Use the showNotation property to turn board notation on or off. + +===== HTML +
+ +===== JS +var config = { + showNotation: false, + position: 'start' } -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) diff --git a/examples/2003-draggable-snapback.example b/examples/2003-draggable-snapback.example index 194e8580..1614fc96 100644 --- a/examples/2003-draggable-snapback.example +++ b/examples/2003-draggable-snapback.example @@ -1,19 +1,19 @@ ===== id 2003 -===== Name -Draggable Snapback - -===== Description -Set draggable to true to allow drag and drop of pieces. Pieces will return to their original square when dropped off the board (ie: the default for dropOffBoard is 'snapback'). - -===== HTML -
- -===== JS -var config = { - draggable: true, - dropOffBoard: 'snapback', // this is the default - position: 'start' +===== Name +Draggable Snapback + +===== Description +Set draggable to true to allow drag and drop of pieces. Pieces will return to their original square when dropped off the board (ie: the default for dropOffBoard is 'snapback'). + +===== HTML +
+ +===== JS +var config = { + draggable: true, + dropOffBoard: 'snapback', // this is the default + position: 'start' } -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) diff --git a/examples/2004-piece-theme-string.example b/examples/2004-piece-theme-string.example index fe503528..e9fd858d 100644 --- a/examples/2004-piece-theme-string.example +++ b/examples/2004-piece-theme-string.example @@ -15,4 +15,4 @@ var config = { pieceTheme: 'img/chesspieces/alpha/{piece}.png', position: 'start' } -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) diff --git a/examples/2005-animation-speed.example b/examples/2005-animation-speed.example index e76fa249..c95bbeef 100644 --- a/examples/2005-animation-speed.example +++ b/examples/2005-animation-speed.example @@ -13,7 +13,7 @@ You can control animation speeds with the < ===== JS -var board = Chessboard('myBoard', { +const board = new Chessboard('myBoard', { draggable: true, moveSpeed: 'slow', snapbackSpeed: 500, @@ -21,7 +21,9 @@ var board = Chessboard('myBoard', { position: 'start' }) -$('#ruyLopezBtn').on('click', function () { - board.position('r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R') -}) -$('#startBtn').on('click', board.start) +const ruyLopezFen = 'r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R'; + +document.getElementById('ruyLopezBtn') + .onclick = () => board.setPosition(ruyLopezFen) +document.getElementById('startBtn') + .onclick = () => board.start() diff --git a/examples/2006-spare-pieces.example b/examples/2006-spare-pieces.example index 25166835..c533748c 100644 --- a/examples/2006-spare-pieces.example +++ b/examples/2006-spare-pieces.example @@ -13,11 +13,13 @@ Set sparePiecesClear Board ===== JS -var board = Chessboard('myBoard', { +const board = new Chessboard('myBoard', { draggable: true, dropOffBoard: 'trash', sparePieces: true }) -$('#startBtn').on('click', board.start) -$('#clearBtn').on('click', board.clear) +document.getElementById('startBtn') + .onclick = () => board.start() +document.getElementById('clearBtn') + .onclick = () => board.clear() diff --git a/examples/2030-piece-theme-function.example b/examples/2030-piece-theme-function.example index cf132718..61c49486 100644 --- a/examples/2030-piece-theme-function.example +++ b/examples/2030-piece-theme-function.example @@ -25,4 +25,4 @@ var config = { pieceTheme: pieceTheme, position: 'start' } -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) diff --git a/examples/2044-position-fen.example b/examples/2044-position-fen.example index 1d31a14f..560c940b 100644 --- a/examples/2044-position-fen.example +++ b/examples/2044-position-fen.example @@ -14,4 +14,4 @@ You can set the posit var config = { position: 'r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R' } -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) diff --git a/examples/2063-position-object.example b/examples/2063-position-object.example index 6b031000..5ea398ec 100644 --- a/examples/2063-position-object.example +++ b/examples/2063-position-object.example @@ -18,4 +18,4 @@ var config = { e4: 'wK' } }; -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) diff --git a/examples/2082-draggable-trash.example b/examples/2082-draggable-trash.example index 7d31ccee..8bb202a7 100644 --- a/examples/2082-draggable-trash.example +++ b/examples/2082-draggable-trash.example @@ -16,4 +16,4 @@ var config = { dropOffBoard: 'trash', position: 'start' } -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) diff --git a/examples/3000-get-position.example b/examples/3000-get-position.example index 6aadc63d..5ff6ac68 100644 --- a/examples/3000-get-position.example +++ b/examples/3000-get-position.example @@ -1,29 +1,29 @@ ===== id 3000 -===== Name -Get Position - -===== Description -Use the position and fen methods to retrieve the current position of the board. - -===== HTML -
+===== Name +Get Position + +===== Description +Use the position and fen methods to retrieve the current position of the board. + +===== HTML +
- -===== JS + +===== JS var config = { draggable: true, position: 'start' } -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) function clickShowPositionBtn () { console.log('Current position as an Object:') - console.log(board.position()) + console.log(board.position) console.log('Current position as a FEN string:') - console.log(board.fen()) + console.log(board.fen) } -$('#showPositionBtn').on('click', clickShowPositionBtn) +document.getElementById('showPositionBtn').onclick = (evt) => clickShowPositionBtn(evt) diff --git a/examples/3001-set-position.example b/examples/3001-set-position.example index e999f107..7e0d4960 100644 --- a/examples/3001-set-position.example +++ b/examples/3001-set-position.example @@ -1,31 +1,21 @@ ===== id 3001 -===== Name -Set Position - -===== Description -Use the start and position methods to set the board position. - -===== HTML -
+===== Name +Set Position + +===== Description +Use the start and position methods to set the board position. + +===== HTML +
- -===== JS -var board = Chessboard('myBoard') - -$('#setRuyLopezBtn').on('click', function () { - board.position('r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R') -}) -$('#setStartBtn').on('click', board.start) +===== JS +const board = new Chessboard('myBoard') -$('#setRookCheckmateBtn').on('click', function () { - board.position({ - a4: 'bK', - c4: 'wK', - a7: 'wR' - }) -}) +document.getElementById('setRuyLopezBtn').onclick = () => board.setPosition('r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R') +document.getElementById('setStartBtn').onclick = () => board.start() +document.getElementById('setRookCheckmateBtn').onclick = () => board.setPosition({ a4: 'bK', c4: 'wK', a7: 'wR' }) diff --git a/examples/3002-set-position-instant.example b/examples/3002-set-position-instant.example index 08e5ad15..50b21d20 100644 --- a/examples/3002-set-position-instant.example +++ b/examples/3002-set-position-instant.example @@ -1,35 +1,21 @@ ===== id 3002 -===== Name -Set Position Instantly - -===== Description -Pass false as the second argument to the start and position methods to set the board position instantly. - -===== HTML -
+===== Name +Set Position Instantly + +===== Description +Pass false as the second argument to the start and position methods to set the board position instantly. + +===== HTML +
- -===== JS -var board = Chessboard('myBoard') - -$('#setRuyLopezBtn').on('click', function () { - var ruyLopez = 'r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R' - board.position(ruyLopez, false) -}) -$('#setStartBtn').on('click', function () { - board.start(false) -}) +===== JS +const board = new Chessboard('myBoard') -$('#setRookCheckmateBtn').on('click', function () { - var rookCheckmate = { - a4: 'bK', - c4: 'wK', - a7: 'wR' - } - board.position(rookCheckmate, false) -}) +document.getElementById('setRuyLopezBtn').onclick = () => board.setPosition('r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R', false) +document.getElementById('setStartBtn').onclick = () => board.start(false) +document.getElementById('setRookCheckmateBtn').onclick = () => board.setPosition({ a4: 'bK', c4: 'wK', a7: 'wR' }, false) diff --git a/examples/3003-clear.example b/examples/3003-clear.example index d1243b68..eec03646 100644 --- a/examples/3003-clear.example +++ b/examples/3003-clear.example @@ -1,25 +1,24 @@ ===== id 3003 -===== Name -Clear Board - -===== Description -Use the clear method to remove all the pieces from the board. - -===== HTML -
+===== Name +Clear Board + +===== Description +Use the clear method to remove all the pieces from the board. + +===== HTML +
- -===== JS -var board = Chessboard('myBoard', 'start') - -$('#clearBoardBtn').on('click', board.clear) -$('#startPositionBtn').on('click', board.start) +===== JS +const board = new Chessboard('myBoard', 'start') -$('#clearBoardInstantBtn').on('click', function () { - board.clear(false) -}) +document.getElementById('clearBoardBtn') + .onclick = () => board.clear() +document.getElementById('startPositionBtn') + .onclick = () => board.start() +document.getElementById('clearBoardInstantBtn') + .onclick = () => board.clear(false) diff --git a/examples/3004-move-pieces.example b/examples/3004-move-pieces.example index c380527d..15d08cdd 100644 --- a/examples/3004-move-pieces.example +++ b/examples/3004-move-pieces.example @@ -1,27 +1,24 @@ ===== id 3004 -===== Name -Move Pieces - -===== Description -Use the move method to make one or more moves on the board. - -===== HTML -
+===== Name +Move Pieces + +===== Description +Use the move method to make one or more moves on the board. + +===== HTML +
-===== JS -var board = Chessboard('myBoard', 'start') - -$('#move1Btn').on('click', function () { - board.move('e2-e4') -}) - -$('#move2Btn').on('click', function () { - board.move('d2-d4', 'g8-f6') -}) +===== JS +const board = new Chessboard('myBoard', 'start') -$('#startPositionBtn').on('click', board.start) +document.getElementById('move1Btn') + .onclick = () => board.move('e2-e4') +document.getElementById('move2Btn') + .onclick = () => board.move('d2-d4', 'g8-f6') +document.getElementById('startPositionBtn') + .onclick = () => board.start() diff --git a/examples/3005-orientation.example b/examples/3005-orientation.example index b3221564..8be1571b 100644 --- a/examples/3005-orientation.example +++ b/examples/3005-orientation.example @@ -1,34 +1,25 @@ ===== id 3005 -===== Name -Orientation - -===== Description -Use the orientation method to retrieve and set the orientation. Use the flip method to flip orientation. - -===== HTML -
+===== Name +Orientation + +===== Description +Use the orientation method to retrieve and set the orientation. Use the flip method to flip orientation. + +===== HTML +

-===== JS -var ruyLopez = 'r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R' -var board = Chessboard('myBoard', ruyLopez) - -$('#showOrientationBtn').on('click', function () { - console.log('Board orientation is: ' + board.orientation()) -}) - -$('#flipOrientationBtn').on('click', board.flip) - -$('#whiteOrientationBtn').on('click', function () { - board.orientation('white') -}) +===== JS +const ruyLopez = 'r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R' +const board = new Chessboard('myBoard', ruyLopez) -$('#blackOrientationBtn').on('click', function () { - board.orientation('black') -}) +document.getElementById('showOrientationBtn').onclick = () => console.log('Board orientation is: ' + board.orientation()) +document.getElementById('flipOrientationBtn').onclick = () => board.flip() +document.getElementById('whiteOrientationBtn').onclick = () => board.orientation('white') +document.getElementById('blackOrientationBtn').onclick = () => board.orientation('black') diff --git a/examples/3006-destroy.example b/examples/3006-destroy.example index f7f30ed4..bfed7570 100644 --- a/examples/3006-destroy.example +++ b/examples/3006-destroy.example @@ -12,6 +12,6 @@ Use the destroyDestroy Board ===== JS -var board = Chessboard('myBoard', 'start') +const board = new Chessboard('myBoard', 'start') -$('#destroyBtn').on('click', board.destroy) +document.getElementById('destroyBtn').onclick = () => board.destroy() diff --git a/examples/3007-resize.example b/examples/3007-resize.example index 3a4fc1e4..517291e3 100644 --- a/examples/3007-resize.example +++ b/examples/3007-resize.example @@ -12,5 +12,5 @@ Use the resize< ===== JS // NOTE: click "View example in new window." to see the full effect of this example -var board = Chessboard('myBoard', 'start') -$(window).resize(board.resize) +const board = new Chessboard('myBoard', 'start') +window.onresize = () => board.resize() diff --git a/examples/4000-onchange.example b/examples/4000-onchange.example index bc8c60da..6fb3853e 100644 --- a/examples/4000-onchange.example +++ b/examples/4000-onchange.example @@ -1,35 +1,32 @@ ===== id 4000 -===== Name -onChange - -===== Description +===== Name +onChange + +===== Description The onChange event fires when the board position changes. - -===== HTML + +===== HTML
-===== JS -function onChange (oldPos, newPos) { - console.log('Position changed:') - console.log('Old position: ' + Chessboard.objToFen(oldPos)) - console.log('New position: ' + Chessboard.objToFen(newPos)) - console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') -} - -var config = { +===== JS +const board = new Chessboard('myBoard', { draggable: true, position: 'start', - onChange: onChange -} -var board = Chessboard('myBoard', config) - -$('#ruyLopezBtn').on('click', function () { - var ruyLopez = 'r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R' - board.position(ruyLopez) + onChange(oldPos, newPos) { + console.log('Position changed:') + console.log('Old position: ' + Chessboard.objToFen(oldPos)) + console.log('New position: ' + Chessboard.objToFen(newPos)) + console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') + } }) -$('#startPositionBtn').on('click', board.start) +const ruyLopezFen = 'r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R'; + +document.getElementById('ruyLopezBtn') + .onclick = () => board.setPosition(ruyLopezFen) +document.getElementById('startPositionBtn') + .onclick = () => board.start() diff --git a/examples/4001-ondragstart.example b/examples/4001-ondragstart.example index c7f57954..799aecba 100644 --- a/examples/4001-ondragstart.example +++ b/examples/4001-ondragstart.example @@ -26,4 +26,4 @@ var config = { onDragStart: onDragStart, sparePieces: true } -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) diff --git a/examples/4002-ondragstart-prevent-drag.example b/examples/4002-ondragstart-prevent-drag.example index d63cf86a..4b2cd9c5 100644 --- a/examples/4002-ondragstart-prevent-drag.example +++ b/examples/4002-ondragstart-prevent-drag.example @@ -12,20 +12,16 @@ Prevent the drag action by returning false from ===== JS -// only allow pieces to be dragged when the board is oriented -// in their direction -function onDragStart (source, piece, position, orientation) { - if ((orientation === 'white' && piece.search(/^w/) === -1) || - (orientation === 'black' && piece.search(/^b/) === -1)) { - return false - } -} - -var config = { +const board = new Chessboard('myBoard', { draggable: true, position: 'start', - onDragStart: onDragStart -} -var board = Chessboard('myBoard', config) + // only allow pieces to be dragged when the board is oriented in their direction + onDragStart(source, piece, position, orientation) { + if ((orientation === 'white' && piece.search(/^w/) === -1) || + (orientation === 'black' && piece.search(/^b/) === -1)) { + return false + } + } +}) -$('#flipOrientationBtn').on('click', board.flip) +document.getElementById('flipOrientationBtn').onclick = () => board.flip() diff --git a/examples/4003-ondragmove.example b/examples/4003-ondragmove.example index b91e9ffb..2604221f 100644 --- a/examples/4003-ondragmove.example +++ b/examples/4003-ondragmove.example @@ -28,4 +28,4 @@ var config = { onDragMove: onDragMove, sparePieces: true } -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) diff --git a/examples/4004-ondrop.example b/examples/4004-ondrop.example index 0e6c6d32..b013dedd 100644 --- a/examples/4004-ondrop.example +++ b/examples/4004-ondrop.example @@ -27,4 +27,4 @@ var config = { onDrop: onDrop, sparePieces: true } -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) diff --git a/examples/4005-ondrop-snapback.example b/examples/4005-ondrop-snapback.example index 9c578fa0..2c26f56d 100644 --- a/examples/4005-ondrop-snapback.example +++ b/examples/4005-ondrop-snapback.example @@ -23,4 +23,4 @@ var config = { position: 'start', onDrop: onDrop } -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) diff --git a/examples/4006-ondrop-trash.example b/examples/4006-ondrop-trash.example index ff2e6afc..a0f9c665 100644 --- a/examples/4006-ondrop-trash.example +++ b/examples/4006-ondrop-trash.example @@ -23,4 +23,4 @@ var config = { position: 'start', onDrop: onDrop } -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) diff --git a/examples/4011-onsnapbackend.example b/examples/4011-onsnapbackend.example index 62b3aea4..c046dd3a 100644 --- a/examples/4011-onsnapbackend.example +++ b/examples/4011-onsnapbackend.example @@ -24,4 +24,4 @@ var config = { position: 'start', onSnapbackEnd: onSnapbackEnd } -var board = Chessboard('myBoard', config) +var board = new Chessboard('myBoard', config) diff --git a/examples/4012-onmoveend.example b/examples/4012-onmoveend.example index ed79823e..984b4ea3 100644 --- a/examples/4012-onmoveend.example +++ b/examples/4012-onmoveend.example @@ -15,24 +15,23 @@ The onMoveEnd ===== JS -function onMoveEnd (oldPos, newPos) { - console.log('Move animation complete:') - console.log('Old position: ' + Chessboard.objToFen(oldPos)) - console.log('New position: ' + Chessboard.objToFen(newPos)) - console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') -} - -var config = { +const board = new Chessboard('myBoard', { position: 'start', - onMoveEnd: onMoveEnd -} -var board = Chessboard('myBoard', config) - -$('#ruyLopezBtn').on('click', function () { - board.position('r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R') -}) -$('#moveBtn').on('click', function () { - board.move('a2-a4', 'h7-h5') + onMoveEnd(oldPos, newPos) { + console.log('Move animation complete:') + console.log('Old position: ' + Chessboard.objToFen(oldPos)) + console.log('New position: ' + Chessboard.objToFen(newPos)) + console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') + } }) -$('#startBtn').on('click', board.start) -$('#clearBtn').on('click', board.clear) + +const ruyLopezFen = 'r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R'; + +document.getElementById('ruyLopezBtn') + .onclick = () => board.setPosition(ruyLopezFen) +document.getElementById('moveBtn') + .onclick = () => board.move('a2-a4', 'h7-h5') +document.getElementById('startBtn') + .onclick = () => board.start() +document.getElementById('clearBtn') + .onclick = () => board.clear() diff --git a/examples/5000-legal-moves.example b/examples/5000-legal-moves.example index 1175a792..6296ba6b 100644 --- a/examples/5000-legal-moves.example +++ b/examples/5000-legal-moves.example @@ -22,9 +22,9 @@ You can integrate chessboard.js with the +===== Name +Piece Highlighting 1 + +===== Description +Use CSS to show piece highlighting. + +===== HTML + -
- +
+ ===== JS // NOTE: this example uses the chess.js library: // https://github.com/jhlywa/chess.js var board = null -var $board = $('#myBoard') +var $board = document.getElementById('myBoard') var game = new Chess() var squareClass = 'square-55d63' var squareToHighlight = null @@ -53,7 +53,7 @@ function makeRandomMove () { } game.move(possibleMoves[randomIdx].san) - board.position(game.fen()) + board.setPosition(game.fen()) window.setTimeout(makeRandomMove, 1200) } @@ -67,6 +67,6 @@ var config = { position: 'start', onMoveEnd: onMoveEnd } -board = Chessboard('myBoard', config) +board = new Chessboard('myBoard', config) window.setTimeout(makeRandomMove, 500) diff --git a/examples/5005-piece-highlight2.example b/examples/5005-piece-highlight2.example index 933ed067..fc62bbf9 100644 --- a/examples/5005-piece-highlight2.example +++ b/examples/5005-piece-highlight2.example @@ -23,7 +23,7 @@ Use CSS to show piece highlighting. // https://github.com/jhlywa/chess.js var board = null -var $board = $('#myBoard') +var $board = document.getElementById('myBoard') var game = new Chess() var squareToHighlight = null var squareClass = 'square-55d63' @@ -59,7 +59,7 @@ function makeRandomMove () { squareToHighlight = move.to // update the board to the new position - board.position(game.fen()) + board.setPosition(game.fen()) } function onDrop (source, target) { @@ -90,7 +90,7 @@ function onMoveEnd () { // update the board position after the piece snap // for castling, en passant, pawn promotion function onSnapEnd () { - board.position(game.fen()) + board.setPosition(game.fen()) } var config = { @@ -101,4 +101,4 @@ var config = { onMoveEnd: onMoveEnd, onSnapEnd: onSnapEnd } -board = Chessboard('myBoard', config) +board = new Chessboard('myBoard', config) diff --git a/lib/chessboard.css b/lib/chessboard.css index 27ffdccb..e6f6b0f2 100644 --- a/lib/chessboard.css +++ b/lib/chessboard.css @@ -1,54 +1,59 @@ -/*! chessboard.js v@VERSION | (c) 2019 Chris Oakman | MIT License chessboardjs.com/license */ - .clearfix-7da63 { - clear: both; -} - + clear: both; +} + .board-b72b1 { border: 2px solid #404040; box-sizing: content-box; -} - +} + .square-55d63 { - float: left; - position: relative; - - /* disable any native browser highlighting */ - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - + float: left; + position: relative; + + /* disable any native browser highlighting */ + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.spare-pieces-7492f { + display: flex; + align-items: center; + justify-content: center; +} + .white-1e1d7 { - background-color: #f0d9b5; - color: #b58863; -} - + background-color: #f0d9b5; + color: #b58863; +} + .black-3c85d { - background-color: #b58863; - color: #f0d9b5; -} + background-color: #b58863; + color: #f0d9b5; +} -.highlight1-32417, .highlight2-9c5d2 { - box-shadow: inset 0 0 3px 3px yellow; -} +.highlight1-32417, +.highlight2-9c5d2 { + box-shadow: inset 0 0 3px 3px yellow; +} -.notation-322f9 { - cursor: default; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 14px; - position: absolute; +.notation-322f9 { + cursor: default; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + position: absolute; } -.alpha-d2270 { - bottom: 1px; - right: 3px; +.alpha-d2270 { + bottom: 1px; + right: 3px; } -.numeric-fc462 { - top: 2px; - left: 2px; +.numeric-fc462 { + top: 2px; + left: 2px; } diff --git a/lib/chessboard.js b/lib/chessboard.js index ed27db18..6cf66d26 100644 --- a/lib/chessboard.js +++ b/lib/chessboard.js @@ -1,1823 +1,1160 @@ -// chessboard.js v@VERSION -// https://github.com/oakmac/chessboardjs/ -// -// Copyright (c) 2019, Chris Oakman -// Released under the MIT license -// https://github.com/oakmac/chessboardjs/blob/master/LICENSE.md - -// start anonymous scope -;(function () { - 'use strict' - - var $ = window['jQuery'] - - // --------------------------------------------------------------------------- - // Constants - // --------------------------------------------------------------------------- - - var COLUMNS = 'abcdefgh'.split('') - var DEFAULT_DRAG_THROTTLE_RATE = 20 - var ELLIPSIS = '…' - var MINIMUM_JQUERY_VERSION = '1.8.3' - var RUN_ASSERTS = true - var START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR' - var START_POSITION = fenToObj(START_FEN) - - // default animation speeds - var DEFAULT_APPEAR_SPEED = 200 - var DEFAULT_MOVE_SPEED = 200 - var DEFAULT_SNAPBACK_SPEED = 60 - var DEFAULT_SNAP_SPEED = 30 - var DEFAULT_TRASH_SPEED = 100 - - // use unique class names to prevent clashing with anything else on the page - // and simplify selectors - // NOTE: these should never change - var CSS = {} - CSS['alpha'] = 'alpha-d2270' - CSS['black'] = 'black-3c85d' - CSS['board'] = 'board-b72b1' - CSS['chessboard'] = 'chessboard-63f37' - CSS['clearfix'] = 'clearfix-7da63' - CSS['highlight1'] = 'highlight1-32417' - CSS['highlight2'] = 'highlight2-9c5d2' - CSS['notation'] = 'notation-322f9' - CSS['numeric'] = 'numeric-fc462' - CSS['piece'] = 'piece-417db' - CSS['row'] = 'row-5277c' - CSS['sparePieces'] = 'spare-pieces-7492f' - CSS['sparePiecesBottom'] = 'spare-pieces-bottom-ae20f' - CSS['sparePiecesTop'] = 'spare-pieces-top-4028b' - CSS['square'] = 'square-55d63' - CSS['white'] = 'white-1e1d7' - - // --------------------------------------------------------------------------- - // Misc Util Functions - // --------------------------------------------------------------------------- - - function throttle (f, interval, scope) { - var timeout = 0 - var shouldFire = false - var args = [] - - var handleTimeout = function () { - timeout = 0 - if (shouldFire) { - shouldFire = false - fire() - } - } +/// +/* eslint-env browser */ + +import * as pieces from './pieces.svg.js'; + +// ------------------------ // +// Constants // +// ------------------------ // + +const COLUMNS = 'abcdefgh'.split(''); +const DEFAULT_DRAG_THROTTLE_RATE = 20; +const START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'; +const START_POSITION = fenToObj(START_FEN); + +// default animation speeds +const DEFAULT_APPEAR_SPEED = 200; +const DEFAULT_MOVE_SPEED = 200; +const DEFAULT_SNAPBACK_SPEED = 60; +const DEFAULT_SNAP_SPEED = 30; +const DEFAULT_TRASH_SPEED = 100; + +// use unique class names to prevent clashing with anything else on the page +// and simplify selectors +// NOTE: these should never change +const ClassNameLookup = { + alpha: 'alpha-d2270', + black: 'black-3c85d', + board: 'board-b72b1', + chessboard: 'chessboard-63f37', + clearfix: 'clearfix-7da63', + highlight1: 'highlight1-32417', + highlight2: 'highlight2-9c5d2', + notation: 'notation-322f9', + numeric: 'numeric-fc462', + piece: 'piece-417db', + row: 'row-5277c', + sparePieces: 'spare-pieces-7492f', + sparePiecesBottom: 'spare-pieces-bottom-ae20f', + sparePiecesTop: 'spare-pieces-top-4028b', + square: 'square-55d63', + white: 'white-1e1d7' +}; + +// ------------------------ // +// Misc Utils // +// ------------------------ // + +function uuid() { + return 'xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx'.replace(/x/g, function () { + const r = (Math.random() * 16) | 0; + return r.toString(16); + }); +} - var fire = function () { - timeout = window.setTimeout(handleTimeout, interval) - f.apply(scope, args) - } +/** + * + * @param {HTMLElement} element + * @returns {{ top: number, left: number }} + */ +function getJqueryStyleOffset(element) { + const { documentElement: { clientTop, clientLeft } } = document; + const { x, y } = element.getBoundingClientRect(); + return { + top: y + scrollY - clientTop, + left: x + scrollX - clientLeft + }; +} - return function (_args) { - args = arguments - if (!timeout) { - fire() - } else { - shouldFire = true - } +/** + * @param {unknown} o + * @returns {o is {}} + */ +function isPlainObject(o) { + return Object.prototype.toString.call(o) === '[object Object]'; +} + + +// ------------------------ // +// Chess Utils // +// ------------------------ // + +function validMove(move) { + // move should be a string + if (typeof move !== 'string') return false; + + // move should be in the form of "e2-e4", "f6-d5" + const squares = move.split('-'); + if (squares.length !== 2) return false; + + const [start, end] = squares; + return validSquare(start) && validSquare(end); +} + +function validSquare(square) { + return typeof square === 'string' && square.search(/^[a-h][1-8]$/) !== -1; +} + +function validPieceCode(code) { + return typeof code === 'string' && code.search(/^[bw][KQRNBP]$/) !== -1; +} + +/** + * @param {string} [fen] + * @returns {boolean} + */ +function validFen(fen) { + if (typeof fen !== 'string') return false; + + // cut off any move, castling, etc info from the end + // we're only interested in position information + fen = fen.replace(/ .+$/, ''); + + // expand the empty square numbers to just 1s + fen = expandFenEmptySquares(fen); + + // FEN should be 8 sections separated by slashes + const chunks = fen.split('/'); + if (chunks.length !== 8) return false; + + // check each section + for (let i = 0; i < 8; i++) { + if (chunks[i].length !== 8 || + chunks[i].search(/[^kqrnbpKQRNBP1]/) !== -1) { + return false; } } - // function debounce (f, interval, scope) { - // var timeout = 0 - // return function (_args) { - // window.clearTimeout(timeout) - // var args = arguments - // timeout = window.setTimeout(function () { - // f.apply(scope, args) - // }, interval) - // } - // } - - function uuid () { - return 'xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx'.replace(/x/g, function (c) { - var r = (Math.random() * 16) | 0 - return r.toString(16) - }) - } + return true; +} - function deepCopy (thing) { - return JSON.parse(JSON.stringify(thing)) - } +function validPositionObject(pos) { + if (!isPlainObject(pos)) return false; + + for (const i in pos) { + if (!Object.prototype.hasOwnProperty.call(pos, i)) continue; - function parseSemVer (version) { - var tmp = version.split('.') - return { - major: parseInt(tmp[0], 10), - minor: parseInt(tmp[1], 10), - patch: parseInt(tmp[2], 10) + if (!validSquare(i) || !validPieceCode(pos[i])) { + return false; } } - // returns true if version is >= minimum - function validSemanticVersion (version, minimum) { - version = parseSemVer(version) - minimum = parseSemVer(minimum) + return true; +} - var versionNum = (version.major * 100000 * 100000) + - (version.minor * 100000) + - version.patch - var minimumNum = (minimum.major * 100000 * 100000) + - (minimum.minor * 100000) + - minimum.patch +/** + * Convert FEN string to position object. Returns `false` if the FEN string is invalid + * @param {string} [fen] + * @returns {false | { [x: string]: string; }} + */ +function fenToObj(fen) { + if (!fen || !validFen(fen)) return false; + + // cut off any move, castling, etc info from the end + // we're only interested in position information + fen = fen.replace(/ .+$/, ''); + + const rows = fen.split('/'); + const position = /** @type {{ [x: string]: string; }} */({}); + + let currentRow = 8; + for (let i = 0; i < 8; i++) { + const row = rows[i].split(''); + let colIdx = 0; + + // loop through each character in the FEN section + for (let j = 0; j < row.length; j++) { + // number / empty squares + const piece = row[j]; + if (piece.search(/[1-8]/) !== -1) { + const numEmptySquares = parseInt(piece, 10); + colIdx = colIdx + numEmptySquares; + } else { + // piece + const square = COLUMNS[colIdx] + currentRow; + position[square] = piece.toLowerCase() === piece + ? `b${piece.toUpperCase()}` // black + : `w${piece.toUpperCase()}`; // white; + colIdx = colIdx + 1; + } + } - return versionNum >= minimumNum + currentRow = currentRow - 1; } - function interpolateTemplate (str, obj) { - for (var key in obj) { - if (!obj.hasOwnProperty(key)) continue - var keyTemplateStr = '{' + key + '}' - var value = obj[key] - while (str.indexOf(keyTemplateStr) !== -1) { - str = str.replace(keyTemplateStr, value) + return position; +} + +/** + * Converts position object to FEN string. Returns false if the obj is not a valid position object + * @param {object} obj + * @returns {false | string} + */ +function objToFen(obj) { + if (!validPositionObject(obj)) return false; + + let fen = ''; + + let currentRow = 8; + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + const square = COLUMNS[j] + currentRow; + + // piece exists + if (Object.prototype.hasOwnProperty.call(obj, square)) { + const [color, pieceKey] = obj[square].split(''); + fen += color === 'w' + ? pieceKey.toUpperCase() // white + : pieceKey.toLowerCase(); // black + } else { + fen += '1'; // empty space } } - return str - } - if (RUN_ASSERTS) { - console.assert(interpolateTemplate('abc', {a: 'x'}) === 'abc') - console.assert(interpolateTemplate('{a}bc', {}) === '{a}bc') - console.assert(interpolateTemplate('{a}bc', {p: 'q'}) === '{a}bc') - console.assert(interpolateTemplate('{a}bc', {a: 'x'}) === 'xbc') - console.assert(interpolateTemplate('{a}bc{a}bc', {a: 'x'}) === 'xbcxbc') - console.assert(interpolateTemplate('{a}{a}{b}', {a: 'x', b: 'y'}) === 'xxy') + if (i !== 7) { + fen = fen + '/'; + } + + currentRow = currentRow - 1; } - // --------------------------------------------------------------------------- - // Predicates - // --------------------------------------------------------------------------- + // squeeze the empty numbers together + fen = squeezeFenEmptySquares(fen); - function isString (s) { - return typeof s === 'string' - } + return fen; +} - function isFunction (f) { - return typeof f === 'function' - } +/** + * @param {string} fen + * @returns {string} + */ +function squeezeFenEmptySquares(fen) { + return fen.replace(/11111111/g, '8') + .replace(/1111111/g, '7') + .replace(/111111/g, '6') + .replace(/11111/g, '5') + .replace(/1111/g, '4') + .replace(/111/g, '3') + .replace(/11/g, '2'); +} - function isInteger (n) { - return typeof n === 'number' && - isFinite(n) && - Math.floor(n) === n - } +/** + * @param {string} fen + * @returns {string} + */ +function expandFenEmptySquares(fen) { + return fen.replace(/8/g, '11111111') + .replace(/7/g, '1111111') + .replace(/6/g, '111111') + .replace(/5/g, '11111') + .replace(/4/g, '1111') + .replace(/3/g, '111') + .replace(/2/g, '11'); +} - function validAnimationSpeed (speed) { - if (speed === 'fast' || speed === 'slow') return true - if (!isInteger(speed)) return false - return speed >= 0 - } +/** + * Returns the distance between two squares + * @param {string} squareA + * @param {string} squareB + * @returns {number} + */ +function squareDistance(squareA, squareB) { + const squareAArray = squareA.split(''); + const squareAx = COLUMNS.indexOf(squareAArray[0]) + 1; + const squareAy = parseInt(squareAArray[1], 10); + + const squareBArray = squareB.split(''); + const squareBx = COLUMNS.indexOf(squareBArray[0]) + 1; + const squareBy = parseInt(squareBArray[1], 10); + + const xDelta = Math.abs(squareAx - squareBx); + const yDelta = Math.abs(squareAy - squareBy); + + if (xDelta >= yDelta) return xDelta; + return yDelta; +} - function validThrottleRate (rate) { - return isInteger(rate) && - rate >= 1 +/** + * Returns the square of the closest instance of piece, or `false` if no piece found + * @param {*} position + * @param {*} piece + * @param {*} square + * @returns {string | false} + */ +function findClosestPiece(position, piece, square) { + // create array of closest squares from square + const closestSquares = createRadius(square); + + // search through the position in order of distance for the piece + for (let i = 0; i < closestSquares.length; i++) { + const s = closestSquares[i]; + + if (Object.prototype.hasOwnProperty.call(position, s) && position[s] === piece) { + return s; + } } - function validMove (move) { - // move should be a string - if (!isString(move)) return false - - // move should be in the form of "e2-e4", "f6-d5" - var squares = move.split('-') - if (squares.length !== 2) return false - - return validSquare(squares[0]) && validSquare(squares[1]) - } - - function validSquare (square) { - return isString(square) && square.search(/^[a-h][1-8]$/) !== -1 - } - - if (RUN_ASSERTS) { - console.assert(validSquare('a1')) - console.assert(validSquare('e2')) - console.assert(!validSquare('D2')) - console.assert(!validSquare('g9')) - console.assert(!validSquare('a')) - console.assert(!validSquare(true)) - console.assert(!validSquare(null)) - console.assert(!validSquare({})) - } + return false; +} - function validPieceCode (code) { - return isString(code) && code.search(/^[bw][KQRNBP]$/) !== -1 - } - - if (RUN_ASSERTS) { - console.assert(validPieceCode('bP')) - console.assert(validPieceCode('bK')) - console.assert(validPieceCode('wK')) - console.assert(validPieceCode('wR')) - console.assert(!validPieceCode('WR')) - console.assert(!validPieceCode('Wr')) - console.assert(!validPieceCode('a')) - console.assert(!validPieceCode(true)) - console.assert(!validPieceCode(null)) - console.assert(!validPieceCode({})) +/** + * Returns an array of closest squares from square + * @param {string} square + * @returns + */ +function createRadius(square) { + const squares = []; + + // calculate distance of all squares + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + const s = COLUMNS[i] + (j + 1); + + // skip the square we're starting from + if (square === s) continue; + + squares.push({ + square: s, + distance: squareDistance(square, s) + }); + } } - function validFen (fen) { - if (!isString(fen)) return false - - // cut off any move, castling, etc info from the end - // we're only interested in position information - fen = fen.replace(/ .+$/, '') - - // expand the empty square numbers to just 1s - fen = expandFenEmptySquares(fen) - - // FEN should be 8 sections separated by slashes - var chunks = fen.split('/') - if (chunks.length !== 8) return false - - // check each section - for (var i = 0; i < 8; i++) { - if (chunks[i].length !== 8 || - chunks[i].search(/[^kqrnbpKQRNBP1]/) !== -1) { - return false - } - } - - return true - } - - if (RUN_ASSERTS) { - console.assert(validFen(START_FEN)) - console.assert(validFen('8/8/8/8/8/8/8/8')) - console.assert(validFen('r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R')) - console.assert(validFen('3r3r/1p4pp/2nb1k2/pP3p2/8/PB2PN2/p4PPP/R4RK1 b - - 0 1')) - console.assert(!validFen('3r3z/1p4pp/2nb1k2/pP3p2/8/PB2PN2/p4PPP/R4RK1 b - - 0 1')) - console.assert(!validFen('anbqkbnr/8/8/8/8/8/PPPPPPPP/8')) - console.assert(!validFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/')) - console.assert(!validFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBN')) - console.assert(!validFen('888888/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR')) - console.assert(!validFen('888888/pppppppp/74/8/8/8/PPPPPPPP/RNBQKBNR')) - console.assert(!validFen({})) + // sort by distance + squares.sort(function (a, b) { + return a.distance - b.distance; + }); + + // just return the square code + const surroundingSquares = []; + for (let i = 0; i < squares.length; i++) { + surroundingSquares.push(squares[i].square); } - function validPositionObject (pos) { - if (!$.isPlainObject(pos)) return false - - for (var i in pos) { - if (!pos.hasOwnProperty(i)) continue - - if (!validSquare(i) || !validPieceCode(pos[i])) { - return false - } - } - - return true - } - - if (RUN_ASSERTS) { - console.assert(validPositionObject(START_POSITION)) - console.assert(validPositionObject({})) - console.assert(validPositionObject({e2: 'wP'})) - console.assert(validPositionObject({e2: 'wP', d2: 'wP'})) - console.assert(!validPositionObject({e2: 'BP'})) - console.assert(!validPositionObject({y2: 'wP'})) - console.assert(!validPositionObject(null)) - console.assert(!validPositionObject('start')) - console.assert(!validPositionObject(START_FEN)) + return surroundingSquares; +} + +class Chessboard { + static pieces = pieces; + static fenToObj = fenToObj; + static objToFen = objToFen; + + /** + * @type {HTMLElement} + */ + #container; + /** + * @type {HTMLElement} + */ + #board; + /** + * @type {'white' | 'black'} + */ + #orientation = 'white'; + /** + * @type {Map} + */ + #position = new Map(); + /** + * @type {Map} + */ + #squares = new Map(); + /** + * @type {Map} + */ + #pieces = new Map(); + /** + * @type {Map} + */ + #sparePieces = new Map(); + /** + * @type {HTMLElement} + */ + #topShelf; + /** + * @type {HTMLElement} + */ + #bottomShelf; + + /** + * @typedef {object} ChessboardConfig + * @property {number} appearSpeed + * @property {number} dragThrottleRate + * @property {number} moveSpeed + * @property {number} snapbackSpeed + * @property {number} snapSpeed + * @property {number} trashSpeed + * @property {boolean} draggable + * @property {boolean} showNotation + * @property {boolean} useAnimation + * @property {string | boolean} sparePieces + * @property {'snapback' | 'trash'} dropOffBoard + * @property {'white' | 'black'} orientation + * @property {string | { [x: string]: string }} position + * @property {'alert' | 'console' | boolean | ((x: string, y: any, z: HTMLElement) => void)} showErrors + * @property {string | ((x: string) => string)} pieceTheme + * @property {(x: object, y: object) => void} [onChange] + * @property {(x: string, y: string, z: string, w: HTMLElement, r: object, s: 'white' | 'black') => void} [onDragMove] + * @property {(x: string | null | undefined, y: boolean, z: object, o: 'white' | 'black') => boolean} [onDragStart] + * @property {(x: string, y: any, z: HTMLElement, w: object, r: object, s: 'white' | 'black') => 'snapback' | 'trash'} [onDrop] + * @property {(x: object, y: object) => void} [onMoveEnd] + */ + #config = /** @type {ChessboardConfig} */ ({ + showErrors: 'alert' // default value only used in constructor prior to user config assignment + }); + + get fen() { + return objToFen(this.position); } - function isTouchDevice () { - return 'ontouchstart' in document.documentElement + get orientation() { + return this.#orientation; } - function validJQueryVersion () { - return typeof window.$ && - $.fn && - $.fn.jquery && - validSemanticVersion($.fn.jquery, MINIMUM_JQUERY_VERSION) + get position() { + return Object.fromEntries(this.#position.entries()); } - // --------------------------------------------------------------------------- - // Chess Util Functions - // --------------------------------------------------------------------------- + /** + * Returns calculated square size based on the width of the container + * @type {number} + */ + get squareSize() { + // got a little CSS black magic here, so let me explain: + // get the width of the container element (could be anything), reduce by 1 for + // fudge factor, and then keep reducing until we find an exact mod 8 for + // our square size + const containerWidth = this.#container.clientWidth; - // convert FEN piece code to bP, wK, etc - function fenToPieceCode (piece) { - // black piece - if (piece.toLowerCase() === piece) { - return 'b' + piece.toUpperCase() + // defensive, prevent infinite loop + if (!containerWidth || containerWidth <= 0) { + return 0; } - // white piece - return 'w' + piece.toUpperCase() - } - - // convert bP, wK, etc code to FEN structure - function pieceCodeToFen (piece) { - var pieceCodeLetters = piece.split('') + // pad one pixel + let boardWidth = containerWidth - 1; - // white piece - if (pieceCodeLetters[0] === 'w') { - return pieceCodeLetters[1].toUpperCase() + while (boardWidth % 8 !== 0 && boardWidth > 0) { + boardWidth = boardWidth - 1; } - // black piece - return pieceCodeLetters[1].toLowerCase() + return boardWidth / 8; } - // convert FEN string to position object - // returns false if the FEN string is invalid - function fenToObj (fen) { - if (!validFen(fen)) return false - - // cut off any move, castling, etc info from the end - // we're only interested in position information - fen = fen.replace(/ .+$/, '') - - var rows = fen.split('/') - var position = {} - - var currentRow = 8 - for (var i = 0; i < 8; i++) { - var row = rows[i].split('') - var colIdx = 0 - - // loop through each character in the FEN section - for (var j = 0; j < row.length; j++) { - // number / empty squares - if (row[j].search(/[1-8]/) !== -1) { - var numEmptySquares = parseInt(row[j], 10) - colIdx = colIdx + numEmptySquares - } else { - // piece - var square = COLUMNS[colIdx] + currentRow - position[square] = fenToPieceCode(row[j]) - colIdx = colIdx + 1 - } - } - - currentRow = currentRow - 1 - } - - return position - } + /** + * @param {string | HTMLElement} containerElOrString + * @param {string | Partial} config + * @returns + */ + constructor(containerElOrString, config) { + // first things first: check basic dependencies + if (containerElOrString === '') { + this.#error(1001, 'The first argument to new Chessboard() cannot be an empty string.\n\nExiting…'); + return; + } - // position object to FEN string - // returns false if the obj is not a valid position object - function objToFen (obj) { - if (!validPositionObject(obj)) return false - - var fen = '' - - var currentRow = 8 - for (var i = 0; i < 8; i++) { - for (var j = 0; j < 8; j++) { - var square = COLUMNS[j] + currentRow - - // piece exists - if (obj.hasOwnProperty(square)) { - fen = fen + pieceCodeToFen(obj[square]) - } else { - // empty space - fen = fen + '1' - } - } - - if (i !== 7) { - fen = fen + '/' + if (containerElOrString instanceof HTMLElement) { + this.#container = containerElOrString; + } else if (typeof containerElOrString === 'string') { + // convert containerEl to query selector if it is a string + if (containerElOrString.startsWith('#') === false) { + containerElOrString = `#${containerElOrString}`; } - - currentRow = currentRow - 1 - } - - // squeeze the empty numbers together - fen = squeezeFenEmptySquares(fen) - - return fen - } - - if (RUN_ASSERTS) { - console.assert(objToFen(START_POSITION) === START_FEN) - console.assert(objToFen({}) === '8/8/8/8/8/8/8/8') - console.assert(objToFen({a2: 'wP', 'b2': 'bP'}) === '8/8/8/8/8/8/Pp6/8') - } - - function squeezeFenEmptySquares (fen) { - return fen.replace(/11111111/g, '8') - .replace(/1111111/g, '7') - .replace(/111111/g, '6') - .replace(/11111/g, '5') - .replace(/1111/g, '4') - .replace(/111/g, '3') - .replace(/11/g, '2') - } - function expandFenEmptySquares (fen) { - return fen.replace(/8/g, '11111111') - .replace(/7/g, '1111111') - .replace(/6/g, '111111') - .replace(/5/g, '11111') - .replace(/4/g, '1111') - .replace(/3/g, '111') - .replace(/2/g, '11') - } + // containerEl must be something that becomes a NodeList of size 1 + const container = document.querySelectorAll(containerElOrString); + if (container.length !== 1) { + this.#error(1003, 'The first argument to new Chessboard() must be the ID of a DOM node, an ID query selector, or a single DOM node.\n\nExiting…'); + return; + } - // returns the distance between two squares - function squareDistance (squareA, squareB) { - var squareAArray = squareA.split('') - var squareAx = COLUMNS.indexOf(squareAArray[0]) + 1 - var squareAy = parseInt(squareAArray[1], 10) + this.#container = /** @type {HTMLElement} */((container.item(0))); + this.#container.classList.add(ClassNameLookup.chessboard); + this.#container.replaceChildren(); + } else { + this.#error(1003, 'The first argument to new Chessboard() must be the ID of a DOM node, an ID query selector, or a single DOM node.\n\nExiting…'); + return; + } - var squareBArray = squareB.split('') - var squareBx = COLUMNS.indexOf(squareBArray[0]) + 1 - var squareBy = parseInt(squareBArray[1], 10) + this.#board = document.createElement('div'); + this.#board.classList.add(ClassNameLookup.board); + this.#config = this.#validateConfig(config); + this.#orientation = this.#config.orientation; - var xDelta = Math.abs(squareAx - squareBx) - var yDelta = Math.abs(squareAy - squareBy) + this.#topShelf = document.createElement('div'); + this.#topShelf.classList.add(ClassNameLookup.sparePieces, ClassNameLookup.sparePiecesTop); + this.#bottomShelf = document.createElement('div'); + this.#bottomShelf.classList.add(ClassNameLookup.sparePieces, ClassNameLookup.sparePiecesBottom); - if (xDelta >= yDelta) return xDelta - return yDelta - } + this.#container.appendChild(this.#topShelf); + this.#container.appendChild(this.#board); + this.#container.appendChild(this.#bottomShelf); - // returns the square of the closest instance of piece - // returns false if no instance of piece is found in position - function findClosestPiece (position, piece, square) { - // create array of closest squares from square - var closestSquares = createRadius(square) + if (this.#config.sparePieces) { + for (const pieceKey of 'KQRNBP'.split('')) { + const whitePieceName = `w${pieceKey}`; + const whitePiece = this.#buildPiece(whitePieceName); - // search through the position in order of distance for the piece - for (var i = 0; i < closestSquares.length; i++) { - var s = closestSquares[i] + const blackPieceName = `b${pieceKey}`; + const blackPiece = this.#buildPiece(blackPieceName); - if (position.hasOwnProperty(s) && position[s] === piece) { - return s + this.#sparePieces.set(whitePieceName, whitePiece); + this.#sparePieces.set(blackPieceName, blackPiece); } } - return false + this.render(); } - // returns an array of closest squares from square - function createRadius (square) { - var squares = [] + /** + * Clear the board of pieces + */ + clear() { + this.setPosition('clear'); + } - // calculate distance of all squares - for (var i = 0; i < 8; i++) { - for (var j = 0; j < 8; j++) { - var s = COLUMNS[i] + (j + 1) + /** + * Set the board to the starting position + */ + start() { + this.setPosition('start'); + } - // skip the square we're starting from - if (square === s) continue + /** + * Flip orientation + */ + flip() { + return this.setOrientation('flip'); + } - squares.push({ - square: s, - distance: squareDistance(square, s) - }) + /** + * Move pieces + * @param {string | string[]} moveset + */ + move(moveset) { + if (moveset.length === 0) return; + if (!Array.isArray(moveset)) moveset = [moveset]; + + // collect the moves into an object + const moves = moveset + .filter(move => validMove(move) || this.#error(2826, 'Invalid move passed to the move method.', move)) + .map(move => move.split('-')); + + for (const [source, destination] of moves) { + if (!this.#position.has(source)) continue; + + const piece = this.#position.get(source); + this.#position.delete(source); + if (typeof piece === 'string' && piece.length > 0) { + this.#position.set(destination, piece); } } - // sort by distance - squares.sort(function (a, b) { - return a.distance - b.distance - }) + this.setPosition(this.position); + } - // just return the square code - var surroundingSquares = [] - for (i = 0; i < squares.length; i++) { - surroundingSquares.push(squares[i].square) + /** + * @param {'white' | 'black' | 'flip'} orientation + */ + setOrientation(orientation) { + if (!orientation) { + return this.#orientation; + } else if (orientation === 'white' || orientation === 'black') { + this.#orientation = orientation; + } else if (orientation === 'flip') { + this.#orientation = this.#orientation === 'white' ? 'black' : 'white'; + } else { + this.#error(5482, 'Invalid value passed to the orientation method.', orientation); } - return surroundingSquares - } + this.render(); - // given a position and a set of moves, return a new position - // with the moves executed - function calculatePositionFromMoves (position, moves) { - var newPosition = deepCopy(position) + return this.#orientation; + } - for (var i in moves) { - if (!moves.hasOwnProperty(i)) continue + /** + * @param {'clear' | 'start' | object} position + */ + setPosition(position) { + const positionIsString = typeof position === 'string'; - // skip the move if the position doesn't have a piece on the source square - if (!newPosition.hasOwnProperty(i)) continue + if (positionIsString) { + const positionLowerCase = position.toLowerCase(); - var piece = newPosition[i] - delete newPosition[i] - newPosition[moves[i]] = piece + if (positionLowerCase === 'clear') { + position = {}; + } else if (positionLowerCase === 'start') { + position = START_POSITION; + } } - return newPosition - } - - // TODO: add some asserts here for calculatePositionFromMoves - - // --------------------------------------------------------------------------- - // HTML - // --------------------------------------------------------------------------- + // convert FEN to position object + if (validFen(position)) { + position = fenToObj(position); + } - function buildContainerHTML (hasSparePieces) { - var html = '
' - if (hasSparePieces) { - html += '
' + // validate position object + if (validPositionObject(position)) { + position = { ...position }; + } else { + this.#error(6482, 'Invalid value passed to the position method.', position); + return; } - html += '
' - - if (hasSparePieces) { - html += '
' + const setCurrentPosition = () => { + const { position: currentPosition } = this; + if (objToFen(currentPosition) !== objToFen(position)) { + if (typeof this.#config.onChange === 'function') { + this.#config.onChange(currentPosition, position); + } + this.#position = new Map(Object.entries(position)); + } + }; + + if (this.#config.useAnimation) { + const animations = this.#calculateAnimations({ ...position }); + this.#doAnimations(animations, { ...position }).then(setCurrentPosition); + } else { + setCurrentPosition(); + this.#drawPosition(); } + } - html += '
' + render() { + this.#drawBoard(); + this.#drawPosition(); + } - return interpolateTemplate(html, CSS) + /** + * Irretrievably remove everything. The Chessboard instance will become useless. + */ + destroy() { + this.#container.replaceChildren(); + this.#container = /** @type {any} */(undefined); } - // --------------------------------------------------------------------------- - // Config - // --------------------------------------------------------------------------- + #error(code, msg, obj) { + // do nothing if showErrors is not set + if ( + Object.prototype.hasOwnProperty.call(this.#config, 'showErrors') !== true || + this.#config.showErrors === false + ) { + return; + } - function expandConfigArgumentShorthand (config) { - if (config === 'start') { - config = {position: deepCopy(START_POSITION)} - } else if (validFen(config)) { - config = {position: fenToObj(config)} - } else if (validPositionObject(config)) { - config = {position: deepCopy(config)} + let errorText = 'Chessboard Error ' + code + ': ' + msg; + + // print to console + if ( + this.#config.showErrors === 'console' && + typeof console === 'object' && + typeof console.log === 'function' + ) { + console.log(errorText); + if (obj) { + console.log(obj); + } + return; } - // config must be an object - if (!$.isPlainObject(config)) config = {} + // alert errors + if (this.#config.showErrors === 'alert') { + if (obj) { + errorText += '\n\n' + JSON.stringify(obj); + } + window.alert(errorText); + return; + } - return config + // custom function + if (typeof this.#config.showErrors === 'function') { + this.#config.showErrors(code, msg, obj); + } } - // validate config / set default options - function expandConfig (config) { - // default for orientation is white - if (config.orientation !== 'black') config.orientation = 'white' - - // default for showNotation is true - if (config.showNotation !== false) config.showNotation = true - - // default for draggable is false - if (config.draggable !== true) config.draggable = false - - // default for dropOffBoard is 'snapback' - if (config.dropOffBoard !== 'trash') config.dropOffBoard = 'snapback' - - // default for sparePieces is false - if (config.sparePieces !== true) config.sparePieces = false - - // draggable must be true if sparePieces is enabled - if (config.sparePieces) config.draggable = true + /** + * @param {string | Partial} config + * @returns {ChessboardConfig} + */ + #validateConfig(config) { + // ensure the config object is what we expect + if (typeof config === 'string') { + if (config === 'start') { + config = { position: { ...START_POSITION } }; + } else if (validFen(config)) { + config = { position: fenToObj(config) || {} }; + } else { + config = {}; + } + } else if (validPositionObject(config)) { + config = { position: { .../** @type {{ [x: string]: string; }} */(config) } }; + } else if (!isPlainObject(config)) { + config = {}; + } - // default piece theme is wikipedia - if (!config.hasOwnProperty('pieceTheme') || - (!isString(config.pieceTheme) && !isFunction(config.pieceTheme))) { - config.pieceTheme = 'img/chesspieces/wikipedia/{piece}.png' + if (config.orientation !== 'black') config.orientation = 'white'; + if (config.useAnimation !== false) config.useAnimation = true; + if (config.showNotation !== false) config.showNotation = true; + if (config.draggable !== true) config.draggable = false; + if (config.dropOffBoard !== 'trash') config.dropOffBoard = 'snapback'; + if (config.sparePieces !== true) config.sparePieces = false; + if (config.sparePieces) config.draggable = true; // draggable must be true if sparePieces is enabled + + // default piece theme is built-in svg + if (!Object.prototype.hasOwnProperty.call(config, 'pieceTheme') || + (typeof config.pieceTheme !== 'string' && typeof config.pieceTheme !== 'function')) { + config.pieceTheme = '{piece}'; } - // animation speeds - if (!validAnimationSpeed(config.appearSpeed)) config.appearSpeed = DEFAULT_APPEAR_SPEED - if (!validAnimationSpeed(config.moveSpeed)) config.moveSpeed = DEFAULT_MOVE_SPEED - if (!validAnimationSpeed(config.snapbackSpeed)) config.snapbackSpeed = DEFAULT_SNAPBACK_SPEED - if (!validAnimationSpeed(config.snapSpeed)) config.snapSpeed = DEFAULT_SNAP_SPEED - if (!validAnimationSpeed(config.trashSpeed)) config.trashSpeed = DEFAULT_TRASH_SPEED + const validThrottleRate = (rate) => Number.isInteger(rate) && rate >= 1; // throttle rate - if (!validThrottleRate(config.dragThrottleRate)) config.dragThrottleRate = DEFAULT_DRAG_THROTTLE_RATE + if (!validThrottleRate(config.dragThrottleRate)) + config.dragThrottleRate = DEFAULT_DRAG_THROTTLE_RATE; - return config - } + const getValidAnimationSpeed = (speed, defaultValue) => { + if (speed === 'fast') return defaultValue / 2; + if (speed === 'slow') return defaultValue * 3; + if (Number.isInteger(speed) && speed >= 0) return speed; + return defaultValue; + }; - // --------------------------------------------------------------------------- - // Dependencies - // --------------------------------------------------------------------------- - - // check for a compatible version of jQuery - function checkJQuery () { - if (!validJQueryVersion()) { - var errorMsg = 'Chessboard Error 1005: Unable to find a valid version of jQuery. ' + - 'Please include jQuery ' + MINIMUM_JQUERY_VERSION + ' or higher on the page' + - '\n\n' + - 'Exiting' + ELLIPSIS - window.alert(errorMsg) - return false + // animation speeds + config.appearSpeed = getValidAnimationSpeed(config.appearSpeed, DEFAULT_APPEAR_SPEED); + config.snapbackSpeed = getValidAnimationSpeed(config.snapbackSpeed, DEFAULT_SNAPBACK_SPEED); + config.snapSpeed = getValidAnimationSpeed(config.snapSpeed, DEFAULT_SNAP_SPEED); + config.moveSpeed = getValidAnimationSpeed(config.moveSpeed, DEFAULT_MOVE_SPEED); + config.trashSpeed = getValidAnimationSpeed(config.trashSpeed, DEFAULT_TRASH_SPEED); + + // make sure position is valid + if (config.position != null) { + if (typeof config.position === 'string') { + if (config.position === 'start') { + this.#position = new Map(Object.entries(START_POSITION)); + } else if (validFen(config.position)) { + this.#position = new Map(Object.entries(fenToObj(config.position))); + } + } else if (validPositionObject(config.position)) { + this.#position = new Map(Object.entries(config.position)); + } else { + this.#error(7263, 'Invalid value passed to config.position.', config.position); + } } - return true + return /** @type {ChessboardConfig} */(config); } - // return either boolean false or the $container element - function checkContainerArg (containerElOrString) { - if (containerElOrString === '') { - var errorMsg1 = 'Chessboard Error 1001: ' + - 'The first argument to Chessboard() cannot be an empty string.' + - '\n\n' + - 'Exiting' + ELLIPSIS - window.alert(errorMsg1) - return false - } + #buildPiece(pieceName) { + const { squareSize } = this; + const piece = new Image(squareSize, squareSize); - // convert containerEl to query selector if it is a string - if (isString(containerElOrString) && - containerElOrString.charAt(0) !== '#') { - containerElOrString = '#' + containerElOrString + let src = ''; + if (typeof this.#config.pieceTheme === 'function') { + src = this.#config.pieceTheme(pieceName); + } + if (typeof this.#config.pieceTheme === 'string') { + src = this.#config.pieceTheme.replaceAll('{piece}', pieceName); } - // containerEl must be something that becomes a jQuery collection of size 1 - var $container = $(containerElOrString) - if ($container.length !== 1) { - var errorMsg2 = 'Chessboard Error 1003: ' + - 'The first argument to Chessboard() must be the ID of a DOM node, ' + - 'an ID query selector, or a single DOM node.' + - '\n\n' + - 'Exiting' + ELLIPSIS - window.alert(errorMsg2) - return false + piece.id = `${pieceName}-${uuid()}`; + piece.alt = ''; + piece.src = src === pieceName ? pieces[src] : src; + piece.classList.add(ClassNameLookup.piece); + piece.dataset.piece = pieceName; + + piece.draggable = this.#config.draggable; + if (piece.draggable) { + piece.ondragstart = (evt) => { + if (evt.target instanceof HTMLElement && evt.dataTransfer != null) { + const source = evt.target.dataset.square ?? "spare"; + if (source !== "spare") { + const square = this.#squares.get(source); + if (square instanceof HTMLElement) { + square.classList.add(ClassNameLookup.highlight1); + } + } + evt.dataTransfer.setData("pieceName", pieceName); + evt.dataTransfer.setData("source", source); + evt.dataTransfer.effectAllowed = "move"; + } + }; } - return $container + return piece; } - // --------------------------------------------------------------------------- - // Constructor - // --------------------------------------------------------------------------- + /** + * Calculate an array of animations that need to happen in order to get from `pos1` to `pos2` + * @param {{ [index: string ]: string }} position + * @returns {any[]} + */ + #calculateAnimations(position) { + const animations = []; + + if (Object.keys(position).length === 0) { + for (const [square, piece] of this.#position.entries()) { + animations.push({ type: 'clear', square, piece }); + } + return animations; + } - function constructor (containerElOrString, config) { - // first things first: check basic dependencies - if (!checkJQuery()) return null - var $container = checkContainerArg(containerElOrString) - if (!$container) return null + const { position: currentPosition } = this; - // ensure the config object is what we expect - config = expandConfigArgumentShorthand(config) - config = expandConfig(config) - - // DOM elements - var $board = null - var $draggedPiece = null - var $sparePiecesTop = null - var $sparePiecesBottom = null - - // constructor return object - var widget = {} - - // ------------------------------------------------------------------------- - // Stateful - // ------------------------------------------------------------------------- - - var boardBorderSize = 2 - var currentOrientation = 'white' - var currentPosition = {} - var draggedPiece = null - var draggedPieceLocation = null - var draggedPieceSource = null - var isDragging = false - var sparePiecesElsIds = {} - var squareElsIds = {} - var squareElsOffsets = {} - var squareSize = 16 - - // ------------------------------------------------------------------------- - // Validation / Errors - // ------------------------------------------------------------------------- - - function error (code, msg, obj) { - // do nothing if showErrors is not set - if ( - config.hasOwnProperty('showErrors') !== true || - config.showErrors === false - ) { - return - } - - var errorText = 'Chessboard Error ' + code + ': ' + msg - - // print to console - if ( - config.showErrors === 'console' && - typeof console === 'object' && - typeof console.log === 'function' - ) { - console.log(errorText) - if (arguments.length >= 2) { - console.log(obj) - } - return - } - - // alert errors - if (config.showErrors === 'alert') { - if (obj) { - errorText += '\n\n' + JSON.stringify(obj) - } - window.alert(errorText) - return - } - - // custom function - if (isFunction(config.showErrors)) { - config.showErrors(code, msg, obj) - } - } + const squaresMovedTo = {}; - function setInitialState () { - currentOrientation = config.orientation + // remove pieces that are the same in both positions + for (const i in position) { + if ( + Object.prototype.hasOwnProperty.call(position, i) && + Object.prototype.hasOwnProperty.call(currentPosition, i) && + currentPosition[i] === position[i] + ) { + delete currentPosition[i]; + delete position[i]; + } + } - // make sure position is valid - if (config.hasOwnProperty('position')) { - if (config.position === 'start') { - currentPosition = deepCopy(START_POSITION) - } else if (validFen(config.position)) { - currentPosition = fenToObj(config.position) - } else if (validPositionObject(config.position)) { - currentPosition = deepCopy(config.position) - } else { - error( - 7263, - 'Invalid value passed to config.position.', - config.position - ) + // find all the "move" animations + for (const square in position) { + if (Object.prototype.hasOwnProperty.call(position, square)) { + const source = findClosestPiece(currentPosition, position[square], square); + if (source) { + animations.push({ type: 'move', source, destination: square, piece: position[square] }); + delete currentPosition[source]; + delete position[square]; + squaresMovedTo[square] = true; } } } - // ------------------------------------------------------------------------- - // DOM Misc - // ------------------------------------------------------------------------- - - // calculates square size based on the width of the container - // got a little CSS black magic here, so let me explain: - // get the width of the container element (could be anything), reduce by 1 for - // fudge factor, and then keep reducing until we find an exact mod 8 for - // our square size - function calculateSquareSize () { - var containerWidth = parseInt($container.width(), 10) - - // defensive, prevent infinite loop - if (!containerWidth || containerWidth <= 0) { - return 0 - } - - // pad one pixel - var boardWidth = containerWidth - 1 - - while (boardWidth % 8 !== 0 && boardWidth > 0) { - boardWidth = boardWidth - 1 - } - - return boardWidth / 8 - } - - // create random IDs for elements - function createElIds () { - // squares on the board - for (var i = 0; i < COLUMNS.length; i++) { - for (var j = 1; j <= 8; j++) { - var square = COLUMNS[i] + j - squareElsIds[square] = square + '-' + uuid() - } - } - - // spare pieces - var pieces = 'KQRNBP'.split('') - for (i = 0; i < pieces.length; i++) { - var whitePiece = 'w' + pieces[i] - var blackPiece = 'b' + pieces[i] - sparePiecesElsIds[whitePiece] = whitePiece + '-' + uuid() - sparePiecesElsIds[blackPiece] = blackPiece + '-' + uuid() - } - } - - // ------------------------------------------------------------------------- - // Markup Building - // ------------------------------------------------------------------------- - - function buildBoardHTML (orientation) { - if (orientation !== 'black') { - orientation = 'white' + // "add" animations + for (const square in position) { + if (Object.prototype.hasOwnProperty.call(position, square)) { + animations.push({ type: 'add', square, piece: position[square] }); + delete position[square]; } + } - var html = '' - - // algebraic notation / orientation - var alpha = deepCopy(COLUMNS) - var row = 8 - if (orientation === 'black') { - alpha.reverse() - row = 1 + // "clear" animations + for (const square in currentPosition) { + if ( + Object.prototype.hasOwnProperty.call(currentPosition, square) && + // do not clear a piece if it is on a square that is the result + // of a "move", ie: a piece capture + !Object.prototype.hasOwnProperty.call(squaresMovedTo, square) + ) { + animations.push({ type: 'clear', square: square, piece: currentPosition[square] }); + delete currentPosition[square]; } + } - var squareColor = 'white' - for (var i = 0; i < 8; i++) { - html += '
' - for (var j = 0; j < 8; j++) { - var square = alpha[j] + row - - html += '
' - - if (config.showNotation) { - // alpha notation - if ((orientation === 'white' && row === 1) || - (orientation === 'black' && row === 8)) { - html += '
' + alpha[j] + '
' - } + return animations; + } - // numeric notation - if (j === 0) { - html += '
' + row + '
' + /** + * Execute an array of animations + * @param {any[]} animations + * @param {{ [index: string]: string; }} newPosition + */ + #doAnimations(animations, newPosition) { + if (animations.length === 0) return Promise.resolve(); + + let numFinished = 0; + return Promise.all( + animations.map(animation => /** @type {Promise} */(new Promise((resolve) => { + const onFinishAnimation = () => { + numFinished += 1; + if (numFinished === animations.length) { + if (typeof this.#config.onMoveEnd === 'function') { + this.#config.onMoveEnd(this.position, { ...newPosition }); } + resolve(); } + }; + if (animation.type === 'add') { + this.#doAddAnimation(animation, onFinishAnimation); + } else if (animation.type === 'clear') { + this.#doClearAnimation(animation, onFinishAnimation); + } else if (animation.type === 'move') { + this.#doMoveAnimation(animation, onFinishAnimation); + } + }))) + ); + } + + #doAddAnimation(animation, onFinishAnimation) { + const { piece: pieceName, square: squareName } = animation; + const { sparePieces } = this.#config; - html += '
' // end .square + const piece = this.#buildPiece(pieceName); + const destination = /** @type {HTMLElement} */(this.#squares.get(squareName)); - squareColor = (squareColor === 'white') ? 'black' : 'white' + if (destination != null) { + if (sparePieces) { + const sparePiece = this.#sparePieces.get(pieceName); + if (sparePiece == null) { + throw new Error(`Unable to locate spare piece for ${pieceName}`); } - html += '
' - squareColor = (squareColor === 'white') ? 'black' : 'white' + document.body.appendChild(piece); + piece.style.setProperty('position', 'absolute'); - if (orientation === 'white') { - row = row - 1 - } else { - row = row + 1 - } - } + piece + .animate( + [getJqueryStyleOffset(sparePiece), getJqueryStyleOffset(destination)], + this.#config.moveSpeed + ) + .onfinish = () => { + piece.style.removeProperty('position'); + piece.dataset.square = squareName; + destination.prepend(piece); - return interpolateTemplate(html, CSS) - } - - function buildPieceImgSrc (piece) { - if (isFunction(config.pieceTheme)) { - return config.pieceTheme(piece) - } - - if (isString(config.pieceTheme)) { - return interpolateTemplate(config.pieceTheme, {piece: piece}) - } - - // NOTE: this should never happen - error(8272, 'Unable to build image source for config.pieceTheme.') - return '' - } - - function buildPieceHTML (piece, hidden, id) { - var html = '' - - return interpolateTemplate(html, CSS) - } - - function buildSparePiecesHTML (color) { - var pieces = ['wK', 'wQ', 'wR', 'wB', 'wN', 'wP'] - if (color === 'black') { - pieces = ['bK', 'bQ', 'bR', 'bB', 'bN', 'bP'] - } - - var html = '' - for (var i = 0; i < pieces.length; i++) { - html += buildPieceHTML(pieces[i], false, sparePiecesElsIds[pieces[i]]) - } - - return html - } - - // ------------------------------------------------------------------------- - // Animations - // ------------------------------------------------------------------------- - - function animateSquareToSquare (src, dest, piece, completeFn) { - // get information about the source and destination squares - var $srcSquare = $('#' + squareElsIds[src]) - var srcSquarePosition = $srcSquare.offset() - var $destSquare = $('#' + squareElsIds[dest]) - var destSquarePosition = $destSquare.offset() - - // create the animated piece and absolutely position it - // over the source square - var animatedPieceId = uuid() - $('body').append(buildPieceHTML(piece, true, animatedPieceId)) - var $animatedPiece = $('#' + animatedPieceId) - $animatedPiece.css({ - display: '', - position: 'absolute', - top: srcSquarePosition.top, - left: srcSquarePosition.left - }) - - // remove original piece from source square - $srcSquare.find('.' + CSS.piece).remove() - - function onFinishAnimation1 () { - // add the "real" piece to the destination square - $destSquare.append(buildPieceHTML(piece)) - - // remove the animated piece - $animatedPiece.remove() - - // run complete function - if (isFunction(completeFn)) { - completeFn() - } - } - - // animate the piece to the destination square - var opts = { - duration: config.moveSpeed, - complete: onFinishAnimation1 - } - $animatedPiece.animate(destSquarePosition, opts) - } - - function animateSparePieceToSquare (piece, dest, completeFn) { - var srcOffset = $('#' + sparePiecesElsIds[piece]).offset() - var $destSquare = $('#' + squareElsIds[dest]) - var destOffset = $destSquare.offset() - - // create the animate piece - var pieceId = uuid() - $('body').append(buildPieceHTML(piece, true, pieceId)) - var $animatedPiece = $('#' + pieceId) - $animatedPiece.css({ - display: '', - position: 'absolute', - left: srcOffset.left, - top: srcOffset.top - }) - - // on complete - function onFinishAnimation2 () { - // add the "real" piece to the destination square - $destSquare.find('.' + CSS.piece).remove() - $destSquare.append(buildPieceHTML(piece)) - - // remove the animated piece - $animatedPiece.remove() - - // run complete function - if (isFunction(completeFn)) { - completeFn() - } - } - - // animate the piece to the destination square - var opts = { - duration: config.moveSpeed, - complete: onFinishAnimation2 - } - $animatedPiece.animate(destOffset, opts) + onFinishAnimation(); + }; + } else { + destination.prepend(piece); + piece + .animate([{ opacity: 0 }, { opacity: 1 }], this.#config.appearSpeed) + .onfinish = () => { + this.#pieces.delete(squareName); + this.#pieces.set(squareName, piece); + onFinishAnimation(); + }; + } + } else { + throw new Error(`Unable to locate destination square ${squareName}`); } + } - // execute an array of animations - function doAnimations (animations, oldPos, newPos) { - if (animations.length === 0) return - - var numFinished = 0 - function onFinishAnimation3 () { - // exit if all the animations aren't finished - numFinished = numFinished + 1 - if (numFinished !== animations.length) return - - drawPositionInstant() - - // run their onMoveEnd function - if (isFunction(config.onMoveEnd)) { - config.onMoveEnd(deepCopy(oldPos), deepCopy(newPos)) - } - } - - for (var i = 0; i < animations.length; i++) { - var animation = animations[i] - - // clear a piece - if (animation.type === 'clear') { - $('#' + squareElsIds[animation.square] + ' .' + CSS.piece) - .fadeOut(config.trashSpeed, onFinishAnimation3) - - // add a piece with no spare pieces - fade the piece onto the square - } else if (animation.type === 'add' && !config.sparePieces) { - $('#' + squareElsIds[animation.square]) - .append(buildPieceHTML(animation.piece, true)) - .find('.' + CSS.piece) - .fadeIn(config.appearSpeed, onFinishAnimation3) - - // add a piece with spare pieces - animate from the spares - } else if (animation.type === 'add' && config.sparePieces) { - animateSparePieceToSquare(animation.piece, animation.square, onFinishAnimation3) - - // move a piece from squareA to squareB - } else if (animation.type === 'move') { - animateSquareToSquare(animation.source, animation.destination, animation.piece, onFinishAnimation3) - } - } + #doClearAnimation(animation, onFinishAnimation) { + const piece = this.#pieces.get(animation.square); + if (piece != null) { + piece + .animate([{ opacity: 1 }, { opacity: 0 }], this.#config.trashSpeed) + .onfinish = () => { + piece.remove(); + this.#pieces.delete(animation.square); + this.#position.delete(animation.square); + onFinishAnimation(); + }; } + } - // calculate an array of animations that need to happen in order to get - // from pos1 to pos2 - function calculateAnimations (pos1, pos2) { - // make copies of both - pos1 = deepCopy(pos1) - pos2 = deepCopy(pos2) - - var animations = [] - var squaresMovedTo = {} - - // remove pieces that are the same in both positions - for (var i in pos2) { - if (!pos2.hasOwnProperty(i)) continue - - if (pos1.hasOwnProperty(i) && pos1[i] === pos2[i]) { - delete pos1[i] - delete pos2[i] - } - } - - // find all the "move" animations - for (i in pos2) { - if (!pos2.hasOwnProperty(i)) continue - - var closestPiece = findClosestPiece(pos1, pos2[i], i) - if (closestPiece) { - animations.push({ - type: 'move', - source: closestPiece, - destination: i, - piece: pos2[i] - }) - - delete pos1[closestPiece] - delete pos2[i] - squaresMovedTo[i] = true - } - } + #doMoveAnimation(animation, onFinishAnimation) { + const { piece: pieceName, source: sourceName, destination: destinationName } = animation; - // "add" animations - for (i in pos2) { - if (!pos2.hasOwnProperty(i)) continue - - animations.push({ - type: 'add', - square: i, - piece: pos2[i] - }) - - delete pos2[i] - } - - // "clear" animations - for (i in pos1) { - if (!pos1.hasOwnProperty(i)) continue - - // do not clear a piece if it is on a square that is the result - // of a "move", ie: a piece capture - if (squaresMovedTo.hasOwnProperty(i)) continue - - animations.push({ - type: 'clear', - square: i, - piece: pos1[i] - }) - - delete pos1[i] - } - - return animations - } - - // ------------------------------------------------------------------------- - // Control Flow - // ------------------------------------------------------------------------- - - function drawPositionInstant () { - // clear the board - $board.find('.' + CSS.piece).remove() - - // add the pieces - for (var i in currentPosition) { - if (!currentPosition.hasOwnProperty(i)) continue - - $('#' + squareElsIds[i]).append(buildPieceHTML(currentPosition[i])) - } - } - - function drawBoard () { - $board.html(buildBoardHTML(currentOrientation, squareSize, config.showNotation)) - drawPositionInstant() - - if (config.sparePieces) { - if (currentOrientation === 'white') { - $sparePiecesTop.html(buildSparePiecesHTML('black')) - $sparePiecesBottom.html(buildSparePiecesHTML('white')) - } else { - $sparePiecesTop.html(buildSparePiecesHTML('white')) - $sparePiecesBottom.html(buildSparePiecesHTML('black')) - } - } - } - - function setCurrentPosition (position) { - var oldPos = deepCopy(currentPosition) - var newPos = deepCopy(position) - var oldFen = objToFen(oldPos) - var newFen = objToFen(newPos) - - // do nothing if no change in position - if (oldFen === newFen) return - - // run their onChange function - if (isFunction(config.onChange)) { - config.onChange(oldPos, newPos) - } - - // update state - currentPosition = position - } - - function isXYOnSquare (x, y) { - for (var i in squareElsOffsets) { - if (!squareElsOffsets.hasOwnProperty(i)) continue - - var s = squareElsOffsets[i] - if (x >= s.left && - x < s.left + squareSize && - y >= s.top && - y < s.top + squareSize) { - return i - } - } - - return 'offboard' - } - - // records the XY coords of every square into memory - function captureSquareOffsets () { - squareElsOffsets = {} - - for (var i in squareElsIds) { - if (!squareElsIds.hasOwnProperty(i)) continue - - squareElsOffsets[i] = $('#' + squareElsIds[i]).offset() - } - } - - function removeSquareHighlights () { - $board - .find('.' + CSS.square) - .removeClass(CSS.highlight1 + ' ' + CSS.highlight2) - } - - function snapbackDraggedPiece () { - // there is no "snapback" for spare pieces - if (draggedPieceSource === 'spare') { - trashDraggedPiece() - return - } - - removeSquareHighlights() - - // animation complete - function complete () { - drawPositionInstant() - $draggedPiece.css('display', 'none') - - // run their onSnapbackEnd function - if (isFunction(config.onSnapbackEnd)) { - config.onSnapbackEnd( - draggedPiece, - draggedPieceSource, - deepCopy(currentPosition), - currentOrientation - ) - } - } - - // get source square position - var sourceSquarePosition = $('#' + squareElsIds[draggedPieceSource]).offset() - - // animate the piece to the target square - var opts = { - duration: config.snapbackSpeed, - complete: complete - } - $draggedPiece.animate(sourceSquarePosition, opts) - - // set state - isDragging = false - } - - function trashDraggedPiece () { - removeSquareHighlights() - - // remove the source piece - var newPosition = deepCopy(currentPosition) - delete newPosition[draggedPieceSource] - setCurrentPosition(newPosition) - - // redraw the position - drawPositionInstant() - - // hide the dragged piece - $draggedPiece.fadeOut(config.trashSpeed) - - // set state - isDragging = false - } - - function dropDraggedPieceOnSquare (square) { - removeSquareHighlights() - - // update position - var newPosition = deepCopy(currentPosition) - delete newPosition[draggedPieceSource] - newPosition[square] = draggedPiece - setCurrentPosition(newPosition) - - // get target square information - var targetSquarePosition = $('#' + squareElsIds[square]).offset() - - // animation complete - function onAnimationComplete () { - drawPositionInstant() - $draggedPiece.css('display', 'none') - - // execute their onSnapEnd function - if (isFunction(config.onSnapEnd)) { - config.onSnapEnd(draggedPieceSource, square, draggedPiece) - } - } - - // snap the piece to the target square - var opts = { - duration: config.snapSpeed, - complete: onAnimationComplete - } - $draggedPiece.animate(targetSquarePosition, opts) - - // set state - isDragging = false - } - - function beginDraggingPiece (source, piece, x, y) { - // run their custom onDragStart function - // their custom onDragStart function can cancel drag start - if (isFunction(config.onDragStart) && - config.onDragStart(source, piece, deepCopy(currentPosition), currentOrientation) === false) { - return - } - - // set state - isDragging = true - draggedPiece = piece - draggedPieceSource = source - - // if the piece came from spare pieces, location is offboard - if (source === 'spare') { - draggedPieceLocation = 'offboard' - } else { - draggedPieceLocation = source - } - - // capture the x, y coords of all squares in memory - captureSquareOffsets() - - // create the dragged piece - $draggedPiece.attr('src', buildPieceImgSrc(piece)).css({ - display: '', - position: 'absolute', - left: x - squareSize / 2, - top: y - squareSize / 2 - }) - - if (source !== 'spare') { - // highlight the source square and hide the piece - $('#' + squareElsIds[source]) - .addClass(CSS.highlight1) - .find('.' + CSS.piece) - .css('display', 'none') - } - } - - function updateDraggedPiece (x, y) { - // put the dragged piece over the mouse cursor - $draggedPiece.css({ - left: x - squareSize / 2, - top: y - squareSize / 2 - }) - - // get location - var location = isXYOnSquare(x, y) - - // do nothing if the location has not changed - if (location === draggedPieceLocation) return - - // remove highlight from previous square - if (validSquare(draggedPieceLocation)) { - $('#' + squareElsIds[draggedPieceLocation]).removeClass(CSS.highlight2) - } - - // add highlight to new square - if (validSquare(location)) { - $('#' + squareElsIds[location]).addClass(CSS.highlight2) - } - - // run onDragMove - if (isFunction(config.onDragMove)) { - config.onDragMove( - location, - draggedPieceLocation, - draggedPieceSource, - draggedPiece, - deepCopy(currentPosition), - currentOrientation - ) - } - - // update state - draggedPieceLocation = location - } - - function stopDraggedPiece (location) { - // determine what the action should be - var action = 'drop' - if (location === 'offboard' && config.dropOffBoard === 'snapback') { - action = 'snapback' - } - if (location === 'offboard' && config.dropOffBoard === 'trash') { - action = 'trash' - } - - // run their onDrop function, which can potentially change the drop action - if (isFunction(config.onDrop)) { - var newPosition = deepCopy(currentPosition) - - // source piece is a spare piece and position is off the board - // if (draggedPieceSource === 'spare' && location === 'offboard') {...} - // position has not changed; do nothing - - // source piece is a spare piece and position is on the board - if (draggedPieceSource === 'spare' && validSquare(location)) { - // add the piece to the board - newPosition[location] = draggedPiece - } - - // source piece was on the board and position is off the board - if (validSquare(draggedPieceSource) && location === 'offboard') { - // remove the piece from the board - delete newPosition[draggedPieceSource] - } - - // source piece was on the board and position is on the board - if (validSquare(draggedPieceSource) && validSquare(location)) { - // move the piece - delete newPosition[draggedPieceSource] - newPosition[location] = draggedPiece - } - - var oldPosition = deepCopy(currentPosition) - - var result = config.onDrop( - draggedPieceSource, - location, - draggedPiece, - newPosition, - oldPosition, - currentOrientation - ) - if (result === 'snapback' || result === 'trash') { - action = result - } - } - - // do it! - if (action === 'snapback') { - snapbackDraggedPiece() - } else if (action === 'trash') { - trashDraggedPiece() - } else if (action === 'drop') { - dropDraggedPieceOnSquare(location) - } - } - - // ------------------------------------------------------------------------- - // Public Methods - // ------------------------------------------------------------------------- - - // clear the board - widget.clear = function (useAnimation) { - widget.position({}, useAnimation) - } - - // remove the widget from the page - widget.destroy = function () { - // remove markup - $container.html('') - $draggedPiece.remove() - - // remove event handlers - $container.unbind() - } - - // shorthand method to get the current FEN - widget.fen = function () { - return widget.position('fen') - } - - // flip orientation - widget.flip = function () { - return widget.orientation('flip') - } + const source = /** @type {HTMLElement} */(this.#squares.get(sourceName)); + const destination = /** @type {HTMLElement} */(this.#squares.get(destinationName)); - // move pieces - // TODO: this method should be variadic as well as accept an array of moves - widget.move = function () { - // no need to throw an error here; just do nothing - // TODO: this should return the current position - if (arguments.length === 0) return - - var useAnimation = true - - // collect the moves into an object - var moves = {} - for (var i = 0; i < arguments.length; i++) { - // any "false" to this function means no animations - if (arguments[i] === false) { - useAnimation = false - continue - } - - // skip invalid arguments - if (!validMove(arguments[i])) { - error(2826, 'Invalid move passed to the move method.', arguments[i]) - continue - } - - var tmp = arguments[i].split('-') - moves[tmp[0]] = tmp[1] - } - - // calculate position from moves - var newPos = calculatePositionFromMoves(currentPosition, moves) - - // update the board - widget.position(newPos, useAnimation) - - // return the new position object - return newPos - } - - widget.orientation = function (arg) { - // no arguments, return the current orientation - if (arguments.length === 0) { - return currentOrientation - } - - // set to white or black - if (arg === 'white' || arg === 'black') { - currentOrientation = arg - drawBoard() - return currentOrientation - } - - // flip orientation - if (arg === 'flip') { - currentOrientation = currentOrientation === 'white' ? 'black' : 'white' - drawBoard() - return currentOrientation - } - - error(5482, 'Invalid value passed to the orientation method.', arg) + const piece = this.#pieces.get(sourceName); + if (piece == null) { + throw new Error(`Unable to locate piece for square ${sourceName}`); } + document.body.appendChild(piece); + piece.style.setProperty('position', 'absolute'); + + piece + .animate( + [getJqueryStyleOffset(source), getJqueryStyleOffset(destination)], + this.#config.moveSpeed + ) + .onfinish = () => { + piece.style.removeProperty('position'); + + this.#pieces.delete(sourceName); + this.#position.delete(sourceName); + this.#pieces.get(destinationName)?.remove(); + this.#pieces.set(destinationName, piece); + this.#position.set(destinationName, pieceName); + + destination.appendChild(piece); + onFinishAnimation(); + }; + } - widget.position = function (position, useAnimation) { - // no arguments, return the current position - if (arguments.length === 0) { - return deepCopy(currentPosition) - } - - // get position as FEN - if (isString(position) && position.toLowerCase() === 'fen') { - return objToFen(currentPosition) + // ------------------------ + // Control Flow + // ------------------------ + #clearPieces() { + if (this.#pieces.size === 0) return; + + for (const piece of this.#pieces.values()) { + if (this.#config.useAnimation) { + piece + .animate([{ opacity: 1 }, { opacity: 0 }], this.#config.trashSpeed) + .onfinish = () => piece.remove(); + } else { + piece.remove(); } - - // start position - if (isString(position) && position.toLowerCase() === 'start') { - position = deepCopy(START_POSITION) - } - - // convert FEN to position object - if (validFen(position)) { - position = fenToObj(position) - } - - // validate position object - if (!validPositionObject(position)) { - error(6482, 'Invalid value passed to the position method.', position) - return - } - - // default for useAnimations is true - if (useAnimation !== false) useAnimation = true - - if (useAnimation) { - // start the animations - var animations = calculateAnimations(currentPosition, position) - doAnimations(animations, currentPosition, position) - - // set the new position - setCurrentPosition(position) - } else { - // instant update - setCurrentPosition(position) - drawPositionInstant() - } } + this.#pieces.clear(); + this.#removeSquareHighlights(); + } - widget.resize = function () { - // calulate the new square size - squareSize = calculateSquareSize() - - // set board width - $board.css('width', squareSize * 8 + 'px') - - // set drag piece size - $draggedPiece.css({ - height: squareSize, - width: squareSize - }) - - // spare pieces - if (config.sparePieces) { - $container - .find('.' + CSS.sparePieces) - .css('paddingLeft', squareSize + boardBorderSize + 'px') - } - - // redraw the board - drawBoard() + #drawPosition() { + this.#clearPieces(); + + for (const [squareName, pieceName] of this.#position.entries()) { + const square = this.#squares.get(squareName); + if (square != null) { + const piece = this.#buildPiece(pieceName); + piece.dataset.square = squareName; + this.#pieces.set(squareName, piece); + square.appendChild(piece); + } } + } + + #drawBoard() { + this.#clearPieces(); + this.#board.style.setProperty('width', `${this.squareSize * 8}px`); - // set the starting position - widget.start = function (useAnimation) { - widget.position('start', useAnimation) + // algebraic notation / orientation + const alpha = COLUMNS.slice(); + let currentRow = 8; + if (this.#orientation === 'black') { + alpha.reverse(); + currentRow = 1; } - // ------------------------------------------------------------------------- - // Browser Events - // ------------------------------------------------------------------------- + this.#board.replaceChildren(); + + let squareColor = 'white'; + for (let i = 0; i < 8; i++) { + const row = document.createElement('row'); + row.classList.add(ClassNameLookup.row); + + for (let j = 0; j < 8; j++) { + const { squareSize } = this; + const squareName = alpha[j] + currentRow; + const square = document.createElement('div'); + + square.id = `${squareName}-${uuid()}`; + square.dataset.square = squareName; + square.classList.add(ClassNameLookup.square, ClassNameLookup[squareColor], `square-${squareName}`); + square.style.setProperty('height', `${squareSize}px`); + square.style.setProperty('width', `${squareSize}px`); + + square.ondrop = (evt) => { + evt.preventDefault(); + if (evt.currentTarget != null && evt.dataTransfer != null) { + const source = evt.dataTransfer.getData("source"); + const pieceName = evt.dataTransfer.getData("pieceName"); + const piece = source === "spare" + ? this.#buildPiece(pieceName) + : /** @type {HTMLElement} */(this.#pieces.get(source)); + + piece.dataset.square = squareName; + + this.#pieces.get(source)?.remove(); + this.#pieces.delete(source); + this.#pieces.get(squareName)?.remove(); + this.#pieces.set(squareName, piece); + + this.#position.delete(source); + this.#position.set(squareName, pieceName); + + /** @type {HTMLElement} */(evt.currentTarget).appendChild(piece); + this.#removeSquareHighlights(); + } + }; + square.ondragover = (evt) => { + evt.preventDefault(); + if (evt.target != null && evt.dataTransfer != null) { + /** @type {HTMLElement} */(evt.target).classList.add(ClassNameLookup.highlight1); + evt.dataTransfer.dropEffect = "move"; + } + return false; + }; + square.ondragleave = (evt) => { + evt.preventDefault(); + if (evt.target != null) { + /** @type {HTMLElement} */(evt.target).classList.remove(ClassNameLookup.highlight1); + } + }; + + if (this.#config.showNotation) { + // alpha notation + if ((this.#orientation === 'white' && currentRow === 1) || + (this.#orientation === 'black' && currentRow === 8)) { + const notation = document.createElement('div'); + notation.classList.add(ClassNameLookup.notation, ClassNameLookup.alpha); + notation.innerText = alpha[j]; + square.appendChild(notation); + } + + // numeric notation + if (j === 0) { + const notation = document.createElement('div'); + notation.classList.add(ClassNameLookup.notation, ClassNameLookup.numeric); + notation.innerText = currentRow.toString(); + square.appendChild(notation); + } + } + + row.appendChild(square); + squareColor = (squareColor === 'white') ? 'black' : 'white'; + + this.#squares.set(squareName, square); + } - function stopDefault (evt) { - evt.preventDefault() - } - - function mousedownSquare (evt) { - // do nothing if we're not draggable - if (!config.draggable) return + const clearfix = document.createElement('div'); + clearfix.classList.add(ClassNameLookup.clearfix); - // do nothing if there is no piece on this square - var square = $(this).attr('data-square') - if (!validSquare(square)) return - if (!currentPosition.hasOwnProperty(square)) return + row.appendChild(clearfix); + this.#board.appendChild(row); - beginDraggingPiece(square, currentPosition[square], evt.pageX, evt.pageY) + squareColor = (squareColor === 'white') ? 'black' : 'white'; + + if (this.#orientation === 'white') { + currentRow = currentRow - 1; + } else { + currentRow = currentRow + 1; + } } - function touchstartSquare (e) { - // do nothing if we're not draggable - if (!config.draggable) return - - // do nothing if there is no piece on this square - var square = $(this).attr('data-square') - if (!validSquare(square)) return - if (!currentPosition.hasOwnProperty(square)) return - - e = e.originalEvent - beginDraggingPiece( - square, - currentPosition[square], - e.changedTouches[0].pageX, - e.changedTouches[0].pageY - ) - } - - function mousedownSparePiece (evt) { - // do nothing if sparePieces is not enabled - if (!config.sparePieces) return - - var piece = $(this).attr('data-piece') - - beginDraggingPiece('spare', piece, evt.pageX, evt.pageY) - } - - function touchstartSparePiece (e) { - // do nothing if sparePieces is not enabled - if (!config.sparePieces) return - - var piece = $(this).attr('data-piece') - - e = e.originalEvent - beginDraggingPiece( - 'spare', - piece, - e.changedTouches[0].pageX, - e.changedTouches[0].pageY - ) - } - - function mousemoveWindow (evt) { - if (isDragging) { - updateDraggedPiece(evt.pageX, evt.pageY) + if (this.#config.sparePieces) { + this.#topShelf.replaceChildren(); + this.#bottomShelf.replaceChildren(); + for (const [pieceName, piece] of this.#sparePieces.entries()) { + if (this.#orientation === 'white') { + if (pieceName.startsWith('w')) { + this.#bottomShelf.appendChild(piece); + } else { + this.#topShelf.appendChild(piece); + } + } else { + if (pieceName.startsWith('w')) { + this.#topShelf.appendChild(piece); + } else { + this.#bottomShelf.appendChild(piece); + } + } } } + } - var throttledMousemoveWindow = throttle(mousemoveWindow, config.dragThrottleRate) - - function touchmoveWindow (evt) { - // do nothing if we are not dragging a piece - if (!isDragging) return - - // prevent screen from scrolling - evt.preventDefault() - - updateDraggedPiece(evt.originalEvent.changedTouches[0].pageX, - evt.originalEvent.changedTouches[0].pageY) - } - - var throttledTouchmoveWindow = throttle(touchmoveWindow, config.dragThrottleRate) - - function mouseupWindow (evt) { - // do nothing if we are not dragging a piece - if (!isDragging) return - - // get the location - var location = isXYOnSquare(evt.pageX, evt.pageY) - - stopDraggedPiece(location) - } - - function touchendWindow (evt) { - // do nothing if we are not dragging a piece - if (!isDragging) return - - // get the location - var location = isXYOnSquare(evt.originalEvent.changedTouches[0].pageX, - evt.originalEvent.changedTouches[0].pageY) - - stopDraggedPiece(location) - } - - function mouseenterSquare (evt) { - // do not fire this event if we are dragging a piece - // NOTE: this should never happen, but it's a safeguard - if (isDragging) return - - // exit if they did not provide a onMouseoverSquare function - if (!isFunction(config.onMouseoverSquare)) return - - // get the square - var square = $(evt.currentTarget).attr('data-square') - - // NOTE: this should never happen; defensive - if (!validSquare(square)) return - - // get the piece on this square - var piece = false - if (currentPosition.hasOwnProperty(square)) { - piece = currentPosition[square] - } - - // execute their function - config.onMouseoverSquare(square, piece, deepCopy(currentPosition), currentOrientation) - } - - function mouseleaveSquare (evt) { - // do not fire this event if we are dragging a piece - // NOTE: this should never happen, but it's a safeguard - if (isDragging) return - - // exit if they did not provide an onMouseoutSquare function - if (!isFunction(config.onMouseoutSquare)) return - - // get the square - var square = $(evt.currentTarget).attr('data-square') - - // NOTE: this should never happen; defensive - if (!validSquare(square)) return - - // get the piece on this square - var piece = false - if (currentPosition.hasOwnProperty(square)) { - piece = currentPosition[square] - } - - // execute their function - config.onMouseoutSquare(square, piece, deepCopy(currentPosition), currentOrientation) - } - - // ------------------------------------------------------------------------- - // Initialization - // ------------------------------------------------------------------------- - - function addEvents () { - // prevent "image drag" - $('body').on('mousedown mousemove', '.' + CSS.piece, stopDefault) - - // mouse drag pieces - $board.on('mousedown', '.' + CSS.square, mousedownSquare) - $container.on('mousedown', '.' + CSS.sparePieces + ' .' + CSS.piece, mousedownSparePiece) - - // mouse enter / leave square - $board - .on('mouseenter', '.' + CSS.square, mouseenterSquare) - .on('mouseleave', '.' + CSS.square, mouseleaveSquare) - - // piece drag - var $window = $(window) - $window - .on('mousemove', throttledMousemoveWindow) - .on('mouseup', mouseupWindow) - - // touch drag pieces - if (isTouchDevice()) { - $board.on('touchstart', '.' + CSS.square, touchstartSquare) - $container.on('touchstart', '.' + CSS.sparePieces + ' .' + CSS.piece, touchstartSparePiece) - $window - .on('touchmove', throttledTouchmoveWindow) - .on('touchend', touchendWindow) - } - } - - function initDOM () { - // create unique IDs for all the elements we will create - createElIds() - - // build board and save it in memory - $container.html(buildContainerHTML(config.sparePieces)) - $board = $container.find('.' + CSS.board) - - if (config.sparePieces) { - $sparePiecesTop = $container.find('.' + CSS.sparePiecesTop) - $sparePiecesBottom = $container.find('.' + CSS.sparePiecesBottom) - } - - // create the drag piece - var draggedPieceId = uuid() - $('body').append(buildPieceHTML('wP', true, draggedPieceId)) - $draggedPiece = $('#' + draggedPieceId) - - // TODO: need to remove this dragged piece element if the board is no - // longer in the DOM - - // get the border size - boardBorderSize = parseInt($board.css('borderLeftWidth'), 10) - - // set the size and draw the board - widget.resize() - } - - // ------------------------------------------------------------------------- - // Initialization - // ------------------------------------------------------------------------- - - setInitialState() - initDOM() - addEvents() - - // return the widget object - return widget - } // end constructor - - // TODO: do module exports here - window['Chessboard'] = constructor - - // support legacy ChessBoard name - window['ChessBoard'] = window['Chessboard'] - - // expose util functions - window['Chessboard']['fenToObj'] = fenToObj - window['Chessboard']['objToFen'] = objToFen -})() // end anonymous wrapper - -/* export Chessboard object if using node or any other CommonJS compatible - * environment */ -if (typeof exports !== 'undefined') { - exports.Chessboard = window.Chessboard + #removeSquareHighlights() { + for (const square of this.#squares.values()) { + square.classList.remove(ClassNameLookup.highlight1); + square.classList.remove(ClassNameLookup.highlight2); + } + } } + +export { + Chessboard, + fenToObj, + objToFen, + validFen, + validPieceCode, + validPositionObject, + validSquare, + START_FEN, + START_POSITION, +}; diff --git a/lib/chessboard.test.js b/lib/chessboard.test.js new file mode 100644 index 00000000..5a5447ba --- /dev/null +++ b/lib/chessboard.test.js @@ -0,0 +1,61 @@ +/* eslint-env node */ +import assert from 'node:assert'; +import test from 'node:test'; + +import { objToFen, START_FEN, START_POSITION, validFen, validPieceCode, validPositionObject, validSquare } from './chessboard.js'; + +test('objToFen', () => { + assert.equal(objToFen(START_POSITION), START_FEN); + assert.equal(objToFen({}), '8/8/8/8/8/8/8/8'); + assert.equal(objToFen({ a2: 'wP', b2: 'bP' }), '8/8/8/8/8/8/Pp6/8'); +}); + +test('validSquare', () => { + assert.ok(validSquare('a1')); + assert.ok(validSquare('e2')); + assert.ok(!validSquare('D2')); + assert.ok(!validSquare('g9')); + assert.ok(!validSquare('a')); + assert.ok(!validSquare(true)); + assert.ok(!validSquare(null)); + assert.ok(!validSquare({})); +}); + +test('validPieceCode', () => { + assert.ok(validPieceCode('bP')); + assert.ok(validPieceCode('bK')); + assert.ok(validPieceCode('wK')); + assert.ok(validPieceCode('wR')); + assert.ok(!validPieceCode('WR')); + assert.ok(!validPieceCode('Wr')); + assert.ok(!validPieceCode('a')); + assert.ok(!validPieceCode(true)); + assert.ok(!validPieceCode(null)); + assert.ok(!validPieceCode({})); +}); + +test('validFen', () => { + assert.ok(validFen(START_FEN)); + assert.ok(validFen('8/8/8/8/8/8/8/8')); + assert.ok(validFen('r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R')); + assert.ok(validFen('3r3r/1p4pp/2nb1k2/pP3p2/8/PB2PN2/p4PPP/R4RK1 b - - 0 1')); + assert.ok(!validFen('3r3z/1p4pp/2nb1k2/pP3p2/8/PB2PN2/p4PPP/R4RK1 b - - 0 1')); + assert.ok(!validFen('anbqkbnr/8/8/8/8/8/PPPPPPPP/8')); + assert.ok(!validFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/')); + assert.ok(!validFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBN')); + assert.ok(!validFen('888888/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR')); + assert.ok(!validFen('888888/pppppppp/74/8/8/8/PPPPPPPP/RNBQKBNR')); + assert.ok(!validFen('')); +}); + +test('validPositionObject', () => { + assert.ok(validPositionObject(START_POSITION)); + assert.ok(validPositionObject({})); + assert.ok(validPositionObject({ e2: 'wP' })); + assert.ok(validPositionObject({ e2: 'wP', d2: 'wP' })); + assert.ok(!validPositionObject({ e2: 'BP' })); + assert.ok(!validPositionObject({ y2: 'wP' })); + assert.ok(!validPositionObject(null)); + assert.ok(!validPositionObject('start')); + assert.ok(!validPositionObject(START_FEN)); +}); diff --git a/lib/pieces.svg.js b/lib/pieces.svg.js new file mode 100644 index 00000000..fe09cdf1 --- /dev/null +++ b/lib/pieces.svg.js @@ -0,0 +1,36 @@ +/** + * Piece SVGs courtesy of Wikimedia Commons + * @see https://commons.wikimedia.org/wiki/Category:SVG_chess_pieces + */ + +/** + * White + */ + +export const wK = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg fill='none' fill-rule='evenodd' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5'%3E%3Cpath stroke-linejoin='miter' d='M22.5 11.63V6M20 8h5'/%3E%3Cpath fill='%23fff' stroke-linecap='butt' stroke-linejoin='miter' d='M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5'/%3E%3Cpath fill='%23fff' d='M12.5 37c5.5 3.5 14.5 3.5 20 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-2.5-7.5-12-10.5-16-4-3 6 6 10.5 6 10.5v7'/%3E%3Cpath d='M12.5 30c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0'/%3E%3C/g%3E%3C/svg%3E`; + +export const wQ = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='fill:%23ffffff;stroke:%23000000;stroke-width:1.5;stroke-linejoin:round'%3E%3Cpath d='M 9,26 C 17.5,24.5 30,24.5 36,26 L 38.5,13.5 L 31,25 L 30.7,10.9 L 25.5,24.5 L 22.5,10 L 19.5,24.5 L 14.3,10.9 L 14,25 L 6.5,13.5 L 9,26 z'/%3E%3Cpath d='M 9,26 C 9,28 10.5,28 11.5,30 C 12.5,31.5 12.5,31 12,33.5 C 10.5,34.5 11,36 11,36 C 9.5,37.5 11,38.5 11,38.5 C 17.5,39.5 27.5,39.5 34,38.5 C 34,38.5 35.5,37.5 34,36 C 34,36 34.5,34.5 33,33.5 C 32.5,31 32.5,31.5 33.5,30 C 34.5,28 36,28 36,26 C 27.5,24.5 17.5,24.5 9,26 z'/%3E%3Cpath d='M 11.5,30 C 15,29 30,29 33.5,30' style='fill:none'/%3E%3Cpath d='M 12,33.5 C 18,32.5 27,32.5 33,33.5' style='fill:none'/%3E%3Ccircle cx='6' cy='12' r='2' /%3E%3Ccircle cx='14' cy='9' r='2' /%3E%3Ccircle cx='22.5' cy='8' r='2' /%3E%3Ccircle cx='31' cy='9' r='2' /%3E%3Ccircle cx='39' cy='12' r='2' /%3E%3C/g%3E%3C/svg%3E`; + +export const wB = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='opacity:1; fill:none; fill-rule:evenodd; fill-opacity:1; stroke:%23000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;' transform='translate(0,0.6)'%3E%3Cg style='fill:%23ffffff; stroke:%23000000; stroke-linecap:butt;'%3E%3Cpath d='M 9,36 C 12.39,35.03 19.11,36.43 22.5,34 C 25.89,36.43 32.61,35.03 36,36 C 36,36 37.65,36.54 39,38 C 38.32,38.97 37.35,38.99 36,38.5 C 32.61,37.53 25.89,38.96 22.5,37.5 C 19.11,38.96 12.39,37.53 9,38.5 C 7.65,38.99 6.68,38.97 6,38 C 7.35,36.54 9,36 9,36 z'/%3E%3Cpath d='M 15,32 C 17.5,34.5 27.5,34.5 30,32 C 30.5,30.5 30,30 30,30 C 30,27.5 27.5,26 27.5,26 C 33,24.5 33.5,14.5 22.5,10.5 C 11.5,14.5 12,24.5 17.5,26 C 17.5,26 15,27.5 15,30 C 15,30 14.5,30.5 15,32 z'/%3E%3Cpath d='M 25 8 A 2.5 2.5 0 1 1 20,8 A 2.5 2.5 0 1 1 25 8 z'/%3E%3C/g%3E%3Cpath d='M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18' style='fill:none; stroke:%23000000; stroke-linejoin:miter;'/%3E%3C/g%3E%3C/svg%3E`; + +export const wN = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='opacity:1; fill:none; fill-opacity:1; fill-rule:evenodd; stroke:%23000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;' transform='translate(0,0.3)'%3E%3Cpath d='M 22,10 C 32.5,11 38.5,18 38,39 L 15,39 C 15,30 25,32.5 23,18' style='fill:%23ffffff; stroke:%23000000;' /%3E%3Cpath d='M 24,18 C 24.38,20.91 18.45,25.37 16,27 C 13,29 13.18,31.34 11,31 C 9.958,30.06 12.41,27.96 11,28 C 10,28 11.19,29.23 10,30 C 9,30 5.997,31 6,26 C 6,24 12,14 12,14 C 12,14 13.89,12.1 14,10.5 C 13.27,9.506 13.5,8.5 13.5,7.5 C 14.5,6.5 16.5,10 16.5,10 L 18.5,10 C 18.5,10 19.28,8.008 21,7 C 22,7 22,10 22,10' style='fill:%23ffffff; stroke:%23000000;' /%3E%3Cpath d='M 9.5 25.5 A 0.5 0.5 0 1 1 8.5,25.5 A 0.5 0.5 0 1 1 9.5 25.5 z' style='fill:%23000000; stroke:%23000000;' /%3E%3Cpath d='M 15 15.5 A 0.5 1.5 0 1 1 14,15.5 A 0.5 1.5 0 1 1 15 15.5 z' transform='matrix(0.866,0.5,-0.5,0.866,9.693,-5.173)' style='fill:%23000000; stroke:%23000000;' /%3E%3C/g%3E%3C/svg%3E`; + +export const wR = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='opacity:1; fill:%23ffffff; fill-opacity:1; fill-rule:evenodd; stroke:%23000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;' transform='translate(0,0.3)'%3E%3Cpath d='M 9,39 L 36,39 L 36,36 L 9,36 L 9,39 z ' style='stroke-linecap:butt;' /%3E%3Cpath d='M 12,36 L 12,32 L 33,32 L 33,36 L 12,36 z ' style='stroke-linecap:butt;' /%3E%3Cpath d='M 11,14 L 11,9 L 15,9 L 15,11 L 20,11 L 20,9 L 25,9 L 25,11 L 30,11 L 30,9 L 34,9 L 34,14' style='stroke-linecap:butt;' /%3E%3Cpath d='M 34,14 L 31,17 L 14,17 L 11,14' /%3E%3Cpath d='M 31,17 L 31,29.5 L 14,29.5 L 14,17' style='stroke-linecap:butt; stroke-linejoin:miter;' /%3E%3Cpath d='M 31,29.5 L 32.5,32 L 12.5,32 L 14,29.5' /%3E%3Cpath d='M 11,14 L 34,14' style='fill:none; stroke:%23000000; stroke-linejoin:miter;' /%3E%3C/g%3E%3C/svg%3E`; + +export const wP = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cpath d='m 22.5,9 c -2.21,0 -4,1.79 -4,4 0,0.89 0.29,1.71 0.78,2.38 C 17.33,16.5 16,18.59 16,21 c 0,2.03 0.94,3.84 2.41,5.03 C 15.41,27.09 11,31.58 11,39.5 H 34 C 34,31.58 29.59,27.09 26.59,26.03 28.06,24.84 29,23.03 29,21 29,18.59 27.67,16.5 25.72,15.38 26.21,14.71 26.5,13.89 26.5,13 c 0,-2.21 -1.79,-4 -4,-4 z' style='opacity:1; fill:%23ffffff; fill-opacity:1; fill-rule:nonzero; stroke:%23000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;'/%3E%3C/svg%3E`; + +/** + * Black + */ + +export const bK = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='fill:none; fill-opacity:1; fill-rule:evenodd; stroke:%23000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;'%3E%3Cpath d='M 22.5,11.63 L 22.5,6' style='fill:none; stroke:%23000000; stroke-linejoin:miter;' id='path6570'/%3E%3Cpath d='M 22.5,25 C 22.5,25 27,17.5 25.5,14.5 C 25.5,14.5 24.5,12 22.5,12 C 20.5,12 19.5,14.5 19.5,14.5 C 18,17.5 22.5,25 22.5,25' style='fill:%23000000;fill-opacity:1; stroke-linecap:butt; stroke-linejoin:miter;'/%3E%3Cpath d='M 12.5,37 C 18,40.5 27,40.5 32.5,37 L 32.5,30 C 32.5,30 41.5,25.5 38.5,19.5 C 34.5,13 25,16 22.5,23.5 L 22.5,27 L 22.5,23.5 C 20,16 10.5,13 6.5,19.5 C 3.5,25.5 12.5,30 12.5,30 L 12.5,37' style='fill:%23000000; stroke:%23000000;'/%3E%3Cpath d='M 20,8 L 25,8' style='fill:none; stroke:%23000000; stroke-linejoin:miter;'/%3E%3Cpath d='M 32,29.5 C 32,29.5 40.5,25.5 38.03,19.85 C 34.15,14 25,18 22.5,24.5 L 22.5,26.6 L 22.5,24.5 C 20,18 10.85,14 6.97,19.85 C 4.5,25.5 13,29.5 13,29.5' style='fill:none; stroke:%23ffffff;'/%3E%3Cpath d='M 12.5,30 C 18,27 27,27 32.5,30 M 12.5,33.5 C 18,30.5 27,30.5 32.5,33.5 M 12.5,37 C 18,34 27,34 32.5,37' style='fill:none; stroke:%23ffffff;'/%3E%3C/g%3E%3C/svg%3E`; + +export const bQ = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='fill:%23000000;stroke:%23000000;stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round'%3E%3Cpath d='M 9,26 C 17.5,24.5 30,24.5 36,26 L 38.5,13.5 L 31,25 L 30.7,10.9 L 25.5,24.5 L 22.5,10 L 19.5,24.5 L 14.3,10.9 L 14,25 L 6.5,13.5 L 9,26 z' style='stroke-linecap:butt;fill:%23000000' /%3E%3Cpath d='m 9,26 c 0,2 1.5,2 2.5,4 1,1.5 1,1 0.5,3.5 -1.5,1 -1,2.5 -1,2.5 -1.5,1.5 0,2.5 0,2.5 6.5,1 16.5,1 23,0 0,0 1.5,-1 0,-2.5 0,0 0.5,-1.5 -1,-2.5 -0.5,-2.5 -0.5,-2 0.5,-3.5 1,-2 2.5,-2 2.5,-4 -8.5,-1.5 -18.5,-1.5 -27,0 z' /%3E%3Cpath d='M 11.5,30 C 15,29 30,29 33.5,30' /%3E%3Cpath d='m 12,33.5 c 6,-1 15,-1 21,0' /%3E%3Ccircle cx='6' cy='12' r='2' /%3E%3Ccircle cx='14' cy='9' r='2' /%3E%3Ccircle cx='22.5' cy='8' r='2' /%3E%3Ccircle cx='31' cy='9' r='2' /%3E%3Ccircle cx='39' cy='12' r='2' /%3E%3Cpath d='M 11,38.5 A 35,35 1 0 0 34,38.5' style='fill:none; stroke:%23000000;stroke-linecap:butt;' /%3E%3Cg style='fill:none; stroke:%23ffffff;'%3E%3Cpath d='M 11,29 A 35,35 1 0 1 34,29' /%3E%3Cpath d='M 12.5,31.5 L 32.5,31.5' /%3E%3Cpath d='M 11.5,34.5 A 35,35 1 0 0 33.5,34.5' /%3E%3Cpath d='M 10.5,37.5 A 35,35 1 0 0 34.5,37.5' /%3E%3C/g%3E%3C/g%3E%3C/svg%3E`; + +export const bB = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='opacity:1; fill:none; fill-rule:evenodd; fill-opacity:1; stroke:%23000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;' transform='translate(0,0.6)'%3E%3Cg style='fill:%23000000; stroke:%23000000; stroke-linecap:butt;'%3E%3Cpath d='M 9,36 C 12.39,35.03 19.11,36.43 22.5,34 C 25.89,36.43 32.61,35.03 36,36 C 36,36 37.65,36.54 39,38 C 38.32,38.97 37.35,38.99 36,38.5 C 32.61,37.53 25.89,38.96 22.5,37.5 C 19.11,38.96 12.39,37.53 9,38.5 C 7.65,38.99 6.68,38.97 6,38 C 7.35,36.54 9,36 9,36 z'/%3E%3Cpath d='M 15,32 C 17.5,34.5 27.5,34.5 30,32 C 30.5,30.5 30,30 30,30 C 30,27.5 27.5,26 27.5,26 C 33,24.5 33.5,14.5 22.5,10.5 C 11.5,14.5 12,24.5 17.5,26 C 17.5,26 15,27.5 15,30 C 15,30 14.5,30.5 15,32 z'/%3E%3Cpath d='M 25 8 A 2.5 2.5 0 1 1 20,8 A 2.5 2.5 0 1 1 25 8 z'/%3E%3C/g%3E%3Cpath d='M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18' style='fill:none; stroke:%23ffffff; stroke-linejoin:miter;'/%3E%3C/g%3E%3C/svg%3E`; + +export const bN = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='opacity:1; fill:none; fill-opacity:1; fill-rule:evenodd; stroke:%23000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;' transform='translate(0,0.3)'%3E%3Cpath d='M 22,10 C 32.5,11 38.5,18 38,39 L 15,39 C 15,30 25,32.5 23,18' style='fill:%23000000; stroke:%23000000;' /%3E%3Cpath d='M 24,18 C 24.38,20.91 18.45,25.37 16,27 C 13,29 13.18,31.34 11,31 C 9.958,30.06 12.41,27.96 11,28 C 10,28 11.19,29.23 10,30 C 9,30 5.997,31 6,26 C 6,24 12,14 12,14 C 12,14 13.89,12.1 14,10.5 C 13.27,9.506 13.5,8.5 13.5,7.5 C 14.5,6.5 16.5,10 16.5,10 L 18.5,10 C 18.5,10 19.28,8.008 21,7 C 22,7 22,10 22,10' style='fill:%23000000; stroke:%23000000;' /%3E%3Cpath d='M 9.5 25.5 A 0.5 0.5 0 1 1 8.5,25.5 A 0.5 0.5 0 1 1 9.5 25.5 z' style='fill:%23ffffff; stroke:%23ffffff;' /%3E%3Cpath d='M 15 15.5 A 0.5 1.5 0 1 1 14,15.5 A 0.5 1.5 0 1 1 15 15.5 z' transform='matrix(0.866,0.5,-0.5,0.866,9.693,-5.173)' style='fill:%23ffffff; stroke:%23ffffff;' /%3E%3Cpath d='M 24.55,10.4 L 24.1,11.85 L 24.6,12 C 27.75,13 30.25,14.49 32.5,18.75 C 34.75,23.01 35.75,29.06 35.25,39 L 35.2,39.5 L 37.45,39.5 L 37.5,39 C 38,28.94 36.62,22.15 34.25,17.66 C 31.88,13.17 28.46,11.02 25.06,10.5 L 24.55,10.4 z ' style='fill:%23ffffff; stroke:none;' /%3E%3C/g%3E%3C/svg%3E`; + +export const bR = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cg style='opacity:1; fill:%23000000; fill-opacity:1; fill-rule:evenodd; stroke:%23000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;' transform='translate(0,0.3)'%3E%3Cpath d='M 9,39 L 36,39 L 36,36 L 9,36 L 9,39 z ' style='stroke-linecap:butt;' /%3E%3Cpath d='M 12.5,32 L 14,29.5 L 31,29.5 L 32.5,32 L 12.5,32 z ' style='stroke-linecap:butt;' /%3E%3Cpath d='M 12,36 L 12,32 L 33,32 L 33,36 L 12,36 z ' style='stroke-linecap:butt;' /%3E%3Cpath d='M 14,29.5 L 14,16.5 L 31,16.5 L 31,29.5 L 14,29.5 z ' style='stroke-linecap:butt;stroke-linejoin:miter;' /%3E%3Cpath d='M 14,16.5 L 11,14 L 34,14 L 31,16.5 L 14,16.5 z ' style='stroke-linecap:butt;' /%3E%3Cpath d='M 11,14 L 11,9 L 15,9 L 15,11 L 20,11 L 20,9 L 25,9 L 25,11 L 30,11 L 30,9 L 34,9 L 34,14 L 11,14 z ' style='stroke-linecap:butt;' /%3E%3Cpath d='M 12,35.5 L 33,35.5 L 33,35.5' style='fill:none; stroke:%23ffffff; stroke-width:1; stroke-linejoin:miter;' /%3E%3Cpath d='M 13,31.5 L 32,31.5' style='fill:none; stroke:%23ffffff; stroke-width:1; stroke-linejoin:miter;' /%3E%3Cpath d='M 14,29.5 L 31,29.5' style='fill:none; stroke:%23ffffff; stroke-width:1; stroke-linejoin:miter;' /%3E%3Cpath d='M 14,16.5 L 31,16.5' style='fill:none; stroke:%23ffffff; stroke-width:1; stroke-linejoin:miter;' /%3E%3Cpath d='M 11,14 L 34,14' style='fill:none; stroke:%23ffffff; stroke-width:1; stroke-linejoin:miter;' /%3E%3C/g%3E%3C/svg%3E`; + +export const bP = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Cpath d='m 22.5,9 c -2.21,0 -4,1.79 -4,4 0,0.89 0.29,1.71 0.78,2.38 C 17.33,16.5 16,18.59 16,21 c 0,2.03 0.94,3.84 2.41,5.03 C 15.41,27.09 11,31.58 11,39.5 H 34 C 34,31.58 29.59,27.09 26.59,26.03 28.06,24.84 29,23.03 29,21 29,18.59 27.67,16.5 25.72,15.38 26.21,14.71 26.5,13.89 26.5,13 c 0,-2.21 -1.79,-4 -4,-4 z' style='opacity:1; fill:%23000000; fill-opacity:1; fill-rule:nonzero; stroke:%23000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;'/%3E%3C/svg%3E`; diff --git a/package.json b/package.json index bf071a30..4360b940 100644 --- a/package.json +++ b/package.json @@ -4,26 +4,41 @@ "description": "JavaScript chessboard widget", "homepage": "https://chessboardjs.com", "license": "MIT", + "type": "module", "version": "1.0.0", "repository": { "type": "git", "url": "git://github.com/oakmac/chessboardjs.git" }, - "files": ["dist/"], - "dependencies": { - "jquery": ">=3.4.1" - }, + "files": [ + "dist/" + ], "devDependencies": { - "csso": "3.5.1", - "fs-plus": "3.1.1", + "@types/node": "18.7.23", + "eslint": "8.24.0", + "gulp": "4.0.2", "kidif": "1.1.0", "mustache": "2.3.0", - "standard": "10.0.2", - "uglify-js": "3.6.0" + "rollup": "2.79.1", + "rollup-plugin-terser": "7.0.2", + "sass": "1.55.0" + }, + "resolutions": { + "chokidar": "3.5.3" }, "scripts": { - "build": "standard lib/chessboard.js && node scripts/build.js", - "standard": "standard --fix lib/*.js website/js/*.js", - "website": "node scripts/website.js" + "lint": "eslint Gulpfile.js lib website/js/examples.js", + "build": "npm run lint -- --fix && gulp", + "start": "gulp watch", + "test": "node --test lib" + }, + "eslintConfig": { + "extends": "eslint:recommended", + "env": { + "es2022": true + }, + "parserOptions": { + "sourceType": "module" + } } } diff --git a/scripts/build.js b/scripts/build.js deleted file mode 100644 index 798bec10..00000000 --- a/scripts/build.js +++ /dev/null @@ -1,46 +0,0 @@ -// ----------------------------------------------------------------------------- -// This file creates a build in the dist/ folder -// ----------------------------------------------------------------------------- - -// libraries -const fs = require('fs-plus') -const csso = require('csso') -const uglify = require('uglify-js') - -const encoding = {encoding: 'utf8'} - -const package = JSON.parse(fs.readFileSync('package.json', encoding)) -const version = package.version -const year = new Date().getFullYear() -const cssSrc = fs.readFileSync('lib/chessboard.css', encoding) - .replace('@VERSION', version) -const jsSrc = fs.readFileSync('lib/chessboard.js', encoding) - .replace('@VERSION', version) - .replace('RUN_ASSERTS = true', 'RUN_ASSERTS = false') - -// TODO: need to remove the RUN_ASSERTS calls from the non-minified file - -const minifiedCSS = csso.minify(cssSrc).css -const uglifyResult = uglify.minify(jsSrc) -const minifiedJS = uglifyResult.code - -// quick sanity checks -console.assert(!uglifyResult.error, 'error minifying JS!') -console.assert(typeof minifiedCSS === 'string' && minifiedCSS !== '', 'error minifying CSS!') - -// add license to the top of minified files -const minifiedJSWithBanner = banner() + minifiedJS - -// create a fresh dist/ folder -fs.removeSync('dist') -fs.makeTreeSync('dist') - -// copy lib files to dist/ -fs.writeFileSync('dist/chessboard-' + version + '.css', cssSrc, encoding) -fs.writeFileSync('dist/chessboard-' + version + '.min.css', minifiedCSS, encoding) -fs.writeFileSync('dist/chessboard-' + version + '.js', jsSrc, encoding) -fs.writeFileSync('dist/chessboard-' + version + '.min.js', minifiedJSWithBanner, encoding) - -function banner () { - return '/*! chessboard.js v' + version + ' | (c) ' + year + ' Chris Oakman | MIT License chessboardjs.com/license */\n' -} diff --git a/scripts/rename.js b/scripts/rename.js deleted file mode 100644 index c6a99385..00000000 --- a/scripts/rename.js +++ /dev/null @@ -1,22 +0,0 @@ -// TODO: this is unfinished / not working - -var fs = require('fs') -var glob = require('glob') - -var exampleFiles = glob.sync('examples/*.example') - -exampleFiles.forEach(processFile) - -function processFile (f) { - var fileContents = fs.readFileSync(f, {encoding:'utf8'}) - var lines = fileContents.split('\n') - - lines = lines.map(processLine) - - fs.writeFileSync(f, lines.join('\n')) -} - -function processLine (line) { - line = line.replace('docs#', 'docs.html#') - return line -} diff --git a/scripts/website.js b/scripts/website.js deleted file mode 100755 index 1b107fa1..00000000 --- a/scripts/website.js +++ /dev/null @@ -1,406 +0,0 @@ -#! /usr/bin/env node - -// ----------------------------------------------------------------------------- -// This file builds the contents of the website/ folder. -// ----------------------------------------------------------------------------- - -// libraries -const fs = require('fs-plus') -const kidif = require('kidif') -const mustache = require('mustache') -const docs = require('../data/docs.json') - -const encoding = {encoding: 'utf8'} - -// toggle development version -const useDevFile = false -const jsCDNLink = '' -const cssCDNLink = 'https://unpkg.com/@chrisoakman/chessboardjs@1.0.0/dist/chessboard-1.0.0.min.css' - -let chessboardJsScript = jsCDNLink -if (useDevFile) { - chessboardJsScript = '' -} - -// grab some mustache templates -const docsTemplate = fs.readFileSync('templates/docs.mustache', encoding) -const downloadTemplate = fs.readFileSync('templates/download.mustache', encoding) -const examplesTemplate = fs.readFileSync('templates/examples.mustache', encoding) -const homepageTemplate = fs.readFileSync('templates/homepage.mustache', encoding) -const singleExampleTemplate = fs.readFileSync('templates/single-example.mustache', encoding) -const licensePageTemplate = fs.readFileSync('templates/license.mustache', encoding) -const headTemplate = fs.readFileSync('templates/_head.mustache', encoding) -const headerTemplate = fs.readFileSync('templates/_header.mustache', encoding) -const footerTemplate = fs.readFileSync('templates/_footer.mustache', encoding) - -const latestChessboardJS = fs.readFileSync('lib/chessboard.js', encoding) -const latestChessboardCSS = fs.readFileSync('lib/chessboard.css', encoding) - -// grab the examples -const examplesArr = kidif('examples/*.example') -console.assert(examplesArr, 'Could not load the Example files') -console.assert(examplesArr.length > 1, 'Zero examples loaded') - -const examplesObj = examplesArr.reduce(function (examplesObj, example, idx) { - examplesObj[example.id] = example - return examplesObj -}, {}) - -const examplesGroups = [ - { - name: 'Basic Usage', - examples: [1000, 1001, 1002, 1003, 1004] - }, - { - name: 'Config', - examples: [2000, 2044, 2063, 2001, 2002, 2003, 2082, 2004, 2030, 2005, 2006] - }, - { - name: 'Methods', - examples: [3000, 3001, 3002, 3003, 3004, 3005, 3006, 3007] - }, - { - name: 'Events', - examples: [4000, 4001, 4002, 4003, 4004, 4005, 4006, 4011, 4012] - }, - { - name: 'Integration', - examples: [5000, 5001, 5002, 5003, 5004, 5005] - } -] - -const homepageExample1 = '' - -const homepageExample2 = ` -var board2 = Chessboard('board2', { - draggable: true, - dropOffBoard: 'trash', - sparePieces: true -}) - -$('#startBtn').on('click', board2.start) -$('#clearBtn').on('click', board2.clear)`.trim() - -function writeSrcFiles () { - fs.writeFileSync('website/js/chessboard.js', latestChessboardJS, encoding) - fs.writeFileSync('website/css/chessboard.css', latestChessboardCSS, encoding) -} - -function writeHomepage () { - const headHTML = mustache.render(headTemplate, {pageTitle: 'Homepage'}) - - const html = mustache.render(homepageTemplate, { - chessboardJsScript: chessboardJsScript, - example2: homepageExample2, - footer: footerTemplate, - head: headHTML - }) - fs.writeFileSync('website/index.html', html, encoding) -} - -function writeExamplesPage () { - const headHTML = mustache.render(headTemplate, {pageTitle: 'Examples'}) - const headerHTML = mustache.render(headerTemplate, {examplesActive: true}) - - const html = mustache.render(examplesTemplate, { - chessboardJsScript: chessboardJsScript, - examplesJavaScript: buildExamplesJS(), - footer: footerTemplate, - head: headHTML, - header: headerHTML, - nav: buildExamplesNavHTML() - }) - fs.writeFileSync('website/examples.html', html, encoding) -} - -const configTableRowsHTML = docs.config.reduce(function (html, itm) { - if (isString(itm)) return html - return html + buildConfigDocsTableRowHTML('config', itm) -}, '') - -const methodTableRowsHTML = docs.methods.reduce(function (html, itm) { - if (isString(itm)) return html - return html + buildMethodRowHTML(itm) -}, '') - -const errorRowsHTML = docs.errors.reduce(function (html, itm) { - if (isString(itm)) return html - return html + buildErrorRowHTML(itm) -}, '') - -function isIntegrationExample (example) { - return (example.id + '').startsWith('5') -} - -function writeSingleExamplePage (example) { - if (isIntegrationExample(example)) { - example.includeChessJS = true - } - example.chessboardJsScript = chessboardJsScript - const html = mustache.render(singleExampleTemplate, example) - fs.writeFileSync('website/examples/' + example.id + '.html', html, encoding) -} - -function writeSingleExamplesPages () { - examplesArr.forEach(writeSingleExamplePage) -} - -function writeDocsPage () { - const headHTML = mustache.render(headTemplate, {pageTitle: 'Documentation'}) - const headerHTML = mustache.render(headerTemplate, {docsActive: true}) - - const html = mustache.render(docsTemplate, { - configTableRows: configTableRowsHTML, - errorRows: errorRowsHTML, - footer: footerTemplate, - head: headHTML, - header: headerHTML, - methodTableRows: methodTableRowsHTML - }) - fs.writeFileSync('website/docs.html', html, encoding) -} - -function writeDownloadPage () { - const headHTML = mustache.render(headTemplate, {pageTitle: 'Download'}) - const headerHTML = mustache.render(headerTemplate, {downloadActive: true}) - - const html = mustache.render(downloadTemplate, { - footer: footerTemplate, - head: headHTML, - header: headerHTML - }) - fs.writeFileSync('website/download.html', html, encoding) -} - -function writeLicensePage () { - const html = mustache.render(licensePageTemplate) - fs.writeFileSync('website/license.html', html, encoding) -} - -function writeWebsite () { - writeSrcFiles() - writeHomepage() - writeExamplesPage() - writeSingleExamplesPages() - writeDocsPage() - writeDownloadPage() - writeLicensePage() -} - -writeWebsite() - -// ----------------------------------------------------------------------------- -// HTML -// ----------------------------------------------------------------------------- - -function htmlEscape (str) { - return (str + '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/\//g, '/') - .replace(/`/g, '`') -} - -function buildExampleGroupHTML (idx, groupName, examplesInGroup) { - const groupNum = idx + 1 - let html = '

' + groupName + '

' + - '' - - return html -} - -function buildExamplesNavHTML () { - let html = '' - examplesGroups.forEach(function (group, idx) { - html += buildExampleGroupHTML(idx, group.name, group.examples) - }) - return html -} - -function buildExamplesJS () { - let txt = 'window.CHESSBOARD_EXAMPLES = {}\n\n' - - examplesArr.forEach(function (ex) { - txt += 'CHESSBOARD_EXAMPLES["' + ex.id + '"] = {\n' + - ' description: ' + JSON.stringify(ex.description) + ',\n' + - ' html: ' + JSON.stringify(ex.html) + ',\n' + - ' name: ' + JSON.stringify(ex.name) + ',\n' + - ' jsStr: ' + JSON.stringify(ex.js) + ',\n' + - ' jsFn: function () {\n' + ex.js + '\n }\n' + - '};\n\n' - }) - - return txt -} - -function buildConfigDocsTableRowHTML (propType, prop) { - let html = '' - - // table row - html += '' - - // property and type - html += '' + buildPropertyAndTypeHTML(propType, prop.name, prop.type) + '' - - // default - html += '

' + buildDefaultHTML(prop.default) + '

' - - // description - html += '' + buildDescriptionHTML(prop.desc) + '' - - // examples - html += '' + buildExamplesCellHTML(prop.examples) + '' - - html += '' - - return html -} - -function buildMethodRowHTML (method) { - const nameNoParens = method.name.replace(/\(.+$/, '') - - let html = '' - - // table row - if (method.noId) { - html += '' - } else { - html += '' - } - - // name - html += '

' + - '' + method.name + '

' - - // args - if (method.args) { - html += '' - method.args.forEach(function (arg) { - html += '

' + arg[1] + '

' - }) - html += '' - } else { - html += 'none' - } - - // description - html += '' + buildDescriptionHTML(method.desc) + '' - - // examples - html += '' + buildExamplesCellHTML(method.examples) + '' - - html += '' - - return html -} - -function buildPropertyAndTypeHTML (section, name, type) { - let html = '

' + - '' + name + '

' + - '

' + buildTypeHTML(type) + '

' - return html -} - -function buildTypeHTML (type) { - if (!Array.isArray(type)) { - type = [type] - } - - let html = '' - for (var i = 0; i < type.length; i++) { - if (i !== 0) { - html += ' or
' - } - html += type[i] - } - - return html -} - -function buildRequiredHTML (req) { - if (!req) return 'no' - if (req === true) return 'yes' - return req -} - -function buildDescriptionHTML (desc) { - if (!Array.isArray(desc)) { - desc = [desc] - } - - let html = '' - desc.forEach(function (d) { - html += '

' + d + '

' - }) - - return html -} - -function buildDefaultHTML (defaultValue) { - if (!defaultValue) { - return 'n/a' - } - return defaultValue -} - -function buildExamplesCellHTML (examplesIds) { - if (!Array.isArray(examplesIds)) { - examplesIds = [examplesIds] - } - - let html = '' - examplesIds.forEach(function (exampleId) { - var example = examplesObj[exampleId] - if (!example) return - html += '

' + example.name + '

' - }) - - return html -} - -function buildErrorRowHTML (error) { - let html = '' - - // table row - html += '' - - // id - html += '' + - '

' + error.id + '

' - - // desc - html += '

' + error.desc + '

' - - // more information - if (error.fix) { - if (!Array.isArray(error.fix)) { - error.fix = [error.fix] - } - - html += '' - error.fix.forEach(function (p) { - html += '

' + p + '

' - }) - html += '' - } else { - html += 'n/a' - } - - html += '' - - return html -} - -function isString (s) { - return typeof s === 'string' -} diff --git a/templates/_head.mustache b/templates/_head.mustache index 07511c7e..e3c04dd9 100644 --- a/templates/_head.mustache +++ b/templates/_head.mustache @@ -7,9 +7,8 @@ - + - - + diff --git a/templates/docs.mustache b/templates/docs.mustache index 39c0ce23..458b57a4 100644 --- a/templates/docs.mustache +++ b/templates/docs.mustache @@ -92,30 +92,27 @@ {{{footer}}} - - diff --git a/templates/download.mustache b/templates/download.mustache index 2a984e11..83c08172 100644 --- a/templates/download.mustache +++ b/templates/download.mustache @@ -29,18 +29,8 @@ yarn add @chrisoakman/chessboardjs

Content Delivery Network

You can use chessboardjs via the unpkg CDN with the following links: -

<link rel="stylesheet"
-      href="https://unpkg.com/@chrisoakman/chessboardjs@1.0.0/dist/chessboard-1.0.0.min.css"
-      integrity="sha384-q94+BZtLrkL1/ohfjR8c6L+A6qzNH9R2hBLwyoAfu3i/WCvQjzL2RQJ3uNHDISdU"
-      crossorigin="anonymous">
-
<script src="https://code.jquery.com/jquery-3.5.1.min.js"
-        integrity="sha384-ZvpUoO/+PpLXR1lu4jmpXWu80pZlYUAfxl5NsBMWOEPSjUn/6Z/hRTt8+pR6L4N2"
-        crossorigin="anonymous"></script>
-
-<script src="https://unpkg.com/@chrisoakman/chessboardjs@1.0.0/dist/chessboard-1.0.0.min.js"
-        integrity="sha384-8Vi8VHwn3vjQ9eUHUxex3JSN/NFqUg3QbPyX8kWyb93+8AC/pPWTzj+nHtbC5bxD"
-        crossorigin="anonymous"></script>
-

Please note that jQuery is a required dependency of chessboard.js and must be loaded on your webpage before the chessboard-1.0.0.min.js file.

+
<link rel="stylesheet" href="https://unpkg.com/@chrisoakman/chessboardjs@{{version}}/dist/chessboard-{{version}}.min.css">
+
<script src="https://unpkg.com/@chrisoakman/chessboardjs@{{version}}/dist/chessboard-{{version}}.min.js"></script>
diff --git a/templates/examples.mustache b/templates/examples.mustache index f11d4f45..5c49f1ae 100644 --- a/templates/examples.mustache +++ b/templates/examples.mustache @@ -16,7 +16,6 @@ - {{{chessboardJsScript}}} diff --git a/templates/homepage.mustache b/templates/homepage.mustache index 0ff8ee63..4744ccc6 100644 --- a/templates/homepage.mustache +++ b/templates/homepage.mustache @@ -5,7 +5,7 @@
- Black King + Black King

chessboard.js

The easiest way to embed a chess board on your site.

Download v1.0.0 @@ -29,7 +29,7 @@

HTML

<div id="board1" style="width: 400px"></div>

JavaScript

-
var board1 = Chessboard('board1', 'start')
+
var board1 = new Chessboard('board1', 'start')
@@ -59,35 +59,18 @@ {{{footer}}} - {{{chessboardJsScript}}} diff --git a/templates/single-example.mustache b/templates/single-example.mustache index d170aa9b..4a966a8c 100644 --- a/templates/single-example.mustache +++ b/templates/single-example.mustache @@ -5,7 +5,7 @@ chessboardjs Example #{{{id}}} - {{{name}}} - + {{#css}}