@alepot55/chessboardjs 2.3.6 → 2.3.8

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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Service for managing coordinate conversions and board orientation
2
+ * CoordinateService - Handles coordinate conversions and board orientation logic
3
3
  * @module services/CoordinateService
4
4
  * @since 2.0.0
5
5
  */
@@ -10,19 +10,20 @@ import { ERROR_MESSAGES } from '../errors/messages.js';
10
10
 
11
11
  /**
12
12
  * Service responsible for coordinate conversions and board orientation
13
- * @class
13
+ * @class CoordinateService
14
14
  */
15
15
  export class CoordinateService {
16
16
  /**
17
- * Creates a new CoordinateService instance
18
- * @param {ChessboardConfig} config - Board configuration
17
+ * Create a new CoordinateService instance
18
+ * @param {Object} config - Board configuration
19
19
  */
20
20
  constructor(config) {
21
+ /** @type {Object} */
21
22
  this.config = config;
22
23
  }
23
24
 
24
25
  /**
25
- * Converts logical coordinates to real coordinates based on board orientation
26
+ * Convert logical coordinates to real coordinates based on board orientation
26
27
  * @param {number} row - Row index (0-7)
27
28
  * @param {number} col - Column index (0-7)
28
29
  * @returns {Array<number>} Real coordinates [row, col] (1-8)
@@ -30,18 +31,16 @@ export class CoordinateService {
30
31
  realCoord(row, col) {
31
32
  let realRow = row;
32
33
  let realCol = col;
33
-
34
34
  if (this.isWhiteOriented()) {
35
35
  realRow = 7 - row;
36
36
  } else {
37
37
  realCol = 7 - col;
38
38
  }
39
-
40
39
  return [realRow + 1, realCol + 1];
41
40
  }
42
41
 
43
42
  /**
44
- * Converts board coordinates to square ID
43
+ * Convert board coordinates to square ID
45
44
  * @param {number} row - Row index (0-7)
46
45
  * @param {number} col - Column index (0-7)
47
46
  * @returns {string} Square ID (e.g., 'e4')
@@ -49,7 +48,6 @@ export class CoordinateService {
49
48
  getSquareID(row, col) {
50
49
  row = parseInt(row);
51
50
  col = parseInt(col);
52
-
53
51
  if (this.isWhiteOriented()) {
54
52
  row = 8 - row;
55
53
  col = col + 1;
@@ -57,7 +55,6 @@ export class CoordinateService {
57
55
  row = row + 1;
58
56
  col = 8 - col;
59
57
  }
60
-
61
58
  if (col < 1 || col > 8 || row < 1 || row > 8) {
62
59
  throw new ValidationError(
63
60
  `Invalid board coordinates: row=${row}, col=${col}`,
@@ -65,13 +62,12 @@ export class CoordinateService {
65
62
  { row, col }
66
63
  );
67
64
  }
68
-
69
65
  const letter = BOARD_LETTERS[col - 1];
70
66
  return letter + row;
71
67
  }
72
68
 
73
69
  /**
74
- * Converts square ID to board coordinates
70
+ * Convert square ID to board coordinates
75
71
  * @param {string} squareId - Square ID (e.g., 'e4')
76
72
  * @returns {Array<number>} Board coordinates [row, col] (0-7)
77
73
  */
@@ -83,10 +79,8 @@ export class CoordinateService {
83
79
  squareId
84
80
  );
85
81
  }
86
-
87
82
  const letter = squareId[0];
88
83
  const number = parseInt(squareId[1]);
89
-
90
84
  const col = BOARD_LETTERS.indexOf(letter);
91
85
  if (col === -1) {
92
86
  throw new ValidationError(
@@ -95,7 +89,6 @@ export class CoordinateService {
95
89
  squareId
96
90
  );
97
91
  }
98
-
99
92
  if (number < 1 || number > 8) {
100
93
  throw new ValidationError(
101
94
  ERROR_MESSAGES.invalid_square + squareId,
@@ -103,9 +96,7 @@ export class CoordinateService {
103
96
  squareId
104
97
  );
105
98
  }
106
-
107
99
  let row, boardCol;
108
-
109
100
  if (this.isWhiteOriented()) {
110
101
  row = 8 - number;
111
102
  boardCol = col;
@@ -113,12 +104,11 @@ export class CoordinateService {
113
104
  row = number - 1;
114
105
  boardCol = 7 - col;
115
106
  }
116
-
117
107
  return [row, boardCol];
118
108
  }
119
109
 
120
110
  /**
121
- * Converts pixel coordinates to square ID
111
+ * Convert pixel coordinates to square ID
122
112
  * @param {number} x - X coordinate in pixels
123
113
  * @param {number} y - Y coordinate in pixels
124
114
  * @param {HTMLElement} boardElement - Board DOM element
@@ -126,21 +116,15 @@ export class CoordinateService {
126
116
  */
127
117
  pixelToSquareID(x, y, boardElement) {
128
118
  if (!boardElement) return null;
129
-
130
119
  const rect = boardElement.getBoundingClientRect();
131
120
  const { width, height } = rect;
132
-
133
- // Check if coordinates are within board bounds
134
121
  if (x < 0 || x >= width || y < 0 || y >= height) {
135
122
  return null;
136
123
  }
137
-
138
124
  const squareWidth = width / 8;
139
125
  const squareHeight = height / 8;
140
-
141
126
  const col = Math.floor(x / squareWidth);
142
127
  const row = Math.floor(y / squareHeight);
143
-
144
128
  try {
145
129
  return this.getSquareID(row, col);
146
130
  } catch (error) {
@@ -149,25 +133,21 @@ export class CoordinateService {
149
133
  }
150
134
 
151
135
  /**
152
- * Converts square ID to pixel coordinates
136
+ * Convert square ID to pixel coordinates
153
137
  * @param {string} squareId - Square ID (e.g., 'e4')
154
138
  * @param {HTMLElement} boardElement - Board DOM element
155
139
  * @returns {Object|null} Pixel coordinates {x, y} or null if invalid
156
140
  */
157
141
  squareIDToPixel(squareId, boardElement) {
158
142
  if (!boardElement) return null;
159
-
160
143
  try {
161
144
  const [row, col] = this.getCoordinatesFromSquareID(squareId);
162
145
  const rect = boardElement.getBoundingClientRect();
163
146
  const { width, height } = rect;
164
-
165
147
  const squareWidth = width / 8;
166
148
  const squareHeight = height / 8;
167
-
168
149
  const x = col * squareWidth;
169
150
  const y = row * squareHeight;
170
-
171
151
  return { x, y };
172
152
  } catch (error) {
173
153
  return null;
@@ -175,7 +155,7 @@ export class CoordinateService {
175
155
  }
176
156
 
177
157
  /**
178
- * Gets the center pixel coordinates of a square
158
+ * Get the center pixel coordinates of a square
179
159
  * @param {string} squareId - Square ID (e.g., 'e4')
180
160
  * @param {HTMLElement} boardElement - Board DOM element
181
161
  * @returns {Object|null} Center coordinates {x, y} or null if invalid
@@ -183,11 +163,9 @@ export class CoordinateService {
183
163
  getSquareCenter(squareId, boardElement) {
184
164
  const coords = this.squareIDToPixel(squareId, boardElement);
185
165
  if (!coords) return null;
186
-
187
166
  const rect = boardElement.getBoundingClientRect();
188
167
  const squareWidth = rect.width / 8;
189
168
  const squareHeight = rect.height / 8;
190
-
191
169
  return {
192
170
  x: coords.x + squareWidth / 2,
193
171
  y: coords.y + squareHeight / 2
@@ -195,7 +173,7 @@ export class CoordinateService {
195
173
  }
196
174
 
197
175
  /**
198
- * Calculates the distance between two squares
176
+ * Calculate the distance between two squares
199
177
  * @param {string} fromSquare - Source square ID
200
178
  * @param {string} toSquare - Target square ID
201
179
  * @returns {number} Distance between squares
@@ -204,10 +182,8 @@ export class CoordinateService {
204
182
  try {
205
183
  const [fromRow, fromCol] = this.getCoordinatesFromSquareID(fromSquare);
206
184
  const [toRow, toCol] = this.getCoordinatesFromSquareID(toSquare);
207
-
208
185
  const rowDiff = Math.abs(toRow - fromRow);
209
186
  const colDiff = Math.abs(toCol - fromCol);
210
-
211
187
  return Math.sqrt(rowDiff * rowDiff + colDiff * colDiff);
212
188
  } catch (error) {
213
189
  return 0;
@@ -215,7 +191,7 @@ export class CoordinateService {
215
191
  }
216
192
 
217
193
  /**
218
- * Checks if the board is oriented from white's perspective
194
+ * Check if the board is oriented from white's perspective
219
195
  * @returns {boolean} True if white-oriented
220
196
  */
221
197
  isWhiteOriented() {
@@ -223,7 +199,7 @@ export class CoordinateService {
223
199
  }
224
200
 
225
201
  /**
226
- * Checks if the board is oriented from black's perspective
202
+ * Check if the board is oriented from black's perspective
227
203
  * @returns {boolean} True if black-oriented
228
204
  */
229
205
  isBlackOriented() {
@@ -231,31 +207,31 @@ export class CoordinateService {
231
207
  }
232
208
 
233
209
  /**
234
- * Flips the board orientation
210
+ * Flip the board orientation
235
211
  */
236
212
  flipOrientation() {
237
213
  this.config.orientation = this.isWhiteOriented() ? 'b' : 'w';
238
214
  }
239
215
 
240
216
  /**
241
- * Sets the board orientation
242
- * @param {string} orientation - 'w' for white, 'b' for black
243
- * @throws {ValidationError} When orientation is invalid
217
+ * Set the board orientation
218
+ * @param {string} orientation - 'w', 'b', 'white', or 'black'
219
+ * @throws {ValidationError} If orientation is invalid
244
220
  */
245
221
  setOrientation(orientation) {
246
- if (orientation !== 'w' && orientation !== 'b') {
222
+ const normalized = orientation === 'white' ? 'w' : orientation === 'black' ? 'b' : orientation;
223
+ if (normalized !== 'w' && normalized !== 'b') {
247
224
  throw new ValidationError(
248
225
  ERROR_MESSAGES.invalid_orientation + orientation,
249
226
  'orientation',
250
227
  orientation
251
228
  );
252
229
  }
253
-
254
- this.config.orientation = orientation;
230
+ this.config.orientation = normalized;
255
231
  }
256
232
 
257
233
  /**
258
- * Gets the current orientation
234
+ * Get the current orientation
259
235
  * @returns {string} Current orientation ('w' or 'b')
260
236
  */
261
237
  getOrientation() {
@@ -263,50 +239,21 @@ export class CoordinateService {
263
239
  }
264
240
 
265
241
  /**
266
- * Sets the orientation
267
- * @param {string} orientation - New orientation ('w', 'b', 'white', 'black')
268
- */
269
- setOrientation(orientation) {
270
- // Normalize orientation
271
- const normalizedOrientation = orientation === 'white' ? 'w' :
272
- orientation === 'black' ? 'b' : orientation;
273
-
274
- if (normalizedOrientation !== 'w' && normalizedOrientation !== 'b') {
275
- throw new ValidationError(
276
- ERROR_MESSAGES.invalid_orientation + orientation,
277
- 'orientation',
278
- orientation
279
- );
280
- }
281
-
282
- this.config.orientation = normalizedOrientation;
283
- }
284
-
285
- /**
286
- * Flips the board orientation
287
- */
288
- flipOrientation() {
289
- this.config.orientation = this.isWhiteOriented() ? 'b' : 'w';
290
- }
291
-
292
- /**
293
- * Gets all square IDs in order
242
+ * Get all square IDs in order
294
243
  * @returns {Array<string>} Array of all square IDs
295
244
  */
296
245
  getAllSquareIDs() {
297
246
  const squares = [];
298
-
299
247
  for (let row = 0; row < 8; row++) {
300
248
  for (let col = 0; col < 8; col++) {
301
249
  squares.push(this.getSquareID(row, col));
302
250
  }
303
251
  }
304
-
305
252
  return squares;
306
253
  }
307
254
 
308
255
  /**
309
- * Gets squares in a specific rank (row)
256
+ * Get all squares in a specific rank (row)
310
257
  * @param {number} rank - Rank number (1-8)
311
258
  * @returns {Array<string>} Array of square IDs in the rank
312
259
  */
@@ -318,19 +265,16 @@ export class CoordinateService {
318
265
  rank
319
266
  );
320
267
  }
321
-
322
268
  const squares = [];
323
-
324
269
  for (let col = 0; col < 8; col++) {
325
270
  const row = this.isWhiteOriented() ? 8 - rank : rank - 1;
326
271
  squares.push(this.getSquareID(row, col));
327
272
  }
328
-
329
273
  return squares;
330
274
  }
331
275
 
332
276
  /**
333
- * Gets squares in a specific file (column)
277
+ * Get all squares in a specific file (column)
334
278
  * @param {string} file - File letter (a-h)
335
279
  * @returns {Array<string>} Array of square IDs in the file
336
280
  */
@@ -343,13 +287,10 @@ export class CoordinateService {
343
287
  file
344
288
  );
345
289
  }
346
-
347
290
  const squares = [];
348
-
349
291
  for (let row = 0; row < 8; row++) {
350
292
  squares.push(this.getSquareID(row, col));
351
293
  }
352
-
353
294
  return squares;
354
295
  }
355
296
  }
@@ -9,6 +9,7 @@ import { DragOptimizations } from '../utils/cross-browser.js';
9
9
  import { ValidationError } from '../errors/ChessboardError.js';
10
10
  import Move from '../components/Move.js';
11
11
  import Piece from '../components/Piece.js';
12
+ import { ListenerManager } from '../utils/listenerManager.js';
12
13
 
13
14
  /**
14
15
  * Service responsible for event handling and user interactions
@@ -35,8 +36,8 @@ export class EventService {
35
36
  this.promoting = false;
36
37
  this.isAnimating = false;
37
38
 
38
- // Event listeners storage for cleanup
39
- this.eventListeners = new Map();
39
+ // Listener manager per gestione centralizzata
40
+ this.listenerManager = new ListenerManager();
40
41
  }
41
42
 
42
43
  /**
@@ -46,8 +47,8 @@ export class EventService {
46
47
  * @param {Function} onPieceLeave - Callback for piece leave
47
48
  */
48
49
  addListeners(onSquareClick, onPieceHover, onPieceLeave) {
49
- // Remove existing listeners to avoid duplicates
50
- this.removeListeners();
50
+ // Rimuovi tutti i listener esistenti
51
+ this.listenerManager.removeAll();
51
52
 
52
53
  const squares = this.boardService.getAllSquares();
53
54
 
@@ -65,8 +66,6 @@ export class EventService {
65
66
  * @param {Function} onPieceLeave - Leave callback
66
67
  */
67
68
  _addSquareListeners(square, onSquareClick, onPieceHover, onPieceLeave) {
68
- const listeners = [];
69
-
70
69
  // Throttled hover handlers for performance
71
70
  const throttledHover = rafThrottle((e) => {
72
71
  if (!this.clicked && this.config.hints) {
@@ -136,26 +135,13 @@ export class EventService {
136
135
  }
137
136
  };
138
137
 
139
- // Add listeners
140
- square.element.addEventListener('mouseover', throttledHover);
141
- square.element.addEventListener('mouseout', throttledLeave);
142
- square.element.addEventListener('click', handleClick);
143
- // Touch: separa tap e drag
144
- square.element.addEventListener('touchstart', handleTouchStart);
145
- square.element.addEventListener('touchmove', handleTouchMove);
146
- square.element.addEventListener('touchend', handleTouchEnd);
147
-
148
- // Store listeners for cleanup
149
- listeners.push(
150
- { element: square.element, type: 'mouseover', handler: throttledHover },
151
- { element: square.element, type: 'mouseout', handler: throttledLeave },
152
- { element: square.element, type: 'click', handler: handleClick },
153
- { element: square.element, type: 'touchstart', handler: handleTouchStart },
154
- { element: square.element, type: 'touchmove', handler: handleTouchMove },
155
- { element: square.element, type: 'touchend', handler: handleTouchEnd }
156
- );
157
-
158
- this.eventListeners.set(square.id, listeners);
138
+ // Usa ListenerManager per aggiungere tutti i listener
139
+ this.listenerManager.add(square.element, 'mouseover', throttledHover);
140
+ this.listenerManager.add(square.element, 'mouseout', throttledLeave);
141
+ this.listenerManager.add(square.element, 'click', handleClick);
142
+ this.listenerManager.add(square.element, 'touchstart', handleTouchStart);
143
+ this.listenerManager.add(square.element, 'touchmove', handleTouchMove);
144
+ this.listenerManager.add(square.element, 'touchend', handleTouchEnd);
159
145
  }
160
146
 
161
147
  /**
@@ -172,8 +158,6 @@ export class EventService {
172
158
  */
173
159
  createDragFunction(square, piece, onDragStart, onDragMove, onDrop, onSnapback, onMove, onRemove) {
174
160
  return (event) => {
175
- event.preventDefault();
176
-
177
161
  if (!this.config.draggable || !piece || this.isAnimating) {
178
162
  return;
179
163
  }
@@ -183,7 +167,6 @@ export class EventService {
183
167
  let from = originalFrom;
184
168
  let to = square;
185
169
  let previousHighlight = null;
186
-
187
170
  const img = piece.element;
188
171
 
189
172
  if (!this.moveService.canMove(from)) {
@@ -194,6 +177,15 @@ export class EventService {
194
177
  const isTouch = event.type && event.type.startsWith('touch');
195
178
  const startX = event.clientX || (event.touches && event.touches[0]?.clientX) || 0;
196
179
  const startY = event.clientY || (event.touches && event.touches[0]?.clientY) || 0;
180
+ let dragStarted = false;
181
+
182
+ // --- Calculate offset between cursor/finger and piece top-left at drag start ---
183
+ const pieceRect = img.getBoundingClientRect();
184
+ const boardElement = this.boardService.element;
185
+ const boardRect = boardElement.getBoundingClientRect();
186
+ // Offset from cursor/finger to top-left of piece
187
+ const offsetX = startX - pieceRect.left;
188
+ const offsetY = startY - pieceRect.top;
197
189
 
198
190
  // --- Touch scroll lock helper ---
199
191
  const addScrollLock = () => {
@@ -205,10 +197,6 @@ export class EventService {
205
197
 
206
198
  // --- MOVE HANDLER (mouse + touch unified) ---
207
199
  const moveAt = (event) => {
208
- const boardElement = this.boardService.element;
209
- const squareSize = boardElement.offsetWidth / 8;
210
-
211
- // Get mouse/touch coordinates
212
200
  let clientX, clientY;
213
201
  if (event.touches && event.touches[0]) {
214
202
  clientX = event.touches[0].clientX;
@@ -217,77 +205,55 @@ export class EventService {
217
205
  clientX = event.clientX;
218
206
  clientY = event.clientY;
219
207
  }
220
-
221
- // Calculate position relative to board
222
- const boardRect = boardElement.getBoundingClientRect();
223
- const x = clientX - boardRect.left - (squareSize / 2);
224
- const y = clientY - boardRect.top - (squareSize / 2);
225
-
208
+ // Position the piece so the cursor/finger stays at the same relative point
209
+ const x = clientX - boardRect.left - offsetX;
210
+ const y = clientY - boardRect.top - offsetY;
226
211
  img.style.left = x + 'px';
227
212
  img.style.top = y + 'px';
228
-
229
213
  return true;
230
214
  };
231
215
 
232
216
  // --- DRAG MOVE (mouse + touch) ---
217
+ const moveThreshold = 5; // px
233
218
  const onPointerMove = (event) => {
234
- // For touch, only handle the first finger
235
219
  if (event.touches && event.touches.length > 1) return;
236
220
  if (event.touches && !event.touches[0]) return;
237
- if (event.type && event.type.startsWith('touch')) event.preventDefault();
238
-
239
221
  const currentX = event.clientX || (event.touches && event.touches[0]?.clientX) || 0;
240
222
  const currentY = event.clientY || (event.touches && event.touches[0]?.clientY) || 0;
241
223
  const deltaX = Math.abs(currentX - startX);
242
224
  const deltaY = Math.abs(currentY - startY);
243
-
244
- // Start dragging if mouse/touch moved enough
245
- if (!isDragging && (deltaX > 3 || deltaY > 3)) {
225
+ if (!dragStarted && (deltaX > moveThreshold || deltaY > moveThreshold)) {
226
+ dragStarted = true;
246
227
  isDragging = true;
247
- // Inizio drag: blocca update board
228
+ if (isTouch) addScrollLock();
248
229
  if (this.chessboard) this.chessboard._isDragging = true;
249
-
250
- // Mostra hint all'inizio del drag se attivi
251
230
  if (this.config.hints && typeof this.chessboard._boundOnPieceHover === 'function') {
252
231
  this.chessboard._boundOnPieceHover(from);
253
232
  }
254
-
255
- // Set up drag state
256
233
  if (!this.config.clickable) {
257
234
  this.clicked = null;
258
235
  this.clicked = from;
259
236
  } else if (!this.clicked) {
260
237
  this.clicked = from;
261
238
  }
262
-
263
- // Visual feedback
264
239
  if (this.config.clickable) {
265
240
  from.select();
266
241
  }
267
-
268
- // Prepare piece for dragging
269
242
  img.style.position = 'absolute';
270
243
  img.style.zIndex = '100';
271
244
  img.classList.add('dragging');
272
-
273
245
  DragOptimizations.enableForDrag(img);
274
-
275
- // Lock scroll for touch
276
- if (isTouch) addScrollLock();
277
-
278
- // Call drag start callback
279
246
  if (!onDragStart(square, piece)) {
280
247
  return;
281
248
  }
282
249
  }
283
-
284
- if (!isDragging) return;
285
-
250
+ // Only block scroll and call preventDefault after drag has started
251
+ if (dragStarted && isTouch && event.cancelable) {
252
+ event.preventDefault();
253
+ }
254
+ if (!dragStarted) return;
286
255
  if (!moveAt(event)) return;
287
-
288
256
  // Update target square
289
- const boardElement = this.boardService.element;
290
- const boardRect = boardElement.getBoundingClientRect();
291
257
  let clientX, clientY;
292
258
  if (event.touches && event.touches[0]) {
293
259
  clientX = event.touches[0].clientX;
@@ -298,22 +264,16 @@ export class EventService {
298
264
  }
299
265
  const x = clientX - boardRect.left;
300
266
  const y = clientY - boardRect.top;
301
-
302
267
  let newTo = null;
303
268
  if (x >= 0 && x <= boardRect.width && y >= 0 && y <= boardRect.height) {
304
269
  const squareId = this.coordinateService.pixelToSquareID(x, y, boardElement);
305
270
  newTo = squareId ? this.boardService.getSquare(squareId) : null;
306
271
  }
307
-
308
272
  to = newTo;
309
273
  onDragMove(from, to, piece);
310
-
311
- // Update visual feedback
312
- if (to !== previousHighlight) {
313
- to?.highlight();
314
- previousHighlight?.dehighlight();
315
- previousHighlight = to;
316
- }
274
+ if (to) to.highlight();
275
+ if (previousHighlight && previousHighlight !== to) previousHighlight.dehighlight();
276
+ previousHighlight = to;
317
277
  };
318
278
 
319
279
  // --- DRAG END (mouse + touch) ---
@@ -324,26 +284,19 @@ export class EventService {
324
284
  document.removeEventListener('touchmove', onPointerMove);
325
285
  window.removeEventListener('touchend', onPointerUp);
326
286
  if (isTouch) removeScrollLock();
327
- // Fine drag: sblocca update board
328
287
  if (this.chessboard) this.chessboard._isDragging = false;
329
-
330
- // Rimuovi hint alla fine del drag se attivi
331
288
  if (this.config.hints && typeof this.chessboard._boundOnPieceLeave === 'function') {
332
289
  this.chessboard._boundOnPieceLeave(from);
333
290
  }
334
-
335
- if (!isDragging) {
291
+ if (!dragStarted) {
336
292
  return;
337
293
  }
338
-
339
294
  img.style.zIndex = '20';
340
295
  img.classList.remove('dragging');
341
296
  img.style.willChange = 'auto';
342
-
343
297
  // Handle drop
344
298
  const dropResult = onDrop(originalFrom, to, piece);
345
299
  const isTrashDrop = !to && (this.config.dropOffBoard === 'trash' || dropResult === 'trash');
346
-
347
300
  if (isTrashDrop) {
348
301
  this._handleTrashDrop(originalFrom, onRemove);
349
302
  } else if (!to) {
@@ -357,16 +310,12 @@ export class EventService {
357
310
  }
358
311
  };
359
312
 
360
- // --- Attach listeners (mouse + touch) ---
313
+ // Attach listeners (mouse + touch)
361
314
  window.addEventListener('mouseup', onPointerUp, { once: true });
362
315
  document.addEventListener('mousemove', onPointerMove);
363
316
  img.addEventListener('mouseup', onPointerUp, { once: true });
364
- // Touch events
365
317
  window.addEventListener('touchend', onPointerUp, { once: true });
366
318
  document.addEventListener('touchmove', onPointerMove, { passive: false });
367
-
368
- // Per robustezza: se il drag parte da touch, blocca subito lo scroll
369
- if (isTouch) addScrollLock();
370
319
  };
371
320
  }
372
321
 
@@ -859,33 +808,21 @@ export class EventService {
859
808
  * Removes all existing event listeners
860
809
  */
861
810
  removeListeners() {
862
- this.eventListeners.forEach((listeners, squareId) => {
863
- listeners.forEach(({ element, type, handler }) => {
864
- element.removeEventListener(type, handler);
865
- });
866
- });
867
-
868
- this.eventListeners.clear();
811
+ this.listenerManager.removeAll();
869
812
  }
870
813
 
871
814
  /**
872
815
  * Removes all event listeners
873
816
  */
874
817
  removeAllListeners() {
875
- this.eventListeners.forEach((listeners, squareId) => {
876
- listeners.forEach(({ element, type, handler }) => {
877
- element.removeEventListener(type, handler);
878
- });
879
- });
880
-
881
- this.eventListeners.clear();
818
+ this.listenerManager.removeAll();
882
819
  }
883
820
 
884
821
  /**
885
822
  * Cleans up resources
886
823
  */
887
824
  destroy() {
888
- this.removeAllListeners();
825
+ this.listenerManager.destroy();
889
826
  this.clicked = null;
890
827
  this.promoting = false;
891
828
  this.isAnimating = false;