| import { MoveInformation } from './MoveInformation.js'; |
| import { EvaluationBar } from '../board/EvaluationBar.js'; |
| import { EngineLines } from './EngineLines.js'; |
| import { GameGraph } from '../report/GameGraph.js'; |
| import { Clock } from '../board/Clock.js'; |
|
|
| export class MoveNavigator { |
| constructor(chessUI) { |
| this.chessUI = chessUI; |
| } |
|
|
| |
| |
| |
| setupEventHandlers() { |
| $("#forward").on("click", () => this.handleForwardMove()); |
| $("#backward").on("click", () => this.handleBackwardMove()); |
| $("#flip").on("click", () => this.handleFlipBoard()); |
| $("#restart").on("click", () => this.handleRestart()); |
| $("#skip-to-end").on("click", () => this.handleSkipToEnd()); |
| $("#fen-to-clipboard").on("click", () => this.handleCopyFenToClipboard()); |
|
|
| |
| $("#popup-quick-menu").on("click", (e) => this.handleQuickMenuToggle(e)); |
| $("#share-fen").on("click", () => this.handleShareFen()); |
| $("#copy-pgn").on("click", () => this.handleCopyPgn()); |
| $("#flip-board").on("click", () => this.handleFlipBoard()); |
| $("#download-pgn").on("click", () => this.handleDownloadPgn()); |
|
|
| |
| $(document).on("click", (e) => this.handleDocumentClick(e)); |
|
|
| |
| $(document).on('keydown', (e) => { |
| switch (e.keyCode) { |
| case 39: $("#forward").trigger('click'); break; |
| case 37: $("#backward").trigger('click'); break; |
| case 38: $("#restart").trigger('click'); break; |
| case 70: $("#flip").trigger('click'); break; |
| } |
| }); |
|
|
| |
| $("#chessboard").on('wheel', (e) => { |
| |
| const delta = e.originalEvent.deltaY; |
| |
| if (delta > 0) { |
| |
| this.handleForwardMove(); |
| } else if (delta < 0) { |
| |
| this.handleBackwardMove(); |
| } |
| }); |
|
|
| |
| this.chessUI.board.on('usermove', (moveObj) => this.handleUserMove(moveObj)); |
| } |
|
|
| handleCopyFenToClipboard() { |
| const currentNode = this.chessUI.moveTree.currentNode; |
| const fen = currentNode.move.after; |
| navigator.clipboard.writeText(fen); |
| } |
|
|
| updateAfterMove(node) { |
| this.chessUI.moveTree.updateCurrentMove(node.id); |
|
|
| if (node.evalScore !== undefined) { |
| EvaluationBar.updateEvaluationBar(node); |
| } |
|
|
| MoveInformation.updateMoveInfo(node, this.chessUI.moveTree.getPreviousMove()); |
|
|
| EngineLines.updateEngineLines( |
| node, |
| this.chessUI.moveTree, |
| (node) => this.handleTreeNodeClick(node), |
| (node, resultFen, prevFen) => this.queueMoveForEvaluation(node, resultFen, prevFen) |
| ); |
|
|
| GameGraph.updateCurrentMoveNumber(node.moveNumber); |
| |
| |
| Clock.updateFromMoveTree(this.chessUI.moveTree, this.chessUI.board.flipped, this.chessUI.game?.pgn); |
| } |
|
|
| handleForwardMove() { |
| const nextNode = this.chessUI.moveTree.getNextMove(); |
| if (!nextNode || !nextNode.move) return; |
|
|
| const classification = nextNode.classification; |
| this.chessUI.board.move(nextNode.move, true, classification, nextNode.move.before, false, nextNode.move.promotion, false); |
|
|
| this.chessUI.moveTree.navigateTo(nextNode.id); |
| this.updateAfterMove(nextNode); |
| } |
|
|
| handleBackwardMove() { |
| if (this.chessUI.moveTree.currentNode === this.chessUI.moveTree.mainline[0]) return; |
|
|
| const currentNode = this.chessUI.moveTree.currentNode; |
| const prevNode = this.chessUI.moveTree.getPreviousMove(); |
|
|
| if (!prevNode || !currentNode.move) return; |
|
|
| this.chessUI.board.unmove(true, currentNode.move, currentNode.move.before); |
| |
| this.chessUI.moveTree.navigateTo(prevNode.id); |
| this.chessUI.moveTree.updateNodeClassification(prevNode, this.chessUI.board); |
|
|
| this.updateAfterMove(prevNode); |
| } |
|
|
| handleFlipBoard() { |
| this.chessUI.board.flip(); |
|
|
| |
| $(".eval-bar").toggleClass("flipped"); |
| $(".chess-container").toggleClass("flipped"); |
|
|
| const currentNode = this.chessUI.moveTree.currentNode; |
| this.chessUI.moveTree.updateNodeClassification(currentNode, this.chessUI.board); |
| |
| if (currentNode.evalScore !== undefined) { |
| EvaluationBar.updateEvaluationBar(currentNode); |
| } |
| |
| |
| Clock.updateFromMoveTree(this.chessUI.moveTree, this.chessUI.board.flipped, this.chessUI.game?.pgn); |
| } |
|
|
| handleRestart() { |
| this.chessUI.board.fen(); |
|
|
| this.chessUI.moveTree.navigateTo('root'); |
| this.chessUI.moveTree.updateCurrentMove('root'); |
|
|
| GameGraph.updateCurrentMoveNumber(0); |
| EvaluationBar.updateEvaluationBar(); |
| EngineLines.updateEngineLines( |
| this.chessUI.moveTree.currentNode, |
| this.chessUI.moveTree, |
| (node) => this.handleTreeNodeClick(node), |
| (node, resultFen, prevFen) => this.queueMoveForEvaluation(node, resultFen, prevFen) |
| ); |
| |
| |
| Clock.updateFromMoveTree(this.chessUI.moveTree, this.chessUI.board.flipped, this.chessUI.game?.pgn); |
| } |
|
|
| handleSkipToEnd() { |
| const lastMove = this.chessUI.moveTree.getFinalMove(); |
| this.handleTreeNodeClick(lastMove); |
| } |
|
|
| handleUserMove(moveObj) { |
| |
| const currentIndex = this.chessUI.moveTree.getNodeIndex(this.chessUI.moveTree.currentNode); |
| if (currentIndex !== -1 && currentIndex + 1 < this.chessUI.moveTree.mainline.length) { |
| const nextMainlineMove = this.chessUI.moveTree.mainline[currentIndex + 1]; |
| if (nextMainlineMove.move && |
| nextMainlineMove.move.from === moveObj.from && |
| nextMainlineMove.move.to === moveObj.to && |
| nextMainlineMove.move.promotion === moveObj.promotion) { |
| return this.navigateToExistingMove(nextMainlineMove); |
| } |
| } |
|
|
| |
| const existingChild = this.chessUI.moveTree.currentNode.children.find(child => |
| child.move && child.move.from === moveObj.from && |
| child.move.to === moveObj.to && |
| child.move.promotion === moveObj.promotion |
| ); |
|
|
| return existingChild ? |
| this.navigateToExistingMove(existingChild) : |
| this.createNewVariation(moveObj); |
| } |
|
|
| handleTreeNodeClick(node) { |
| |
| const path = this.chessUI.moveTree.getPathToNode(node.id); |
|
|
| if (path.length > 0) { |
| const lastNode = path[path.length - 1]; |
|
|
| |
| if (lastNode.id === this.chessUI.moveTree.currentNode.id) { |
| return; |
| } |
|
|
| |
| if (this.chessUI.moveTree.currentNode.children.some(child => child.id === lastNode.id)) { |
| const nextNode = this.chessUI.moveTree.getNextMove(); |
| if (nextNode && nextNode.id === lastNode.id) { |
| $("#forward").trigger('click'); |
| return; |
| } |
| } |
|
|
| |
| const currentIndex = this.chessUI.moveTree.getNodeIndex(this.chessUI.moveTree.currentNode); |
| const lastNodeIndex = this.chessUI.moveTree.getNodeIndex(lastNode); |
| if (currentIndex > 0 && lastNodeIndex === currentIndex - 1) { |
| $("#backward").trigger('click'); |
| return; |
| } |
| |
| |
| if (this.chessUI.moveTree.currentNode.parentId === lastNode.id) { |
| $("#backward").trigger('click'); |
| return; |
| } |
| } |
|
|
| |
| const targetNode = this.chessUI.moveTree.nodeMap.get(node.id); |
|
|
| |
|
|
| |
| if (!targetNode || targetNode.id === 'root') { |
| this.handleRestart(); |
| return; |
| } |
|
|
| |
| this.navigateToTargetPosition(targetNode); |
| } |
|
|
| navigateToExistingMove(moveNode) { |
| this.chessUI.moveTree.navigateTo(moveNode.id); |
|
|
| |
| const fromIdx = this.chessUI.board.algebraicToIndex(moveNode.move.from, this.chessUI.board.flipped); |
| const toIdx = this.chessUI.board.algebraicToIndex(moveNode.move.to, this.chessUI.board.flipped); |
| this.chessUI.board.addClassification( |
| moveNode.classification, |
| this.chessUI.board.getSquare(fromIdx, this.chessUI.board.flipped), |
| this.chessUI.board.getSquare(toIdx, this.chessUI.board.flipped) |
| ); |
|
|
| this.updateAfterMove(moveNode); |
|
|
| return true; |
| } |
|
|
| navigateToTargetPosition(targetNode) { |
| let parentNode; |
| |
| |
| const targetNodeIndex = this.chessUI.moveTree.getNodeIndex(targetNode); |
| if (targetNodeIndex !== -1) { |
| |
| parentNode = this.chessUI.moveTree.mainline[targetNodeIndex - 1]; |
| } else if (targetNode.parentId) { |
| |
| parentNode = this.chessUI.moveTree.nodeMap.get(targetNode.parentId); |
| } else { |
| console.error("Cannot find parent for node", targetNode); |
| return; |
| } |
| |
| this.chessUI.board.fen(targetNode.move.before); |
| if (targetNode.move) { |
| this.chessUI.board.move(targetNode.move, true, targetNode.classification, targetNode.move.before, false, targetNode.move.promotion); |
| } |
|
|
| |
| this.chessUI.moveTree.navigateTo(targetNode.id); |
| this.updateAfterMove(targetNode); |
| } |
|
|
| createNewVariation(moveObj) { |
| |
| const newNode = this.chessUI.moveTree.addMove(moveObj, this.chessUI.moveTree.currentNode.id); |
| newNode.evaluationStatus = 'pending'; |
|
|
| this.chessUI.moveTree.navigateTo(newNode.id); |
| this.chessUI.moveTree.render('move-tree', (node) => this.handleTreeNodeClick(node)); |
| this.updateAfterMove(newNode); |
|
|
| |
| const fenBefore = newNode.move.before; |
| const fenAfter = newNode.move.after; |
| this.queueMoveForEvaluation(newNode, fenAfter, fenBefore); |
|
|
| return true; |
| } |
|
|
| queueMoveForEvaluation(node, resultFen, prevFen) { |
| this.chessUI.evaluationQueue.addToQueue(node, resultFen, prevFen, (evaluatedMove) => { |
| |
| this.chessUI.moveTree.updateClassification(node.id, evaluatedMove); |
| node.evaluationStatus = 'complete'; |
|
|
| const topLine = evaluatedMove.lines.find(line => line.id === 1); |
| if (topLine) { |
| node.evalScore = topLine.score; |
| node.evalType = topLine.type || 'cp'; |
| } |
|
|
| |
| const currentNode = this.chessUI.moveTree.currentNode; |
| if (currentNode.id === node.id && node.move) { |
| const fromIdx = this.chessUI.board.algebraicToIndex(node.move.from, this.chessUI.board.flipped); |
| const toIdx = this.chessUI.board.algebraicToIndex(node.move.to, this.chessUI.board.flipped); |
|
|
| this.chessUI.board.addClassification( |
| evaluatedMove.classification.type, |
| this.chessUI.board.getSquare(fromIdx, this.chessUI.board.flipped), |
| this.chessUI.board.getSquare(toIdx, this.chessUI.board.flipped) |
| ); |
| } |
|
|
| this.chessUI.moveTree.render('move-tree', (node) => this.handleTreeNodeClick(node)); |
| this.updateAfterMove(currentNode); |
|
|
| }, this.chessUI.moveTree); |
| } |
|
|
| handleQuickMenuToggle(e) { |
| e.stopPropagation(); |
| const menu = $("#quick-menu"); |
| const isVisible = menu.hasClass('show'); |
| |
| if (isVisible) { |
| menu.removeClass('show'); |
| } else { |
| menu.addClass('show'); |
| } |
| } |
|
|
| handleDocumentClick(e) { |
| const menu = $("#quick-menu"); |
| const button = $("#play"); |
| |
| if (!menu.is(e.target) && menu.has(e.target).length === 0 && |
| !button.is(e.target) && button.has(e.target).length === 0) { |
| menu.removeClass('show'); |
| } |
| } |
|
|
| handleShareFen() { |
| const currentNode = this.chessUI.moveTree.currentNode; |
| const fen = currentNode.move ? currentNode.move.after : currentNode.fen; |
| |
| if (navigator.share) { |
| navigator.share({ |
| title: 'Chess Position', |
| text: `Check out this chess position: ${fen}`, |
| url: window.location.href |
| }).catch(console.error); |
| } else { |
| |
| navigator.clipboard.writeText(fen).then(() => { |
| this.showNotification('FEN copied to clipboard!'); |
| }).catch(() => { |
| this.showNotification('Failed to copy FEN'); |
| }); |
| } |
| $("#quick-menu").removeClass('show'); |
| } |
|
|
| handleCopyPgn() { |
| const pgn = this.chessUI.game?.pgn || ''; |
| if (pgn) { |
| navigator.clipboard.writeText(pgn).then(() => { |
| this.showNotification('PGN copied to clipboard!'); |
| }).catch(() => { |
| this.showNotification('Failed to copy PGN'); |
| }); |
| } else { |
| this.showNotification('No PGN available'); |
| } |
| $("#quick-menu").removeClass('show'); |
| } |
|
|
| handleDownloadPgn() { |
| const pgn = this.chessUI.game?.pgn || ''; |
| if (pgn) { |
| const blob = new Blob([pgn], { type: 'text/plain' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = `chess-game-${new Date().toISOString().split('T')[0]}.pgn`; |
| document.body.appendChild(a); |
| a.click(); |
| document.body.removeChild(a); |
| URL.revokeObjectURL(url); |
| this.showNotification('PGN downloaded!'); |
| } else { |
| this.showNotification('No PGN available'); |
| } |
| $("#quick-menu").removeClass('show'); |
| } |
|
|
| showNotification(message) { |
| |
| const notification = $(` |
| <div class="quick-notification" style=" |
| position: fixed; |
| top: 20px; |
| right: 20px; |
| background: var(--sidebar-base); |
| color: var(--text-primary); |
| padding: 12px 20px; |
| border-radius: 8px; |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); |
| z-index: 10000; |
| font-size: 14px; |
| border: 1px solid var(--dark-border); |
| animation: slideInRight 0.3s ease; |
| ">${message}</div> |
| `); |
| |
| $('body').append(notification); |
| |
| setTimeout(() => { |
| notification.fadeOut(300, () => notification.remove()); |
| }, 3000); |
| } |
| } |