payload-plugin-aspect-preview 0.1.1 → 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/AspectRatioPreviewGrid.js +6 -4
- package/dist/components/FocalPointEditor.js +112 -63
- package/dist/defaults.d.ts +4 -0
- package/dist/defaults.js +4 -1
- package/dist/focal.d.ts +17 -0
- package/dist/focal.js +21 -0
- package/dist/plugin.d.ts +25 -0
- package/dist/plugin.js +28 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.js +5 -1
- package/package.json +1 -1
|
@@ -46,9 +46,11 @@ function resolveCropPixels(crop, imgW, imgH) {
|
|
|
46
46
|
h: crop.height
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
// Editor
|
|
50
|
-
//
|
|
51
|
-
|
|
49
|
+
// Editor passes focalX/focalY as percentages of the FULL image; convert to
|
|
50
|
+
// percentages within the crop region. Pixel-based sibling of focal.ts'
|
|
51
|
+
// `toCropRelativeFocal` — identical result, but expressed in pixels here
|
|
52
|
+
// because this component already resolves the crop to pixels.
|
|
53
|
+
function focalWithinCropPixels(focalX, focalY, crop, imgW, imgH) {
|
|
52
54
|
const relX = (focalX / 100 * imgW - crop.x) / crop.w;
|
|
53
55
|
const relY = (focalY / 100 * imgH - crop.y) / crop.h;
|
|
54
56
|
return {
|
|
@@ -67,7 +69,7 @@ function computeCroppedBackgroundStyle(frameW, frameH, imgW, imgH, crop, focalX,
|
|
|
67
69
|
// Scale the crop region to cover the frame (same math as object-fit:cover)
|
|
68
70
|
const scale = Math.max(frameW / crop.w, frameH / crop.h);
|
|
69
71
|
// Focal point inside the crop region, expressed as %
|
|
70
|
-
const relFocal =
|
|
72
|
+
const relFocal = focalWithinCropPixels(focalX, focalY, crop, imgW, imgH);
|
|
71
73
|
// Where the crop region's top-left lands in the frame, per object-position math
|
|
72
74
|
const cropDisplayW = crop.w * scale;
|
|
73
75
|
const cropDisplayH = crop.h * scale;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useConfig, useDocumentInfo, useForm, useUploadEdits } from '@payloadcms/ui';
|
|
3
|
+
import { useConfig, useDocumentInfo, useField, useForm, useUploadEdits } from '@payloadcms/ui';
|
|
4
4
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
5
5
|
import 'react-image-crop/dist/ReactCrop.css';
|
|
6
6
|
import ReactCrop from 'react-image-crop';
|
|
7
7
|
import { DEFAULT_ASPECT_RATIOS } from '../defaults';
|
|
8
|
+
import { toCropRelativeFocal, toFullImageFocal } from '../focal';
|
|
8
9
|
import { AspectRatioPreviewGrid } from './AspectRatioPreviewGrid';
|
|
9
10
|
export const FocalPointEditor = ()=>{
|
|
10
11
|
const { data } = useDocumentInfo();
|
|
@@ -20,16 +21,36 @@ export const FocalPointEditor = ()=>{
|
|
|
20
21
|
}) : undefined;
|
|
21
22
|
const aspectRatios = entityConfig?.custom?.aspectPreview?.aspectRatios ?? DEFAULT_ASPECT_RATIOS;
|
|
22
23
|
const hasUserEditedRef = useRef(false);
|
|
24
|
+
// The freshly-picked File lives in the shared `file` form field (set by the
|
|
25
|
+
// upload component) before the document is saved. Reading it here lets us
|
|
26
|
+
// show the editor + preview immediately on selection, saving a save-round-trip.
|
|
27
|
+
const { value: pendingFile } = useField({
|
|
28
|
+
path: 'file'
|
|
29
|
+
});
|
|
30
|
+
const pendingUrl = React.useMemo(()=>pendingFile instanceof File && pendingFile.type.startsWith('image/') ? URL.createObjectURL(pendingFile) : undefined, [
|
|
31
|
+
pendingFile
|
|
32
|
+
]);
|
|
33
|
+
// Revoke the object URL when it changes or the editor unmounts.
|
|
34
|
+
useEffect(()=>{
|
|
35
|
+
if (!pendingUrl) return;
|
|
36
|
+
return ()=>URL.revokeObjectURL(pendingUrl);
|
|
37
|
+
}, [
|
|
38
|
+
pendingUrl
|
|
39
|
+
]);
|
|
23
40
|
// Bust the browser cache when the document is re-saved so the regenerated
|
|
24
41
|
// cropped image (same URL, new bytes) shows up in the preview grid.
|
|
25
42
|
const rawUrl = data?.url;
|
|
26
43
|
const updatedAtTag = data?.updatedAt ? String(data.updatedAt) : undefined;
|
|
27
44
|
const imageUrl = React.useMemo(()=>{
|
|
45
|
+
// A pending selection wins over the saved doc — it's what the user is
|
|
46
|
+
// about to save. Blob URLs take no query string, so skip cache-busting.
|
|
47
|
+
if (pendingUrl) return pendingUrl;
|
|
28
48
|
if (!rawUrl) return undefined;
|
|
29
49
|
if (!updatedAtTag) return rawUrl;
|
|
30
50
|
const sep = rawUrl.includes('?') ? '&' : '?';
|
|
31
51
|
return `${rawUrl}${sep}v=${encodeURIComponent(updatedAtTag)}`;
|
|
32
52
|
}, [
|
|
53
|
+
pendingUrl,
|
|
33
54
|
rawUrl,
|
|
34
55
|
updatedAtTag
|
|
35
56
|
]);
|
|
@@ -44,9 +65,18 @@ export const FocalPointEditor = ()=>{
|
|
|
44
65
|
width: uploadEdits.crop.width,
|
|
45
66
|
height: uploadEdits.crop.height
|
|
46
67
|
} : undefined);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
74
|
+
const [focalPoint, setFocalPoint] = useState(()=>{
|
|
75
|
+
if (uploadEdits?.focalPoint) return uploadEdits.focalPoint;
|
|
76
|
+
return {
|
|
77
|
+
x: typeof data?.focalX === 'number' ? data.focalX : 50,
|
|
78
|
+
y: typeof data?.focalY === 'number' ? data.focalY : 50
|
|
79
|
+
};
|
|
50
80
|
});
|
|
51
81
|
const containerRef = useRef(null);
|
|
52
82
|
const [isDragging, setIsDragging] = useState(false);
|
|
@@ -62,7 +92,9 @@ export const FocalPointEditor = ()=>{
|
|
|
62
92
|
const docWidth = typeof data?.width === 'number' ? data.width : 0;
|
|
63
93
|
const docHeight = typeof data?.height === 'number' ? data.height : 0;
|
|
64
94
|
useEffect(()=>{
|
|
65
|
-
|
|
95
|
+
// For a pending selection the doc dimensions are stale/absent — always
|
|
96
|
+
// decode the picked image so crop pixel math uses the real size.
|
|
97
|
+
if (!pendingUrl && docWidth > 0 && docHeight > 0) {
|
|
66
98
|
queueMicrotask(()=>setNaturalSize({
|
|
67
99
|
width: docWidth,
|
|
68
100
|
height: docHeight
|
|
@@ -80,42 +112,60 @@ export const FocalPointEditor = ()=>{
|
|
|
80
112
|
img.src = imageUrl;
|
|
81
113
|
}, [
|
|
82
114
|
imageUrl,
|
|
115
|
+
pendingUrl,
|
|
83
116
|
docWidth,
|
|
84
117
|
docHeight
|
|
85
118
|
]);
|
|
86
|
-
|
|
87
|
-
//
|
|
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);
|
|
88
126
|
useEffect(()=>{
|
|
89
|
-
if (updatedAtTag
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
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
|
+
});
|
|
109
144
|
}, [
|
|
110
145
|
updatedAtTag,
|
|
111
|
-
prevUpdatedAtTag,
|
|
112
146
|
data?.focalX,
|
|
113
147
|
data?.focalY,
|
|
114
|
-
uploadEdits?.crop,
|
|
115
|
-
uploadEdits?.focalPoint,
|
|
116
148
|
updateUploadEdits
|
|
117
149
|
]);
|
|
118
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
|
+
]);
|
|
119
169
|
// Auto-save to uploadEdits whenever state changes
|
|
120
170
|
useEffect(()=>{
|
|
121
171
|
if (!hasUserEditedRef.current) return;
|
|
@@ -140,6 +190,8 @@ export const FocalPointEditor = ()=>{
|
|
|
140
190
|
width: crop.width,
|
|
141
191
|
height: crop.height
|
|
142
192
|
},
|
|
193
|
+
// focalPoint is already relative to the crop region — exactly the
|
|
194
|
+
// space Payload reads it in for the cropped image.
|
|
143
195
|
focalPoint,
|
|
144
196
|
heightInPixels,
|
|
145
197
|
widthInPixels
|
|
@@ -162,35 +214,28 @@ export const FocalPointEditor = ()=>{
|
|
|
162
214
|
setModified,
|
|
163
215
|
updateUploadEdits
|
|
164
216
|
]);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
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).
|
|
179
220
|
const updateFocalFromEvent = useCallback((e)=>{
|
|
180
221
|
const container = containerRef.current;
|
|
181
222
|
if (!container) return;
|
|
182
223
|
const rect = container.getBoundingClientRect();
|
|
183
224
|
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
184
225
|
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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);
|
|
190
232
|
hasUserEditedRef.current = true;
|
|
191
|
-
setFocalPoint(
|
|
233
|
+
setFocalPoint({
|
|
234
|
+
x: Math.max(0, Math.min(100, rel.x)),
|
|
235
|
+
y: Math.max(0, Math.min(100, rel.y))
|
|
236
|
+
});
|
|
192
237
|
}, [
|
|
193
|
-
|
|
238
|
+
cropRect
|
|
194
239
|
]);
|
|
195
240
|
const handleMouseDown = useCallback((e)=>{
|
|
196
241
|
e.preventDefault();
|
|
@@ -285,6 +330,10 @@ export const FocalPointEditor = ()=>{
|
|
|
285
330
|
height: crop.height
|
|
286
331
|
} : undefined;
|
|
287
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);
|
|
288
337
|
return /*#__PURE__*/ _jsx("div", {
|
|
289
338
|
className: "focal-editor",
|
|
290
339
|
children: /*#__PURE__*/ _jsxs("div", {
|
|
@@ -331,10 +380,10 @@ export const FocalPointEditor = ()=>{
|
|
|
331
380
|
value: Math.round(focalPoint.x),
|
|
332
381
|
onChange: (e)=>{
|
|
333
382
|
hasUserEditedRef.current = true;
|
|
334
|
-
setFocalPoint((prev)=>{
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
383
|
+
setFocalPoint((prev)=>({
|
|
384
|
+
...prev,
|
|
385
|
+
x: Math.max(0, Math.min(100, Number(e.target.value)))
|
|
386
|
+
}));
|
|
338
387
|
}
|
|
339
388
|
}),
|
|
340
389
|
/*#__PURE__*/ _jsx("span", {
|
|
@@ -357,10 +406,10 @@ export const FocalPointEditor = ()=>{
|
|
|
357
406
|
value: Math.round(focalPoint.y),
|
|
358
407
|
onChange: (e)=>{
|
|
359
408
|
hasUserEditedRef.current = true;
|
|
360
|
-
setFocalPoint((prev)=>{
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
409
|
+
setFocalPoint((prev)=>({
|
|
410
|
+
...prev,
|
|
411
|
+
y: Math.max(0, Math.min(100, Number(e.target.value)))
|
|
412
|
+
}));
|
|
364
413
|
}
|
|
365
414
|
}),
|
|
366
415
|
/*#__PURE__*/ _jsx("span", {
|
|
@@ -591,8 +640,8 @@ export const FocalPointEditor = ()=>{
|
|
|
591
640
|
children: mode === 'focal' && /*#__PURE__*/ _jsxs("div", {
|
|
592
641
|
className: "focal-editor__crosshair",
|
|
593
642
|
style: {
|
|
594
|
-
left: `${
|
|
595
|
-
top: `${
|
|
643
|
+
left: `${focalFullImage.x}%`,
|
|
644
|
+
top: `${focalFullImage.y}%`
|
|
596
645
|
},
|
|
597
646
|
children: [
|
|
598
647
|
/*#__PURE__*/ _jsx("div", {
|
|
@@ -614,8 +663,8 @@ export const FocalPointEditor = ()=>{
|
|
|
614
663
|
className: "focal-editor__right",
|
|
615
664
|
children: /*#__PURE__*/ _jsx(AspectRatioPreviewGrid, {
|
|
616
665
|
url: imageUrl,
|
|
617
|
-
focalX:
|
|
618
|
-
focalY:
|
|
666
|
+
focalX: focalFullImage.x,
|
|
667
|
+
focalY: focalFullImage.y,
|
|
619
668
|
aspectRatios: aspectRatios,
|
|
620
669
|
crop: cropConfig
|
|
621
670
|
})
|
package/dist/defaults.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import type { AspectRatioConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The aspect ratios previewed when {@link AspectPreviewPluginOptions.aspectRatios}
|
|
4
|
+
* is omitted: square, 4:3, 3:2, 16:9, 9:16, and a 1200×630 social-share target.
|
|
5
|
+
*/
|
|
2
6
|
export declare const DEFAULT_ASPECT_RATIOS: AspectRatioConfig[];
|
package/dist/defaults.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The aspect ratios previewed when {@link AspectPreviewPluginOptions.aspectRatios}
|
|
3
|
+
* is omitted: square, 4:3, 3:2, 16:9, 9:16, and a 1200×630 social-share target.
|
|
4
|
+
*/ export const DEFAULT_ASPECT_RATIOS = [
|
|
2
5
|
{
|
|
3
6
|
name: 'Square',
|
|
4
7
|
ratio: '1:1',
|
package/dist/focal.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CropConfig, FocalPointState } from './types.js';
|
|
2
|
+
/** The crop rectangle in percentage units — all this math needs. */
|
|
3
|
+
type PercentCrop = Pick<CropConfig, 'x' | 'y' | 'width' | 'height'>;
|
|
4
|
+
/**
|
|
5
|
+
* Payload stores the focal point relative to the CROP region: on save it bakes
|
|
6
|
+
* the crop into the base image, then interprets `focalX/focalY` as percentages
|
|
7
|
+
* of that cropped image. The editor, by contrast, works in full-image
|
|
8
|
+
* coordinates because the crosshair is drawn over the whole picture.
|
|
9
|
+
*
|
|
10
|
+
* These two pure inverses convert between the spaces. The math is percentage
|
|
11
|
+
* only — the image's pixel dimensions cancel out, so just the crop rectangle
|
|
12
|
+
* (in %) is required. Callers must guarantee a non-zero crop width/height.
|
|
13
|
+
*/
|
|
14
|
+
export declare function toCropRelativeFocal(focal: FocalPointState, crop: PercentCrop): FocalPointState;
|
|
15
|
+
/** Inverse of {@link toCropRelativeFocal}: crop-relative % back to full-image %. */
|
|
16
|
+
export declare function toFullImageFocal(focal: FocalPointState, crop: PercentCrop): FocalPointState;
|
|
17
|
+
export {};
|
package/dist/focal.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payload stores the focal point relative to the CROP region: on save it bakes
|
|
3
|
+
* the crop into the base image, then interprets `focalX/focalY` as percentages
|
|
4
|
+
* of that cropped image. The editor, by contrast, works in full-image
|
|
5
|
+
* coordinates because the crosshair is drawn over the whole picture.
|
|
6
|
+
*
|
|
7
|
+
* These two pure inverses convert between the spaces. The math is percentage
|
|
8
|
+
* only — the image's pixel dimensions cancel out, so just the crop rectangle
|
|
9
|
+
* (in %) is required. Callers must guarantee a non-zero crop width/height.
|
|
10
|
+
*/ export function toCropRelativeFocal(focal, crop) {
|
|
11
|
+
return {
|
|
12
|
+
x: (focal.x - crop.x) / crop.width * 100,
|
|
13
|
+
y: (focal.y - crop.y) / crop.height * 100
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Inverse of {@link toCropRelativeFocal}: crop-relative % back to full-image %. */ export function toFullImageFocal(focal, crop) {
|
|
17
|
+
return {
|
|
18
|
+
x: crop.x + focal.x / 100 * crop.width,
|
|
19
|
+
y: crop.y + focal.y / 100 * crop.height
|
|
20
|
+
};
|
|
21
|
+
}
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
1
|
import type { Plugin } from 'payload';
|
|
2
2
|
import type { AspectPreviewPluginOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Adds an inline focal-point + crop editor with a live aspect-ratio preview
|
|
5
|
+
* grid to Payload upload collections.
|
|
6
|
+
*
|
|
7
|
+
* For every collection listed in {@link AspectPreviewPluginOptions.collections}
|
|
8
|
+
* it appends an `aspectPreview` UI field that renders the editor and (unless
|
|
9
|
+
* {@link AspectPreviewPluginOptions.disabled}) overrides the collection's upload
|
|
10
|
+
* component to replace Payload's default crop drawer.
|
|
11
|
+
*
|
|
12
|
+
* @param options - see {@link AspectPreviewPluginOptions}.
|
|
13
|
+
* @returns a Payload {@link Plugin}.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* // payload.config.ts
|
|
18
|
+
* import { aspectPreviewPlugin } from 'payload-plugin-aspect-preview'
|
|
19
|
+
*
|
|
20
|
+
* export default buildConfig({
|
|
21
|
+
* plugins: [
|
|
22
|
+
* // aspectRatios defaults to DEFAULT_ASPECT_RATIOS when omitted
|
|
23
|
+
* aspectPreviewPlugin({ collections: ['media'] }),
|
|
24
|
+
* ],
|
|
25
|
+
* })
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
3
28
|
export declare const aspectPreviewPlugin: (options: AspectPreviewPluginOptions) => Plugin;
|
package/dist/plugin.js
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
import { DEFAULT_ASPECT_RATIOS } from './defaults.js';
|
|
2
2
|
const FIELD_COMPONENT = 'payload-plugin-aspect-preview/client#FocalPointEditor';
|
|
3
3
|
const UPLOAD_COMPONENT = 'payload-plugin-aspect-preview/client#CustomUpload';
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Adds an inline focal-point + crop editor with a live aspect-ratio preview
|
|
6
|
+
* grid to Payload upload collections.
|
|
7
|
+
*
|
|
8
|
+
* For every collection listed in {@link AspectPreviewPluginOptions.collections}
|
|
9
|
+
* it appends an `aspectPreview` UI field that renders the editor and (unless
|
|
10
|
+
* {@link AspectPreviewPluginOptions.disabled}) overrides the collection's upload
|
|
11
|
+
* component to replace Payload's default crop drawer.
|
|
12
|
+
*
|
|
13
|
+
* @param options - see {@link AspectPreviewPluginOptions}.
|
|
14
|
+
* @returns a Payload {@link Plugin}.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // payload.config.ts
|
|
19
|
+
* import { aspectPreviewPlugin } from 'payload-plugin-aspect-preview'
|
|
20
|
+
*
|
|
21
|
+
* export default buildConfig({
|
|
22
|
+
* plugins: [
|
|
23
|
+
* // aspectRatios defaults to DEFAULT_ASPECT_RATIOS when omitted
|
|
24
|
+
* aspectPreviewPlugin({ collections: ['media'] }),
|
|
25
|
+
* ],
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*/ export const aspectPreviewPlugin = (options)=>(config)=>{
|
|
5
29
|
const aspectRatios = options.aspectRatios ?? DEFAULT_ASPECT_RATIOS;
|
|
6
30
|
const enabled = new Set(options.collections);
|
|
7
31
|
return {
|
|
@@ -16,6 +40,9 @@ export const aspectPreviewPlugin = (options)=>(config)=>{
|
|
|
16
40
|
name: 'aspectPreview',
|
|
17
41
|
type: 'ui',
|
|
18
42
|
admin: {
|
|
43
|
+
// It's an edit-view editor, not data — keep it out of the
|
|
44
|
+
// list view's columns.
|
|
45
|
+
disableListColumn: true,
|
|
19
46
|
components: {
|
|
20
47
|
Field: FIELD_COMPONENT
|
|
21
48
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single aspect ratio shown in the editor's live preview grid. Pass an array
|
|
3
|
+
* of these as {@link AspectPreviewPluginOptions.aspectRatios} to tailor the
|
|
4
|
+
* ratios to your frontend; see {@link DEFAULT_ASPECT_RATIOS} for the shipped set.
|
|
5
|
+
*/
|
|
1
6
|
export interface AspectRatioConfig {
|
|
2
7
|
/** Display label, e.g. "Social Share". */
|
|
3
8
|
name: string;
|
|
@@ -14,6 +19,7 @@ export interface AspectRatioConfig {
|
|
|
14
19
|
/** Optional free-form grouping label. */
|
|
15
20
|
category?: string;
|
|
16
21
|
}
|
|
22
|
+
/** Options for {@link aspectPreviewPlugin}. */
|
|
17
23
|
export interface AspectPreviewPluginOptions {
|
|
18
24
|
/** Upload collections (by slug) to enhance with the inline editor. */
|
|
19
25
|
collections: string[];
|
|
@@ -22,6 +28,7 @@ export interface AspectPreviewPluginOptions {
|
|
|
22
28
|
/** Disable the plugin while keeping the schema field for DB consistency. */
|
|
23
29
|
disabled?: boolean;
|
|
24
30
|
}
|
|
31
|
+
/** A crop rectangle. `x`/`y`/`width`/`height` are in `unit` (percent or pixels). */
|
|
25
32
|
export interface CropConfig {
|
|
26
33
|
unit: 'px' | '%';
|
|
27
34
|
x: number;
|
|
@@ -29,10 +36,12 @@ export interface CropConfig {
|
|
|
29
36
|
width: number;
|
|
30
37
|
height: number;
|
|
31
38
|
}
|
|
39
|
+
/** Focal point as percentages (0-100) of its reference image. */
|
|
32
40
|
export interface FocalPointState {
|
|
33
41
|
x: number;
|
|
34
42
|
y: number;
|
|
35
43
|
}
|
|
44
|
+
/** The editor's pending edits, mirrored into Payload's upload-edits context. */
|
|
36
45
|
export interface UploadEditsState {
|
|
37
46
|
crop?: CropConfig;
|
|
38
47
|
focalPoint?: FocalPointState;
|
package/dist/types.js
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* A single aspect ratio shown in the editor's live preview grid. Pass an array
|
|
3
|
+
* of these as {@link AspectPreviewPluginOptions.aspectRatios} to tailor the
|
|
4
|
+
* ratios to your frontend; see {@link DEFAULT_ASPECT_RATIOS} for the shipped set.
|
|
5
|
+
*/ /** The editor's pending edits, mirrored into Payload's upload-edits context. */ export { };
|
package/package.json
CHANGED