@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.
@@ -39,7 +39,7 @@ var import_fabric = __toESM(require("fabric"));
39
39
  /**
40
40
  * @file image-editor.js
41
41
  * @module image-editor
42
- * @version 1.5.1
42
+ * @version 1.5.2
43
43
  * @author Ben Situ
44
44
  * @license MIT
45
45
  * @description Lightweight canvas-based image editor with masking/transform/export support.
@@ -112,6 +112,7 @@ var ImageEditor = class {
112
112
  imageLoadTimeoutMs: 3e4,
113
113
  exportMultiplier: 1,
114
114
  maxExportPixels: 5e7,
115
+ maxHistorySize: 50,
115
116
  exportImageAreaByDefault: true,
116
117
  defaultMaskWidth: 50,
117
118
  defaultMaskHeight: 80,
@@ -140,6 +141,7 @@ var ImageEditor = class {
140
141
  ...userCrop
141
142
  }
142
143
  };
144
+ this._normalizeOptions();
143
145
  this._fabricLoaded = !!ensureFabric();
144
146
  if (!this._fabricLoaded) {
145
147
  this._reportError("fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.");
@@ -159,16 +161,18 @@ var ImageEditor = class {
159
161
  this._activeOperationToken = null;
160
162
  this.elements = {};
161
163
  this.isImageLoadedToCanvas = false;
162
- this.maxHistorySize = 50;
164
+ this.maxHistorySize = this.options.maxHistorySize;
163
165
  this._handlersByElementKey = {};
164
166
  this._elementCache = {};
165
167
  this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
168
+ this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
166
169
  this._lastMask = null;
167
170
  this._lastMaskInitialLeft = null;
168
171
  this._lastMaskInitialTop = null;
169
172
  this._lastMaskInitialWidth = null;
170
173
  this._lastSnapshot = null;
171
174
  this._cropMode = false;
175
+ this._isApplyingCrop = false;
172
176
  this._cropRect = null;
173
177
  this._cropHandlers = [];
174
178
  this._cropPrevEvented = null;
@@ -265,6 +269,8 @@ var ImageEditor = class {
265
269
  this._activeOperationName = null;
266
270
  this._activeOperationToken = null;
267
271
  this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
272
+ this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
273
+ this._isApplyingCrop = false;
268
274
  this._containerOriginalOverflow = null;
269
275
  this._lastContainerViewportSize = null;
270
276
  this._canvasElementOriginalStyle = null;
@@ -384,6 +390,54 @@ var ImageEditor = class {
384
390
  `ElementIdMap.${deprecatedKey} is deprecated. Use ${canonicalKey} instead. This alias will be removed in v2.0.0.`
385
391
  );
386
392
  }
393
+ _normalizeFiniteNumber(value, fallback) {
394
+ const numericValue = Number(value);
395
+ return Number.isFinite(numericValue) ? numericValue : fallback;
396
+ }
397
+ _normalizePositiveNumber(value, fallback) {
398
+ const numericValue = this._normalizeFiniteNumber(value, fallback);
399
+ return numericValue > 0 ? numericValue : fallback;
400
+ }
401
+ _normalizeNonNegativeNumber(value, fallback) {
402
+ const numericValue = this._normalizeFiniteNumber(value, fallback);
403
+ return numericValue >= 0 ? numericValue : fallback;
404
+ }
405
+ _normalizePositiveInteger(value, fallback) {
406
+ const numericValue = this._normalizePositiveNumber(value, fallback);
407
+ return Math.max(1, Math.floor(numericValue));
408
+ }
409
+ _normalizeOptions() {
410
+ const options = this.options || {};
411
+ options.canvasWidth = this._normalizePositiveNumber(options.canvasWidth, 800);
412
+ options.canvasHeight = this._normalizePositiveNumber(options.canvasHeight, 600);
413
+ options.animationDuration = this._normalizeNonNegativeNumber(options.animationDuration, 300);
414
+ const minScale = this._normalizePositiveNumber(options.minScale, 0.1);
415
+ const maxScale = this._normalizePositiveNumber(options.maxScale, 5);
416
+ if (minScale > maxScale) {
417
+ options.minScale = 0.1;
418
+ options.maxScale = 5;
419
+ } else {
420
+ options.minScale = minScale;
421
+ options.maxScale = maxScale;
422
+ }
423
+ options.scaleStep = this._normalizePositiveNumber(options.scaleStep, 0.05);
424
+ options.rotationStep = this._normalizeFiniteNumber(options.rotationStep, 90);
425
+ options.downsampleMaxWidth = this._normalizePositiveNumber(options.downsampleMaxWidth, 4e3);
426
+ options.downsampleMaxHeight = this._normalizePositiveNumber(options.downsampleMaxHeight, 3e3);
427
+ options.downsampleQuality = options.downsampleQuality == null ? 0.92 : Math.max(0, Math.min(1, this._normalizeFiniteNumber(options.downsampleQuality, 0.92)));
428
+ options.imageLoadTimeoutMs = this._normalizePositiveNumber(options.imageLoadTimeoutMs, 3e4);
429
+ options.exportMultiplier = this._normalizePositiveNumber(options.exportMultiplier, 1);
430
+ options.maxExportPixels = this._normalizePositiveInteger(options.maxExportPixels, 5e7);
431
+ options.maxHistorySize = this._normalizePositiveInteger(options.maxHistorySize, 50);
432
+ options.defaultMaskWidth = this._normalizePositiveNumber(options.defaultMaskWidth, 50);
433
+ options.defaultMaskHeight = this._normalizePositiveNumber(options.defaultMaskHeight, 80);
434
+ options.maskLabelOffset = this._normalizeNonNegativeNumber(options.maskLabelOffset, 3);
435
+ if (options.crop) {
436
+ options.crop.minWidth = this._normalizePositiveNumber(options.crop.minWidth, 100);
437
+ options.crop.minHeight = this._normalizePositiveNumber(options.crop.minHeight, 100);
438
+ options.crop.padding = this._normalizeNonNegativeNumber(options.crop.padding, 10);
439
+ }
440
+ }
387
441
  _reportError(message, error = null) {
388
442
  const handler = this.options && this.options.onError;
389
443
  if (typeof handler !== "function") return;
@@ -400,10 +454,18 @@ var ImageEditor = class {
400
454
  } catch {
401
455
  }
402
456
  }
457
+ _emitSafeCallback(callback, message) {
458
+ if (typeof callback !== "function") return;
459
+ try {
460
+ callback();
461
+ } catch (error) {
462
+ this._reportWarning(message, error);
463
+ }
464
+ }
403
465
  _notifyImageLoaded() {
404
466
  const optionsCallback = this.options && this.options.onImageLoaded;
405
467
  const callback = typeof optionsCallback === "function" ? optionsCallback : this.onImageLoaded;
406
- if (typeof callback === "function") callback();
468
+ this._emitSafeCallback(callback, "onImageLoaded callback failed");
407
469
  }
408
470
  /**
409
471
  * Initializes the Fabric canvas, viewport elements, and selection event handlers.
@@ -642,7 +704,6 @@ var ImageEditor = class {
642
704
  _loadImageFile(file) {
643
705
  if (!this._isSupportedImageFile(file)) {
644
706
  const error = new Error("Selected file is not a supported image");
645
- this._reportError("Selected file is not a supported image", error);
646
707
  return Promise.reject(error);
647
708
  }
648
709
  return new Promise((resolve, reject) => {
@@ -705,6 +766,7 @@ var ImageEditor = class {
705
766
  const isNestedOperation = this._isOwnInternalOperation(options);
706
767
  const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("loadImage");
707
768
  let transaction = null;
769
+ let shouldNotifyImageLoaded;
708
770
  try {
709
771
  this._isLoading = true;
710
772
  this._updateUI();
@@ -793,7 +855,7 @@ var ImageEditor = class {
793
855
  this._updateUI();
794
856
  this.canvas.renderAll();
795
857
  this._lastSnapshot = this._captureCanvasStateOrThrow("loadImage");
796
- this._notifyImageLoaded();
858
+ shouldNotifyImageLoaded = true;
797
859
  } catch (error) {
798
860
  await this._rollbackLoadImageTransaction(
799
861
  transaction,
@@ -805,6 +867,9 @@ var ImageEditor = class {
805
867
  if (!isNestedOperation) this._endBusyOperation(operationToken);
806
868
  if (!this._disposed && this.canvas) this._updateUI();
807
869
  }
870
+ if (shouldNotifyImageLoaded && !this._disposed && this.canvas) {
871
+ this._notifyImageLoaded();
872
+ }
808
873
  }
809
874
  /**
810
875
  * Checks whether there is a loaded image on the current canvas.
@@ -821,7 +886,7 @@ var ImageEditor = class {
821
886
  * @public
822
887
  */
823
888
  isBusy() {
824
- return !!(this.isAnimating || this._cropMode || this._isLoading || this._activeOperationToken || this.animationQueue && this.animationQueue.isBusy());
889
+ return !!(this.isAnimating || this._cropMode || this._isApplyingCrop || this._isLoading || this._activeOperationToken || this.animationQueue && this.animationQueue.isBusy());
825
890
  }
826
891
  /**
827
892
  * Creates an HTMLImageElement from a given data URL.
@@ -1618,7 +1683,23 @@ var ImageEditor = class {
1618
1683
  _getJpegBackgroundColor() {
1619
1684
  const backgroundColor = String(this.options.backgroundColor || "").trim();
1620
1685
  if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return "#ffffff";
1621
- return backgroundColor;
1686
+ return this._isValidCanvasFillStyle(backgroundColor) ? backgroundColor : "#ffffff";
1687
+ }
1688
+ _isValidCanvasFillStyle(color) {
1689
+ try {
1690
+ if (typeof document === "undefined" || !document.createElement) return false;
1691
+ const validationCanvas = document.createElement("canvas");
1692
+ const context = validationCanvas.getContext && validationCanvas.getContext("2d");
1693
+ if (!context) return false;
1694
+ context.fillStyle = "#010203";
1695
+ context.fillStyle = color;
1696
+ if (context.fillStyle !== "#010203") return true;
1697
+ context.fillStyle = "#040506";
1698
+ context.fillStyle = color;
1699
+ return context.fillStyle !== "#040506";
1700
+ } catch {
1701
+ return false;
1702
+ }
1622
1703
  }
1623
1704
  _isTransparentCssColor(color) {
1624
1705
  const normalizedColor = String(color || "").trim().toLowerCase();
@@ -2028,10 +2109,12 @@ var ImageEditor = class {
2028
2109
  async _scaleImageImpl(factor, options = {}) {
2029
2110
  if (!this.originalImage || this._disposed) return;
2030
2111
  if (this.isAnimating) return;
2112
+ const numericFactor = Number(factor);
2113
+ if (!Number.isFinite(numericFactor)) return;
2031
2114
  const saveHistory = options.saveHistory !== false;
2032
2115
  let didStartAnimation = false;
2033
2116
  try {
2034
- factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, factor));
2117
+ factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, numericFactor));
2035
2118
  this.currentScale = factor;
2036
2119
  this.isAnimating = true;
2037
2120
  didStartAnimation = true;
@@ -2097,7 +2180,8 @@ var ImageEditor = class {
2097
2180
  async _rotateImageImpl(degrees, options = {}) {
2098
2181
  if (!this.originalImage || this._disposed) return;
2099
2182
  if (this.isAnimating) return;
2100
- if (isNaN(degrees)) return;
2183
+ const numericDegrees = Number(degrees);
2184
+ if (!Number.isFinite(numericDegrees)) return;
2101
2185
  const saveHistory = options.saveHistory !== false;
2102
2186
  const image = this.originalImage;
2103
2187
  const previousOriginX = image.originX || "left";
@@ -2106,6 +2190,7 @@ var ImageEditor = class {
2106
2190
  let didStartAnimation = false;
2107
2191
  let didCompleteRotation = false;
2108
2192
  try {
2193
+ degrees = numericDegrees;
2109
2194
  this.currentRotation = degrees;
2110
2195
  this.isAnimating = true;
2111
2196
  didStartAnimation = true;
@@ -2609,7 +2694,11 @@ var ImageEditor = class {
2609
2694
  maskConfig.top = top;
2610
2695
  let mask;
2611
2696
  if (typeof maskConfig.fabricGenerator === "function") {
2612
- mask = maskConfig.fabricGenerator(maskConfig, this.canvas, this.options);
2697
+ try {
2698
+ mask = maskConfig.fabricGenerator(maskConfig, this.canvas, this.options);
2699
+ } catch (error) {
2700
+ return rejectInvalidMask("fabricGenerator failed", error);
2701
+ }
2613
2702
  } else {
2614
2703
  switch (shapeType) {
2615
2704
  case "circle":
@@ -2663,6 +2752,17 @@ var ImageEditor = class {
2663
2752
  } catch (error) {
2664
2753
  return rejectInvalidMask("invalid polygon points", error);
2665
2754
  }
2755
+ const uniquePointKeys = new Set(polygonPoints.map((point) => `${point.x}:${point.y}`));
2756
+ if (uniquePointKeys.size !== polygonPoints.length) {
2757
+ return rejectInvalidMask("polygon points must not contain duplicates");
2758
+ }
2759
+ const doubleArea = polygonPoints.reduce((area, point, index) => {
2760
+ const nextPoint = polygonPoints[(index + 1) % polygonPoints.length];
2761
+ return area + point.x * nextPoint.y - nextPoint.x * point.y;
2762
+ }, 0);
2763
+ if (Math.abs(doubleArea) < 1e-6) {
2764
+ return rejectInvalidMask("polygon masks must have a non-zero area");
2765
+ }
2666
2766
  mask = new fabric.Polygon(polygonPoints, {
2667
2767
  left,
2668
2768
  top,
@@ -2741,7 +2841,12 @@ var ImageEditor = class {
2741
2841
  this._updateUI();
2742
2842
  this.canvas.renderAll();
2743
2843
  this.saveState();
2744
- if (typeof maskConfig.onCreate === "function") maskConfig.onCreate(mask, this.canvas);
2844
+ if (typeof maskConfig.onCreate === "function") {
2845
+ this._emitSafeCallback(
2846
+ () => maskConfig.onCreate(mask, this.canvas),
2847
+ "createMask onCreate callback failed"
2848
+ );
2849
+ }
2745
2850
  return mask;
2746
2851
  }
2747
2852
  /**
@@ -2945,8 +3050,15 @@ var ImageEditor = class {
2945
3050
  this._removeLabelForMask(mask);
2946
3051
  let textObject = null;
2947
3052
  if (this.options.label && typeof this.options.label.create === "function") {
2948
- textObject = this.options.label.create(mask, fabric);
2949
- if (!textObject || typeof textObject.set !== "function") {
3053
+ let didLabelCreateThrow = false;
3054
+ try {
3055
+ textObject = this.options.label.create(mask, fabric);
3056
+ } catch (error) {
3057
+ didLabelCreateThrow = true;
3058
+ this._reportWarning("label.create() failed; using the default label", error);
3059
+ textObject = null;
3060
+ }
3061
+ if (!didLabelCreateThrow && (!textObject || typeof textObject.set !== "function")) {
2950
3062
  this._reportWarning("label.create() returned an invalid Fabric object; using the default label");
2951
3063
  textObject = null;
2952
3064
  }
@@ -2967,7 +3079,12 @@ var ImageEditor = class {
2967
3079
  };
2968
3080
  if (this.options.label) {
2969
3081
  if (typeof this.options.label.getText === "function") {
2970
- labelText = this.options.label.getText(mask, this._getMaskCreationIndex(mask));
3082
+ try {
3083
+ labelText = this.options.label.getText(mask, this._getMaskCreationIndex(mask));
3084
+ } catch (error) {
3085
+ this._reportWarning("label.getText() failed; using the mask name", error);
3086
+ labelText = mask.maskName;
3087
+ }
2971
3088
  }
2972
3089
  if (this.options.label.textOptions) {
2973
3090
  Object.assign(textOptions, this.options.label.textOptions);
@@ -3479,6 +3596,38 @@ var ImageEditor = class {
3479
3596
  height
3480
3597
  };
3481
3598
  }
3599
+ _getCropRectRawBounds(cropRect) {
3600
+ if (!cropRect) return { left: NaN, top: NaN, width: NaN, height: NaN };
3601
+ return {
3602
+ left: Number(cropRect.left),
3603
+ top: Number(cropRect.top),
3604
+ width: Number(cropRect.width) * Math.abs(Number(cropRect.scaleX)),
3605
+ height: Number(cropRect.height) * Math.abs(Number(cropRect.scaleY))
3606
+ };
3607
+ }
3608
+ _isValidCropRegion(cropBounds, imageBounds) {
3609
+ if (!cropBounds || !imageBounds) return false;
3610
+ const left = Number(cropBounds.left);
3611
+ const top = Number(cropBounds.top);
3612
+ const width = Number(cropBounds.width);
3613
+ const height = Number(cropBounds.height);
3614
+ const imageLeft = Number(imageBounds.left);
3615
+ const imageTop = Number(imageBounds.top);
3616
+ const imageWidth = Number(imageBounds.width);
3617
+ const imageHeight = Number(imageBounds.height);
3618
+ if (![left, top, width, height, imageLeft, imageTop, imageWidth, imageHeight].every(Number.isFinite)) return false;
3619
+ if (width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return false;
3620
+ const right = left + width;
3621
+ const bottom = top + height;
3622
+ const imageRight = imageLeft + imageWidth;
3623
+ const imageBottom = imageTop + imageHeight;
3624
+ const overlapsImage = left < imageRight && right > imageLeft && top < imageBottom && bottom > imageTop;
3625
+ if (!overlapsImage) return false;
3626
+ const canvasWidth = this.canvas ? Number(this.canvas.getWidth()) : NaN;
3627
+ const canvasHeight = this.canvas ? Number(this.canvas.getHeight()) : NaN;
3628
+ if (!Number.isFinite(canvasWidth) || !Number.isFinite(canvasHeight) || canvasWidth <= 0 || canvasHeight <= 0) return false;
3629
+ return left < canvasWidth && right > 0 && top < canvasHeight && bottom > 0;
3630
+ }
3482
3631
  /**
3483
3632
  * Enters crop mode by creating a resizable crop rectangle above the base image.
3484
3633
  *
@@ -3490,6 +3639,10 @@ var ImageEditor = class {
3490
3639
  */
3491
3640
  enterCropMode() {
3492
3641
  if (!this.canvas || !this.originalImage || this._cropMode) return;
3642
+ if (this._isApplyingCrop) {
3643
+ this._reportWarning("enterCropMode ignored because a crop is already being applied");
3644
+ return;
3645
+ }
3493
3646
  if (!this._canMutateNow("enterCropMode")) return;
3494
3647
  if (!this.isImageLoaded()) return;
3495
3648
  this._removeCropRect();
@@ -3614,6 +3767,10 @@ var ImageEditor = class {
3614
3767
  * @public
3615
3768
  */
3616
3769
  cancelCrop() {
3770
+ if (this._isApplyingCrop) {
3771
+ this._reportWarning("cancelCrop ignored because a crop is already being applied");
3772
+ return;
3773
+ }
3617
3774
  if (!this.canvas || !this._cropMode) return;
3618
3775
  this._removeCropRect();
3619
3776
  this._restoreCropObjectState();
@@ -3637,11 +3794,23 @@ var ImageEditor = class {
3637
3794
  */
3638
3795
  async applyCrop() {
3639
3796
  if (!this.canvas || !this._cropMode || !this._cropRect) return;
3797
+ if (this._isApplyingCrop) {
3798
+ this._reportWarning("applyCrop ignored because a crop is already being applied");
3799
+ return;
3800
+ }
3640
3801
  this._assertIdleForOperation("applyCrop");
3802
+ this._isApplyingCrop = true;
3641
3803
  const operationToken = this._beginBusyOperation("applyCrop");
3642
3804
  const internalOptions = this._withInternalOperationOptions(operationToken);
3643
3805
  try {
3644
3806
  this._cropRect.setCoords();
3807
+ this.originalImage.setCoords();
3808
+ const imageBounds = this.originalImage.getBoundingRect(true, true);
3809
+ const rawCropBounds = this._getCropRectRawBounds(this._cropRect);
3810
+ if (!this._isValidCropRegion(rawCropBounds, imageBounds)) {
3811
+ this._reportWarning("applyCrop: crop region is invalid");
3812
+ return;
3813
+ }
3645
3814
  const rectBounds = this._getCropRectContentBounds(this._cropRect);
3646
3815
  const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
3647
3816
  const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
@@ -3654,7 +3823,13 @@ var ImageEditor = class {
3654
3823
  beforeJson = null;
3655
3824
  }
3656
3825
  if (!beforeJson) {
3657
- this.cancelCrop();
3826
+ this._removeCropRect();
3827
+ this._cropMode = false;
3828
+ this.canvas.selection = !!this._prevSelectionSetting;
3829
+ this._prevSelectionSetting = void 0;
3830
+ this.canvas.discardActiveObject();
3831
+ this._updateUI();
3832
+ this.canvas.renderAll();
3658
3833
  return;
3659
3834
  }
3660
3835
  const preservedMasks = [];
@@ -3730,6 +3905,7 @@ var ImageEditor = class {
3730
3905
  this._updateUI();
3731
3906
  this.canvas.renderAll();
3732
3907
  } finally {
3908
+ this._isApplyingCrop = false;
3733
3909
  this._endBusyOperation(operationToken);
3734
3910
  }
3735
3911
  }
@@ -3761,9 +3937,11 @@ var ImageEditor = class {
3761
3937
  const isInCropMode = !!this._cropMode;
3762
3938
  const isBusy = this.isBusy();
3763
3939
  if (isInCropMode) {
3940
+ const cropInteractionKeys = /* @__PURE__ */ new Set(["canvas", "canvasContainer", "imagePlaceholder", "imgPlaceholder"]);
3764
3941
  for (const key of Object.keys(this.elements || {})) {
3765
3942
  const element = this._getElement(key);
3766
3943
  if (!element) continue;
3944
+ if (cropInteractionKeys.has(key)) continue;
3767
3945
  if (key === "applyCropButton" || key === "cancelCropButton" || key === "applyCropBtn" || key === "cancelCropBtn") {
3768
3946
  this._setDisabled(key, false);
3769
3947
  } else {
@@ -3801,9 +3979,44 @@ var ImageEditor = class {
3801
3979
  * @param {boolean} disabled - If true, disables the element; otherwise enables.
3802
3980
  * @private
3803
3981
  */
3982
+ _rememberElementDisabledState(key, element) {
3983
+ if (!element) return;
3984
+ if (!this._elementOriginalDisabledState) this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
3985
+ if (this._elementOriginalDisabledState.has(key)) return;
3986
+ this._elementOriginalDisabledState.set(key, {
3987
+ element,
3988
+ hasDisabledProperty: "disabled" in element,
3989
+ disabled: "disabled" in element ? !!element.disabled : void 0,
3990
+ ariaDisabled: element.getAttribute ? element.getAttribute("aria-disabled") : null,
3991
+ pointerEvents: element.style ? element.style.pointerEvents || "" : ""
3992
+ });
3993
+ }
3994
+ _restoreElementDisabledStates() {
3995
+ if (!this._elementOriginalDisabledState) return;
3996
+ for (const state of this._elementOriginalDisabledState.values()) {
3997
+ const element = state && state.element;
3998
+ if (!element) continue;
3999
+ try {
4000
+ if (state.hasDisabledProperty && "disabled" in element) {
4001
+ element.disabled = !!state.disabled;
4002
+ }
4003
+ if (element.getAttribute && element.setAttribute && element.removeAttribute) {
4004
+ if (state.ariaDisabled === null) {
4005
+ element.removeAttribute("aria-disabled");
4006
+ } else {
4007
+ element.setAttribute("aria-disabled", state.ariaDisabled);
4008
+ }
4009
+ }
4010
+ if (element.style) element.style.pointerEvents = state.pointerEvents || "";
4011
+ } catch (error) {
4012
+ void error;
4013
+ }
4014
+ }
4015
+ }
3804
4016
  _setDisabled(key, disabled) {
3805
4017
  const element = this._getElement(key);
3806
4018
  if (!element) return;
4019
+ this._rememberElementDisabledState(key, element);
3807
4020
  if ("disabled" in element) {
3808
4021
  element.disabled = !!disabled;
3809
4022
  return;
@@ -3830,7 +4043,6 @@ var ImageEditor = class {
3830
4043
  * @private
3831
4044
  */
3832
4045
  _updatePlaceholderStatus() {
3833
- if (!this.options.showPlaceholder) return;
3834
4046
  this._setPlaceholderVisible(!this.originalImage);
3835
4047
  }
3836
4048
  /**
@@ -3840,10 +4052,11 @@ var ImageEditor = class {
3840
4052
  * @private
3841
4053
  */
3842
4054
  _setPlaceholderVisible(show) {
3843
- if (this.placeholderElement) this._setElementVisible(this.placeholderElement, show);
4055
+ const shouldShowPlaceholder = !!show && this.options.showPlaceholder !== false;
4056
+ if (this.placeholderElement) this._setElementVisible(this.placeholderElement, shouldShowPlaceholder);
3844
4057
  const canvasVisibilityElement = this._getCanvasVisibilityElement();
3845
4058
  if (canvasVisibilityElement && canvasVisibilityElement !== this.placeholderElement) {
3846
- this._setElementVisible(canvasVisibilityElement, !show);
4059
+ this._setElementVisible(canvasVisibilityElement, !shouldShowPlaceholder);
3847
4060
  }
3848
4061
  }
3849
4062
  _getCanvasVisibilityElement() {
@@ -3922,6 +4135,12 @@ var ImageEditor = class {
3922
4135
  void error;
3923
4136
  }
3924
4137
  if (this._cropRect) this._removeCropRect();
4138
+ this._isApplyingCrop = false;
4139
+ try {
4140
+ this._restoreElementDisabledStates();
4141
+ } catch (error) {
4142
+ void error;
4143
+ }
3925
4144
  if (this.containerElement && this._containerOriginalOverflow) {
3926
4145
  try {
3927
4146
  this._restoreContainerOverflowState();
@@ -3969,6 +4188,7 @@ var ImageEditor = class {
3969
4188
  this._handlersByElementKey = {};
3970
4189
  this._elementCache = {};
3971
4190
  this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
4191
+ this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
3972
4192
  this._clearMaskPlacementMemory();
3973
4193
  this.originalImage = null;
3974
4194
  this.baseImageScale = 1;
@@ -3977,6 +4197,7 @@ var ImageEditor = class {
3977
4197
  this.isAnimating = false;
3978
4198
  this._isLoading = false;
3979
4199
  this._cropMode = false;
4200
+ this._isApplyingCrop = false;
3980
4201
  this._cropRect = null;
3981
4202
  this._cropHandlers = [];
3982
4203
  this._cropPrevEvented = null;
@@ -4079,9 +4300,10 @@ var HistoryManager = class {
4079
4300
  * @param {number} [maxSize=50] - Maximum number of commands to keep in history.
4080
4301
  */
4081
4302
  constructor(maxSize = 50) {
4303
+ const numericMaxSize = Number(maxSize);
4082
4304
  this.history = [];
4083
4305
  this.currentIndex = -1;
4084
- this.maxSize = maxSize;
4306
+ this.maxSize = Number.isFinite(numericMaxSize) && numericMaxSize > 0 ? Math.floor(numericMaxSize) : 50;
4085
4307
  this.pending = Promise.resolve();
4086
4308
  }
4087
4309
  /**