@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.
- package/.eslintrc.json +227 -0
- package/.github/instructions/copilot-instuctions.md +1671 -0
- package/README.md +127 -403
- package/assets/themes/alepot/theme.json +42 -0
- package/assets/themes/default/theme.json +42 -0
- package/chessboard.bundle.js +782 -119
- package/config/jest.config.js +15 -0
- package/config/rollup.config.js +35 -0
- package/dist/chessboard.cjs.js +10476 -0
- package/dist/chessboard.css +197 -0
- package/dist/chessboard.esm.js +10407 -0
- package/dist/chessboard.iife.js +10481 -0
- package/dist/chessboard.umd.js +10482 -0
- package/jest.config.js +2 -7
- package/package.json +18 -3
- package/rollup.config.js +2 -11
- package/{chessboard.move.js → src/components/Move.js} +3 -3
- package/src/components/Piece.js +273 -0
- package/{chessboard.square.js → src/components/Square.js} +60 -7
- package/src/constants/index.js +15 -0
- package/src/constants/positions.js +62 -0
- package/src/core/Chessboard.js +1930 -0
- package/src/core/ChessboardConfig.js +458 -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 +807 -0
- package/src/services/MoveService.js +594 -0
- package/src/services/PieceService.js +303 -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} +3 -0
- package/src/styles/index.css +4 -0
- package/src/styles/pieces.css +66 -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/tests/unit/chessboard-config-animations.test.js +106 -0
- package/tests/unit/chessboard-robust.test.js +163 -0
- package/tests/unit/chessboard.test.js +183 -0
- package/chessboard.config.js +0 -147
- package/chessboard.js +0 -981
- package/chessboard.piece.js +0 -115
- 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
- /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
|
+
}
|