react-file-preview-engine 0.0.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/LICENSE +10 -0
- package/README.md +312 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +240 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.js +1 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Sahil Aggarwal, <aggarwalsahil2004@gmail.com>
|
|
4
|
+
Copyright (c) 2022 Igor Gaponov (gapon2401)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
7
|
+
|
|
8
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# react-file-preview-engine
|
|
2
|
+
|
|
3
|
+
A renderer driven React file preview engine designed for extensibility, correctness, and long term maintainability.
|
|
4
|
+
|
|
5
|
+
`react-file-preview-engine` lets you preview files by delegating rendering to small, isolated renderers that decide if they can handle a file based on runtime context. It supports common media types out of the box and makes it trivial to add or override renderers for custom formats.
|
|
6
|
+
|
|
7
|
+
## Why react-file-preview-engine
|
|
8
|
+
|
|
9
|
+
Inspired by [`@codesmith-99/react-file-preview`](https://github.com/AbdulmueezEmiola/React-File-Previewer) and redesigned to address its architectural and maintenance limitations.
|
|
10
|
+
|
|
11
|
+
- Renderer driven architecture instead of hard coded conditional rendering
|
|
12
|
+
- Built in support for images, video, audio, pdf, html, and plain text
|
|
13
|
+
- Automatic MIME type resolution using file name when needed
|
|
14
|
+
- Supports multiple file source types including URL, File, Blob, and ArrayBuffer
|
|
15
|
+
- Stable loading, ready, and error state handling
|
|
16
|
+
- Abortable fetches with proper cleanup for fast file switching
|
|
17
|
+
- First class support for custom renderers
|
|
18
|
+
- Optional, fully typed additional render context
|
|
19
|
+
- Pluggable error renderer
|
|
20
|
+
- Fully typed public API
|
|
21
|
+
- React is not bundled as a direct dependency
|
|
22
|
+
- Actively maintained and designed for long term extensibility
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# npm
|
|
28
|
+
npm install react-file-preview-engine
|
|
29
|
+
|
|
30
|
+
# yarn
|
|
31
|
+
yarn add react-file-preview-engine
|
|
32
|
+
|
|
33
|
+
# pnpm
|
|
34
|
+
pnpm add react-file-preview-engine
|
|
35
|
+
|
|
36
|
+
# bun
|
|
37
|
+
bun add react-file-preview-engine
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
`react-file-preview-engine` exposes a single default export, the `<FilePreviewer />` component.
|
|
43
|
+
|
|
44
|
+
You can pass it a file source and basic file metadata. The source can be a `URL`, `File`, `Blob`, or `ArrayBuffer`. Using the provided MIME type or file name, the engine dynamically resolves the most suitable renderer at runtime.
|
|
45
|
+
|
|
46
|
+
### Basic Usage
|
|
47
|
+
|
|
48
|
+
This is the minimal setup. The previewer automatically infers the MIME type from `fileName` if not provided.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import React from "react";
|
|
52
|
+
import FilePreviewer from "react-file-preview-engine";
|
|
53
|
+
|
|
54
|
+
export default function App() {
|
|
55
|
+
return <FilePreviewer src="https://example.com/sample.pdf" fileName="sample.pdf" />;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Previewing Local Files
|
|
60
|
+
|
|
61
|
+
You can preview files selected by the user without uploading them first. The engine automatically converts File, Blob, and ArrayBuffer sources to object URLs internally.
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import React, { useState } from "react";
|
|
65
|
+
import FilePreviewer from "react-file-preview-engine";
|
|
66
|
+
|
|
67
|
+
export default function App() {
|
|
68
|
+
const [file, setFile] = useState<File>();
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
<input type="file" onChange={(e) => setFile(e.target.files?.[0])} />
|
|
73
|
+
{file && <FilePreviewer src={file} fileName={file.name} mimeType={file.type} />}
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Providing MIME Type Explicitly
|
|
80
|
+
|
|
81
|
+
If you already know the MIME type, you can pass it directly. This ensures the engine uses the correct renderer even when inference is not possible.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import React from "react";
|
|
85
|
+
import FilePreviewer from "react-file-preview-engine";
|
|
86
|
+
|
|
87
|
+
export default function App() {
|
|
88
|
+
return <FilePreviewer src={new Blob(["Hello world"], { type: "text/plain" })} mimeType="text/plain" fileName="hello.txt" />;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Handling Load and Error Events
|
|
93
|
+
|
|
94
|
+
You can listen to lifecycle events triggered by the active renderer.
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import React from "react";
|
|
98
|
+
import FilePreviewer from "react-file-preview-engine";
|
|
99
|
+
|
|
100
|
+
export default function App() {
|
|
101
|
+
return (
|
|
102
|
+
<FilePreviewer
|
|
103
|
+
src="/document.pdf"
|
|
104
|
+
fileName="document.pdf"
|
|
105
|
+
onLoad={() => {
|
|
106
|
+
console.log("File loaded");
|
|
107
|
+
}}
|
|
108
|
+
onError={() => {
|
|
109
|
+
console.error("Failed to load file");
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Auto Play Media Files
|
|
117
|
+
|
|
118
|
+
For audio and video files, you can enable auto play.
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import React from "react";
|
|
122
|
+
import FilePreviewer from "react-file-preview-engine";
|
|
123
|
+
|
|
124
|
+
export default function App() {
|
|
125
|
+
return <FilePreviewer src="https://example.com/video.mp4" fileName="video.mp4" autoPlay={true} />;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Partial Customization
|
|
130
|
+
|
|
131
|
+
You can customize the loader, container props, and icon props without modifying any renderers.
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
import React from "react";
|
|
135
|
+
import FilePreviewer from "react-file-preview-engine";
|
|
136
|
+
|
|
137
|
+
export default function App() {
|
|
138
|
+
return (
|
|
139
|
+
<FilePreviewer
|
|
140
|
+
src="/image.jpg"
|
|
141
|
+
fileName="image.jpg"
|
|
142
|
+
loader={<div>Loading preview…</div>}
|
|
143
|
+
containerProps={{ style: { border: "1px solid #e5e7eb", padding: 8 } }}
|
|
144
|
+
iconProps={{ style: { fontSize: 48 } }}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Custom Error Renderer
|
|
151
|
+
|
|
152
|
+
When a renderer reports an error, the previewer switches to `errorRenderer`.
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import React from "react";
|
|
156
|
+
import FilePreviewer from "react-file-preview-engine";
|
|
157
|
+
|
|
158
|
+
const errorRenderer = {
|
|
159
|
+
Component() {
|
|
160
|
+
return <div>Preview unavailable</div>;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export default function App() {
|
|
165
|
+
return <FilePreviewer src="/missing.pdf" fileName="missing.pdf" errorRenderer={errorRenderer} />;
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Custom Renderers
|
|
170
|
+
|
|
171
|
+
`react-file-preview-engine` resolves previews using renderers. A renderer declares whether it can handle a file using `canRender` and renders the preview inside its `Component`.
|
|
172
|
+
|
|
173
|
+
Renderers receive a render context that includes `src`, `mimeType`, `fileName`, lifecycle callbacks, and any values passed via `additionalContext`. The same context mechanism applies to `customRenderers` and `errorRenderer`.
|
|
174
|
+
|
|
175
|
+
#### Renderer Resolution Order
|
|
176
|
+
|
|
177
|
+
When previewing a file, the engine checks renderers in this order:
|
|
178
|
+
|
|
179
|
+
1. Your `customRenderers` (from first to last)
|
|
180
|
+
2. Built-in default renderers
|
|
181
|
+
3. `fallbackRenderer` (icon fallback)
|
|
182
|
+
|
|
183
|
+
This means you can **override** built-in renderers by providing a custom renderer with the same `canRender` logic.
|
|
184
|
+
|
|
185
|
+
#### Markdown Renderer Example
|
|
186
|
+
|
|
187
|
+
This example adds support for markdown files. By default, it renders markdown as html, but allows switching to raw text using `additionalContext`.
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
import React, { useEffect, useState } from "react";
|
|
191
|
+
import FilePreviewer from "react-file-preview-engine";
|
|
192
|
+
import type { Renderer } from "react-file-preview-engine/types";
|
|
193
|
+
import remarkGfm from "remark-gfm";
|
|
194
|
+
import rehypeRaw from "rehype-raw";
|
|
195
|
+
import { useRemark } from "react-remarkify";
|
|
196
|
+
import rehypeSanitize from "rehype-sanitize";
|
|
197
|
+
|
|
198
|
+
const markdownRenderer: Renderer<{ renderAsHtml?: boolean }> = {
|
|
199
|
+
name: "markdown",
|
|
200
|
+
canRender({ mimeType }) {
|
|
201
|
+
return mimeType === "text/markdown";
|
|
202
|
+
},
|
|
203
|
+
Component({ src, onLoad, onError, renderAsHtml = true }) {
|
|
204
|
+
const [markdown, setMarkdown] = useState("");
|
|
205
|
+
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
const controller = new AbortController();
|
|
208
|
+
|
|
209
|
+
fetch(src, { signal: controller.signal })
|
|
210
|
+
.then((res) => res.text())
|
|
211
|
+
.then((text) => {
|
|
212
|
+
setMarkdown(text);
|
|
213
|
+
onLoad();
|
|
214
|
+
})
|
|
215
|
+
.catch(onError);
|
|
216
|
+
|
|
217
|
+
return () => controller.abort();
|
|
218
|
+
}, [src]);
|
|
219
|
+
|
|
220
|
+
const reactContent = useRemark({
|
|
221
|
+
markdown,
|
|
222
|
+
remarkPlugins: [remarkGfm],
|
|
223
|
+
rehypePlugins: [rehypeRaw, rehypeSanitize],
|
|
224
|
+
remarkToRehypeOptions: { allowDangerousHtml: true },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return renderAsHtml ? <div>{reactContent}</div> : <pre>{markdown}</pre>;
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export default function App() {
|
|
232
|
+
return <FilePreviewer src="/README.md" fileName="README.md" customRenderers={[markdownRenderer]} />;
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### Passing Additional Context
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
export default function App() {
|
|
240
|
+
return <FilePreviewer src="/README.md" fileName="README.md" customRenderers={[markdownRenderer]} additionalContext={{ renderAsHtml: false }} />;
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### Default Renderers
|
|
245
|
+
|
|
246
|
+
The library includes built-in renderers for text, pdf, html, images, audio, and video. These handle common file types without any configuration.
|
|
247
|
+
|
|
248
|
+
You can explore the actual renderer definitions in the [`defaultRenderers` constant on GitHub](https://github.com/SahilAggarwal2004/react-file-preview-engine/blob/main/src/lib/rendererRegistry.tsx).
|
|
249
|
+
|
|
250
|
+
## API Reference
|
|
251
|
+
|
|
252
|
+
### FilePreviewer Props
|
|
253
|
+
|
|
254
|
+
| Prop | Type | Required | Default | Description |
|
|
255
|
+
| ------------------- | ------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
256
|
+
| `src` | [`FileSource`](#filesource) | Yes | – | Source URL of the file to preview |
|
|
257
|
+
| `mimeType` | `string` | No | inferred | MIME type of the file |
|
|
258
|
+
| `fileName` | `string` | No | - | Used for MIME inference and accessibility |
|
|
259
|
+
| `autoPlay` | `boolean` | No | `false` | Auto play audio and video |
|
|
260
|
+
| `loader` | `ReactNode` | No | [`<Loader />`](https://github.com/SahilAggarwal2004/react-file-preview-engine/blob/main/src/components.tsx) | Shown while the file is loading |
|
|
261
|
+
| `customRenderers` | [`Renderer[]`](#renderer) | No | - | Additional or overriding renderers |
|
|
262
|
+
| `additionalContext` | `object` | No | - | Extra data passed to renderers, including errorRenderer |
|
|
263
|
+
| `errorRenderer` | [`Renderer`](#renderer) | No | [`fallbackRenderer`](https://github.com/SahilAggarwal2004/react-file-preview-engine/blob/main/src/lib/rendererRegistry.tsx) | Renderer used on error |
|
|
264
|
+
| `containerProps` | [`DivProps`](#divprops) | No | - | Props applied to preview container |
|
|
265
|
+
| `iconProps` | [`DivProps`](#divprops) | No | - | Props applied to fallback icon |
|
|
266
|
+
| `onLoad` | [`Eventhandler`](#eventhandler) | No | – | Called when preview is ready |
|
|
267
|
+
| `onError` | [`Eventhandler`](#eventhandler) | No | – | Called when preview fails |
|
|
268
|
+
|
|
269
|
+
## Types
|
|
270
|
+
|
|
271
|
+
### DivProps
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { DetailedHTMLProps, HTMLAttributes } from "react";
|
|
275
|
+
|
|
276
|
+
type DivProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### EventHandler
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
export type EventHandler = () => void;
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### FileSource
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
export type FileSource = string | File | Blob | ArrayBuffer;
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Renderer
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
type RenderContext<T extends object = {}> = {
|
|
295
|
+
src: string;
|
|
296
|
+
mimeType: string;
|
|
297
|
+
fileName: string;
|
|
298
|
+
autoPlay: boolean;
|
|
299
|
+
iconProps: DivProps;
|
|
300
|
+
onLoad: () => void;
|
|
301
|
+
onError: () => void;
|
|
302
|
+
} & T;
|
|
303
|
+
type Renderer<T extends object = {}> = {
|
|
304
|
+
name?: string;
|
|
305
|
+
canRender?(ctx: RenderContext<T>): boolean;
|
|
306
|
+
Component(ctx: RenderContext<T>): JSX.Element;
|
|
307
|
+
};
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Author
|
|
311
|
+
|
|
312
|
+
[Sahil Aggarwal](https://www.github.com/SahilAggarwal2004)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FilePreviewerProps } from './types.js';
|
|
3
|
+
|
|
4
|
+
declare function FilePreviewer<T extends object = {}>({ src, mimeType, fileName, autoPlay, loader, customRenderers, additionalContext, errorRenderer, containerProps, iconProps, onLoad, onError, }: FilePreviewerProps<T>): React.JSX.Element;
|
|
5
|
+
|
|
6
|
+
export { FilePreviewer as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import Mime from 'mime/lite';
|
|
2
|
+
import React2, { useMemo, useState, useRef, useEffect } from 'react';
|
|
3
|
+
import { FileIcon, defaultStyles } from 'react-file-icon';
|
|
4
|
+
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __defProps = Object.defineProperties;
|
|
7
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
8
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
11
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
12
|
+
var __spreadValues = (a, b) => {
|
|
13
|
+
for (var prop in b || (b = {}))
|
|
14
|
+
if (__hasOwnProp.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
if (__getOwnPropSymbols)
|
|
17
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
18
|
+
if (__propIsEnum.call(b, prop))
|
|
19
|
+
__defNormalProp(a, prop, b[prop]);
|
|
20
|
+
}
|
|
21
|
+
return a;
|
|
22
|
+
};
|
|
23
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
24
|
+
var __async = (__this, __arguments, generator) => {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
var fulfilled = (value) => {
|
|
27
|
+
try {
|
|
28
|
+
step(generator.next(value));
|
|
29
|
+
} catch (e) {
|
|
30
|
+
reject(e);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var rejected = (value) => {
|
|
34
|
+
try {
|
|
35
|
+
step(generator.throw(value));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
reject(e);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
41
|
+
step((generator = generator.apply(__this, __arguments)).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
function Loader({ children, text }) {
|
|
45
|
+
return /* @__PURE__ */ React2.createElement("div", { className: "rfpe-loader" }, /* @__PURE__ */ React2.createElement("div", { className: "rfpe-loader-spinner" }), children != null ? children : /* @__PURE__ */ React2.createElement("div", { className: "rfpe-loader-text" }, text));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/constants.ts
|
|
49
|
+
var defaults = { additionalContext: {}, customRenderers: [], props: {} };
|
|
50
|
+
function useResolvedSrc(src) {
|
|
51
|
+
const objectUrlRef = useRef(null);
|
|
52
|
+
const revokeObjectURL = () => {
|
|
53
|
+
if (!objectUrlRef.current) return;
|
|
54
|
+
URL.revokeObjectURL(objectUrlRef.current);
|
|
55
|
+
objectUrlRef.current = null;
|
|
56
|
+
};
|
|
57
|
+
const resolvedSrc = useMemo(() => {
|
|
58
|
+
revokeObjectURL();
|
|
59
|
+
if (typeof src === "string") return src;
|
|
60
|
+
const url = src instanceof File || src instanceof Blob ? URL.createObjectURL(src) : src instanceof ArrayBuffer ? URL.createObjectURL(new Blob([src])) : "";
|
|
61
|
+
if (url) objectUrlRef.current = url;
|
|
62
|
+
return url;
|
|
63
|
+
}, [src]);
|
|
64
|
+
useEffect(() => revokeObjectURL, []);
|
|
65
|
+
return resolvedSrc;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/lib/utils.ts
|
|
69
|
+
var composeClass = (baseClass, props) => `${baseClass}${(props == null ? void 0 : props.className) ? " " + props.className : ""}`;
|
|
70
|
+
var composeProps = (baseClass, props, overrideProps) => {
|
|
71
|
+
const mergedProps = __spreadValues(__spreadValues({}, props), overrideProps);
|
|
72
|
+
return __spreadProps(__spreadValues({}, mergedProps), {
|
|
73
|
+
style: __spreadValues(__spreadValues({}, props == null ? void 0 : props.style), overrideProps == null ? void 0 : overrideProps.style),
|
|
74
|
+
className: composeClass(baseClass, mergedProps)
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
function fetchResource(src, type, signal) {
|
|
78
|
+
return __async(this, null, function* () {
|
|
79
|
+
const res = yield fetch(src, { signal });
|
|
80
|
+
if (!res.ok) throw new Error();
|
|
81
|
+
return res[type]();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/lib/rendererRegistry.tsx
|
|
86
|
+
var audioRenderer = {
|
|
87
|
+
canRender: ({ mimeType }) => mimeType.startsWith("audio/"),
|
|
88
|
+
Component({ src, mimeType, fileName, autoPlay, onLoad, onError }) {
|
|
89
|
+
return /* @__PURE__ */ React2.createElement("audio", { controls: true, autoPlay, onCanPlay: onLoad, onError, style: { width: "100%" }, "aria-label": fileName || "Audio preview" }, /* @__PURE__ */ React2.createElement("source", { src, type: mimeType }));
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
var fallbackRenderer = {
|
|
93
|
+
Component({ mimeType, iconProps, onLoad }) {
|
|
94
|
+
const extension = useMemo(() => {
|
|
95
|
+
var _a;
|
|
96
|
+
return (_a = Mime.getExtension(mimeType)) != null ? _a : "";
|
|
97
|
+
}, [mimeType]);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
onLoad();
|
|
100
|
+
}, []);
|
|
101
|
+
return /* @__PURE__ */ React2.createElement("div", __spreadValues({}, composeProps("rfpe-icon", iconProps)), /* @__PURE__ */ React2.createElement(FileIcon, __spreadValues({ extension }, defaultStyles[extension])));
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var htmlRenderer = {
|
|
105
|
+
canRender: ({ mimeType }) => mimeType === "text/html",
|
|
106
|
+
Component({ src, onLoad, onError }) {
|
|
107
|
+
const [data, setData] = useState("");
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
fetchResource(src, "text", controller.signal).then((data2) => {
|
|
111
|
+
setData(data2);
|
|
112
|
+
onLoad();
|
|
113
|
+
}).catch(onError);
|
|
114
|
+
return () => controller.abort();
|
|
115
|
+
}, [src]);
|
|
116
|
+
return /* @__PURE__ */ React2.createElement("iframe", { src: `data:text/html; charset=utf-8,${encodeURIComponent(data)}`, sandbox: "", className: "rfpe-iframe" });
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var imageRenderer = {
|
|
120
|
+
canRender: ({ mimeType }) => mimeType.startsWith("image/"),
|
|
121
|
+
Component({ src, fileName, onLoad, onError }) {
|
|
122
|
+
return /* @__PURE__ */ React2.createElement("img", { src, alt: fileName || "Image preview", onLoad, onError, style: { width: "100%", height: "100%" } });
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var pdfRenderer = {
|
|
126
|
+
canRender: ({ mimeType }) => mimeType === "application/pdf",
|
|
127
|
+
Component({ src, onLoad, onError }) {
|
|
128
|
+
const [data, setData] = useState("");
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
let objectUrl;
|
|
132
|
+
fetchResource(src, "arrayBuffer", controller.signal).then((buffer) => {
|
|
133
|
+
const blob = new Blob([buffer], { type: "application/pdf" });
|
|
134
|
+
objectUrl = URL.createObjectURL(blob);
|
|
135
|
+
setData(objectUrl);
|
|
136
|
+
onLoad();
|
|
137
|
+
}).catch(onError);
|
|
138
|
+
return () => {
|
|
139
|
+
controller.abort();
|
|
140
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
141
|
+
};
|
|
142
|
+
}, [src]);
|
|
143
|
+
return /* @__PURE__ */ React2.createElement("iframe", { src: data, className: "rfpe-iframe" });
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
var textRenderer = {
|
|
147
|
+
canRender: ({ mimeType }) => mimeType === "text/plain",
|
|
148
|
+
Component({ src, onLoad, onError }) {
|
|
149
|
+
const [data, setData] = useState("");
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
fetchResource(src, "text", controller.signal).then((data2) => {
|
|
153
|
+
setData(data2);
|
|
154
|
+
onLoad();
|
|
155
|
+
}).catch(onError);
|
|
156
|
+
return () => controller.abort();
|
|
157
|
+
}, [src]);
|
|
158
|
+
return /* @__PURE__ */ React2.createElement("div", { style: { width: "100%", height: "100%", overflow: "auto", whiteSpace: "pre-wrap" } }, data);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
var videoRenderer = {
|
|
162
|
+
canRender: ({ mimeType }) => mimeType.startsWith("video/"),
|
|
163
|
+
Component({ src, mimeType, fileName, autoPlay, onLoad, onError }) {
|
|
164
|
+
return /* @__PURE__ */ React2.createElement("video", { controls: true, autoPlay, onCanPlay: onLoad, onError, style: { width: "100%", height: "100%" }, "aria-label": fileName || "Video preview" }, /* @__PURE__ */ React2.createElement("source", { src, type: mimeType }));
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
var defaultRenderers = [textRenderer, pdfRenderer, htmlRenderer, imageRenderer, audioRenderer, videoRenderer];
|
|
168
|
+
function resolveRenderer(customRenderers, ctx) {
|
|
169
|
+
var _a;
|
|
170
|
+
return (_a = customRenderers.concat(defaultRenderers).find((r) => {
|
|
171
|
+
var _a2;
|
|
172
|
+
return (_a2 = r.canRender) == null ? void 0 : _a2.call(r, ctx);
|
|
173
|
+
})) != null ? _a : fallbackRenderer;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/styles.css
|
|
177
|
+
function injectStyle(css) {
|
|
178
|
+
if (typeof document === "undefined") return;
|
|
179
|
+
const head = document.head || document.getElementsByTagName("head")[0];
|
|
180
|
+
const style = document.createElement("style");
|
|
181
|
+
style.type = "text/css";
|
|
182
|
+
if (head.firstChild) {
|
|
183
|
+
head.insertBefore(style, head.firstChild);
|
|
184
|
+
} else {
|
|
185
|
+
head.appendChild(style);
|
|
186
|
+
}
|
|
187
|
+
if (style.styleSheet) {
|
|
188
|
+
style.styleSheet.cssText = css;
|
|
189
|
+
} else {
|
|
190
|
+
style.appendChild(document.createTextNode(css));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
injectStyle(".rfpe-container {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n max-width: 100lvw;\n}\n.rfpe-icon {\n width: 50px;\n height: 50px;\n}\n.rfpe-iframe {\n width: 100%;\n height: 100%;\n background-color: white;\n}\n.rfpe-loader {\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n}\n.rfpe-loader-spinner {\n width: 22px;\n height: 22px;\n border-radius: 50%;\n border: 2px solid transparent;\n border-top-color: black;\n border-bottom-color: black;\n animation: rfpe-spin-fast 0.6s ease infinite;\n}\n.rfpe-loader-text {\n font-size: 0.875rem;\n}\n@keyframes rfpe-spin-fast {\n to {\n transform: rotate(360deg);\n }\n}\n");
|
|
194
|
+
|
|
195
|
+
// src/index.tsx
|
|
196
|
+
var { additionalContext: defaultContext, customRenderers: defaultRenderers2, props: defaultProps } = defaults;
|
|
197
|
+
function FilePreviewer({
|
|
198
|
+
src,
|
|
199
|
+
mimeType,
|
|
200
|
+
fileName = "",
|
|
201
|
+
autoPlay = false,
|
|
202
|
+
loader = /* @__PURE__ */ React2.createElement(Loader, null),
|
|
203
|
+
customRenderers = defaultRenderers2,
|
|
204
|
+
additionalContext = defaultContext,
|
|
205
|
+
errorRenderer = fallbackRenderer,
|
|
206
|
+
containerProps = defaultProps,
|
|
207
|
+
iconProps = defaultProps,
|
|
208
|
+
onLoad,
|
|
209
|
+
onError
|
|
210
|
+
}) {
|
|
211
|
+
const resolvedSrc = useResolvedSrc(src);
|
|
212
|
+
const fileType = useMemo(() => {
|
|
213
|
+
var _a;
|
|
214
|
+
return (_a = mimeType != null ? mimeType : Mime.getType(fileName)) != null ? _a : "";
|
|
215
|
+
}, [mimeType, fileName]);
|
|
216
|
+
const fileKey = `${resolvedSrc}|${fileType}|${fileName}`;
|
|
217
|
+
const [state, setState] = useState({ key: fileKey, status: "loading" });
|
|
218
|
+
if (state.key !== fileKey) setState({ key: fileKey, status: "loading" });
|
|
219
|
+
const isLoading = state.status === "loading";
|
|
220
|
+
const handleLoad = () => {
|
|
221
|
+
setState((prev) => {
|
|
222
|
+
if (prev.key !== fileKey || prev.status !== "loading") return prev;
|
|
223
|
+
onLoad == null ? void 0 : onLoad();
|
|
224
|
+
return { key: fileKey, status: "ready" };
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
const handleError = () => {
|
|
228
|
+
setState((prev) => {
|
|
229
|
+
if (prev.key !== fileKey || prev.status === "error") return prev;
|
|
230
|
+
onError == null ? void 0 : onError();
|
|
231
|
+
return { key: fileKey, status: "error" };
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
const context = __spreadValues({ src: resolvedSrc, mimeType: fileType, fileName, autoPlay, iconProps, onLoad: handleLoad, onError: handleError }, additionalContext);
|
|
235
|
+
const renderer = useMemo(() => resolveRenderer(customRenderers, context), [resolvedSrc, fileType, fileName, autoPlay, additionalContext]);
|
|
236
|
+
const ActiveRenderer = state.status === "error" ? errorRenderer : renderer;
|
|
237
|
+
return /* @__PURE__ */ React2.createElement(React2.Fragment, null, isLoading && loader, /* @__PURE__ */ React2.createElement("div", __spreadValues({}, composeProps("rfpe-container", containerProps, { style: { visibility: isLoading ? "hidden" : "visible" } })), /* @__PURE__ */ React2.createElement(ActiveRenderer.Component, __spreadValues({ key: fileKey }, context))));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export { FilePreviewer as default };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ReactNode, DetailedHTMLProps, HTMLAttributes, JSX } from 'react';
|
|
2
|
+
|
|
3
|
+
type LoaderProps = {
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
text?: string;
|
|
6
|
+
};
|
|
7
|
+
type EventHandler = () => void;
|
|
8
|
+
type RenderBehaviour = {
|
|
9
|
+
autoPlay: boolean;
|
|
10
|
+
iconProps: DivProps;
|
|
11
|
+
onLoad: EventHandler;
|
|
12
|
+
onError: EventHandler;
|
|
13
|
+
};
|
|
14
|
+
type RenderContext<T extends object = {}> = {
|
|
15
|
+
src: string;
|
|
16
|
+
mimeType: string;
|
|
17
|
+
fileName: string;
|
|
18
|
+
} & RenderBehaviour & T;
|
|
19
|
+
type Renderer<T extends object = {}> = {
|
|
20
|
+
name?: string;
|
|
21
|
+
canRender?(ctx: RenderContext<T>): boolean;
|
|
22
|
+
Component(ctx: RenderContext<T>): JSX.Element;
|
|
23
|
+
};
|
|
24
|
+
type DivProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
|
25
|
+
type FetchType = "text" | "arrayBuffer" | "blob" | "json";
|
|
26
|
+
type FilePreviewerProps<T extends object> = {
|
|
27
|
+
src: FileSource;
|
|
28
|
+
loader?: ReactNode;
|
|
29
|
+
customRenderers?: Renderer<T>[];
|
|
30
|
+
additionalContext?: T;
|
|
31
|
+
errorRenderer?: Renderer<T>;
|
|
32
|
+
containerProps?: DivProps;
|
|
33
|
+
} & MimeTypeSource & RenderBehaviour;
|
|
34
|
+
type FileSource = string | File | Blob | ArrayBuffer;
|
|
35
|
+
type MimeTypeSource = {
|
|
36
|
+
mimeType: string;
|
|
37
|
+
fileName?: string;
|
|
38
|
+
} | {
|
|
39
|
+
fileName: string;
|
|
40
|
+
mimeType?: string;
|
|
41
|
+
};
|
|
42
|
+
type State = {
|
|
43
|
+
key: string;
|
|
44
|
+
status: "loading" | "ready" | "error";
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type { DivProps, EventHandler, FetchType, FilePreviewerProps, FileSource, LoaderProps, MimeTypeSource, RenderBehaviour, RenderContext, Renderer, State };
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-file-preview-engine",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A renderer-driven React file preview engine with extensible architecture for images, video, audio, PDF, HTML, and custom formats.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Sahil Aggarwal <aggarwalsahil2004@gmail.com>",
|
|
7
|
+
"contributors": [],
|
|
8
|
+
"homepage": "https://github.com/SahilAggarwal2004/react-file-preview-engine#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/SahilAggarwal2004/react-file-preview-engine.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/SahilAggarwal2004/react-file-preview-engine/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./dist/index.js",
|
|
19
|
+
"./types": "./dist/types.js"
|
|
20
|
+
},
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"types": "dist/index.d.ts",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"mime": "^4.1.0",
|
|
28
|
+
"react-file-icon": "^1.6.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/react": "^19.0.10",
|
|
32
|
+
"@types/react-file-icon": "^1.0.4",
|
|
33
|
+
"tsup": "^8.4.0",
|
|
34
|
+
"typescript": "^5.8.2"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
38
|
+
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"arraybuffer",
|
|
42
|
+
"audio-preview",
|
|
43
|
+
"blob",
|
|
44
|
+
"component",
|
|
45
|
+
"custom-renderer",
|
|
46
|
+
"document-viewer",
|
|
47
|
+
"extensible",
|
|
48
|
+
"file",
|
|
49
|
+
"file-preview",
|
|
50
|
+
"file-viewer",
|
|
51
|
+
"image-preview",
|
|
52
|
+
"mime-type",
|
|
53
|
+
"pdf-viewer",
|
|
54
|
+
"preview",
|
|
55
|
+
"react",
|
|
56
|
+
"react-file-preview-engine",
|
|
57
|
+
"renderer",
|
|
58
|
+
"typescript",
|
|
59
|
+
"video-preview",
|
|
60
|
+
"viewer"
|
|
61
|
+
],
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "pnpm i && pnpm run compile",
|
|
64
|
+
"compile": "tsup",
|
|
65
|
+
"dev": "tsup --watch"
|
|
66
|
+
}
|
|
67
|
+
}
|