@bensitu/image-editor 1.3.0 → 1.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensitu/image-editor",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Lightweight canvas-based image editor",
5
5
  "main": "./dist/image-editor.js",
6
6
  "module": "./dist/image-editor.esm.mjs",
@@ -16,7 +16,7 @@
16
16
  "dev": "node esbuild.config.mjs --watch",
17
17
  "build": "node esbuild.config.mjs",
18
18
  "build:babel": "babel src --out-dir dist --copy-files",
19
- "lint": "eslint src --ext .js",
19
+ "lint": "eslint src",
20
20
  "test": "npm run build && node --test tests/image-editor.test.mjs tests/package.test.mjs"
21
21
  },
22
22
  "repository": {
@@ -33,6 +33,9 @@
33
33
  "peerDependencies": {
34
34
  "fabric": "^5.5.2"
35
35
  },
36
+ "overrides": {
37
+ "canvas": "^3.2.3"
38
+ },
36
39
  "publishConfig": {
37
40
  "access": "public"
38
41
  },
@@ -40,9 +43,11 @@
40
43
  "@babel/cli": "^7.22.9",
41
44
  "@babel/core": "^7.22.9",
42
45
  "@babel/preset-env": "^7.22.9",
43
- "esbuild": "^0.19.12",
46
+ "@eslint/js": "^10.0.1",
47
+ "esbuild": "^0.28.0",
44
48
  "esbuild-plugin-babel": "^0.2.3",
45
- "eslint": "^8.49.0"
49
+ "eslint": "^10.4.0",
50
+ "globals": "^17.6.0"
46
51
  },
47
52
  "browserslist": [
48
53
  "Chrome >= 100",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @file image-editor.js
3
3
  * @module image-editor
4
- * @version 1.3.0
4
+ * @version 1.3.1
5
5
  * @author Ben Situ
6
6
  * @license MIT
7
7
  * @description Lightweight canvas-based image editor with masking/transform/export support.
@@ -868,21 +868,36 @@ function ensureFabric() {
868
868
  };
869
869
  }
870
870
 
871
+ let width = Math.max(1, Math.floor(this.containerElement.clientWidth || this.options.canvasWidth || 1));
872
+ let height = Math.max(1, Math.floor(this.containerElement.clientHeight || this.options.canvasHeight || 1));
873
+
871
874
  if (this._hasFixedContainerScrollbars()) {
872
- return {
873
- width: Math.max(1, Math.floor(this.containerElement.clientWidth || this.options.canvasWidth || 1)),
874
- height: Math.max(1, Math.floor(this.containerElement.clientHeight || this.options.canvasHeight || 1))
875
- };
875
+ return { width, height };
876
876
  }
877
877
 
878
- const width = Math.max(1, Math.floor(this.containerElement.clientWidth || this.options.canvasWidth || 1));
879
- const height = Math.max(1, Math.floor(this.containerElement.clientHeight || this.options.canvasHeight || 1));
878
+ const overflow = this._getContainerOverflowValues();
879
+ const canScrollX = overflow.x.some(value => value === 'auto' || value === 'scroll');
880
+ const canScrollY = overflow.y.some(value => value === 'auto' || value === 'scroll');
881
+ const hasHorizontalScrollbar = canScrollX && this.containerElement.scrollWidth > this.containerElement.clientWidth;
882
+ const hasVerticalScrollbar = canScrollY && this.containerElement.scrollHeight > this.containerElement.clientHeight;
883
+
884
+ if (hasHorizontalScrollbar || hasVerticalScrollbar) {
885
+ const scrollbar = this._getScrollbarSize();
886
+ if (hasVerticalScrollbar) width += scrollbar.width;
887
+ if (hasHorizontalScrollbar) height += scrollbar.height;
888
+ }
880
889
 
881
890
  return { width, height };
882
891
  }
883
892
 
884
- _hasFixedContainerScrollbars() {
885
- if (!this.containerElement) return false;
893
+ /**
894
+ * Reads inline and computed overflow values for both scroll axes.
895
+ *
896
+ * @returns {{x:string[], y:string[]}} Overflow values grouped by axis.
897
+ * @private
898
+ */
899
+ _getContainerOverflowValues() {
900
+ if (!this.containerElement) return { x: [], y: [] };
886
901
  const inlineOverflow = this.containerElement.style.overflow;
887
902
  const inlineOverflowX = this.containerElement.style.overflowX;
888
903
  const inlineOverflowY = this.containerElement.style.overflowY;
@@ -897,8 +912,16 @@ function ensureFabric() {
897
912
  computedOverflowY = style.overflowY;
898
913
  }
899
914
 
900
- return [inlineOverflow, inlineOverflowX, inlineOverflowY, computedOverflow, computedOverflowX, computedOverflowY]
901
- .some(value => value === 'scroll');
915
+ return {
916
+ x: [inlineOverflow, inlineOverflowX, computedOverflow, computedOverflowX],
917
+ y: [inlineOverflow, inlineOverflowY, computedOverflow, computedOverflowY]
918
+ };
919
+ }
920
+
921
+ _hasFixedContainerScrollbars() {
922
+ if (!this.containerElement) return false;
923
+ const overflow = this._getContainerOverflowValues();
924
+ return [...overflow.x, ...overflow.y].some(value => value === 'scroll');
902
925
  }
903
926
 
904
927
  _getScrollbarSize() {
@@ -948,8 +971,8 @@ function ensureFabric() {
948
971
  const scrollbar = this._getScrollbarSize();
949
972
  let hasVertical = false;
950
973
  let hasHorizontal = false;
951
- let effectiveWidth = viewport.width;
952
- let effectiveHeight = viewport.height;
974
+ let effectiveWidth;
975
+ let effectiveHeight;
953
976
 
954
977
  for (let i = 0; i < 4; i += 1) {
955
978
  effectiveWidth = Math.max(1, viewport.width - (hasVertical ? scrollbar.width : 0));
@@ -1000,8 +1023,8 @@ function ensureFabric() {
1000
1023
  let scale = 1;
1001
1024
  let contentWidth = imageWidth;
1002
1025
  let contentHeight = imageHeight;
1003
- let effectiveWidth = viewport.width;
1004
- let effectiveHeight = viewport.height;
1026
+ let effectiveWidth;
1027
+ let effectiveHeight;
1005
1028
 
1006
1029
  for (let i = 0; i < 4; i += 1) {
1007
1030
  effectiveWidth = Math.max(1, viewport.width - (hasVertical ? scrollbar.width : 0));
@@ -1062,26 +1085,36 @@ function ensureFabric() {
1062
1085
  _withNormalizedMaskStyles(callback) {
1063
1086
  if (!this.canvas) return callback();
1064
1087
  const masks = this.canvas.getObjects().filter(object => object.maskId);
1065
- const maskStyleBackups = masks.map(mask => ({
1066
- object: mask,
1067
- stroke: mask.stroke,
1068
- strokeWidth: mask.strokeWidth,
1069
- opacity: mask.opacity
1070
- }));
1088
+ const maskStyleBackups = [];
1071
1089
 
1072
1090
  try {
1073
1091
  masks.forEach(mask => {
1074
- mask.set(this._getMaskNormalStyle(mask));
1092
+ const normalStyle = this._getMaskNormalStyle(mask);
1093
+ const stylePatch = {};
1094
+ Object.keys(normalStyle).forEach(property => {
1095
+ if (mask[property] !== normalStyle[property]) {
1096
+ stylePatch[property] = normalStyle[property];
1097
+ }
1098
+ });
1099
+ const changedProperties = Object.keys(stylePatch);
1100
+ if (!changedProperties.length) return;
1101
+
1102
+ const backup = { object: mask };
1103
+ changedProperties.forEach(property => {
1104
+ backup[property] = mask[property];
1105
+ });
1106
+ maskStyleBackups.push(backup);
1107
+ mask.set(stylePatch);
1075
1108
  });
1076
1109
  return callback();
1077
1110
  } finally {
1078
1111
  maskStyleBackups.forEach(backup => {
1079
1112
  try {
1080
- backup.object.set({
1081
- stroke: backup.stroke,
1082
- strokeWidth: backup.strokeWidth,
1083
- opacity: backup.opacity
1113
+ const restorePatch = {};
1114
+ Object.keys(backup).forEach(property => {
1115
+ if (property !== 'object') restorePatch[property] = backup[property];
1084
1116
  });
1117
+ backup.object.set(restorePatch);
1085
1118
  } catch (error) { void error; }
1086
1119
  });
1087
1120
  }
@@ -1841,18 +1874,27 @@ function ensureFabric() {
1841
1874
 
1842
1875
  // Always start placement relative to canvas left/top.
1843
1876
  const firstOffset = 10;
1844
- let left = firstOffset;
1845
- let top = firstOffset;
1877
+ let left;
1878
+ let top;
1879
+
1880
+ const getCanvasBasis = (axis) => {
1881
+ const canvasWidth = this.canvas ? this.canvas.getWidth() : 0;
1882
+ const canvasHeight = this.canvas ? this.canvas.getHeight() : 0;
1883
+ if (axis === 'height') return canvasHeight;
1884
+ if (axis === 'min') return Math.min(canvasWidth, canvasHeight);
1885
+ return canvasWidth;
1886
+ };
1846
1887
 
1847
- const resolveValue = (value, fallback) => {
1888
+ const resolveValue = (value, fallback, axis = 'width') => {
1848
1889
  if (typeof value === 'function')
1849
1890
  return value(this.canvas, this.options);
1850
1891
  if (typeof value === 'string' && value.endsWith('%')) {
1851
- const percent = parseFloat(value) / 100;
1852
- return Math.floor((this.canvas ? this.canvas.getWidth() : 0) * percent);
1892
+ const percent = Number.parseFloat(value) / 100;
1893
+ if (!Number.isFinite(percent)) return fallback;
1894
+ return Math.floor(getCanvasBasis(axis) * percent);
1853
1895
  }
1854
1896
  return value != null ? value : fallback;
1855
- }
1897
+ };
1856
1898
 
1857
1899
  if (maskConfig.left === undefined && this._lastMask) {
1858
1900
  const previousMask = this._lastMask;
@@ -1866,12 +1908,12 @@ function ensureFabric() {
1866
1908
  left = Math.round(previousMaskRight + maskConfig.gap);
1867
1909
  top = previousMask.top ?? firstOffset;
1868
1910
  } else {
1869
- left = resolveValue(maskConfig.left, firstOffset);
1870
- top = resolveValue(maskConfig.top, firstOffset);
1911
+ left = resolveValue(maskConfig.left, firstOffset, 'width');
1912
+ top = resolveValue(maskConfig.top, firstOffset, 'height');
1871
1913
  }
1872
1914
 
1873
- maskConfig.width = resolveValue(maskConfig.width, this.options.defaultMaskWidth);
1874
- maskConfig.height = resolveValue(maskConfig.height, this.options.defaultMaskHeight);
1915
+ maskConfig.width = resolveValue(maskConfig.width, this.options.defaultMaskWidth, 'width');
1916
+ maskConfig.height = resolveValue(maskConfig.height, this.options.defaultMaskHeight, 'height');
1875
1917
  maskConfig.left = left;
1876
1918
  maskConfig.top = top;
1877
1919
 
@@ -1883,7 +1925,7 @@ function ensureFabric() {
1883
1925
  case 'circle':
1884
1926
  mask = new fabric.Circle({
1885
1927
  left, top,
1886
- radius: resolveValue(maskConfig.radius, Math.min(maskConfig.width, maskConfig.height) / 2),
1928
+ radius: resolveValue(maskConfig.radius, Math.min(maskConfig.width, maskConfig.height) / 2, 'min'),
1887
1929
  fill: maskConfig.color,
1888
1930
  opacity: maskConfig.alpha,
1889
1931
  angle: maskConfig.angle,
@@ -1893,8 +1935,8 @@ function ensureFabric() {
1893
1935
  case 'ellipse':
1894
1936
  mask = new fabric.Ellipse({
1895
1937
  left, top,
1896
- rx: resolveValue(maskConfig.rx, maskConfig.width / 2),
1897
- ry: resolveValue(maskConfig.ry, maskConfig.height / 2),
1938
+ rx: resolveValue(maskConfig.rx, maskConfig.width / 2, 'width'),
1939
+ ry: resolveValue(maskConfig.ry, maskConfig.height / 2, 'height'),
1898
1940
  fill: maskConfig.color,
1899
1941
  opacity: maskConfig.alpha,
1900
1942
  angle: maskConfig.angle,
@@ -1922,8 +1964,8 @@ function ensureFabric() {
1922
1964
  default:
1923
1965
  mask = new fabric.Rect({
1924
1966
  left, top,
1925
- width: resolveValue(maskConfig.width, this.options.defaultMaskWidth),
1926
- height: resolveValue(maskConfig.height, this.options.defaultMaskHeight),
1967
+ width: resolveValue(maskConfig.width, this.options.defaultMaskWidth, 'width'),
1968
+ height: resolveValue(maskConfig.height, this.options.defaultMaskHeight, 'height'),
1927
1969
  fill: maskConfig.color,
1928
1970
  opacity: maskConfig.alpha,
1929
1971
  angle: maskConfig.angle,
@@ -1964,7 +2006,7 @@ function ensureFabric() {
1964
2006
  // Store placement values so the next mask can be positioned beside this one.
1965
2007
  this._lastMaskInitialLeft = left;
1966
2008
  this._lastMaskInitialTop = top;
1967
- this._lastMaskInitialWidth = resolveValue(maskConfig.width, this.options.defaultMaskWidth);
2009
+ this._lastMaskInitialWidth = resolveValue(maskConfig.width, this.options.defaultMaskWidth, 'width');
1968
2010
 
1969
2011
  const maskId = ++this.maskCounter;
1970
2012
  mask.set({
@@ -2752,12 +2794,12 @@ function ensureFabric() {
2752
2794
  this._cropRect.setCoords();
2753
2795
  const rectBounds = this._cropRect.getBoundingRect(true, true);
2754
2796
 
2755
- const cropRegion = this._getClampedCanvasRegion(rectBounds);
2797
+ const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
2756
2798
  const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
2757
2799
 
2758
2800
  this._restoreCropObjectState();
2759
2801
 
2760
- let beforeJson = null;
2802
+ let beforeJson;
2761
2803
  try {
2762
2804
  beforeJson = this._serializeCanvasState();
2763
2805
  } catch (error) {
@@ -2843,7 +2885,7 @@ function ensureFabric() {
2843
2885
  }
2844
2886
 
2845
2887
  // Create an after snapshot and push one history command for the crop operation.
2846
- let afterJson = null;
2888
+ let afterJson;
2847
2889
  try {
2848
2890
  afterJson = this._serializeCanvasState();
2849
2891
  } catch (error) {
@@ -2971,15 +3013,23 @@ function ensureFabric() {
2971
3013
  */
2972
3014
  _setPlaceholderVisible(show) {
2973
3015
  if (!this.placeholderElement || !this.containerElement) return;
2974
- if (show) {
2975
- this.placeholderElement.classList.remove('d-none');
2976
- this.placeholderElement.classList.add('d-flex');
2977
- this.containerElement.classList.add('d-none');
2978
- } else {
2979
- this.placeholderElement.classList.remove('d-flex');
2980
- this.placeholderElement.classList.add('d-none');
2981
- this.containerElement.classList.remove('d-none');
2982
- }
3016
+ this._setElementVisible(this.placeholderElement, show);
3017
+ this._setElementVisible(this.containerElement, !show);
3018
+ }
3019
+
3020
+ /**
3021
+ * Updates element visibility.
3022
+ *
3023
+ * @param {HTMLElement} element - Element whose visibility should be updated.
3024
+ * @param {boolean} isVisible - If true, removes the hidden state.
3025
+ * @returns {void}
3026
+ * @private
3027
+ */
3028
+ _setElementVisible(element, isVisible) {
3029
+ if (!element) return;
3030
+ element.hidden = !isVisible;
3031
+ element.setAttribute('aria-hidden', isVisible ? 'false' : 'true');
3032
+ if (isVisible && element.classList) element.classList.remove('d-none');
2983
3033
  }
2984
3034
 
2985
3035
  /**