@h4md1/visual-image-tool 0.2.2 → 0.2.4

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