@alepot55/chessboardjs 2.2.0 → 2.3.2

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.
Files changed (81) hide show
  1. package/.eslintrc.json +227 -0
  2. package/.github/instructions/copilot-instuctions.md +1671 -0
  3. package/README.md +127 -403
  4. package/assets/themes/alepot/theme.json +42 -0
  5. package/assets/themes/default/theme.json +42 -0
  6. package/chessboard.bundle.js +782 -119
  7. package/config/jest.config.js +15 -0
  8. package/config/rollup.config.js +35 -0
  9. package/dist/chessboard.cjs.js +10476 -0
  10. package/dist/chessboard.css +197 -0
  11. package/dist/chessboard.esm.js +10407 -0
  12. package/dist/chessboard.iife.js +10481 -0
  13. package/dist/chessboard.umd.js +10482 -0
  14. package/jest.config.js +2 -7
  15. package/package.json +18 -3
  16. package/rollup.config.js +2 -11
  17. package/{chessboard.move.js → src/components/Move.js} +3 -3
  18. package/src/components/Piece.js +273 -0
  19. package/{chessboard.square.js → src/components/Square.js} +60 -7
  20. package/src/constants/index.js +15 -0
  21. package/src/constants/positions.js +62 -0
  22. package/src/core/Chessboard.js +1930 -0
  23. package/src/core/ChessboardConfig.js +458 -0
  24. package/src/core/ChessboardFactory.js +385 -0
  25. package/src/core/index.js +141 -0
  26. package/src/errors/ChessboardError.js +133 -0
  27. package/src/errors/index.js +15 -0
  28. package/src/errors/messages.js +189 -0
  29. package/src/index.js +103 -0
  30. package/src/services/AnimationService.js +180 -0
  31. package/src/services/BoardService.js +156 -0
  32. package/src/services/CoordinateService.js +355 -0
  33. package/src/services/EventService.js +807 -0
  34. package/src/services/MoveService.js +594 -0
  35. package/src/services/PieceService.js +303 -0
  36. package/src/services/PositionService.js +237 -0
  37. package/src/services/ValidationService.js +673 -0
  38. package/src/services/index.js +14 -0
  39. package/src/styles/animations.css +46 -0
  40. package/{chessboard.css → src/styles/board.css} +3 -0
  41. package/src/styles/index.css +4 -0
  42. package/src/styles/pieces.css +66 -0
  43. package/src/utils/animations.js +37 -0
  44. package/{chess.js → src/utils/chess.js} +16 -16
  45. package/src/utils/coordinates.js +62 -0
  46. package/src/utils/cross-browser.js +150 -0
  47. package/src/utils/logger.js +422 -0
  48. package/src/utils/performance.js +311 -0
  49. package/src/utils/validation.js +458 -0
  50. package/tests/unit/chessboard-config-animations.test.js +106 -0
  51. package/tests/unit/chessboard-robust.test.js +163 -0
  52. package/tests/unit/chessboard.test.js +183 -0
  53. package/chessboard.config.js +0 -147
  54. package/chessboard.js +0 -981
  55. package/chessboard.piece.js +0 -115
  56. package/test/chessboard.test.js +0 -128
  57. /package/{alepot_theme → assets/themes/alepot}/bb.svg +0 -0
  58. /package/{alepot_theme → assets/themes/alepot}/bw.svg +0 -0
  59. /package/{alepot_theme → assets/themes/alepot}/kb.svg +0 -0
  60. /package/{alepot_theme → assets/themes/alepot}/kw.svg +0 -0
  61. /package/{alepot_theme → assets/themes/alepot}/nb.svg +0 -0
  62. /package/{alepot_theme → assets/themes/alepot}/nw.svg +0 -0
  63. /package/{alepot_theme → assets/themes/alepot}/pb.svg +0 -0
  64. /package/{alepot_theme → assets/themes/alepot}/pw.svg +0 -0
  65. /package/{alepot_theme → assets/themes/alepot}/qb.svg +0 -0
  66. /package/{alepot_theme → assets/themes/alepot}/qw.svg +0 -0
  67. /package/{alepot_theme → assets/themes/alepot}/rb.svg +0 -0
  68. /package/{alepot_theme → assets/themes/alepot}/rw.svg +0 -0
  69. /package/{default_pieces → assets/themes/default}/bb.svg +0 -0
  70. /package/{default_pieces → assets/themes/default}/bw.svg +0 -0
  71. /package/{default_pieces → assets/themes/default}/kb.svg +0 -0
  72. /package/{default_pieces → assets/themes/default}/kw.svg +0 -0
  73. /package/{default_pieces → assets/themes/default}/nb.svg +0 -0
  74. /package/{default_pieces → assets/themes/default}/nw.svg +0 -0
  75. /package/{default_pieces → assets/themes/default}/pb.svg +0 -0
  76. /package/{default_pieces → assets/themes/default}/pw.svg +0 -0
  77. /package/{default_pieces → assets/themes/default}/qb.svg +0 -0
  78. /package/{default_pieces → assets/themes/default}/qw.svg +0 -0
  79. /package/{default_pieces → assets/themes/default}/rb.svg +0 -0
  80. /package/{default_pieces → assets/themes/default}/rw.svg +0 -0
  81. /package/{.babelrc → config/.babelrc} +0 -0
@@ -0,0 +1,807 @@
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
+
13
+ /**
14
+ * Service responsible for event handling and user interactions
15
+ * @class
16
+ */
17
+ export class EventService {
18
+ /**
19
+ * Creates a new EventService instance
20
+ * @param {ChessboardConfig} config - Board configuration
21
+ * @param {BoardService} boardService - Board service instance
22
+ * @param {MoveService} moveService - Move service instance
23
+ * @param {CoordinateService} coordinateService - Coordinate service instance
24
+ * @param {Chessboard} chessboard - Chessboard instance
25
+ */
26
+ constructor(config, boardService, moveService, coordinateService, chessboard) {
27
+ this.config = config;
28
+ this.boardService = boardService;
29
+ this.moveService = moveService;
30
+ this.coordinateService = coordinateService;
31
+ this.chessboard = chessboard;
32
+
33
+ // State management
34
+ this.clicked = null;
35
+ this.promoting = false;
36
+ this.isAnimating = false;
37
+
38
+ // Event listeners storage for cleanup
39
+ this.eventListeners = new Map();
40
+ }
41
+
42
+ /**
43
+ * Adds event listeners to all squares
44
+ * @param {Function} onSquareClick - Callback for square clicks
45
+ * @param {Function} onPieceHover - Callback for piece hover
46
+ * @param {Function} onPieceLeave - Callback for piece leave
47
+ */
48
+ addListeners(onSquareClick, onPieceHover, onPieceLeave) {
49
+ // Remove existing listeners to avoid duplicates
50
+ this.removeListeners();
51
+
52
+ const squares = this.boardService.getAllSquares();
53
+
54
+ Object.values(squares).forEach(square => {
55
+ this._addSquareListeners(square, onSquareClick, onPieceHover, onPieceLeave);
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Adds event listeners to a specific square
61
+ * @private
62
+ * @param {Square} square - Square to add listeners to
63
+ * @param {Function} onSquareClick - Click callback
64
+ * @param {Function} onPieceHover - Hover callback
65
+ * @param {Function} onPieceLeave - Leave callback
66
+ */
67
+ _addSquareListeners(square, onSquareClick, onPieceHover, onPieceLeave) {
68
+ const listeners = [];
69
+
70
+ // Throttled hover handlers for performance
71
+ const throttledHover = rafThrottle((e) => {
72
+ if (!this.clicked && this.config.hints) {
73
+ onPieceHover(square);
74
+ }
75
+ });
76
+
77
+ const throttledLeave = rafThrottle((e) => {
78
+ if (!this.clicked && this.config.hints) {
79
+ onPieceLeave(square);
80
+ }
81
+ });
82
+
83
+ // Click handler
84
+ const handleClick = (e) => {
85
+ e.stopPropagation();
86
+ if (this.config.clickable && !this.isAnimating) {
87
+ onSquareClick(square);
88
+ }
89
+ };
90
+
91
+ // Add listeners
92
+ square.element.addEventListener('mouseover', throttledHover);
93
+ square.element.addEventListener('mouseout', throttledLeave);
94
+ square.element.addEventListener('click', handleClick);
95
+ square.element.addEventListener('touchstart', handleClick);
96
+
97
+ // Store listeners for cleanup
98
+ listeners.push(
99
+ { element: square.element, type: 'mouseover', handler: throttledHover },
100
+ { element: square.element, type: 'mouseout', handler: throttledLeave },
101
+ { element: square.element, type: 'click', handler: handleClick },
102
+ { element: square.element, type: 'touchstart', handler: handleClick }
103
+ );
104
+
105
+ this.eventListeners.set(square.id, listeners);
106
+ }
107
+
108
+ /**
109
+ * Creates a drag function for a piece
110
+ * @param {Square} square - Square containing the piece
111
+ * @param {Piece} piece - Piece to create drag function for
112
+ * @param {Function} onDragStart - Drag start callback
113
+ * @param {Function} onDragMove - Drag move callback
114
+ * @param {Function} onDrop - Drop callback
115
+ * @param {Function} onSnapback - Snapback callback
116
+ * @param {Function} onMove - Move execution callback
117
+ * @param {Function} onRemove - Remove piece callback
118
+ * @returns {Function} Drag event handler
119
+ */
120
+ createDragFunction(square, piece, onDragStart, onDragMove, onDrop, onSnapback, onMove, onRemove) {
121
+ return (event) => {
122
+ event.preventDefault();
123
+
124
+ if (!this.config.draggable || !piece || this.isAnimating) {
125
+ return;
126
+ }
127
+
128
+ const originalFrom = square;
129
+ let isDragging = false;
130
+ let from = originalFrom;
131
+ let to = square;
132
+ let previousHighlight = null;
133
+
134
+ const img = piece.element;
135
+
136
+ if (!this.moveService.canMove(from)) {
137
+ return;
138
+ }
139
+
140
+ // Track initial position for drag threshold
141
+ const startX = event.clientX || (event.touches && event.touches[0]?.clientX) || 0;
142
+ const startY = event.clientY || (event.touches && event.touches[0]?.clientY) || 0;
143
+
144
+ const moveAt = (event) => {
145
+ const boardElement = this.boardService.element;
146
+ const squareSize = boardElement.offsetWidth / 8;
147
+
148
+ // Get mouse coordinates
149
+ let clientX, clientY;
150
+ if (event.touches && event.touches[0]) {
151
+ clientX = event.touches[0].clientX;
152
+ clientY = event.touches[0].clientY;
153
+ } else {
154
+ clientX = event.clientX;
155
+ clientY = event.clientY;
156
+ }
157
+
158
+ // Calculate position relative to board
159
+ const boardRect = boardElement.getBoundingClientRect();
160
+ const x = clientX - boardRect.left - (squareSize / 2);
161
+ const y = clientY - boardRect.top - (squareSize / 2);
162
+
163
+ img.style.left = x + 'px';
164
+ img.style.top = y + 'px';
165
+
166
+ return true;
167
+ };
168
+
169
+ const onMouseMove = (event) => {
170
+ const currentX = event.clientX || 0;
171
+ const currentY = event.clientY || 0;
172
+ const deltaX = Math.abs(currentX - startX);
173
+ const deltaY = Math.abs(currentY - startY);
174
+
175
+ // Start dragging if mouse moved enough
176
+ if (!isDragging && (deltaX > 3 || deltaY > 3)) {
177
+ isDragging = true;
178
+
179
+ // Set up drag state
180
+ if (!this.config.clickable) {
181
+ this.clicked = null;
182
+ this.clicked = from;
183
+ } else if (!this.clicked) {
184
+ this.clicked = from;
185
+ }
186
+
187
+ // Visual feedback
188
+ if (this.config.clickable) {
189
+ from.select();
190
+ // Show hints would be handled by the main class
191
+ }
192
+
193
+ // Prepare piece for dragging
194
+ img.style.position = 'absolute';
195
+ img.style.zIndex = '100';
196
+ img.classList.add('dragging');
197
+
198
+ DragOptimizations.enableForDrag(img);
199
+
200
+ // Call drag start callback
201
+ if (!onDragStart(square, piece)) {
202
+ return;
203
+ }
204
+ }
205
+
206
+ if (!isDragging) return;
207
+
208
+ if (!moveAt(event)) return;
209
+
210
+ // Update target square
211
+ const boardElement = this.boardService.element;
212
+ const boardRect = boardElement.getBoundingClientRect();
213
+ const x = event.clientX - boardRect.left;
214
+ const y = event.clientY - boardRect.top;
215
+
216
+ let newTo = null;
217
+ if (x >= 0 && x <= boardRect.width && y >= 0 && y <= boardRect.height) {
218
+ const squareId = this.coordinateService.pixelToSquareID(x, y, boardElement);
219
+ newTo = squareId ? this.boardService.getSquare(squareId) : null;
220
+ }
221
+
222
+ to = newTo;
223
+ onDragMove(from, to, piece);
224
+
225
+ // Update visual feedback
226
+ if (to !== previousHighlight) {
227
+ to?.highlight();
228
+ previousHighlight?.dehighlight();
229
+ previousHighlight = to;
230
+ }
231
+ };
232
+
233
+ const onMouseUp = () => {
234
+ // Clean up visual feedback
235
+ previousHighlight?.dehighlight();
236
+ document.removeEventListener('mousemove', onMouseMove);
237
+ window.removeEventListener('mouseup', onMouseUp);
238
+
239
+ // If this was just a click, don't interfere
240
+ if (!isDragging) {
241
+ return;
242
+ }
243
+
244
+ // Clean up drag state
245
+ img.style.zIndex = '20';
246
+ img.classList.remove('dragging');
247
+ img.style.willChange = 'auto';
248
+
249
+ // Handle drop
250
+ const dropResult = onDrop(originalFrom, to, piece);
251
+ const isTrashDrop = !to && (this.config.dropOffBoard === 'trash' || dropResult === 'trash');
252
+
253
+ if (isTrashDrop) {
254
+ this._handleTrashDrop(originalFrom, onRemove);
255
+ } else if (!to) {
256
+ // Reset piece position instantly for snapback
257
+ img.style.position = '';
258
+ img.style.left = '';
259
+ img.style.top = '';
260
+ img.style.transform = '';
261
+
262
+ this._handleSnapback(originalFrom, piece, onSnapback);
263
+ } else {
264
+ // Handle drop like a click - simple and reliable
265
+ this._handleDrop(originalFrom, to, piece, onMove, onSnapback);
266
+ }
267
+ };
268
+
269
+ // Attach event listeners
270
+ window.addEventListener('mouseup', onMouseUp, { once: true });
271
+ document.addEventListener('mousemove', onMouseMove);
272
+ img.addEventListener('mouseup', onMouseUp, { once: true });
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Handles trash drop (piece removal)
278
+ * @private
279
+ * @param {Square} fromSquare - Source square
280
+ * @param {Function} onRemove - Callback to remove piece
281
+ */
282
+ _handleTrashDrop(fromSquare, onRemove) {
283
+ this.boardService.applyToAllSquares('unmoved');
284
+ this.boardService.applyToAllSquares('removeHint');
285
+ fromSquare.deselect();
286
+
287
+ if (onRemove) {
288
+ onRemove(fromSquare.getId());
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Handles snapback animation
294
+ * @private
295
+ * @param {Square} fromSquare - Source square
296
+ * @param {Piece} piece - Piece to snapback
297
+ * @param {Function} onSnapback - Snapback callback
298
+ */
299
+ _handleSnapback(fromSquare, piece, onSnapback) {
300
+ if (fromSquare && fromSquare.piece) {
301
+ if (onSnapback) {
302
+ onSnapback(fromSquare, piece);
303
+ }
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Handles successful drop
309
+ * @private
310
+ * @param {Square} fromSquare - Source square
311
+ * @param {Square} toSquare - Target square
312
+ * @param {Piece} piece - Piece being dropped
313
+ * @param {Function} onMove - Move callback
314
+ * @param {Function} onSnapback - Snapback callback
315
+ */
316
+ _handleDrop(fromSquare, toSquare, piece, onMove, onSnapback) {
317
+ this.clicked = fromSquare;
318
+
319
+ // Check if move requires promotion
320
+ if (this.moveService.requiresPromotion(new Move(fromSquare, toSquare))) {
321
+ console.log('Drag move requires promotion:', fromSquare.id, '->', toSquare.id);
322
+
323
+ // Set up promotion UI - use the same logic as click
324
+ this.moveService.setupPromotion(
325
+ new Move(fromSquare, toSquare),
326
+ this.boardService.squares,
327
+ (selectedPromotion) => {
328
+ console.log('Drag promotion selected:', selectedPromotion);
329
+
330
+ // Clear promotion UI first
331
+ this.boardService.applyToAllSquares('removePromotion');
332
+ this.boardService.applyToAllSquares('removeCover');
333
+
334
+ // Execute the move with promotion
335
+ const moveResult = onMove(fromSquare, toSquare, selectedPromotion, true);
336
+
337
+ if (moveResult) {
338
+ // After a successful promotion move, we need to replace the piece
339
+ // after the drop animation completes
340
+ this._schedulePromotionPieceReplacement(toSquare, selectedPromotion);
341
+
342
+ this.clicked = null;
343
+ } else {
344
+ // Move failed - snapback
345
+ this._handleSnapback(fromSquare, piece, onSnapback);
346
+ }
347
+ },
348
+ () => {
349
+ console.log('Drag promotion cancelled');
350
+
351
+ // Clear promotion UI on cancel
352
+ this.boardService.applyToAllSquares('removePromotion');
353
+ this.boardService.applyToAllSquares('removeCover');
354
+
355
+ // Snapback the piece
356
+ this._handleSnapback(fromSquare, piece, onSnapback);
357
+ }
358
+ );
359
+ } else {
360
+ // Regular move - no promotion needed
361
+ const moveSuccess = onMove(fromSquare, toSquare, null, true);
362
+
363
+ if (moveSuccess) {
364
+ // Move successful - reset clicked state
365
+ this.clicked = null;
366
+ } else {
367
+ // Move failed - snapback
368
+ this._handleSnapback(fromSquare, piece, onSnapback);
369
+ }
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Animates piece to center of target square (visual only)
375
+ * @private
376
+ * @param {Piece} piece - Piece to animate
377
+ * @param {Square} targetSquare - Target square
378
+ * @param {Function} callback - Callback when animation completes
379
+ */
380
+ _animatePieceToCenter(piece, targetSquare, callback = null) {
381
+ if (!piece || !targetSquare) {
382
+ if (callback) callback();
383
+ return;
384
+ }
385
+
386
+ const duration = this.config.dropCenterTime;
387
+
388
+ // Get current position of piece element
389
+ const sourceRect = piece.element.getBoundingClientRect();
390
+ const targetRect = targetSquare.element.getBoundingClientRect();
391
+
392
+ const x_start = sourceRect.left + sourceRect.width / 2;
393
+ const y_start = sourceRect.top + sourceRect.height / 2;
394
+ const x_end = targetRect.left + targetRect.width / 2;
395
+ const y_end = targetRect.top + targetRect.height / 2;
396
+ const dx = x_end - x_start;
397
+ const dy = y_end - y_start;
398
+
399
+ if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
400
+ // Already centered, just reset styles
401
+ piece.element.style.position = '';
402
+ piece.element.style.left = '';
403
+ piece.element.style.top = '';
404
+ piece.element.style.transform = '';
405
+ piece.element.style.zIndex = '';
406
+ if (callback) callback();
407
+ return;
408
+ }
409
+
410
+ const keyframes = [
411
+ { transform: 'translate(0, 0)' },
412
+ { transform: `translate(${dx}px, ${dy}px)` }
413
+ ];
414
+
415
+ if (piece.element.animate) {
416
+ const animation = piece.element.animate(keyframes, {
417
+ duration: duration,
418
+ easing: 'ease',
419
+ fill: 'none' // Don't keep the final position
420
+ });
421
+
422
+ animation.onfinish = () => {
423
+ // Defensive: check if element still exists
424
+ if (!piece.element) {
425
+ if (callback) callback();
426
+ return;
427
+ }
428
+ // Reset all drag-related styles to let default CSS handle positioning
429
+ piece.element.style.position = '';
430
+ piece.element.style.left = '';
431
+ piece.element.style.top = '';
432
+ piece.element.style.transform = '';
433
+ piece.element.style.zIndex = '';
434
+ piece.element.style.transition = '';
435
+
436
+ if (callback) callback();
437
+ };
438
+ } else {
439
+ // Fallback for browsers without Web Animations API
440
+ piece.element.style.transition = `transform ${duration}ms ease`;
441
+ piece.element.style.transform = `translate(${dx}px, ${dy}px)`;
442
+
443
+ setTimeout(() => {
444
+ // Defensive: check if element still exists
445
+ if (!piece.element) {
446
+ if (callback) callback();
447
+ return;
448
+ }
449
+ // After animation, reset ALL positioning styles and let CSS handle centering
450
+ piece.element.style.position = 'relative';
451
+ piece.element.style.left = '0';
452
+ piece.element.style.top = '0';
453
+ piece.element.style.transform = 'translate(-50%, -50%)';
454
+ piece.element.style.zIndex = '20';
455
+ piece.element.style.transition = 'none';
456
+
457
+ if (callback) callback();
458
+ }, duration);
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Handles square click events
464
+ * @param {Square} square - Clicked square
465
+ * @param {Function} onMove - Move callback
466
+ * @param {Function} onSelect - Select callback
467
+ * @param {Function} onDeselect - Deselect callback
468
+ * @param {boolean} [animate=true] - Whether to animate the move
469
+ * @param {boolean} [dragged=false] - Whether this was triggered by drag
470
+ * @returns {boolean} True if move was successful
471
+ */
472
+ onClick(square, onMove, onSelect, onDeselect, animate = true, dragged = false) {
473
+ console.log('EventService.onClick: square =', square.id, 'clicked =', this.clicked?.id || 'none');
474
+
475
+ let from = this.clicked;
476
+ let promotion = null;
477
+
478
+ // Handle promotion state
479
+ if (this.promoting) {
480
+ if (this.promoting === 'none') {
481
+ from = null;
482
+ } else {
483
+ promotion = this.promoting;
484
+ }
485
+
486
+ this.promoting = false;
487
+ this.boardService.applyToAllSquares('removePromotion');
488
+ this.boardService.applyToAllSquares('removeCover');
489
+ }
490
+
491
+ // No source square selected
492
+ if (!from) {
493
+ if (this.moveService.canMove(square)) {
494
+ if (this.config.clickable) {
495
+ onSelect(square);
496
+ }
497
+ this.clicked = square;
498
+ return false;
499
+ } else {
500
+ return false;
501
+ }
502
+ }
503
+
504
+ // Clicking same square - deselect
505
+ if (this.clicked === square) {
506
+ onDeselect(square);
507
+ this.clicked = null;
508
+ return false;
509
+ }
510
+
511
+ // Check if move requires promotion
512
+ if (!promotion && this.moveService.requiresPromotion(new Move(from, square))) {
513
+ console.log('Move requires promotion:', from.id, '->', square.id);
514
+
515
+ // Set up promotion UI
516
+ this.moveService.setupPromotion(
517
+ new Move(from, square),
518
+ this.boardService.squares,
519
+ (selectedPromotion) => {
520
+ console.log('Promotion selected:', selectedPromotion);
521
+
522
+ // Clear promotion UI first
523
+ this.boardService.applyToAllSquares('removePromotion');
524
+ this.boardService.applyToAllSquares('removeCover');
525
+
526
+ // Execute the move with promotion
527
+ const moveResult = onMove(from, square, selectedPromotion, animate);
528
+
529
+ if (moveResult) {
530
+ // After a successful promotion move, we need to replace the piece
531
+ // after the drop animation completes
532
+ this._schedulePromotionPieceReplacement(square, selectedPromotion);
533
+
534
+ onDeselect(from);
535
+ this.clicked = null;
536
+ }
537
+ },
538
+ () => {
539
+ console.log('Promotion cancelled');
540
+
541
+ // Clear promotion UI on cancel
542
+ this.boardService.applyToAllSquares('removePromotion');
543
+ this.boardService.applyToAllSquares('removeCover');
544
+
545
+ onDeselect(from);
546
+ this.clicked = null;
547
+ }
548
+ );
549
+ return false;
550
+ }
551
+
552
+ // Attempt to make move
553
+ const moveResult = onMove(from, square, promotion, animate);
554
+
555
+ if (moveResult) {
556
+ // Move successful
557
+ onDeselect(from);
558
+ this.clicked = null;
559
+ return true;
560
+ } else {
561
+ // Move failed - check if clicked square has a piece we can move
562
+ if (this.moveService.canMove(square)) {
563
+ // Deselect the previous piece
564
+ onDeselect(from);
565
+
566
+ // Select the new piece if clicking is enabled
567
+ if (this.config.clickable) {
568
+ onSelect(square);
569
+ }
570
+
571
+ // Set the new piece as clicked
572
+ this.clicked = square;
573
+ return false;
574
+ } else {
575
+ // Move failed and no valid piece to select
576
+ onDeselect(from);
577
+ this.clicked = null;
578
+ return false;
579
+ }
580
+ }
581
+ }
582
+
583
+ /**
584
+ * Schedules piece replacement after promotion animation
585
+ * @private
586
+ * @param {Square} square - Target square
587
+ * @param {string} promotionPiece - Piece to promote to
588
+ */
589
+ _schedulePromotionPieceReplacement(square, promotionPiece) {
590
+ // Mark that we're doing a promotion to prevent interference
591
+ this.chessboard._isPromoting = true;
592
+
593
+ // Use a more robust approach: poll for the piece to be present
594
+ this._waitForPieceAndReplace(square, promotionPiece, 0);
595
+ }
596
+
597
+ /**
598
+ * Waits for piece to be present and then replaces it
599
+ * @private
600
+ * @param {Square} square - Target square
601
+ * @param {string} promotionPiece - Piece to promote to
602
+ * @param {number} attempt - Current attempt number
603
+ */
604
+ _waitForPieceAndReplace(square, promotionPiece, attempt) {
605
+ const maxAttempts = 20; // Maximum 1 second of waiting (20 * 50ms)
606
+ const targetSquare = this.boardService.getSquare(square.id);
607
+
608
+ if (!targetSquare) {
609
+ console.warn('Target square not found:', square.id);
610
+ this.chessboard._isPromoting = false;
611
+ return;
612
+ }
613
+
614
+ // Check if piece is present and ready
615
+ if (targetSquare.piece && targetSquare.piece.element) {
616
+ console.log('Piece found on', square.id, 'after', attempt, 'attempts');
617
+ this._replacePromotionPiece(square, promotionPiece);
618
+
619
+ // Allow normal updates again after transformation
620
+ setTimeout(() => {
621
+ this.chessboard._isPromoting = false;
622
+ console.log('Promotion protection ended');
623
+ // Force a board update to ensure everything is correctly synchronized
624
+ this.chessboard._updateBoardPieces(false);
625
+ }, 400); // Wait for transformation animation to complete
626
+
627
+ return;
628
+ }
629
+
630
+ // If piece not found and we haven't exceeded max attempts, try again
631
+ if (attempt < maxAttempts) {
632
+ setTimeout(() => {
633
+ this._waitForPieceAndReplace(square, promotionPiece, attempt + 1);
634
+ }, 50);
635
+ } else {
636
+ console.warn('Failed to find piece for promotion after', maxAttempts, 'attempts');
637
+ this.chessboard._isPromoting = false;
638
+
639
+ // Force a board update to recover from the failed promotion
640
+ this.chessboard._updateBoardPieces(false);
641
+ }
642
+ }
643
+
644
+ /**
645
+ * Replaces the piece on the square with the promotion piece
646
+ * @private
647
+ * @param {Square} square - Target square
648
+ * @param {string} promotionPiece - Piece to promote to
649
+ */
650
+ _replacePromotionPiece(square, promotionPiece) {
651
+ console.log('Replacing piece on', square.id, 'with', promotionPiece);
652
+
653
+ // Get the target square from the board service
654
+ const targetSquare = this.boardService.getSquare(square.id);
655
+ if (!targetSquare) {
656
+ console.log('Target square not found:', square.id);
657
+ return;
658
+ }
659
+
660
+ // Get the game state to determine the correct piece color
661
+ const gameState = this.chessboard.positionService.getGame();
662
+ const gamePiece = gameState.get(targetSquare.id);
663
+
664
+ if (!gamePiece) {
665
+ console.log('No piece found in game state for', targetSquare.id);
666
+ return;
667
+ }
668
+
669
+ // Get the current piece on the square
670
+ const currentPiece = targetSquare.piece;
671
+
672
+ if (!currentPiece) {
673
+ console.warn('No piece found on target square for promotion');
674
+
675
+ // Try to recover by creating a new piece
676
+ const pieceId = promotionPiece + gamePiece.color;
677
+ const piecePath = this.chessboard.pieceService.getPiecePath(pieceId);
678
+
679
+ const newPiece = new Piece(
680
+ gamePiece.color,
681
+ promotionPiece,
682
+ piecePath
683
+ );
684
+
685
+ // Place the new piece on the square
686
+ targetSquare.putPiece(newPiece);
687
+
688
+ // Set up drag functionality
689
+ const dragFunction = this.chessboard._createDragFunction.bind(this.chessboard);
690
+ newPiece.setDrag(dragFunction(targetSquare, newPiece));
691
+
692
+ console.log('Created new promotion piece:', pieceId, 'on', targetSquare.id);
693
+ return;
694
+ }
695
+
696
+ // Create the piece ID and get the path
697
+ const pieceId = promotionPiece + gamePiece.color;
698
+ const piecePath = this.chessboard.pieceService.getPiecePath(pieceId);
699
+
700
+ console.log('Transforming piece to:', pieceId, 'with path:', piecePath);
701
+
702
+ // Use the new smooth transformation animation
703
+ currentPiece.transformTo(
704
+ promotionPiece,
705
+ piecePath,
706
+ 300, // Duration of the transformation animation
707
+ () => {
708
+ // After transformation, set up drag functionality
709
+ const dragFunction = this.chessboard._createDragFunction.bind(this.chessboard);
710
+ currentPiece.setDrag(dragFunction(targetSquare, currentPiece));
711
+
712
+ // Ensure hints are properly updated after promotion
713
+ if (this.config.hints && this.chessboard.moveService) {
714
+ setTimeout(() => {
715
+ this.chessboard.moveService.clearCache();
716
+ }, 100);
717
+ }
718
+
719
+ console.log('Successfully transformed piece on', targetSquare.id, 'to', pieceId);
720
+ }
721
+ );
722
+ }
723
+
724
+ /**
725
+ * Sets the clicked square
726
+ * @param {Square|null} square - Square to set as clicked
727
+ */
728
+ setClicked(square) {
729
+ this.clicked = square;
730
+ }
731
+
732
+ /**
733
+ * Gets the currently clicked square
734
+ * @returns {Square|null} Currently clicked square
735
+ */
736
+ getClicked() {
737
+ return this.clicked;
738
+ }
739
+
740
+ /**
741
+ * Sets the promotion state
742
+ * @param {string|boolean} promotion - Promotion piece type or false
743
+ */
744
+ setPromoting(promotion) {
745
+ this.promoting = promotion;
746
+ }
747
+
748
+ /**
749
+ * Gets the promotion state
750
+ * @returns {string|boolean} Current promotion state
751
+ */
752
+ getPromoting() {
753
+ return this.promoting;
754
+ }
755
+
756
+ /**
757
+ * Sets the animation state
758
+ * @param {boolean} isAnimating - Whether animations are in progress
759
+ */
760
+ setAnimating(isAnimating) {
761
+ this.isAnimating = isAnimating;
762
+ }
763
+
764
+ /**
765
+ * Gets the animation state
766
+ * @returns {boolean} Whether animations are in progress
767
+ */
768
+ getAnimating() {
769
+ return this.isAnimating;
770
+ }
771
+
772
+ /**
773
+ * Removes all existing event listeners
774
+ */
775
+ removeListeners() {
776
+ this.eventListeners.forEach((listeners, squareId) => {
777
+ listeners.forEach(({ element, type, handler }) => {
778
+ element.removeEventListener(type, handler);
779
+ });
780
+ });
781
+
782
+ this.eventListeners.clear();
783
+ }
784
+
785
+ /**
786
+ * Removes all event listeners
787
+ */
788
+ removeAllListeners() {
789
+ this.eventListeners.forEach((listeners, squareId) => {
790
+ listeners.forEach(({ element, type, handler }) => {
791
+ element.removeEventListener(type, handler);
792
+ });
793
+ });
794
+
795
+ this.eventListeners.clear();
796
+ }
797
+
798
+ /**
799
+ * Cleans up resources
800
+ */
801
+ destroy() {
802
+ this.removeAllListeners();
803
+ this.clicked = null;
804
+ this.promoting = false;
805
+ this.isAnimating = false;
806
+ }
807
+ }