@bensitu/image-editor 1.5.1 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import fabricModule from "fabric";
5
5
  /**
6
6
  * @file image-editor.js
7
7
  * @module image-editor
8
- * @version 1.5.1
8
+ * @version 1.5.2
9
9
  * @author Ben Situ
10
10
  * @license MIT
11
11
  * @description Lightweight canvas-based image editor with masking/transform/export support.
@@ -78,6 +78,7 @@ var ImageEditor = class {
78
78
  imageLoadTimeoutMs: 3e4,
79
79
  exportMultiplier: 1,
80
80
  maxExportPixels: 5e7,
81
+ maxHistorySize: 50,
81
82
  exportImageAreaByDefault: true,
82
83
  defaultMaskWidth: 50,
83
84
  defaultMaskHeight: 80,
@@ -106,6 +107,7 @@ var ImageEditor = class {
106
107
  ...userCrop
107
108
  }
108
109
  };
110
+ this._normalizeOptions();
109
111
  this._fabricLoaded = !!ensureFabric();
110
112
  if (!this._fabricLoaded) {
111
113
  this._reportError("fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.");
@@ -125,16 +127,18 @@ var ImageEditor = class {
125
127
  this._activeOperationToken = null;
126
128
  this.elements = {};
127
129
  this.isImageLoadedToCanvas = false;
128
- this.maxHistorySize = 50;
130
+ this.maxHistorySize = this.options.maxHistorySize;
129
131
  this._handlersByElementKey = {};
130
132
  this._elementCache = {};
131
133
  this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
134
+ this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
132
135
  this._lastMask = null;
133
136
  this._lastMaskInitialLeft = null;
134
137
  this._lastMaskInitialTop = null;
135
138
  this._lastMaskInitialWidth = null;
136
139
  this._lastSnapshot = null;
137
140
  this._cropMode = false;
141
+ this._isApplyingCrop = false;
138
142
  this._cropRect = null;
139
143
  this._cropHandlers = [];
140
144
  this._cropPrevEvented = null;
@@ -231,6 +235,8 @@ var ImageEditor = class {
231
235
  this._activeOperationName = null;
232
236
  this._activeOperationToken = null;
233
237
  this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
238
+ this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
239
+ this._isApplyingCrop = false;
234
240
  this._containerOriginalOverflow = null;
235
241
  this._lastContainerViewportSize = null;
236
242
  this._canvasElementOriginalStyle = null;
@@ -350,6 +356,54 @@ var ImageEditor = class {
350
356
  `ElementIdMap.${deprecatedKey} is deprecated. Use ${canonicalKey} instead. This alias will be removed in v2.0.0.`
351
357
  );
352
358
  }
359
+ _normalizeFiniteNumber(value, fallback) {
360
+ const numericValue = Number(value);
361
+ return Number.isFinite(numericValue) ? numericValue : fallback;
362
+ }
363
+ _normalizePositiveNumber(value, fallback) {
364
+ const numericValue = this._normalizeFiniteNumber(value, fallback);
365
+ return numericValue > 0 ? numericValue : fallback;
366
+ }
367
+ _normalizeNonNegativeNumber(value, fallback) {
368
+ const numericValue = this._normalizeFiniteNumber(value, fallback);
369
+ return numericValue >= 0 ? numericValue : fallback;
370
+ }
371
+ _normalizePositiveInteger(value, fallback) {
372
+ const numericValue = this._normalizePositiveNumber(value, fallback);
373
+ return Math.max(1, Math.floor(numericValue));
374
+ }
375
+ _normalizeOptions() {
376
+ const options = this.options || {};
377
+ options.canvasWidth = this._normalizePositiveNumber(options.canvasWidth, 800);
378
+ options.canvasHeight = this._normalizePositiveNumber(options.canvasHeight, 600);
379
+ options.animationDuration = this._normalizeNonNegativeNumber(options.animationDuration, 300);
380
+ const minScale = this._normalizePositiveNumber(options.minScale, 0.1);
381
+ const maxScale = this._normalizePositiveNumber(options.maxScale, 5);
382
+ if (minScale > maxScale) {
383
+ options.minScale = 0.1;
384
+ options.maxScale = 5;
385
+ } else {
386
+ options.minScale = minScale;
387
+ options.maxScale = maxScale;
388
+ }
389
+ options.scaleStep = this._normalizePositiveNumber(options.scaleStep, 0.05);
390
+ options.rotationStep = this._normalizeFiniteNumber(options.rotationStep, 90);
391
+ options.downsampleMaxWidth = this._normalizePositiveNumber(options.downsampleMaxWidth, 4e3);
392
+ options.downsampleMaxHeight = this._normalizePositiveNumber(options.downsampleMaxHeight, 3e3);
393
+ options.downsampleQuality = options.downsampleQuality == null ? 0.92 : Math.max(0, Math.min(1, this._normalizeFiniteNumber(options.downsampleQuality, 0.92)));
394
+ options.imageLoadTimeoutMs = this._normalizePositiveNumber(options.imageLoadTimeoutMs, 3e4);
395
+ options.exportMultiplier = this._normalizePositiveNumber(options.exportMultiplier, 1);
396
+ options.maxExportPixels = this._normalizePositiveInteger(options.maxExportPixels, 5e7);
397
+ options.maxHistorySize = this._normalizePositiveInteger(options.maxHistorySize, 50);
398
+ options.defaultMaskWidth = this._normalizePositiveNumber(options.defaultMaskWidth, 50);
399
+ options.defaultMaskHeight = this._normalizePositiveNumber(options.defaultMaskHeight, 80);
400
+ options.maskLabelOffset = this._normalizeNonNegativeNumber(options.maskLabelOffset, 3);
401
+ if (options.crop) {
402
+ options.crop.minWidth = this._normalizePositiveNumber(options.crop.minWidth, 100);
403
+ options.crop.minHeight = this._normalizePositiveNumber(options.crop.minHeight, 100);
404
+ options.crop.padding = this._normalizeNonNegativeNumber(options.crop.padding, 10);
405
+ }
406
+ }
353
407
  _reportError(message, error = null) {
354
408
  const handler = this.options && this.options.onError;
355
409
  if (typeof handler !== "function") return;
@@ -366,10 +420,18 @@ var ImageEditor = class {
366
420
  } catch {
367
421
  }
368
422
  }
423
+ _emitSafeCallback(callback, message) {
424
+ if (typeof callback !== "function") return;
425
+ try {
426
+ callback();
427
+ } catch (error) {
428
+ this._reportWarning(message, error);
429
+ }
430
+ }
369
431
  _notifyImageLoaded() {
370
432
  const optionsCallback = this.options && this.options.onImageLoaded;
371
433
  const callback = typeof optionsCallback === "function" ? optionsCallback : this.onImageLoaded;
372
- if (typeof callback === "function") callback();
434
+ this._emitSafeCallback(callback, "onImageLoaded callback failed");
373
435
  }
374
436
  /**
375
437
  * Initializes the Fabric canvas, viewport elements, and selection event handlers.
@@ -608,7 +670,6 @@ var ImageEditor = class {
608
670
  _loadImageFile(file) {
609
671
  if (!this._isSupportedImageFile(file)) {
610
672
  const error = new Error("Selected file is not a supported image");
611
- this._reportError("Selected file is not a supported image", error);
612
673
  return Promise.reject(error);
613
674
  }
614
675
  return new Promise((resolve, reject) => {
@@ -671,6 +732,7 @@ var ImageEditor = class {
671
732
  const isNestedOperation = this._isOwnInternalOperation(options);
672
733
  const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("loadImage");
673
734
  let transaction = null;
735
+ let shouldNotifyImageLoaded;
674
736
  try {
675
737
  this._isLoading = true;
676
738
  this._updateUI();
@@ -759,7 +821,7 @@ var ImageEditor = class {
759
821
  this._updateUI();
760
822
  this.canvas.renderAll();
761
823
  this._lastSnapshot = this._captureCanvasStateOrThrow("loadImage");
762
- this._notifyImageLoaded();
824
+ shouldNotifyImageLoaded = true;
763
825
  } catch (error) {
764
826
  await this._rollbackLoadImageTransaction(
765
827
  transaction,
@@ -771,6 +833,9 @@ var ImageEditor = class {
771
833
  if (!isNestedOperation) this._endBusyOperation(operationToken);
772
834
  if (!this._disposed && this.canvas) this._updateUI();
773
835
  }
836
+ if (shouldNotifyImageLoaded && !this._disposed && this.canvas) {
837
+ this._notifyImageLoaded();
838
+ }
774
839
  }
775
840
  /**
776
841
  * Checks whether there is a loaded image on the current canvas.
@@ -787,7 +852,7 @@ var ImageEditor = class {
787
852
  * @public
788
853
  */
789
854
  isBusy() {
790
- return !!(this.isAnimating || this._cropMode || this._isLoading || this._activeOperationToken || this.animationQueue && this.animationQueue.isBusy());
855
+ return !!(this.isAnimating || this._cropMode || this._isApplyingCrop || this._isLoading || this._activeOperationToken || this.animationQueue && this.animationQueue.isBusy());
791
856
  }
792
857
  /**
793
858
  * Creates an HTMLImageElement from a given data URL.
@@ -1584,7 +1649,23 @@ var ImageEditor = class {
1584
1649
  _getJpegBackgroundColor() {
1585
1650
  const backgroundColor = String(this.options.backgroundColor || "").trim();
1586
1651
  if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return "#ffffff";
1587
- return backgroundColor;
1652
+ return this._isValidCanvasFillStyle(backgroundColor) ? backgroundColor : "#ffffff";
1653
+ }
1654
+ _isValidCanvasFillStyle(color) {
1655
+ try {
1656
+ if (typeof document === "undefined" || !document.createElement) return false;
1657
+ const validationCanvas = document.createElement("canvas");
1658
+ const context = validationCanvas.getContext && validationCanvas.getContext("2d");
1659
+ if (!context) return false;
1660
+ context.fillStyle = "#010203";
1661
+ context.fillStyle = color;
1662
+ if (context.fillStyle !== "#010203") return true;
1663
+ context.fillStyle = "#040506";
1664
+ context.fillStyle = color;
1665
+ return context.fillStyle !== "#040506";
1666
+ } catch {
1667
+ return false;
1668
+ }
1588
1669
  }
1589
1670
  _isTransparentCssColor(color) {
1590
1671
  const normalizedColor = String(color || "").trim().toLowerCase();
@@ -1994,10 +2075,12 @@ var ImageEditor = class {
1994
2075
  async _scaleImageImpl(factor, options = {}) {
1995
2076
  if (!this.originalImage || this._disposed) return;
1996
2077
  if (this.isAnimating) return;
2078
+ const numericFactor = Number(factor);
2079
+ if (!Number.isFinite(numericFactor)) return;
1997
2080
  const saveHistory = options.saveHistory !== false;
1998
2081
  let didStartAnimation = false;
1999
2082
  try {
2000
- factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, factor));
2083
+ factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, numericFactor));
2001
2084
  this.currentScale = factor;
2002
2085
  this.isAnimating = true;
2003
2086
  didStartAnimation = true;
@@ -2063,7 +2146,8 @@ var ImageEditor = class {
2063
2146
  async _rotateImageImpl(degrees, options = {}) {
2064
2147
  if (!this.originalImage || this._disposed) return;
2065
2148
  if (this.isAnimating) return;
2066
- if (isNaN(degrees)) return;
2149
+ const numericDegrees = Number(degrees);
2150
+ if (!Number.isFinite(numericDegrees)) return;
2067
2151
  const saveHistory = options.saveHistory !== false;
2068
2152
  const image = this.originalImage;
2069
2153
  const previousOriginX = image.originX || "left";
@@ -2072,6 +2156,7 @@ var ImageEditor = class {
2072
2156
  let didStartAnimation = false;
2073
2157
  let didCompleteRotation = false;
2074
2158
  try {
2159
+ degrees = numericDegrees;
2075
2160
  this.currentRotation = degrees;
2076
2161
  this.isAnimating = true;
2077
2162
  didStartAnimation = true;
@@ -2575,7 +2660,11 @@ var ImageEditor = class {
2575
2660
  maskConfig.top = top;
2576
2661
  let mask;
2577
2662
  if (typeof maskConfig.fabricGenerator === "function") {
2578
- mask = maskConfig.fabricGenerator(maskConfig, this.canvas, this.options);
2663
+ try {
2664
+ mask = maskConfig.fabricGenerator(maskConfig, this.canvas, this.options);
2665
+ } catch (error) {
2666
+ return rejectInvalidMask("fabricGenerator failed", error);
2667
+ }
2579
2668
  } else {
2580
2669
  switch (shapeType) {
2581
2670
  case "circle":
@@ -2629,6 +2718,17 @@ var ImageEditor = class {
2629
2718
  } catch (error) {
2630
2719
  return rejectInvalidMask("invalid polygon points", error);
2631
2720
  }
2721
+ const uniquePointKeys = new Set(polygonPoints.map((point) => `${point.x}:${point.y}`));
2722
+ if (uniquePointKeys.size !== polygonPoints.length) {
2723
+ return rejectInvalidMask("polygon points must not contain duplicates");
2724
+ }
2725
+ const doubleArea = polygonPoints.reduce((area, point, index) => {
2726
+ const nextPoint = polygonPoints[(index + 1) % polygonPoints.length];
2727
+ return area + point.x * nextPoint.y - nextPoint.x * point.y;
2728
+ }, 0);
2729
+ if (Math.abs(doubleArea) < 1e-6) {
2730
+ return rejectInvalidMask("polygon masks must have a non-zero area");
2731
+ }
2632
2732
  mask = new fabric.Polygon(polygonPoints, {
2633
2733
  left,
2634
2734
  top,
@@ -2707,7 +2807,12 @@ var ImageEditor = class {
2707
2807
  this._updateUI();
2708
2808
  this.canvas.renderAll();
2709
2809
  this.saveState();
2710
- if (typeof maskConfig.onCreate === "function") maskConfig.onCreate(mask, this.canvas);
2810
+ if (typeof maskConfig.onCreate === "function") {
2811
+ this._emitSafeCallback(
2812
+ () => maskConfig.onCreate(mask, this.canvas),
2813
+ "createMask onCreate callback failed"
2814
+ );
2815
+ }
2711
2816
  return mask;
2712
2817
  }
2713
2818
  /**
@@ -2911,8 +3016,15 @@ var ImageEditor = class {
2911
3016
  this._removeLabelForMask(mask);
2912
3017
  let textObject = null;
2913
3018
  if (this.options.label && typeof this.options.label.create === "function") {
2914
- textObject = this.options.label.create(mask, fabric);
2915
- if (!textObject || typeof textObject.set !== "function") {
3019
+ let didLabelCreateThrow = false;
3020
+ try {
3021
+ textObject = this.options.label.create(mask, fabric);
3022
+ } catch (error) {
3023
+ didLabelCreateThrow = true;
3024
+ this._reportWarning("label.create() failed; using the default label", error);
3025
+ textObject = null;
3026
+ }
3027
+ if (!didLabelCreateThrow && (!textObject || typeof textObject.set !== "function")) {
2916
3028
  this._reportWarning("label.create() returned an invalid Fabric object; using the default label");
2917
3029
  textObject = null;
2918
3030
  }
@@ -2933,7 +3045,12 @@ var ImageEditor = class {
2933
3045
  };
2934
3046
  if (this.options.label) {
2935
3047
  if (typeof this.options.label.getText === "function") {
2936
- labelText = this.options.label.getText(mask, this._getMaskCreationIndex(mask));
3048
+ try {
3049
+ labelText = this.options.label.getText(mask, this._getMaskCreationIndex(mask));
3050
+ } catch (error) {
3051
+ this._reportWarning("label.getText() failed; using the mask name", error);
3052
+ labelText = mask.maskName;
3053
+ }
2937
3054
  }
2938
3055
  if (this.options.label.textOptions) {
2939
3056
  Object.assign(textOptions, this.options.label.textOptions);
@@ -3445,6 +3562,38 @@ var ImageEditor = class {
3445
3562
  height
3446
3563
  };
3447
3564
  }
3565
+ _getCropRectRawBounds(cropRect) {
3566
+ if (!cropRect) return { left: NaN, top: NaN, width: NaN, height: NaN };
3567
+ return {
3568
+ left: Number(cropRect.left),
3569
+ top: Number(cropRect.top),
3570
+ width: Number(cropRect.width) * Math.abs(Number(cropRect.scaleX)),
3571
+ height: Number(cropRect.height) * Math.abs(Number(cropRect.scaleY))
3572
+ };
3573
+ }
3574
+ _isValidCropRegion(cropBounds, imageBounds) {
3575
+ if (!cropBounds || !imageBounds) return false;
3576
+ const left = Number(cropBounds.left);
3577
+ const top = Number(cropBounds.top);
3578
+ const width = Number(cropBounds.width);
3579
+ const height = Number(cropBounds.height);
3580
+ const imageLeft = Number(imageBounds.left);
3581
+ const imageTop = Number(imageBounds.top);
3582
+ const imageWidth = Number(imageBounds.width);
3583
+ const imageHeight = Number(imageBounds.height);
3584
+ if (![left, top, width, height, imageLeft, imageTop, imageWidth, imageHeight].every(Number.isFinite)) return false;
3585
+ if (width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return false;
3586
+ const right = left + width;
3587
+ const bottom = top + height;
3588
+ const imageRight = imageLeft + imageWidth;
3589
+ const imageBottom = imageTop + imageHeight;
3590
+ const overlapsImage = left < imageRight && right > imageLeft && top < imageBottom && bottom > imageTop;
3591
+ if (!overlapsImage) return false;
3592
+ const canvasWidth = this.canvas ? Number(this.canvas.getWidth()) : NaN;
3593
+ const canvasHeight = this.canvas ? Number(this.canvas.getHeight()) : NaN;
3594
+ if (!Number.isFinite(canvasWidth) || !Number.isFinite(canvasHeight) || canvasWidth <= 0 || canvasHeight <= 0) return false;
3595
+ return left < canvasWidth && right > 0 && top < canvasHeight && bottom > 0;
3596
+ }
3448
3597
  /**
3449
3598
  * Enters crop mode by creating a resizable crop rectangle above the base image.
3450
3599
  *
@@ -3456,6 +3605,10 @@ var ImageEditor = class {
3456
3605
  */
3457
3606
  enterCropMode() {
3458
3607
  if (!this.canvas || !this.originalImage || this._cropMode) return;
3608
+ if (this._isApplyingCrop) {
3609
+ this._reportWarning("enterCropMode ignored because a crop is already being applied");
3610
+ return;
3611
+ }
3459
3612
  if (!this._canMutateNow("enterCropMode")) return;
3460
3613
  if (!this.isImageLoaded()) return;
3461
3614
  this._removeCropRect();
@@ -3580,6 +3733,10 @@ var ImageEditor = class {
3580
3733
  * @public
3581
3734
  */
3582
3735
  cancelCrop() {
3736
+ if (this._isApplyingCrop) {
3737
+ this._reportWarning("cancelCrop ignored because a crop is already being applied");
3738
+ return;
3739
+ }
3583
3740
  if (!this.canvas || !this._cropMode) return;
3584
3741
  this._removeCropRect();
3585
3742
  this._restoreCropObjectState();
@@ -3603,11 +3760,23 @@ var ImageEditor = class {
3603
3760
  */
3604
3761
  async applyCrop() {
3605
3762
  if (!this.canvas || !this._cropMode || !this._cropRect) return;
3763
+ if (this._isApplyingCrop) {
3764
+ this._reportWarning("applyCrop ignored because a crop is already being applied");
3765
+ return;
3766
+ }
3606
3767
  this._assertIdleForOperation("applyCrop");
3768
+ this._isApplyingCrop = true;
3607
3769
  const operationToken = this._beginBusyOperation("applyCrop");
3608
3770
  const internalOptions = this._withInternalOperationOptions(operationToken);
3609
3771
  try {
3610
3772
  this._cropRect.setCoords();
3773
+ this.originalImage.setCoords();
3774
+ const imageBounds = this.originalImage.getBoundingRect(true, true);
3775
+ const rawCropBounds = this._getCropRectRawBounds(this._cropRect);
3776
+ if (!this._isValidCropRegion(rawCropBounds, imageBounds)) {
3777
+ this._reportWarning("applyCrop: crop region is invalid");
3778
+ return;
3779
+ }
3611
3780
  const rectBounds = this._getCropRectContentBounds(this._cropRect);
3612
3781
  const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
3613
3782
  const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
@@ -3620,7 +3789,13 @@ var ImageEditor = class {
3620
3789
  beforeJson = null;
3621
3790
  }
3622
3791
  if (!beforeJson) {
3623
- this.cancelCrop();
3792
+ this._removeCropRect();
3793
+ this._cropMode = false;
3794
+ this.canvas.selection = !!this._prevSelectionSetting;
3795
+ this._prevSelectionSetting = void 0;
3796
+ this.canvas.discardActiveObject();
3797
+ this._updateUI();
3798
+ this.canvas.renderAll();
3624
3799
  return;
3625
3800
  }
3626
3801
  const preservedMasks = [];
@@ -3696,6 +3871,7 @@ var ImageEditor = class {
3696
3871
  this._updateUI();
3697
3872
  this.canvas.renderAll();
3698
3873
  } finally {
3874
+ this._isApplyingCrop = false;
3699
3875
  this._endBusyOperation(operationToken);
3700
3876
  }
3701
3877
  }
@@ -3727,9 +3903,11 @@ var ImageEditor = class {
3727
3903
  const isInCropMode = !!this._cropMode;
3728
3904
  const isBusy = this.isBusy();
3729
3905
  if (isInCropMode) {
3906
+ const cropInteractionKeys = /* @__PURE__ */ new Set(["canvas", "canvasContainer", "imagePlaceholder", "imgPlaceholder"]);
3730
3907
  for (const key of Object.keys(this.elements || {})) {
3731
3908
  const element = this._getElement(key);
3732
3909
  if (!element) continue;
3910
+ if (cropInteractionKeys.has(key)) continue;
3733
3911
  if (key === "applyCropButton" || key === "cancelCropButton" || key === "applyCropBtn" || key === "cancelCropBtn") {
3734
3912
  this._setDisabled(key, false);
3735
3913
  } else {
@@ -3767,9 +3945,44 @@ var ImageEditor = class {
3767
3945
  * @param {boolean} disabled - If true, disables the element; otherwise enables.
3768
3946
  * @private
3769
3947
  */
3948
+ _rememberElementDisabledState(key, element) {
3949
+ if (!element) return;
3950
+ if (!this._elementOriginalDisabledState) this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
3951
+ if (this._elementOriginalDisabledState.has(key)) return;
3952
+ this._elementOriginalDisabledState.set(key, {
3953
+ element,
3954
+ hasDisabledProperty: "disabled" in element,
3955
+ disabled: "disabled" in element ? !!element.disabled : void 0,
3956
+ ariaDisabled: element.getAttribute ? element.getAttribute("aria-disabled") : null,
3957
+ pointerEvents: element.style ? element.style.pointerEvents || "" : ""
3958
+ });
3959
+ }
3960
+ _restoreElementDisabledStates() {
3961
+ if (!this._elementOriginalDisabledState) return;
3962
+ for (const state of this._elementOriginalDisabledState.values()) {
3963
+ const element = state && state.element;
3964
+ if (!element) continue;
3965
+ try {
3966
+ if (state.hasDisabledProperty && "disabled" in element) {
3967
+ element.disabled = !!state.disabled;
3968
+ }
3969
+ if (element.getAttribute && element.setAttribute && element.removeAttribute) {
3970
+ if (state.ariaDisabled === null) {
3971
+ element.removeAttribute("aria-disabled");
3972
+ } else {
3973
+ element.setAttribute("aria-disabled", state.ariaDisabled);
3974
+ }
3975
+ }
3976
+ if (element.style) element.style.pointerEvents = state.pointerEvents || "";
3977
+ } catch (error) {
3978
+ void error;
3979
+ }
3980
+ }
3981
+ }
3770
3982
  _setDisabled(key, disabled) {
3771
3983
  const element = this._getElement(key);
3772
3984
  if (!element) return;
3985
+ this._rememberElementDisabledState(key, element);
3773
3986
  if ("disabled" in element) {
3774
3987
  element.disabled = !!disabled;
3775
3988
  return;
@@ -3796,7 +4009,6 @@ var ImageEditor = class {
3796
4009
  * @private
3797
4010
  */
3798
4011
  _updatePlaceholderStatus() {
3799
- if (!this.options.showPlaceholder) return;
3800
4012
  this._setPlaceholderVisible(!this.originalImage);
3801
4013
  }
3802
4014
  /**
@@ -3806,10 +4018,11 @@ var ImageEditor = class {
3806
4018
  * @private
3807
4019
  */
3808
4020
  _setPlaceholderVisible(show) {
3809
- if (this.placeholderElement) this._setElementVisible(this.placeholderElement, show);
4021
+ const shouldShowPlaceholder = !!show && this.options.showPlaceholder !== false;
4022
+ if (this.placeholderElement) this._setElementVisible(this.placeholderElement, shouldShowPlaceholder);
3810
4023
  const canvasVisibilityElement = this._getCanvasVisibilityElement();
3811
4024
  if (canvasVisibilityElement && canvasVisibilityElement !== this.placeholderElement) {
3812
- this._setElementVisible(canvasVisibilityElement, !show);
4025
+ this._setElementVisible(canvasVisibilityElement, !shouldShowPlaceholder);
3813
4026
  }
3814
4027
  }
3815
4028
  _getCanvasVisibilityElement() {
@@ -3888,6 +4101,12 @@ var ImageEditor = class {
3888
4101
  void error;
3889
4102
  }
3890
4103
  if (this._cropRect) this._removeCropRect();
4104
+ this._isApplyingCrop = false;
4105
+ try {
4106
+ this._restoreElementDisabledStates();
4107
+ } catch (error) {
4108
+ void error;
4109
+ }
3891
4110
  if (this.containerElement && this._containerOriginalOverflow) {
3892
4111
  try {
3893
4112
  this._restoreContainerOverflowState();
@@ -3935,6 +4154,7 @@ var ImageEditor = class {
3935
4154
  this._handlersByElementKey = {};
3936
4155
  this._elementCache = {};
3937
4156
  this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
4157
+ this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
3938
4158
  this._clearMaskPlacementMemory();
3939
4159
  this.originalImage = null;
3940
4160
  this.baseImageScale = 1;
@@ -3943,6 +4163,7 @@ var ImageEditor = class {
3943
4163
  this.isAnimating = false;
3944
4164
  this._isLoading = false;
3945
4165
  this._cropMode = false;
4166
+ this._isApplyingCrop = false;
3946
4167
  this._cropRect = null;
3947
4168
  this._cropHandlers = [];
3948
4169
  this._cropPrevEvented = null;
@@ -4045,9 +4266,10 @@ var HistoryManager = class {
4045
4266
  * @param {number} [maxSize=50] - Maximum number of commands to keep in history.
4046
4267
  */
4047
4268
  constructor(maxSize = 50) {
4269
+ const numericMaxSize = Number(maxSize);
4048
4270
  this.history = [];
4049
4271
  this.currentIndex = -1;
4050
- this.maxSize = maxSize;
4272
+ this.maxSize = Number.isFinite(numericMaxSize) && numericMaxSize > 0 ? Math.floor(numericMaxSize) : 50;
4051
4273
  this.pending = Promise.resolve();
4052
4274
  }
4053
4275
  /**