payload-plugin-aspect-preview 0.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.md +21 -0
- package/README.md +94 -0
- package/dist/components/AspectRatioPreviewGrid.d.ts +11 -0
- package/dist/components/AspectRatioPreviewGrid.js +230 -0
- package/dist/components/AspectRatioPreviewGrid.js.map +1 -0
- package/dist/components/CustomUpload.d.ts +4 -0
- package/dist/components/CustomUpload.js +167 -0
- package/dist/components/CustomUpload.js.map +1 -0
- package/dist/components/FocalPointEditor.d.ts +4 -0
- package/dist/components/FocalPointEditor.js +629 -0
- package/dist/components/FocalPointEditor.js.map +1 -0
- package/dist/defaults.d.ts +2 -0
- package/dist/defaults.js +58 -0
- package/dist/defaults.js.map +1 -0
- package/dist/exports/client.d.ts +3 -0
- package/dist/exports/client.js +5 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/exports/types.d.ts +1 -0
- package/dist/exports/types.js +3 -0
- package/dist/exports/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +52 -0
- package/dist/plugin.js.map +1 -0
- package/dist/styles.scss +482 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +82 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useConfig, useDocumentInfo, useForm, useUploadEdits } from '@payloadcms/ui';
|
|
4
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import 'react-image-crop/dist/ReactCrop.css';
|
|
6
|
+
import ReactCrop from 'react-image-crop';
|
|
7
|
+
import { DEFAULT_ASPECT_RATIOS } from '../defaults';
|
|
8
|
+
import { AspectRatioPreviewGrid } from './AspectRatioPreviewGrid';
|
|
9
|
+
export const FocalPointEditor = ()=>{
|
|
10
|
+
const { data } = useDocumentInfo();
|
|
11
|
+
const { setModified } = useForm();
|
|
12
|
+
const { uploadEdits, updateUploadEdits } = useUploadEdits();
|
|
13
|
+
const { getEntityConfig } = useConfig();
|
|
14
|
+
// Read the ratios the plugin stashed in the collection's `custom` config.
|
|
15
|
+
// Falls back to the shipped defaults so the editor renders even if a
|
|
16
|
+
// consumer wired the field without options.
|
|
17
|
+
const collectionSlug = typeof data?.collection === 'string' ? data.collection : undefined;
|
|
18
|
+
const entityConfig = collectionSlug ? getEntityConfig({
|
|
19
|
+
collectionSlug
|
|
20
|
+
}) : undefined;
|
|
21
|
+
const aspectRatios = entityConfig?.custom?.aspectPreview?.aspectRatios ?? DEFAULT_ASPECT_RATIOS;
|
|
22
|
+
const hasUserEditedRef = useRef(false);
|
|
23
|
+
// Bust the browser cache when the document is re-saved so the regenerated
|
|
24
|
+
// cropped image (same URL, new bytes) shows up in the preview grid.
|
|
25
|
+
const rawUrl = data?.url;
|
|
26
|
+
const updatedAtTag = data?.updatedAt ? String(data.updatedAt) : undefined;
|
|
27
|
+
const imageUrl = React.useMemo(()=>{
|
|
28
|
+
if (!rawUrl) return undefined;
|
|
29
|
+
if (!updatedAtTag) return rawUrl;
|
|
30
|
+
const sep = rawUrl.includes('?') ? '&' : '?';
|
|
31
|
+
return `${rawUrl}${sep}v=${encodeURIComponent(updatedAtTag)}`;
|
|
32
|
+
}, [
|
|
33
|
+
rawUrl,
|
|
34
|
+
updatedAtTag
|
|
35
|
+
]);
|
|
36
|
+
const [mode, setMode] = useState('focal');
|
|
37
|
+
// Crop is undefined when none has been drawn yet. When the user enters crop
|
|
38
|
+
// mode without an existing crop, ReactCrop should wait for them to draw one
|
|
39
|
+
// — we don't pre-fill 100% any more.
|
|
40
|
+
const [crop, setCrop] = useState(uploadEdits?.crop ? {
|
|
41
|
+
unit: uploadEdits.crop.unit,
|
|
42
|
+
x: uploadEdits.crop.x,
|
|
43
|
+
y: uploadEdits.crop.y,
|
|
44
|
+
width: uploadEdits.crop.width,
|
|
45
|
+
height: uploadEdits.crop.height
|
|
46
|
+
} : undefined);
|
|
47
|
+
const [focalPoint, setFocalPoint] = useState({
|
|
48
|
+
x: uploadEdits?.focalPoint?.x || data?.focalX || 50,
|
|
49
|
+
y: uploadEdits?.focalPoint?.y || data?.focalY || 50
|
|
50
|
+
});
|
|
51
|
+
const containerRef = useRef(null);
|
|
52
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
53
|
+
// Natural pixel dimensions of the underlying image. Needed because Payload's
|
|
54
|
+
// cropImage requires `widthInPixels` / `heightInPixels` in addition to the
|
|
55
|
+
// percent crop — without them sharp.extract receives NaN and the save fails.
|
|
56
|
+
const [naturalSize, setNaturalSize] = useState(()=>({
|
|
57
|
+
width: typeof data?.width === 'number' ? data.width : 0,
|
|
58
|
+
height: typeof data?.height === 'number' ? data.height : 0
|
|
59
|
+
}));
|
|
60
|
+
// Prefer the Payload document's authoritative width/height — that's what
|
|
61
|
+
// sharp reads on the server. Fall back to the browser-decoded natural size.
|
|
62
|
+
const docWidth = typeof data?.width === 'number' ? data.width : 0;
|
|
63
|
+
const docHeight = typeof data?.height === 'number' ? data.height : 0;
|
|
64
|
+
useEffect(()=>{
|
|
65
|
+
if (docWidth > 0 && docHeight > 0) {
|
|
66
|
+
queueMicrotask(()=>setNaturalSize({
|
|
67
|
+
width: docWidth,
|
|
68
|
+
height: docHeight
|
|
69
|
+
}));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!imageUrl) return;
|
|
73
|
+
const img = new Image();
|
|
74
|
+
img.onload = ()=>{
|
|
75
|
+
setNaturalSize({
|
|
76
|
+
width: img.naturalWidth,
|
|
77
|
+
height: img.naturalHeight
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
img.src = imageUrl;
|
|
81
|
+
}, [
|
|
82
|
+
imageUrl,
|
|
83
|
+
docWidth,
|
|
84
|
+
docHeight
|
|
85
|
+
]);
|
|
86
|
+
const [prevUpdatedAtTag, setPrevUpdatedAtTag] = useState(updatedAtTag);
|
|
87
|
+
// Re-sync local state from the saved document each time `updatedAt` changes.
|
|
88
|
+
useEffect(()=>{
|
|
89
|
+
if (updatedAtTag && updatedAtTag !== prevUpdatedAtTag) {
|
|
90
|
+
const nextFocal = {
|
|
91
|
+
x: typeof data?.focalX === 'number' ? data.focalX : 50,
|
|
92
|
+
y: typeof data?.focalY === 'number' ? data.focalY : 50
|
|
93
|
+
};
|
|
94
|
+
queueMicrotask(()=>{
|
|
95
|
+
setPrevUpdatedAtTag(updatedAtTag);
|
|
96
|
+
hasUserEditedRef.current = false;
|
|
97
|
+
setFocalPoint(nextFocal);
|
|
98
|
+
setCrop(undefined);
|
|
99
|
+
});
|
|
100
|
+
if (uploadEdits?.crop || uploadEdits?.focalPoint) {
|
|
101
|
+
updateUploadEdits({
|
|
102
|
+
crop: undefined,
|
|
103
|
+
focalPoint: nextFocal,
|
|
104
|
+
heightInPixels: undefined,
|
|
105
|
+
widthInPixels: undefined
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}, [
|
|
110
|
+
updatedAtTag,
|
|
111
|
+
prevUpdatedAtTag,
|
|
112
|
+
data?.focalX,
|
|
113
|
+
data?.focalY,
|
|
114
|
+
uploadEdits?.crop,
|
|
115
|
+
uploadEdits?.focalPoint,
|
|
116
|
+
updateUploadEdits
|
|
117
|
+
]);
|
|
118
|
+
const hasRealCrop = !!crop && crop.width > 0 && crop.height > 0 && (crop.width !== 100 || crop.height !== 100 || crop.x !== 0 || crop.y !== 0);
|
|
119
|
+
// Auto-save to uploadEdits whenever state changes
|
|
120
|
+
useEffect(()=>{
|
|
121
|
+
if (!hasUserEditedRef.current) return;
|
|
122
|
+
const timeout = setTimeout(()=>{
|
|
123
|
+
setModified(true);
|
|
124
|
+
if (hasRealCrop && crop && naturalSize.width > 0 && naturalSize.height > 0) {
|
|
125
|
+
// Payload's cropImage computes `left = floor(x% * imgW / 100)` and
|
|
126
|
+
// `top = floor(y% * imgH / 100)`, then calls sharp.extract with our
|
|
127
|
+
// widthInPixels / heightInPixels. Sharp rejects the call if
|
|
128
|
+
// left + width > imgW (likewise for height) — even by 1px.
|
|
129
|
+
// We must compute width/height with floor and clamp so they always
|
|
130
|
+
// fit inside the image from that floored offset.
|
|
131
|
+
const leftPx = Math.floor(crop.x / 100 * naturalSize.width);
|
|
132
|
+
const topPx = Math.floor(crop.y / 100 * naturalSize.height);
|
|
133
|
+
const widthInPixels = Math.max(1, Math.min(Math.floor(crop.width / 100 * naturalSize.width), naturalSize.width - leftPx));
|
|
134
|
+
const heightInPixels = Math.max(1, Math.min(Math.floor(crop.height / 100 * naturalSize.height), naturalSize.height - topPx));
|
|
135
|
+
updateUploadEdits({
|
|
136
|
+
crop: {
|
|
137
|
+
unit: '%',
|
|
138
|
+
x: crop.x,
|
|
139
|
+
y: crop.y,
|
|
140
|
+
width: crop.width,
|
|
141
|
+
height: crop.height
|
|
142
|
+
},
|
|
143
|
+
focalPoint,
|
|
144
|
+
heightInPixels,
|
|
145
|
+
widthInPixels
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
updateUploadEdits({
|
|
149
|
+
crop: undefined,
|
|
150
|
+
focalPoint,
|
|
151
|
+
heightInPixels: undefined,
|
|
152
|
+
widthInPixels: undefined
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}, 100);
|
|
156
|
+
return ()=>clearTimeout(timeout);
|
|
157
|
+
}, [
|
|
158
|
+
crop,
|
|
159
|
+
hasRealCrop,
|
|
160
|
+
focalPoint,
|
|
161
|
+
naturalSize,
|
|
162
|
+
setModified,
|
|
163
|
+
updateUploadEdits
|
|
164
|
+
]);
|
|
165
|
+
const clampFocalToCrop = useCallback((x, y)=>{
|
|
166
|
+
if (crop && crop.unit === '%' && hasRealCrop) {
|
|
167
|
+
x = Math.max(crop.x, Math.min(crop.x + crop.width, x));
|
|
168
|
+
y = Math.max(crop.y, Math.min(crop.y + crop.height, y));
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
x,
|
|
172
|
+
y
|
|
173
|
+
};
|
|
174
|
+
}, [
|
|
175
|
+
crop,
|
|
176
|
+
hasRealCrop
|
|
177
|
+
]);
|
|
178
|
+
// Focal point drag handlers
|
|
179
|
+
const updateFocalFromEvent = useCallback((e)=>{
|
|
180
|
+
const container = containerRef.current;
|
|
181
|
+
if (!container) return;
|
|
182
|
+
const rect = container.getBoundingClientRect();
|
|
183
|
+
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
184
|
+
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
185
|
+
let x = (clientX - rect.left) / rect.width * 100;
|
|
186
|
+
let y = (clientY - rect.top) / rect.height * 100;
|
|
187
|
+
x = Math.max(0, Math.min(100, x));
|
|
188
|
+
y = Math.max(0, Math.min(100, y));
|
|
189
|
+
const clamped = clampFocalToCrop(x, y);
|
|
190
|
+
hasUserEditedRef.current = true;
|
|
191
|
+
setFocalPoint(clamped);
|
|
192
|
+
}, [
|
|
193
|
+
clampFocalToCrop
|
|
194
|
+
]);
|
|
195
|
+
const handleMouseDown = useCallback((e)=>{
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
setIsDragging(true);
|
|
198
|
+
updateFocalFromEvent(e);
|
|
199
|
+
}, [
|
|
200
|
+
updateFocalFromEvent
|
|
201
|
+
]);
|
|
202
|
+
const handleTouchStart = useCallback((e)=>{
|
|
203
|
+
setIsDragging(true);
|
|
204
|
+
updateFocalFromEvent(e);
|
|
205
|
+
}, [
|
|
206
|
+
updateFocalFromEvent
|
|
207
|
+
]);
|
|
208
|
+
useEffect(()=>{
|
|
209
|
+
if (!isDragging) return;
|
|
210
|
+
const handleMove = (e)=>{
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
updateFocalFromEvent(e);
|
|
213
|
+
};
|
|
214
|
+
const handleUp = ()=>setIsDragging(false);
|
|
215
|
+
window.addEventListener('mousemove', handleMove);
|
|
216
|
+
window.addEventListener('mouseup', handleUp);
|
|
217
|
+
window.addEventListener('touchmove', handleMove, {
|
|
218
|
+
passive: false
|
|
219
|
+
});
|
|
220
|
+
window.addEventListener('touchend', handleUp);
|
|
221
|
+
return ()=>{
|
|
222
|
+
window.removeEventListener('mousemove', handleMove);
|
|
223
|
+
window.removeEventListener('mouseup', handleUp);
|
|
224
|
+
window.removeEventListener('touchmove', handleMove);
|
|
225
|
+
window.removeEventListener('touchend', handleUp);
|
|
226
|
+
};
|
|
227
|
+
}, [
|
|
228
|
+
isDragging,
|
|
229
|
+
updateFocalFromEvent
|
|
230
|
+
]);
|
|
231
|
+
// react-image-crop gives two values: `crop` (in the current unit, often px
|
|
232
|
+
// relative to the *displayed* image) and `percentCrop` (always 0-100%).
|
|
233
|
+
// Store percentCrop so preview math is display-size-independent.
|
|
234
|
+
const handleCropChange = useCallback((_crop, _percentCrop)=>{
|
|
235
|
+
hasUserEditedRef.current = true;
|
|
236
|
+
setCrop(_percentCrop);
|
|
237
|
+
}, []);
|
|
238
|
+
const handleReset = useCallback(()=>{
|
|
239
|
+
hasUserEditedRef.current = true;
|
|
240
|
+
setFocalPoint({
|
|
241
|
+
x: 50,
|
|
242
|
+
y: 50
|
|
243
|
+
});
|
|
244
|
+
setCrop(undefined);
|
|
245
|
+
}, []);
|
|
246
|
+
const handleCropFieldChange = useCallback((field, value)=>{
|
|
247
|
+
hasUserEditedRef.current = true;
|
|
248
|
+
setCrop((prev)=>{
|
|
249
|
+
// Bootstrap a crop on first edit
|
|
250
|
+
const base = prev ?? {
|
|
251
|
+
unit: '%',
|
|
252
|
+
x: 0,
|
|
253
|
+
y: 0,
|
|
254
|
+
width: 100,
|
|
255
|
+
height: 100
|
|
256
|
+
};
|
|
257
|
+
const next = {
|
|
258
|
+
...base,
|
|
259
|
+
[field]: value
|
|
260
|
+
};
|
|
261
|
+
// Clamp to image bounds (percent units)
|
|
262
|
+
if (next.unit === '%') {
|
|
263
|
+
next.x = Math.max(0, Math.min(100, next.x));
|
|
264
|
+
next.y = Math.max(0, Math.min(100, next.y));
|
|
265
|
+
next.width = Math.max(0, Math.min(100 - next.x, next.width));
|
|
266
|
+
next.height = Math.max(0, Math.min(100 - next.y, next.height));
|
|
267
|
+
}
|
|
268
|
+
return next;
|
|
269
|
+
});
|
|
270
|
+
}, []);
|
|
271
|
+
if (!imageUrl) {
|
|
272
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
273
|
+
className: "focal-editor",
|
|
274
|
+
children: /*#__PURE__*/ _jsx("div", {
|
|
275
|
+
className: "focal-editor__empty",
|
|
276
|
+
children: "Upload an image to edit focal point and preview aspect ratios."
|
|
277
|
+
})
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const cropConfig = hasRealCrop && crop ? {
|
|
281
|
+
unit: crop.unit,
|
|
282
|
+
x: crop.x,
|
|
283
|
+
y: crop.y,
|
|
284
|
+
width: crop.width,
|
|
285
|
+
height: crop.height
|
|
286
|
+
} : undefined;
|
|
287
|
+
const cropUnitLabel = crop?.unit === 'px' ? 'px' : '%';
|
|
288
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
289
|
+
className: "focal-editor",
|
|
290
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
291
|
+
className: "focal-editor__layout",
|
|
292
|
+
children: [
|
|
293
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
294
|
+
className: "focal-editor__left",
|
|
295
|
+
children: [
|
|
296
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
297
|
+
className: "focal-editor__toolbar",
|
|
298
|
+
children: [
|
|
299
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
300
|
+
className: "focal-editor__toolbar-inputs",
|
|
301
|
+
children: [
|
|
302
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
303
|
+
className: "focal-editor__toolbar-col",
|
|
304
|
+
children: [
|
|
305
|
+
/*#__PURE__*/ _jsx("span", {
|
|
306
|
+
className: "focal-editor__row-label",
|
|
307
|
+
children: "Focal"
|
|
308
|
+
}),
|
|
309
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
310
|
+
className: "focal-editor__subgroup",
|
|
311
|
+
children: [
|
|
312
|
+
/*#__PURE__*/ _jsx("span", {
|
|
313
|
+
className: "focal-editor__subgroup-label",
|
|
314
|
+
children: "Position"
|
|
315
|
+
}),
|
|
316
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
317
|
+
className: "focal-editor__subgroup-inputs",
|
|
318
|
+
children: [
|
|
319
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
320
|
+
className: "focal-editor__input-group",
|
|
321
|
+
children: [
|
|
322
|
+
/*#__PURE__*/ _jsx("label", {
|
|
323
|
+
htmlFor: "focal-x",
|
|
324
|
+
children: "X"
|
|
325
|
+
}),
|
|
326
|
+
/*#__PURE__*/ _jsx("input", {
|
|
327
|
+
id: "focal-x",
|
|
328
|
+
type: "number",
|
|
329
|
+
min: 0,
|
|
330
|
+
max: 100,
|
|
331
|
+
value: Math.round(focalPoint.x),
|
|
332
|
+
onChange: (e)=>{
|
|
333
|
+
hasUserEditedRef.current = true;
|
|
334
|
+
setFocalPoint((prev)=>{
|
|
335
|
+
const x = Math.max(0, Math.min(100, Number(e.target.value)));
|
|
336
|
+
return clampFocalToCrop(x, prev.y);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}),
|
|
340
|
+
/*#__PURE__*/ _jsx("span", {
|
|
341
|
+
children: "%"
|
|
342
|
+
})
|
|
343
|
+
]
|
|
344
|
+
}),
|
|
345
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
346
|
+
className: "focal-editor__input-group",
|
|
347
|
+
children: [
|
|
348
|
+
/*#__PURE__*/ _jsx("label", {
|
|
349
|
+
htmlFor: "focal-y",
|
|
350
|
+
children: "Y"
|
|
351
|
+
}),
|
|
352
|
+
/*#__PURE__*/ _jsx("input", {
|
|
353
|
+
id: "focal-y",
|
|
354
|
+
type: "number",
|
|
355
|
+
min: 0,
|
|
356
|
+
max: 100,
|
|
357
|
+
value: Math.round(focalPoint.y),
|
|
358
|
+
onChange: (e)=>{
|
|
359
|
+
hasUserEditedRef.current = true;
|
|
360
|
+
setFocalPoint((prev)=>{
|
|
361
|
+
const y = Math.max(0, Math.min(100, Number(e.target.value)));
|
|
362
|
+
return clampFocalToCrop(prev.x, y);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}),
|
|
366
|
+
/*#__PURE__*/ _jsx("span", {
|
|
367
|
+
children: "%"
|
|
368
|
+
})
|
|
369
|
+
]
|
|
370
|
+
})
|
|
371
|
+
]
|
|
372
|
+
})
|
|
373
|
+
]
|
|
374
|
+
})
|
|
375
|
+
]
|
|
376
|
+
}),
|
|
377
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
378
|
+
className: "focal-editor__toolbar-col",
|
|
379
|
+
children: [
|
|
380
|
+
/*#__PURE__*/ _jsx("span", {
|
|
381
|
+
className: "focal-editor__row-label",
|
|
382
|
+
children: "Crop"
|
|
383
|
+
}),
|
|
384
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
385
|
+
className: "focal-editor__toolbar-row",
|
|
386
|
+
children: [
|
|
387
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
388
|
+
className: "focal-editor__subgroup",
|
|
389
|
+
children: [
|
|
390
|
+
/*#__PURE__*/ _jsx("span", {
|
|
391
|
+
className: "focal-editor__subgroup-label",
|
|
392
|
+
children: "Position"
|
|
393
|
+
}),
|
|
394
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
395
|
+
className: "focal-editor__subgroup-inputs",
|
|
396
|
+
children: [
|
|
397
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
398
|
+
className: "focal-editor__input-group",
|
|
399
|
+
children: [
|
|
400
|
+
/*#__PURE__*/ _jsx("label", {
|
|
401
|
+
htmlFor: "crop-x",
|
|
402
|
+
children: "X"
|
|
403
|
+
}),
|
|
404
|
+
/*#__PURE__*/ _jsx("input", {
|
|
405
|
+
id: "crop-x",
|
|
406
|
+
type: "number",
|
|
407
|
+
min: 0,
|
|
408
|
+
max: 100,
|
|
409
|
+
value: crop ? Math.round(crop.x) : 0,
|
|
410
|
+
onChange: (e)=>handleCropFieldChange('x', Number(e.target.value)),
|
|
411
|
+
disabled: !hasRealCrop
|
|
412
|
+
}),
|
|
413
|
+
/*#__PURE__*/ _jsx("span", {
|
|
414
|
+
children: cropUnitLabel
|
|
415
|
+
})
|
|
416
|
+
]
|
|
417
|
+
}),
|
|
418
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
419
|
+
className: "focal-editor__input-group",
|
|
420
|
+
children: [
|
|
421
|
+
/*#__PURE__*/ _jsx("label", {
|
|
422
|
+
htmlFor: "crop-y",
|
|
423
|
+
children: "Y"
|
|
424
|
+
}),
|
|
425
|
+
/*#__PURE__*/ _jsx("input", {
|
|
426
|
+
id: "crop-y",
|
|
427
|
+
type: "number",
|
|
428
|
+
min: 0,
|
|
429
|
+
max: 100,
|
|
430
|
+
value: crop ? Math.round(crop.y) : 0,
|
|
431
|
+
onChange: (e)=>handleCropFieldChange('y', Number(e.target.value)),
|
|
432
|
+
disabled: !hasRealCrop
|
|
433
|
+
}),
|
|
434
|
+
/*#__PURE__*/ _jsx("span", {
|
|
435
|
+
children: cropUnitLabel
|
|
436
|
+
})
|
|
437
|
+
]
|
|
438
|
+
})
|
|
439
|
+
]
|
|
440
|
+
})
|
|
441
|
+
]
|
|
442
|
+
}),
|
|
443
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
444
|
+
className: "focal-editor__subgroup",
|
|
445
|
+
children: [
|
|
446
|
+
/*#__PURE__*/ _jsx("span", {
|
|
447
|
+
className: "focal-editor__subgroup-label",
|
|
448
|
+
children: "Size"
|
|
449
|
+
}),
|
|
450
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
451
|
+
className: "focal-editor__subgroup-inputs",
|
|
452
|
+
children: [
|
|
453
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
454
|
+
className: "focal-editor__input-group",
|
|
455
|
+
children: [
|
|
456
|
+
/*#__PURE__*/ _jsx("label", {
|
|
457
|
+
htmlFor: "crop-w",
|
|
458
|
+
children: "W"
|
|
459
|
+
}),
|
|
460
|
+
/*#__PURE__*/ _jsx("input", {
|
|
461
|
+
id: "crop-w",
|
|
462
|
+
type: "number",
|
|
463
|
+
min: 0,
|
|
464
|
+
max: 100,
|
|
465
|
+
value: crop ? Math.round(crop.width) : 0,
|
|
466
|
+
onChange: (e)=>handleCropFieldChange('width', Number(e.target.value)),
|
|
467
|
+
disabled: !hasRealCrop
|
|
468
|
+
}),
|
|
469
|
+
/*#__PURE__*/ _jsx("span", {
|
|
470
|
+
children: cropUnitLabel
|
|
471
|
+
})
|
|
472
|
+
]
|
|
473
|
+
}),
|
|
474
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
475
|
+
className: "focal-editor__input-group",
|
|
476
|
+
children: [
|
|
477
|
+
/*#__PURE__*/ _jsx("label", {
|
|
478
|
+
htmlFor: "crop-h",
|
|
479
|
+
children: "H"
|
|
480
|
+
}),
|
|
481
|
+
/*#__PURE__*/ _jsx("input", {
|
|
482
|
+
id: "crop-h",
|
|
483
|
+
type: "number",
|
|
484
|
+
min: 0,
|
|
485
|
+
max: 100,
|
|
486
|
+
value: crop ? Math.round(crop.height) : 0,
|
|
487
|
+
onChange: (e)=>handleCropFieldChange('height', Number(e.target.value)),
|
|
488
|
+
disabled: !hasRealCrop
|
|
489
|
+
}),
|
|
490
|
+
/*#__PURE__*/ _jsx("span", {
|
|
491
|
+
children: cropUnitLabel
|
|
492
|
+
})
|
|
493
|
+
]
|
|
494
|
+
})
|
|
495
|
+
]
|
|
496
|
+
})
|
|
497
|
+
]
|
|
498
|
+
})
|
|
499
|
+
]
|
|
500
|
+
})
|
|
501
|
+
]
|
|
502
|
+
})
|
|
503
|
+
]
|
|
504
|
+
}),
|
|
505
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
506
|
+
className: "focal-editor__toolbar-actions",
|
|
507
|
+
children: [
|
|
508
|
+
/*#__PURE__*/ _jsx("button", {
|
|
509
|
+
className: "focal-editor__btn",
|
|
510
|
+
onClick: handleReset,
|
|
511
|
+
type: "button",
|
|
512
|
+
children: "Reset"
|
|
513
|
+
}),
|
|
514
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
515
|
+
className: "focal-editor__mode-toggle",
|
|
516
|
+
children: [
|
|
517
|
+
/*#__PURE__*/ _jsx("button", {
|
|
518
|
+
className: `focal-editor__mode-btn ${mode === 'focal' ? 'focal-editor__mode-btn--active' : ''}`,
|
|
519
|
+
onClick: ()=>setMode('focal'),
|
|
520
|
+
type: "button",
|
|
521
|
+
children: "Focal Point"
|
|
522
|
+
}),
|
|
523
|
+
/*#__PURE__*/ _jsx("button", {
|
|
524
|
+
className: `focal-editor__mode-btn ${mode === 'crop' ? 'focal-editor__mode-btn--active' : ''}`,
|
|
525
|
+
onClick: ()=>setMode('crop'),
|
|
526
|
+
type: "button",
|
|
527
|
+
children: "Crop"
|
|
528
|
+
})
|
|
529
|
+
]
|
|
530
|
+
})
|
|
531
|
+
]
|
|
532
|
+
})
|
|
533
|
+
]
|
|
534
|
+
}),
|
|
535
|
+
/*#__PURE__*/ _jsx("div", {
|
|
536
|
+
className: "focal-editor__image-wrapper",
|
|
537
|
+
children: /*#__PURE__*/ _jsx(ReactCrop, {
|
|
538
|
+
crop: mode === 'crop' ? crop : undefined,
|
|
539
|
+
onChange: handleCropChange,
|
|
540
|
+
disabled: mode === 'focal',
|
|
541
|
+
className: `focal-editor__react-crop ${mode === 'focal' ? 'is-focal-mode' : 'is-crop-mode'}`,
|
|
542
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
543
|
+
style: {
|
|
544
|
+
position: 'relative',
|
|
545
|
+
display: 'inline-block',
|
|
546
|
+
lineHeight: 0,
|
|
547
|
+
width: '100%'
|
|
548
|
+
},
|
|
549
|
+
children: [
|
|
550
|
+
/*#__PURE__*/ _jsx("img", {
|
|
551
|
+
src: imageUrl,
|
|
552
|
+
alt: "Edit",
|
|
553
|
+
draggable: false,
|
|
554
|
+
style: {
|
|
555
|
+
display: 'block',
|
|
556
|
+
width: '100%',
|
|
557
|
+
height: 'auto',
|
|
558
|
+
objectFit: 'contain'
|
|
559
|
+
},
|
|
560
|
+
onLoad: (e)=>{
|
|
561
|
+
const img = e.currentTarget;
|
|
562
|
+
const displayedW = img.clientWidth;
|
|
563
|
+
const displayedH = img.clientHeight;
|
|
564
|
+
// If a legacy px crop was loaded from uploadEdits, convert
|
|
565
|
+
// it to percentages now that we know the displayed size.
|
|
566
|
+
if (crop?.unit === 'px' && displayedW > 0 && displayedH > 0) {
|
|
567
|
+
setCrop({
|
|
568
|
+
unit: '%',
|
|
569
|
+
x: crop.x / displayedW * 100,
|
|
570
|
+
y: crop.y / displayedH * 100,
|
|
571
|
+
width: crop.width / displayedW * 100,
|
|
572
|
+
height: crop.height / displayedH * 100
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}),
|
|
577
|
+
mode === 'focal' && crop && crop.unit === '%' && hasRealCrop && /*#__PURE__*/ _jsx("div", {
|
|
578
|
+
className: "focal-editor__crop-overlay",
|
|
579
|
+
style: {
|
|
580
|
+
left: `${crop.x}%`,
|
|
581
|
+
top: `${crop.y}%`,
|
|
582
|
+
width: `${crop.width}%`,
|
|
583
|
+
height: `${crop.height}%`
|
|
584
|
+
}
|
|
585
|
+
}),
|
|
586
|
+
/*#__PURE__*/ _jsx("div", {
|
|
587
|
+
ref: containerRef,
|
|
588
|
+
className: `focal-editor__focal-overlay ${mode === 'focal' ? 'is-active' : ''}`,
|
|
589
|
+
onMouseDown: handleMouseDown,
|
|
590
|
+
onTouchStart: handleTouchStart,
|
|
591
|
+
children: mode === 'focal' && /*#__PURE__*/ _jsxs("div", {
|
|
592
|
+
className: "focal-editor__crosshair",
|
|
593
|
+
style: {
|
|
594
|
+
left: `${focalPoint.x}%`,
|
|
595
|
+
top: `${focalPoint.y}%`
|
|
596
|
+
},
|
|
597
|
+
children: [
|
|
598
|
+
/*#__PURE__*/ _jsx("div", {
|
|
599
|
+
className: "focal-editor__crosshair-h"
|
|
600
|
+
}),
|
|
601
|
+
/*#__PURE__*/ _jsx("div", {
|
|
602
|
+
className: "focal-editor__crosshair-v"
|
|
603
|
+
})
|
|
604
|
+
]
|
|
605
|
+
})
|
|
606
|
+
})
|
|
607
|
+
]
|
|
608
|
+
})
|
|
609
|
+
})
|
|
610
|
+
})
|
|
611
|
+
]
|
|
612
|
+
}),
|
|
613
|
+
/*#__PURE__*/ _jsx("div", {
|
|
614
|
+
className: "focal-editor__right",
|
|
615
|
+
children: /*#__PURE__*/ _jsx(AspectRatioPreviewGrid, {
|
|
616
|
+
url: imageUrl,
|
|
617
|
+
focalX: focalPoint.x,
|
|
618
|
+
focalY: focalPoint.y,
|
|
619
|
+
aspectRatios: aspectRatios,
|
|
620
|
+
crop: cropConfig
|
|
621
|
+
})
|
|
622
|
+
})
|
|
623
|
+
]
|
|
624
|
+
})
|
|
625
|
+
});
|
|
626
|
+
};
|
|
627
|
+
export default FocalPointEditor;
|
|
628
|
+
|
|
629
|
+
//# sourceMappingURL=FocalPointEditor.js.map
|