@h4md1/visual-image-tool 0.2.1 → 0.2.3

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,38 +1,39 @@
1
1
  /**
2
- * ImageTool - Un outil léger pour définir des points focaux et zones de recadrage sur des images
2
+ * ImageTool - A lightweight tool for defining focus points and crop zones on images
3
3
  * @module visual-image-tool
4
4
  */
5
5
 
6
6
  class VisualImageTool {
7
7
  /**
8
- * Crée une instance de l'outil d'image
9
- * @param {Object} options - Options de configuration
10
- * @param {HTMLElement|string} options.imageElement - Élément image ou sélecteur CSS
11
- * @param {Object} [options.focusPoint] - Configuration du point focal
12
- * @param {boolean} [options.focusPoint.enabled=true] - Activer la fonctionnalité de point focal
13
- * @param {Object} [options.focusPoint.style] - Styles personnalisés pour le marqueur de point focal
14
- * @param {Object} [options.cropZone] - Configuration de la zone de recadrage
15
- * @param {boolean} [options.cropZone.enabled=true] - Activer la fonctionnalité de zone de recadrage
16
- * @param {Object} [options.cropZone.style] - Styles personnalisés pour l'overlay de recadrage
17
- * @param {Function} [options.onChange] - Callback appelé lors des changements
8
+ * Creates an instance of the image tool.
9
+ * The focus point is initialized to `{x:0, y:0}` in `this.state`. When `toggleFocusPoint(true)` is called for the first time, if the focus point is still `{x:0, y:0}`, it will be automatically centered on the image.
10
+ * @param {Object} options - Configuration options
11
+ * @param {HTMLElement|string} options.imageElement - Image element or CSS selector
12
+ * @param {Object} [options.focusPoint] - Focus point configuration
13
+ * @param {boolean} [options.focusPoint.enabled=true] - Enable focus point functionality
14
+ * @param {Object} [options.focusPoint.style] - Custom styles for the focus point marker
15
+ * @param {Object} [options.cropZone] - Crop zone configuration
16
+ * @param {boolean} [options.cropZone.enabled=true] - Enable crop zone functionality
17
+ * @param {Object} [options.cropZone.style] - Custom styles for the crop overlay
18
+ * @param {Function} [options.onChange] - Callback called on changes
18
19
  */
19
20
  constructor(options) {
20
- // Valider les options
21
+ // Validate options
21
22
  if (!options || !options.imageElement) {
22
- throw new Error("L'élément image est requis");
23
+ throw new Error("Image element is required");
23
24
  }
24
25
 
25
- // Initialiser les propriétés
26
+ // Initialize properties
26
27
  this.imageElement =
27
28
  typeof options.imageElement === "string"
28
29
  ? document.querySelector(options.imageElement)
29
30
  : options.imageElement;
30
31
 
31
32
  if (!this.imageElement || this.imageElement.tagName !== "IMG") {
32
- throw new Error("Élément image invalide");
33
+ throw new Error("Invalid image element");
33
34
  }
34
35
 
35
- // Options par défaut
36
+ // Default options
36
37
  this.options = {
37
38
  focusPoint: {
38
39
  enabled: true,
@@ -64,7 +65,7 @@ class VisualImageTool {
64
65
  debug: options.debug || false,
65
66
  };
66
67
 
67
- // État interne
68
+ // Internal state
68
69
  this.state = {
69
70
  focusMarker: null,
70
71
  cropOverlay: null,
@@ -80,7 +81,21 @@ class VisualImageTool {
80
81
  scaleY: 1,
81
82
  };
82
83
 
83
- // Variables pour le suivi des interactions
84
+ // Handlers are bound once here: addEventListener and removeEventListener
85
+ // must be handed the same function reference for removal to work.
86
+ this._boundUpdateScaling = this._updateScaling.bind(this);
87
+ this._boundHandleMouseUp = this._handleMouseUp.bind(this);
88
+ this._boundHandleMouseMove = this._handleMouseMove.bind(this);
89
+ this._boundFocusMarkerMouseDown =
90
+ this._handleFocusMarkerMouseDown.bind(this);
91
+ this._boundCropOverlayMouseDown =
92
+ this._handleCropOverlayMouseDown.bind(this);
93
+ this._boundCropHandleMouseDown = this._handleCropHandleMouseDown.bind(this);
94
+
95
+ // Resize handles, tracked so their listeners can be removed on destroy
96
+ this.cropHandles = [];
97
+
98
+ // Variables for tracking interactions
84
99
  this.interaction = {
85
100
  focusDragging: false,
86
101
  focusDragOffsetX: 0,
@@ -96,22 +111,25 @@ class VisualImageTool {
96
111
  startMouseY: 0,
97
112
  };
98
113
 
99
- // Initialiser l'outil
114
+ this.spinnerElement = null;
115
+ this.initialLoadComplete = false;
116
+
117
+ // Initialize the tool
100
118
  this._init();
101
119
  }
102
120
 
103
121
  /**
104
- * Initialise l'outil d'image
122
+ * Initializes the image tool
105
123
  * @private
106
124
  */
107
125
  _init() {
108
- // Préparer le conteneur parent
126
+ // Prepare the parent container
109
127
  this._prepareContainer();
110
128
 
111
- // Initialiser les dimensions
129
+ // Initialize dimensions
112
130
  this._updateScaling();
113
131
 
114
- // Créer les éléments d'interface si activés
132
+ // Create UI elements if enabled
115
133
  if (this.options.focusPoint.enabled) {
116
134
  this._createFocusMarker();
117
135
  }
@@ -120,34 +138,82 @@ class VisualImageTool {
120
138
  this._createCropOverlay();
121
139
  }
122
140
 
123
- // Ajouter les écouteurs d'événements
141
+ // Add event listeners
124
142
  this._setupEventListeners();
125
143
  }
126
144
 
127
145
  /**
128
- * Prépare le conteneur parent de l'image
146
+ * Prepares the image's parent container
129
147
  * @private
130
148
  */
131
149
  _prepareContainer() {
132
- // S'assurer que le parent est positionné
150
+ // Ensure the parent is positioned
133
151
  if (this.imageElement.parentNode) {
134
152
  this.imageElement.parentNode.style.position = "relative";
153
+
154
+ // Create spinner element
155
+ this.spinnerElement = document.createElement("div");
156
+ this.spinnerElement.style.position = "absolute";
157
+ this.spinnerElement.style.top = "50%";
158
+ this.spinnerElement.style.left = "50%";
159
+ this.spinnerElement.style.transform = "translate(-50%, -50%)";
160
+ this.spinnerElement.style.width = "40px";
161
+ this.spinnerElement.style.height = "40px";
162
+ this.spinnerElement.style.border = "4px solid #f3f3f3"; // Light grey
163
+ this.spinnerElement.style.borderTop = "4px solid #3498db"; // Blue
164
+ this.spinnerElement.style.borderRadius = "50%";
165
+ this.spinnerElement.style.boxSizing = "border-box";
166
+ this.spinnerElement.style.zIndex = "1000"; // Ensure it's above other elements
167
+ this.spinnerElement.style.display = "block"; // Initially visible
168
+ this.spinnerElement.style.animation = "spin 1s linear infinite";
169
+
170
+ this.imageElement.parentNode.appendChild(this.spinnerElement);
171
+
172
+ // Add CSS keyframes for spinner animation
173
+ if (!document.getElementById("visual-image-tool-spinner-styles")) {
174
+ const styleElement = document.createElement("style");
175
+ styleElement.id = "visual-image-tool-spinner-styles";
176
+ styleElement.innerHTML = `
177
+ @keyframes spin {
178
+ 0% { transform: translate(-50%, -50%) rotate(0deg); }
179
+ 100% { transform: translate(-50%, -50%) rotate(360deg); }
180
+ }
181
+ `;
182
+ document.head.appendChild(styleElement);
183
+ }
135
184
  }
136
185
  }
137
186
 
138
187
  /**
139
- * Met à jour les facteurs d'échelle
188
+ * Updates scaling factors
140
189
  * @private
141
190
  */
142
191
  _updateScaling() {
143
192
  this.state.originalWidth = this.imageElement.naturalWidth || 1;
144
193
  this.state.originalHeight = this.imageElement.naturalHeight || 1;
194
+
195
+ // Hide spinner once image is loaded and dimensions are available
196
+ if (
197
+ !this.initialLoadComplete &&
198
+ this.spinnerElement &&
199
+ this.state.originalWidth > 1 &&
200
+ this.state.originalHeight > 1
201
+ ) {
202
+ this.spinnerElement.style.display = "none";
203
+ this.initialLoadComplete = true;
204
+ }
145
205
  this.state.displayWidth = this.imageElement.offsetWidth;
146
206
  this.state.displayHeight = this.imageElement.offsetHeight;
147
207
  this.state.scaleX = this.state.displayWidth / this.state.originalWidth;
148
208
  this.state.scaleY = this.state.displayHeight / this.state.originalHeight;
149
209
 
150
- // Repositionner les éléments si actifs
210
+ // Update spinner position during initial load if still visible
211
+ if (!this.initialLoadComplete && this.spinnerElement) {
212
+ this.spinnerElement.style.top = `${this.state.displayHeight / 2}px`;
213
+ this.spinnerElement.style.left = `${this.state.displayWidth / 2}px`;
214
+ }
215
+
216
+ // Reposition elements if active
151
217
  if (this.state.focusActive) {
152
218
  this._updateFocusMarkerPosition();
153
219
  }
@@ -158,11 +224,11 @@ class VisualImageTool {
158
224
  }
159
225
 
160
226
  /**
161
- * Convertit des coordonnées d'affichage en coordonnées originales
227
+ * Converts display coordinates to original coordinates
162
228
  * @private
163
- * @param {number} scaledX - Coordonnée X à l'échelle d'affichage
164
- * @param {number} scaledY - Coordonnée Y à l'échelle d'affichage
165
- * @returns {Object} Coordonnées originales
229
+ * @param {number} scaledX - X-coordinate at display scale
230
+ * @param {number} scaledY - Y-coordinate at display scale
231
+ * @returns {Object} Original coordinates {x, y}
166
232
  */
167
233
  _toOriginalCoords(scaledX, scaledY) {
168
234
  return {
@@ -172,11 +238,11 @@ class VisualImageTool {
172
238
  }
173
239
 
174
240
  /**
175
- * Convertit des coordonnées originales en coordonnées d'affichage
241
+ * Converts original coordinates to display coordinates
176
242
  * @private
177
- * @param {number} originalX - Coordonnée X originale
178
- * @param {number} originalY - Coordonnée Y originale
179
- * @returns {Object} Coordonnées à l'échelle d'affichage
243
+ * @param {number} originalX - Original X-coordinate
244
+ * @param {number} originalY - Original Y-coordinate
245
+ * @returns {Object} Display scale coordinates {x, y}
180
246
  */
181
247
  _toScaledCoords(originalX, originalY) {
182
248
  return {
@@ -186,7 +252,7 @@ class VisualImageTool {
186
252
  }
187
253
 
188
254
  /**
189
- * Crée le marqueur de point focal
255
+ * Creates the focus point marker
190
256
  * @private
191
257
  */
192
258
  _createFocusMarker() {
@@ -209,23 +275,20 @@ class VisualImageTool {
209
275
  this.imageElement.parentNode.appendChild(marker);
210
276
  this.state.focusMarker = marker;
211
277
 
212
- // Ajouter les écouteurs d'événements au marqueur
213
- marker.addEventListener(
214
- "mousedown",
215
- this._handleFocusMarkerMouseDown.bind(this),
216
- );
278
+ // Add event listeners to the marker
279
+ marker.addEventListener("mousedown", this._boundFocusMarkerMouseDown);
217
280
  }
218
281
 
219
282
  /**
220
- * Gère l'événement mousedown sur le marqueur de point focal
283
+ * Handles mousedown event on the focus point marker
221
284
  * @private
222
- * @param {MouseEvent} e - Événement mousedown
285
+ * @param {MouseEvent} e - Mousedown event
223
286
  */
224
287
  _handleFocusMarkerMouseDown(e) {
225
288
  this.interaction.focusDragging = true;
226
289
  this.state.focusMarker.style.cursor = "grabbing";
227
290
 
228
- // Calculer le décalage par rapport au centre du marqueur
291
+ // Calculate offset from marker center
229
292
  const rect = this.state.focusMarker.getBoundingClientRect();
230
293
  this.interaction.focusDragOffsetX =
231
294
  e.clientX - (rect.left + rect.width / 2);
@@ -236,7 +299,7 @@ class VisualImageTool {
236
299
  }
237
300
 
238
301
  /**
239
- * Crée l'overlay de zone de recadrage
302
+ * Creates the crop zone overlay
240
303
  * @private
241
304
  */
242
305
  _createCropOverlay() {
@@ -251,7 +314,7 @@ class VisualImageTool {
251
314
  overlay.style.display = "none";
252
315
  overlay.style.zIndex = "998";
253
316
 
254
- // Ajouter les poignées de redimensionnement
317
+ // Add resize handles
255
318
  const handles = ["tl", "tm", "tr", "ml", "mr", "bl", "bm", "br"];
256
319
  for (const handleType of handles) {
257
320
  const handle = document.createElement("div");
@@ -265,7 +328,7 @@ class VisualImageTool {
265
328
  handle.style.boxSizing = "border-box";
266
329
  handle.dataset.handle = handleType;
267
330
 
268
- // Positionner la poignée
331
+ // Position the handle
269
332
  switch (handleType) {
270
333
  case "tl": // Top-left
271
334
  handle.style.top = "-7px";
@@ -313,35 +376,32 @@ class VisualImageTool {
313
376
  break;
314
377
  }
315
378
 
316
- // Ajouter l'écouteur d'événement
317
- handle.addEventListener("mousedown", (e) =>
318
- this._handleCropHandleMouseDown(e, handleType),
319
- );
379
+ // Add event listener
380
+ handle.addEventListener("mousedown", this._boundCropHandleMouseDown);
320
381
 
321
382
  overlay.appendChild(handle);
383
+ this.cropHandles.push(handle);
322
384
  }
323
385
 
324
- // Ajouter l'écouteur pour le déplacement de l'overlay
325
- overlay.addEventListener(
326
- "mousedown",
327
- this._handleCropOverlayMouseDown.bind(this),
328
- );
386
+ // Add listener for overlay dragging
387
+ overlay.addEventListener("mousedown", this._boundCropOverlayMouseDown);
329
388
 
330
389
  this.imageElement.parentNode.appendChild(overlay);
331
390
  this.state.cropOverlay = overlay;
332
391
  }
333
392
 
334
393
  /**
335
- * Gère l'événement mousedown sur une poignée de redimensionnement
394
+ * Handles mousedown event on a resize handle
336
395
  * @private
337
- * @param {MouseEvent} e - Événement mousedown
338
- * @param {string} handleType - Type de poignée
396
+ * @param {MouseEvent} e - Mousedown event
339
397
  */
340
- _handleCropHandleMouseDown(e, handleType) {
398
+ _handleCropHandleMouseDown(e) {
399
+ const handleType = e.currentTarget.dataset.handle;
400
+
341
401
  this.interaction.cropResizing = true;
342
402
  this.interaction.activeHandle = handleType;
343
403
 
344
- // Enregistrer les dimensions et position initiales
404
+ // Record initial dimensions and position
345
405
  this.interaction.startX =
346
406
  Number.parseInt(this.state.cropOverlay.style.left, 10) || 0;
347
407
  this.interaction.startY =
@@ -356,18 +416,18 @@ class VisualImageTool {
356
416
  }
357
417
 
358
418
  /**
359
- * Gère l'événement mousedown sur l'overlay de recadrage
419
+ * Handles mousedown event on the crop overlay
360
420
  * @private
361
- * @param {MouseEvent} e - Événement mousedown
421
+ * @param {MouseEvent} e - Mousedown event
362
422
  */
363
423
  _handleCropOverlayMouseDown(e) {
364
- // Ignorer si on clique sur une poignée
424
+ // Ignore if clicking on a handle
365
425
  if (e.target !== this.state.cropOverlay) return;
366
426
 
367
427
  this.interaction.cropDragging = true;
368
428
  this.state.cropOverlay.style.cursor = "grabbing";
369
429
 
370
- // Enregistrer la position initiale
430
+ // Record initial position
371
431
  this.interaction.startX =
372
432
  Number.parseInt(this.state.cropOverlay.style.left, 10) || 0;
373
433
  this.interaction.startY =
@@ -379,28 +439,25 @@ class VisualImageTool {
379
439
  }
380
440
 
381
441
  /**
382
- * Configure les écouteurs d'événements globaux
442
+ * Sets up global event listeners
383
443
  * @private
384
444
  */
385
445
  _setupEventListeners() {
386
- // Écouteur pour le redimensionnement de la fenêtre
387
- window.addEventListener("resize", this._updateScaling.bind(this));
446
+ // Listener for window resize
447
+ window.addEventListener("resize", this._boundUpdateScaling);
388
448
 
389
- // Écouteur pour le chargement de l'image
449
+ // Listener for image load
390
450
  if (!this.imageElement.complete) {
391
- this.imageElement.addEventListener(
392
- "load",
393
- this._updateScaling.bind(this),
394
- );
451
+ this.imageElement.addEventListener("load", this._boundUpdateScaling);
395
452
  }
396
453
 
397
- // Écouteurs pour les interactions de souris
398
- document.addEventListener("mouseup", this._handleMouseUp.bind(this));
399
- document.addEventListener("mousemove", this._handleMouseMove.bind(this));
454
+ // Listeners for mouse interactions
455
+ document.addEventListener("mouseup", this._boundHandleMouseUp);
456
+ document.addEventListener("mousemove", this._boundHandleMouseMove);
400
457
  }
401
458
 
402
459
  /**
403
- * Gère l'événement mouseup global
460
+ * Handles global mouseup event
404
461
  * @private
405
462
  */
406
463
  _handleMouseUp() {
@@ -420,75 +477,75 @@ class VisualImageTool {
420
477
  }
421
478
 
422
479
  /**
423
- * Gère l'événement mousemove global
480
+ * Handles global mousemove event
424
481
  * @private
425
- * @param {MouseEvent} e - Événement mousemove
482
+ * @param {MouseEvent} e - Mousemove event
426
483
  */
427
484
  _handleMouseMove(e) {
428
- // Gestion du déplacement du point focal
485
+ // Handle focus point dragging
429
486
  if (this.interaction.focusDragging && this.state.focusMarker) {
430
487
  this._handleFocusMarkerDrag(e);
431
488
  }
432
489
 
433
- // Gestion du déplacement de la zone de recadrage
490
+ // Handle crop zone dragging
434
491
  if (this.interaction.cropDragging) {
435
492
  this._handleCropOverlayDrag(e);
436
493
  }
437
494
 
438
- // Gestion du redimensionnement de la zone de recadrage
495
+ // Handle crop zone resizing
439
496
  if (this.interaction.cropResizing) {
440
497
  this._handleCropOverlayResize(e);
441
498
  }
442
499
  }
443
500
 
444
501
  /**
445
- * Gère le déplacement du marqueur de point focal
502
+ * Handles focus point marker drag
446
503
  * @private
447
- * @param {MouseEvent} e - Événement mousemove
504
+ * @param {MouseEvent} e - Mousemove event
448
505
  */
449
506
  _handleFocusMarkerDrag(e) {
450
507
  const imageRect = this.imageElement.getBoundingClientRect();
451
508
 
452
- // Calculer la position cible relative à l'image
509
+ // Calculate target position relative to the image
453
510
  const targetScaledX =
454
511
  e.clientX - imageRect.left - this.interaction.focusDragOffsetX;
455
512
  const targetScaledY =
456
513
  e.clientY - imageRect.top - this.interaction.focusDragOffsetY;
457
514
 
458
- // Convertir en coordonnées originales
515
+ // Convert to original coordinates
459
516
  const original = this._toOriginalCoords(targetScaledX, targetScaledY);
460
517
 
461
- // Mettre à jour le point focal
518
+ // Update focus point
462
519
  this.setFocusPoint(original.x, original.y);
463
520
  }
464
521
 
465
522
  /**
466
- * Gère le déplacement de l'overlay de recadrage
523
+ * Handles crop overlay drag
467
524
  * @private
468
- * @param {MouseEvent} e - Événement mousemove
525
+ * @param {MouseEvent} e - Mousemove event
469
526
  */
470
527
  _handleCropOverlayDrag(e) {
471
528
  const deltaX = e.clientX - this.interaction.startMouseX;
472
529
  const deltaY = e.clientY - this.interaction.startMouseY;
473
530
 
474
- // Calculer la nouvelle position en pixels d'affichage
531
+ // Calculate new position in display pixels
475
532
  const newX = this.interaction.startX + deltaX;
476
533
  const newY = this.interaction.startY + deltaY;
477
534
 
478
- // Convertir en coordonnées originales
535
+ // Convert to original coordinates
479
536
  const original = this._toOriginalCoords(newX, newY);
480
537
 
481
- // Obtenir les dimensions actuelles en coordonnées originales
538
+ // Get current dimensions in original coordinates
482
539
  const { width, height } = this.state.cropZone;
483
540
 
484
- // Mettre à jour la zone de recadrage
541
+ // Update crop zone
485
542
  this.setCropZone(original.x, original.y, width, height);
486
543
  }
487
544
 
488
545
  /**
489
- * Gère le redimensionnement de l'overlay de recadrage
546
+ * Handles crop overlay resize
490
547
  * @private
491
- * @param {MouseEvent} e - Événement mousemove
548
+ * @param {MouseEvent} e - Mousemove event
492
549
  */
493
550
  _handleCropOverlayResize(e) {
494
551
  if (!this.interaction.activeHandle) return;
@@ -501,7 +558,7 @@ class VisualImageTool {
501
558
  let newWidth = this.interaction.startWidth;
502
559
  let newHeight = this.interaction.startHeight;
503
560
 
504
- // Ajuster en fonction de la poignée active
561
+ // Adjust based on active handle
505
562
  if (this.interaction.activeHandle.includes("t")) {
506
563
  // Top
507
564
  newY = this.interaction.startY + deltaY;
@@ -521,7 +578,7 @@ class VisualImageTool {
521
578
  newWidth = this.interaction.startWidth + deltaX;
522
579
  }
523
580
 
524
- // Empêcher les dimensions négatives
581
+ // Prevent negative dimensions
525
582
  if (newWidth < 10) {
526
583
  if (this.interaction.activeHandle.includes("l")) {
527
584
  newX = this.interaction.startX + this.interaction.startWidth - 10;
@@ -535,18 +592,18 @@ class VisualImageTool {
535
592
  newHeight = 10;
536
593
  }
537
594
 
538
- // Convertir les coordonnées d'affichage en coordonnées originales
595
+ // Convert display coordinates to original coordinates
539
596
  const originalX = newX / this.state.scaleX;
540
597
  const originalY = newY / this.state.scaleY;
541
598
  const originalWidth = newWidth / this.state.scaleX;
542
599
  const originalHeight = newHeight / this.state.scaleY;
543
600
 
544
- // Mettre à jour la zone de recadrage
601
+ // Update crop zone
545
602
  this.setCropZone(originalX, originalY, originalWidth, originalHeight);
546
603
  }
547
604
 
548
605
  /**
549
- * Met à jour la position du marqueur de point focal
606
+ * Updates the focus point marker position
550
607
  * @private
551
608
  */
552
609
  _updateFocusMarkerPosition() {
@@ -554,11 +611,11 @@ class VisualImageTool {
554
611
 
555
612
  const { x, y } = this.state.focusPoint;
556
613
 
557
- // Limiter les coordonnées aux dimensions de l'image
614
+ // Clamp coordinates to image dimensions
558
615
  const clampedX = Math.max(0, Math.min(x, this.state.originalWidth));
559
616
  const clampedY = Math.max(0, Math.min(y, this.state.originalHeight));
560
617
 
561
- // Mettre à jour l'état si les coordonnées ont été limitées
618
+ // Update state if coordinates were clamped
562
619
  if (x !== clampedX || y !== clampedY) {
563
620
  this.state.focusPoint = { x: clampedX, y: clampedY };
564
621
  }
@@ -583,7 +640,7 @@ class VisualImageTool {
583
640
  );
584
641
  }
585
642
 
586
- // Ajuster pour centrer le marqueur
643
+ // Adjust to center the marker
587
644
  // Adjust for container padding (use unique variable names)
588
645
  let focusPaddingLeft = 0;
589
646
  let focusPaddingTop = 0;
@@ -611,7 +668,7 @@ class VisualImageTool {
611
668
  }
612
669
 
613
670
  /**
614
- * Met à jour la position et les dimensions de l'overlay de recadrage
671
+ * Updates the crop overlay position and dimensions
615
672
  * @private
616
673
  */
617
674
  _updateCropOverlayPosition() {
@@ -619,7 +676,7 @@ class VisualImageTool {
619
676
 
620
677
  const { x, y, width, height } = this.state.cropZone;
621
678
 
622
- // Limiter aux dimensions de l'image
679
+ // Clamp to image dimensions
623
680
  const clampedX = Math.max(0, Math.min(x, this.state.originalWidth - width));
624
681
  const clampedY = Math.max(
625
682
  0,
@@ -634,7 +691,7 @@ class VisualImageTool {
634
691
  Math.min(height, this.state.originalHeight - clampedY),
635
692
  );
636
693
 
637
- // Mettre à jour l'état si les valeurs ont été limitées
694
+ // Update state if values were clamped
638
695
  if (
639
696
  x !== clampedX ||
640
697
  y !== clampedY ||
@@ -649,7 +706,7 @@ class VisualImageTool {
649
706
  };
650
707
  }
651
708
 
652
- // Convertir en coordonnées d'affichage
709
+ // Convert to display coordinates
653
710
  const scaled = this._toScaledCoords(clampedX, clampedY);
654
711
  const scaledWidth = clampedWidth * this.state.scaleX;
655
712
  const scaledHeight = clampedHeight * this.state.scaleY;
@@ -672,7 +729,7 @@ class VisualImageTool {
672
729
  );
673
730
  }
674
731
 
675
- // Mettre à jour l'overlay
732
+ // Update overlay
676
733
  // Adjust for container padding (use unique variable names)
677
734
  let cropPaddingLeft = 0;
678
735
  let cropPaddingTop = 0;
@@ -698,23 +755,24 @@ class VisualImageTool {
698
755
  }
699
756
 
700
757
  /**
701
- * Active ou désactive le point focal
758
+ * Enables or disables the focus point.
759
+ * If the focus point is at its initial state `{x:0, y:0}` (top-left corner), it will be moved to the center of the image when this method is called with `active = true` for the first time.
702
760
  * @public
703
- * @param {boolean} active - État d'activation
704
- * @returns {ImageTool} Instance pour chaînage
761
+ * @param {boolean} active - Activation state
762
+ * @returns {VisualImageTool} Instance for chaining
705
763
  */
706
764
  toggleFocusPoint(active) {
707
765
  if (!this.options.focusPoint.enabled) return this;
708
766
 
709
767
  const isActive = active === undefined ? !this.state.focusActive : active;
710
768
 
711
- if (active && !this.state.focusActive) {
712
- // Activer le point focal
769
+ if (isActive && !this.state.focusActive) {
770
+ // Enable focus point
713
771
  if (!this.state.focusMarker) {
714
772
  this._createFocusMarker();
715
773
  }
716
774
 
717
- // Positionner au centre de l'image par défaut si pas déjà défini
775
+ // Position at image center by default if not already set
718
776
  if (this.state.focusPoint.x === 0 && this.state.focusPoint.y === 0) {
719
777
  this.state.focusPoint = {
720
778
  x: this.state.originalWidth / 2,
@@ -725,38 +783,38 @@ class VisualImageTool {
725
783
  this._updateFocusMarkerPosition();
726
784
  this.state.focusMarker.style.display = "block";
727
785
  this.state.focusActive = true;
728
- } else if (!active && this.state.focusActive) {
729
- // Désactiver le point focal
786
+ } else if (!isActive && this.state.focusActive) {
787
+ // Disable focus point
730
788
  if (this.state.focusMarker) {
731
789
  this.state.focusMarker.style.display = "none";
732
790
  }
733
791
  this.state.focusActive = false;
734
792
  }
735
793
 
736
- // Notifier le changement
794
+ // Notify change
737
795
  this._notifyChange();
738
796
 
739
797
  return this;
740
798
  }
741
799
 
742
800
  /**
743
- * Active ou désactive la zone de recadrage
801
+ * Enables or disables the crop zone
744
802
  * @public
745
- * @param {boolean} active - État d'activation
746
- * @returns {ImageTool} Instance pour chaînage
803
+ * @param {boolean} active - Activation state
804
+ * @returns {VisualImageTool} Instance for chaining
747
805
  */
748
806
  toggleCropZone(active) {
749
807
  if (!this.options.cropZone.enabled) return this;
750
808
 
751
809
  const isActive = active === undefined ? !this.state.cropActive : active;
752
810
 
753
- if (active && !this.state.cropActive) {
754
- // Activer la zone de recadrage
811
+ if (isActive && !this.state.cropActive) {
812
+ // Enable crop zone
755
813
  if (!this.state.cropOverlay) {
756
814
  this._createCropOverlay();
757
815
  }
758
816
 
759
- // Définir une zone par défaut si pas déjà définie
817
+ // Define a default zone if not already set
760
818
  if (this.state.cropZone.width === 0 || this.state.cropZone.height === 0) {
761
819
  const defaultWidth = this.state.originalWidth / 2;
762
820
  const defaultHeight = this.state.originalHeight / 2;
@@ -774,29 +832,29 @@ class VisualImageTool {
774
832
  this._updateCropOverlayPosition();
775
833
  this.state.cropOverlay.style.display = "block";
776
834
  this.state.cropActive = true;
777
- } else if (!active && this.state.cropActive) {
778
- // Désactiver la zone de recadrage
835
+ } else if (!isActive && this.state.cropActive) {
836
+ // Disable crop zone
779
837
  if (this.state.cropOverlay) {
780
838
  this.state.cropOverlay.style.display = "none";
781
839
  }
782
840
  this.state.cropActive = false;
783
841
  }
784
842
 
785
- // Notifier le changement
843
+ // Notify change
786
844
  this._notifyChange();
787
845
 
788
846
  return this;
789
847
  }
790
848
 
791
849
  /**
792
- * Définit la position du point focal
850
+ * Sets the focus point position
793
851
  * @public
794
- * @param {number} x - Coordonnée X en pixels originaux
795
- * @param {number} y - Coordonnée Y en pixels originaux
796
- * @returns {ImageTool} Instance pour chaînage
852
+ * @param {number} x - X-coordinate in original pixels
853
+ * @param {number} y - Y-coordinate in original pixels
854
+ * @returns {VisualImageTool} Instance for chaining
797
855
  */
798
856
  setFocusPoint(x, y) {
799
- // Limiter les coordonnées aux dimensions de l'image
857
+ // Clamp coordinates to image dimensions
800
858
  const clampedX = Math.max(0, Math.min(x, this.state.originalWidth));
801
859
  const clampedY = Math.max(0, Math.min(y, this.state.originalHeight));
802
860
 
@@ -806,23 +864,23 @@ class VisualImageTool {
806
864
  this._updateFocusMarkerPosition();
807
865
  }
808
866
 
809
- // Notifier le changement
867
+ // Notify change
810
868
  this._notifyChange();
811
869
 
812
870
  return this;
813
871
  }
814
872
 
815
873
  /**
816
- * Définit la position et les dimensions de la zone de recadrage
874
+ * Sets the crop zone position and dimensions
817
875
  * @public
818
- * @param {number} x - Coordonnée X en pixels originaux
819
- * @param {number} y - Coordonnée Y en pixels originaux
820
- * @param {number} width - Largeur en pixels originaux
821
- * @param {number} height - Hauteur en pixels originaux
822
- * @returns {ImageTool} Instance pour chaînage
876
+ * @param {number} x - X-coordinate in original pixels
877
+ * @param {number} y - Y-coordinate in original pixels
878
+ * @param {number} width - Width in original pixels
879
+ * @param {number} height - Height in original pixels
880
+ * @returns {VisualImageTool} Instance for chaining
823
881
  */
824
882
  setCropZone(x, y, width, height) {
825
- // Limiter aux dimensions de l'image
883
+ // Clamp to image dimensions
826
884
  const clampedX = Math.max(0, Math.min(x, this.state.originalWidth - width));
827
885
  const clampedY = Math.max(
828
886
  0,
@@ -848,32 +906,32 @@ class VisualImageTool {
848
906
  this._updateCropOverlayPosition();
849
907
  }
850
908
 
851
- // Notifier le changement
909
+ // Notify change
852
910
  this._notifyChange();
853
911
 
854
912
  return this;
855
913
  }
856
914
 
857
915
  /**
858
- * Obtient la position actuelle du point focal
916
+ * Gets the current focus point position
859
917
  * @public
860
- * @returns {Object} Coordonnées du point focal {x, y}
918
+ * @returns {Object} Focus point coordinates {x, y}
861
919
  */
862
920
  getFocusPoint() {
863
921
  return { ...this.state.focusPoint };
864
922
  }
865
923
 
866
924
  /**
867
- * Obtient la position et les dimensions actuelles de la zone de recadrage
925
+ * Gets the current crop zone position and dimensions
868
926
  * @public
869
- * @returns {Object} Zone de recadrage {x, y, width, height}
927
+ * @returns {Object} Crop zone {x, y, width, height}
870
928
  */
871
929
  getCropZone() {
872
930
  return { ...this.state.cropZone };
873
931
  }
874
932
 
875
933
  /**
876
- * Obtient les dimensions originales de l'image
934
+ * Gets the original image dimensions
877
935
  * @public
878
936
  * @returns {Object} Dimensions {width, height}
879
937
  */
@@ -885,7 +943,7 @@ class VisualImageTool {
885
943
  }
886
944
 
887
945
  /**
888
- * Notifie les changements via le callback
946
+ * Notifies changes via callback
889
947
  * @private
890
948
  */
891
949
  _notifyChange() {
@@ -900,11 +958,36 @@ class VisualImageTool {
900
958
  }
901
959
 
902
960
  /**
903
- * Détruit l'instance et nettoie les ressources
961
+ * Destroys the instance and cleans up resources
904
962
  * @public
905
963
  */
906
964
  destroy() {
907
- // Supprimer les éléments DOM
965
+ // Already destroyed
966
+ if (!this.state) {
967
+ return;
968
+ }
969
+
970
+ // Remove element listeners
971
+ if (this.state.focusMarker) {
972
+ this.state.focusMarker.removeEventListener(
973
+ "mousedown",
974
+ this._boundFocusMarkerMouseDown,
975
+ );
976
+ }
977
+
978
+ if (this.state.cropOverlay) {
979
+ this.state.cropOverlay.removeEventListener(
980
+ "mousedown",
981
+ this._boundCropOverlayMouseDown,
982
+ );
983
+ }
984
+
985
+ for (const handle of this.cropHandles) {
986
+ handle.removeEventListener("mousedown", this._boundCropHandleMouseDown);
987
+ }
988
+ this.cropHandles = [];
989
+
990
+ // Remove DOM elements
908
991
  if (this.state.focusMarker?.parentNode) {
909
992
  this.state.focusMarker.parentNode.removeChild(this.state.focusMarker);
910
993
  }
@@ -913,12 +996,29 @@ class VisualImageTool {
913
996
  this.state.cropOverlay.parentNode.removeChild(this.state.cropOverlay);
914
997
  }
915
998
 
916
- // Supprimer les écouteurs d'événements
917
- window.removeEventListener("resize", this._updateScaling.bind(this));
918
- document.removeEventListener("mouseup", this._handleMouseUp.bind(this));
919
- document.removeEventListener("mousemove", this._handleMouseMove.bind(this));
999
+ if (this.spinnerElement?.parentNode) {
1000
+ this.spinnerElement.parentNode.removeChild(this.spinnerElement);
1001
+ }
1002
+ this.spinnerElement = null;
1003
+
1004
+ // Remove global listeners
1005
+ window.removeEventListener("resize", this._boundUpdateScaling);
1006
+ document.removeEventListener("mouseup", this._boundHandleMouseUp);
1007
+ document.removeEventListener("mousemove", this._boundHandleMouseMove);
1008
+
1009
+ if (this.imageElement) {
1010
+ this.imageElement.removeEventListener("load", this._boundUpdateScaling);
1011
+ }
1012
+
1013
+ // Release bound handlers
1014
+ this._boundUpdateScaling = null;
1015
+ this._boundHandleMouseUp = null;
1016
+ this._boundHandleMouseMove = null;
1017
+ this._boundFocusMarkerMouseDown = null;
1018
+ this._boundCropOverlayMouseDown = null;
1019
+ this._boundCropHandleMouseDown = null;
920
1020
 
921
- // Réinitialiser l'état
1021
+ // Reset state
922
1022
  this.state = null;
923
1023
  this.interaction = null;
924
1024
  this.options = null;
@@ -926,5 +1026,5 @@ class VisualImageTool {
926
1026
  }
927
1027
  }
928
1028
 
929
- // Exporter la classe
1029
+ // Export class
930
1030
  export default VisualImageTool;