@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/dist/image-editor.esm.js +230 -277
- package/dist/image-editor.esm.js.map +2 -2
- package/dist/image-editor.esm.min.js +3 -3
- package/dist/image-editor.esm.min.js.map +3 -3
- package/dist/image-editor.esm.min.mjs +3 -3
- package/dist/image-editor.esm.min.mjs.map +3 -3
- package/dist/image-editor.esm.mjs +230 -277
- package/dist/image-editor.esm.mjs.map +2 -2
- package/dist/image-editor.js +230 -277
- package/dist/image-editor.js.map +2 -2
- package/dist/image-editor.min.js +2 -2
- package/dist/image-editor.min.js.map +3 -3
- package/package.json +9 -4
- package/src/image-editor.js +104 -54
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bensitu/image-editor",
|
|
3
|
-
"version": "1.3.
|
|
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
|
|
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
|
-
"
|
|
46
|
+
"@eslint/js": "^10.0.1",
|
|
47
|
+
"esbuild": "^0.28.0",
|
|
44
48
|
"esbuild-plugin-babel": "^0.2.3",
|
|
45
|
-
"eslint": "^
|
|
49
|
+
"eslint": "^10.4.0",
|
|
50
|
+
"globals": "^17.6.0"
|
|
46
51
|
},
|
|
47
52
|
"browserslist": [
|
|
48
53
|
"Chrome >= 100",
|
package/src/image-editor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file image-editor.js
|
|
3
3
|
* @module image-editor
|
|
4
|
-
* @version 1.3.
|
|
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
|
|
879
|
-
const
|
|
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
|
-
|
|
885
|
-
|
|
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
|
|
901
|
-
|
|
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
|
|
952
|
-
let effectiveHeight
|
|
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
|
|
1004
|
-
let effectiveHeight
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
|
1845
|
-
let top
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
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
|
/**
|