@mezzanine-ui/react 1.0.0-beta.6 → 1.0.0-beta.7
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/Accordion/Accordion.d.ts +23 -1
- package/Accordion/Accordion.js +59 -11
- package/Accordion/AccordionActions.d.ts +13 -0
- package/Accordion/AccordionActions.js +24 -0
- package/Accordion/AccordionContent.d.ts +9 -0
- package/Accordion/{AccordionDetails.js → AccordionContent.js} +4 -6
- package/Accordion/AccordionControlContext.d.ts +2 -2
- package/Accordion/AccordionGroup.d.ts +10 -0
- package/Accordion/AccordionGroup.js +26 -0
- package/Accordion/AccordionTitle.d.ts +14 -0
- package/Accordion/AccordionTitle.js +56 -0
- package/Accordion/index.d.ts +8 -4
- package/Accordion/index.js +4 -2
- package/AutoComplete/AutoComplete.d.ts +20 -6
- package/AutoComplete/AutoComplete.js +118 -30
- package/Backdrop/Backdrop.js +15 -19
- package/Calendar/CalendarDays.js +1 -1
- package/Card/BaseCard.d.ts +11 -0
- package/Card/BaseCard.js +48 -0
- package/Card/BaseCardSkeleton.d.ts +14 -0
- package/Card/BaseCardSkeleton.js +18 -0
- package/Card/CardGroup.d.ts +47 -0
- package/Card/CardGroup.js +147 -0
- package/Card/FourThumbnailCard.d.ts +14 -0
- package/Card/FourThumbnailCard.js +73 -0
- package/Card/FourThumbnailCardSkeleton.d.ts +14 -0
- package/Card/FourThumbnailCardSkeleton.js +20 -0
- package/Card/QuickActionCard.d.ts +12 -0
- package/Card/QuickActionCard.js +23 -0
- package/Card/QuickActionCardSkeleton.d.ts +14 -0
- package/Card/QuickActionCardSkeleton.js +18 -0
- package/Card/SingleThumbnailCard.d.ts +13 -0
- package/Card/SingleThumbnailCard.js +44 -0
- package/Card/SingleThumbnailCardSkeleton.d.ts +19 -0
- package/Card/SingleThumbnailCardSkeleton.js +18 -0
- package/Card/Thumbnail.d.ts +12 -0
- package/Card/Thumbnail.js +18 -0
- package/Card/ThumbnailCardInfo.d.ts +34 -0
- package/Card/ThumbnailCardInfo.js +43 -0
- package/Card/index.d.ts +43 -4
- package/Card/index.js +19 -2
- package/Card/typings.d.ts +442 -0
- package/Checkbox/Checkbox.d.ts +8 -0
- package/Checkbox/Checkbox.js +3 -2
- package/Checkbox/CheckboxGroup.js +1 -1
- package/ContentHeader/ContentHeader.d.ts +22 -70
- package/ContentHeader/ContentHeader.js +1 -1
- package/ContentHeader/ContentHeaderResponsive.d.ts +9 -0
- package/ContentHeader/ContentHeaderResponsive.js +7 -0
- package/ContentHeader/utils.d.ts +3 -3
- package/ContentHeader/utils.js +66 -20
- package/Cropper/Cropper.d.ts +66 -0
- package/Cropper/Cropper.js +115 -0
- package/Cropper/CropperElement.d.ts +10 -0
- package/Cropper/CropperElement.js +892 -0
- package/Cropper/index.d.ts +18 -0
- package/Cropper/index.js +8 -0
- package/Cropper/tools.d.ts +90 -0
- package/Cropper/tools.js +143 -0
- package/Cropper/typings.d.ts +69 -0
- package/Cropper/utils/cropper-calculations.d.ts +39 -0
- package/Cropper/utils/cropper-calculations.js +95 -0
- package/DateTimePicker/DateTimePicker.d.ts +1 -1
- package/DateTimePicker/DateTimePicker.js +14 -1
- package/Dropdown/Dropdown.d.ts +7 -1
- package/Dropdown/Dropdown.js +31 -14
- package/Dropdown/DropdownItem.d.ts +7 -1
- package/Dropdown/DropdownItem.js +36 -6
- package/Dropdown/DropdownItemCard.js +2 -1
- package/FloatingButton/FloatingButton.d.ts +21 -0
- package/FloatingButton/FloatingButton.js +18 -0
- package/FloatingButton/index.d.ts +2 -0
- package/FloatingButton/index.js +1 -0
- package/Form/FormField.d.ts +21 -10
- package/Form/FormField.js +12 -4
- package/Input/Input.js +9 -2
- package/Message/Message.js +1 -1
- package/MultipleDatePicker/MultipleDatePicker.js +2 -2
- package/Navigation/NavigationHeader.js +1 -1
- package/Picker/FormattedInput.d.ts +1 -1
- package/Picker/FormattedInput.js +2 -1
- package/Picker/PickerTriggerWithSeparator.d.ts +10 -0
- package/Picker/PickerTriggerWithSeparator.js +2 -2
- package/Picker/useDateInputFormatter.d.ts +6 -0
- package/Picker/useDateInputFormatter.js +4 -1
- package/Select/Select.d.ts +2 -8
- package/Select/Select.js +12 -33
- package/Select/SelectTrigger.js +21 -7
- package/Select/index.d.ts +0 -4
- package/Select/index.js +0 -2
- package/Select/typings.d.ts +0 -4
- package/Select/useSelectTriggerTags.d.ts +1 -1
- package/Select/useSelectTriggerTags.js +9 -6
- package/Separator/Separator.d.ts +14 -0
- package/Separator/Separator.js +17 -0
- package/Separator/index.d.ts +2 -0
- package/Separator/index.js +1 -0
- package/Table/utils/useTableRowSelection.js +6 -0
- package/Tag/TagGroup.d.ts +4 -2
- package/Tag/TagGroup.js +7 -4
- package/TextField/TextField.d.ts +1 -1
- package/TextField/TextField.js +63 -9
- package/TimePanel/TimePanelColumn.js +19 -12
- package/index.d.ts +27 -28
- package/index.js +23 -25
- package/package.json +4 -4
- package/Accordion/AccordionDetails.d.ts +0 -9
- package/Accordion/AccordionSummary.d.ts +0 -18
- package/Accordion/AccordionSummary.js +0 -51
- package/Alert/Alert.d.ts +0 -20
- package/Alert/Alert.js +0 -18
- package/Alert/index.d.ts +0 -3
- package/Alert/index.js +0 -1
- package/Card/Card.d.ts +0 -51
- package/Card/Card.js +0 -20
- package/Card/CardActions.d.ts +0 -34
- package/Card/CardActions.js +0 -15
- package/ConfirmActions/ConfirmActions.d.ts +0 -46
- package/ConfirmActions/ConfirmActions.js +0 -15
- package/ConfirmActions/index.d.ts +0 -2
- package/ConfirmActions/index.js +0 -1
- package/Select/Option.d.ts +0 -18
- package/Select/Option.js +0 -45
- package/Select/TreeSelect.d.ts +0 -72
- package/Select/TreeSelect.js +0 -205
- package/Tree/Tree.d.ts +0 -70
- package/Tree/Tree.js +0 -139
- package/Tree/TreeNode.d.ts +0 -40
- package/Tree/TreeNode.js +0 -50
- package/Tree/TreeNodeList.d.ts +0 -24
- package/Tree/TreeNodeList.js +0 -28
- package/Tree/getTreeNodeEntities.d.ts +0 -11
- package/Tree/getTreeNodeEntities.js +0 -92
- package/Tree/index.d.ts +0 -13
- package/Tree/index.js +0 -7
- package/Tree/toggleValue.d.ts +0 -4
- package/Tree/toggleValue.js +0 -19
- package/Tree/traverseTree.d.ts +0 -2
- package/Tree/traverseTree.js +0 -11
- package/Tree/typings.d.ts +0 -16
- package/Tree/useTreeExpandedValue.d.ts +0 -14
- package/Tree/useTreeExpandedValue.js +0 -33
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { forwardRef, useRef, useState, useCallback, useEffect, useLayoutEffect, useMemo } from 'react';
|
|
4
|
+
import { cropperClasses } from '@mezzanine-ui/core/cropper';
|
|
5
|
+
import { MinusIcon, PlusIcon } from '@mezzanine-ui/icons';
|
|
6
|
+
import { useDocumentEvents } from '../hooks/useDocumentEvents.js';
|
|
7
|
+
import Typography from '../Typography/Typography.js';
|
|
8
|
+
import { getCSSVariableValue } from '../utils/get-css-variable-value.js';
|
|
9
|
+
import { isCropAreaSimilar, isImagePositionSimilar, calculateInitialCropArea, getBaseDisplaySize, constrainImagePosition, getBaseScale } from './utils/cropper-calculations.js';
|
|
10
|
+
import Slider from '../Slider/Slider.js';
|
|
11
|
+
import cx from 'clsx';
|
|
12
|
+
|
|
13
|
+
// Constants
|
|
14
|
+
const DEFAULT_MIN_WIDTH = 50;
|
|
15
|
+
const DEFAULT_MIN_HEIGHT = 50;
|
|
16
|
+
const MIN_SCALE = 1;
|
|
17
|
+
const MAX_SCALE = 2;
|
|
18
|
+
const SCALE_STEP = 0.01;
|
|
19
|
+
const BORDER_WIDTH = 2;
|
|
20
|
+
const TAG_INSET_PX = 10;
|
|
21
|
+
const CROP_AREA_SIMILARITY_THRESHOLD = 0.5;
|
|
22
|
+
const IMAGE_POSITION_SIMILARITY_THRESHOLD = 0.1;
|
|
23
|
+
/**
|
|
24
|
+
* The react component for `mezzanine` cropper element (canvas).
|
|
25
|
+
*/
|
|
26
|
+
const CropperElement = forwardRef(function CropperElement(props, ref) {
|
|
27
|
+
const { children, className, component: Component = 'canvas', size = 'main', imageSrc, onCropChange, onCropDragEnd, onImageDragEnd, onScaleChange, onImageLoad, onImageError, initialCropArea, aspectRatio, minWidth = DEFAULT_MIN_WIDTH, minHeight = DEFAULT_MIN_HEIGHT, ...rest } = props;
|
|
28
|
+
// Refs
|
|
29
|
+
const canvasRef = useRef(null);
|
|
30
|
+
const elementRef = useRef(null);
|
|
31
|
+
const imageRef = useRef(null);
|
|
32
|
+
const imageLoadIdRef = useRef(0);
|
|
33
|
+
// State - Core
|
|
34
|
+
const [cropArea, setCropArea] = useState(initialCropArea || null);
|
|
35
|
+
const [imageLoaded, setImageLoaded] = useState(false);
|
|
36
|
+
const [initReady, setInitReady] = useState(false);
|
|
37
|
+
const [scale, setScale] = useState(1);
|
|
38
|
+
const [imagePosition, setImagePosition] = useState({
|
|
39
|
+
offsetX: 0,
|
|
40
|
+
offsetY: 0,
|
|
41
|
+
});
|
|
42
|
+
// State - UI
|
|
43
|
+
const [tagPosition, setTagPosition] = useState(null);
|
|
44
|
+
const [tagCropArea, setTagCropArea] = useState(null);
|
|
45
|
+
// State - Interactions
|
|
46
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
47
|
+
const [isDraggingImage, setIsDraggingImage] = useState(false);
|
|
48
|
+
const [dragHandle, setDragHandle] = useState(null);
|
|
49
|
+
const [dragStart, setDragStart] = useState(null);
|
|
50
|
+
const [imageDragStart, setImageDragStart] = useState(null);
|
|
51
|
+
// Refs - Cache for optimization
|
|
52
|
+
const baseDisplaySizeRef = useRef(null);
|
|
53
|
+
const lastCanvasSizeRef = useRef(null);
|
|
54
|
+
const lastMeasuredSizeRef = useRef(null);
|
|
55
|
+
const lastTagPositionRef = useRef(null);
|
|
56
|
+
const lastCropAreaRef = useRef(null);
|
|
57
|
+
const lastImagePositionRef = useRef(null);
|
|
58
|
+
const lastDrawTriggerRef = useRef(null);
|
|
59
|
+
// Refs - Animation & Control
|
|
60
|
+
const rafIdRef = useRef(null);
|
|
61
|
+
const skipDrawRef = useRef(false);
|
|
62
|
+
const resizeRafIdRef = useRef(null);
|
|
63
|
+
const dragRafIdRef = useRef(null);
|
|
64
|
+
const composedRef = useCallback((node) => {
|
|
65
|
+
canvasRef.current = node;
|
|
66
|
+
if (typeof ref === 'function') {
|
|
67
|
+
ref(node);
|
|
68
|
+
}
|
|
69
|
+
else if (ref) {
|
|
70
|
+
ref.current = node;
|
|
71
|
+
}
|
|
72
|
+
}, [ref]);
|
|
73
|
+
// State management with debouncing
|
|
74
|
+
const setCropAreaIfChanged = useCallback((nextCropArea) => {
|
|
75
|
+
if (!isCropAreaSimilar(lastCropAreaRef.current, nextCropArea, CROP_AREA_SIMILARITY_THRESHOLD)) {
|
|
76
|
+
lastCropAreaRef.current = nextCropArea;
|
|
77
|
+
setCropArea(nextCropArea);
|
|
78
|
+
}
|
|
79
|
+
}, []);
|
|
80
|
+
const setImagePositionIfChanged = useCallback((nextImagePosition) => {
|
|
81
|
+
if (!isImagePositionSimilar(lastImagePositionRef.current, nextImagePosition, IMAGE_POSITION_SIMILARITY_THRESHOLD)) {
|
|
82
|
+
lastImagePositionRef.current = nextImagePosition;
|
|
83
|
+
setImagePosition(nextImagePosition);
|
|
84
|
+
}
|
|
85
|
+
}, []);
|
|
86
|
+
// Calculation helpers
|
|
87
|
+
const calculateInitialCropArea$1 = useCallback((img, rect) => {
|
|
88
|
+
return calculateInitialCropArea(img, rect, aspectRatio);
|
|
89
|
+
}, [aspectRatio]);
|
|
90
|
+
const updateTagPosition = useCallback(() => {
|
|
91
|
+
if (!cropArea || !canvasRef.current || !elementRef.current) {
|
|
92
|
+
if (lastTagPositionRef.current) {
|
|
93
|
+
lastTagPositionRef.current = null;
|
|
94
|
+
setTagPosition(null);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const canvasRect = canvasRef.current.getBoundingClientRect();
|
|
99
|
+
const elementRect = elementRef.current.getBoundingClientRect();
|
|
100
|
+
const nextPosition = {
|
|
101
|
+
left: canvasRect.left -
|
|
102
|
+
elementRect.left +
|
|
103
|
+
cropArea.x +
|
|
104
|
+
cropArea.width -
|
|
105
|
+
TAG_INSET_PX -
|
|
106
|
+
BORDER_WIDTH,
|
|
107
|
+
top: canvasRect.top -
|
|
108
|
+
elementRect.top +
|
|
109
|
+
cropArea.y +
|
|
110
|
+
cropArea.height -
|
|
111
|
+
TAG_INSET_PX -
|
|
112
|
+
BORDER_WIDTH,
|
|
113
|
+
};
|
|
114
|
+
const prevPosition = lastTagPositionRef.current;
|
|
115
|
+
if (!prevPosition ||
|
|
116
|
+
prevPosition.left !== nextPosition.left ||
|
|
117
|
+
prevPosition.top !== nextPosition.top) {
|
|
118
|
+
lastTagPositionRef.current = nextPosition;
|
|
119
|
+
setTagPosition(nextPosition);
|
|
120
|
+
}
|
|
121
|
+
}, [cropArea]);
|
|
122
|
+
// Load image
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const loadId = imageLoadIdRef.current + 1;
|
|
125
|
+
imageLoadIdRef.current = loadId;
|
|
126
|
+
setInitReady(false);
|
|
127
|
+
if (!imageSrc) {
|
|
128
|
+
setImageLoaded(false);
|
|
129
|
+
imageRef.current = null;
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
setImageLoaded(false);
|
|
133
|
+
imageRef.current = null;
|
|
134
|
+
let objectUrl = null;
|
|
135
|
+
const img = new Image();
|
|
136
|
+
img.crossOrigin = 'anonymous';
|
|
137
|
+
const loadImage = async () => {
|
|
138
|
+
try {
|
|
139
|
+
if (typeof imageSrc === 'string') {
|
|
140
|
+
img.src = imageSrc;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
objectUrl = URL.createObjectURL(imageSrc);
|
|
144
|
+
img.src = objectUrl;
|
|
145
|
+
}
|
|
146
|
+
await new Promise((resolve, reject) => {
|
|
147
|
+
img.onload = resolve;
|
|
148
|
+
img.onerror = reject;
|
|
149
|
+
});
|
|
150
|
+
if (imageLoadIdRef.current !== loadId) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
imageRef.current = img;
|
|
154
|
+
setImageLoaded(true);
|
|
155
|
+
onImageLoad === null || onImageLoad === void 0 ? void 0 : onImageLoad();
|
|
156
|
+
// Initialize crop area if not provided
|
|
157
|
+
if (!initialCropArea && canvasRef.current) {
|
|
158
|
+
const canvas = canvasRef.current;
|
|
159
|
+
const rect = canvas.getBoundingClientRect();
|
|
160
|
+
const { baseDisplayHeight, baseDisplayWidth, cropArea: nextCropArea, imagePosition: nextImagePosition, } = calculateInitialCropArea$1(img, rect);
|
|
161
|
+
baseDisplaySizeRef.current = {
|
|
162
|
+
height: baseDisplayHeight,
|
|
163
|
+
width: baseDisplayWidth,
|
|
164
|
+
};
|
|
165
|
+
setCropAreaIfChanged(nextCropArea);
|
|
166
|
+
setImagePosition(nextImagePosition);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (imageLoadIdRef.current !== loadId) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
console.error('Failed to load image:', error);
|
|
174
|
+
setImageLoaded(false);
|
|
175
|
+
onImageError === null || onImageError === void 0 ? void 0 : onImageError(error instanceof Error ? error : new Error(String(error)));
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
loadImage();
|
|
179
|
+
return () => {
|
|
180
|
+
if (objectUrl) {
|
|
181
|
+
URL.revokeObjectURL(objectUrl);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}, [
|
|
185
|
+
imageSrc,
|
|
186
|
+
initialCropArea,
|
|
187
|
+
aspectRatio,
|
|
188
|
+
calculateInitialCropArea$1,
|
|
189
|
+
setCropAreaIfChanged,
|
|
190
|
+
onImageLoad,
|
|
191
|
+
onImageError,
|
|
192
|
+
]);
|
|
193
|
+
useLayoutEffect(() => {
|
|
194
|
+
updateTagPosition();
|
|
195
|
+
}, [updateTagPosition]);
|
|
196
|
+
// Calculate base display size
|
|
197
|
+
const getBaseDisplaySize$1 = useCallback(() => {
|
|
198
|
+
if (!canvasRef.current || !imageRef.current)
|
|
199
|
+
return null;
|
|
200
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
201
|
+
return getBaseDisplaySize(rect, imageRef.current);
|
|
202
|
+
}, []);
|
|
203
|
+
// Draw canvas
|
|
204
|
+
const drawCanvas = useCallback(() => {
|
|
205
|
+
const canvas = canvasRef.current;
|
|
206
|
+
const img = imageRef.current;
|
|
207
|
+
const crop = cropArea;
|
|
208
|
+
if (!canvas || !img || !crop)
|
|
209
|
+
return;
|
|
210
|
+
const ctx = canvas.getContext('2d');
|
|
211
|
+
if (!ctx)
|
|
212
|
+
return;
|
|
213
|
+
const rect = canvas.getBoundingClientRect();
|
|
214
|
+
const dpr = window.devicePixelRatio || 1;
|
|
215
|
+
canvas.width = rect.width * dpr;
|
|
216
|
+
canvas.height = rect.height * dpr;
|
|
217
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
218
|
+
// Calculate display size with zoom
|
|
219
|
+
const baseSize = getBaseDisplaySize(rect, img);
|
|
220
|
+
if (!baseSize)
|
|
221
|
+
return;
|
|
222
|
+
const baseDisplayWidth = baseSize.width;
|
|
223
|
+
const baseDisplayHeight = baseSize.height;
|
|
224
|
+
if (!baseDisplaySizeRef.current) {
|
|
225
|
+
baseDisplaySizeRef.current = {
|
|
226
|
+
width: baseDisplayWidth,
|
|
227
|
+
height: baseDisplayHeight,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const displayWidth = baseDisplayWidth * scale;
|
|
231
|
+
const displayHeight = baseDisplayHeight * scale;
|
|
232
|
+
// Constrain image position
|
|
233
|
+
const constrainedPosition = constrainImagePosition(imagePosition.offsetX, imagePosition.offsetY, displayWidth, displayHeight, crop);
|
|
234
|
+
const finalOffsetX = constrainedPosition.offsetX;
|
|
235
|
+
const finalOffsetY = constrainedPosition.offsetY;
|
|
236
|
+
// Clear canvas
|
|
237
|
+
ctx.clearRect(0, 0, rect.width, rect.height);
|
|
238
|
+
// Draw image
|
|
239
|
+
ctx.drawImage(img, finalOffsetX, finalOffsetY, displayWidth, displayHeight);
|
|
240
|
+
// Draw overlay (darken outside crop area only)
|
|
241
|
+
// Use clip to exclude crop area from overlay
|
|
242
|
+
ctx.save();
|
|
243
|
+
// Create a path that covers the entire canvas except the crop area
|
|
244
|
+
// Using even-odd rule to create a hole
|
|
245
|
+
const path = new Path2D();
|
|
246
|
+
path.rect(0, 0, rect.width, rect.height);
|
|
247
|
+
path.rect(crop.x, crop.y, crop.width, crop.height);
|
|
248
|
+
ctx.clip(path, 'evenodd');
|
|
249
|
+
// Get overlay color from CSS variable with fallback
|
|
250
|
+
const overlayColor = getCSSVariableValue('--mzn-color-overlay-strong') ||
|
|
251
|
+
'rgba(0, 0, 0, 0.60)';
|
|
252
|
+
ctx.fillStyle = overlayColor;
|
|
253
|
+
ctx.fillRect(0, 0, rect.width, rect.height);
|
|
254
|
+
ctx.restore();
|
|
255
|
+
// Draw crop border
|
|
256
|
+
ctx.strokeStyle =
|
|
257
|
+
getCSSVariableValue('--mzn-color-border-brand') || '#5D74E9';
|
|
258
|
+
ctx.lineWidth = BORDER_WIDTH;
|
|
259
|
+
ctx.strokeRect(crop.x, crop.y, crop.width, crop.height);
|
|
260
|
+
}, [cropArea, scale, imagePosition]);
|
|
261
|
+
const scheduleDraw = useCallback(() => {
|
|
262
|
+
if (rafIdRef.current !== null)
|
|
263
|
+
return;
|
|
264
|
+
rafIdRef.current = window.requestAnimationFrame(() => {
|
|
265
|
+
rafIdRef.current = null;
|
|
266
|
+
drawCanvas();
|
|
267
|
+
});
|
|
268
|
+
}, [drawCanvas]);
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
return () => {
|
|
271
|
+
if (rafIdRef.current !== null) {
|
|
272
|
+
window.cancelAnimationFrame(rafIdRef.current);
|
|
273
|
+
rafIdRef.current = null;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}, []);
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
if (!elementRef.current || !canvasRef.current)
|
|
279
|
+
return undefined;
|
|
280
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
281
|
+
if (resizeRafIdRef.current !== null)
|
|
282
|
+
return;
|
|
283
|
+
resizeRafIdRef.current = window.requestAnimationFrame(() => {
|
|
284
|
+
var _a;
|
|
285
|
+
resizeRafIdRef.current = null;
|
|
286
|
+
if (imageLoaded) {
|
|
287
|
+
const rect = (_a = canvasRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
|
|
288
|
+
if (!rect || !rect.height)
|
|
289
|
+
return;
|
|
290
|
+
const isSizeChanged = !lastMeasuredSizeRef.current ||
|
|
291
|
+
rect.height !== lastMeasuredSizeRef.current.height;
|
|
292
|
+
let shouldSkipDraw = false;
|
|
293
|
+
if (isSizeChanged) {
|
|
294
|
+
lastMeasuredSizeRef.current = {
|
|
295
|
+
height: rect.height,
|
|
296
|
+
width: rect.width,
|
|
297
|
+
};
|
|
298
|
+
lastCanvasSizeRef.current = {
|
|
299
|
+
height: rect.height,
|
|
300
|
+
width: rect.width,
|
|
301
|
+
};
|
|
302
|
+
setInitReady(false);
|
|
303
|
+
const baseSize = getBaseDisplaySize$1();
|
|
304
|
+
if (baseSize) {
|
|
305
|
+
baseDisplaySizeRef.current = baseSize;
|
|
306
|
+
}
|
|
307
|
+
if (!initialCropArea && canvasRef.current && imageRef.current) {
|
|
308
|
+
shouldSkipDraw = true;
|
|
309
|
+
skipDrawRef.current = true;
|
|
310
|
+
const { cropArea: nextCropArea, imagePosition: nextImagePosition, } = calculateInitialCropArea$1(imageRef.current, rect);
|
|
311
|
+
setCropAreaIfChanged(nextCropArea);
|
|
312
|
+
setImagePosition(nextImagePosition);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (!shouldSkipDraw) {
|
|
316
|
+
scheduleDraw();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
resizeObserver.observe(elementRef.current);
|
|
322
|
+
resizeObserver.observe(canvasRef.current);
|
|
323
|
+
window.addEventListener('resize', updateTagPosition);
|
|
324
|
+
return () => {
|
|
325
|
+
resizeObserver.disconnect();
|
|
326
|
+
window.removeEventListener('resize', updateTagPosition);
|
|
327
|
+
if (resizeRafIdRef.current !== null) {
|
|
328
|
+
window.cancelAnimationFrame(resizeRafIdRef.current);
|
|
329
|
+
resizeRafIdRef.current = null;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}, [
|
|
333
|
+
calculateInitialCropArea$1,
|
|
334
|
+
getBaseDisplaySize$1,
|
|
335
|
+
imageLoaded,
|
|
336
|
+
initialCropArea,
|
|
337
|
+
scheduleDraw,
|
|
338
|
+
updateTagPosition,
|
|
339
|
+
setCropAreaIfChanged,
|
|
340
|
+
]);
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
if (imageLoaded) {
|
|
343
|
+
const baseSize = getBaseDisplaySize$1();
|
|
344
|
+
if (baseSize) {
|
|
345
|
+
baseDisplaySizeRef.current = baseSize;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}, [imageLoaded, getBaseDisplaySize$1]);
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
if (!cropArea || !imageLoaded || !canvasRef.current || !imageRef.current)
|
|
351
|
+
return;
|
|
352
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
353
|
+
const baseSize = getBaseDisplaySize(rect, imageRef.current);
|
|
354
|
+
if (!baseSize)
|
|
355
|
+
return;
|
|
356
|
+
const displayWidth = baseSize.width * scale;
|
|
357
|
+
const displayHeight = baseSize.height * scale;
|
|
358
|
+
const constrainedPosition = constrainImagePosition(imagePosition.offsetX, imagePosition.offsetY, displayWidth, displayHeight, cropArea);
|
|
359
|
+
if (constrainedPosition.offsetX !== imagePosition.offsetX ||
|
|
360
|
+
constrainedPosition.offsetY !== imagePosition.offsetY) {
|
|
361
|
+
skipDrawRef.current = true;
|
|
362
|
+
setImagePositionIfChanged(constrainedPosition);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
skipDrawRef.current = false;
|
|
366
|
+
}
|
|
367
|
+
}, [
|
|
368
|
+
cropArea,
|
|
369
|
+
imageLoaded,
|
|
370
|
+
imagePosition,
|
|
371
|
+
scale,
|
|
372
|
+
setImagePositionIfChanged,
|
|
373
|
+
]);
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
if (cropArea) {
|
|
376
|
+
lastCropAreaRef.current = cropArea;
|
|
377
|
+
}
|
|
378
|
+
}, [cropArea]);
|
|
379
|
+
useEffect(() => {
|
|
380
|
+
lastImagePositionRef.current = imagePosition;
|
|
381
|
+
}, [imagePosition]);
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
if (!imageLoaded || !cropArea)
|
|
384
|
+
return;
|
|
385
|
+
if (!lastCanvasSizeRef.current)
|
|
386
|
+
return;
|
|
387
|
+
setInitReady(true);
|
|
388
|
+
}, [cropArea, imageLoaded]);
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
if (!imageLoaded || !initReady)
|
|
391
|
+
return;
|
|
392
|
+
if (skipDrawRef.current) {
|
|
393
|
+
skipDrawRef.current = false;
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const lastTrigger = lastDrawTriggerRef.current;
|
|
397
|
+
const hasChanged = !lastTrigger ||
|
|
398
|
+
!cropArea ||
|
|
399
|
+
!isCropAreaSimilar(lastTrigger.cropArea, cropArea, CROP_AREA_SIMILARITY_THRESHOLD) ||
|
|
400
|
+
!isImagePositionSimilar(lastTrigger.imagePosition, imagePosition, IMAGE_POSITION_SIMILARITY_THRESHOLD) ||
|
|
401
|
+
lastTrigger.scale !== scale;
|
|
402
|
+
if (!hasChanged) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
lastDrawTriggerRef.current = {
|
|
406
|
+
cropArea,
|
|
407
|
+
imagePosition,
|
|
408
|
+
scale,
|
|
409
|
+
};
|
|
410
|
+
// Skip tag position update during drag for better performance
|
|
411
|
+
if (!isDraggingImage && !isDragging) {
|
|
412
|
+
updateTagPosition();
|
|
413
|
+
}
|
|
414
|
+
scheduleDraw();
|
|
415
|
+
}, [
|
|
416
|
+
cropArea,
|
|
417
|
+
imageLoaded,
|
|
418
|
+
initReady,
|
|
419
|
+
imagePosition,
|
|
420
|
+
scale,
|
|
421
|
+
updateTagPosition,
|
|
422
|
+
scheduleDraw,
|
|
423
|
+
isDraggingImage,
|
|
424
|
+
isDragging,
|
|
425
|
+
]);
|
|
426
|
+
const emitCropChange = useCallback((nextCropArea) => {
|
|
427
|
+
if (!onCropChange)
|
|
428
|
+
return;
|
|
429
|
+
if (!canvasRef.current || !imageRef.current) {
|
|
430
|
+
onCropChange(nextCropArea);
|
|
431
|
+
setTagCropArea(nextCropArea);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
435
|
+
const img = imageRef.current;
|
|
436
|
+
const baseScale = getBaseScale(rect, img);
|
|
437
|
+
const imageScale = baseScale / scale;
|
|
438
|
+
const rawWidth = nextCropArea.width * imageScale;
|
|
439
|
+
const rawHeight = nextCropArea.height * imageScale;
|
|
440
|
+
const rawX = (nextCropArea.x - imagePosition.offsetX) * imageScale;
|
|
441
|
+
const rawY = (nextCropArea.y - imagePosition.offsetY) * imageScale;
|
|
442
|
+
const clampedWidth = Math.min(img.width, Math.max(0, rawWidth));
|
|
443
|
+
const clampedHeight = Math.min(img.height, Math.max(0, rawHeight));
|
|
444
|
+
const clampedX = Math.min(img.width - clampedWidth, Math.max(0, rawX));
|
|
445
|
+
const clampedY = Math.min(img.height - clampedHeight, Math.max(0, rawY));
|
|
446
|
+
const emittedCropArea = {
|
|
447
|
+
height: clampedHeight,
|
|
448
|
+
width: clampedWidth,
|
|
449
|
+
x: clampedX,
|
|
450
|
+
y: clampedY,
|
|
451
|
+
};
|
|
452
|
+
onCropChange(emittedCropArea);
|
|
453
|
+
setTagCropArea(emittedCropArea);
|
|
454
|
+
}, [imagePosition.offsetX, imagePosition.offsetY, onCropChange, scale]);
|
|
455
|
+
// Update crop area based on drag
|
|
456
|
+
const updateCropArea = useCallback((handle, deltaX, deltaY) => {
|
|
457
|
+
if (!cropArea)
|
|
458
|
+
return;
|
|
459
|
+
let x = cropArea.x;
|
|
460
|
+
let y = cropArea.y;
|
|
461
|
+
let width = cropArea.width;
|
|
462
|
+
let height = cropArea.height;
|
|
463
|
+
switch (handle.type) {
|
|
464
|
+
case 'move':
|
|
465
|
+
x = Math.max(0, cropArea.x + deltaX);
|
|
466
|
+
y = Math.max(0, cropArea.y + deltaY);
|
|
467
|
+
if (canvasRef.current) {
|
|
468
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
469
|
+
x = Math.min(x, rect.width - width);
|
|
470
|
+
y = Math.min(y, rect.height - height);
|
|
471
|
+
}
|
|
472
|
+
break;
|
|
473
|
+
case 'nw':
|
|
474
|
+
width = cropArea.width - deltaX;
|
|
475
|
+
height = cropArea.height - deltaY;
|
|
476
|
+
x = cropArea.x + deltaX;
|
|
477
|
+
y = cropArea.y + deltaY;
|
|
478
|
+
break;
|
|
479
|
+
case 'ne':
|
|
480
|
+
width = cropArea.width + deltaX;
|
|
481
|
+
height = cropArea.height - deltaY;
|
|
482
|
+
y = cropArea.y + deltaY;
|
|
483
|
+
break;
|
|
484
|
+
case 'sw':
|
|
485
|
+
width = cropArea.width - deltaX;
|
|
486
|
+
height = cropArea.height + deltaY;
|
|
487
|
+
x = cropArea.x + deltaX;
|
|
488
|
+
break;
|
|
489
|
+
case 'se':
|
|
490
|
+
width = cropArea.width + deltaX;
|
|
491
|
+
height = cropArea.height + deltaY;
|
|
492
|
+
break;
|
|
493
|
+
case 'n':
|
|
494
|
+
height = cropArea.height - deltaY;
|
|
495
|
+
y = cropArea.y + deltaY;
|
|
496
|
+
break;
|
|
497
|
+
case 's':
|
|
498
|
+
height = cropArea.height + deltaY;
|
|
499
|
+
break;
|
|
500
|
+
case 'w':
|
|
501
|
+
width = cropArea.width - deltaX;
|
|
502
|
+
x = cropArea.x + deltaX;
|
|
503
|
+
break;
|
|
504
|
+
case 'e':
|
|
505
|
+
width = cropArea.width + deltaX;
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
const anchor = {
|
|
509
|
+
bottom: y + height,
|
|
510
|
+
centerX: x + width / 2,
|
|
511
|
+
centerY: y + height / 2,
|
|
512
|
+
left: x,
|
|
513
|
+
right: x + width,
|
|
514
|
+
top: y,
|
|
515
|
+
};
|
|
516
|
+
// Apply aspect ratio if provided
|
|
517
|
+
if (aspectRatio && handle.type !== 'move') {
|
|
518
|
+
const currentAspect = width / height;
|
|
519
|
+
if (Math.abs(currentAspect - aspectRatio) > 0.01) {
|
|
520
|
+
if (handle.type.includes('w') || handle.type.includes('e')) {
|
|
521
|
+
height = width / aspectRatio;
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
width = height * aspectRatio;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
switch (handle.type) {
|
|
528
|
+
case 'nw':
|
|
529
|
+
x = anchor.right - width;
|
|
530
|
+
y = anchor.bottom - height;
|
|
531
|
+
break;
|
|
532
|
+
case 'ne':
|
|
533
|
+
x = anchor.left;
|
|
534
|
+
y = anchor.bottom - height;
|
|
535
|
+
break;
|
|
536
|
+
case 'sw':
|
|
537
|
+
x = anchor.right - width;
|
|
538
|
+
y = anchor.top;
|
|
539
|
+
break;
|
|
540
|
+
case 'se':
|
|
541
|
+
x = anchor.left;
|
|
542
|
+
y = anchor.top;
|
|
543
|
+
break;
|
|
544
|
+
case 'n':
|
|
545
|
+
x = anchor.centerX - width / 2;
|
|
546
|
+
y = anchor.bottom - height;
|
|
547
|
+
break;
|
|
548
|
+
case 's':
|
|
549
|
+
x = anchor.centerX - width / 2;
|
|
550
|
+
y = anchor.top;
|
|
551
|
+
break;
|
|
552
|
+
case 'w':
|
|
553
|
+
x = anchor.right - width;
|
|
554
|
+
y = anchor.centerY - height / 2;
|
|
555
|
+
break;
|
|
556
|
+
case 'e':
|
|
557
|
+
x = anchor.left;
|
|
558
|
+
y = anchor.centerY - height / 2;
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// Apply constraints - allow crop area to extend to image bounds
|
|
563
|
+
if (canvasRef.current && imageRef.current) {
|
|
564
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
565
|
+
const baseSize = getBaseDisplaySize(rect, imageRef.current);
|
|
566
|
+
if (!baseSize) {
|
|
567
|
+
const newCrop = { x, y, width, height };
|
|
568
|
+
setCropArea(newCrop);
|
|
569
|
+
if (onCropChange) {
|
|
570
|
+
emitCropChange(newCrop);
|
|
571
|
+
}
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const baseDisplayWidth = baseSize.width;
|
|
575
|
+
const baseDisplayHeight = baseSize.height;
|
|
576
|
+
const displayWidth = baseDisplayWidth * scale;
|
|
577
|
+
const displayHeight = baseDisplayHeight * scale;
|
|
578
|
+
const imageOffsetX = imagePosition.offsetX;
|
|
579
|
+
const imageOffsetY = imagePosition.offsetY;
|
|
580
|
+
// Image display bounds
|
|
581
|
+
const imageLeft = imageOffsetX;
|
|
582
|
+
const imageRight = imageOffsetX + displayWidth;
|
|
583
|
+
const imageTop = imageOffsetY;
|
|
584
|
+
const imageBottom = imageOffsetY + displayHeight;
|
|
585
|
+
if (aspectRatio) {
|
|
586
|
+
// For aspect ratio, calculate maximum size within image bounds
|
|
587
|
+
// First, calculate max dimensions based on image bounds
|
|
588
|
+
const maxWidthFromImage = Math.min(imageRight - Math.max(x, imageLeft), Math.max(x + width, imageRight) - imageLeft);
|
|
589
|
+
const maxHeightFromImage = Math.min(imageBottom - Math.max(y, imageTop), Math.max(y + height, imageBottom) - imageTop);
|
|
590
|
+
// Calculate which dimension limits the size
|
|
591
|
+
const maxWidthByAspect = maxHeightFromImage * aspectRatio;
|
|
592
|
+
const maxHeightByAspect = maxWidthFromImage / aspectRatio;
|
|
593
|
+
if (maxWidthByAspect <= maxWidthFromImage) {
|
|
594
|
+
// Height is the limiting factor
|
|
595
|
+
width = Math.max(minWidth, Math.min(width, maxWidthByAspect));
|
|
596
|
+
height = width / aspectRatio;
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
// Width is the limiting factor
|
|
600
|
+
height = Math.max(minHeight, Math.min(height, maxHeightByAspect));
|
|
601
|
+
width = height * aspectRatio;
|
|
602
|
+
}
|
|
603
|
+
// Ensure crop area stays within image bounds
|
|
604
|
+
if (x < imageLeft) {
|
|
605
|
+
x = imageLeft;
|
|
606
|
+
}
|
|
607
|
+
else if (x + width > imageRight) {
|
|
608
|
+
x = imageRight - width;
|
|
609
|
+
}
|
|
610
|
+
if (y < imageTop) {
|
|
611
|
+
y = imageTop;
|
|
612
|
+
}
|
|
613
|
+
else if (y + height > imageBottom) {
|
|
614
|
+
y = imageBottom - height;
|
|
615
|
+
}
|
|
616
|
+
// Also ensure crop area doesn't go outside canvas bounds
|
|
617
|
+
x = Math.max(0, Math.min(x, rect.width - width));
|
|
618
|
+
y = Math.max(0, Math.min(y, rect.height - height));
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
// No aspect ratio - constrain to both image and canvas bounds
|
|
622
|
+
const maxWidth = Math.min(rect.width - x, imageRight - Math.max(x, imageLeft));
|
|
623
|
+
const maxHeight = Math.min(rect.height - y, imageBottom - Math.max(y, imageTop));
|
|
624
|
+
width = Math.max(minWidth, Math.min(width, maxWidth));
|
|
625
|
+
height = Math.max(minHeight, Math.min(height, maxHeight));
|
|
626
|
+
// Ensure crop area is within image bounds
|
|
627
|
+
if (x < imageLeft) {
|
|
628
|
+
x = imageLeft;
|
|
629
|
+
}
|
|
630
|
+
else if (x + width > imageRight) {
|
|
631
|
+
x = imageRight - width;
|
|
632
|
+
}
|
|
633
|
+
if (y < imageTop) {
|
|
634
|
+
y = imageTop;
|
|
635
|
+
}
|
|
636
|
+
else if (y + height > imageBottom) {
|
|
637
|
+
y = imageBottom - height;
|
|
638
|
+
}
|
|
639
|
+
// Also ensure crop area doesn't go outside canvas bounds
|
|
640
|
+
x = Math.max(0, Math.min(x, rect.width - width));
|
|
641
|
+
y = Math.max(0, Math.min(y, rect.height - height));
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
else if (canvasRef.current) {
|
|
645
|
+
// Fallback if image not loaded
|
|
646
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
647
|
+
width = Math.max(minWidth, Math.min(width, rect.width - x));
|
|
648
|
+
height = Math.max(minHeight, Math.min(height, rect.height - y));
|
|
649
|
+
x = Math.max(0, Math.min(x, rect.width - width));
|
|
650
|
+
y = Math.max(0, Math.min(y, rect.height - height));
|
|
651
|
+
}
|
|
652
|
+
const newCrop = { x, y, width, height };
|
|
653
|
+
setCropArea(newCrop);
|
|
654
|
+
if (onCropChange) {
|
|
655
|
+
emitCropChange(newCrop);
|
|
656
|
+
}
|
|
657
|
+
}, [
|
|
658
|
+
cropArea,
|
|
659
|
+
aspectRatio,
|
|
660
|
+
minWidth,
|
|
661
|
+
minHeight,
|
|
662
|
+
imagePosition.offsetX,
|
|
663
|
+
imagePosition.offsetY,
|
|
664
|
+
scale,
|
|
665
|
+
emitCropChange,
|
|
666
|
+
onCropChange,
|
|
667
|
+
]);
|
|
668
|
+
useEffect(() => {
|
|
669
|
+
if (!cropArea || !imageLoaded)
|
|
670
|
+
return;
|
|
671
|
+
// Skip emitting during drag to avoid performance issues
|
|
672
|
+
if (isDraggingImage || isDragging)
|
|
673
|
+
return;
|
|
674
|
+
emitCropChange(cropArea);
|
|
675
|
+
}, [cropArea, emitCropChange, imageLoaded, isDraggingImage, isDragging]);
|
|
676
|
+
// Check if point is on image
|
|
677
|
+
const isPointOnImage = useCallback((x, y) => {
|
|
678
|
+
if (!cropArea || !canvasRef.current || !imageRef.current)
|
|
679
|
+
return false;
|
|
680
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
681
|
+
const baseSize = getBaseDisplaySize(rect, imageRef.current);
|
|
682
|
+
if (!baseSize)
|
|
683
|
+
return false;
|
|
684
|
+
const displayWidth = baseSize.width * scale;
|
|
685
|
+
const displayHeight = baseSize.height * scale;
|
|
686
|
+
return (x >= imagePosition.offsetX &&
|
|
687
|
+
x <= imagePosition.offsetX + displayWidth &&
|
|
688
|
+
y >= imagePosition.offsetY &&
|
|
689
|
+
y <= imagePosition.offsetY + displayHeight);
|
|
690
|
+
}, [cropArea, scale, imagePosition]);
|
|
691
|
+
// Mouse down handler
|
|
692
|
+
const handleMouseDown = useCallback((e) => {
|
|
693
|
+
if (!cropArea || !canvasRef.current)
|
|
694
|
+
return;
|
|
695
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
696
|
+
const x = e.clientX - rect.left;
|
|
697
|
+
const y = e.clientY - rect.top;
|
|
698
|
+
// Check if clicking on image (for dragging image)
|
|
699
|
+
if (isPointOnImage(x, y)) {
|
|
700
|
+
setIsDraggingImage(true);
|
|
701
|
+
setImageDragStart({
|
|
702
|
+
x: e.clientX,
|
|
703
|
+
y: e.clientY,
|
|
704
|
+
offsetX: imagePosition.offsetX,
|
|
705
|
+
offsetY: imagePosition.offsetY,
|
|
706
|
+
});
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
// Crop area is fixed; no resize or move handlers.
|
|
710
|
+
}, [cropArea, isPointOnImage, imagePosition]);
|
|
711
|
+
// Mouse move handler for crop area
|
|
712
|
+
const handleMouseMove = useCallback((e) => {
|
|
713
|
+
if (!isDragging || !dragHandle || !dragStart || !cropArea)
|
|
714
|
+
return;
|
|
715
|
+
const deltaX = e.clientX - dragStart.x;
|
|
716
|
+
const deltaY = e.clientY - dragStart.y;
|
|
717
|
+
updateCropArea(dragHandle, deltaX, deltaY);
|
|
718
|
+
setDragStart({ x: e.clientX, y: e.clientY });
|
|
719
|
+
}, [isDragging, dragHandle, dragStart, cropArea, updateCropArea]);
|
|
720
|
+
// Mouse move handler for image dragging
|
|
721
|
+
const handleImageMouseMove = useCallback((e) => {
|
|
722
|
+
if (!isDraggingImage ||
|
|
723
|
+
!imageDragStart ||
|
|
724
|
+
!canvasRef.current ||
|
|
725
|
+
!imageRef.current ||
|
|
726
|
+
!cropArea)
|
|
727
|
+
return;
|
|
728
|
+
// Cancel previous RAF if exists
|
|
729
|
+
if (dragRafIdRef.current !== null) {
|
|
730
|
+
window.cancelAnimationFrame(dragRafIdRef.current);
|
|
731
|
+
}
|
|
732
|
+
dragRafIdRef.current = window.requestAnimationFrame(() => {
|
|
733
|
+
var _a;
|
|
734
|
+
dragRafIdRef.current = null;
|
|
735
|
+
const rect = (_a = canvasRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
|
|
736
|
+
const img = imageRef.current;
|
|
737
|
+
if (!rect || !img)
|
|
738
|
+
return;
|
|
739
|
+
const deltaX = e.clientX - imageDragStart.x;
|
|
740
|
+
const deltaY = e.clientY - imageDragStart.y;
|
|
741
|
+
const newOffsetX = imageDragStart.offsetX + deltaX;
|
|
742
|
+
const newOffsetY = imageDragStart.offsetY + deltaY;
|
|
743
|
+
// Calculate display size
|
|
744
|
+
const baseSize = getBaseDisplaySize(rect, img);
|
|
745
|
+
if (!baseSize)
|
|
746
|
+
return;
|
|
747
|
+
const displayWidth = baseSize.width * scale;
|
|
748
|
+
const displayHeight = baseSize.height * scale;
|
|
749
|
+
// Constrain position
|
|
750
|
+
const constrained = constrainImagePosition(newOffsetX, newOffsetY, displayWidth, displayHeight, cropArea);
|
|
751
|
+
// Check if position actually changed (avoid updates when at boundary)
|
|
752
|
+
// Use ref to get latest position value to avoid closure issues
|
|
753
|
+
const currentPosition = lastImagePositionRef.current || imagePosition;
|
|
754
|
+
if (!isImagePositionSimilar(currentPosition, constrained, IMAGE_POSITION_SIMILARITY_THRESHOLD)) {
|
|
755
|
+
setImagePosition(constrained);
|
|
756
|
+
scheduleDraw();
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
}, [
|
|
760
|
+
isDraggingImage,
|
|
761
|
+
imageDragStart,
|
|
762
|
+
scale,
|
|
763
|
+
cropArea,
|
|
764
|
+
scheduleDraw,
|
|
765
|
+
imagePosition,
|
|
766
|
+
]);
|
|
767
|
+
// Mouse up handler
|
|
768
|
+
const handleMouseUp = useCallback(() => {
|
|
769
|
+
// Cancel any pending drag RAF
|
|
770
|
+
if (dragRafIdRef.current !== null) {
|
|
771
|
+
window.cancelAnimationFrame(dragRafIdRef.current);
|
|
772
|
+
dragRafIdRef.current = null;
|
|
773
|
+
}
|
|
774
|
+
const wasDraggingCrop = isDragging;
|
|
775
|
+
const wasDraggingImage = isDraggingImage;
|
|
776
|
+
setIsDragging(false);
|
|
777
|
+
setDragHandle(null);
|
|
778
|
+
setDragStart(null);
|
|
779
|
+
setIsDraggingImage(false);
|
|
780
|
+
setImageDragStart(null);
|
|
781
|
+
// Update tag position and emit crop change after drag ends
|
|
782
|
+
if (wasDraggingCrop || wasDraggingImage) {
|
|
783
|
+
updateTagPosition();
|
|
784
|
+
if (cropArea && imageLoaded) {
|
|
785
|
+
emitCropChange(cropArea);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
// Call drag end callbacks
|
|
789
|
+
if (wasDraggingCrop && cropArea && onCropDragEnd) {
|
|
790
|
+
onCropDragEnd(cropArea);
|
|
791
|
+
}
|
|
792
|
+
if (wasDraggingImage && onImageDragEnd) {
|
|
793
|
+
onImageDragEnd();
|
|
794
|
+
}
|
|
795
|
+
}, [
|
|
796
|
+
isDragging,
|
|
797
|
+
isDraggingImage,
|
|
798
|
+
cropArea,
|
|
799
|
+
imageLoaded,
|
|
800
|
+
emitCropChange,
|
|
801
|
+
updateTagPosition,
|
|
802
|
+
onCropDragEnd,
|
|
803
|
+
onImageDragEnd,
|
|
804
|
+
]);
|
|
805
|
+
// Document events for dragging
|
|
806
|
+
useDocumentEvents(() => {
|
|
807
|
+
if (isDragging) {
|
|
808
|
+
return {
|
|
809
|
+
mousemove: handleMouseMove,
|
|
810
|
+
mouseup: handleMouseUp,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
if (isDraggingImage) {
|
|
814
|
+
return {
|
|
815
|
+
mousemove: handleImageMouseMove,
|
|
816
|
+
mouseup: handleMouseUp,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
return undefined;
|
|
820
|
+
}, [isDragging, isDraggingImage, handleMouseMove, handleImageMouseMove, handleMouseUp]);
|
|
821
|
+
// Handle scale change with center point preservation
|
|
822
|
+
const handleScaleChange = useCallback((newScale) => {
|
|
823
|
+
if (!canvasRef.current || !imageRef.current || !cropArea) {
|
|
824
|
+
setScale(newScale);
|
|
825
|
+
onScaleChange === null || onScaleChange === void 0 ? void 0 : onScaleChange(newScale);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
829
|
+
const baseSize = getBaseDisplaySize(rect, imageRef.current);
|
|
830
|
+
if (!baseSize) {
|
|
831
|
+
setScale(newScale);
|
|
832
|
+
onScaleChange === null || onScaleChange === void 0 ? void 0 : onScaleChange(newScale);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
// Calculate center point of crop area in canvas coordinates
|
|
836
|
+
const centerX = cropArea.x + cropArea.width / 2;
|
|
837
|
+
const centerY = cropArea.y + cropArea.height / 2;
|
|
838
|
+
// Calculate current image position relative to center
|
|
839
|
+
const currentDisplayWidth = baseSize.width * scale;
|
|
840
|
+
const currentDisplayHeight = baseSize.height * scale;
|
|
841
|
+
const currentCenterOffsetX = centerX - (imagePosition.offsetX + currentDisplayWidth / 2);
|
|
842
|
+
const currentCenterOffsetY = centerY - (imagePosition.offsetY + currentDisplayHeight / 2);
|
|
843
|
+
// Calculate new display size
|
|
844
|
+
const newDisplayWidth = baseSize.width * newScale;
|
|
845
|
+
const newDisplayHeight = baseSize.height * newScale;
|
|
846
|
+
// Calculate new position to keep center point
|
|
847
|
+
const newOffsetX = centerX - newDisplayWidth / 2 - currentCenterOffsetX;
|
|
848
|
+
const newOffsetY = centerY - newDisplayHeight / 2 - currentCenterOffsetY;
|
|
849
|
+
// Constrain new position
|
|
850
|
+
const constrained = constrainImagePosition(newOffsetX, newOffsetY, newDisplayWidth, newDisplayHeight, cropArea);
|
|
851
|
+
setScale(newScale);
|
|
852
|
+
setImagePosition(constrained);
|
|
853
|
+
onScaleChange === null || onScaleChange === void 0 ? void 0 : onScaleChange(newScale);
|
|
854
|
+
}, [cropArea, imagePosition, scale, onScaleChange]);
|
|
855
|
+
// Handle slider change
|
|
856
|
+
const handleSliderChange = useCallback((value) => {
|
|
857
|
+
handleScaleChange(value);
|
|
858
|
+
}, [handleScaleChange]);
|
|
859
|
+
// Handle wheel (trackpad) zoom
|
|
860
|
+
const handleWheel = useCallback((e) => {
|
|
861
|
+
if (!cropArea)
|
|
862
|
+
return;
|
|
863
|
+
e.preventDefault();
|
|
864
|
+
const delta = e.deltaY > 0 ? -0.05 : 0.05;
|
|
865
|
+
const newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale + delta));
|
|
866
|
+
handleScaleChange(newScale);
|
|
867
|
+
}, [scale, cropArea, handleScaleChange]);
|
|
868
|
+
// Add wheel event listener with passive: false to allow preventDefault
|
|
869
|
+
useEffect(() => {
|
|
870
|
+
const canvas = canvasRef.current;
|
|
871
|
+
if (!canvas)
|
|
872
|
+
return;
|
|
873
|
+
canvas.addEventListener('wheel', handleWheel, { passive: false });
|
|
874
|
+
return () => {
|
|
875
|
+
canvas.removeEventListener('wheel', handleWheel);
|
|
876
|
+
};
|
|
877
|
+
}, [handleWheel]);
|
|
878
|
+
// Update cursor style
|
|
879
|
+
const cursorStyle = useMemo(() => {
|
|
880
|
+
if (!cropArea)
|
|
881
|
+
return 'default';
|
|
882
|
+
if (isDragging || isDraggingImage)
|
|
883
|
+
return 'grabbing';
|
|
884
|
+
return 'default';
|
|
885
|
+
}, [cropArea, isDragging, isDraggingImage]);
|
|
886
|
+
return (jsxs("div", { className: cropperClasses.element, ref: elementRef, children: [jsx(Component, { ...rest, className: cx(cropperClasses.host, cropperClasses.size(size), className), onMouseDown: handleMouseDown, ref: composedRef, style: { cursor: cursorStyle, width: '100%', ...rest.style }, children: children }), cropArea && tagPosition && (jsxs(Typography, { className: cropperClasses.tag, color: "text-fixed-light", style: {
|
|
887
|
+
left: tagPosition.left,
|
|
888
|
+
top: tagPosition.top,
|
|
889
|
+
}, children: [Math.round((tagCropArea || cropArea).width), " \u00D7", ' ', Math.round((tagCropArea || cropArea).height), " px"] })), jsx("div", { className: cropperClasses.controls, children: jsx(Slider, { min: MIN_SCALE, max: MAX_SCALE, step: SCALE_STEP, value: scale, onChange: handleSliderChange, suffixIcon: PlusIcon, prefixIcon: MinusIcon }) })] }));
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
export { CropperElement as default };
|