@alepot55/chessboardjs 2.2.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -401
- package/assets/themes/alepot/theme.json +42 -0
- package/assets/themes/default/theme.json +42 -0
- package/dist/chessboard.cjs.js +11785 -0
- package/dist/chessboard.css +243 -0
- package/dist/chessboard.esm.js +11716 -0
- package/dist/chessboard.iife.js +11791 -0
- package/dist/chessboard.umd.js +11791 -0
- package/package.json +33 -3
- package/{chessboard.move.js → src/components/Move.js} +3 -3
- package/src/components/Piece.js +771 -0
- package/{chessboard.square.js → src/components/Square.js} +61 -8
- package/src/constants/index.js +15 -0
- package/src/constants/positions.js +62 -0
- package/src/core/Chessboard.js +2346 -0
- package/src/core/ChessboardConfig.js +707 -0
- package/src/core/ChessboardFactory.js +385 -0
- package/src/core/index.js +141 -0
- package/src/errors/ChessboardError.js +133 -0
- package/src/errors/index.js +15 -0
- package/src/errors/messages.js +189 -0
- package/src/index.js +103 -0
- package/src/services/AnimationService.js +180 -0
- package/src/services/BoardService.js +156 -0
- package/src/services/CoordinateService.js +355 -0
- package/src/services/EventService.js +955 -0
- package/src/services/MoveService.js +567 -0
- package/src/services/PieceService.js +339 -0
- package/src/services/PositionService.js +237 -0
- package/src/services/ValidationService.js +673 -0
- package/src/services/index.js +14 -0
- package/src/styles/animations.css +46 -0
- package/{chessboard.css → src/styles/board.css} +30 -7
- package/src/styles/index.css +4 -0
- package/src/styles/pieces.css +70 -0
- package/src/utils/animations.js +37 -0
- package/{chess.js → src/utils/chess.js} +16 -16
- package/src/utils/coordinates.js +62 -0
- package/src/utils/cross-browser.js +150 -0
- package/src/utils/logger.js +422 -0
- package/src/utils/performance.js +311 -0
- package/src/utils/validation.js +458 -0
- package/.babelrc +0 -4
- package/chessboard.bundle.js +0 -3422
- package/chessboard.config.js +0 -147
- package/chessboard.js +0 -979
- package/chessboard.piece.js +0 -115
- package/jest.config.js +0 -7
- package/rollup.config.js +0 -11
- package/test/chessboard.test.js +0 -128
- /package/{alepot_theme → assets/themes/alepot}/bb.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/bw.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/kb.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/kw.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/nb.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/nw.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/pb.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/pw.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/qb.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/qw.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/rb.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/rw.svg +0 -0
- /package/{default_pieces → assets/themes/default}/bb.svg +0 -0
- /package/{default_pieces → assets/themes/default}/bw.svg +0 -0
- /package/{default_pieces → assets/themes/default}/kb.svg +0 -0
- /package/{default_pieces → assets/themes/default}/kw.svg +0 -0
- /package/{default_pieces → assets/themes/default}/nb.svg +0 -0
- /package/{default_pieces → assets/themes/default}/nw.svg +0 -0
- /package/{default_pieces → assets/themes/default}/pb.svg +0 -0
- /package/{default_pieces → assets/themes/default}/pw.svg +0 -0
- /package/{default_pieces → assets/themes/default}/qb.svg +0 -0
- /package/{default_pieces → assets/themes/default}/qw.svg +0 -0
- /package/{default_pieces → assets/themes/default}/rb.svg +0 -0
- /package/{default_pieces → assets/themes/default}/rw.svg +0 -0
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service for managing events and user interactions
|
|
3
|
+
* @module services/EventService
|
|
4
|
+
* @since 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { rafThrottle } from '../utils/performance.js';
|
|
8
|
+
import { DragOptimizations } from '../utils/cross-browser.js';
|
|
9
|
+
import { ValidationError } from '../errors/ChessboardError.js';
|
|
10
|
+
import Move from '../components/Move.js';
|
|
11
|
+
import Piece from '../components/Piece.js';
|
|
12
|
+
import { logger } from '../utils/logger.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Service responsible for event handling and user interactions
|
|
16
|
+
* @class
|
|
17
|
+
*/
|
|
18
|
+
export class EventService {
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new EventService instance
|
|
21
|
+
* @param {ChessboardConfig} config - Board configuration
|
|
22
|
+
* @param {BoardService} boardService - Board service instance
|
|
23
|
+
* @param {MoveService} moveService - Move service instance
|
|
24
|
+
* @param {CoordinateService} coordinateService - Coordinate service instance
|
|
25
|
+
* @param {Chessboard} chessboard - Chessboard instance
|
|
26
|
+
*/
|
|
27
|
+
constructor(config, boardService, moveService, coordinateService, chessboard) {
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.boardService = boardService;
|
|
30
|
+
this.moveService = moveService;
|
|
31
|
+
this.coordinateService = coordinateService;
|
|
32
|
+
this.chessboard = chessboard;
|
|
33
|
+
|
|
34
|
+
this.clicked = null;
|
|
35
|
+
this.isDragging = false;
|
|
36
|
+
this.isAnimating = false;
|
|
37
|
+
this.promoting = false;
|
|
38
|
+
this._isProcessingDrop = false;
|
|
39
|
+
|
|
40
|
+
this.eventListeners = new Map();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Adds event listeners to all squares
|
|
45
|
+
* @param {Function} onSquareClick - Callback for square clicks
|
|
46
|
+
* @param {Function} onPieceHover - Callback for piece hover
|
|
47
|
+
* @param {Function} onPieceLeave - Callback for piece leave
|
|
48
|
+
*/
|
|
49
|
+
addListeners(onSquareClick, onPieceHover, onPieceLeave) {
|
|
50
|
+
// Remove existing listeners to avoid duplicates
|
|
51
|
+
this.removeListeners();
|
|
52
|
+
|
|
53
|
+
const squares = this.boardService.getAllSquares();
|
|
54
|
+
|
|
55
|
+
Object.values(squares).forEach(square => {
|
|
56
|
+
this._addSquareListeners(square, onSquareClick, onPieceHover, onPieceLeave);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Adds event listeners to a specific square
|
|
62
|
+
* @private
|
|
63
|
+
* @param {Square} square - Square to add listeners to
|
|
64
|
+
* @param {Function} onSquareClick - Click callback
|
|
65
|
+
* @param {Function} onPieceHover - Hover callback
|
|
66
|
+
* @param {Function} onPieceLeave - Leave callback
|
|
67
|
+
*/
|
|
68
|
+
_addSquareListeners(square, onSquareClick, onPieceHover, onPieceLeave) {
|
|
69
|
+
const listeners = [];
|
|
70
|
+
|
|
71
|
+
// Throttled hover handlers for performance
|
|
72
|
+
const throttledHover = rafThrottle((e) => {
|
|
73
|
+
if (!this.clicked && this.config.hints) {
|
|
74
|
+
onPieceHover(square);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const throttledLeave = rafThrottle((e) => {
|
|
79
|
+
if (!this.clicked && this.config.hints) {
|
|
80
|
+
onPieceLeave(square);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Click handler
|
|
85
|
+
const handleClick = (e) => {
|
|
86
|
+
e.stopPropagation();
|
|
87
|
+
if (this.config.clickable && !this.isAnimating) {
|
|
88
|
+
// Cancel any pending drag operations on pieces in this square
|
|
89
|
+
if (square.piece && square.piece._dragTimeout) {
|
|
90
|
+
clearTimeout(square.piece._dragTimeout);
|
|
91
|
+
square.piece._dragTimeout = null;
|
|
92
|
+
}
|
|
93
|
+
onSquareClick(square);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Add listeners
|
|
98
|
+
square.element.addEventListener('mouseover', throttledHover);
|
|
99
|
+
square.element.addEventListener('mouseout', throttledLeave);
|
|
100
|
+
square.element.addEventListener('click', handleClick);
|
|
101
|
+
square.element.addEventListener('touchstart', handleClick);
|
|
102
|
+
|
|
103
|
+
// Store listeners for cleanup
|
|
104
|
+
listeners.push(
|
|
105
|
+
{ element: square.element, type: 'mouseover', handler: throttledHover },
|
|
106
|
+
{ element: square.element, type: 'mouseout', handler: throttledLeave },
|
|
107
|
+
{ element: square.element, type: 'click', handler: handleClick },
|
|
108
|
+
{ element: square.element, type: 'touchstart', handler: handleClick }
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
this.eventListeners.set(square.id, listeners);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Creates a drag function for a piece
|
|
116
|
+
* @param {Square} square - Square containing the piece
|
|
117
|
+
* @param {Piece} piece - Piece to create drag function for
|
|
118
|
+
* @param {Function} onDragStart - Drag start callback
|
|
119
|
+
* @param {Function} onDragMove - Drag move callback
|
|
120
|
+
* @param {Function} onDrop - Drop callback
|
|
121
|
+
* @param {Function} onSnapback - Snapback callback
|
|
122
|
+
* @param {Function} onMove - Move execution callback
|
|
123
|
+
* @param {Function} onRemove - Remove piece callback
|
|
124
|
+
* @returns {Function} Drag event handler
|
|
125
|
+
*/
|
|
126
|
+
createDragFunction(square, piece, onDragStart, onDragMove, onDrop, onSnapback, onMove, onRemove) {
|
|
127
|
+
console.log('Creating drag function for:', square.id, piece ? `${piece.color}${piece.type}` : 'null');
|
|
128
|
+
|
|
129
|
+
return (event) => {
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
|
|
132
|
+
if (!this.config.draggable || !piece || this.isAnimating || this.isDragging) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Set dragging state immediately
|
|
137
|
+
this.isDragging = true;
|
|
138
|
+
|
|
139
|
+
const originalFrom = square;
|
|
140
|
+
let isDragging = false;
|
|
141
|
+
let from = originalFrom;
|
|
142
|
+
let to = square;
|
|
143
|
+
let previousHighlight = null;
|
|
144
|
+
|
|
145
|
+
const img = piece.element;
|
|
146
|
+
|
|
147
|
+
if (!this.moveService.canMove(from)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Track initial position for drag threshold
|
|
152
|
+
const startX = event.clientX || (event.touches && event.touches[0]?.clientX) || 0;
|
|
153
|
+
const startY = event.clientY || (event.touches && event.touches[0]?.clientY) || 0;
|
|
154
|
+
|
|
155
|
+
const moveAt = (event) => {
|
|
156
|
+
const boardElement = this.boardService.element;
|
|
157
|
+
const squareSize = boardElement.offsetWidth / 8;
|
|
158
|
+
|
|
159
|
+
// Get mouse coordinates
|
|
160
|
+
let clientX, clientY;
|
|
161
|
+
if (event.touches && event.touches[0]) {
|
|
162
|
+
clientX = event.touches[0].clientX;
|
|
163
|
+
clientY = event.touches[0].clientY;
|
|
164
|
+
} else {
|
|
165
|
+
clientX = event.clientX;
|
|
166
|
+
clientY = event.clientY;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Calculate position relative to board
|
|
170
|
+
const boardRect = boardElement.getBoundingClientRect();
|
|
171
|
+
const x = clientX - boardRect.left - (squareSize / 2);
|
|
172
|
+
const y = clientY - boardRect.top - (squareSize / 2);
|
|
173
|
+
|
|
174
|
+
img.style.left = x + 'px';
|
|
175
|
+
img.style.top = y + 'px';
|
|
176
|
+
|
|
177
|
+
return true;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const onMouseMove = (event) => {
|
|
181
|
+
const currentX = event.clientX || 0;
|
|
182
|
+
const currentY = event.clientY || 0;
|
|
183
|
+
const deltaX = Math.abs(currentX - startX);
|
|
184
|
+
const deltaY = Math.abs(currentY - startY);
|
|
185
|
+
|
|
186
|
+
// Start dragging if mouse moved enough
|
|
187
|
+
if (!isDragging && (deltaX > 3 || deltaY > 3)) {
|
|
188
|
+
isDragging = true;
|
|
189
|
+
|
|
190
|
+
// During drag, we don't need to manage clicked state
|
|
191
|
+
// The drag operation is independent of click state
|
|
192
|
+
|
|
193
|
+
// Visual feedback
|
|
194
|
+
if (this.config.clickable) {
|
|
195
|
+
from.select();
|
|
196
|
+
// Show hints would be handled by the main class
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Prepare piece for dragging
|
|
200
|
+
// First, preserve the current computed dimensions
|
|
201
|
+
const computedStyle = window.getComputedStyle(img);
|
|
202
|
+
const currentWidth = computedStyle.width;
|
|
203
|
+
const currentHeight = computedStyle.height;
|
|
204
|
+
|
|
205
|
+
img.style.position = 'absolute';
|
|
206
|
+
img.style.zIndex = '100';
|
|
207
|
+
img.classList.add('dragging');
|
|
208
|
+
|
|
209
|
+
// Explicitly set the dimensions to maintain size during drag
|
|
210
|
+
img.style.width = currentWidth;
|
|
211
|
+
img.style.height = currentHeight;
|
|
212
|
+
|
|
213
|
+
DragOptimizations.enableForDrag(img);
|
|
214
|
+
|
|
215
|
+
// Call drag start callback
|
|
216
|
+
if (!onDragStart(square, piece)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!isDragging) return;
|
|
222
|
+
|
|
223
|
+
if (!moveAt(event)) return;
|
|
224
|
+
|
|
225
|
+
// Update target square
|
|
226
|
+
const boardElement = this.boardService.element;
|
|
227
|
+
const boardRect = boardElement.getBoundingClientRect();
|
|
228
|
+
const x = event.clientX - boardRect.left;
|
|
229
|
+
const y = event.clientY - boardRect.top;
|
|
230
|
+
|
|
231
|
+
let newTo = null;
|
|
232
|
+
if (x >= 0 && x <= boardRect.width && y >= 0 && y <= boardRect.height) {
|
|
233
|
+
const squareId = this.coordinateService.pixelToSquareID(x, y, boardElement);
|
|
234
|
+
newTo = squareId ? this.boardService.getSquare(squareId) : null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
to = newTo;
|
|
238
|
+
onDragMove(from, to, piece);
|
|
239
|
+
|
|
240
|
+
// Update visual feedback
|
|
241
|
+
if (to !== previousHighlight) {
|
|
242
|
+
to?.highlight();
|
|
243
|
+
previousHighlight?.dehighlight();
|
|
244
|
+
previousHighlight = to;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const onMouseUp = () => {
|
|
249
|
+
// Clean up visual feedback
|
|
250
|
+
previousHighlight?.dehighlight();
|
|
251
|
+
|
|
252
|
+
// CRITICAL FIX: Remove ALL event listeners immediately to prevent interference
|
|
253
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
254
|
+
window.removeEventListener('mouseup', onMouseUp);
|
|
255
|
+
img.removeEventListener('mouseup', onMouseUp);
|
|
256
|
+
|
|
257
|
+
// If this was just a click, don't interfere
|
|
258
|
+
if (!isDragging) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
console.log('onMouseUp: Handling drag completion for piece at', originalFrom.id);
|
|
263
|
+
|
|
264
|
+
// Clean up drag state
|
|
265
|
+
img.style.zIndex = '20';
|
|
266
|
+
img.classList.remove('dragging');
|
|
267
|
+
img.style.willChange = 'auto';
|
|
268
|
+
|
|
269
|
+
// Clean up drag optimizations that might interfere with click
|
|
270
|
+
DragOptimizations.cleanupAfterDrag(img);
|
|
271
|
+
|
|
272
|
+
// Reset drag state immediately to allow subsequent clicks
|
|
273
|
+
this.isDragging = false;
|
|
274
|
+
|
|
275
|
+
// Ensure clicked state is clean before handling drop
|
|
276
|
+
// This prevents drag operations from interfering with subsequent clicks
|
|
277
|
+
const previousClicked = this.clicked;
|
|
278
|
+
this.clicked = null;
|
|
279
|
+
|
|
280
|
+
// Handle drop
|
|
281
|
+
const dropResult = onDrop(originalFrom, to, piece);
|
|
282
|
+
const isTrashDrop = !to && (this.config.dropOffBoard === 'trash' || dropResult === 'trash');
|
|
283
|
+
|
|
284
|
+
if (isTrashDrop) {
|
|
285
|
+
this._handleTrashDrop(originalFrom, onRemove);
|
|
286
|
+
} else if (!to) {
|
|
287
|
+
// Reset piece position instantly for snapback
|
|
288
|
+
img.style.position = '';
|
|
289
|
+
img.style.left = '';
|
|
290
|
+
img.style.top = '';
|
|
291
|
+
img.style.transform = '';
|
|
292
|
+
img.style.width = '';
|
|
293
|
+
img.style.height = '';
|
|
294
|
+
|
|
295
|
+
// Clean up drag optimizations
|
|
296
|
+
DragOptimizations.cleanupAfterDrag(img);
|
|
297
|
+
|
|
298
|
+
this._handleSnapback(originalFrom, piece, onSnapback);
|
|
299
|
+
this._cleanupAfterFailedMove(originalFrom);
|
|
300
|
+
} else {
|
|
301
|
+
// Handle drop like a click - simple and reliable
|
|
302
|
+
this._handleDrop(originalFrom, to, piece, onMove, onSnapback);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Attach event listeners
|
|
307
|
+
window.addEventListener('mouseup', onMouseUp, { once: true });
|
|
308
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
309
|
+
img.addEventListener('mouseup', onMouseUp, { once: true });
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Handles trash drop (piece removal)
|
|
315
|
+
* @private
|
|
316
|
+
* @param {Square} fromSquare - Source square
|
|
317
|
+
* @param {Function} onRemove - Callback to remove piece
|
|
318
|
+
*/
|
|
319
|
+
_handleTrashDrop(fromSquare, onRemove) {
|
|
320
|
+
// Clear visual states
|
|
321
|
+
this.boardService.applyToAllSquares('unmoved');
|
|
322
|
+
this.boardService.applyToAllSquares('removeHint');
|
|
323
|
+
fromSquare.deselect();
|
|
324
|
+
|
|
325
|
+
// Reset clicked state
|
|
326
|
+
this.clicked = null;
|
|
327
|
+
|
|
328
|
+
if (onRemove) {
|
|
329
|
+
onRemove(fromSquare.getId());
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
logger.debug('EventService: Trash drop executed, state cleaned up');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Handles snapback animation
|
|
337
|
+
* @private
|
|
338
|
+
* @param {Square} fromSquare - Source square
|
|
339
|
+
* @param {Piece} piece - Piece to snapback
|
|
340
|
+
* @param {Function} onSnapback - Snapback callback
|
|
341
|
+
*/
|
|
342
|
+
_handleSnapback(fromSquare, piece, onSnapback) {
|
|
343
|
+
if (fromSquare && fromSquare.piece) {
|
|
344
|
+
if (onSnapback) {
|
|
345
|
+
onSnapback(fromSquare, piece);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Always cleanup state after snapback
|
|
350
|
+
logger.debug('EventService: Snapback executed, cleaning up state');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Handles successful drop
|
|
355
|
+
* @private
|
|
356
|
+
* @param {Square} fromSquare - Source square
|
|
357
|
+
* @param {Square} toSquare - Target square
|
|
358
|
+
* @param {Piece} piece - Piece being dropped
|
|
359
|
+
* @param {Function} onMove - Move callback
|
|
360
|
+
* @param {Function} onSnapback - Snapback callback
|
|
361
|
+
*/
|
|
362
|
+
_handleDrop(fromSquare, toSquare, piece, onMove, onSnapback) {
|
|
363
|
+
console.log('=== _handleDrop CALLED ===');
|
|
364
|
+
console.log('From:', fromSquare.id, 'To:', toSquare.id);
|
|
365
|
+
console.log('Piece:', piece ? `${piece.color}${piece.type}` : 'null');
|
|
366
|
+
console.log('Stack trace:', new Error().stack.split('\n')[1]);
|
|
367
|
+
console.log('Caller:', new Error().stack.split('\n')[2]);
|
|
368
|
+
|
|
369
|
+
// CRITICAL FIX: Prevent multiple simultaneous drop operations
|
|
370
|
+
if (this.isAnimating || this._isProcessingDrop) {
|
|
371
|
+
console.log('Drop ignored - already processing or animating');
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Set processing flag to prevent interference
|
|
376
|
+
this._isProcessingDrop = true;
|
|
377
|
+
|
|
378
|
+
const cleanup = () => {
|
|
379
|
+
this._isProcessingDrop = false;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// The core issue is state management. A drop action should be atomic.
|
|
383
|
+
// It either succeeds or fails, and the state should be cleaned completely afterward.
|
|
384
|
+
// We should not be setting `this.clicked` here at all.
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
// Check for promotion first, as it's a special case.
|
|
388
|
+
if (this.moveService.requiresPromotion(new Move(fromSquare, toSquare))) {
|
|
389
|
+
logger.debug('Drag move requires promotion:', fromSquare.id, '->', toSquare.id);
|
|
390
|
+
|
|
391
|
+
this.moveService.setupPromotion(
|
|
392
|
+
new Move(fromSquare, toSquare),
|
|
393
|
+
this.boardService.squares,
|
|
394
|
+
(selectedPromotion) => {
|
|
395
|
+
logger.debug('Drag promotion selected:', selectedPromotion);
|
|
396
|
+
this.boardService.applyToAllSquares('removePromotion');
|
|
397
|
+
this.boardService.applyToAllSquares('removeCover');
|
|
398
|
+
|
|
399
|
+
// Attempt the move with promotion.
|
|
400
|
+
const moveResult = onMove(fromSquare, toSquare, selectedPromotion, true);
|
|
401
|
+
if (moveResult) {
|
|
402
|
+
this._schedulePromotionPieceReplacement(toSquare, selectedPromotion);
|
|
403
|
+
this._cleanupAfterSuccessfulMove(fromSquare);
|
|
404
|
+
} else {
|
|
405
|
+
this._handleSnapback(fromSquare, piece, onSnapback);
|
|
406
|
+
this._cleanupAfterFailedMove(fromSquare);
|
|
407
|
+
}
|
|
408
|
+
cleanup();
|
|
409
|
+
},
|
|
410
|
+
() => {
|
|
411
|
+
logger.debug('Drag promotion cancelled');
|
|
412
|
+
this.boardService.applyToAllSquares('removePromotion');
|
|
413
|
+
this.boardService.applyToAllSquares('removeCover');
|
|
414
|
+
this._handleSnapback(fromSquare, piece, onSnapback);
|
|
415
|
+
this._cleanupAfterFailedMove(fromSquare);
|
|
416
|
+
cleanup();
|
|
417
|
+
}
|
|
418
|
+
);
|
|
419
|
+
} else {
|
|
420
|
+
// Regular move.
|
|
421
|
+
const moveSuccess = onMove(fromSquare, toSquare, null, true);
|
|
422
|
+
|
|
423
|
+
if (moveSuccess) {
|
|
424
|
+
this._cleanupAfterSuccessfulMove(fromSquare);
|
|
425
|
+
} else {
|
|
426
|
+
this._handleSnapback(fromSquare, piece, onSnapback);
|
|
427
|
+
this._cleanupAfterFailedMove(fromSquare);
|
|
428
|
+
}
|
|
429
|
+
cleanup();
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
console.error('Error in _handleDrop:', error);
|
|
433
|
+
cleanup();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Cleans up visual state after a SUCCESSFUL move
|
|
439
|
+
* @private
|
|
440
|
+
* @param {Square} fromSquare - Source square
|
|
441
|
+
*/
|
|
442
|
+
_cleanupAfterSuccessfulMove(fromSquare) {
|
|
443
|
+
// Always reset interaction state after any successful move (drag or click)
|
|
444
|
+
this.clicked = null;
|
|
445
|
+
this.isDragging = false;
|
|
446
|
+
|
|
447
|
+
// Clear specific visual states related to the move
|
|
448
|
+
if (fromSquare) {
|
|
449
|
+
fromSquare.deselect();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Clean up ALL visual elements after a successful move
|
|
453
|
+
// This includes removing old move highlights to keep the board clean
|
|
454
|
+
this.boardService.applyToAllSquares('removeHint');
|
|
455
|
+
this.boardService.applyToAllSquares('dehighlight');
|
|
456
|
+
this.boardService.applyToAllSquares('removePromotion');
|
|
457
|
+
this.boardService.applyToAllSquares('removeCover');
|
|
458
|
+
|
|
459
|
+
logger.debug('EventService: All visual state cleaned up after SUCCESSFUL move');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Cleans up visual state after a FAILED move (snapback, invalid move)
|
|
464
|
+
* @private
|
|
465
|
+
* @param {Square} fromSquare - Source square
|
|
466
|
+
*/
|
|
467
|
+
_cleanupAfterFailedMove(fromSquare) {
|
|
468
|
+
// Reset interaction state but be more conservative with visual cleanup
|
|
469
|
+
this.clicked = null;
|
|
470
|
+
this.isDragging = false;
|
|
471
|
+
|
|
472
|
+
// Clean specific visual elements related to failed operation
|
|
473
|
+
this.boardService.applyToAllSquares('removePromotion');
|
|
474
|
+
this.boardService.applyToAllSquares('removeCover');
|
|
475
|
+
this.boardService.applyToAllSquares('removeHint');
|
|
476
|
+
|
|
477
|
+
// Only deselect the specific square that failed
|
|
478
|
+
if (fromSquare) {
|
|
479
|
+
fromSquare.deselect();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Don't aggressively clear all highlights - preserve move history
|
|
483
|
+
// this.boardService.applyToAllSquares('dehighlight');
|
|
484
|
+
|
|
485
|
+
logger.debug('EventService: Conservative cleanup after FAILED move');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Animates piece to center of target square (visual only)
|
|
490
|
+
* @private
|
|
491
|
+
* @param {Piece} piece - Piece to animate
|
|
492
|
+
* @param {Square} targetSquare - Target square
|
|
493
|
+
* @param {Function} callback - Callback when animation completes
|
|
494
|
+
*/
|
|
495
|
+
_animatePieceToCenter(piece, targetSquare, callback = null) {
|
|
496
|
+
if (!piece || !targetSquare) {
|
|
497
|
+
if (callback) callback();
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const duration = this.config.dropCenterTime;
|
|
502
|
+
|
|
503
|
+
// Get current position of piece element
|
|
504
|
+
const sourceRect = piece.element.getBoundingClientRect();
|
|
505
|
+
const targetRect = targetSquare.element.getBoundingClientRect();
|
|
506
|
+
|
|
507
|
+
const x_start = sourceRect.left + sourceRect.width / 2;
|
|
508
|
+
const y_start = sourceRect.top + sourceRect.height / 2;
|
|
509
|
+
const x_end = targetRect.left + targetRect.width / 2;
|
|
510
|
+
const y_end = targetRect.top + targetRect.height / 2;
|
|
511
|
+
const dx = x_end - x_start;
|
|
512
|
+
const dy = y_end - y_start;
|
|
513
|
+
|
|
514
|
+
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
|
|
515
|
+
// Already centered, just reset styles
|
|
516
|
+
piece.element.style.position = '';
|
|
517
|
+
piece.element.style.left = '';
|
|
518
|
+
piece.element.style.top = '';
|
|
519
|
+
piece.element.style.transform = '';
|
|
520
|
+
piece.element.style.zIndex = '';
|
|
521
|
+
if (callback) callback();
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const keyframes = [
|
|
526
|
+
{ transform: 'translate(0, 0)' },
|
|
527
|
+
{ transform: `translate(${dx}px, ${dy}px)` }
|
|
528
|
+
];
|
|
529
|
+
|
|
530
|
+
if (piece.element.animate) {
|
|
531
|
+
const animation = piece.element.animate(keyframes, {
|
|
532
|
+
duration: duration,
|
|
533
|
+
easing: 'ease',
|
|
534
|
+
fill: 'none' // Don't keep the final position
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
animation.onfinish = () => {
|
|
538
|
+
// Defensive: check if element still exists
|
|
539
|
+
if (!piece.element) {
|
|
540
|
+
if (callback) callback();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
// Reset all drag-related styles to let default CSS handle positioning
|
|
544
|
+
piece.element.style.position = '';
|
|
545
|
+
piece.element.style.left = '';
|
|
546
|
+
piece.element.style.top = '';
|
|
547
|
+
piece.element.style.transform = '';
|
|
548
|
+
piece.element.style.zIndex = '';
|
|
549
|
+
piece.element.style.transition = '';
|
|
550
|
+
|
|
551
|
+
if (callback) callback();
|
|
552
|
+
};
|
|
553
|
+
} else {
|
|
554
|
+
// Fallback for browsers without Web Animations API
|
|
555
|
+
piece.element.style.transition = `transform ${duration}ms ease`;
|
|
556
|
+
piece.element.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
557
|
+
|
|
558
|
+
setTimeout(() => {
|
|
559
|
+
// Defensive: check if element still exists
|
|
560
|
+
if (!piece.element) {
|
|
561
|
+
if (callback) callback();
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
// After animation, reset ALL positioning styles and let CSS handle centering
|
|
565
|
+
piece.element.style.position = 'relative';
|
|
566
|
+
piece.element.style.left = '0';
|
|
567
|
+
piece.element.style.top = '0';
|
|
568
|
+
piece.element.style.transform = 'translate(-50%, -50%)';
|
|
569
|
+
piece.element.style.zIndex = '20';
|
|
570
|
+
piece.element.style.transition = 'none';
|
|
571
|
+
|
|
572
|
+
if (callback) callback();
|
|
573
|
+
}, duration);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Handles square click events
|
|
579
|
+
* @param {Square} square - Clicked square
|
|
580
|
+
* @param {Function} onMove - Move callback
|
|
581
|
+
* @param {Function} onSelect - Select callback
|
|
582
|
+
* @param {Function} onDeselect - Deselect callback
|
|
583
|
+
* @param {boolean} [animate=true] - Whether to animate the move
|
|
584
|
+
* @param {boolean} [dragged=false] - Whether this was triggered by drag
|
|
585
|
+
* @returns {boolean} True if move was successful
|
|
586
|
+
*/
|
|
587
|
+
onClick(square, onMove, onSelect, onDeselect, animate = true, dragged = false) {
|
|
588
|
+
// Always ensure we're not in dragging state during click operations
|
|
589
|
+
this.isDragging = false;
|
|
590
|
+
|
|
591
|
+
// If we're animating, ignore the click to prevent state corruption
|
|
592
|
+
if (this.isAnimating) {
|
|
593
|
+
logger.debug('EventService.onClick: Ignoring click during animation');
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
logger.debug('=== EventService.onClick START ===');
|
|
598
|
+
logger.debug('EventService.onClick: square =', square.id, 'clicked =', this.clicked?.id || 'none', 'isAnimating =', this.isAnimating);
|
|
599
|
+
logger.debug('EventService.onClick: Square piece =', square.piece ? `${square.piece.color}${square.piece.type}` : 'empty');
|
|
600
|
+
logger.debug('EventService.onClick: Clicked square piece =', this.clicked?.piece ? `${this.clicked.piece.color}${this.clicked.piece.type}` : (this.clicked ? 'empty' : 'none'));
|
|
601
|
+
|
|
602
|
+
let from = this.clicked;
|
|
603
|
+
let promotion = null;
|
|
604
|
+
|
|
605
|
+
// Handle promotion state
|
|
606
|
+
if (this.promoting) {
|
|
607
|
+
if (this.promoting === 'none') {
|
|
608
|
+
from = null;
|
|
609
|
+
} else {
|
|
610
|
+
promotion = this.promoting;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
this.promoting = false;
|
|
614
|
+
this.boardService.applyToAllSquares('removePromotion');
|
|
615
|
+
this.boardService.applyToAllSquares('removeCover');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// No source square selected
|
|
619
|
+
if (!from) {
|
|
620
|
+
logger.debug('EventService.onClick: No source square, checking if can move:', square.id);
|
|
621
|
+
if (this.moveService.canMove(square)) {
|
|
622
|
+
logger.debug('EventService.onClick: Can move! Setting clicked to:', square.id);
|
|
623
|
+
this.clicked = square;
|
|
624
|
+
if (this.config.clickable) {
|
|
625
|
+
// Ensure visual selection happens after state is set
|
|
626
|
+
onSelect(square);
|
|
627
|
+
logger.debug('EventService.onClick: Square selected visually:', square.id);
|
|
628
|
+
}
|
|
629
|
+
logger.debug('EventService.onClick: After setting, clicked =', this.clicked?.id || 'none');
|
|
630
|
+
return false;
|
|
631
|
+
} else {
|
|
632
|
+
logger.debug('EventService.onClick: Cannot move square:', square.id);
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
logger.debug('EventService.onClick: We have a source square:', from.id, 'target:', square.id);
|
|
638
|
+
|
|
639
|
+
// Clicking same square - deselect
|
|
640
|
+
if (this.clicked === square) {
|
|
641
|
+
onDeselect(square);
|
|
642
|
+
this.clicked = null;
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Check if move requires promotion
|
|
647
|
+
if (!promotion && this.moveService.requiresPromotion(new Move(from, square))) {
|
|
648
|
+
logger.debug('Move requires promotion:', from.id, '->', square.id);
|
|
649
|
+
|
|
650
|
+
// Set up promotion UI
|
|
651
|
+
this.moveService.setupPromotion(
|
|
652
|
+
new Move(from, square),
|
|
653
|
+
this.boardService.squares,
|
|
654
|
+
(selectedPromotion) => {
|
|
655
|
+
logger.debug('Promotion selected:', selectedPromotion);
|
|
656
|
+
|
|
657
|
+
// Clear promotion UI first
|
|
658
|
+
this.boardService.applyToAllSquares('removePromotion');
|
|
659
|
+
this.boardService.applyToAllSquares('removeCover');
|
|
660
|
+
|
|
661
|
+
// Execute the move with promotion
|
|
662
|
+
const moveResult = onMove(from, square, selectedPromotion, animate);
|
|
663
|
+
|
|
664
|
+
if (moveResult) {
|
|
665
|
+
// After a successful promotion move, we need to replace the piece
|
|
666
|
+
// after the drop animation completes
|
|
667
|
+
this._schedulePromotionPieceReplacement(square, selectedPromotion);
|
|
668
|
+
|
|
669
|
+
onDeselect(from);
|
|
670
|
+
this.clicked = null;
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
() => {
|
|
674
|
+
logger.debug('Promotion cancelled');
|
|
675
|
+
|
|
676
|
+
// Clear promotion UI on cancel
|
|
677
|
+
this.boardService.applyToAllSquares('removePromotion');
|
|
678
|
+
this.boardService.applyToAllSquares('removeCover');
|
|
679
|
+
|
|
680
|
+
onDeselect(from);
|
|
681
|
+
this.clicked = null;
|
|
682
|
+
}
|
|
683
|
+
);
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Attempt to make move
|
|
688
|
+
logger.debug('EventService.onClick: Attempting move from', from.id, 'to', square.id);
|
|
689
|
+
logger.debug('EventService.onClick: Current game state before move attempt');
|
|
690
|
+
const moveResult = onMove(from, square, promotion, animate);
|
|
691
|
+
|
|
692
|
+
if (moveResult) {
|
|
693
|
+
// Move successful
|
|
694
|
+
logger.debug('EventService.onClick: Move successful');
|
|
695
|
+
onDeselect(from);
|
|
696
|
+
this.clicked = null;
|
|
697
|
+
logger.debug('=== EventService.onClick END (move successful) ===');
|
|
698
|
+
return true;
|
|
699
|
+
} else {
|
|
700
|
+
// Move failed - deselect current and try to select new piece
|
|
701
|
+
logger.debug('EventService.onClick: Move failed from', from.id, 'to', square.id, '- resetting state');
|
|
702
|
+
|
|
703
|
+
// Always deselect the current piece first
|
|
704
|
+
onDeselect(from);
|
|
705
|
+
this.clicked = null;
|
|
706
|
+
this.isDragging = false;
|
|
707
|
+
|
|
708
|
+
// Clear move-related visual states but preserve highlights
|
|
709
|
+
this.boardService.applyToAllSquares('removeHint');
|
|
710
|
+
|
|
711
|
+
// Now check if the target square has a piece we can move and select it
|
|
712
|
+
if (this.moveService.canMove(square)) {
|
|
713
|
+
logger.debug('EventService.onClick: Can select new piece at', square.id);
|
|
714
|
+
|
|
715
|
+
this.clicked = square;
|
|
716
|
+
if (this.config.clickable) {
|
|
717
|
+
onSelect(square);
|
|
718
|
+
logger.debug('EventService.onClick: New piece selected visually:', square.id);
|
|
719
|
+
}
|
|
720
|
+
logger.debug('EventService.onClick: New piece selected at', square.id);
|
|
721
|
+
} else {
|
|
722
|
+
logger.debug('EventService.onClick: Cannot select piece at', square.id, '- staying deselected');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
logger.debug('=== EventService.onClick END (move failed) ===');
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Schedules piece replacement after promotion animation
|
|
732
|
+
* @private
|
|
733
|
+
* @param {Square} square - Target square
|
|
734
|
+
* @param {string} promotionPiece - Piece to promote to
|
|
735
|
+
*/
|
|
736
|
+
_schedulePromotionPieceReplacement(square, promotionPiece) {
|
|
737
|
+
// Mark that we're doing a promotion to prevent interference
|
|
738
|
+
this.chessboard._isPromoting = true;
|
|
739
|
+
|
|
740
|
+
// Use a more robust approach: poll for the piece to be present
|
|
741
|
+
this._waitForPieceAndReplace(square, promotionPiece, 0);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Waits for piece to be present and then replaces it
|
|
746
|
+
* @private
|
|
747
|
+
* @param {Square} square - Target square
|
|
748
|
+
* @param {string} promotionPiece - Piece to promote to
|
|
749
|
+
* @param {number} attempt - Current attempt number
|
|
750
|
+
*/
|
|
751
|
+
_waitForPieceAndReplace(square, promotionPiece, attempt) {
|
|
752
|
+
const maxAttempts = 20; // Maximum 1 second of waiting (20 * 50ms)
|
|
753
|
+
const targetSquare = this.boardService.getSquare(square.id);
|
|
754
|
+
|
|
755
|
+
if (!targetSquare) {
|
|
756
|
+
logger.warn('Target square not found:', square.id);
|
|
757
|
+
this.chessboard._isPromoting = false;
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Check if piece is present and ready
|
|
762
|
+
if (targetSquare.piece && targetSquare.piece.element) {
|
|
763
|
+
logger.debug('Piece found on', square.id, 'after', attempt, 'attempts');
|
|
764
|
+
this._replacePromotionPiece(square, promotionPiece);
|
|
765
|
+
|
|
766
|
+
// Allow normal updates again after transformation
|
|
767
|
+
setTimeout(() => {
|
|
768
|
+
this.chessboard._isPromoting = false;
|
|
769
|
+
logger.debug('Promotion protection ended');
|
|
770
|
+
// Force a board update to ensure everything is correctly synchronized
|
|
771
|
+
this.chessboard._updateBoardPieces(false);
|
|
772
|
+
}, 400); // Wait for transformation animation to complete
|
|
773
|
+
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// If piece not found and we haven't exceeded max attempts, try again
|
|
778
|
+
if (attempt < maxAttempts) {
|
|
779
|
+
setTimeout(() => {
|
|
780
|
+
this._waitForPieceAndReplace(square, promotionPiece, attempt + 1);
|
|
781
|
+
}, 50);
|
|
782
|
+
} else {
|
|
783
|
+
logger.warn('Failed to find piece for promotion after', maxAttempts, 'attempts');
|
|
784
|
+
this.chessboard._isPromoting = false;
|
|
785
|
+
|
|
786
|
+
// Force a board update to recover from the failed promotion
|
|
787
|
+
this.chessboard._updateBoardPieces(false);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Replaces the piece on the square with the promotion piece
|
|
793
|
+
* @private
|
|
794
|
+
* @param {Square} square - Target square
|
|
795
|
+
* @param {string} promotionPiece - Piece to promote to
|
|
796
|
+
*/
|
|
797
|
+
_replacePromotionPiece(square, promotionPiece) {
|
|
798
|
+
logger.debug('Replacing piece on', square.id, 'with', promotionPiece);
|
|
799
|
+
|
|
800
|
+
// Get the target square from the board service
|
|
801
|
+
const targetSquare = this.boardService.getSquare(square.id);
|
|
802
|
+
if (!targetSquare) {
|
|
803
|
+
logger.debug('Target square not found:', square.id);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Get the game state to determine the correct piece color
|
|
808
|
+
const gameState = this.chessboard.positionService.getGame();
|
|
809
|
+
const gamePiece = gameState.get(targetSquare.id);
|
|
810
|
+
|
|
811
|
+
if (!gamePiece) {
|
|
812
|
+
logger.debug('No piece found in game state for', targetSquare.id);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Get the current piece on the square
|
|
817
|
+
const currentPiece = targetSquare.piece;
|
|
818
|
+
|
|
819
|
+
if (!currentPiece) {
|
|
820
|
+
logger.warn('No piece found on target square for promotion');
|
|
821
|
+
|
|
822
|
+
// Try to recover by creating a new piece
|
|
823
|
+
const pieceId = promotionPiece + gamePiece.color;
|
|
824
|
+
const piecePath = this.chessboard.pieceService.getPiecePath(pieceId);
|
|
825
|
+
|
|
826
|
+
const newPiece = new Piece(
|
|
827
|
+
gamePiece.color,
|
|
828
|
+
promotionPiece,
|
|
829
|
+
piecePath
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
// Place the new piece on the square
|
|
833
|
+
targetSquare.putPiece(newPiece);
|
|
834
|
+
|
|
835
|
+
// Set up drag functionality
|
|
836
|
+
const dragFunction = this.chessboard._createDragFunction.bind(this.chessboard);
|
|
837
|
+
newPiece.setDrag(dragFunction(targetSquare, newPiece));
|
|
838
|
+
|
|
839
|
+
logger.debug('Created new promotion piece:', pieceId, 'on', targetSquare.id);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Create the piece ID and get the path
|
|
844
|
+
const pieceId = promotionPiece + gamePiece.color;
|
|
845
|
+
const piecePath = this.chessboard.pieceService.getPiecePath(pieceId);
|
|
846
|
+
|
|
847
|
+
logger.debug('Transforming piece to:', pieceId, 'with path:', piecePath);
|
|
848
|
+
|
|
849
|
+
// Use the new smooth transformation animation
|
|
850
|
+
currentPiece.transformTo(
|
|
851
|
+
promotionPiece,
|
|
852
|
+
piecePath,
|
|
853
|
+
300, // Duration of the transformation animation
|
|
854
|
+
() => {
|
|
855
|
+
// After transformation, set up drag functionality
|
|
856
|
+
const dragFunction = this.chessboard._createDragFunction.bind(this.chessboard);
|
|
857
|
+
currentPiece.setDrag(dragFunction(targetSquare, currentPiece));
|
|
858
|
+
|
|
859
|
+
// Ensure hints are properly updated after promotion
|
|
860
|
+
if (this.config.hints && this.chessboard.moveService) {
|
|
861
|
+
setTimeout(() => {
|
|
862
|
+
this.chessboard.moveService.clearCache();
|
|
863
|
+
}, 100);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
logger.debug('Successfully transformed piece on', targetSquare.id, 'to', pieceId);
|
|
867
|
+
}
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Sets the clicked square
|
|
873
|
+
* @param {Square|null} square - Square to set as clicked
|
|
874
|
+
*/
|
|
875
|
+
setClicked(square) {
|
|
876
|
+
this.clicked = square;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Gets the currently clicked square
|
|
881
|
+
* @returns {Square|null} Currently clicked square
|
|
882
|
+
*/
|
|
883
|
+
getClicked() {
|
|
884
|
+
return this.clicked;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Sets the promotion state
|
|
889
|
+
* @param {string|boolean} promotion - Promotion piece type or false
|
|
890
|
+
*/
|
|
891
|
+
setPromoting(promotion) {
|
|
892
|
+
this.promoting = promotion;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Gets the promotion state
|
|
897
|
+
* @returns {string|boolean} Current promotion state
|
|
898
|
+
*/
|
|
899
|
+
getPromoting() {
|
|
900
|
+
return this.promoting;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Sets the animation state
|
|
905
|
+
* @param {boolean} isAnimating - Whether animations are in progress
|
|
906
|
+
*/
|
|
907
|
+
setAnimating(isAnimating) {
|
|
908
|
+
this.isAnimating = isAnimating;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Gets the animation state
|
|
913
|
+
* @returns {boolean} Whether animations are in progress
|
|
914
|
+
*/
|
|
915
|
+
getAnimating() {
|
|
916
|
+
return this.isAnimating;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Removes all existing event listeners
|
|
921
|
+
*/
|
|
922
|
+
removeListeners() {
|
|
923
|
+
this.eventListeners.forEach((listeners, squareId) => {
|
|
924
|
+
listeners.forEach(({ element, type, handler }) => {
|
|
925
|
+
element.removeEventListener(type, handler);
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
this.eventListeners.clear();
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Removes all event listeners
|
|
934
|
+
*/
|
|
935
|
+
removeAllListeners() {
|
|
936
|
+
this.eventListeners.forEach((listeners, squareId) => {
|
|
937
|
+
listeners.forEach(({ element, type, handler }) => {
|
|
938
|
+
element.removeEventListener(type, handler);
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
this.eventListeners.clear();
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Cleans up resources
|
|
947
|
+
*/
|
|
948
|
+
destroy() {
|
|
949
|
+
this.removeAllListeners();
|
|
950
|
+
this.clicked = null;
|
|
951
|
+
this.promoting = false;
|
|
952
|
+
this.isAnimating = false;
|
|
953
|
+
this.isDragging = false;
|
|
954
|
+
}
|
|
955
|
+
}
|