@zerohive/hive-viewer 0.2.1 → 0.2.2
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/README.md +123 -6
- package/dist/index.cjs +237 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +241 -105
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +241 -105
- package/dist/styles.css +75 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
# ModalViewer Usage Example
|
|
2
|
+
|
|
3
|
+
You can use the ModalViewer component to display any content (such as DocumentViewer) in a modal dialog. Here is a simple example:
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import React, { useState } from 'react';
|
|
7
|
+
import { ModalViewer } from './src/components/ModalViewer';
|
|
8
|
+
import { DocumentViewer } from './src/components/DocumentViewer';
|
|
9
|
+
import './src/components/ModalViewer.css';
|
|
10
|
+
|
|
11
|
+
export default function App() {
|
|
12
|
+
const [open, setOpen] = useState(false);
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<button onClick={() => setOpen(true)}>Open Document Modal</button>
|
|
16
|
+
<ModalViewer open={open} onClose={() => setOpen(false)}>
|
|
17
|
+
<DocumentViewer url="/path/to/document.pdf" />
|
|
18
|
+
</ModalViewer>
|
|
19
|
+
</>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Props:**
|
|
25
|
+
|
|
26
|
+
- `open` (boolean): Whether the modal is visible.
|
|
27
|
+
- `onClose` (function): Called when the modal requests to close (overlay click, ESC, close button).
|
|
28
|
+
- `children` (ReactNode): Content to display inside the modal.
|
|
29
|
+
- `ariaLabel` (optional string): Accessibility label for the modal dialog.
|
|
30
|
+
|
|
31
|
+
**Styling:**
|
|
32
|
+
|
|
33
|
+
Import `ModalViewer.css` for default modal styles, or customize as needed.
|
|
1
34
|
# @zerohive/hive-viewer
|
|
2
35
|
|
|
3
36
|
A self-hostable, browser-first document viewer/editor for React and Next.js.
|
|
@@ -11,13 +44,15 @@ npm i @zerohive/hive-viewer
|
|
|
11
44
|
Import styles once in your app:
|
|
12
45
|
|
|
13
46
|
```ts
|
|
14
|
-
import
|
|
47
|
+
import "@zerohive/hive-viewer/styles.css";
|
|
15
48
|
```
|
|
16
49
|
|
|
17
50
|
## Usage
|
|
18
51
|
|
|
52
|
+
### Basic Usage
|
|
53
|
+
|
|
19
54
|
```tsx
|
|
20
|
-
import { DocumentViewer } from
|
|
55
|
+
import { DocumentViewer } from "@zerohive/hive-viewer";
|
|
21
56
|
|
|
22
57
|
export default function Page() {
|
|
23
58
|
return (
|
|
@@ -28,17 +63,99 @@ export default function Page() {
|
|
|
28
63
|
fileType="pdf"
|
|
29
64
|
allowSigning
|
|
30
65
|
onSignRequest={async () => ({
|
|
31
|
-
signatureImageUrl:
|
|
32
|
-
signedBy:
|
|
66
|
+
signatureImageUrl: "https://.../sig.png",
|
|
67
|
+
signedBy: "Jane Doe",
|
|
33
68
|
dateSigned: new Date().toISOString(),
|
|
34
|
-
comment:
|
|
69
|
+
comment: "Approved",
|
|
35
70
|
})}
|
|
36
|
-
onSave={(b64, meta) => {
|
|
71
|
+
onSave={(b64, meta) => {
|
|
72
|
+
/* persist */
|
|
73
|
+
}}
|
|
37
74
|
/>
|
|
38
75
|
);
|
|
39
76
|
}
|
|
40
77
|
```
|
|
41
78
|
|
|
79
|
+
### Using in a Modal (Recommended)
|
|
80
|
+
|
|
81
|
+
Most consumers use the viewer in a modal dialog. Here is a recommended pattern:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import React, { useState } from "react";
|
|
85
|
+
import { DocumentViewer } from "@zerohive/hive-viewer";
|
|
86
|
+
|
|
87
|
+
function ModalDocViewer({ open, onClose, fileUrl, fileName, fileType }) {
|
|
88
|
+
if (!open) return null;
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
style={{
|
|
92
|
+
position: "fixed",
|
|
93
|
+
inset: 0,
|
|
94
|
+
background: "rgba(0,0,0,0.45)",
|
|
95
|
+
zIndex: 1000,
|
|
96
|
+
display: "flex",
|
|
97
|
+
alignItems: "center",
|
|
98
|
+
justifyContent: "center",
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<div
|
|
102
|
+
style={{
|
|
103
|
+
background: "#fff",
|
|
104
|
+
borderRadius: 16,
|
|
105
|
+
maxWidth: "90vw",
|
|
106
|
+
maxHeight: "90vh",
|
|
107
|
+
overflow: "auto",
|
|
108
|
+
position: "relative",
|
|
109
|
+
padding: 0,
|
|
110
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.25)",
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<button
|
|
114
|
+
onClick={onClose}
|
|
115
|
+
aria-label="Close"
|
|
116
|
+
style={{
|
|
117
|
+
position: "absolute",
|
|
118
|
+
top: 12,
|
|
119
|
+
right: 16,
|
|
120
|
+
background: "none",
|
|
121
|
+
border: "none",
|
|
122
|
+
fontSize: "2rem",
|
|
123
|
+
color: "#888",
|
|
124
|
+
cursor: "pointer",
|
|
125
|
+
zIndex: 1,
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
×
|
|
129
|
+
</button>
|
|
130
|
+
<DocumentViewer
|
|
131
|
+
mode="view"
|
|
132
|
+
fileUrl={fileUrl}
|
|
133
|
+
fileName={fileName}
|
|
134
|
+
fileType={fileType}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Usage example:
|
|
142
|
+
export default function Example() {
|
|
143
|
+
const [open, setOpen] = useState(false);
|
|
144
|
+
return (
|
|
145
|
+
<>
|
|
146
|
+
<button onClick={() => setOpen(true)}>Open Document</button>
|
|
147
|
+
<ModalDocViewer
|
|
148
|
+
open={open}
|
|
149
|
+
onClose={() => setOpen(false)}
|
|
150
|
+
fileUrl="https://example.com/my.pdf"
|
|
151
|
+
fileName="my.pdf"
|
|
152
|
+
fileType="pdf"
|
|
153
|
+
/>
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
42
159
|
## Signing Workflow (decoupled)
|
|
43
160
|
|
|
44
161
|
- If `allowSigning={true}`, the toolbar shows **Sign Document**.
|
package/dist/index.cjs
CHANGED
|
@@ -39,7 +39,7 @@ var import_react6 = require("react");
|
|
|
39
39
|
|
|
40
40
|
// src/utils/locale.ts
|
|
41
41
|
var defaultLocale = {
|
|
42
|
-
|
|
42
|
+
loading: "Loading\u2026",
|
|
43
43
|
"error.title": "Error",
|
|
44
44
|
"toolbar.layout.single": "Single page",
|
|
45
45
|
"toolbar.layout.two": "Side-by-side",
|
|
@@ -62,7 +62,17 @@ var defaultLocale = {
|
|
|
62
62
|
function guessFileType(name, explicit) {
|
|
63
63
|
if (explicit) return explicit;
|
|
64
64
|
const ext = (name?.split(".").pop() || "").toLowerCase();
|
|
65
|
-
const allowed = [
|
|
65
|
+
const allowed = [
|
|
66
|
+
"pdf",
|
|
67
|
+
"md",
|
|
68
|
+
"docx",
|
|
69
|
+
"xlsx",
|
|
70
|
+
"pptx",
|
|
71
|
+
"txt",
|
|
72
|
+
"png",
|
|
73
|
+
"jpg",
|
|
74
|
+
"svg"
|
|
75
|
+
];
|
|
66
76
|
return allowed.includes(ext) ? ext : "txt";
|
|
67
77
|
}
|
|
68
78
|
function arrayBufferToBase64(buf) {
|
|
@@ -93,7 +103,8 @@ async function resolveSource(args) {
|
|
|
93
103
|
const ab = await base64ToArrayBuffer(args.base64);
|
|
94
104
|
return { fileType, fileName, arrayBuffer: ab };
|
|
95
105
|
}
|
|
96
|
-
if (!args.fileUrl)
|
|
106
|
+
if (!args.fileUrl)
|
|
107
|
+
throw new Error("No file source provided. Use fileUrl, blob, or base64.");
|
|
97
108
|
const res = await fetch(args.fileUrl);
|
|
98
109
|
if (!res.ok) throw new Error(`Failed to fetch file (${res.status})`);
|
|
99
110
|
const total = Number(res.headers.get("content-length") || "") || void 0;
|
|
@@ -216,38 +227,60 @@ function PdfRenderer(props) {
|
|
|
216
227
|
const { url, arrayBuffer } = props;
|
|
217
228
|
const [doc, setDoc] = (0, import_react.useState)(null);
|
|
218
229
|
const [pageCount, setPageCount] = (0, import_react.useState)(0);
|
|
219
|
-
const [rendered, setRendered] = (0, import_react.useState)(
|
|
230
|
+
const [rendered, setRendered] = (0, import_react.useState)(
|
|
231
|
+
/* @__PURE__ */ new Map()
|
|
232
|
+
);
|
|
220
233
|
const [thumbs, setThumbs] = (0, import_react.useState)([]);
|
|
221
234
|
const [size, setSize] = (0, import_react.useState)({ w: 840, h: 1188 });
|
|
235
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
236
|
+
const [loading, setLoading] = (0, import_react.useState)(false);
|
|
222
237
|
const containerRef = (0, import_react.useRef)(null);
|
|
223
238
|
(0, import_react.useEffect)(() => {
|
|
224
239
|
try {
|
|
225
|
-
import_pdfjs_dist.GlobalWorkerOptions.workerSrc = new URL(
|
|
240
|
+
import_pdfjs_dist.GlobalWorkerOptions.workerSrc = new URL(
|
|
241
|
+
"pdfjs-dist/build/pdf.worker.min.mjs",
|
|
242
|
+
import_meta.url
|
|
243
|
+
).toString();
|
|
226
244
|
} catch {
|
|
227
245
|
}
|
|
228
246
|
}, []);
|
|
229
247
|
(0, import_react.useEffect)(() => {
|
|
230
248
|
let cancel = false;
|
|
249
|
+
setError(null);
|
|
250
|
+
setLoading(true);
|
|
231
251
|
(async () => {
|
|
232
252
|
setDoc(null);
|
|
233
253
|
setRendered(/* @__PURE__ */ new Map());
|
|
234
254
|
setThumbs([]);
|
|
235
|
-
if (!url && !arrayBuffer)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
255
|
+
if (!url && !arrayBuffer) {
|
|
256
|
+
setError("No PDF source provided.");
|
|
257
|
+
setLoading(false);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const task = (0, import_pdfjs_dist.getDocument)(
|
|
262
|
+
url ? { url, rangeChunkSize: 512 * 1024 } : { data: arrayBuffer }
|
|
263
|
+
);
|
|
264
|
+
const pdf = await task.promise;
|
|
265
|
+
if (cancel) return;
|
|
266
|
+
setDoc(pdf);
|
|
267
|
+
setPageCount(pdf.numPages);
|
|
268
|
+
props.onPageCount(pdf.numPages);
|
|
269
|
+
setThumbs(Array.from({ length: pdf.numPages }));
|
|
270
|
+
const p1 = await pdf.getPage(1);
|
|
271
|
+
const base = p1.getViewport({ scale: 1 });
|
|
272
|
+
const w = Math.min(980, Math.max(640, base.width));
|
|
273
|
+
const s = w / base.width;
|
|
274
|
+
const vp = p1.getViewport({ scale: s });
|
|
275
|
+
setSize({ w: Math.round(vp.width), h: Math.round(vp.height) });
|
|
276
|
+
} catch (e) {
|
|
277
|
+
setError(
|
|
278
|
+
"Failed to load PDF. " + (e instanceof Error ? e.message : "")
|
|
279
|
+
);
|
|
280
|
+
} finally {
|
|
281
|
+
setLoading(false);
|
|
282
|
+
}
|
|
283
|
+
})();
|
|
251
284
|
return () => {
|
|
252
285
|
cancel = true;
|
|
253
286
|
};
|
|
@@ -269,37 +302,43 @@ function PdfRenderer(props) {
|
|
|
269
302
|
(async () => {
|
|
270
303
|
for (const p of pagesToShow) {
|
|
271
304
|
if (rendered.has(p)) continue;
|
|
272
|
-
const page = await doc.getPage(p);
|
|
273
|
-
if (cancel) return;
|
|
274
|
-
const base = page.getViewport({ scale: 1 });
|
|
275
|
-
const vp = page.getViewport({ scale: size.w / base.width });
|
|
276
|
-
const canvas = document.createElement("canvas");
|
|
277
|
-
canvas.width = Math.round(vp.width);
|
|
278
|
-
canvas.height = Math.round(vp.height);
|
|
279
|
-
const ctx = canvas.getContext("2d", { alpha: false });
|
|
280
|
-
if (!ctx) continue;
|
|
281
|
-
await page.render({ canvasContext: ctx, viewport: vp }).promise;
|
|
282
|
-
if (cancel) return;
|
|
283
|
-
setRendered((prev) => {
|
|
284
|
-
const next = new Map(prev);
|
|
285
|
-
next.set(p, canvas);
|
|
286
|
-
return next;
|
|
287
|
-
});
|
|
288
305
|
try {
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
306
|
+
const page = await doc.getPage(p);
|
|
307
|
+
if (cancel) return;
|
|
308
|
+
const base = page.getViewport({ scale: 1 });
|
|
309
|
+
const vp = page.getViewport({ scale: size.w / base.width });
|
|
310
|
+
const canvas = document.createElement("canvas");
|
|
311
|
+
canvas.width = Math.round(vp.width);
|
|
312
|
+
canvas.height = Math.round(vp.height);
|
|
313
|
+
const ctx = canvas.getContext("2d", { alpha: false });
|
|
314
|
+
if (!ctx) continue;
|
|
315
|
+
await page.render({ canvasContext: ctx, viewport: vp }).promise;
|
|
316
|
+
if (cancel) return;
|
|
317
|
+
setRendered((prev) => {
|
|
318
|
+
const next = new Map(prev);
|
|
319
|
+
next.set(p, canvas);
|
|
320
|
+
return next;
|
|
321
|
+
});
|
|
322
|
+
if (!thumbs[p - 1]) {
|
|
323
|
+
const thumbCanvas = document.createElement("canvas");
|
|
324
|
+
const thumbScale = 120 / vp.width;
|
|
325
|
+
thumbCanvas.width = Math.round(vp.width * thumbScale);
|
|
326
|
+
thumbCanvas.height = Math.round(vp.height * thumbScale);
|
|
327
|
+
const thumbCtx = thumbCanvas.getContext("2d", { alpha: false });
|
|
328
|
+
if (thumbCtx) {
|
|
329
|
+
thumbCtx.drawImage(
|
|
330
|
+
canvas,
|
|
331
|
+
0,
|
|
332
|
+
0,
|
|
333
|
+
thumbCanvas.width,
|
|
334
|
+
thumbCanvas.height
|
|
335
|
+
);
|
|
336
|
+
setThumbs((prev) => {
|
|
337
|
+
const arr = prev.slice();
|
|
338
|
+
arr[p - 1] = thumbCanvas.toDataURL("image/png");
|
|
339
|
+
return arr;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
303
342
|
}
|
|
304
343
|
} catch {
|
|
305
344
|
}
|
|
@@ -308,13 +347,16 @@ function PdfRenderer(props) {
|
|
|
308
347
|
return () => {
|
|
309
348
|
cancel = true;
|
|
310
349
|
};
|
|
311
|
-
}, [doc, pagesToShow, size.w, rendered]);
|
|
350
|
+
}, [doc, pagesToShow, size.w, rendered, thumbs]);
|
|
312
351
|
function onWheel(e) {
|
|
313
352
|
if (!pageCount) return;
|
|
314
353
|
if (Math.abs(e.deltaY) < 10) return;
|
|
315
354
|
const dir = e.deltaY > 0 ? 1 : -1;
|
|
316
355
|
const step = props.layout === "side-by-side" ? 2 : 1;
|
|
317
|
-
const next = Math.max(
|
|
356
|
+
const next = Math.max(
|
|
357
|
+
1,
|
|
358
|
+
Math.min(pageCount, props.currentPage + dir * step)
|
|
359
|
+
);
|
|
318
360
|
props.onCurrentPageChange(next);
|
|
319
361
|
}
|
|
320
362
|
function clickPlace(e, page) {
|
|
@@ -327,22 +369,37 @@ function PdfRenderer(props) {
|
|
|
327
369
|
}
|
|
328
370
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hv-doc", ref: containerRef, onWheel, children: [
|
|
329
371
|
!doc ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hv-loading", children: "Loading PDF\u2026" }) : null,
|
|
330
|
-
doc ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
"
|
|
334
|
-
{
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
372
|
+
doc ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
373
|
+
"div",
|
|
374
|
+
{
|
|
375
|
+
className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages",
|
|
376
|
+
children: pagesToShow.map((p) => {
|
|
377
|
+
const c = rendered.get(p);
|
|
378
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
379
|
+
"div",
|
|
380
|
+
{
|
|
381
|
+
className: "hv-page",
|
|
382
|
+
style: { width: size.w, height: size.h },
|
|
383
|
+
onClick: (e) => clickPlace(e, p),
|
|
384
|
+
children: c ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
385
|
+
"canvas",
|
|
386
|
+
{
|
|
387
|
+
className: "hv-canvas",
|
|
388
|
+
width: c.width,
|
|
389
|
+
height: c.height,
|
|
390
|
+
ref: (node) => {
|
|
391
|
+
if (!node) return;
|
|
392
|
+
const ctx = node.getContext("2d");
|
|
393
|
+
if (ctx) ctx.drawImage(c, 0, 0);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hv-loading", children: "Rendering\u2026" })
|
|
397
|
+
},
|
|
398
|
+
p
|
|
399
|
+
);
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
) : null
|
|
346
403
|
] });
|
|
347
404
|
}
|
|
348
405
|
|
|
@@ -643,7 +700,11 @@ function ensureExt(name, ext) {
|
|
|
643
700
|
// src/renderers/ImageRenderer.tsx
|
|
644
701
|
var import_react4 = require("react");
|
|
645
702
|
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
646
|
-
function ImageRenderer({
|
|
703
|
+
function ImageRenderer({
|
|
704
|
+
arrayBuffer,
|
|
705
|
+
fileType,
|
|
706
|
+
fileName
|
|
707
|
+
}) {
|
|
647
708
|
const [zoom, setZoom] = (0, import_react4.useState)(1);
|
|
648
709
|
const url = (0, import_react4.useMemo)(() => {
|
|
649
710
|
if (!arrayBuffer) return void 0;
|
|
@@ -659,14 +720,42 @@ function ImageRenderer({ arrayBuffer, fileType, fileName }) {
|
|
|
659
720
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hv-mini-toolbar", children: [
|
|
660
721
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hv-title", children: fileName }),
|
|
661
722
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hv-spacer" }),
|
|
662
|
-
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
723
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
724
|
+
"button",
|
|
725
|
+
{
|
|
726
|
+
type: "button",
|
|
727
|
+
className: "hv-btn",
|
|
728
|
+
onClick: () => setZoom((z) => Math.max(0.25, z - 0.25)),
|
|
729
|
+
children: "-"
|
|
730
|
+
}
|
|
731
|
+
),
|
|
663
732
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hv-zoom", children: [
|
|
664
733
|
Math.round(zoom * 100),
|
|
665
734
|
"%"
|
|
666
735
|
] }),
|
|
667
|
-
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
736
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
737
|
+
"button",
|
|
738
|
+
{
|
|
739
|
+
type: "button",
|
|
740
|
+
className: "hv-btn",
|
|
741
|
+
onClick: () => setZoom((z) => Math.min(4, z + 0.25)),
|
|
742
|
+
children: "+"
|
|
743
|
+
}
|
|
744
|
+
)
|
|
668
745
|
] }),
|
|
669
|
-
/* @__PURE__ */ (0, import_jsx_runtime7.
|
|
746
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hv-center", children: [
|
|
747
|
+
!arrayBuffer && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hv-error", children: "No image data provided." }),
|
|
748
|
+
arrayBuffer && !url && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hv-error", children: "Failed to load image." }),
|
|
749
|
+
url && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
750
|
+
"img",
|
|
751
|
+
{
|
|
752
|
+
src: url,
|
|
753
|
+
alt: fileName,
|
|
754
|
+
style: { transform: `scale(${zoom})` },
|
|
755
|
+
className: "hv-image"
|
|
756
|
+
}
|
|
757
|
+
)
|
|
758
|
+
] })
|
|
670
759
|
] });
|
|
671
760
|
}
|
|
672
761
|
|
|
@@ -683,37 +772,57 @@ function extractText(xml) {
|
|
|
683
772
|
function PptxRenderer(props) {
|
|
684
773
|
const [slides, setSlides] = (0, import_react5.useState)([]);
|
|
685
774
|
const [thumbs, setThumbs] = (0, import_react5.useState)([]);
|
|
775
|
+
const [error, setError] = (0, import_react5.useState)(null);
|
|
776
|
+
const [loading, setLoading] = (0, import_react5.useState)(false);
|
|
686
777
|
(0, import_react5.useEffect)(() => {
|
|
687
778
|
let cancelled = false;
|
|
779
|
+
setError(null);
|
|
780
|
+
setLoading(true);
|
|
688
781
|
(async () => {
|
|
689
782
|
setSlides([]);
|
|
690
783
|
setThumbs([]);
|
|
691
784
|
if (!props.arrayBuffer) {
|
|
692
785
|
props.onSlideCount(1);
|
|
693
786
|
setSlides([{ index: 1, text: "No content" }]);
|
|
787
|
+
setError("No PPTX data provided.");
|
|
788
|
+
setLoading(false);
|
|
694
789
|
return;
|
|
695
790
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
791
|
+
try {
|
|
792
|
+
const zip = await import_jszip.default.loadAsync(props.arrayBuffer);
|
|
793
|
+
const files = Object.keys(zip.files).filter((p) => /^ppt\/slides\/slide\d+\.xml$/.test(p)).sort((a, b) => {
|
|
794
|
+
const na = Number(a.match(/slide(\d+)\.xml/)?.[1] || 0);
|
|
795
|
+
const nb = Number(b.match(/slide(\d+)\.xml/)?.[1] || 0);
|
|
796
|
+
return na - nb;
|
|
797
|
+
});
|
|
798
|
+
const out = [];
|
|
799
|
+
for (let i = 0; i < files.length; i++) {
|
|
800
|
+
const xml = await zip.file(files[i]).async("string");
|
|
801
|
+
out.push({ index: i + 1, text: extractText(xml) });
|
|
802
|
+
}
|
|
803
|
+
if (cancelled) return;
|
|
804
|
+
const count = Math.max(1, out.length);
|
|
805
|
+
props.onSlideCount(count);
|
|
806
|
+
setSlides(out.length ? out : [{ index: 1, text: "(empty)" }]);
|
|
807
|
+
setThumbs(
|
|
808
|
+
Array.from(
|
|
809
|
+
{ length: count },
|
|
810
|
+
(_, i) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgThumb(i + 1))}`
|
|
811
|
+
)
|
|
812
|
+
);
|
|
813
|
+
} catch (e) {
|
|
814
|
+
props.onSlideCount(1);
|
|
815
|
+
setSlides([
|
|
816
|
+
{ index: 1, text: "Unable to render this .pptx in-browser." }
|
|
817
|
+
]);
|
|
818
|
+
setThumbs([void 0]);
|
|
819
|
+
setError(
|
|
820
|
+
"Failed to load PPTX. " + (e instanceof Error ? e.message : "")
|
|
821
|
+
);
|
|
822
|
+
} finally {
|
|
823
|
+
setLoading(false);
|
|
706
824
|
}
|
|
707
|
-
|
|
708
|
-
const count = Math.max(1, out.length);
|
|
709
|
-
props.onSlideCount(count);
|
|
710
|
-
setSlides(out.length ? out : [{ index: 1, text: "(empty)" }]);
|
|
711
|
-
setThumbs(Array.from({ length: count }, (_, i) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgThumb(i + 1))}`));
|
|
712
|
-
})().catch(() => {
|
|
713
|
-
props.onSlideCount(1);
|
|
714
|
-
setSlides([{ index: 1, text: "Unable to render this .pptx in-browser." }]);
|
|
715
|
-
setThumbs([void 0]);
|
|
716
|
-
});
|
|
825
|
+
})();
|
|
717
826
|
return () => {
|
|
718
827
|
cancelled = true;
|
|
719
828
|
};
|
|
@@ -722,19 +831,43 @@ function PptxRenderer(props) {
|
|
|
722
831
|
props.onThumbs(thumbs);
|
|
723
832
|
}, [thumbs]);
|
|
724
833
|
const pagesToShow = (0, import_react5.useMemo)(() => {
|
|
725
|
-
if (props.layout === "side-by-side")
|
|
834
|
+
if (props.layout === "side-by-side")
|
|
835
|
+
return [
|
|
836
|
+
props.currentPage,
|
|
837
|
+
Math.min(slides.length || props.currentPage + 1, props.currentPage + 1)
|
|
838
|
+
];
|
|
726
839
|
return [props.currentPage];
|
|
727
840
|
}, [props.currentPage, props.layout, slides.length]);
|
|
728
|
-
return /* @__PURE__ */ (0, import_jsx_runtime8.
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
841
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "hv-doc", children: [
|
|
842
|
+
loading && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "hv-loading", children: "Loading PPTX\u2026" }),
|
|
843
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "hv-error", children: error }),
|
|
844
|
+
!loading && !error && (!slides || slides.length === 0) && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "hv-error", children: "No slides to display." }),
|
|
845
|
+
!error && slides && slides.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
846
|
+
"div",
|
|
847
|
+
{
|
|
848
|
+
className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages",
|
|
849
|
+
children: pagesToShow.map((p) => {
|
|
850
|
+
const s = slides[p - 1];
|
|
851
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
852
|
+
"div",
|
|
853
|
+
{
|
|
854
|
+
className: "hv-slide",
|
|
855
|
+
tabIndex: 0,
|
|
856
|
+
onFocus: () => props.onCurrentPageChange(p),
|
|
857
|
+
children: [
|
|
858
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "hv-slide-title", children: [
|
|
859
|
+
"Slide ",
|
|
860
|
+
p
|
|
861
|
+
] }),
|
|
862
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "hv-slide-text", children: s?.text || "" })
|
|
863
|
+
]
|
|
864
|
+
},
|
|
865
|
+
p
|
|
866
|
+
);
|
|
867
|
+
})
|
|
868
|
+
}
|
|
869
|
+
)
|
|
870
|
+
] });
|
|
738
871
|
}
|
|
739
872
|
function svgThumb(n) {
|
|
740
873
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="180" height="100"><rect width="100%" height="100%" rx="12" fill="#111827"/><text x="50%" y="54%" font-size="18" fill="#e5e7eb" text-anchor="middle">${n}</text></svg>`;
|