@seshuk/payload-media-preview 1.0.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 +21 -0
- package/README.md +507 -0
- package/dist/components/Cell/Cell.client.d.ts +18 -0
- package/dist/components/Cell/Cell.client.js +131 -0
- package/dist/components/Cell/Cell.scss +43 -0
- package/dist/components/Cell/Cell.server.d.ts +11 -0
- package/dist/components/Cell/Cell.server.js +33 -0
- package/dist/components/ExternalLinkIcon/ExternalLinkIcon.d.ts +5 -0
- package/dist/components/ExternalLinkIcon/ExternalLinkIcon.js +17 -0
- package/dist/components/ExternalLinkIcon/ExternalLinkIcon.scss +19 -0
- package/dist/components/Field/Field.d.ts +18 -0
- package/dist/components/Field/Field.js +107 -0
- package/dist/components/Field/Field.scss +14 -0
- package/dist/components/MediaPreview.constants.d.ts +4 -0
- package/dist/components/MediaPreview.constants.js +28 -0
- package/dist/components/MediaPreview.d.ts +10 -0
- package/dist/components/MediaPreview.js +57 -0
- package/dist/components/MediaPreview.types.d.ts +2 -0
- package/dist/components/MediaPreview.types.js +1 -0
- package/dist/components/MediaPreview.utils.d.ts +6 -0
- package/dist/components/MediaPreview.utils.js +38 -0
- package/dist/components/Modal/Modal.constants.d.ts +16 -0
- package/dist/components/Modal/Modal.constants.js +16 -0
- package/dist/components/Modal/Modal.d.ts +19 -0
- package/dist/components/Modal/Modal.js +277 -0
- package/dist/components/Modal/Modal.scss +173 -0
- package/dist/components/Viewer/AudioViewer.d.ts +3 -0
- package/dist/components/Viewer/AudioViewer.js +24 -0
- package/dist/components/Viewer/IframeViewer.d.ts +3 -0
- package/dist/components/Viewer/IframeViewer.js +14 -0
- package/dist/components/Viewer/ImageViewer.d.ts +3 -0
- package/dist/components/Viewer/ImageViewer.js +10 -0
- package/dist/components/Viewer/VideoViewer.d.ts +3 -0
- package/dist/components/Viewer/VideoViewer.js +25 -0
- package/dist/components/Viewer/Viewer.d.ts +12 -0
- package/dist/components/Viewer/Viewer.js +50 -0
- package/dist/components/adapterResolver.d.ts +13 -0
- package/dist/components/adapterResolver.js +43 -0
- package/dist/exports/client.d.ts +5 -0
- package/dist/exports/client.js +5 -0
- package/dist/exports/rsc.d.ts +2 -0
- package/dist/exports/rsc.js +2 -0
- package/dist/field.d.ts +18 -0
- package/dist/field.js +36 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +90 -0
- package/dist/translations/index.d.ts +8 -0
- package/dist/translations/index.js +90 -0
- package/dist/translations/locales/ar.d.ts +2 -0
- package/dist/translations/locales/ar.js +11 -0
- package/dist/translations/locales/az.d.ts +2 -0
- package/dist/translations/locales/az.js +11 -0
- package/dist/translations/locales/bg.d.ts +2 -0
- package/dist/translations/locales/bg.js +11 -0
- package/dist/translations/locales/bnBd.d.ts +2 -0
- package/dist/translations/locales/bnBd.js +11 -0
- package/dist/translations/locales/bnIn.d.ts +2 -0
- package/dist/translations/locales/bnIn.js +11 -0
- package/dist/translations/locales/ca.d.ts +2 -0
- package/dist/translations/locales/ca.js +11 -0
- package/dist/translations/locales/cs.d.ts +2 -0
- package/dist/translations/locales/cs.js +11 -0
- package/dist/translations/locales/da.d.ts +2 -0
- package/dist/translations/locales/da.js +11 -0
- package/dist/translations/locales/de.d.ts +2 -0
- package/dist/translations/locales/de.js +11 -0
- package/dist/translations/locales/en.d.ts +2 -0
- package/dist/translations/locales/en.js +11 -0
- package/dist/translations/locales/es.d.ts +2 -0
- package/dist/translations/locales/es.js +11 -0
- package/dist/translations/locales/et.d.ts +2 -0
- package/dist/translations/locales/et.js +11 -0
- package/dist/translations/locales/fa.d.ts +2 -0
- package/dist/translations/locales/fa.js +11 -0
- package/dist/translations/locales/fr.d.ts +2 -0
- package/dist/translations/locales/fr.js +11 -0
- package/dist/translations/locales/he.d.ts +2 -0
- package/dist/translations/locales/he.js +11 -0
- package/dist/translations/locales/hr.d.ts +2 -0
- package/dist/translations/locales/hr.js +11 -0
- package/dist/translations/locales/hu.d.ts +2 -0
- package/dist/translations/locales/hu.js +11 -0
- package/dist/translations/locales/hy.d.ts +2 -0
- package/dist/translations/locales/hy.js +11 -0
- package/dist/translations/locales/id.d.ts +2 -0
- package/dist/translations/locales/id.js +11 -0
- package/dist/translations/locales/is.d.ts +2 -0
- package/dist/translations/locales/is.js +11 -0
- package/dist/translations/locales/it.d.ts +2 -0
- package/dist/translations/locales/it.js +11 -0
- package/dist/translations/locales/ja.d.ts +2 -0
- package/dist/translations/locales/ja.js +11 -0
- package/dist/translations/locales/ko.d.ts +2 -0
- package/dist/translations/locales/ko.js +11 -0
- package/dist/translations/locales/lt.d.ts +2 -0
- package/dist/translations/locales/lt.js +11 -0
- package/dist/translations/locales/lv.d.ts +2 -0
- package/dist/translations/locales/lv.js +11 -0
- package/dist/translations/locales/my.d.ts +2 -0
- package/dist/translations/locales/my.js +11 -0
- package/dist/translations/locales/nb.d.ts +2 -0
- package/dist/translations/locales/nb.js +11 -0
- package/dist/translations/locales/nl.d.ts +2 -0
- package/dist/translations/locales/nl.js +11 -0
- package/dist/translations/locales/pl.d.ts +2 -0
- package/dist/translations/locales/pl.js +11 -0
- package/dist/translations/locales/pt.d.ts +2 -0
- package/dist/translations/locales/pt.js +11 -0
- package/dist/translations/locales/ro.d.ts +2 -0
- package/dist/translations/locales/ro.js +11 -0
- package/dist/translations/locales/rs.d.ts +2 -0
- package/dist/translations/locales/rs.js +11 -0
- package/dist/translations/locales/rsLatin.d.ts +2 -0
- package/dist/translations/locales/rsLatin.js +11 -0
- package/dist/translations/locales/ru.d.ts +2 -0
- package/dist/translations/locales/ru.js +11 -0
- package/dist/translations/locales/sk.d.ts +2 -0
- package/dist/translations/locales/sk.js +11 -0
- package/dist/translations/locales/sl.d.ts +2 -0
- package/dist/translations/locales/sl.js +11 -0
- package/dist/translations/locales/sv.d.ts +2 -0
- package/dist/translations/locales/sv.js +11 -0
- package/dist/translations/locales/ta.d.ts +2 -0
- package/dist/translations/locales/ta.js +11 -0
- package/dist/translations/locales/th.d.ts +2 -0
- package/dist/translations/locales/th.js +11 -0
- package/dist/translations/locales/tr.d.ts +2 -0
- package/dist/translations/locales/tr.js +11 -0
- package/dist/translations/locales/uk.d.ts +2 -0
- package/dist/translations/locales/uk.js +11 -0
- package/dist/translations/locales/vi.d.ts +2 -0
- package/dist/translations/locales/vi.js +11 -0
- package/dist/translations/locales/zh.d.ts +2 -0
- package/dist/translations/locales/zh.js +11 -0
- package/dist/translations/locales/zhTw.d.ts +2 -0
- package/dist/translations/locales/zhTw.js +11 -0
- package/dist/translations/types.d.ts +11 -0
- package/dist/translations/types.js +1 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.js +2 -0
- package/dist/utils/insertField.d.ts +3 -0
- package/dist/utils/insertField.js +97 -0
- package/package.json +121 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maxim Seshuk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
# Media Preview Plugin for Payload CMS
|
|
2
|
+
|
|
3
|
+
[](https://github.com/maximseshuk/payload-plugin-media-preview/releases/) [](https://www.npmjs.com/package/@seshuk/payload-media-preview) [](https://github.com/maximseshuk/payload-plugin-media-preview/blob/main/LICENSE) [](https://www.npmjs.com/package/@seshuk/payload-media-preview) [](https://ko-fi.com/V7V61UCT39)
|
|
4
|
+
|
|
5
|
+
Preview images, videos, audio, and documents directly in the Payload CMS admin panel.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Inline previews in list view cells and edit view fields
|
|
10
|
+
- Popup previews on desktop, fullscreen modals on mobile
|
|
11
|
+
- Built-in viewers for images, video, audio, and documents (PDF, Office, etc.)
|
|
12
|
+
- Extensible adapter system for custom viewers
|
|
13
|
+
- Zero database fields — works as a virtual UI field
|
|
14
|
+
|
|
15
|
+
## Table of Contents
|
|
16
|
+
|
|
17
|
+
- [Requirements](#requirements)
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Quick Start](#quick-start)
|
|
20
|
+
- [Configuration](#configuration)
|
|
21
|
+
- [Plugin Config](#plugin-config)
|
|
22
|
+
- [Collection Config](#collection-config)
|
|
23
|
+
- [Display Modes](#display-modes)
|
|
24
|
+
- [Content Modes](#content-modes)
|
|
25
|
+
- [Field Position](#field-position)
|
|
26
|
+
- [Supported File Types](#supported-file-types)
|
|
27
|
+
- [Adapters](#adapters)
|
|
28
|
+
- [Standalone Field](#standalone-field)
|
|
29
|
+
- [Internationalization](#internationalization)
|
|
30
|
+
- [Exports](#exports)
|
|
31
|
+
- [TypeScript](#typescript)
|
|
32
|
+
- [License](#license)
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- Payload `^3.53.0`
|
|
37
|
+
- Node.js `^18.20.2 || >=20.9.0`
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pnpm add @seshuk/payload-media-preview
|
|
43
|
+
# or
|
|
44
|
+
npm install @seshuk/payload-media-preview
|
|
45
|
+
# or
|
|
46
|
+
yarn add @seshuk/payload-media-preview
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { mediaPreview } from '@seshuk/payload-media-preview'
|
|
53
|
+
import { buildConfig } from 'payload'
|
|
54
|
+
|
|
55
|
+
export default buildConfig({
|
|
56
|
+
// ...
|
|
57
|
+
plugins: [
|
|
58
|
+
mediaPreview({
|
|
59
|
+
collections: {
|
|
60
|
+
media: true,
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
],
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This adds a preview column to your `media` collection's list view and a preview button to the edit view.
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
### Plugin Config
|
|
72
|
+
|
|
73
|
+
| Option | Type | Default | Description |
|
|
74
|
+
| ------------- | ------------------------------------------ | ------- | -------------------------------------------- |
|
|
75
|
+
| `enabled` | `boolean` | `true` | Enable or disable the plugin |
|
|
76
|
+
| `adapters` | `MediaPreviewAdapter[]` | `[]` | Global adapters available to all collections |
|
|
77
|
+
| `collections` | `Record<string, CollectionConfig \| true>` | — | Which upload collections to add preview to |
|
|
78
|
+
|
|
79
|
+
### Collection Config
|
|
80
|
+
|
|
81
|
+
Each collection entry can be `true` (all defaults) or an object:
|
|
82
|
+
|
|
83
|
+
| Option | Type | Default | Description |
|
|
84
|
+
| ------------- | ------------------------------------ | ---------- | ----------------------------------------------------------------- |
|
|
85
|
+
| `mode` | `'auto' \| 'fullscreen'` | `'auto'` | Preview display mode |
|
|
86
|
+
| `contentMode` | `Partial<MediaPreviewContentMode>` | all inline | How to open each content type (`'inline'` or `'newTab'`) |
|
|
87
|
+
| `adapters` | `MediaPreviewAdapter[]` | — | Per-collection adapters (override global) |
|
|
88
|
+
| `field` | `false \| { position?, overrides? }` | `{}` | Field injection config, or `false` to skip (for manual placement) |
|
|
89
|
+
|
|
90
|
+
**`field` options:**
|
|
91
|
+
|
|
92
|
+
| Option | Type | Default | Description |
|
|
93
|
+
| ----------- | ------------------------------------------ | -------- | ---------------------------------------------------------------- |
|
|
94
|
+
| `position` | `'first' \| 'last' \| { after \| before }` | `'last'` | Where to insert the preview field |
|
|
95
|
+
| `overrides` | `Partial<Omit<UIField, 'name' \| 'type'>>` | — | Payload UI field overrides (`name` and `type` cannot be changed) |
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
mediaPreview({
|
|
99
|
+
collections: {
|
|
100
|
+
media: true,
|
|
101
|
+
'hero-images': {
|
|
102
|
+
mode: 'fullscreen',
|
|
103
|
+
contentMode: { video: 'newTab' },
|
|
104
|
+
field: { position: { after: 'alt' } },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Display Modes
|
|
111
|
+
|
|
112
|
+
The `mode` option controls how previews are displayed:
|
|
113
|
+
|
|
114
|
+
#### `'auto'` (default)
|
|
115
|
+
|
|
116
|
+
Smart mode that adapts to context and device:
|
|
117
|
+
|
|
118
|
+
| Context | Desktop | Mobile |
|
|
119
|
+
| ----------------- | ---------------------------- | ---------------- |
|
|
120
|
+
| Cell (list view) | Floating popup near the cell | Fullscreen modal |
|
|
121
|
+
| Field (edit view) | Fullscreen modal | Fullscreen modal |
|
|
122
|
+
|
|
123
|
+
#### `'fullscreen'`
|
|
124
|
+
|
|
125
|
+
Always uses a fullscreen modal, regardless of context or device.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
collections: {
|
|
129
|
+
media: {
|
|
130
|
+
mode: 'fullscreen',
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Content Modes
|
|
136
|
+
|
|
137
|
+
Control how each content type is opened with the `contentMode` option. Each content type can be set to `'inline'` (default) or `'newTab'`:
|
|
138
|
+
|
|
139
|
+
| Mode | Behavior |
|
|
140
|
+
| ---------- | --------------------------------- |
|
|
141
|
+
| `'inline'` | Show content in modal preview |
|
|
142
|
+
| `'newTab'` | Open content in a new browser tab |
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
collections: {
|
|
146
|
+
media: {
|
|
147
|
+
contentMode: {
|
|
148
|
+
video: 'newTab', // open videos in a new tab
|
|
149
|
+
document: 'newTab', // open documents in a new tab
|
|
150
|
+
image: 'inline', // show images in modal (default)
|
|
151
|
+
audio: 'inline', // show audio in modal (default)
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Field Position
|
|
158
|
+
|
|
159
|
+
Control where the preview field appears in the edit view with the `field.position` option:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// At the end (default)
|
|
163
|
+
field: {
|
|
164
|
+
position: 'last'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// At the beginning
|
|
168
|
+
field: {
|
|
169
|
+
position: 'first'
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// After a specific field
|
|
173
|
+
field: {
|
|
174
|
+
position: {
|
|
175
|
+
after: 'alt'
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Before a specific field
|
|
180
|
+
field: {
|
|
181
|
+
position: {
|
|
182
|
+
before: 'description'
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Dot-notation paths are supported for fields inside named tabs and nested groups:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
// After a field inside a tab
|
|
191
|
+
field: {
|
|
192
|
+
position: {
|
|
193
|
+
after: 'myTab.fieldName'
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Supported File Types
|
|
199
|
+
|
|
200
|
+
#### Images
|
|
201
|
+
|
|
202
|
+
All `image/*` MIME types — displayed using the native `<img>` element.
|
|
203
|
+
|
|
204
|
+
#### Video
|
|
205
|
+
|
|
206
|
+
All `video/*` MIME types — displayed using the native `<video>` element with controls.
|
|
207
|
+
|
|
208
|
+
#### Audio
|
|
209
|
+
|
|
210
|
+
All `audio/*` MIME types — displayed using the native `<audio>` element with controls.
|
|
211
|
+
|
|
212
|
+
#### Documents
|
|
213
|
+
|
|
214
|
+
Documents are previewed via external viewer services:
|
|
215
|
+
|
|
216
|
+
**Microsoft Office Online Viewer** — for Office formats:
|
|
217
|
+
|
|
218
|
+
- `.doc`, `.docx` (Word)
|
|
219
|
+
- `.xls`, `.xlsx` (Excel)
|
|
220
|
+
- `.ppt`, `.pptx` (PowerPoint)
|
|
221
|
+
|
|
222
|
+
**Google Docs Viewer** — for other document types:
|
|
223
|
+
|
|
224
|
+
- `.pdf`
|
|
225
|
+
- `.txt`, `.css`, `.html`, `.js`, `.php`, `.c`, `.cpp`
|
|
226
|
+
- `.pages` (Apple Pages)
|
|
227
|
+
- `.ai`, `.eps`, `.ps` (PostScript)
|
|
228
|
+
- `.psd` (Photoshop)
|
|
229
|
+
- `.dxf` (AutoCAD)
|
|
230
|
+
- `.svg`
|
|
231
|
+
- `.xps`
|
|
232
|
+
|
|
233
|
+
> **Note:** Document previews use external services (Google Docs Viewer, Microsoft Office Online) that fetch files by URL. This only works when your media URLs are publicly accessible. Google Docs Viewer also has a 25 MB file size limit.
|
|
234
|
+
|
|
235
|
+
## Adapters
|
|
236
|
+
|
|
237
|
+
Adapters let you customize how files are previewed. When an adapter matches a document, it takes priority over built-in viewers and `contentMode` settings.
|
|
238
|
+
|
|
239
|
+
Each adapter has a `resolve()` function that returns one of two modes:
|
|
240
|
+
|
|
241
|
+
- `{ mode: 'inline', props }` — renders a custom component inside the modal preview
|
|
242
|
+
- `{ mode: 'newTab', url }` — opens a link in a new browser tab
|
|
243
|
+
|
|
244
|
+
The `Component` field is only needed for `inline` mode. Adapters that only use `newTab` mode don't need a component.
|
|
245
|
+
|
|
246
|
+
### Examples
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import type { IframeViewerProps, MediaPreviewAdapter } from '@seshuk/payload-media-preview'
|
|
250
|
+
|
|
251
|
+
// Inline — custom component in modal
|
|
252
|
+
const videoAdapter: MediaPreviewAdapter = {
|
|
253
|
+
name: 'video-embed',
|
|
254
|
+
Component: 'my-pkg/client#VideoPlayer',
|
|
255
|
+
resolve: ({ doc }) => {
|
|
256
|
+
if (!doc.videoId) return null
|
|
257
|
+
return {
|
|
258
|
+
mode: 'inline',
|
|
259
|
+
props: { videoId: doc.videoId, autoplay: false },
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// NewTab — opens link in new browser tab (no Component needed)
|
|
265
|
+
const externalPreview: MediaPreviewAdapter = {
|
|
266
|
+
name: 'external',
|
|
267
|
+
resolve: ({ url }) => {
|
|
268
|
+
if (!url) return null
|
|
269
|
+
return {
|
|
270
|
+
mode: 'newTab',
|
|
271
|
+
url: `https://preview.service.com/?file=${encodeURIComponent(url)}`,
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Built-in viewer with typed props via satisfies
|
|
277
|
+
const iframeAdapter: MediaPreviewAdapter = {
|
|
278
|
+
name: 'iframe',
|
|
279
|
+
Component: '@seshuk/payload-media-preview/client#IframeViewer',
|
|
280
|
+
resolve: ({ url }) => {
|
|
281
|
+
if (!url) return null
|
|
282
|
+
return {
|
|
283
|
+
mode: 'inline',
|
|
284
|
+
props: { src: url, allowFullScreen: true } satisfies IframeViewerProps,
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Registering Adapters
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
mediaPreview({
|
|
294
|
+
adapters: [videoAdapter], // global adapters
|
|
295
|
+
collections: {
|
|
296
|
+
media: {
|
|
297
|
+
adapters: [externalPreview], // per-collection adapters
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
})
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### How Adapters Work
|
|
304
|
+
|
|
305
|
+
1. When a document is loaded, all registered adapters are tried in order
|
|
306
|
+
2. Each adapter's `resolve()` function receives `{ doc, url, mimeType }`
|
|
307
|
+
3. The first adapter to return a non-null value wins
|
|
308
|
+
4. For `inline` results, the `props` are passed to the adapter's `Component`
|
|
309
|
+
5. For `newTab` results, clicking the preview opens the URL in a new browser tab
|
|
310
|
+
6. If no adapter matches, the default built-in viewer is used
|
|
311
|
+
|
|
312
|
+
### Built-in Viewer Components
|
|
313
|
+
|
|
314
|
+
The plugin exports four built-in viewer components that you can use in adapters with `inline` mode:
|
|
315
|
+
|
|
316
|
+
| Component | Import Path | Props Type |
|
|
317
|
+
| -------------- | --------------------------------------------------- | ------------------- |
|
|
318
|
+
| `ImageViewer` | `@seshuk/payload-media-preview/client#ImageViewer` | `ImageViewerProps` |
|
|
319
|
+
| `VideoViewer` | `@seshuk/payload-media-preview/client#VideoViewer` | `VideoViewerProps` |
|
|
320
|
+
| `AudioViewer` | `@seshuk/payload-media-preview/client#AudioViewer` | `AudioViewerProps` |
|
|
321
|
+
| `IframeViewer` | `@seshuk/payload-media-preview/client#IframeViewer` | `IframeViewerProps` |
|
|
322
|
+
|
|
323
|
+
### Adapter Props Reference
|
|
324
|
+
|
|
325
|
+
`**ImageViewerProps**`
|
|
326
|
+
|
|
327
|
+
| Prop | Type | Required |
|
|
328
|
+
| ----------- | -------- | -------- |
|
|
329
|
+
| `src` | `string` | Yes |
|
|
330
|
+
| `alt` | `string` | No |
|
|
331
|
+
| `className` | `string` | No |
|
|
332
|
+
|
|
333
|
+
**`VideoViewerProps`**
|
|
334
|
+
|
|
335
|
+
| Prop | Type | Required |
|
|
336
|
+
| ----------- | -------------------------------- | -------- |
|
|
337
|
+
| `src` | `string` | Yes |
|
|
338
|
+
| `mimeType` | `string` | No |
|
|
339
|
+
| `title` | `string` | No |
|
|
340
|
+
| `controls` | `boolean` | No |
|
|
341
|
+
| `autoPlay` | `boolean` | No |
|
|
342
|
+
| `loop` | `boolean` | No |
|
|
343
|
+
| `muted` | `boolean` | No |
|
|
344
|
+
| `preload` | `'auto' \| 'metadata' \| 'none'` | No |
|
|
345
|
+
| `className` | `string` | No |
|
|
346
|
+
|
|
347
|
+
**`AudioViewerProps`**
|
|
348
|
+
|
|
349
|
+
| Prop | Type | Required |
|
|
350
|
+
| ----------- | -------------------------------- | -------- |
|
|
351
|
+
| `src` | `string` | Yes |
|
|
352
|
+
| `mimeType` | `string` | No |
|
|
353
|
+
| `title` | `string` | No |
|
|
354
|
+
| `controls` | `boolean` | No |
|
|
355
|
+
| `autoPlay` | `boolean` | No |
|
|
356
|
+
| `loop` | `boolean` | No |
|
|
357
|
+
| `muted` | `boolean` | No |
|
|
358
|
+
| `preload` | `'auto' \| 'metadata' \| 'none'` | No |
|
|
359
|
+
| `className` | `string` | No |
|
|
360
|
+
|
|
361
|
+
**`IframeViewerProps`**
|
|
362
|
+
|
|
363
|
+
| Prop | Type | Required |
|
|
364
|
+
| ----------------- | ------------------- | -------- |
|
|
365
|
+
| `src` | `string` | Yes |
|
|
366
|
+
| `title` | `string` | No |
|
|
367
|
+
| `allow` | `string` | No |
|
|
368
|
+
| `allowFullScreen` | `boolean` | No |
|
|
369
|
+
| `loading` | `'eager' \| 'lazy'` | No |
|
|
370
|
+
| `className` | `string` | No |
|
|
371
|
+
|
|
372
|
+
## Standalone Field
|
|
373
|
+
|
|
374
|
+
The plugin automatically injects the preview field into configured collections. If you need more control over field placement, you can add the field manually using `mediaPreviewField()`.
|
|
375
|
+
|
|
376
|
+
### With `field: false`
|
|
377
|
+
|
|
378
|
+
Use `field: false` in the collection config to register adapters without injecting the field. This lets you place the field manually while keeping all adapter and translation registration:
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
import type { MediaPreviewAdapter } from '@seshuk/payload-media-preview'
|
|
382
|
+
import { mediaPreview, mediaPreviewField } from '@seshuk/payload-media-preview'
|
|
383
|
+
|
|
384
|
+
const videoAdapter: MediaPreviewAdapter = {
|
|
385
|
+
name: 'video-embed',
|
|
386
|
+
Component: 'my-pkg/client#VideoPlayer',
|
|
387
|
+
resolve: ({ doc }) => {
|
|
388
|
+
if (!doc.videoId) return null
|
|
389
|
+
return { mode: 'inline', props: { videoId: doc.videoId } }
|
|
390
|
+
},
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export default buildConfig({
|
|
394
|
+
collections: [
|
|
395
|
+
{
|
|
396
|
+
slug: 'media',
|
|
397
|
+
upload: true,
|
|
398
|
+
fields: [
|
|
399
|
+
{ name: 'alt', type: 'text' },
|
|
400
|
+
mediaPreviewField({
|
|
401
|
+
adapterNames: ['video-embed'],
|
|
402
|
+
mode: 'fullscreen',
|
|
403
|
+
}),
|
|
404
|
+
{ name: 'caption', type: 'textarea' },
|
|
405
|
+
],
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
plugins: [
|
|
409
|
+
mediaPreview({
|
|
410
|
+
collections: {
|
|
411
|
+
media: {
|
|
412
|
+
adapters: [videoAdapter],
|
|
413
|
+
field: false, // don't inject — already added manually above
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
}),
|
|
417
|
+
],
|
|
418
|
+
})
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Without collection registration
|
|
422
|
+
|
|
423
|
+
If you don't need per-collection adapters, you can omit the collection from the plugin config entirely and use global adapters:
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
import { mediaPreview, mediaPreviewField } from '@seshuk/payload-media-preview'
|
|
427
|
+
|
|
428
|
+
export default buildConfig({
|
|
429
|
+
collections: [
|
|
430
|
+
{
|
|
431
|
+
slug: 'media',
|
|
432
|
+
upload: true,
|
|
433
|
+
fields: [{ name: 'alt', type: 'text' }, mediaPreviewField({ mode: 'fullscreen' })],
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
plugins: [
|
|
437
|
+
mediaPreview({
|
|
438
|
+
collections: {},
|
|
439
|
+
}),
|
|
440
|
+
],
|
|
441
|
+
})
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
The plugin must still be included to register viewer components and translations. Pass `adapterNames` to `mediaPreviewField()` to use adapters — those adapters must be registered via the plugin's `adapters` (global) or collection `adapters` config.
|
|
445
|
+
|
|
446
|
+
## Internationalization
|
|
447
|
+
|
|
448
|
+
The plugin includes translations for 44 locales. Translations are automatically merged into your Payload i18n configuration under the `@seshuk/payload-media-preview` namespace.
|
|
449
|
+
|
|
450
|
+
Supported locales: `ar`, `az`, `bg`, `bn` (BD/IN), `ca`, `cs`, `da`, `de`, `en`, `es`, `et`, `fa`, `fi`, `fr`, `he`, `hr`, `hu`, `hy`, `id`, `is`, `it`, `ja`, `ka`, `ko`, `lt`, `lv`, `mk`, `nb`, `nl`, `pl`, `pt`, `ro`, `rs` (Cyrillic/Latin), `ru`, `sk`, `sl`, `sv`, `th`, `tr`, `uk`, `vi`, `zh`, `zhTw`.
|
|
451
|
+
|
|
452
|
+
## Exports
|
|
453
|
+
|
|
454
|
+
The package provides three entry points:
|
|
455
|
+
|
|
456
|
+
| Entry Point | Description | Usage |
|
|
457
|
+
| -------------------------------------- | -------------------------------------------------- | ------------------------- |
|
|
458
|
+
| `@seshuk/payload-media-preview` | Plugin function and all public types | Server-side config |
|
|
459
|
+
| `@seshuk/payload-media-preview/client` | Client components (Field, Cell, Modal, Viewers) | `'use client'` components |
|
|
460
|
+
| `@seshuk/payload-media-preview/rsc` | Server components (MediaPreview, MediaPreviewCell) | React Server Components |
|
|
461
|
+
|
|
462
|
+
## TypeScript
|
|
463
|
+
|
|
464
|
+
All types are exported from the main entry point:
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
import type {
|
|
468
|
+
MediaPreviewAdapter,
|
|
469
|
+
MediaPreviewAdapterResolveArgs,
|
|
470
|
+
MediaPreviewAdapterInlineResult,
|
|
471
|
+
MediaPreviewAdapterNewTabResult,
|
|
472
|
+
MediaPreviewAdapterResolveResult,
|
|
473
|
+
MediaPreviewCollectionConfig,
|
|
474
|
+
MediaPreviewContentMode,
|
|
475
|
+
MediaPreviewContentModeType,
|
|
476
|
+
MediaPreviewContentType,
|
|
477
|
+
MediaPreviewFieldConfig,
|
|
478
|
+
MediaPreviewMode,
|
|
479
|
+
MediaPreviewPluginConfig,
|
|
480
|
+
InsertPosition,
|
|
481
|
+
ImageViewerProps,
|
|
482
|
+
VideoViewerProps,
|
|
483
|
+
AudioViewerProps,
|
|
484
|
+
IframeViewerProps,
|
|
485
|
+
} from '@seshuk/payload-media-preview'
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## License
|
|
491
|
+
|
|
492
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
493
|
+
|
|
494
|
+
## Related Plugins
|
|
495
|
+
|
|
496
|
+
- **[@seshuk/payload-storage-bunny](https://github.com/maximseshuk/payload-storage-bunny)** — Bunny.net storage adapter for Payload CMS
|
|
497
|
+
|
|
498
|
+
## Support
|
|
499
|
+
|
|
500
|
+
- **Bug Reports**: [GitHub Issues](https://github.com/maximseshuk/payload-plugin-media-preview/issues)
|
|
501
|
+
- **Questions**: Join the payload-plugin-media-preview in [GitHub Issues](https://github.com/maximseshuk/payload-plugin-media-preview/issues) or [Payload CMS Discord](https://discord.gg/payloadcms)
|
|
502
|
+
|
|
503
|
+
## Credits
|
|
504
|
+
|
|
505
|
+
Built with ❤️ for the Payload CMS community.
|
|
506
|
+
|
|
507
|
+
If you find this plugin useful, [buy me a coffee](https://ko-fi.com/V7V61UCT39).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MediaPreviewContentMode, MediaPreviewMode } from '@/types.js';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
type Props = {
|
|
4
|
+
adapterNewTabUrl?: string;
|
|
5
|
+
contentMode?: Partial<MediaPreviewContentMode>;
|
|
6
|
+
customViewer?: React.ReactNode;
|
|
7
|
+
media: {
|
|
8
|
+
fileSize?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
mimeType?: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
width?: number;
|
|
13
|
+
};
|
|
14
|
+
mode?: MediaPreviewMode;
|
|
15
|
+
rowId?: number | string;
|
|
16
|
+
};
|
|
17
|
+
export declare const MediaPreviewCellClient: React.FC<Props>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Pill, useTranslation } from "@payloadcms/ui";
|
|
4
|
+
import { EyeIcon } from "@payloadcms/ui/icons/Eye";
|
|
5
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { ExternalLinkIcon } from "../ExternalLinkIcon/ExternalLinkIcon.js";
|
|
7
|
+
import { canPreviewDocument, getDocumentViewerType, getGoogleViewerUrl, getMicrosoftViewerUrl, getPreviewType } from "../MediaPreview.utils.js";
|
|
8
|
+
import { MediaPreviewModal } from "../Modal/Modal.js";
|
|
9
|
+
export const MediaPreviewCellClient = ({ adapterNewTabUrl, contentMode, customViewer, media, mode = 'auto', rowId })=>{
|
|
10
|
+
const { fileSize, height, mimeType, url, width } = media;
|
|
11
|
+
const [isTouchDevice, setIsTouchDevice] = useState(false);
|
|
12
|
+
const [showModal, setShowModal] = useState(false);
|
|
13
|
+
const buttonRef = useRef(null);
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const audioViewerMode = contentMode?.audio ?? 'inline';
|
|
16
|
+
const documentViewerMode = contentMode?.document ?? 'inline';
|
|
17
|
+
const imageViewerMode = contentMode?.image ?? 'inline';
|
|
18
|
+
const videoViewerMode = contentMode?.video ?? 'inline';
|
|
19
|
+
const previewType = useMemo(()=>getPreviewType(mimeType), [
|
|
20
|
+
mimeType
|
|
21
|
+
]);
|
|
22
|
+
const isAudioFile = previewType === 'audio';
|
|
23
|
+
const isDocumentFile = previewType === 'document';
|
|
24
|
+
const isImageFile = previewType === 'image';
|
|
25
|
+
const isVideoFile = previewType === 'video';
|
|
26
|
+
const canPreview = !isDocumentFile || canPreviewDocument(fileSize);
|
|
27
|
+
const modalMode = useMemo(()=>mode === 'fullscreen' ? 'fullscreen' : isTouchDevice ? 'fullscreen' : 'popup', [
|
|
28
|
+
mode,
|
|
29
|
+
isTouchDevice
|
|
30
|
+
]);
|
|
31
|
+
const documentViewerUrl = useMemo(()=>{
|
|
32
|
+
if (isDocumentFile && url && mimeType) {
|
|
33
|
+
const viewerType = getDocumentViewerType(mimeType);
|
|
34
|
+
return viewerType === 'microsoft' ? getMicrosoftViewerUrl(url) : getGoogleViewerUrl(url);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}, [
|
|
38
|
+
isDocumentFile,
|
|
39
|
+
url,
|
|
40
|
+
mimeType
|
|
41
|
+
]);
|
|
42
|
+
useEffect(()=>{
|
|
43
|
+
setIsTouchDevice('ontouchstart' in window || navigator.maxTouchPoints > 0);
|
|
44
|
+
}, []);
|
|
45
|
+
const handleClick = useCallback((e)=>{
|
|
46
|
+
e.stopPropagation();
|
|
47
|
+
setShowModal((prev)=>!prev);
|
|
48
|
+
}, []);
|
|
49
|
+
const renderNewTabLink = useCallback((href)=>/*#__PURE__*/ _jsx("div", {
|
|
50
|
+
className: "media-preview-cell",
|
|
51
|
+
children: /*#__PURE__*/ _jsx("a", {
|
|
52
|
+
className: "media-preview-cell__button-wrapper",
|
|
53
|
+
href: href,
|
|
54
|
+
rel: "noopener noreferrer",
|
|
55
|
+
target: "_blank",
|
|
56
|
+
children: /*#__PURE__*/ _jsx(Pill, {
|
|
57
|
+
alignIcon: "left",
|
|
58
|
+
className: "media-preview-cell__pill",
|
|
59
|
+
icon: /*#__PURE__*/ _jsx(ExternalLinkIcon, {
|
|
60
|
+
className: "media-preview-cell__icon"
|
|
61
|
+
}),
|
|
62
|
+
size: "small",
|
|
63
|
+
children: t('@seshuk/payload-media-preview:open')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
}), [
|
|
67
|
+
t
|
|
68
|
+
]);
|
|
69
|
+
if (previewType === 'unsupported' || !url || !canPreview) {
|
|
70
|
+
if (!customViewer) {
|
|
71
|
+
return /*#__PURE__*/ _jsx("span", {
|
|
72
|
+
children: "—"
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (adapterNewTabUrl) {
|
|
77
|
+
return renderNewTabLink(adapterNewTabUrl);
|
|
78
|
+
}
|
|
79
|
+
if (!customViewer) {
|
|
80
|
+
if (isVideoFile && videoViewerMode === 'newTab' && url) {
|
|
81
|
+
return renderNewTabLink(url);
|
|
82
|
+
}
|
|
83
|
+
if (isAudioFile && audioViewerMode === 'newTab' && url) {
|
|
84
|
+
return renderNewTabLink(url);
|
|
85
|
+
}
|
|
86
|
+
if (isImageFile && imageViewerMode === 'newTab' && url) {
|
|
87
|
+
return renderNewTabLink(url);
|
|
88
|
+
}
|
|
89
|
+
if (isDocumentFile && documentViewerMode === 'newTab' && documentViewerUrl) {
|
|
90
|
+
return renderNewTabLink(documentViewerUrl);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
94
|
+
children: [
|
|
95
|
+
/*#__PURE__*/ _jsx("div", {
|
|
96
|
+
className: "media-preview-cell",
|
|
97
|
+
children: /*#__PURE__*/ _jsx("button", {
|
|
98
|
+
className: "media-preview-cell__button-wrapper",
|
|
99
|
+
onClick: handleClick,
|
|
100
|
+
ref: buttonRef,
|
|
101
|
+
type: "button",
|
|
102
|
+
children: /*#__PURE__*/ _jsx(Pill, {
|
|
103
|
+
alignIcon: "left",
|
|
104
|
+
className: `media-preview-cell__pill ${showModal ? 'media-preview-cell__pill--active' : ''}`,
|
|
105
|
+
icon: /*#__PURE__*/ _jsx(EyeIcon, {
|
|
106
|
+
active: modalMode === 'popup' && showModal,
|
|
107
|
+
className: "media-preview-cell__icon"
|
|
108
|
+
}),
|
|
109
|
+
size: "small",
|
|
110
|
+
children: modalMode === 'popup' && showModal ? t('@seshuk/payload-media-preview:close') : t('@seshuk/payload-media-preview:open')
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
}),
|
|
114
|
+
/*#__PURE__*/ _jsx(MediaPreviewModal, {
|
|
115
|
+
customViewer: customViewer,
|
|
116
|
+
media: {
|
|
117
|
+
documentViewerUrl,
|
|
118
|
+
height,
|
|
119
|
+
mimeType,
|
|
120
|
+
url,
|
|
121
|
+
width
|
|
122
|
+
},
|
|
123
|
+
mode: modalMode,
|
|
124
|
+
onClose: ()=>setShowModal(false),
|
|
125
|
+
rowId: rowId,
|
|
126
|
+
show: showModal,
|
|
127
|
+
triggerRef: buttonRef
|
|
128
|
+
})
|
|
129
|
+
]
|
|
130
|
+
});
|
|
131
|
+
};
|