@comitor/ui 2.0.0 → 2.1.0
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/LICENSE +35 -0
- package/README.md +14 -6
- package/dist/components/ui/action-bar-constants.d.ts +31 -0
- package/dist/components/ui/action-bar-constants.js +9 -0
- package/dist/components/ui/action-bar-constants.js.map +1 -0
- package/dist/components/ui/action-bar.d.ts +67 -0
- package/dist/components/ui/action-bar.js +522 -0
- package/dist/components/ui/action-bar.js.map +1 -0
- package/dist/components/ui/badge-overflow.d.ts +30 -0
- package/dist/components/ui/badge-overflow.js +179 -0
- package/dist/components/ui/badge-overflow.js.map +1 -0
- package/dist/components/ui/banner.d.ts +79 -0
- package/dist/components/ui/banner.js +518 -0
- package/dist/components/ui/banner.js.map +1 -0
- package/dist/components/ui/circular-progress.d.ts +50 -0
- package/dist/components/ui/circular-progress.js +266 -0
- package/dist/components/ui/circular-progress.js.map +1 -0
- package/dist/components/ui/cropper-constants.d.ts +34 -0
- package/dist/components/ui/cropper-constants.js +10 -0
- package/dist/components/ui/cropper-constants.js.map +1 -0
- package/dist/components/ui/cropper.d.ts +106 -0
- package/dist/components/ui/cropper.js +1269 -0
- package/dist/components/ui/cropper.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +1 -1
- package/dist/components/ui/drawer.js +2 -2
- package/dist/components/ui/drawer.js.map +1 -1
- package/dist/components/ui/kanban-constants.d.ts +61 -0
- package/dist/components/ui/kanban-constants.js +21 -0
- package/dist/components/ui/kanban-constants.js.map +1 -0
- package/dist/components/ui/kanban.d.ts +72 -0
- package/dist/components/ui/kanban.js +787 -0
- package/dist/components/ui/kanban.js.map +1 -0
- package/dist/components/ui/key-value-constants.d.ts +33 -0
- package/dist/components/ui/key-value-constants.js +11 -0
- package/dist/components/ui/key-value-constants.js.map +1 -0
- package/dist/components/ui/key-value.d.ts +96 -0
- package/dist/components/ui/key-value.js +641 -0
- package/dist/components/ui/key-value.js.map +1 -0
- package/dist/components/ui/mask-input-constants.d.ts +99 -0
- package/dist/components/ui/mask-input-constants.js +601 -0
- package/dist/components/ui/mask-input-constants.js.map +1 -0
- package/dist/components/ui/mask-input.d.ts +36 -0
- package/dist/components/ui/mask-input.js +558 -0
- package/dist/components/ui/mask-input.js.map +1 -0
- package/dist/components/ui/mention.d.ts +31 -0
- package/dist/components/ui/mention.js +128 -0
- package/dist/components/ui/mention.js.map +1 -0
- package/dist/components/ui/phone-input-constants.d.ts +54 -0
- package/dist/components/ui/phone-input-constants.js +9 -0
- package/dist/components/ui/phone-input-constants.js.map +1 -0
- package/dist/components/ui/phone-input.d.ts +85 -0
- package/dist/components/ui/phone-input.js +743 -0
- package/dist/components/ui/phone-input.js.map +1 -0
- package/dist/components/ui/rating.d.ts +56 -0
- package/dist/components/ui/rating.js +646 -0
- package/dist/components/ui/rating.js.map +1 -0
- package/dist/components/ui/responsive-dialog.d.ts +35 -0
- package/dist/components/ui/responsive-dialog.js +193 -0
- package/dist/components/ui/responsive-dialog.js.map +1 -0
- package/dist/components/ui/scroll-spy.d.ts +42 -0
- package/dist/components/ui/scroll-spy.js +334 -0
- package/dist/components/ui/scroll-spy.js.map +1 -0
- package/dist/components/ui/stepper-constants.d.ts +29 -0
- package/dist/components/ui/stepper-constants.js +9 -0
- package/dist/components/ui/stepper-constants.js.map +1 -0
- package/dist/components/ui/stepper.d.ts +80 -0
- package/dist/components/ui/stepper.js +902 -0
- package/dist/components/ui/stepper.js.map +1 -0
- package/dist/components/ui/tags-input.d.ts +41 -0
- package/dist/components/ui/tags-input.js +127 -0
- package/dist/components/ui/tags-input.js.map +1 -0
- package/dist/components/ui/time-picker-constants.d.ts +48 -0
- package/dist/components/ui/time-picker-constants.js +14 -0
- package/dist/components/ui/time-picker-constants.js.map +1 -0
- package/dist/components/ui/time-picker.d.ts +115 -0
- package/dist/components/ui/time-picker.js +1620 -0
- package/dist/components/ui/time-picker.js.map +1 -0
- package/dist/components/ui/timeline.d.ts +32 -0
- package/dist/components/ui/timeline.js +559 -0
- package/dist/components/ui/timeline.js.map +1 -0
- package/dist/components/ui/tour-constants.d.ts +39 -0
- package/dist/components/ui/tour-constants.js +12 -0
- package/dist/components/ui/tour-constants.js.map +1 -0
- package/dist/components/ui/tour.d.ts +124 -0
- package/dist/components/ui/tour.js +1207 -0
- package/dist/components/ui/tour.js.map +1 -0
- package/dist/data-grid/data-grid-cell-wrapper.js +1 -1
- package/dist/data-grid/data-grid-cell-wrapper.js.map +1 -1
- package/dist/data-grid/data-grid-context-menu.js +1 -1
- package/dist/data-grid/data-grid-context-menu.js.map +1 -1
- package/dist/data-grid/data-grid-paste-dialog.js +1 -1
- package/dist/data-grid/data-grid-paste-dialog.js.map +1 -1
- package/dist/data-grid/data-grid-row.js +1 -1
- package/dist/data-grid/data-grid-row.js.map +1 -1
- package/dist/data-grid/data-grid-search.js +1 -1
- package/dist/data-grid/data-grid-search.js.map +1 -1
- package/dist/data-grid/data-grid.js +1 -1
- package/dist/data-grid/data-grid.js.map +1 -1
- package/dist/data-grid/sortable.js +1 -1
- package/dist/data-grid/sortable.js.map +1 -1
- package/dist/data-grid/use-data-grid-undo-redo.js +2 -2
- package/dist/data-grid/use-data-grid-undo-redo.js.map +1 -1
- package/dist/data-grid/use-data-grid.js +3 -3
- package/dist/data-grid/use-data-grid.js.map +1 -1
- package/dist/index.d.ts +31 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/compose-refs.js.map +1 -0
- package/dist/lib/use-as-ref.js.map +1 -0
- package/dist/lib/use-is-mobile.d.ts +10 -0
- package/dist/lib/use-is-mobile.js +21 -0
- package/dist/lib/use-is-mobile.js.map +1 -0
- package/dist/lib/use-isomorphic-layout-effect.js.map +1 -0
- package/dist/lib/use-lazy-ref.js.map +1 -0
- package/dist/lib/visually-hidden-input.d.ts +12 -0
- package/dist/lib/visually-hidden-input.js +105 -0
- package/dist/lib/visually-hidden-input.js.map +1 -0
- package/package.json +5 -1
- package/styles.css +19 -0
- package/dist/data-grid/compose-refs.js.map +0 -1
- package/dist/data-grid/use-as-ref.js.map +0 -1
- package/dist/data-grid/use-isomorphic-layout-effect.js.map +0 -1
- package/dist/data-grid/use-lazy-ref.js.map +0 -1
- /package/dist/{data-grid → lib}/compose-refs.d.ts +0 -0
- /package/dist/{data-grid → lib}/compose-refs.js +0 -0
- /package/dist/{data-grid → lib}/use-as-ref.d.ts +0 -0
- /package/dist/{data-grid → lib}/use-as-ref.js +0 -0
- /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.d.ts +0 -0
- /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.js +0 -0
- /package/dist/{data-grid → lib}/use-lazy-ref.d.ts +0 -0
- /package/dist/{data-grid → lib}/use-lazy-ref.js +0 -0
|
@@ -0,0 +1,1269 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
4
|
+
import { cva } from "class-variance-authority";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { cn } from "../../lib/cn.js";
|
|
7
|
+
import { useComposedRefs } from "../../lib/compose-refs.js";
|
|
8
|
+
import { useAsRef } from "../../lib/use-as-ref.js";
|
|
9
|
+
import { useIsomorphicLayoutEffect } from "../../lib/use-isomorphic-layout-effect.js";
|
|
10
|
+
import { useLazyRef } from "../../lib/use-lazy-ref.js";
|
|
11
|
+
import { DEFAULT_CROPPER_LABELS } from "./cropper-constants.js";
|
|
12
|
+
const ROOT_NAME = "Cropper";
|
|
13
|
+
const ROOT_IMPL_NAME = "CropperImpl";
|
|
14
|
+
const IMAGE_NAME = "CropperImage";
|
|
15
|
+
const VIDEO_NAME = "CropperVideo";
|
|
16
|
+
const AREA_NAME = "CropperArea";
|
|
17
|
+
const MAX_CACHE_SIZE = 200;
|
|
18
|
+
const ZOOM_IN_KEYS = /* @__PURE__ */ new Set(["+", "="]);
|
|
19
|
+
const ZOOM_OUT_KEYS = /* @__PURE__ */ new Set(["-", "_"]);
|
|
20
|
+
const KEYBOARD_ZOOM_STEP = 0.1;
|
|
21
|
+
function getDevicePixelRatio() {
|
|
22
|
+
return typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
|
|
23
|
+
}
|
|
24
|
+
function getServerDevicePixelRatio() {
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
function subscribeToDevicePixelRatio(onChange) {
|
|
28
|
+
window.addEventListener("resize", onChange);
|
|
29
|
+
return () => window.removeEventListener("resize", onChange);
|
|
30
|
+
}
|
|
31
|
+
function useDevicePixelRatio() {
|
|
32
|
+
return React.useSyncExternalStore(subscribeToDevicePixelRatio, getDevicePixelRatio, getServerDevicePixelRatio);
|
|
33
|
+
}
|
|
34
|
+
const rotationSizeCache = /* @__PURE__ */ new Map();
|
|
35
|
+
const cropSizeCache = /* @__PURE__ */ new Map();
|
|
36
|
+
const croppedAreaCache = /* @__PURE__ */ new Map();
|
|
37
|
+
const onPositionClampCache = /* @__PURE__ */ new Map();
|
|
38
|
+
function clamp(value, min, max) {
|
|
39
|
+
return Math.min(Math.max(value, min), max);
|
|
40
|
+
}
|
|
41
|
+
function quantize(n, step = 2 / getDevicePixelRatio()) {
|
|
42
|
+
return Math.round(n / step) * step;
|
|
43
|
+
}
|
|
44
|
+
function quantizePosition(n, step = 4 / getDevicePixelRatio()) {
|
|
45
|
+
return Math.round(n / step) * step;
|
|
46
|
+
}
|
|
47
|
+
function quantizeZoom(n, step = 0.01) {
|
|
48
|
+
return Math.round(n / step) * step;
|
|
49
|
+
}
|
|
50
|
+
function quantizeRotation(n, step = 1) {
|
|
51
|
+
return Math.round(n / step) * step;
|
|
52
|
+
}
|
|
53
|
+
function snapToDevicePixel(n, dpr) {
|
|
54
|
+
return Math.round(n * dpr) / dpr;
|
|
55
|
+
}
|
|
56
|
+
function lruGet(map, key) {
|
|
57
|
+
const v = map.get(key);
|
|
58
|
+
if (v !== void 0) {
|
|
59
|
+
map.delete(key);
|
|
60
|
+
map.set(key, v);
|
|
61
|
+
}
|
|
62
|
+
return v;
|
|
63
|
+
}
|
|
64
|
+
function lruSet(map, key, val, max = MAX_CACHE_SIZE) {
|
|
65
|
+
if (map.has(key)) {
|
|
66
|
+
map.delete(key);
|
|
67
|
+
}
|
|
68
|
+
map.set(key, val);
|
|
69
|
+
if (map.size > max) {
|
|
70
|
+
const firstKey = map.keys().next().value;
|
|
71
|
+
if (firstKey !== void 0) {
|
|
72
|
+
map.delete(firstKey);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function getDistanceBetweenPoints(pointA, pointB) {
|
|
77
|
+
return Math.sqrt((pointA.y - pointB.y) ** 2 + (pointA.x - pointB.x) ** 2);
|
|
78
|
+
}
|
|
79
|
+
function getCenter(a, b) {
|
|
80
|
+
return {
|
|
81
|
+
x: (b.x + a.x) * 0.5,
|
|
82
|
+
y: (b.y + a.y) * 0.5
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function getRotationBetweenPoints(pointA, pointB) {
|
|
86
|
+
return Math.atan2(pointB.y - pointA.y, pointB.x - pointA.x) * 180 / Math.PI;
|
|
87
|
+
}
|
|
88
|
+
function getRadianAngle(degreeValue) {
|
|
89
|
+
return degreeValue * Math.PI / 180;
|
|
90
|
+
}
|
|
91
|
+
function rotateSize(width, height, rotation) {
|
|
92
|
+
const cacheKey = `${quantize(width)}-${quantize(height)}-${quantizeRotation(rotation)}`;
|
|
93
|
+
const cached = lruGet(rotationSizeCache, cacheKey);
|
|
94
|
+
if (cached) {
|
|
95
|
+
return cached;
|
|
96
|
+
}
|
|
97
|
+
const rotRad = getRadianAngle(rotation);
|
|
98
|
+
const cosRot = Math.cos(rotRad);
|
|
99
|
+
const sinRot = Math.sin(rotRad);
|
|
100
|
+
const result = {
|
|
101
|
+
width: Math.abs(cosRot * width) + Math.abs(sinRot * height),
|
|
102
|
+
height: Math.abs(sinRot * width) + Math.abs(cosRot * height)
|
|
103
|
+
};
|
|
104
|
+
lruSet(rotationSizeCache, cacheKey, result, MAX_CACHE_SIZE);
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
function getCropSize(mediaWidth, mediaHeight, contentWidth, contentHeight, aspect, rotation = 0) {
|
|
108
|
+
const cacheKey = `${quantize(mediaWidth, 8)}-${quantize(mediaHeight, 8)}-${quantize(contentWidth, 8)}-${quantize(contentHeight, 8)}-${quantize(aspect, 0.01)}-${quantizeRotation(rotation)}`;
|
|
109
|
+
const cached = lruGet(cropSizeCache, cacheKey);
|
|
110
|
+
if (cached) {
|
|
111
|
+
return cached;
|
|
112
|
+
}
|
|
113
|
+
const { width, height } = rotateSize(mediaWidth, mediaHeight, rotation);
|
|
114
|
+
const fittingWidth = Math.min(width, contentWidth);
|
|
115
|
+
const fittingHeight = Math.min(height, contentHeight);
|
|
116
|
+
const result = fittingWidth > fittingHeight * aspect ? {
|
|
117
|
+
width: fittingHeight * aspect,
|
|
118
|
+
height: fittingHeight
|
|
119
|
+
} : {
|
|
120
|
+
width: fittingWidth,
|
|
121
|
+
height: fittingWidth / aspect
|
|
122
|
+
};
|
|
123
|
+
lruSet(cropSizeCache, cacheKey, result, MAX_CACHE_SIZE);
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
function onPositionClamp(position, mediaSize, cropSize, zoom, rotation = 0) {
|
|
127
|
+
const cacheKey = `${quantize(mediaSize.width)}-${quantize(mediaSize.height)}-${quantize(cropSize.width)}-${quantize(cropSize.height)}-${quantizeZoom(zoom)}-${quantizeRotation(rotation)}`;
|
|
128
|
+
let maxPosition = lruGet(onPositionClampCache, cacheKey);
|
|
129
|
+
if (!maxPosition) {
|
|
130
|
+
const { width, height } = rotateSize(mediaSize.width, mediaSize.height, rotation);
|
|
131
|
+
maxPosition = {
|
|
132
|
+
x: width * zoom * 0.5 - cropSize.width * 0.5,
|
|
133
|
+
y: height * zoom * 0.5 - cropSize.height * 0.5
|
|
134
|
+
};
|
|
135
|
+
lruSet(onPositionClampCache, cacheKey, maxPosition, MAX_CACHE_SIZE);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
x: clamp(position.x, -maxPosition.x, maxPosition.x),
|
|
139
|
+
y: clamp(position.y, -maxPosition.y, maxPosition.y)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function getCroppedArea(crop, mediaSize, cropSize, aspect, zoom, rotation = 0, allowOverflow = false) {
|
|
143
|
+
const cacheKey = `${quantizePosition(crop.x)}-${quantizePosition(crop.y)}-${quantize(mediaSize.width)}-${quantize(mediaSize.height)}-${quantize(mediaSize.naturalWidth)}-${quantize(mediaSize.naturalHeight)}-${quantize(cropSize.width)}-${quantize(cropSize.height)}-${quantize(aspect, 0.01)}-${quantizeZoom(zoom)}-${quantizeRotation(rotation)}-${allowOverflow}`;
|
|
144
|
+
const cached = lruGet(croppedAreaCache, cacheKey);
|
|
145
|
+
if (cached) return cached;
|
|
146
|
+
const onAreaLimit = !allowOverflow ? (max, value) => Math.min(max, Math.max(0, value)) : (_max, value) => value;
|
|
147
|
+
const mediaBBoxSize = rotateSize(mediaSize.width, mediaSize.height, rotation);
|
|
148
|
+
const mediaNaturalBBoxSize = rotateSize(mediaSize.naturalWidth, mediaSize.naturalHeight, rotation);
|
|
149
|
+
const croppedAreaPercentages = {
|
|
150
|
+
x: onAreaLimit(
|
|
151
|
+
100,
|
|
152
|
+
((mediaBBoxSize.width - cropSize.width / zoom) / 2 - crop.x / zoom) / mediaBBoxSize.width * 100
|
|
153
|
+
),
|
|
154
|
+
y: onAreaLimit(
|
|
155
|
+
100,
|
|
156
|
+
((mediaBBoxSize.height - cropSize.height / zoom) / 2 - crop.y / zoom) / mediaBBoxSize.height * 100
|
|
157
|
+
),
|
|
158
|
+
width: onAreaLimit(100, cropSize.width / mediaBBoxSize.width * 100 / zoom),
|
|
159
|
+
height: onAreaLimit(100, cropSize.height / mediaBBoxSize.height * 100 / zoom)
|
|
160
|
+
};
|
|
161
|
+
const widthInPixels = Math.round(
|
|
162
|
+
onAreaLimit(mediaNaturalBBoxSize.width, croppedAreaPercentages.width * mediaNaturalBBoxSize.width / 100)
|
|
163
|
+
);
|
|
164
|
+
const heightInPixels = Math.round(
|
|
165
|
+
onAreaLimit(mediaNaturalBBoxSize.height, croppedAreaPercentages.height * mediaNaturalBBoxSize.height / 100)
|
|
166
|
+
);
|
|
167
|
+
const isImageWiderThanHigh = mediaNaturalBBoxSize.width >= mediaNaturalBBoxSize.height * aspect;
|
|
168
|
+
const sizePixels = isImageWiderThanHigh ? {
|
|
169
|
+
width: Math.round(heightInPixels * aspect),
|
|
170
|
+
height: heightInPixels
|
|
171
|
+
} : {
|
|
172
|
+
width: widthInPixels,
|
|
173
|
+
height: Math.round(widthInPixels / aspect)
|
|
174
|
+
};
|
|
175
|
+
const croppedAreaPixels = {
|
|
176
|
+
...sizePixels,
|
|
177
|
+
x: Math.round(
|
|
178
|
+
onAreaLimit(
|
|
179
|
+
mediaNaturalBBoxSize.width - sizePixels.width,
|
|
180
|
+
croppedAreaPercentages.x * mediaNaturalBBoxSize.width / 100
|
|
181
|
+
)
|
|
182
|
+
),
|
|
183
|
+
y: Math.round(
|
|
184
|
+
onAreaLimit(
|
|
185
|
+
mediaNaturalBBoxSize.height - sizePixels.height,
|
|
186
|
+
croppedAreaPercentages.y * mediaNaturalBBoxSize.height / 100
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
};
|
|
190
|
+
const result = { croppedAreaPercentages, croppedAreaPixels };
|
|
191
|
+
lruSet(croppedAreaCache, cacheKey, result, MAX_CACHE_SIZE);
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
const StoreContext = React.createContext(null);
|
|
195
|
+
function useStoreContext(consumerName) {
|
|
196
|
+
const context = React.useContext(StoreContext);
|
|
197
|
+
if (!context) {
|
|
198
|
+
throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong \`${ROOT_NAME}\``);
|
|
199
|
+
}
|
|
200
|
+
return context;
|
|
201
|
+
}
|
|
202
|
+
function useStore(selector) {
|
|
203
|
+
const store = useStoreContext("useCropper");
|
|
204
|
+
const getSnapshot = React.useCallback(() => selector(store.getState()), [store, selector]);
|
|
205
|
+
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
|
|
206
|
+
}
|
|
207
|
+
const CropperContext = React.createContext(null);
|
|
208
|
+
function useCropperContext(consumerName) {
|
|
209
|
+
const context = React.useContext(CropperContext);
|
|
210
|
+
if (!context) {
|
|
211
|
+
throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong \`${ROOT_NAME}\``);
|
|
212
|
+
}
|
|
213
|
+
return context;
|
|
214
|
+
}
|
|
215
|
+
function Cropper(props) {
|
|
216
|
+
const {
|
|
217
|
+
/*
|
|
218
|
+
* ⚠ Khác thượng nguồn: bản gốc gán mặc định ngay ở đây (`crop = { x: 0, y: 0 }`, `zoom = 1`,
|
|
219
|
+
* `rotation = 0`), nên nhánh `!== undefined` trong effect đồng bộ bên dưới LUÔN đúng, và
|
|
220
|
+
* `{ x: 0, y: 0 }` là object MỚI mỗi lượt render. Hệ quả ở chế độ không kiểm soát: mỗi lần
|
|
221
|
+
* component cha render lại — kể cả vì chính `onCropComplete` gọi `setState` ở cha — vị trí và
|
|
222
|
+
* độ phóng bị kéo về mặc định ngay sau khi người dùng vừa chỉnh xong. Mặc định nay chỉ dùng
|
|
223
|
+
* cho state KHỞI TẠO; effect chỉ đồng bộ prop nào thật sự được truyền, đúng ý của nhánh đó.
|
|
224
|
+
*/
|
|
225
|
+
crop: cropProp,
|
|
226
|
+
zoom: zoomProp,
|
|
227
|
+
minZoom = 1,
|
|
228
|
+
maxZoom = 3,
|
|
229
|
+
zoomSpeed = 1,
|
|
230
|
+
rotation: rotationProp,
|
|
231
|
+
keyboardStep = 1,
|
|
232
|
+
aspectRatio = 4 / 3,
|
|
233
|
+
shape = "rectangle",
|
|
234
|
+
objectFit = "contain",
|
|
235
|
+
allowOverflow = false,
|
|
236
|
+
preventScrollZoom = false,
|
|
237
|
+
withGrid = false,
|
|
238
|
+
labels: labelsProp,
|
|
239
|
+
onCropChange,
|
|
240
|
+
onCropSizeChange,
|
|
241
|
+
onCropAreaChange,
|
|
242
|
+
onCropComplete,
|
|
243
|
+
onZoomChange,
|
|
244
|
+
onRotationChange,
|
|
245
|
+
onMediaLoaded,
|
|
246
|
+
onInteractionStart,
|
|
247
|
+
onInteractionEnd,
|
|
248
|
+
className,
|
|
249
|
+
...rootProps
|
|
250
|
+
} = props;
|
|
251
|
+
const listenersRef = useLazyRef(() => /* @__PURE__ */ new Set());
|
|
252
|
+
const stateRef = useLazyRef(() => ({
|
|
253
|
+
crop: cropProp ?? { x: 0, y: 0 },
|
|
254
|
+
zoom: zoomProp ?? 1,
|
|
255
|
+
rotation: rotationProp ?? 0,
|
|
256
|
+
mediaSize: null,
|
|
257
|
+
cropSize: null,
|
|
258
|
+
isDragging: false,
|
|
259
|
+
isWheelZooming: false
|
|
260
|
+
}));
|
|
261
|
+
const propsRef = useAsRef({
|
|
262
|
+
onCropChange,
|
|
263
|
+
onCropSizeChange,
|
|
264
|
+
onCropAreaChange,
|
|
265
|
+
onCropComplete,
|
|
266
|
+
onZoomChange,
|
|
267
|
+
onRotationChange,
|
|
268
|
+
onMediaLoaded,
|
|
269
|
+
onInteractionStart,
|
|
270
|
+
onInteractionEnd
|
|
271
|
+
});
|
|
272
|
+
const rootRef = React.useRef(null);
|
|
273
|
+
const store = React.useMemo(() => {
|
|
274
|
+
let isBatching = false;
|
|
275
|
+
let raf = null;
|
|
276
|
+
function notifyCropAreaChange() {
|
|
277
|
+
if (raf != null) return;
|
|
278
|
+
raf = requestAnimationFrame(() => {
|
|
279
|
+
raf = null;
|
|
280
|
+
const s = stateRef.current;
|
|
281
|
+
if (s?.mediaSize && s.cropSize && propsRef.current.onCropAreaChange) {
|
|
282
|
+
const { croppedAreaPercentages, croppedAreaPixels } = getCroppedArea(
|
|
283
|
+
s.crop,
|
|
284
|
+
s.mediaSize,
|
|
285
|
+
s.cropSize,
|
|
286
|
+
aspectRatio,
|
|
287
|
+
s.zoom,
|
|
288
|
+
s.rotation
|
|
289
|
+
);
|
|
290
|
+
propsRef.current.onCropAreaChange(croppedAreaPercentages, croppedAreaPixels);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
subscribe: (cb) => {
|
|
296
|
+
listenersRef.current.add(cb);
|
|
297
|
+
return () => listenersRef.current.delete(cb);
|
|
298
|
+
},
|
|
299
|
+
getState: () => stateRef.current,
|
|
300
|
+
setState: (key, value) => {
|
|
301
|
+
if (Object.is(stateRef.current[key], value)) return;
|
|
302
|
+
stateRef.current[key] = value;
|
|
303
|
+
if (key === "crop" && typeof value === "object" && value && "x" in value) {
|
|
304
|
+
propsRef.current.onCropChange?.(value);
|
|
305
|
+
} else if (key === "zoom" && typeof value === "number") {
|
|
306
|
+
propsRef.current.onZoomChange?.(value);
|
|
307
|
+
} else if (key === "rotation" && typeof value === "number") {
|
|
308
|
+
propsRef.current.onRotationChange?.(value);
|
|
309
|
+
} else if (key === "cropSize" && typeof value === "object" && value && "width" in value) {
|
|
310
|
+
propsRef.current.onCropSizeChange?.(value);
|
|
311
|
+
} else if (key === "mediaSize" && typeof value === "object" && value && "naturalWidth" in value) {
|
|
312
|
+
propsRef.current.onMediaLoaded?.(value);
|
|
313
|
+
} else if (key === "isDragging") {
|
|
314
|
+
if (value) {
|
|
315
|
+
propsRef.current.onInteractionStart?.();
|
|
316
|
+
} else {
|
|
317
|
+
propsRef.current.onInteractionEnd?.();
|
|
318
|
+
const currentState = stateRef.current;
|
|
319
|
+
if (currentState?.mediaSize && currentState.cropSize && propsRef.current.onCropComplete) {
|
|
320
|
+
const { croppedAreaPercentages, croppedAreaPixels } = getCroppedArea(
|
|
321
|
+
currentState.crop,
|
|
322
|
+
currentState.mediaSize,
|
|
323
|
+
currentState.cropSize,
|
|
324
|
+
aspectRatio,
|
|
325
|
+
currentState.zoom,
|
|
326
|
+
currentState.rotation
|
|
327
|
+
);
|
|
328
|
+
propsRef.current.onCropComplete(croppedAreaPercentages, croppedAreaPixels);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if ((key === "crop" || key === "zoom" || key === "rotation" || key === "mediaSize" || key === "cropSize") && propsRef.current.onCropAreaChange) {
|
|
333
|
+
notifyCropAreaChange();
|
|
334
|
+
}
|
|
335
|
+
if (!isBatching) {
|
|
336
|
+
store.notify();
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
notify: () => {
|
|
340
|
+
for (const cb of listenersRef.current) {
|
|
341
|
+
cb();
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
batch: (fn) => {
|
|
345
|
+
if (isBatching) {
|
|
346
|
+
fn();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
isBatching = true;
|
|
350
|
+
try {
|
|
351
|
+
fn();
|
|
352
|
+
} finally {
|
|
353
|
+
isBatching = false;
|
|
354
|
+
store.notify();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}, [listenersRef, stateRef, propsRef, aspectRatio]);
|
|
359
|
+
useIsomorphicLayoutEffect(() => {
|
|
360
|
+
const updates = {};
|
|
361
|
+
let hasUpdates = false;
|
|
362
|
+
let shouldRecompute = false;
|
|
363
|
+
if (cropProp !== void 0) {
|
|
364
|
+
const currentState = store.getState();
|
|
365
|
+
if (!Object.is(currentState.crop, cropProp)) {
|
|
366
|
+
updates.crop = cropProp;
|
|
367
|
+
hasUpdates = true;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (zoomProp !== void 0) {
|
|
371
|
+
const currentState = store.getState();
|
|
372
|
+
if (currentState.zoom !== zoomProp) {
|
|
373
|
+
updates.zoom = zoomProp;
|
|
374
|
+
hasUpdates = true;
|
|
375
|
+
shouldRecompute = true;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (rotationProp !== void 0) {
|
|
379
|
+
const currentState = store.getState();
|
|
380
|
+
if (currentState.rotation !== rotationProp) {
|
|
381
|
+
updates.rotation = rotationProp;
|
|
382
|
+
hasUpdates = true;
|
|
383
|
+
shouldRecompute = true;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (hasUpdates) {
|
|
387
|
+
store.batch(() => {
|
|
388
|
+
Object.entries(updates).forEach(([key, value]) => {
|
|
389
|
+
store.setState(key, value);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
if (shouldRecompute && rootRef.current) {
|
|
393
|
+
requestAnimationFrame(() => {
|
|
394
|
+
const currentState = store.getState();
|
|
395
|
+
if (currentState.cropSize && currentState.mediaSize) {
|
|
396
|
+
const newPosition = !allowOverflow ? onPositionClamp(
|
|
397
|
+
currentState.crop,
|
|
398
|
+
currentState.mediaSize,
|
|
399
|
+
currentState.cropSize,
|
|
400
|
+
currentState.zoom,
|
|
401
|
+
currentState.rotation
|
|
402
|
+
) : currentState.crop;
|
|
403
|
+
if (Math.abs(newPosition.x - currentState.crop.x) > 1e-3 || Math.abs(newPosition.y - currentState.crop.y) > 1e-3) {
|
|
404
|
+
store.setState("crop", newPosition);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}, [cropProp, zoomProp, rotationProp, store, allowOverflow]);
|
|
411
|
+
const labels = React.useMemo(() => ({ ...DEFAULT_CROPPER_LABELS, ...labelsProp }), [labelsProp]);
|
|
412
|
+
const contextValue = React.useMemo(
|
|
413
|
+
() => ({
|
|
414
|
+
minZoom,
|
|
415
|
+
maxZoom,
|
|
416
|
+
zoomSpeed,
|
|
417
|
+
keyboardStep,
|
|
418
|
+
aspectRatio,
|
|
419
|
+
shape,
|
|
420
|
+
objectFit,
|
|
421
|
+
preventScrollZoom,
|
|
422
|
+
allowOverflow,
|
|
423
|
+
withGrid,
|
|
424
|
+
rootRef,
|
|
425
|
+
labels
|
|
426
|
+
}),
|
|
427
|
+
[
|
|
428
|
+
minZoom,
|
|
429
|
+
maxZoom,
|
|
430
|
+
zoomSpeed,
|
|
431
|
+
keyboardStep,
|
|
432
|
+
aspectRatio,
|
|
433
|
+
shape,
|
|
434
|
+
objectFit,
|
|
435
|
+
preventScrollZoom,
|
|
436
|
+
allowOverflow,
|
|
437
|
+
withGrid,
|
|
438
|
+
labels
|
|
439
|
+
]
|
|
440
|
+
);
|
|
441
|
+
return /* @__PURE__ */ jsx(StoreContext.Provider, { value: store, children: /* @__PURE__ */ jsx(CropperContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("div", { "data-slot": "cropper-wrapper", className: cn("relative size-full overflow-hidden", className), children: /* @__PURE__ */ jsx(CropperImpl, { ...rootProps }) }) }) });
|
|
442
|
+
}
|
|
443
|
+
function CropperImpl(props) {
|
|
444
|
+
const {
|
|
445
|
+
onWheelZoom: onWheelZoomProp,
|
|
446
|
+
onKeyUp: onKeyUpProp,
|
|
447
|
+
onKeyDown: onKeyDownProp,
|
|
448
|
+
onMouseDown: onMouseDownProp,
|
|
449
|
+
onTouchStart: onTouchStartProp,
|
|
450
|
+
"aria-describedby": ariaDescribedByProp,
|
|
451
|
+
asChild,
|
|
452
|
+
className,
|
|
453
|
+
ref,
|
|
454
|
+
...rootImplProps
|
|
455
|
+
} = props;
|
|
456
|
+
const context = useCropperContext(ROOT_IMPL_NAME);
|
|
457
|
+
const store = useStoreContext(ROOT_IMPL_NAME);
|
|
458
|
+
const crop = useStore((state) => state.crop);
|
|
459
|
+
const zoom = useStore((state) => state.zoom);
|
|
460
|
+
const rotation = useStore((state) => state.rotation);
|
|
461
|
+
const mediaSize = useStore((state) => state.mediaSize);
|
|
462
|
+
const cropSize = useStore((state) => state.cropSize);
|
|
463
|
+
const descriptionId = React.useId();
|
|
464
|
+
const [zoomStatus, setZoomStatus] = React.useState("");
|
|
465
|
+
const propsRef = useAsRef({
|
|
466
|
+
onWheelZoom: onWheelZoomProp,
|
|
467
|
+
onKeyUp: onKeyUpProp,
|
|
468
|
+
onKeyDown: onKeyDownProp,
|
|
469
|
+
onMouseDown: onMouseDownProp,
|
|
470
|
+
onTouchStart: onTouchStartProp
|
|
471
|
+
});
|
|
472
|
+
const composedRef = useComposedRefs(ref, context.rootRef);
|
|
473
|
+
const dragStartPositionRef = React.useRef({ x: 0, y: 0 });
|
|
474
|
+
const dragStartCropRef = React.useRef({ x: 0, y: 0 });
|
|
475
|
+
const contentPositionRef = React.useRef({ x: 0, y: 0 });
|
|
476
|
+
const lastPinchDistanceRef = React.useRef(0);
|
|
477
|
+
const lastPinchRotationRef = React.useRef(0);
|
|
478
|
+
const rafDragTimeoutRef = React.useRef(null);
|
|
479
|
+
const rafPinchTimeoutRef = React.useRef(null);
|
|
480
|
+
const wheelTimerRef = React.useRef(null);
|
|
481
|
+
const isTouchingRef = React.useRef(false);
|
|
482
|
+
const gestureZoomStartRef = React.useRef(0);
|
|
483
|
+
const gestureRotationStartRef = React.useRef(0);
|
|
484
|
+
const onRefsCleanup = React.useCallback(() => {
|
|
485
|
+
if (rafDragTimeoutRef.current) {
|
|
486
|
+
cancelAnimationFrame(rafDragTimeoutRef.current);
|
|
487
|
+
rafDragTimeoutRef.current = null;
|
|
488
|
+
}
|
|
489
|
+
if (rafPinchTimeoutRef.current) {
|
|
490
|
+
cancelAnimationFrame(rafPinchTimeoutRef.current);
|
|
491
|
+
rafPinchTimeoutRef.current = null;
|
|
492
|
+
}
|
|
493
|
+
if (wheelTimerRef.current) {
|
|
494
|
+
clearTimeout(wheelTimerRef.current);
|
|
495
|
+
wheelTimerRef.current = null;
|
|
496
|
+
}
|
|
497
|
+
isTouchingRef.current = false;
|
|
498
|
+
}, []);
|
|
499
|
+
const onCacheCleanup = React.useCallback(() => {
|
|
500
|
+
if (onPositionClampCache.size > MAX_CACHE_SIZE * 1.5) {
|
|
501
|
+
onPositionClampCache.clear();
|
|
502
|
+
}
|
|
503
|
+
if (croppedAreaCache.size > MAX_CACHE_SIZE * 1.5) {
|
|
504
|
+
croppedAreaCache.clear();
|
|
505
|
+
}
|
|
506
|
+
}, []);
|
|
507
|
+
const getMousePoint = React.useCallback(
|
|
508
|
+
(event) => ({
|
|
509
|
+
x: Number(event.clientX),
|
|
510
|
+
y: Number(event.clientY)
|
|
511
|
+
}),
|
|
512
|
+
[]
|
|
513
|
+
);
|
|
514
|
+
const getTouchPoint = React.useCallback(
|
|
515
|
+
(touch) => ({
|
|
516
|
+
x: Number(touch.clientX),
|
|
517
|
+
y: Number(touch.clientY)
|
|
518
|
+
}),
|
|
519
|
+
[]
|
|
520
|
+
);
|
|
521
|
+
const onContentPositionChange = React.useCallback(() => {
|
|
522
|
+
if (context.rootRef?.current) {
|
|
523
|
+
const bounds = context.rootRef.current.getBoundingClientRect();
|
|
524
|
+
contentPositionRef.current = { x: bounds.left, y: bounds.top };
|
|
525
|
+
}
|
|
526
|
+
}, [context.rootRef]);
|
|
527
|
+
const getPointOnContent = React.useCallback(
|
|
528
|
+
({ x, y }, contentTopLeft) => {
|
|
529
|
+
if (!context.rootRef?.current) {
|
|
530
|
+
return { x: 0, y: 0 };
|
|
531
|
+
}
|
|
532
|
+
const contentRect = context.rootRef.current.getBoundingClientRect();
|
|
533
|
+
return {
|
|
534
|
+
x: contentRect.width / 2 - (x - contentTopLeft.x),
|
|
535
|
+
y: contentRect.height / 2 - (y - contentTopLeft.y)
|
|
536
|
+
};
|
|
537
|
+
},
|
|
538
|
+
[context.rootRef]
|
|
539
|
+
);
|
|
540
|
+
const getPointOnMedia = React.useCallback(
|
|
541
|
+
({ x, y }) => {
|
|
542
|
+
return {
|
|
543
|
+
x: (x + crop.x) / zoom,
|
|
544
|
+
y: (y + crop.y) / zoom
|
|
545
|
+
};
|
|
546
|
+
},
|
|
547
|
+
[crop, zoom]
|
|
548
|
+
);
|
|
549
|
+
const recomputeCropPosition = React.useCallback(() => {
|
|
550
|
+
if (!cropSize || !mediaSize) return;
|
|
551
|
+
const newPosition = !context.allowOverflow ? onPositionClamp(crop, mediaSize, cropSize, zoom, rotation) : crop;
|
|
552
|
+
if (Math.abs(newPosition.x - crop.x) > 1e-3 || Math.abs(newPosition.y - crop.y) > 1e-3) {
|
|
553
|
+
store.setState("crop", newPosition);
|
|
554
|
+
}
|
|
555
|
+
}, [cropSize, mediaSize, context.allowOverflow, crop, zoom, rotation, store]);
|
|
556
|
+
const onZoomChange = React.useCallback(
|
|
557
|
+
(newZoom, point, shouldUpdatePosition = true) => {
|
|
558
|
+
if (!cropSize || !mediaSize) return;
|
|
559
|
+
const clampedZoom = clamp(newZoom, context.minZoom, context.maxZoom);
|
|
560
|
+
store.batch(() => {
|
|
561
|
+
if (shouldUpdatePosition) {
|
|
562
|
+
const zoomPoint = getPointOnContent(point, contentPositionRef.current);
|
|
563
|
+
const zoomTarget = getPointOnMedia(zoomPoint);
|
|
564
|
+
const requestedPosition = {
|
|
565
|
+
x: zoomTarget.x * clampedZoom - zoomPoint.x,
|
|
566
|
+
y: zoomTarget.y * clampedZoom - zoomPoint.y
|
|
567
|
+
};
|
|
568
|
+
const newPosition = !context.allowOverflow ? onPositionClamp(requestedPosition, mediaSize, cropSize, clampedZoom, rotation) : requestedPosition;
|
|
569
|
+
store.setState("crop", newPosition);
|
|
570
|
+
}
|
|
571
|
+
store.setState("zoom", clampedZoom);
|
|
572
|
+
});
|
|
573
|
+
requestAnimationFrame(() => {
|
|
574
|
+
recomputeCropPosition();
|
|
575
|
+
});
|
|
576
|
+
},
|
|
577
|
+
[
|
|
578
|
+
cropSize,
|
|
579
|
+
mediaSize,
|
|
580
|
+
context.minZoom,
|
|
581
|
+
context.maxZoom,
|
|
582
|
+
context.allowOverflow,
|
|
583
|
+
getPointOnContent,
|
|
584
|
+
getPointOnMedia,
|
|
585
|
+
rotation,
|
|
586
|
+
store,
|
|
587
|
+
recomputeCropPosition
|
|
588
|
+
]
|
|
589
|
+
);
|
|
590
|
+
const onDragStart = React.useCallback(
|
|
591
|
+
({ x, y }) => {
|
|
592
|
+
dragStartPositionRef.current = { x, y };
|
|
593
|
+
dragStartCropRef.current = { ...crop };
|
|
594
|
+
store.setState("isDragging", true);
|
|
595
|
+
},
|
|
596
|
+
[crop, store]
|
|
597
|
+
);
|
|
598
|
+
const onDrag = React.useCallback(
|
|
599
|
+
({ x, y }) => {
|
|
600
|
+
if (rafDragTimeoutRef.current) {
|
|
601
|
+
cancelAnimationFrame(rafDragTimeoutRef.current);
|
|
602
|
+
}
|
|
603
|
+
rafDragTimeoutRef.current = requestAnimationFrame(() => {
|
|
604
|
+
if (!cropSize || !mediaSize) return;
|
|
605
|
+
if (x === void 0 || y === void 0) return;
|
|
606
|
+
const offsetX = x - dragStartPositionRef.current.x;
|
|
607
|
+
const offsetY = y - dragStartPositionRef.current.y;
|
|
608
|
+
if (Math.abs(offsetX) < 2 && Math.abs(offsetY) < 2) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const requestedPosition = {
|
|
612
|
+
x: dragStartCropRef.current.x + offsetX,
|
|
613
|
+
y: dragStartCropRef.current.y + offsetY
|
|
614
|
+
};
|
|
615
|
+
const newPosition = !context.allowOverflow ? onPositionClamp(requestedPosition, mediaSize, cropSize, zoom, rotation) : requestedPosition;
|
|
616
|
+
const currentCrop = store.getState().crop;
|
|
617
|
+
if (Math.abs(newPosition.x - currentCrop.x) > 1 || Math.abs(newPosition.y - currentCrop.y) > 1) {
|
|
618
|
+
store.setState("crop", newPosition);
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
},
|
|
622
|
+
[cropSize, mediaSize, context.allowOverflow, zoom, rotation, store]
|
|
623
|
+
);
|
|
624
|
+
const onMouseMove = React.useCallback((event) => onDrag(getMousePoint(event)), [getMousePoint, onDrag]);
|
|
625
|
+
const onTouchMove = React.useCallback(
|
|
626
|
+
(event) => {
|
|
627
|
+
event.preventDefault();
|
|
628
|
+
if (event.touches.length === 2) {
|
|
629
|
+
const [firstTouch, secondTouch] = event.touches ?? [];
|
|
630
|
+
if (firstTouch && secondTouch) {
|
|
631
|
+
const pointA = getTouchPoint(firstTouch);
|
|
632
|
+
const pointB = getTouchPoint(secondTouch);
|
|
633
|
+
const center = getCenter(pointA, pointB);
|
|
634
|
+
onDrag(center);
|
|
635
|
+
if (rafPinchTimeoutRef.current) {
|
|
636
|
+
cancelAnimationFrame(rafPinchTimeoutRef.current);
|
|
637
|
+
}
|
|
638
|
+
rafPinchTimeoutRef.current = requestAnimationFrame(() => {
|
|
639
|
+
const distance = getDistanceBetweenPoints(pointA, pointB);
|
|
640
|
+
const distanceRatio = distance / lastPinchDistanceRef.current;
|
|
641
|
+
if (Math.abs(distanceRatio - 1) > 0.01) {
|
|
642
|
+
const newZoom = zoom * distanceRatio;
|
|
643
|
+
onZoomChange(newZoom, center, false);
|
|
644
|
+
lastPinchDistanceRef.current = distance;
|
|
645
|
+
}
|
|
646
|
+
const rotationAngle = getRotationBetweenPoints(pointA, pointB);
|
|
647
|
+
const rotationDiff = rotationAngle - lastPinchRotationRef.current;
|
|
648
|
+
if (Math.abs(rotationDiff) > 0.5) {
|
|
649
|
+
const newRotation = rotation + rotationDiff;
|
|
650
|
+
store.setState("rotation", newRotation);
|
|
651
|
+
lastPinchRotationRef.current = rotationAngle;
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
} else if (event.touches.length === 1) {
|
|
656
|
+
const firstTouch = event.touches[0];
|
|
657
|
+
if (firstTouch) {
|
|
658
|
+
onDrag(getTouchPoint(firstTouch));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
[getTouchPoint, onDrag, zoom, onZoomChange, rotation, store]
|
|
663
|
+
);
|
|
664
|
+
const onGestureChange = React.useCallback(
|
|
665
|
+
(event) => {
|
|
666
|
+
event.preventDefault();
|
|
667
|
+
if (isTouchingRef.current) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const point = { x: Number(event.clientX), y: Number(event.clientY) };
|
|
671
|
+
const newZoom = gestureZoomStartRef.current - 1 + event.scale;
|
|
672
|
+
onZoomChange(newZoom, point, true);
|
|
673
|
+
const newRotation = gestureRotationStartRef.current + event.rotation;
|
|
674
|
+
store.setState("rotation", newRotation);
|
|
675
|
+
},
|
|
676
|
+
[onZoomChange, store]
|
|
677
|
+
);
|
|
678
|
+
const onGestureEnd = React.useCallback(() => {
|
|
679
|
+
document.removeEventListener("gesturechange", onGestureChange);
|
|
680
|
+
document.removeEventListener("gestureend", onGestureEnd);
|
|
681
|
+
}, [onGestureChange]);
|
|
682
|
+
const onGestureStart = React.useCallback(
|
|
683
|
+
(event) => {
|
|
684
|
+
event.preventDefault();
|
|
685
|
+
onContentPositionChange();
|
|
686
|
+
document.addEventListener("gesturechange", onGestureChange);
|
|
687
|
+
document.addEventListener("gestureend", onGestureEnd);
|
|
688
|
+
gestureZoomStartRef.current = zoom;
|
|
689
|
+
gestureRotationStartRef.current = rotation;
|
|
690
|
+
},
|
|
691
|
+
[zoom, rotation, onGestureChange, onGestureEnd, onContentPositionChange]
|
|
692
|
+
);
|
|
693
|
+
const onSafariZoomPrevent = React.useCallback((event) => event.preventDefault(), []);
|
|
694
|
+
const onEventsCleanup = React.useCallback(() => {
|
|
695
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
696
|
+
document.removeEventListener("touchmove", onTouchMove);
|
|
697
|
+
document.removeEventListener("gesturechange", onGestureChange);
|
|
698
|
+
document.removeEventListener("gestureend", onGestureEnd);
|
|
699
|
+
}, [onMouseMove, onTouchMove, onGestureChange, onGestureEnd]);
|
|
700
|
+
const onDragStopped = React.useCallback(() => {
|
|
701
|
+
isTouchingRef.current = false;
|
|
702
|
+
store.setState("isDragging", false);
|
|
703
|
+
onRefsCleanup();
|
|
704
|
+
document.removeEventListener("mouseup", onDragStopped);
|
|
705
|
+
document.removeEventListener("touchend", onDragStopped);
|
|
706
|
+
onEventsCleanup();
|
|
707
|
+
}, [store, onEventsCleanup, onRefsCleanup]);
|
|
708
|
+
const getWheelDelta = React.useCallback((event) => {
|
|
709
|
+
let deltaX = event.deltaX;
|
|
710
|
+
let deltaY = event.deltaY;
|
|
711
|
+
let deltaZ = event.deltaZ;
|
|
712
|
+
if (event.deltaMode === 1) {
|
|
713
|
+
deltaX *= 16;
|
|
714
|
+
deltaY *= 16;
|
|
715
|
+
deltaZ *= 16;
|
|
716
|
+
} else if (event.deltaMode === 2) {
|
|
717
|
+
deltaX *= 400;
|
|
718
|
+
deltaY *= 400;
|
|
719
|
+
deltaZ *= 400;
|
|
720
|
+
}
|
|
721
|
+
return { deltaX, deltaY, deltaZ };
|
|
722
|
+
}, []);
|
|
723
|
+
const onWheelZoom = React.useCallback(
|
|
724
|
+
(event) => {
|
|
725
|
+
propsRef.current.onWheelZoom?.(event);
|
|
726
|
+
if (event.defaultPrevented) return;
|
|
727
|
+
event.preventDefault();
|
|
728
|
+
onContentPositionChange();
|
|
729
|
+
const point = getMousePoint(event);
|
|
730
|
+
const { deltaY } = getWheelDelta(event);
|
|
731
|
+
const newZoom = zoom - deltaY * context.zoomSpeed / 200;
|
|
732
|
+
onZoomChange(newZoom, point, true);
|
|
733
|
+
store.batch(() => {
|
|
734
|
+
const currentState = store.getState();
|
|
735
|
+
if (!currentState.isWheelZooming) {
|
|
736
|
+
store.setState("isWheelZooming", true);
|
|
737
|
+
}
|
|
738
|
+
if (!currentState.isDragging) {
|
|
739
|
+
store.setState("isDragging", true);
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
if (wheelTimerRef.current) {
|
|
743
|
+
clearTimeout(wheelTimerRef.current);
|
|
744
|
+
}
|
|
745
|
+
wheelTimerRef.current = window.setTimeout(() => {
|
|
746
|
+
store.batch(() => {
|
|
747
|
+
store.setState("isWheelZooming", false);
|
|
748
|
+
store.setState("isDragging", false);
|
|
749
|
+
});
|
|
750
|
+
}, 250);
|
|
751
|
+
},
|
|
752
|
+
[propsRef, getMousePoint, zoom, context.zoomSpeed, onZoomChange, getWheelDelta, store, onContentPositionChange]
|
|
753
|
+
);
|
|
754
|
+
const onKeyUp = React.useCallback(
|
|
755
|
+
(event) => {
|
|
756
|
+
propsRef.current.onKeyUp?.(event);
|
|
757
|
+
if (event.defaultPrevented) return;
|
|
758
|
+
const arrowKeys = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]);
|
|
759
|
+
if (arrowKeys.has(event.key) || ZOOM_IN_KEYS.has(event.key) || ZOOM_OUT_KEYS.has(event.key)) {
|
|
760
|
+
event.preventDefault();
|
|
761
|
+
store.setState("isDragging", false);
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
[propsRef, store]
|
|
765
|
+
);
|
|
766
|
+
const onKeyDown = React.useCallback(
|
|
767
|
+
(event) => {
|
|
768
|
+
propsRef.current.onKeyDown?.(event);
|
|
769
|
+
if (event.defaultPrevented || !cropSize || !mediaSize) return;
|
|
770
|
+
const zoomDirection = ZOOM_IN_KEYS.has(event.key) ? 1 : ZOOM_OUT_KEYS.has(event.key) ? -1 : 0;
|
|
771
|
+
if (zoomDirection !== 0) {
|
|
772
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
773
|
+
const root = context.rootRef.current;
|
|
774
|
+
if (!root) return;
|
|
775
|
+
event.preventDefault();
|
|
776
|
+
if (!event.repeat) {
|
|
777
|
+
store.setState("isDragging", true);
|
|
778
|
+
}
|
|
779
|
+
const nextZoom = clamp(zoom + zoomDirection * KEYBOARD_ZOOM_STEP, context.minZoom, context.maxZoom);
|
|
780
|
+
onContentPositionChange();
|
|
781
|
+
const rect = root.getBoundingClientRect();
|
|
782
|
+
onZoomChange(nextZoom, { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }, true);
|
|
783
|
+
setZoomStatus(context.labels.zoomStatus(nextZoom));
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
let step = context.keyboardStep;
|
|
787
|
+
if (event.shiftKey) {
|
|
788
|
+
step *= 0.2;
|
|
789
|
+
}
|
|
790
|
+
const keyCallbacks = {
|
|
791
|
+
ArrowUp: () => ({ ...crop, y: crop.y - step }),
|
|
792
|
+
ArrowDown: () => ({ ...crop, y: crop.y + step }),
|
|
793
|
+
ArrowLeft: () => ({ ...crop, x: crop.x - step }),
|
|
794
|
+
ArrowRight: () => ({ ...crop, x: crop.x + step })
|
|
795
|
+
};
|
|
796
|
+
const callback = keyCallbacks[event.key];
|
|
797
|
+
if (!callback) return;
|
|
798
|
+
event.preventDefault();
|
|
799
|
+
let newCrop = callback();
|
|
800
|
+
if (!context.allowOverflow) {
|
|
801
|
+
newCrop = onPositionClamp(newCrop, mediaSize, cropSize, zoom, rotation);
|
|
802
|
+
}
|
|
803
|
+
if (!event.repeat) {
|
|
804
|
+
store.setState("isDragging", true);
|
|
805
|
+
}
|
|
806
|
+
store.setState("crop", newCrop);
|
|
807
|
+
},
|
|
808
|
+
[
|
|
809
|
+
propsRef,
|
|
810
|
+
cropSize,
|
|
811
|
+
mediaSize,
|
|
812
|
+
context.keyboardStep,
|
|
813
|
+
context.allowOverflow,
|
|
814
|
+
context.rootRef,
|
|
815
|
+
context.minZoom,
|
|
816
|
+
context.maxZoom,
|
|
817
|
+
context.labels,
|
|
818
|
+
crop,
|
|
819
|
+
zoom,
|
|
820
|
+
rotation,
|
|
821
|
+
store,
|
|
822
|
+
onContentPositionChange,
|
|
823
|
+
onZoomChange
|
|
824
|
+
]
|
|
825
|
+
);
|
|
826
|
+
const onMouseDown = React.useCallback(
|
|
827
|
+
(event) => {
|
|
828
|
+
propsRef.current.onMouseDown?.(event);
|
|
829
|
+
if (event.defaultPrevented) return;
|
|
830
|
+
event.preventDefault();
|
|
831
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
832
|
+
document.addEventListener("mouseup", onDragStopped);
|
|
833
|
+
onContentPositionChange();
|
|
834
|
+
onDragStart(getMousePoint(event));
|
|
835
|
+
},
|
|
836
|
+
[propsRef, getMousePoint, onDragStart, onDragStopped, onMouseMove, onContentPositionChange]
|
|
837
|
+
);
|
|
838
|
+
const onTouchStart = React.useCallback(
|
|
839
|
+
(event) => {
|
|
840
|
+
propsRef.current.onTouchStart?.(event);
|
|
841
|
+
if (event.defaultPrevented) return;
|
|
842
|
+
isTouchingRef.current = true;
|
|
843
|
+
document.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
844
|
+
document.addEventListener("touchend", onDragStopped);
|
|
845
|
+
onContentPositionChange();
|
|
846
|
+
if (event.touches.length === 2) {
|
|
847
|
+
const [firstTouch, secondTouch] = event.touches ? Array.from(event.touches) : [];
|
|
848
|
+
if (firstTouch && secondTouch) {
|
|
849
|
+
const pointA = getTouchPoint(firstTouch);
|
|
850
|
+
const pointB = getTouchPoint(secondTouch);
|
|
851
|
+
lastPinchDistanceRef.current = getDistanceBetweenPoints(pointA, pointB);
|
|
852
|
+
lastPinchRotationRef.current = getRotationBetweenPoints(pointA, pointB);
|
|
853
|
+
onDragStart(getCenter(pointA, pointB));
|
|
854
|
+
}
|
|
855
|
+
} else if (event.touches.length === 1) {
|
|
856
|
+
const firstTouch = event.touches[0];
|
|
857
|
+
if (firstTouch) {
|
|
858
|
+
onDragStart(getTouchPoint(firstTouch));
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
},
|
|
862
|
+
[propsRef, onDragStopped, onTouchMove, onContentPositionChange, getTouchPoint, onDragStart]
|
|
863
|
+
);
|
|
864
|
+
React.useEffect(() => {
|
|
865
|
+
const content = context.rootRef?.current;
|
|
866
|
+
if (!content) return;
|
|
867
|
+
if (!context.preventScrollZoom) {
|
|
868
|
+
content.addEventListener("wheel", onWheelZoom, { passive: false });
|
|
869
|
+
}
|
|
870
|
+
content.addEventListener("gesturestart", onSafariZoomPrevent);
|
|
871
|
+
content.addEventListener("gesturestart", onGestureStart);
|
|
872
|
+
return () => {
|
|
873
|
+
if (!context.preventScrollZoom) {
|
|
874
|
+
content.removeEventListener("wheel", onWheelZoom);
|
|
875
|
+
}
|
|
876
|
+
content.removeEventListener("gesturestart", onSafariZoomPrevent);
|
|
877
|
+
content.removeEventListener("gesturestart", onGestureStart);
|
|
878
|
+
};
|
|
879
|
+
}, [context.rootRef, context.preventScrollZoom, onWheelZoom, onSafariZoomPrevent, onGestureStart]);
|
|
880
|
+
React.useEffect(() => {
|
|
881
|
+
return () => {
|
|
882
|
+
onRefsCleanup();
|
|
883
|
+
onCacheCleanup();
|
|
884
|
+
};
|
|
885
|
+
}, [onRefsCleanup, onCacheCleanup]);
|
|
886
|
+
const RootPrimitive = asChild ? Slot : "div";
|
|
887
|
+
const describedBy = [descriptionId, ariaDescribedByProp].filter(Boolean).join(" ");
|
|
888
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
889
|
+
/* @__PURE__ */ jsx(
|
|
890
|
+
RootPrimitive,
|
|
891
|
+
{
|
|
892
|
+
role: "application",
|
|
893
|
+
"aria-roledescription": context.labels.roleDescription,
|
|
894
|
+
"aria-label": context.labels.label,
|
|
895
|
+
"aria-describedby": describedBy,
|
|
896
|
+
"data-slot": "cropper",
|
|
897
|
+
tabIndex: 0,
|
|
898
|
+
...rootImplProps,
|
|
899
|
+
ref: composedRef,
|
|
900
|
+
className: cn(
|
|
901
|
+
"absolute inset-0 flex cursor-move touch-none items-center justify-center overflow-hidden outline-none select-none",
|
|
902
|
+
/*
|
|
903
|
+
* ⚠ Khác thượng nguồn: bản gốc `outline-none` mà KHÔNG có chỉ báo focus nào (WCAG 2.4.7).
|
|
904
|
+
* Vòng focus vẽ trên `::after` phủ kín khung chứ không trên chính phần tử: ảnh và khung
|
|
905
|
+
* cắt là con `absolute` nên được vẽ ĐÈ lên outline của cha, còn `::after` là con cuối
|
|
906
|
+
* cùng nên nằm trên cùng. Cùng khuôn `outline-ring` với `Button`, thụt vào trong vì hai
|
|
907
|
+
* lớp bọc ngoài đều `overflow-hidden`.
|
|
908
|
+
*/
|
|
909
|
+
"after:pointer-events-none after:absolute after:inset-0 focus-visible:after:-outline-offset-2 focus-visible:after:outline-2 focus-visible:after:outline-ring",
|
|
910
|
+
className
|
|
911
|
+
),
|
|
912
|
+
onKeyUp,
|
|
913
|
+
onKeyDown,
|
|
914
|
+
onMouseDown,
|
|
915
|
+
onTouchStart
|
|
916
|
+
}
|
|
917
|
+
),
|
|
918
|
+
/* @__PURE__ */ jsx("span", { id: descriptionId, hidden: true, children: context.labels.instructions }),
|
|
919
|
+
/* @__PURE__ */ jsx("span", { "aria-live": "polite", "aria-atomic": "true", className: "sr-only", children: zoomStatus })
|
|
920
|
+
] });
|
|
921
|
+
}
|
|
922
|
+
const cropperMediaVariants = cva("will-change-transform", {
|
|
923
|
+
variants: {
|
|
924
|
+
objectFit: {
|
|
925
|
+
contain: "absolute inset-0 m-auto max-h-full max-w-full",
|
|
926
|
+
cover: "h-auto w-full",
|
|
927
|
+
"horizontal-cover": "h-auto w-full",
|
|
928
|
+
"vertical-cover": "h-full w-auto"
|
|
929
|
+
}
|
|
930
|
+
},
|
|
931
|
+
defaultVariants: {
|
|
932
|
+
objectFit: "contain"
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
function useMediaComputation({
|
|
936
|
+
mediaRef,
|
|
937
|
+
context,
|
|
938
|
+
store,
|
|
939
|
+
rotation,
|
|
940
|
+
getNaturalDimensions
|
|
941
|
+
}) {
|
|
942
|
+
const computeSizes = React.useCallback(() => {
|
|
943
|
+
const media = mediaRef.current;
|
|
944
|
+
const content = context.rootRef?.current;
|
|
945
|
+
if (!media || !content) return;
|
|
946
|
+
const contentRect = content.getBoundingClientRect();
|
|
947
|
+
const containerAspect = contentRect.width / contentRect.height;
|
|
948
|
+
const { width: naturalWidth, height: naturalHeight } = getNaturalDimensions(media);
|
|
949
|
+
const isScaledDown = media.offsetWidth < naturalWidth || media.offsetHeight < naturalHeight;
|
|
950
|
+
const mediaAspect = naturalWidth / naturalHeight;
|
|
951
|
+
let renderedMediaSize;
|
|
952
|
+
if (isScaledDown) {
|
|
953
|
+
const objectFitCallbacks = {
|
|
954
|
+
contain: () => containerAspect > mediaAspect ? {
|
|
955
|
+
width: contentRect.height * mediaAspect,
|
|
956
|
+
height: contentRect.height
|
|
957
|
+
} : {
|
|
958
|
+
width: contentRect.width,
|
|
959
|
+
height: contentRect.width / mediaAspect
|
|
960
|
+
},
|
|
961
|
+
"horizontal-cover": () => ({
|
|
962
|
+
width: contentRect.width,
|
|
963
|
+
height: contentRect.width / mediaAspect
|
|
964
|
+
}),
|
|
965
|
+
"vertical-cover": () => ({
|
|
966
|
+
width: contentRect.height * mediaAspect,
|
|
967
|
+
height: contentRect.height
|
|
968
|
+
}),
|
|
969
|
+
cover: () => containerAspect < mediaAspect ? {
|
|
970
|
+
width: contentRect.width,
|
|
971
|
+
height: contentRect.width / mediaAspect
|
|
972
|
+
} : {
|
|
973
|
+
width: contentRect.height * mediaAspect,
|
|
974
|
+
height: contentRect.height
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
const callback = objectFitCallbacks[context.objectFit];
|
|
978
|
+
renderedMediaSize = callback ? callback() : containerAspect > mediaAspect ? {
|
|
979
|
+
width: contentRect.height * mediaAspect,
|
|
980
|
+
height: contentRect.height
|
|
981
|
+
} : {
|
|
982
|
+
width: contentRect.width,
|
|
983
|
+
height: contentRect.width / mediaAspect
|
|
984
|
+
};
|
|
985
|
+
} else {
|
|
986
|
+
renderedMediaSize = {
|
|
987
|
+
width: media.offsetWidth,
|
|
988
|
+
height: media.offsetHeight
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
const mediaSize = {
|
|
992
|
+
...renderedMediaSize,
|
|
993
|
+
naturalWidth,
|
|
994
|
+
naturalHeight
|
|
995
|
+
};
|
|
996
|
+
store.setState("mediaSize", mediaSize);
|
|
997
|
+
const cropSize = getCropSize(
|
|
998
|
+
mediaSize.width,
|
|
999
|
+
mediaSize.height,
|
|
1000
|
+
contentRect.width,
|
|
1001
|
+
contentRect.height,
|
|
1002
|
+
context.aspectRatio,
|
|
1003
|
+
rotation
|
|
1004
|
+
);
|
|
1005
|
+
store.setState("cropSize", cropSize);
|
|
1006
|
+
requestAnimationFrame(() => {
|
|
1007
|
+
const currentState = store.getState();
|
|
1008
|
+
if (currentState.cropSize && currentState.mediaSize) {
|
|
1009
|
+
const newPosition = onPositionClamp(
|
|
1010
|
+
currentState.crop,
|
|
1011
|
+
currentState.mediaSize,
|
|
1012
|
+
currentState.cropSize,
|
|
1013
|
+
currentState.zoom,
|
|
1014
|
+
currentState.rotation
|
|
1015
|
+
);
|
|
1016
|
+
if (Math.abs(newPosition.x - currentState.crop.x) > 1e-3 || Math.abs(newPosition.y - currentState.crop.y) > 1e-3) {
|
|
1017
|
+
store.setState("crop", newPosition);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
return { mediaSize, cropSize };
|
|
1022
|
+
}, [mediaRef, context.aspectRatio, context.rootRef, context.objectFit, store, rotation, getNaturalDimensions]);
|
|
1023
|
+
return { computeSizes };
|
|
1024
|
+
}
|
|
1025
|
+
function CropperImage(props) {
|
|
1026
|
+
const { className, style, asChild, ref, onLoad, objectFit, snapPixels = false, ...imageProps } = props;
|
|
1027
|
+
const context = useCropperContext(IMAGE_NAME);
|
|
1028
|
+
const store = useStoreContext(IMAGE_NAME);
|
|
1029
|
+
const crop = useStore((state) => state.crop);
|
|
1030
|
+
const zoom = useStore((state) => state.zoom);
|
|
1031
|
+
const rotation = useStore((state) => state.rotation);
|
|
1032
|
+
const dpr = useDevicePixelRatio();
|
|
1033
|
+
const imageRef = React.useRef(null);
|
|
1034
|
+
const composedRef = useComposedRefs(ref, imageRef);
|
|
1035
|
+
const getNaturalDimensions = React.useCallback(
|
|
1036
|
+
(image) => ({
|
|
1037
|
+
width: image.naturalWidth,
|
|
1038
|
+
height: image.naturalHeight
|
|
1039
|
+
}),
|
|
1040
|
+
[]
|
|
1041
|
+
);
|
|
1042
|
+
const { computeSizes } = useMediaComputation({
|
|
1043
|
+
mediaRef: imageRef,
|
|
1044
|
+
context,
|
|
1045
|
+
store,
|
|
1046
|
+
rotation,
|
|
1047
|
+
getNaturalDimensions
|
|
1048
|
+
});
|
|
1049
|
+
const onMediaLoad = React.useCallback(() => {
|
|
1050
|
+
const image = imageRef.current;
|
|
1051
|
+
if (!image) return;
|
|
1052
|
+
computeSizes();
|
|
1053
|
+
onLoad?.(new Event("load"));
|
|
1054
|
+
}, [computeSizes, onLoad]);
|
|
1055
|
+
React.useEffect(() => {
|
|
1056
|
+
const image = imageRef.current;
|
|
1057
|
+
if (image?.complete && image.naturalWidth > 0) {
|
|
1058
|
+
onMediaLoad();
|
|
1059
|
+
}
|
|
1060
|
+
}, [onMediaLoad]);
|
|
1061
|
+
React.useEffect(() => {
|
|
1062
|
+
const content = context.rootRef?.current;
|
|
1063
|
+
if (!content) return;
|
|
1064
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
1065
|
+
let isFirstResize = true;
|
|
1066
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
1067
|
+
if (isFirstResize) {
|
|
1068
|
+
isFirstResize = false;
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const callback = () => {
|
|
1072
|
+
const image = imageRef.current;
|
|
1073
|
+
if (image?.complete && image.naturalWidth > 0) {
|
|
1074
|
+
computeSizes();
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
if ("requestIdleCallback" in window) {
|
|
1078
|
+
requestIdleCallback(callback);
|
|
1079
|
+
} else {
|
|
1080
|
+
setTimeout(callback, 16);
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
resizeObserver.observe(content);
|
|
1084
|
+
return () => {
|
|
1085
|
+
resizeObserver.disconnect();
|
|
1086
|
+
};
|
|
1087
|
+
} else {
|
|
1088
|
+
const onWindowResize = () => {
|
|
1089
|
+
const image = imageRef.current;
|
|
1090
|
+
if (image?.complete && image.naturalWidth > 0) {
|
|
1091
|
+
computeSizes();
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
window.addEventListener("resize", onWindowResize);
|
|
1095
|
+
return () => {
|
|
1096
|
+
window.removeEventListener("resize", onWindowResize);
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
}, [context.rootRef, computeSizes]);
|
|
1100
|
+
const ImagePrimitive = asChild ? Slot : "img";
|
|
1101
|
+
return /* @__PURE__ */ jsx(
|
|
1102
|
+
ImagePrimitive,
|
|
1103
|
+
{
|
|
1104
|
+
"data-slot": "cropper-image",
|
|
1105
|
+
...imageProps,
|
|
1106
|
+
ref: composedRef,
|
|
1107
|
+
className: cn(
|
|
1108
|
+
cropperMediaVariants({
|
|
1109
|
+
objectFit: objectFit ?? context.objectFit,
|
|
1110
|
+
className
|
|
1111
|
+
})
|
|
1112
|
+
),
|
|
1113
|
+
style: {
|
|
1114
|
+
transform: snapPixels ? `translate(${snapToDevicePixel(crop.x, dpr)}px, ${snapToDevicePixel(crop.y, dpr)}px) rotate(${rotation}deg) scale(${zoom})` : `translate(${crop.x}px, ${crop.y}px) rotate(${rotation}deg) scale(${zoom})`,
|
|
1115
|
+
...style
|
|
1116
|
+
},
|
|
1117
|
+
onLoad: onMediaLoad
|
|
1118
|
+
}
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
function CropperVideo(props) {
|
|
1122
|
+
const { className, style, asChild, ref, onLoadedMetadata, objectFit, snapPixels = false, ...videoProps } = props;
|
|
1123
|
+
const context = useCropperContext(VIDEO_NAME);
|
|
1124
|
+
const store = useStoreContext(VIDEO_NAME);
|
|
1125
|
+
const crop = useStore((state) => state.crop);
|
|
1126
|
+
const zoom = useStore((state) => state.zoom);
|
|
1127
|
+
const rotation = useStore((state) => state.rotation);
|
|
1128
|
+
const dpr = useDevicePixelRatio();
|
|
1129
|
+
const videoRef = React.useRef(null);
|
|
1130
|
+
const composedRef = useComposedRefs(ref, videoRef);
|
|
1131
|
+
const getNaturalDimensions = React.useCallback(
|
|
1132
|
+
(video) => ({
|
|
1133
|
+
width: video.videoWidth,
|
|
1134
|
+
height: video.videoHeight
|
|
1135
|
+
}),
|
|
1136
|
+
[]
|
|
1137
|
+
);
|
|
1138
|
+
const { computeSizes } = useMediaComputation({
|
|
1139
|
+
mediaRef: videoRef,
|
|
1140
|
+
context,
|
|
1141
|
+
store,
|
|
1142
|
+
rotation,
|
|
1143
|
+
getNaturalDimensions
|
|
1144
|
+
});
|
|
1145
|
+
const onMediaLoad = React.useCallback(() => {
|
|
1146
|
+
const video = videoRef.current;
|
|
1147
|
+
if (!video) return;
|
|
1148
|
+
computeSizes();
|
|
1149
|
+
onLoadedMetadata?.(new Event("loadedmetadata"));
|
|
1150
|
+
}, [computeSizes, onLoadedMetadata]);
|
|
1151
|
+
React.useEffect(() => {
|
|
1152
|
+
const content = context.rootRef?.current;
|
|
1153
|
+
if (!content) return;
|
|
1154
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
1155
|
+
let isFirstResize = true;
|
|
1156
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
1157
|
+
if (isFirstResize) {
|
|
1158
|
+
isFirstResize = false;
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const callback = () => {
|
|
1162
|
+
const video = videoRef.current;
|
|
1163
|
+
if (video && video.videoWidth > 0 && video.videoHeight > 0) {
|
|
1164
|
+
computeSizes();
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
if ("requestIdleCallback" in window) {
|
|
1168
|
+
requestIdleCallback(callback);
|
|
1169
|
+
} else {
|
|
1170
|
+
setTimeout(callback, 16);
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
resizeObserver.observe(content);
|
|
1174
|
+
return () => {
|
|
1175
|
+
resizeObserver.disconnect();
|
|
1176
|
+
};
|
|
1177
|
+
} else {
|
|
1178
|
+
const onWindowResize = () => {
|
|
1179
|
+
const video = videoRef.current;
|
|
1180
|
+
if (video && video.videoWidth > 0 && video.videoHeight > 0) {
|
|
1181
|
+
computeSizes();
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
window.addEventListener("resize", onWindowResize);
|
|
1185
|
+
return () => {
|
|
1186
|
+
window.removeEventListener("resize", onWindowResize);
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
}, [context.rootRef, computeSizes]);
|
|
1190
|
+
const VideoPrimitive = asChild ? Slot : "video";
|
|
1191
|
+
return /* @__PURE__ */ jsx(
|
|
1192
|
+
VideoPrimitive,
|
|
1193
|
+
{
|
|
1194
|
+
"data-slot": "cropper-video",
|
|
1195
|
+
autoPlay: true,
|
|
1196
|
+
playsInline: true,
|
|
1197
|
+
loop: true,
|
|
1198
|
+
muted: true,
|
|
1199
|
+
controls: false,
|
|
1200
|
+
...videoProps,
|
|
1201
|
+
ref: composedRef,
|
|
1202
|
+
className: cn(
|
|
1203
|
+
cropperMediaVariants({
|
|
1204
|
+
objectFit: objectFit ?? context.objectFit,
|
|
1205
|
+
className
|
|
1206
|
+
})
|
|
1207
|
+
),
|
|
1208
|
+
style: {
|
|
1209
|
+
transform: snapPixels ? `translate(${snapToDevicePixel(crop.x, dpr)}px, ${snapToDevicePixel(crop.y, dpr)}px) rotate(${rotation}deg) scale(${zoom})` : `translate(${crop.x}px, ${crop.y}px) rotate(${rotation}deg) scale(${zoom})`,
|
|
1210
|
+
...style
|
|
1211
|
+
},
|
|
1212
|
+
onLoadedMetadata: onMediaLoad
|
|
1213
|
+
}
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
const cropperAreaVariants = cva(
|
|
1217
|
+
"absolute top-1/2 left-1/2 box-border -translate-x-1/2 -translate-y-1/2 overflow-hidden border border-[2.5px] border-paper shadow-[0_0_0_9999em] shadow-ink/60 dark:shadow-ink/80",
|
|
1218
|
+
{
|
|
1219
|
+
variants: {
|
|
1220
|
+
shape: {
|
|
1221
|
+
rectangle: "",
|
|
1222
|
+
circle: "rounded-full"
|
|
1223
|
+
},
|
|
1224
|
+
withGrid: {
|
|
1225
|
+
true: "before:absolute before:top-0 before:right-1/3 before:bottom-0 before:left-1/3 before:box-border before:border before:border-t-0 before:border-b-0 before:border-paper/50 before:content-[''] after:absolute after:top-1/3 after:right-0 after:bottom-1/3 after:left-0 after:box-border after:border after:border-r-0 after:border-l-0 after:border-paper/50 after:content-['']",
|
|
1226
|
+
false: ""
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1229
|
+
defaultVariants: {
|
|
1230
|
+
shape: "rectangle",
|
|
1231
|
+
withGrid: false
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
);
|
|
1235
|
+
function CropperArea(props) {
|
|
1236
|
+
const { className, style, asChild, ref, snapPixels = false, shape, withGrid, ...areaProps } = props;
|
|
1237
|
+
const context = useCropperContext(AREA_NAME);
|
|
1238
|
+
const cropSize = useStore((state) => state.cropSize);
|
|
1239
|
+
if (!cropSize) return null;
|
|
1240
|
+
const AreaPrimitive = asChild ? Slot : "div";
|
|
1241
|
+
return /* @__PURE__ */ jsx(
|
|
1242
|
+
AreaPrimitive,
|
|
1243
|
+
{
|
|
1244
|
+
"data-slot": "cropper-area",
|
|
1245
|
+
...areaProps,
|
|
1246
|
+
ref,
|
|
1247
|
+
className: cn(
|
|
1248
|
+
cropperAreaVariants({
|
|
1249
|
+
shape: shape ?? context.shape,
|
|
1250
|
+
withGrid: withGrid ?? context.withGrid,
|
|
1251
|
+
className
|
|
1252
|
+
})
|
|
1253
|
+
),
|
|
1254
|
+
style: {
|
|
1255
|
+
width: snapPixels ? Math.round(cropSize.width) : cropSize.width,
|
|
1256
|
+
height: snapPixels ? Math.round(cropSize.height) : cropSize.height,
|
|
1257
|
+
...style
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
export {
|
|
1263
|
+
Cropper,
|
|
1264
|
+
CropperArea,
|
|
1265
|
+
CropperImage,
|
|
1266
|
+
CropperVideo,
|
|
1267
|
+
useStore as useCropper
|
|
1268
|
+
};
|
|
1269
|
+
//# sourceMappingURL=cropper.js.map
|