payload-plugin-aspect-preview 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/FocalPointEditor.js +82 -70
- package/dist/plugin.js +3 -0
- package/package.json +1 -1
|
@@ -65,13 +65,14 @@ export const FocalPointEditor = ()=>{
|
|
|
65
65
|
width: uploadEdits.crop.width,
|
|
66
66
|
height: uploadEdits.crop.height
|
|
67
67
|
} : undefined);
|
|
68
|
+
// Focal point is stored relative to the active crop region: a 0-100 position
|
|
69
|
+
// inside the crop, or inside the whole image when there is no crop. This is
|
|
70
|
+
// the same space Payload persists (it bakes the crop into the base image and
|
|
71
|
+
// reads focalX/focalY as percentages of the cropped result), so uploadEdits
|
|
72
|
+
// and data.focalX/Y are already in this space — no conversion on load.
|
|
73
|
+
// `typeof` (not `||`) so a valid edge focal of 0 doesn't fall through to 50.
|
|
68
74
|
const [focalPoint, setFocalPoint] = useState(()=>{
|
|
69
|
-
|
|
70
|
-
// lift it back into the full-image space the editor works in. `??` — a
|
|
71
|
-
// focal of 0 (an edge) is valid and must not fall through to the centre.
|
|
72
|
-
if (uploadEdits?.focalPoint) {
|
|
73
|
-
return uploadEdits.crop && uploadEdits.crop.unit === '%' ? toFullImageFocal(uploadEdits.focalPoint, uploadEdits.crop) : uploadEdits.focalPoint;
|
|
74
|
-
}
|
|
75
|
+
if (uploadEdits?.focalPoint) return uploadEdits.focalPoint;
|
|
75
76
|
return {
|
|
76
77
|
x: typeof data?.focalX === 'number' ? data.focalX : 50,
|
|
77
78
|
y: typeof data?.focalY === 'number' ? data.focalY : 50
|
|
@@ -115,39 +116,56 @@ export const FocalPointEditor = ()=>{
|
|
|
115
116
|
docWidth,
|
|
116
117
|
docHeight
|
|
117
118
|
]);
|
|
118
|
-
|
|
119
|
-
//
|
|
119
|
+
// Re-sync local state from the saved document once per save. The guard is a
|
|
120
|
+
// ref (not state) so it flips synchronously — the effect cannot re-enter when
|
|
121
|
+
// the updateUploadEdits call below changes the upload-edits context. Driving
|
|
122
|
+
// the guard through state (and listing uploadEdits in the deps) is what
|
|
123
|
+
// previously re-entered on every context change and exceeded React's update
|
|
124
|
+
// depth on save.
|
|
125
|
+
const lastSyncedTagRef = useRef(updatedAtTag);
|
|
120
126
|
useEffect(()=>{
|
|
121
|
-
if (updatedAtTag
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
127
|
+
if (!updatedAtTag || updatedAtTag === lastSyncedTagRef.current) return;
|
|
128
|
+
lastSyncedTagRef.current = updatedAtTag;
|
|
129
|
+
hasUserEditedRef.current = false;
|
|
130
|
+
const nextFocal = {
|
|
131
|
+
x: typeof data?.focalX === 'number' ? data.focalX : 50,
|
|
132
|
+
y: typeof data?.focalY === 'number' ? data.focalY : 50
|
|
133
|
+
};
|
|
134
|
+
setFocalPoint(nextFocal);
|
|
135
|
+
// The crop is now baked into the saved image, so there is no pending crop
|
|
136
|
+
// and the focal is relative to the whole (already-cropped) image again.
|
|
137
|
+
setCrop(undefined);
|
|
138
|
+
updateUploadEdits({
|
|
139
|
+
crop: undefined,
|
|
140
|
+
focalPoint: nextFocal,
|
|
141
|
+
heightInPixels: undefined,
|
|
142
|
+
widthInPixels: undefined
|
|
143
|
+
});
|
|
141
144
|
}, [
|
|
142
145
|
updatedAtTag,
|
|
143
|
-
prevUpdatedAtTag,
|
|
144
146
|
data?.focalX,
|
|
145
147
|
data?.focalY,
|
|
146
|
-
uploadEdits?.crop,
|
|
147
|
-
uploadEdits?.focalPoint,
|
|
148
148
|
updateUploadEdits
|
|
149
149
|
]);
|
|
150
150
|
const hasRealCrop = !!crop && crop.width > 0 && crop.height > 0 && (crop.width !== 100 || crop.height !== 100 || crop.x !== 0 || crop.y !== 0);
|
|
151
|
+
// The rectangle the focal point is measured against: the crop when one exists
|
|
152
|
+
// (in %), otherwise the whole image. Focal state is always a 0-100 position
|
|
153
|
+
// inside this rectangle; converting to/from full-image space happens only at
|
|
154
|
+
// the pointer and rendering boundaries.
|
|
155
|
+
const cropRect = React.useMemo(()=>hasRealCrop && crop && crop.unit === '%' ? {
|
|
156
|
+
x: crop.x,
|
|
157
|
+
y: crop.y,
|
|
158
|
+
width: crop.width,
|
|
159
|
+
height: crop.height
|
|
160
|
+
} : {
|
|
161
|
+
x: 0,
|
|
162
|
+
y: 0,
|
|
163
|
+
width: 100,
|
|
164
|
+
height: 100
|
|
165
|
+
}, [
|
|
166
|
+
hasRealCrop,
|
|
167
|
+
crop
|
|
168
|
+
]);
|
|
151
169
|
// Auto-save to uploadEdits whenever state changes
|
|
152
170
|
useEffect(()=>{
|
|
153
171
|
if (!hasUserEditedRef.current) return;
|
|
@@ -164,11 +182,6 @@ export const FocalPointEditor = ()=>{
|
|
|
164
182
|
const topPx = Math.floor(crop.y / 100 * naturalSize.height);
|
|
165
183
|
const widthInPixels = Math.max(1, Math.min(Math.floor(crop.width / 100 * naturalSize.width), naturalSize.width - leftPx));
|
|
166
184
|
const heightInPixels = Math.max(1, Math.min(Math.floor(crop.height / 100 * naturalSize.height), naturalSize.height - topPx));
|
|
167
|
-
// Payload interprets the saved focal point relative to the CROPPED
|
|
168
|
-
// image (it swaps the base image for the cropped bytes before sizing),
|
|
169
|
-
// so re-express our full-image focal within the crop region. clampFocal-
|
|
170
|
-
// ToCrop already keeps the focal inside the crop, so this stays 0-100.
|
|
171
|
-
const cropRelativeFocal = toCropRelativeFocal(focalPoint, crop);
|
|
172
185
|
updateUploadEdits({
|
|
173
186
|
crop: {
|
|
174
187
|
unit: '%',
|
|
@@ -177,7 +190,9 @@ export const FocalPointEditor = ()=>{
|
|
|
177
190
|
width: crop.width,
|
|
178
191
|
height: crop.height
|
|
179
192
|
},
|
|
180
|
-
focalPoint
|
|
193
|
+
// focalPoint is already relative to the crop region — exactly the
|
|
194
|
+
// space Payload reads it in for the cropped image.
|
|
195
|
+
focalPoint,
|
|
181
196
|
heightInPixels,
|
|
182
197
|
widthInPixels
|
|
183
198
|
});
|
|
@@ -199,35 +214,28 @@ export const FocalPointEditor = ()=>{
|
|
|
199
214
|
setModified,
|
|
200
215
|
updateUploadEdits
|
|
201
216
|
]);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
y = Math.max(crop.y, Math.min(crop.y + crop.height, y));
|
|
206
|
-
}
|
|
207
|
-
return {
|
|
208
|
-
x,
|
|
209
|
-
y
|
|
210
|
-
};
|
|
211
|
-
}, [
|
|
212
|
-
crop,
|
|
213
|
-
hasRealCrop
|
|
214
|
-
]);
|
|
215
|
-
// Focal point drag handlers
|
|
217
|
+
// Focal point drag handler. The pointer lands somewhere on the full image;
|
|
218
|
+
// convert that full-image % into the crop-relative space the focal lives in,
|
|
219
|
+
// then clamp to the crop (0-100 inside cropRect).
|
|
216
220
|
const updateFocalFromEvent = useCallback((e)=>{
|
|
217
221
|
const container = containerRef.current;
|
|
218
222
|
if (!container) return;
|
|
219
223
|
const rect = container.getBoundingClientRect();
|
|
220
224
|
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
221
225
|
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
const fullX = Math.max(0, Math.min(100, (clientX - rect.left) / rect.width * 100));
|
|
227
|
+
const fullY = Math.max(0, Math.min(100, (clientY - rect.top) / rect.height * 100));
|
|
228
|
+
const rel = toCropRelativeFocal({
|
|
229
|
+
x: fullX,
|
|
230
|
+
y: fullY
|
|
231
|
+
}, cropRect);
|
|
227
232
|
hasUserEditedRef.current = true;
|
|
228
|
-
setFocalPoint(
|
|
233
|
+
setFocalPoint({
|
|
234
|
+
x: Math.max(0, Math.min(100, rel.x)),
|
|
235
|
+
y: Math.max(0, Math.min(100, rel.y))
|
|
236
|
+
});
|
|
229
237
|
}, [
|
|
230
|
-
|
|
238
|
+
cropRect
|
|
231
239
|
]);
|
|
232
240
|
const handleMouseDown = useCallback((e)=>{
|
|
233
241
|
e.preventDefault();
|
|
@@ -322,6 +330,10 @@ export const FocalPointEditor = ()=>{
|
|
|
322
330
|
height: crop.height
|
|
323
331
|
} : undefined;
|
|
324
332
|
const cropUnitLabel = crop?.unit === 'px' ? 'px' : '%';
|
|
333
|
+
// Full-image position of the focal point (crop-relative → whole image). Used
|
|
334
|
+
// to place the crosshair absolutely and to feed the preview grid, which
|
|
335
|
+
// re-derives the crop-relative position itself.
|
|
336
|
+
const focalFullImage = toFullImageFocal(focalPoint, cropRect);
|
|
325
337
|
return /*#__PURE__*/ _jsx("div", {
|
|
326
338
|
className: "focal-editor",
|
|
327
339
|
children: /*#__PURE__*/ _jsxs("div", {
|
|
@@ -368,10 +380,10 @@ export const FocalPointEditor = ()=>{
|
|
|
368
380
|
value: Math.round(focalPoint.x),
|
|
369
381
|
onChange: (e)=>{
|
|
370
382
|
hasUserEditedRef.current = true;
|
|
371
|
-
setFocalPoint((prev)=>{
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
383
|
+
setFocalPoint((prev)=>({
|
|
384
|
+
...prev,
|
|
385
|
+
x: Math.max(0, Math.min(100, Number(e.target.value)))
|
|
386
|
+
}));
|
|
375
387
|
}
|
|
376
388
|
}),
|
|
377
389
|
/*#__PURE__*/ _jsx("span", {
|
|
@@ -394,10 +406,10 @@ export const FocalPointEditor = ()=>{
|
|
|
394
406
|
value: Math.round(focalPoint.y),
|
|
395
407
|
onChange: (e)=>{
|
|
396
408
|
hasUserEditedRef.current = true;
|
|
397
|
-
setFocalPoint((prev)=>{
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
409
|
+
setFocalPoint((prev)=>({
|
|
410
|
+
...prev,
|
|
411
|
+
y: Math.max(0, Math.min(100, Number(e.target.value)))
|
|
412
|
+
}));
|
|
401
413
|
}
|
|
402
414
|
}),
|
|
403
415
|
/*#__PURE__*/ _jsx("span", {
|
|
@@ -628,8 +640,8 @@ export const FocalPointEditor = ()=>{
|
|
|
628
640
|
children: mode === 'focal' && /*#__PURE__*/ _jsxs("div", {
|
|
629
641
|
className: "focal-editor__crosshair",
|
|
630
642
|
style: {
|
|
631
|
-
left: `${
|
|
632
|
-
top: `${
|
|
643
|
+
left: `${focalFullImage.x}%`,
|
|
644
|
+
top: `${focalFullImage.y}%`
|
|
633
645
|
},
|
|
634
646
|
children: [
|
|
635
647
|
/*#__PURE__*/ _jsx("div", {
|
|
@@ -651,8 +663,8 @@ export const FocalPointEditor = ()=>{
|
|
|
651
663
|
className: "focal-editor__right",
|
|
652
664
|
children: /*#__PURE__*/ _jsx(AspectRatioPreviewGrid, {
|
|
653
665
|
url: imageUrl,
|
|
654
|
-
focalX:
|
|
655
|
-
focalY:
|
|
666
|
+
focalX: focalFullImage.x,
|
|
667
|
+
focalY: focalFullImage.y,
|
|
656
668
|
aspectRatios: aspectRatios,
|
|
657
669
|
crop: cropConfig
|
|
658
670
|
})
|
package/dist/plugin.js
CHANGED
|
@@ -40,6 +40,9 @@ const UPLOAD_COMPONENT = 'payload-plugin-aspect-preview/client#CustomUpload';
|
|
|
40
40
|
name: 'aspectPreview',
|
|
41
41
|
type: 'ui',
|
|
42
42
|
admin: {
|
|
43
|
+
// It's an edit-view editor, not data — keep it out of the
|
|
44
|
+
// list view's columns.
|
|
45
|
+
disableListColumn: true,
|
|
43
46
|
components: {
|
|
44
47
|
Field: FIELD_COMPONENT
|
|
45
48
|
}
|
package/package.json
CHANGED