@zerohive/hive-viewer 0.2.0 → 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 +257 -112
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +261 -113
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +261 -113
- 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
|
|
|
@@ -457,17 +514,29 @@ var RichTextEditor = (0, import_react2.forwardRef)((props, ref) => {
|
|
|
457
514
|
}
|
|
458
515
|
if (props.fileType === "docx") {
|
|
459
516
|
try {
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
517
|
+
const response = await fetch("/api/export-docx", {
|
|
518
|
+
method: "POST",
|
|
519
|
+
headers: { "Content-Type": "application/json" },
|
|
520
|
+
body: JSON.stringify({
|
|
521
|
+
html: stitched,
|
|
522
|
+
fileName: replaceExt(props.fileName, "docx")
|
|
523
|
+
})
|
|
467
524
|
});
|
|
525
|
+
if (!response.ok) throw new Error("Failed to generate DOCX");
|
|
526
|
+
const blob = await response.blob();
|
|
527
|
+
const url = window.URL.createObjectURL(blob);
|
|
528
|
+
const a = document.createElement("a");
|
|
529
|
+
a.href = url;
|
|
530
|
+
a.download = replaceExt(props.fileName, "docx");
|
|
531
|
+
document.body.appendChild(a);
|
|
532
|
+
a.click();
|
|
533
|
+
setTimeout(() => {
|
|
534
|
+
window.URL.revokeObjectURL(url);
|
|
535
|
+
a.remove();
|
|
536
|
+
}, 100);
|
|
468
537
|
} catch (err) {
|
|
469
538
|
alert(
|
|
470
|
-
"DOCX export
|
|
539
|
+
"DOCX export failed: " + (err instanceof Error ? err.message : String(err))
|
|
471
540
|
);
|
|
472
541
|
}
|
|
473
542
|
return;
|
|
@@ -631,7 +700,11 @@ function ensureExt(name, ext) {
|
|
|
631
700
|
// src/renderers/ImageRenderer.tsx
|
|
632
701
|
var import_react4 = require("react");
|
|
633
702
|
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
634
|
-
function ImageRenderer({
|
|
703
|
+
function ImageRenderer({
|
|
704
|
+
arrayBuffer,
|
|
705
|
+
fileType,
|
|
706
|
+
fileName
|
|
707
|
+
}) {
|
|
635
708
|
const [zoom, setZoom] = (0, import_react4.useState)(1);
|
|
636
709
|
const url = (0, import_react4.useMemo)(() => {
|
|
637
710
|
if (!arrayBuffer) return void 0;
|
|
@@ -647,14 +720,42 @@ function ImageRenderer({ arrayBuffer, fileType, fileName }) {
|
|
|
647
720
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hv-mini-toolbar", children: [
|
|
648
721
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hv-title", children: fileName }),
|
|
649
722
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hv-spacer" }),
|
|
650
|
-
/* @__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
|
+
),
|
|
651
732
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hv-zoom", children: [
|
|
652
733
|
Math.round(zoom * 100),
|
|
653
734
|
"%"
|
|
654
735
|
] }),
|
|
655
|
-
/* @__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
|
+
)
|
|
656
745
|
] }),
|
|
657
|
-
/* @__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
|
+
] })
|
|
658
759
|
] });
|
|
659
760
|
}
|
|
660
761
|
|
|
@@ -671,37 +772,57 @@ function extractText(xml) {
|
|
|
671
772
|
function PptxRenderer(props) {
|
|
672
773
|
const [slides, setSlides] = (0, import_react5.useState)([]);
|
|
673
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);
|
|
674
777
|
(0, import_react5.useEffect)(() => {
|
|
675
778
|
let cancelled = false;
|
|
779
|
+
setError(null);
|
|
780
|
+
setLoading(true);
|
|
676
781
|
(async () => {
|
|
677
782
|
setSlides([]);
|
|
678
783
|
setThumbs([]);
|
|
679
784
|
if (!props.arrayBuffer) {
|
|
680
785
|
props.onSlideCount(1);
|
|
681
786
|
setSlides([{ index: 1, text: "No content" }]);
|
|
787
|
+
setError("No PPTX data provided.");
|
|
788
|
+
setLoading(false);
|
|
682
789
|
return;
|
|
683
790
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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);
|
|
694
824
|
}
|
|
695
|
-
|
|
696
|
-
const count = Math.max(1, out.length);
|
|
697
|
-
props.onSlideCount(count);
|
|
698
|
-
setSlides(out.length ? out : [{ index: 1, text: "(empty)" }]);
|
|
699
|
-
setThumbs(Array.from({ length: count }, (_, i) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgThumb(i + 1))}`));
|
|
700
|
-
})().catch(() => {
|
|
701
|
-
props.onSlideCount(1);
|
|
702
|
-
setSlides([{ index: 1, text: "Unable to render this .pptx in-browser." }]);
|
|
703
|
-
setThumbs([void 0]);
|
|
704
|
-
});
|
|
825
|
+
})();
|
|
705
826
|
return () => {
|
|
706
827
|
cancelled = true;
|
|
707
828
|
};
|
|
@@ -710,19 +831,43 @@ function PptxRenderer(props) {
|
|
|
710
831
|
props.onThumbs(thumbs);
|
|
711
832
|
}, [thumbs]);
|
|
712
833
|
const pagesToShow = (0, import_react5.useMemo)(() => {
|
|
713
|
-
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
|
+
];
|
|
714
839
|
return [props.currentPage];
|
|
715
840
|
}, [props.currentPage, props.layout, slides.length]);
|
|
716
|
-
return /* @__PURE__ */ (0, import_jsx_runtime8.
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
+
] });
|
|
726
871
|
}
|
|
727
872
|
function svgThumb(n) {
|
|
728
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>`;
|