@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.
package/image-editor.d.ts CHANGED
@@ -56,6 +56,8 @@ declare module '@bensitu/image-editor' {
56
56
 
57
57
  exportMultiplier?: number;
58
58
  maxExportPixels?: number;
59
+ /** Maximum undo/redo history entries to keep. Large base64 images can make each snapshot expensive. */
60
+ maxHistorySize?: number;
59
61
  exportImageAreaByDefault?: boolean;
60
62
 
61
63
  defaultMaskWidth?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensitu/image-editor",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "Lightweight canvas-based image editor",
5
5
  "main": "./dist/image-editor.cjs",
6
6
  "module": "./dist/image-editor.esm.mjs",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @file image-editor.js
3
3
  * @module image-editor
4
- * @version 1.5.1
4
+ * @version 1.5.2
5
5
  * @author Ben Situ
6
6
  * @license MIT
7
7
  * @description Lightweight canvas-based image editor with masking/transform/export support.
@@ -133,6 +133,7 @@ function ensureFabric() {
133
133
  * @param {number} [options.imageLoadTimeoutMs=30000] - Timeout for image decode operations.
134
134
  * @param {number} [options.exportMultiplier=1] - Scale output image by this multiplier on export.
135
135
  * @param {number} [options.maxExportPixels=50000000] - Maximum output pixels allowed per export.
136
+ * @param {number} [options.maxHistorySize=50] - Maximum undo/redo history entries to keep. Large base64 images can make each snapshot expensive.
136
137
  * @param {boolean} [options.exportImageAreaByDefault=true] - Export only the image area (clipped to masks).
137
138
  * @param {number} [options.defaultMaskWidth=50] - Default width of new masks.
138
139
  * @param {number} [options.defaultMaskHeight=80] - Default height of new masks.
@@ -204,6 +205,7 @@ function ensureFabric() {
204
205
 
205
206
  exportMultiplier: 1,
206
207
  maxExportPixels: 50000000,
208
+ maxHistorySize: 50,
207
209
  exportImageAreaByDefault: true,
208
210
 
209
211
  defaultMaskWidth: 50,
@@ -236,6 +238,7 @@ function ensureFabric() {
236
238
  ...userCrop
237
239
  }
238
240
  };
241
+ this._normalizeOptions();
239
242
 
240
243
  // Verify that Fabric.js is present before any canvas work starts.
241
244
  this._fabricLoaded = !!ensureFabric();
@@ -260,11 +263,12 @@ function ensureFabric() {
260
263
  this._activeOperationToken = null;
261
264
  this.elements = {};
262
265
  this.isImageLoadedToCanvas = false;
263
- this.maxHistorySize = 50;
266
+ this.maxHistorySize = this.options.maxHistorySize;
264
267
 
265
268
  this._handlersByElementKey = {};
266
269
  this._elementCache = {};
267
270
  this._elementOriginalPointerEvents = new Map();
271
+ this._elementOriginalDisabledState = new Map();
268
272
 
269
273
  this._lastMask = null;
270
274
  this._lastMaskInitialLeft = null;
@@ -273,6 +277,7 @@ function ensureFabric() {
273
277
  this._lastSnapshot = null;
274
278
 
275
279
  this._cropMode = false;
280
+ this._isApplyingCrop = false;
276
281
  this._cropRect = null;
277
282
  this._cropHandlers = [];
278
283
  this._cropPrevEvented = null;
@@ -378,6 +383,8 @@ function ensureFabric() {
378
383
  this._activeOperationName = null;
379
384
  this._activeOperationToken = null;
380
385
  this._elementOriginalPointerEvents = new Map();
386
+ this._elementOriginalDisabledState = new Map();
387
+ this._isApplyingCrop = false;
381
388
  this._containerOriginalOverflow = null;
382
389
  this._lastContainerViewportSize = null;
383
390
  this._canvasElementOriginalStyle = null;
@@ -513,6 +520,66 @@ function ensureFabric() {
513
520
  );
514
521
  }
515
522
 
523
+ _normalizeFiniteNumber(value, fallback) {
524
+ const numericValue = Number(value);
525
+ return Number.isFinite(numericValue) ? numericValue : fallback;
526
+ }
527
+
528
+ _normalizePositiveNumber(value, fallback) {
529
+ const numericValue = this._normalizeFiniteNumber(value, fallback);
530
+ return numericValue > 0 ? numericValue : fallback;
531
+ }
532
+
533
+ _normalizeNonNegativeNumber(value, fallback) {
534
+ const numericValue = this._normalizeFiniteNumber(value, fallback);
535
+ return numericValue >= 0 ? numericValue : fallback;
536
+ }
537
+
538
+ _normalizePositiveInteger(value, fallback) {
539
+ const numericValue = this._normalizePositiveNumber(value, fallback);
540
+ return Math.max(1, Math.floor(numericValue));
541
+ }
542
+
543
+ _normalizeOptions() {
544
+ const options = this.options || {};
545
+ options.canvasWidth = this._normalizePositiveNumber(options.canvasWidth, 800);
546
+ options.canvasHeight = this._normalizePositiveNumber(options.canvasHeight, 600);
547
+ options.animationDuration = this._normalizeNonNegativeNumber(options.animationDuration, 300);
548
+
549
+ const minScale = this._normalizePositiveNumber(options.minScale, 0.1);
550
+ const maxScale = this._normalizePositiveNumber(options.maxScale, 5);
551
+ if (minScale > maxScale) {
552
+ options.minScale = 0.1;
553
+ options.maxScale = 5;
554
+ } else {
555
+ options.minScale = minScale;
556
+ options.maxScale = maxScale;
557
+ }
558
+ options.scaleStep = this._normalizePositiveNumber(options.scaleStep, 0.05);
559
+ options.rotationStep = this._normalizeFiniteNumber(options.rotationStep, 90);
560
+
561
+ options.downsampleMaxWidth = this._normalizePositiveNumber(options.downsampleMaxWidth, 4000);
562
+ options.downsampleMaxHeight = this._normalizePositiveNumber(options.downsampleMaxHeight, 3000);
563
+ options.downsampleQuality = options.downsampleQuality == null
564
+ ? 0.92
565
+ : Math.max(0, Math.min(1, this._normalizeFiniteNumber(options.downsampleQuality, 0.92)));
566
+ options.imageLoadTimeoutMs = this._normalizePositiveNumber(options.imageLoadTimeoutMs, 30000);
567
+
568
+ options.exportMultiplier = this._normalizePositiveNumber(options.exportMultiplier, 1);
569
+ options.maxExportPixels = this._normalizePositiveInteger(options.maxExportPixels, 50000000);
570
+ options.maxHistorySize = this._normalizePositiveInteger(options.maxHistorySize, 50);
571
+
572
+ options.defaultMaskWidth = this._normalizePositiveNumber(options.defaultMaskWidth, 50);
573
+ options.defaultMaskHeight = this._normalizePositiveNumber(options.defaultMaskHeight, 80);
574
+ options.maskLabelOffset = this._normalizeNonNegativeNumber(options.maskLabelOffset, 3);
575
+
576
+ if (options.crop) {
577
+ options.crop.minWidth = this._normalizePositiveNumber(options.crop.minWidth, 100);
578
+ options.crop.minHeight = this._normalizePositiveNumber(options.crop.minHeight, 100);
579
+ options.crop.padding = this._normalizeNonNegativeNumber(options.crop.padding, 10);
580
+ }
581
+ }
582
+
516
583
  _reportError(message, error = null) {
517
584
  const handler = this.options && this.options.onError;
518
585
  if (typeof handler !== 'function') return;
@@ -535,10 +602,19 @@ function ensureFabric() {
535
602
  }
536
603
  }
537
604
 
605
+ _emitSafeCallback(callback, message) {
606
+ if (typeof callback !== 'function') return;
607
+ try {
608
+ callback();
609
+ } catch (error) {
610
+ this._reportWarning(message, error);
611
+ }
612
+ }
613
+
538
614
  _notifyImageLoaded() {
539
615
  const optionsCallback = this.options && this.options.onImageLoaded;
540
616
  const callback = typeof optionsCallback === 'function' ? optionsCallback : this.onImageLoaded;
541
- if (typeof callback === 'function') callback();
617
+ this._emitSafeCallback(callback, 'onImageLoaded callback failed');
542
618
  }
543
619
 
544
620
  /**
@@ -804,7 +880,6 @@ function ensureFabric() {
804
880
  _loadImageFile(file) {
805
881
  if (!this._isSupportedImageFile(file)) {
806
882
  const error = new Error('Selected file is not a supported image');
807
- this._reportError('Selected file is not a supported image', error);
808
883
  return Promise.reject(error);
809
884
  }
810
885
 
@@ -878,6 +953,7 @@ function ensureFabric() {
878
953
  ? this._getInternalOperationToken(options)
879
954
  : this._beginBusyOperation('loadImage');
880
955
  let transaction = null;
956
+ let shouldNotifyImageLoaded;
881
957
 
882
958
  try {
883
959
  this._isLoading = true;
@@ -981,8 +1057,7 @@ function ensureFabric() {
981
1057
  this._updateUI();
982
1058
  this.canvas.renderAll();
983
1059
  this._lastSnapshot = this._captureCanvasStateOrThrow('loadImage');
984
-
985
- this._notifyImageLoaded();
1060
+ shouldNotifyImageLoaded = true;
986
1061
  } catch (error) {
987
1062
  await this._rollbackLoadImageTransaction(
988
1063
  transaction,
@@ -994,6 +1069,9 @@ function ensureFabric() {
994
1069
  if (!isNestedOperation) this._endBusyOperation(operationToken);
995
1070
  if (!this._disposed && this.canvas) this._updateUI();
996
1071
  }
1072
+ if (shouldNotifyImageLoaded && !this._disposed && this.canvas) {
1073
+ this._notifyImageLoaded();
1074
+ }
997
1075
  }
998
1076
 
999
1077
  /**
@@ -1021,6 +1099,7 @@ function ensureFabric() {
1021
1099
  return !!(
1022
1100
  this.isAnimating ||
1023
1101
  this._cropMode ||
1102
+ this._isApplyingCrop ||
1024
1103
  this._isLoading ||
1025
1104
  this._activeOperationToken ||
1026
1105
  (this.animationQueue && this.animationQueue.isBusy())
@@ -1908,7 +1987,24 @@ function ensureFabric() {
1908
1987
  _getJpegBackgroundColor() {
1909
1988
  const backgroundColor = String(this.options.backgroundColor || '').trim();
1910
1989
  if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return '#ffffff';
1911
- return backgroundColor;
1990
+ return this._isValidCanvasFillStyle(backgroundColor) ? backgroundColor : '#ffffff';
1991
+ }
1992
+
1993
+ _isValidCanvasFillStyle(color) {
1994
+ try {
1995
+ if (typeof document === 'undefined' || !document.createElement) return false;
1996
+ const validationCanvas = document.createElement('canvas');
1997
+ const context = validationCanvas.getContext && validationCanvas.getContext('2d');
1998
+ if (!context) return false;
1999
+ context.fillStyle = '#010203';
2000
+ context.fillStyle = color;
2001
+ if (context.fillStyle !== '#010203') return true;
2002
+ context.fillStyle = '#040506';
2003
+ context.fillStyle = color;
2004
+ return context.fillStyle !== '#040506';
2005
+ } catch {
2006
+ return false;
2007
+ }
1912
2008
  }
1913
2009
 
1914
2010
  _isTransparentCssColor(color) {
@@ -2364,10 +2460,12 @@ function ensureFabric() {
2364
2460
  async _scaleImageImpl(factor, options = {}) {
2365
2461
  if (!this.originalImage || this._disposed) return;
2366
2462
  if (this.isAnimating) return;
2463
+ const numericFactor = Number(factor);
2464
+ if (!Number.isFinite(numericFactor)) return;
2367
2465
  const saveHistory = options.saveHistory !== false;
2368
2466
  let didStartAnimation = false;
2369
2467
  try {
2370
- factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, factor));
2468
+ factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, numericFactor));
2371
2469
  this.currentScale = factor;
2372
2470
  this.isAnimating = true;
2373
2471
  didStartAnimation = true;
@@ -2442,7 +2540,8 @@ function ensureFabric() {
2442
2540
  async _rotateImageImpl(degrees, options = {}) {
2443
2541
  if (!this.originalImage || this._disposed) return;
2444
2542
  if (this.isAnimating) return;
2445
- if (isNaN(degrees)) return;
2543
+ const numericDegrees = Number(degrees);
2544
+ if (!Number.isFinite(numericDegrees)) return;
2446
2545
  const saveHistory = options.saveHistory !== false;
2447
2546
  const image = this.originalImage;
2448
2547
  const previousOriginX = image.originX || 'left';
@@ -2451,6 +2550,7 @@ function ensureFabric() {
2451
2550
  let didStartAnimation = false;
2452
2551
  let didCompleteRotation = false;
2453
2552
  try {
2553
+ degrees = numericDegrees;
2454
2554
  this.currentRotation = degrees;
2455
2555
  this.isAnimating = true;
2456
2556
  didStartAnimation = true;
@@ -3018,7 +3118,11 @@ function ensureFabric() {
3018
3118
 
3019
3119
  let mask;
3020
3120
  if (typeof maskConfig.fabricGenerator === 'function') {
3021
- mask = maskConfig.fabricGenerator(maskConfig, this.canvas, this.options);
3121
+ try {
3122
+ mask = maskConfig.fabricGenerator(maskConfig, this.canvas, this.options);
3123
+ } catch (error) {
3124
+ return rejectInvalidMask('fabricGenerator failed', error);
3125
+ }
3022
3126
  } else {
3023
3127
  switch (shapeType) {
3024
3128
  case 'circle':
@@ -3070,6 +3174,17 @@ function ensureFabric() {
3070
3174
  } catch (error) {
3071
3175
  return rejectInvalidMask('invalid polygon points', error);
3072
3176
  }
3177
+ const uniquePointKeys = new Set(polygonPoints.map(point => `${point.x}:${point.y}`));
3178
+ if (uniquePointKeys.size !== polygonPoints.length) {
3179
+ return rejectInvalidMask('polygon points must not contain duplicates');
3180
+ }
3181
+ const doubleArea = polygonPoints.reduce((area, point, index) => {
3182
+ const nextPoint = polygonPoints[(index + 1) % polygonPoints.length];
3183
+ return area + point.x * nextPoint.y - nextPoint.x * point.y;
3184
+ }, 0);
3185
+ if (Math.abs(doubleArea) < 0.000001) {
3186
+ return rejectInvalidMask('polygon masks must have a non-zero area');
3187
+ }
3073
3188
  mask = new fabric.Polygon(polygonPoints, {
3074
3189
  left, top,
3075
3190
  fill: maskConfig.color,
@@ -3154,7 +3269,12 @@ function ensureFabric() {
3154
3269
  this.canvas.renderAll();
3155
3270
  this.saveState();
3156
3271
 
3157
- if (typeof maskConfig.onCreate === 'function') maskConfig.onCreate(mask, this.canvas);
3272
+ if (typeof maskConfig.onCreate === 'function') {
3273
+ this._emitSafeCallback(
3274
+ () => maskConfig.onCreate(mask, this.canvas),
3275
+ 'createMask onCreate callback failed'
3276
+ );
3277
+ }
3158
3278
  return mask;
3159
3279
  }
3160
3280
 
@@ -3369,8 +3489,15 @@ function ensureFabric() {
3369
3489
  this._removeLabelForMask(mask);
3370
3490
  let textObject = null;
3371
3491
  if (this.options.label && typeof this.options.label.create === 'function') {
3372
- textObject = this.options.label.create(mask, fabric);
3373
- if (!textObject || typeof textObject.set !== 'function') {
3492
+ let didLabelCreateThrow = false;
3493
+ try {
3494
+ textObject = this.options.label.create(mask, fabric);
3495
+ } catch (error) {
3496
+ didLabelCreateThrow = true;
3497
+ this._reportWarning('label.create() failed; using the default label', error);
3498
+ textObject = null;
3499
+ }
3500
+ if (!didLabelCreateThrow && (!textObject || typeof textObject.set !== 'function')) {
3374
3501
  this._reportWarning('label.create() returned an invalid Fabric object; using the default label');
3375
3502
  textObject = null;
3376
3503
  }
@@ -3391,7 +3518,12 @@ function ensureFabric() {
3391
3518
  };
3392
3519
  if (this.options.label) {
3393
3520
  if (typeof this.options.label.getText === 'function') {
3394
- labelText = this.options.label.getText(mask, this._getMaskCreationIndex(mask));
3521
+ try {
3522
+ labelText = this.options.label.getText(mask, this._getMaskCreationIndex(mask));
3523
+ } catch (error) {
3524
+ this._reportWarning('label.getText() failed; using the mask name', error);
3525
+ labelText = mask.maskName;
3526
+ }
3395
3527
  }
3396
3528
  // Merge external styles
3397
3529
  if (this.options.label.textOptions) {
@@ -3940,6 +4072,42 @@ function ensureFabric() {
3940
4072
  };
3941
4073
  }
3942
4074
 
4075
+ _getCropRectRawBounds(cropRect) {
4076
+ if (!cropRect) return { left: NaN, top: NaN, width: NaN, height: NaN };
4077
+ return {
4078
+ left: Number(cropRect.left),
4079
+ top: Number(cropRect.top),
4080
+ width: Number(cropRect.width) * Math.abs(Number(cropRect.scaleX)),
4081
+ height: Number(cropRect.height) * Math.abs(Number(cropRect.scaleY))
4082
+ };
4083
+ }
4084
+
4085
+ _isValidCropRegion(cropBounds, imageBounds) {
4086
+ if (!cropBounds || !imageBounds) return false;
4087
+ const left = Number(cropBounds.left);
4088
+ const top = Number(cropBounds.top);
4089
+ const width = Number(cropBounds.width);
4090
+ const height = Number(cropBounds.height);
4091
+ const imageLeft = Number(imageBounds.left);
4092
+ const imageTop = Number(imageBounds.top);
4093
+ const imageWidth = Number(imageBounds.width);
4094
+ const imageHeight = Number(imageBounds.height);
4095
+ if (![left, top, width, height, imageLeft, imageTop, imageWidth, imageHeight].every(Number.isFinite)) return false;
4096
+ if (width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return false;
4097
+
4098
+ const right = left + width;
4099
+ const bottom = top + height;
4100
+ const imageRight = imageLeft + imageWidth;
4101
+ const imageBottom = imageTop + imageHeight;
4102
+ const overlapsImage = left < imageRight && right > imageLeft && top < imageBottom && bottom > imageTop;
4103
+ if (!overlapsImage) return false;
4104
+
4105
+ const canvasWidth = this.canvas ? Number(this.canvas.getWidth()) : NaN;
4106
+ const canvasHeight = this.canvas ? Number(this.canvas.getHeight()) : NaN;
4107
+ if (!Number.isFinite(canvasWidth) || !Number.isFinite(canvasHeight) || canvasWidth <= 0 || canvasHeight <= 0) return false;
4108
+ return left < canvasWidth && right > 0 && top < canvasHeight && bottom > 0;
4109
+ }
4110
+
3943
4111
  /**
3944
4112
  * Enters crop mode by creating a resizable crop rectangle above the base image.
3945
4113
  *
@@ -3951,6 +4119,10 @@ function ensureFabric() {
3951
4119
  */
3952
4120
  enterCropMode() {
3953
4121
  if (!this.canvas || !this.originalImage || this._cropMode) return;
4122
+ if (this._isApplyingCrop) {
4123
+ this._reportWarning('enterCropMode ignored because a crop is already being applied');
4124
+ return;
4125
+ }
3954
4126
  if (!this._canMutateNow('enterCropMode')) return;
3955
4127
  if (!this.isImageLoaded()) return;
3956
4128
  this._removeCropRect();
@@ -4090,6 +4262,10 @@ function ensureFabric() {
4090
4262
  * @public
4091
4263
  */
4092
4264
  cancelCrop() {
4265
+ if (this._isApplyingCrop) {
4266
+ this._reportWarning('cancelCrop ignored because a crop is already being applied');
4267
+ return;
4268
+ }
4093
4269
  if (!this.canvas || !this._cropMode) return;
4094
4270
  this._removeCropRect();
4095
4271
  this._restoreCropObjectState();
@@ -4116,13 +4292,25 @@ function ensureFabric() {
4116
4292
  */
4117
4293
  async applyCrop() {
4118
4294
  if (!this.canvas || !this._cropMode || !this._cropRect) return;
4295
+ if (this._isApplyingCrop) {
4296
+ this._reportWarning('applyCrop ignored because a crop is already being applied');
4297
+ return;
4298
+ }
4119
4299
  this._assertIdleForOperation('applyCrop');
4300
+ this._isApplyingCrop = true;
4120
4301
  const operationToken = this._beginBusyOperation('applyCrop');
4121
4302
  const internalOptions = this._withInternalOperationOptions(operationToken);
4122
4303
 
4123
4304
  try {
4124
4305
  // Fabric does not update control coordinates automatically after programmatic transforms.
4125
4306
  this._cropRect.setCoords();
4307
+ this.originalImage.setCoords();
4308
+ const imageBounds = this.originalImage.getBoundingRect(true, true);
4309
+ const rawCropBounds = this._getCropRectRawBounds(this._cropRect);
4310
+ if (!this._isValidCropRegion(rawCropBounds, imageBounds)) {
4311
+ this._reportWarning('applyCrop: crop region is invalid');
4312
+ return;
4313
+ }
4126
4314
  const rectBounds = this._getCropRectContentBounds(this._cropRect);
4127
4315
 
4128
4316
  const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
@@ -4138,7 +4326,13 @@ function ensureFabric() {
4138
4326
  beforeJson = null;
4139
4327
  }
4140
4328
  if (!beforeJson) {
4141
- this.cancelCrop();
4329
+ this._removeCropRect();
4330
+ this._cropMode = false;
4331
+ this.canvas.selection = !!this._prevSelectionSetting;
4332
+ this._prevSelectionSetting = undefined;
4333
+ this.canvas.discardActiveObject();
4334
+ this._updateUI();
4335
+ this.canvas.renderAll();
4142
4336
  return;
4143
4337
  }
4144
4338
 
@@ -4232,6 +4426,7 @@ function ensureFabric() {
4232
4426
  this._updateUI();
4233
4427
  this.canvas.renderAll();
4234
4428
  } finally {
4429
+ this._isApplyingCrop = false;
4235
4430
  this._endBusyOperation(operationToken);
4236
4431
  }
4237
4432
  }
@@ -4268,10 +4463,12 @@ function ensureFabric() {
4268
4463
  const isBusy = this.isBusy();
4269
4464
 
4270
4465
  if (isInCropMode) {
4271
- // Disable all controls except the crop action buttons while crop mode is active.
4466
+ // Disable operation controls while keeping canvas interaction and viewport scrolling available.
4467
+ const cropInteractionKeys = new Set(['canvas', 'canvasContainer', 'imagePlaceholder', 'imgPlaceholder']);
4272
4468
  for (const key of Object.keys(this.elements || {})) {
4273
4469
  const element = this._getElement(key);
4274
4470
  if (!element) continue;
4471
+ if (cropInteractionKeys.has(key)) continue;
4275
4472
  if (key === 'applyCropButton' || key === 'cancelCropButton' || key === 'applyCropBtn' || key === 'cancelCropBtn') {
4276
4473
  this._setDisabled(key, false);
4277
4474
  } else {
@@ -4311,9 +4508,44 @@ function ensureFabric() {
4311
4508
  * @param {boolean} disabled - If true, disables the element; otherwise enables.
4312
4509
  * @private
4313
4510
  */
4511
+ _rememberElementDisabledState(key, element) {
4512
+ if (!element) return;
4513
+ if (!this._elementOriginalDisabledState) this._elementOriginalDisabledState = new Map();
4514
+ if (this._elementOriginalDisabledState.has(key)) return;
4515
+ this._elementOriginalDisabledState.set(key, {
4516
+ element,
4517
+ hasDisabledProperty: 'disabled' in element,
4518
+ disabled: ('disabled' in element) ? !!element.disabled : undefined,
4519
+ ariaDisabled: element.getAttribute ? element.getAttribute('aria-disabled') : null,
4520
+ pointerEvents: element.style ? (element.style.pointerEvents || '') : ''
4521
+ });
4522
+ }
4523
+
4524
+ _restoreElementDisabledStates() {
4525
+ if (!this._elementOriginalDisabledState) return;
4526
+ for (const state of this._elementOriginalDisabledState.values()) {
4527
+ const element = state && state.element;
4528
+ if (!element) continue;
4529
+ try {
4530
+ if (state.hasDisabledProperty && 'disabled' in element) {
4531
+ element.disabled = !!state.disabled;
4532
+ }
4533
+ if (element.getAttribute && element.setAttribute && element.removeAttribute) {
4534
+ if (state.ariaDisabled === null) {
4535
+ element.removeAttribute('aria-disabled');
4536
+ } else {
4537
+ element.setAttribute('aria-disabled', state.ariaDisabled);
4538
+ }
4539
+ }
4540
+ if (element.style) element.style.pointerEvents = state.pointerEvents || '';
4541
+ } catch (error) { void error; }
4542
+ }
4543
+ }
4544
+
4314
4545
  _setDisabled(key, disabled) {
4315
4546
  const element = this._getElement(key);
4316
4547
  if (!element) return;
4548
+ this._rememberElementDisabledState(key, element);
4317
4549
  if ('disabled' in element) {
4318
4550
  element.disabled = !!disabled;
4319
4551
  return;
@@ -4343,7 +4575,6 @@ function ensureFabric() {
4343
4575
  * @private
4344
4576
  */
4345
4577
  _updatePlaceholderStatus() {
4346
- if (!this.options.showPlaceholder) return;
4347
4578
  this._setPlaceholderVisible(!this.originalImage);
4348
4579
  }
4349
4580
 
@@ -4354,10 +4585,11 @@ function ensureFabric() {
4354
4585
  * @private
4355
4586
  */
4356
4587
  _setPlaceholderVisible(show) {
4357
- if (this.placeholderElement) this._setElementVisible(this.placeholderElement, show);
4588
+ const shouldShowPlaceholder = !!show && this.options.showPlaceholder !== false;
4589
+ if (this.placeholderElement) this._setElementVisible(this.placeholderElement, shouldShowPlaceholder);
4358
4590
  const canvasVisibilityElement = this._getCanvasVisibilityElement();
4359
4591
  if (canvasVisibilityElement && canvasVisibilityElement !== this.placeholderElement) {
4360
- this._setElementVisible(canvasVisibilityElement, !show);
4592
+ this._setElementVisible(canvasVisibilityElement, !shouldShowPlaceholder);
4361
4593
  }
4362
4594
  }
4363
4595
 
@@ -4443,6 +4675,9 @@ function ensureFabric() {
4443
4675
  } catch (error) { void error; }
4444
4676
 
4445
4677
  if (this._cropRect) this._removeCropRect();
4678
+ this._isApplyingCrop = false;
4679
+
4680
+ try { this._restoreElementDisabledStates(); } catch (error) { void error; }
4446
4681
 
4447
4682
  if (this.containerElement && this._containerOriginalOverflow) {
4448
4683
  try { this._restoreContainerOverflowState(); } catch (error) { void error; }
@@ -4480,6 +4715,7 @@ function ensureFabric() {
4480
4715
  this._handlersByElementKey = {};
4481
4716
  this._elementCache = {};
4482
4717
  this._elementOriginalPointerEvents = new Map();
4718
+ this._elementOriginalDisabledState = new Map();
4483
4719
  this._clearMaskPlacementMemory();
4484
4720
  this.originalImage = null;
4485
4721
  this.baseImageScale = 1;
@@ -4488,6 +4724,7 @@ function ensureFabric() {
4488
4724
  this.isAnimating = false;
4489
4725
  this._isLoading = false;
4490
4726
  this._cropMode = false;
4727
+ this._isApplyingCrop = false;
4491
4728
  this._cropRect = null;
4492
4729
  this._cropHandlers = [];
4493
4730
  this._cropPrevEvented = null;
@@ -4660,12 +4897,13 @@ function ensureFabric() {
4660
4897
  * @param {number} [maxSize=50] - Maximum number of commands to keep in history.
4661
4898
  */
4662
4899
  constructor(maxSize = 50) {
4900
+ const numericMaxSize = Number(maxSize);
4663
4901
  /** @type {Array<Command>} */
4664
4902
  this.history = [];
4665
4903
  /** @type {number} */
4666
4904
  this.currentIndex = -1;
4667
4905
  /** @type {number} */
4668
- this.maxSize = maxSize;
4906
+ this.maxSize = Number.isFinite(numericMaxSize) && numericMaxSize > 0 ? Math.floor(numericMaxSize) : 50;
4669
4907
  /** @type {Promise<void>} */
4670
4908
  this.pending = Promise.resolve();
4671
4909
  }