@zerohive/hive-viewer 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +859 -0
- package/package.json +7 -7
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
// src/components/DocumentViewer.tsx
|
|
2
|
+
import { useEffect as useEffect6, useMemo as useMemo6, useRef as useRef3, useState as useState6 } from "react";
|
|
3
|
+
|
|
4
|
+
// src/utils/locale.ts
|
|
5
|
+
var defaultLocale = {
|
|
6
|
+
"loading": "Loading\u2026",
|
|
7
|
+
"error.title": "Error",
|
|
8
|
+
"toolbar.layout.single": "Single page",
|
|
9
|
+
"toolbar.layout.two": "Side-by-side",
|
|
10
|
+
"toolbar.thumbs": "Thumbnails",
|
|
11
|
+
"toolbar.signatures": "Signatures",
|
|
12
|
+
"toolbar.sign": "Sign Document",
|
|
13
|
+
"toolbar.save": "Save",
|
|
14
|
+
"toolbar.exportPdf": "Export as PDF",
|
|
15
|
+
"thumbnails.title": "Thumbnails",
|
|
16
|
+
"thumbnails.page": "Page",
|
|
17
|
+
"signatures.title": "Signatures",
|
|
18
|
+
"signatures.empty": "No signatures",
|
|
19
|
+
"signatures.placeHint": "Click on the document to place the signature.",
|
|
20
|
+
"a11y.viewer": "Document viewer",
|
|
21
|
+
"a11y.ribbon": "Ribbon",
|
|
22
|
+
"a11y.editor": "Document editor"
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/utils/fileSource.ts
|
|
26
|
+
function guessFileType(name, explicit) {
|
|
27
|
+
if (explicit) return explicit;
|
|
28
|
+
const ext = (name?.split(".").pop() || "").toLowerCase();
|
|
29
|
+
const allowed = ["pdf", "md", "docx", "xlsx", "pptx", "txt", "png", "jpg", "svg"];
|
|
30
|
+
return allowed.includes(ext) ? ext : "txt";
|
|
31
|
+
}
|
|
32
|
+
function arrayBufferToBase64(buf) {
|
|
33
|
+
const bytes = new Uint8Array(buf);
|
|
34
|
+
let binary = "";
|
|
35
|
+
const chunk = 32768;
|
|
36
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
37
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
38
|
+
}
|
|
39
|
+
return btoa(binary);
|
|
40
|
+
}
|
|
41
|
+
async function base64ToArrayBuffer(b64) {
|
|
42
|
+
const bin = atob(b64);
|
|
43
|
+
const len = bin.length;
|
|
44
|
+
const bytes = new Uint8Array(len);
|
|
45
|
+
for (let i = 0; i < len; i++) bytes[i] = bin.charCodeAt(i);
|
|
46
|
+
return bytes.buffer;
|
|
47
|
+
}
|
|
48
|
+
async function resolveSource(args) {
|
|
49
|
+
const fileType = guessFileType(args.fileName, args.fileType);
|
|
50
|
+
const fileName = args.fileName ?? `document.${fileType}`;
|
|
51
|
+
if (args.blob) {
|
|
52
|
+
const ab = await args.blob.arrayBuffer();
|
|
53
|
+
const url = URL.createObjectURL(args.blob);
|
|
54
|
+
return { fileType, fileName, arrayBuffer: ab, url };
|
|
55
|
+
}
|
|
56
|
+
if (args.base64) {
|
|
57
|
+
const ab = await base64ToArrayBuffer(args.base64);
|
|
58
|
+
return { fileType, fileName, arrayBuffer: ab };
|
|
59
|
+
}
|
|
60
|
+
if (!args.fileUrl) throw new Error("No file source provided. Use fileUrl, blob, or base64.");
|
|
61
|
+
const res = await fetch(args.fileUrl);
|
|
62
|
+
if (!res.ok) throw new Error(`Failed to fetch file (${res.status})`);
|
|
63
|
+
const total = Number(res.headers.get("content-length") || "") || void 0;
|
|
64
|
+
if (!res.body) {
|
|
65
|
+
const ab = await res.arrayBuffer();
|
|
66
|
+
args.onProgress?.(ab.byteLength, total);
|
|
67
|
+
return { fileType, fileName, arrayBuffer: ab, url: args.fileUrl };
|
|
68
|
+
}
|
|
69
|
+
const reader = res.body.getReader();
|
|
70
|
+
const chunks = [];
|
|
71
|
+
let loaded = 0;
|
|
72
|
+
while (true) {
|
|
73
|
+
const { done, value } = await reader.read();
|
|
74
|
+
if (done) break;
|
|
75
|
+
if (value) {
|
|
76
|
+
chunks.push(value);
|
|
77
|
+
loaded += value.length;
|
|
78
|
+
args.onProgress?.(loaded, total);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const out = new Uint8Array(loaded);
|
|
82
|
+
let offset = 0;
|
|
83
|
+
for (const c of chunks) {
|
|
84
|
+
out.set(c, offset);
|
|
85
|
+
offset += c.length;
|
|
86
|
+
}
|
|
87
|
+
return { fileType, fileName, arrayBuffer: out.buffer, url: args.fileUrl };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/components/Toolbar.tsx
|
|
91
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
92
|
+
function Toolbar(props) {
|
|
93
|
+
const t = (k, fallback) => props.locale[k] ?? fallback;
|
|
94
|
+
return /* @__PURE__ */ jsxs("div", { className: "hv-toolbar", role: "toolbar", "aria-label": t("a11y.toolbar", "Document toolbar"), children: [
|
|
95
|
+
/* @__PURE__ */ jsxs("div", { className: "hv-toolbar__left", children: [
|
|
96
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "hv-btn", onClick: props.onToggleThumbnails, "aria-pressed": props.showThumbnails, children: t("toolbar.thumbs", "Thumbnails") }),
|
|
97
|
+
props.mode !== "create" && /* @__PURE__ */ jsx("button", { type: "button", className: "hv-btn", onClick: props.onToggleSignatures, "aria-pressed": props.showSignatures, children: t("toolbar.signatures", "Signatures") }),
|
|
98
|
+
/* @__PURE__ */ jsx("span", { className: "hv-sep" }),
|
|
99
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: props.layout === "single" ? "hv-btn hv-btn--active" : "hv-btn", onClick: () => props.onChangeLayout("single"), children: t("toolbar.layout.single", "Single") }),
|
|
100
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: props.layout === "side-by-side" ? "hv-btn hv-btn--active" : "hv-btn", onClick: () => props.onChangeLayout("side-by-side"), children: t("toolbar.layout.two", "Two") })
|
|
101
|
+
] }),
|
|
102
|
+
/* @__PURE__ */ jsxs("div", { className: "hv-toolbar__right", children: [
|
|
103
|
+
props.showHeaderFooterToggle && /* @__PURE__ */ jsxs("label", { className: "hv-toggle", children: [
|
|
104
|
+
/* @__PURE__ */ jsx("input", { type: "checkbox", checked: props.headerFooterEnabled, onChange: props.onToggleHeaderFooter }),
|
|
105
|
+
/* @__PURE__ */ jsx("span", { children: t("toolbar.letterhead", "Letterhead") })
|
|
106
|
+
] }),
|
|
107
|
+
props.allowSigning && /* @__PURE__ */ jsx("button", { type: "button", className: "hv-btn hv-btn--primary", onClick: props.onSign, disabled: props.signingDisabled, children: t("toolbar.sign", "Sign Document") }),
|
|
108
|
+
props.canExportPdf && /* @__PURE__ */ jsx("button", { type: "button", className: "hv-btn", onClick: props.onExportPdf, children: t("toolbar.exportPdf", "Export as PDF") }),
|
|
109
|
+
props.canSave && /* @__PURE__ */ jsx("button", { type: "button", className: "hv-btn hv-btn--primary", onClick: props.onSave, children: t("toolbar.save", "Save") })
|
|
110
|
+
] })
|
|
111
|
+
] });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/components/ThumbnailsSidebar.tsx
|
|
115
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
116
|
+
function ThumbnailsSidebar(props) {
|
|
117
|
+
const t = props.locale["thumbnails.title"] ?? "Thumbnails";
|
|
118
|
+
return /* @__PURE__ */ jsxs2("aside", { className: props.collapsed ? "hv-thumbs hv-thumbs--collapsed" : "hv-thumbs", "aria-label": t, children: [
|
|
119
|
+
/* @__PURE__ */ jsxs2("div", { className: "hv-thumbs__header", children: [
|
|
120
|
+
/* @__PURE__ */ jsx2(
|
|
121
|
+
"button",
|
|
122
|
+
{
|
|
123
|
+
type: "button",
|
|
124
|
+
className: "hv-icon",
|
|
125
|
+
onClick: props.onToggle,
|
|
126
|
+
"aria-label": props.collapsed ? props.locale["thumbnails.open"] ?? "Open thumbnails" : props.locale["thumbnails.close"] ?? "Close thumbnails",
|
|
127
|
+
children: props.collapsed ? "\u25B8" : "\u25BE"
|
|
128
|
+
}
|
|
129
|
+
),
|
|
130
|
+
!props.collapsed ? /* @__PURE__ */ jsx2("div", { className: "hv-thumbs__title", children: t }) : null
|
|
131
|
+
] }),
|
|
132
|
+
!props.collapsed ? /* @__PURE__ */ jsx2("div", { className: "hv-thumbs__list", role: "list", children: props.thumbnails.map((th, idx) => {
|
|
133
|
+
const p = idx + 1;
|
|
134
|
+
const active = p === props.currentPage;
|
|
135
|
+
return /* @__PURE__ */ jsxs2(
|
|
136
|
+
"button",
|
|
137
|
+
{
|
|
138
|
+
type: "button",
|
|
139
|
+
role: "listitem",
|
|
140
|
+
className: active ? "hv-thumb hv-thumb--active" : "hv-thumb",
|
|
141
|
+
onClick: () => props.onSelectPage(p),
|
|
142
|
+
"aria-current": active ? "page" : void 0,
|
|
143
|
+
children: [
|
|
144
|
+
/* @__PURE__ */ jsx2("div", { className: "hv-thumb__img", "aria-hidden": true, children: th.dataUrl ? /* @__PURE__ */ jsx2("img", { src: th.dataUrl, alt: "" }) : /* @__PURE__ */ jsx2("div", { className: "hv-thumb__placeholder" }) }),
|
|
145
|
+
/* @__PURE__ */ jsx2("div", { className: "hv-thumb__label", children: th.label })
|
|
146
|
+
]
|
|
147
|
+
},
|
|
148
|
+
th.id
|
|
149
|
+
);
|
|
150
|
+
}) }) : null
|
|
151
|
+
] });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/components/SignaturePanel.tsx
|
|
155
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
156
|
+
function SignaturePanel(props) {
|
|
157
|
+
const title = props.locale["signatures.title"] ?? "Signatures";
|
|
158
|
+
return /* @__PURE__ */ jsxs3("aside", { className: props.collapsed ? "hv-side hv-side--collapsed" : "hv-side", "aria-label": title, children: [
|
|
159
|
+
/* @__PURE__ */ jsxs3("div", { className: "hv-sidebar-header", children: [
|
|
160
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: "hv-icon", onClick: props.onToggle, "aria-label": props.locale["toolbar.signatures"] ?? "Signatures", children: "\u270D" }),
|
|
161
|
+
/* @__PURE__ */ jsx3("div", { className: "hv-sidebar-title", children: title })
|
|
162
|
+
] }),
|
|
163
|
+
/* @__PURE__ */ jsx3("div", { className: "hv-sidebar-body", children: props.signatures.map((s, idx) => /* @__PURE__ */ jsxs3("div", { className: "hv-signature-card", children: [
|
|
164
|
+
/* @__PURE__ */ jsx3("img", { src: s.signatureImageUrl, alt: `Signature by ${s.signedBy}`, className: "hv-signature-img" }),
|
|
165
|
+
/* @__PURE__ */ jsxs3("div", { className: "hv-signature-meta", children: [
|
|
166
|
+
/* @__PURE__ */ jsx3("div", { className: "hv-signature-name", children: s.signedBy }),
|
|
167
|
+
/* @__PURE__ */ jsx3("div", { className: "hv-signature-date", children: new Date(s.dateSigned).toLocaleString() }),
|
|
168
|
+
s.comment ? /* @__PURE__ */ jsx3("div", { className: "hv-signature-comment", children: s.comment }) : null
|
|
169
|
+
] })
|
|
170
|
+
] }, `${s.signedBy}-${s.dateSigned}-${idx}`)) })
|
|
171
|
+
] });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/renderers/PdfRenderer.tsx
|
|
175
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
176
|
+
import { GlobalWorkerOptions, getDocument } from "pdfjs-dist";
|
|
177
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
178
|
+
function PdfRenderer(props) {
|
|
179
|
+
const { url, arrayBuffer } = props;
|
|
180
|
+
const [doc, setDoc] = useState(null);
|
|
181
|
+
const [pageCount, setPageCount] = useState(0);
|
|
182
|
+
const [rendered, setRendered] = useState(/* @__PURE__ */ new Map());
|
|
183
|
+
const [thumbs, setThumbs] = useState([]);
|
|
184
|
+
const [size, setSize] = useState({ w: 840, h: 1188 });
|
|
185
|
+
const containerRef = useRef(null);
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
try {
|
|
188
|
+
GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).toString();
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}, []);
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
let cancel = false;
|
|
194
|
+
(async () => {
|
|
195
|
+
setDoc(null);
|
|
196
|
+
setRendered(/* @__PURE__ */ new Map());
|
|
197
|
+
setThumbs([]);
|
|
198
|
+
if (!url && !arrayBuffer) return;
|
|
199
|
+
const task = getDocument(url ? { url, rangeChunkSize: 512 * 1024 } : { data: arrayBuffer });
|
|
200
|
+
const pdf = await task.promise;
|
|
201
|
+
if (cancel) return;
|
|
202
|
+
setDoc(pdf);
|
|
203
|
+
setPageCount(pdf.numPages);
|
|
204
|
+
props.onPageCount(pdf.numPages);
|
|
205
|
+
setThumbs(Array.from({ length: pdf.numPages }));
|
|
206
|
+
const p1 = await pdf.getPage(1);
|
|
207
|
+
const base = p1.getViewport({ scale: 1 });
|
|
208
|
+
const w = Math.min(980, Math.max(640, base.width));
|
|
209
|
+
const s = w / base.width;
|
|
210
|
+
const vp = p1.getViewport({ scale: s });
|
|
211
|
+
setSize({ w: Math.round(vp.width), h: Math.round(vp.height) });
|
|
212
|
+
})().catch(() => {
|
|
213
|
+
});
|
|
214
|
+
return () => {
|
|
215
|
+
cancel = true;
|
|
216
|
+
};
|
|
217
|
+
}, [url, arrayBuffer]);
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
props.onThumbs(thumbs);
|
|
220
|
+
}, [thumbs]);
|
|
221
|
+
const pagesToShow = useMemo(() => {
|
|
222
|
+
if (props.layout === "side-by-side") {
|
|
223
|
+
const left = props.currentPage;
|
|
224
|
+
const right = Math.min(pageCount || left + 1, left + 1);
|
|
225
|
+
return [left, right];
|
|
226
|
+
}
|
|
227
|
+
return [props.currentPage];
|
|
228
|
+
}, [props.currentPage, props.layout, pageCount]);
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (!doc) return;
|
|
231
|
+
let cancel = false;
|
|
232
|
+
(async () => {
|
|
233
|
+
for (const p of pagesToShow) {
|
|
234
|
+
if (rendered.has(p)) continue;
|
|
235
|
+
const page = await doc.getPage(p);
|
|
236
|
+
if (cancel) return;
|
|
237
|
+
const base = page.getViewport({ scale: 1 });
|
|
238
|
+
const vp = page.getViewport({ scale: size.w / base.width });
|
|
239
|
+
const canvas = document.createElement("canvas");
|
|
240
|
+
canvas.width = Math.round(vp.width);
|
|
241
|
+
canvas.height = Math.round(vp.height);
|
|
242
|
+
const ctx = canvas.getContext("2d", { alpha: false });
|
|
243
|
+
if (!ctx) continue;
|
|
244
|
+
await page.render({ canvasContext: ctx, viewport: vp }).promise;
|
|
245
|
+
if (cancel) return;
|
|
246
|
+
setRendered((prev) => {
|
|
247
|
+
const next = new Map(prev);
|
|
248
|
+
next.set(p, canvas);
|
|
249
|
+
return next;
|
|
250
|
+
});
|
|
251
|
+
try {
|
|
252
|
+
const tw = 140;
|
|
253
|
+
const th = Math.round(tw * (canvas.height / canvas.width));
|
|
254
|
+
const t = document.createElement("canvas");
|
|
255
|
+
t.width = tw;
|
|
256
|
+
t.height = th;
|
|
257
|
+
const tctx = t.getContext("2d");
|
|
258
|
+
if (tctx) {
|
|
259
|
+
tctx.drawImage(canvas, 0, 0, tw, th);
|
|
260
|
+
const url2 = t.toDataURL("image/jpeg", 0.75);
|
|
261
|
+
setThumbs((prev) => {
|
|
262
|
+
const next = prev.slice();
|
|
263
|
+
next[p - 1] = url2;
|
|
264
|
+
return next;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
})();
|
|
271
|
+
return () => {
|
|
272
|
+
cancel = true;
|
|
273
|
+
};
|
|
274
|
+
}, [doc, pagesToShow, size.w, rendered]);
|
|
275
|
+
function onWheel(e) {
|
|
276
|
+
if (!pageCount) return;
|
|
277
|
+
if (Math.abs(e.deltaY) < 10) return;
|
|
278
|
+
const dir = e.deltaY > 0 ? 1 : -1;
|
|
279
|
+
const step = props.layout === "side-by-side" ? 2 : 1;
|
|
280
|
+
const next = Math.max(1, Math.min(pageCount, props.currentPage + dir * step));
|
|
281
|
+
props.onCurrentPageChange(next);
|
|
282
|
+
}
|
|
283
|
+
function clickPlace(e, page) {
|
|
284
|
+
const stamp = props.signatureStamp;
|
|
285
|
+
if (!stamp?.armed) return;
|
|
286
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
287
|
+
const x = (e.clientX - rect.left) / rect.width;
|
|
288
|
+
const y = (e.clientY - rect.top) / rect.height;
|
|
289
|
+
stamp.onPlaced({ page, x, y, w: 0.22, h: 0.08 });
|
|
290
|
+
}
|
|
291
|
+
return /* @__PURE__ */ jsxs4("div", { className: "hv-doc", ref: containerRef, onWheel, children: [
|
|
292
|
+
!doc ? /* @__PURE__ */ jsx4("div", { className: "hv-loading", children: "Loading PDF\u2026" }) : null,
|
|
293
|
+
doc ? /* @__PURE__ */ jsx4("div", { className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages", children: pagesToShow.map((p) => {
|
|
294
|
+
const c = rendered.get(p);
|
|
295
|
+
return /* @__PURE__ */ jsx4("div", { className: "hv-page", style: { width: size.w, height: size.h }, onClick: (e) => clickPlace(e, p), children: c ? /* @__PURE__ */ jsx4(
|
|
296
|
+
"canvas",
|
|
297
|
+
{
|
|
298
|
+
className: "hv-canvas",
|
|
299
|
+
width: c.width,
|
|
300
|
+
height: c.height,
|
|
301
|
+
ref: (node) => {
|
|
302
|
+
if (!node) return;
|
|
303
|
+
const ctx = node.getContext("2d");
|
|
304
|
+
if (ctx) ctx.drawImage(c, 0, 0);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
) : /* @__PURE__ */ jsx4("div", { className: "hv-loading", children: "Rendering\u2026" }) }, p);
|
|
308
|
+
}) }) : null
|
|
309
|
+
] });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/editors/RichTextEditor.tsx
|
|
313
|
+
import { forwardRef, useEffect as useEffect2, useImperativeHandle, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
|
|
314
|
+
import mammoth from "mammoth";
|
|
315
|
+
import htmlToDocx from "html-to-docx";
|
|
316
|
+
import MarkdownIt from "markdown-it";
|
|
317
|
+
import html2canvas from "html2canvas";
|
|
318
|
+
|
|
319
|
+
// src/utils/sanitize.ts
|
|
320
|
+
import DOMPurify from "dompurify";
|
|
321
|
+
function sanitizeHtml(html) {
|
|
322
|
+
return DOMPurify.sanitize(html, {
|
|
323
|
+
USE_PROFILES: { html: true },
|
|
324
|
+
ADD_ATTR: ["target", "rel"]
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/editors/RichTextEditor.tsx
|
|
329
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
330
|
+
var PAGE_H = 1122;
|
|
331
|
+
var RichTextEditor = forwardRef((props, ref) => {
|
|
332
|
+
const readOnly = props.mode === "view";
|
|
333
|
+
const md = useMemo2(() => new MarkdownIt({ html: false, linkify: true, breaks: true }), []);
|
|
334
|
+
const scrollerRef = useRef2(null);
|
|
335
|
+
const editorRef = useRef2(null);
|
|
336
|
+
const captureRef = useRef2(null);
|
|
337
|
+
const [html, setHtml] = useState2("<p><br/></p>");
|
|
338
|
+
useEffect2(() => {
|
|
339
|
+
let cancelled = false;
|
|
340
|
+
(async () => {
|
|
341
|
+
if (props.mode === "create") {
|
|
342
|
+
setHtml("<p><br/></p>");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (!props.arrayBuffer) return;
|
|
346
|
+
if (props.fileType === "docx") {
|
|
347
|
+
const res = await mammoth.convertToHtml({ arrayBuffer: props.arrayBuffer });
|
|
348
|
+
if (!cancelled) setHtml(sanitizeHtml(res.value || "<p><br/></p>"));
|
|
349
|
+
} else {
|
|
350
|
+
const text = new TextDecoder().decode(props.arrayBuffer);
|
|
351
|
+
if (props.fileType === "md") setHtml(sanitizeHtml(md.render(text)));
|
|
352
|
+
else setHtml(`<pre>${escapeHtml(text)}</pre>`);
|
|
353
|
+
}
|
|
354
|
+
})();
|
|
355
|
+
return () => {
|
|
356
|
+
cancelled = true;
|
|
357
|
+
};
|
|
358
|
+
}, [props.arrayBuffer, props.fileType, props.mode, md]);
|
|
359
|
+
useEffect2(() => {
|
|
360
|
+
const el = scrollerRef.current;
|
|
361
|
+
if (!el) return;
|
|
362
|
+
const recompute = () => props.onPageCount(Math.max(1, Math.ceil(el.scrollHeight / PAGE_H)));
|
|
363
|
+
recompute();
|
|
364
|
+
const ro = new ResizeObserver(recompute);
|
|
365
|
+
ro.observe(el);
|
|
366
|
+
return () => ro.disconnect();
|
|
367
|
+
}, [html, props.headerFooterEnabled]);
|
|
368
|
+
function exec(cmd) {
|
|
369
|
+
if (readOnly) return;
|
|
370
|
+
document.execCommand(cmd);
|
|
371
|
+
}
|
|
372
|
+
function onClick(e) {
|
|
373
|
+
if (!props.armedSignatureUrl) return;
|
|
374
|
+
const scroller = scrollerRef.current;
|
|
375
|
+
if (!scroller) return;
|
|
376
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
377
|
+
const absY = scroller.scrollTop + (e.clientY - rect.top);
|
|
378
|
+
const page = Math.max(1, Math.floor(absY / PAGE_H) + 1);
|
|
379
|
+
const pageTop = (page - 1) * PAGE_H;
|
|
380
|
+
const x = (e.clientX - rect.left) / rect.width;
|
|
381
|
+
const y = (absY - pageTop) / PAGE_H;
|
|
382
|
+
props.onPlaceSignature({ page, x, y, w: 0.25, h: 0.1 });
|
|
383
|
+
}
|
|
384
|
+
async function requestThumbnail(index) {
|
|
385
|
+
const scroller = scrollerRef.current;
|
|
386
|
+
const capture = captureRef.current;
|
|
387
|
+
if (!scroller || !capture) return void 0;
|
|
388
|
+
const old = scroller.scrollTop;
|
|
389
|
+
scroller.scrollTop = index * PAGE_H;
|
|
390
|
+
await new Promise((r) => requestAnimationFrame(() => r(null)));
|
|
391
|
+
try {
|
|
392
|
+
const canvas = await html2canvas(capture, { backgroundColor: null, scale: 0.25, useCORS: true });
|
|
393
|
+
return canvas.toDataURL("image/png");
|
|
394
|
+
} catch {
|
|
395
|
+
return void 0;
|
|
396
|
+
} finally {
|
|
397
|
+
scroller.scrollTop = old;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async function save(exportPdf) {
|
|
401
|
+
const inner = editorRef.current?.innerHTML ?? html;
|
|
402
|
+
const stitched = `<!doctype html><html><head><meta charset="utf-8" /></head><body>${inner}</body></html>`;
|
|
403
|
+
if (exportPdf) {
|
|
404
|
+
const b642 = btoa(unescape(encodeURIComponent(stitched)));
|
|
405
|
+
props.onSave(b642, { fileName: replaceExt(props.fileName, "html"), fileType: "txt", exportedAsPdf: true, annotations: { signaturePlacements: props.signaturePlacements } });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (props.fileType === "docx") {
|
|
409
|
+
const blob = await htmlToDocx(stitched);
|
|
410
|
+
const ab = await blob.arrayBuffer();
|
|
411
|
+
props.onSave(arrayBufferToBase64(ab), { fileName: replaceExt(props.fileName, "docx"), fileType: "docx", annotations: { signaturePlacements: props.signaturePlacements } });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const text = editorRef.current?.innerText ?? "";
|
|
415
|
+
const b64 = btoa(unescape(encodeURIComponent(text)));
|
|
416
|
+
props.onSave(b64, { fileName: replaceExt(props.fileName, props.fileType), fileType: props.fileType, annotations: { signaturePlacements: props.signaturePlacements } });
|
|
417
|
+
}
|
|
418
|
+
useImperativeHandle(ref, () => ({ save, requestThumbnail }));
|
|
419
|
+
return /* @__PURE__ */ jsxs5("div", { className: "hv-doc", children: [
|
|
420
|
+
/* @__PURE__ */ jsxs5("div", { className: "hv-ribbon", role: "toolbar", children: [
|
|
421
|
+
/* @__PURE__ */ jsx5("button", { className: "hv-btn", onClick: () => exec("bold"), disabled: readOnly, children: "B" }),
|
|
422
|
+
/* @__PURE__ */ jsx5("button", { className: "hv-btn", onClick: () => exec("italic"), disabled: readOnly, children: "I" }),
|
|
423
|
+
/* @__PURE__ */ jsx5("button", { className: "hv-btn", onClick: () => exec("underline"), disabled: readOnly, children: "U" }),
|
|
424
|
+
props.armedSignatureUrl ? /* @__PURE__ */ jsx5("div", { className: "hv-hint", children: "Click to place signature" }) : null
|
|
425
|
+
] }),
|
|
426
|
+
/* @__PURE__ */ jsx5("div", { className: "hv-scroll", ref: scrollerRef, onClick, children: /* @__PURE__ */ jsxs5("div", { className: "hv-pageStage", ref: captureRef, children: [
|
|
427
|
+
props.headerFooterEnabled && props.headerComponent ? /* @__PURE__ */ jsx5("div", { className: "hv-letterhead", children: props.headerComponent }) : null,
|
|
428
|
+
/* @__PURE__ */ jsx5(
|
|
429
|
+
"div",
|
|
430
|
+
{
|
|
431
|
+
ref: editorRef,
|
|
432
|
+
className: readOnly ? "hv-editor hv-editor--ro" : "hv-editor",
|
|
433
|
+
contentEditable: !readOnly,
|
|
434
|
+
suppressContentEditableWarning: true,
|
|
435
|
+
onInput: () => setHtml(editorRef.current?.innerHTML ?? ""),
|
|
436
|
+
dangerouslySetInnerHTML: { __html: html }
|
|
437
|
+
}
|
|
438
|
+
),
|
|
439
|
+
props.headerFooterEnabled && props.footerComponent ? /* @__PURE__ */ jsx5("div", { className: "hv-letterhead hv-letterhead--footer", children: props.footerComponent }) : null,
|
|
440
|
+
props.mode === "create" && props.signatures.length ? /* @__PURE__ */ jsx5("div", { className: "hv-signatures-inline", children: props.signatures.map((s, i) => /* @__PURE__ */ jsxs5("div", { className: "hv-sign-inline", children: [
|
|
441
|
+
/* @__PURE__ */ jsx5("img", { src: s.signatureImageUrl, alt: "", className: "hv-sign-img" }),
|
|
442
|
+
/* @__PURE__ */ jsxs5("div", { children: [
|
|
443
|
+
/* @__PURE__ */ jsx5("div", { className: "hv-sign-name", children: s.signedBy }),
|
|
444
|
+
/* @__PURE__ */ jsx5("div", { className: "hv-sign-date", children: new Date(s.dateSigned).toLocaleString() })
|
|
445
|
+
] })
|
|
446
|
+
] }, i)) }) : null
|
|
447
|
+
] }) })
|
|
448
|
+
] });
|
|
449
|
+
});
|
|
450
|
+
function replaceExt(name, ext) {
|
|
451
|
+
const base = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : name;
|
|
452
|
+
return `${base}.${ext}`;
|
|
453
|
+
}
|
|
454
|
+
function escapeHtml(s) {
|
|
455
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/editors/SpreadsheetEditor.tsx
|
|
459
|
+
import { forwardRef as forwardRef2, useEffect as useEffect3, useImperativeHandle as useImperativeHandle2, useMemo as useMemo3, useState as useState3 } from "react";
|
|
460
|
+
import * as XLSX from "xlsx";
|
|
461
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
462
|
+
var SpreadsheetEditor = forwardRef2(function SpreadsheetEditor2(props, ref) {
|
|
463
|
+
const readonly = props.mode === "view";
|
|
464
|
+
const [grid, setGrid] = useState3(() => Array.from({ length: 30 }, () => Array.from({ length: 12 }, () => "")));
|
|
465
|
+
useEffect3(() => {
|
|
466
|
+
if (!props.arrayBuffer) return;
|
|
467
|
+
try {
|
|
468
|
+
const wb = XLSX.read(props.arrayBuffer, { type: "array" });
|
|
469
|
+
const name = wb.SheetNames[0];
|
|
470
|
+
const ws = wb.Sheets[name];
|
|
471
|
+
const aoa = XLSX.utils.sheet_to_json(ws, { header: 1, raw: true });
|
|
472
|
+
const rows = Math.max(30, aoa.length);
|
|
473
|
+
const cols2 = Math.max(12, Math.max(...aoa.map((r) => r?.length ?? 0), 0));
|
|
474
|
+
const next = Array.from({ length: rows }, (_, r) => Array.from({ length: cols2 }, (_2, c) => {
|
|
475
|
+
const v = aoa[r]?.[c];
|
|
476
|
+
return v == null ? "" : String(v);
|
|
477
|
+
}));
|
|
478
|
+
setGrid(next);
|
|
479
|
+
} catch {
|
|
480
|
+
}
|
|
481
|
+
}, [props.arrayBuffer]);
|
|
482
|
+
async function save(exportPdf) {
|
|
483
|
+
const ws = XLSX.utils.aoa_to_sheet(grid);
|
|
484
|
+
const wb = XLSX.utils.book_new();
|
|
485
|
+
XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
|
|
486
|
+
const out = XLSX.write(wb, { type: "array", bookType: "xlsx" });
|
|
487
|
+
const b64 = arrayBufferToBase64(out);
|
|
488
|
+
props.onSave(b64, { fileName: ensureExt(props.fileName, "xlsx"), fileType: "xlsx", exportedAsPdf: !!exportPdf });
|
|
489
|
+
}
|
|
490
|
+
useImperativeHandle2(ref, () => ({
|
|
491
|
+
save,
|
|
492
|
+
requestThumbnails: async () => void 0
|
|
493
|
+
}));
|
|
494
|
+
const cols = useMemo3(() => Array.from({ length: grid[0]?.length ?? 0 }, (_, i) => String.fromCharCode(65 + i % 26)), [grid]);
|
|
495
|
+
return /* @__PURE__ */ jsxs6("div", { className: "hv-sheet", children: [
|
|
496
|
+
/* @__PURE__ */ jsxs6("div", { className: "hv-sheetbar", children: [
|
|
497
|
+
/* @__PURE__ */ jsx6("div", { className: "hv-sheetbar-title", children: props.fileName }),
|
|
498
|
+
!readonly ? /* @__PURE__ */ jsx6("button", { className: "hv-btn", type: "button", onClick: () => void save(false), children: props.locale["toolbar.save"] ?? "Save" }) : null
|
|
499
|
+
] }),
|
|
500
|
+
/* @__PURE__ */ jsxs6("div", { className: "hv-sheetgrid", role: "table", "aria-label": "Spreadsheet", children: [
|
|
501
|
+
/* @__PURE__ */ jsxs6("div", { className: "hv-sheetrow hv-sheetrow--header", role: "row", children: [
|
|
502
|
+
/* @__PURE__ */ jsx6("div", { className: "hv-sheetcell hv-sheetcell--corner", role: "columnheader" }),
|
|
503
|
+
cols.map((c, i) => /* @__PURE__ */ jsx6("div", { className: "hv-sheetcell hv-sheetcell--header", role: "columnheader", children: c }, i))
|
|
504
|
+
] }),
|
|
505
|
+
grid.map((row, r) => /* @__PURE__ */ jsxs6("div", { className: "hv-sheetrow", role: "row", children: [
|
|
506
|
+
/* @__PURE__ */ jsx6("div", { className: "hv-sheetcell hv-sheetcell--header", role: "rowheader", children: r + 1 }),
|
|
507
|
+
row.map((val, c) => /* @__PURE__ */ jsx6(
|
|
508
|
+
"div",
|
|
509
|
+
{
|
|
510
|
+
className: "hv-sheetcell",
|
|
511
|
+
role: "cell",
|
|
512
|
+
contentEditable: !readonly,
|
|
513
|
+
suppressContentEditableWarning: true,
|
|
514
|
+
onInput: (e) => {
|
|
515
|
+
const text = e.currentTarget.textContent ?? "";
|
|
516
|
+
setGrid((prev) => {
|
|
517
|
+
const next = prev.map((rr) => rr.slice());
|
|
518
|
+
next[r][c] = text;
|
|
519
|
+
return next;
|
|
520
|
+
});
|
|
521
|
+
},
|
|
522
|
+
children: val
|
|
523
|
+
},
|
|
524
|
+
c
|
|
525
|
+
))
|
|
526
|
+
] }, r))
|
|
527
|
+
] })
|
|
528
|
+
] });
|
|
529
|
+
});
|
|
530
|
+
function ensureExt(name, ext) {
|
|
531
|
+
const base = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : name;
|
|
532
|
+
return `${base}.${ext}`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/renderers/ImageRenderer.tsx
|
|
536
|
+
import { useEffect as useEffect4, useMemo as useMemo4, useState as useState4 } from "react";
|
|
537
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
538
|
+
function ImageRenderer({ arrayBuffer, fileType, fileName }) {
|
|
539
|
+
const [zoom, setZoom] = useState4(1);
|
|
540
|
+
const url = useMemo4(() => {
|
|
541
|
+
if (!arrayBuffer) return void 0;
|
|
542
|
+
const mime = fileType === "svg" ? "image/svg+xml" : fileType === "png" ? "image/png" : "image/jpeg";
|
|
543
|
+
return URL.createObjectURL(new Blob([arrayBuffer], { type: mime }));
|
|
544
|
+
}, [arrayBuffer, fileType]);
|
|
545
|
+
useEffect4(() => {
|
|
546
|
+
return () => {
|
|
547
|
+
if (url) URL.revokeObjectURL(url);
|
|
548
|
+
};
|
|
549
|
+
}, [url]);
|
|
550
|
+
return /* @__PURE__ */ jsxs7("div", { className: "hv-doc", children: [
|
|
551
|
+
/* @__PURE__ */ jsxs7("div", { className: "hv-mini-toolbar", children: [
|
|
552
|
+
/* @__PURE__ */ jsx7("div", { className: "hv-title", children: fileName }),
|
|
553
|
+
/* @__PURE__ */ jsx7("div", { className: "hv-spacer" }),
|
|
554
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: "hv-btn", onClick: () => setZoom((z) => Math.max(0.25, z - 0.25)), children: "-" }),
|
|
555
|
+
/* @__PURE__ */ jsxs7("div", { className: "hv-zoom", children: [
|
|
556
|
+
Math.round(zoom * 100),
|
|
557
|
+
"%"
|
|
558
|
+
] }),
|
|
559
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: "hv-btn", onClick: () => setZoom((z) => Math.min(4, z + 0.25)), children: "+" })
|
|
560
|
+
] }),
|
|
561
|
+
/* @__PURE__ */ jsx7("div", { className: "hv-center", children: url ? /* @__PURE__ */ jsx7("img", { src: url, alt: fileName, style: { transform: `scale(${zoom})` }, className: "hv-image" }) : /* @__PURE__ */ jsx7("div", { className: "hv-loading", children: "Loading\u2026" }) })
|
|
562
|
+
] });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/renderers/PptxRenderer.tsx
|
|
566
|
+
import { useEffect as useEffect5, useMemo as useMemo5, useState as useState5 } from "react";
|
|
567
|
+
import JSZip from "jszip";
|
|
568
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
569
|
+
function decodeXml(s) {
|
|
570
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
571
|
+
}
|
|
572
|
+
function extractText(xml) {
|
|
573
|
+
return [...xml.matchAll(/<a:t>(.*?)<\/a:t>/g)].map((m) => decodeXml(m[1] || "")).join(" ").trim();
|
|
574
|
+
}
|
|
575
|
+
function PptxRenderer(props) {
|
|
576
|
+
const [slides, setSlides] = useState5([]);
|
|
577
|
+
const [thumbs, setThumbs] = useState5([]);
|
|
578
|
+
useEffect5(() => {
|
|
579
|
+
let cancelled = false;
|
|
580
|
+
(async () => {
|
|
581
|
+
setSlides([]);
|
|
582
|
+
setThumbs([]);
|
|
583
|
+
if (!props.arrayBuffer) {
|
|
584
|
+
props.onSlideCount(1);
|
|
585
|
+
setSlides([{ index: 1, text: "No content" }]);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const zip = await JSZip.loadAsync(props.arrayBuffer);
|
|
589
|
+
const files = Object.keys(zip.files).filter((p) => /^ppt\/slides\/slide\d+\.xml$/.test(p)).sort((a, b) => {
|
|
590
|
+
const na = Number(a.match(/slide(\d+)\.xml/)?.[1] || 0);
|
|
591
|
+
const nb = Number(b.match(/slide(\d+)\.xml/)?.[1] || 0);
|
|
592
|
+
return na - nb;
|
|
593
|
+
});
|
|
594
|
+
const out = [];
|
|
595
|
+
for (let i = 0; i < files.length; i++) {
|
|
596
|
+
const xml = await zip.file(files[i]).async("string");
|
|
597
|
+
out.push({ index: i + 1, text: extractText(xml) });
|
|
598
|
+
}
|
|
599
|
+
if (cancelled) return;
|
|
600
|
+
const count = Math.max(1, out.length);
|
|
601
|
+
props.onSlideCount(count);
|
|
602
|
+
setSlides(out.length ? out : [{ index: 1, text: "(empty)" }]);
|
|
603
|
+
setThumbs(Array.from({ length: count }, (_, i) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgThumb(i + 1))}`));
|
|
604
|
+
})().catch(() => {
|
|
605
|
+
props.onSlideCount(1);
|
|
606
|
+
setSlides([{ index: 1, text: "Unable to render this .pptx in-browser." }]);
|
|
607
|
+
setThumbs([void 0]);
|
|
608
|
+
});
|
|
609
|
+
return () => {
|
|
610
|
+
cancelled = true;
|
|
611
|
+
};
|
|
612
|
+
}, [props.arrayBuffer]);
|
|
613
|
+
useEffect5(() => {
|
|
614
|
+
props.onThumbs(thumbs);
|
|
615
|
+
}, [thumbs]);
|
|
616
|
+
const pagesToShow = useMemo5(() => {
|
|
617
|
+
if (props.layout === "side-by-side") return [props.currentPage, Math.min(slides.length || props.currentPage + 1, props.currentPage + 1)];
|
|
618
|
+
return [props.currentPage];
|
|
619
|
+
}, [props.currentPage, props.layout, slides.length]);
|
|
620
|
+
return /* @__PURE__ */ jsx8("div", { className: "hv-doc", children: /* @__PURE__ */ jsx8("div", { className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages", children: pagesToShow.map((p) => {
|
|
621
|
+
const s = slides[p - 1];
|
|
622
|
+
return /* @__PURE__ */ jsxs8("div", { className: "hv-slide", tabIndex: 0, onFocus: () => props.onCurrentPageChange(p), children: [
|
|
623
|
+
/* @__PURE__ */ jsxs8("div", { className: "hv-slide-title", children: [
|
|
624
|
+
"Slide ",
|
|
625
|
+
p
|
|
626
|
+
] }),
|
|
627
|
+
/* @__PURE__ */ jsx8("div", { className: "hv-slide-text", children: s?.text || "" })
|
|
628
|
+
] }, p);
|
|
629
|
+
}) }) });
|
|
630
|
+
}
|
|
631
|
+
function svgThumb(n) {
|
|
632
|
+
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>`;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/components/DocumentViewer.tsx
|
|
636
|
+
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
637
|
+
function DocumentViewer(props) {
|
|
638
|
+
const mode = props.mode ?? "view";
|
|
639
|
+
const theme = props.theme ?? "light";
|
|
640
|
+
const locale = useMemo6(() => ({ ...defaultLocale, ...props.locale ?? {} }), [props.locale]);
|
|
641
|
+
const [layout, setLayout] = useState6(props.defaultLayout ?? "single");
|
|
642
|
+
const [showThumbnails, setShowThumbnails] = useState6(true);
|
|
643
|
+
const [showSignatures, setShowSignatures] = useState6(true);
|
|
644
|
+
const [headerFooterEnabled, setHeaderFooterEnabled] = useState6(true);
|
|
645
|
+
const allowSigning = props.allowSigning ?? false;
|
|
646
|
+
const [signingBusy, setSigningBusy] = useState6(false);
|
|
647
|
+
const [resolved, setResolved] = useState6(null);
|
|
648
|
+
const [error, setError] = useState6("");
|
|
649
|
+
const [pageCount, setPageCount] = useState6(1);
|
|
650
|
+
const [currentPage, setCurrentPage] = useState6(1);
|
|
651
|
+
const [thumbs, setThumbs] = useState6([]);
|
|
652
|
+
const [localSignatures, setLocalSignatures] = useState6(props.signatures ?? []);
|
|
653
|
+
useEffect6(() => setLocalSignatures(props.signatures ?? []), [props.signatures]);
|
|
654
|
+
const [sigPlacements, setSigPlacements] = useState6([]);
|
|
655
|
+
const [armedSignatureUrl, setArmedSignatureUrl] = useState6(null);
|
|
656
|
+
const editorRef = useRef3(null);
|
|
657
|
+
useEffect6(() => {
|
|
658
|
+
let cancelled = false;
|
|
659
|
+
(async () => {
|
|
660
|
+
setError("");
|
|
661
|
+
setResolved(null);
|
|
662
|
+
setThumbs([]);
|
|
663
|
+
setPageCount(1);
|
|
664
|
+
setCurrentPage(1);
|
|
665
|
+
setSigPlacements([]);
|
|
666
|
+
setArmedSignatureUrl(null);
|
|
667
|
+
if (mode === "create") {
|
|
668
|
+
const ft = props.fileType ?? "docx";
|
|
669
|
+
setResolved({ fileType: ft, fileName: props.fileName ?? `Untitled.${ft}` });
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
const res = await resolveSource({
|
|
674
|
+
fileUrl: props.fileUrl,
|
|
675
|
+
base64: props.base64,
|
|
676
|
+
blob: props.blob,
|
|
677
|
+
fileName: props.fileName,
|
|
678
|
+
fileType: props.fileType
|
|
679
|
+
});
|
|
680
|
+
if (cancelled) return;
|
|
681
|
+
setResolved({ fileType: res.fileType, fileName: res.fileName, url: res.url, arrayBuffer: res.arrayBuffer });
|
|
682
|
+
} catch (e) {
|
|
683
|
+
if (cancelled) return;
|
|
684
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
685
|
+
}
|
|
686
|
+
})();
|
|
687
|
+
return () => {
|
|
688
|
+
cancelled = true;
|
|
689
|
+
};
|
|
690
|
+
}, [mode, props.fileUrl, props.base64, props.blob, props.fileName, props.fileType]);
|
|
691
|
+
const thumbnails = useMemo6(() => {
|
|
692
|
+
const n = Math.max(1, pageCount);
|
|
693
|
+
return Array.from({ length: n }, (_, i) => ({
|
|
694
|
+
id: `p-${i + 1}`,
|
|
695
|
+
label: `${locale["thumbnails.page"] ?? "Page"} ${i + 1}`,
|
|
696
|
+
dataUrl: thumbs[i]
|
|
697
|
+
}));
|
|
698
|
+
}, [pageCount, thumbs, locale]);
|
|
699
|
+
async function handleSignRequest() {
|
|
700
|
+
if (!allowSigning || signingBusy || !props.onSignRequest) return;
|
|
701
|
+
setSigningBusy(true);
|
|
702
|
+
try {
|
|
703
|
+
const sig = await props.onSignRequest();
|
|
704
|
+
setLocalSignatures((prev) => [...prev, sig]);
|
|
705
|
+
setArmedSignatureUrl(sig.signatureImageUrl);
|
|
706
|
+
} finally {
|
|
707
|
+
setSigningBusy(false);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function placeSignature(p) {
|
|
711
|
+
if (!armedSignatureUrl) return;
|
|
712
|
+
setSigPlacements((prev) => [...prev, { ...p, signatureImageUrl: armedSignatureUrl }]);
|
|
713
|
+
setArmedSignatureUrl(null);
|
|
714
|
+
}
|
|
715
|
+
async function handleSave(exportPdf) {
|
|
716
|
+
if (editorRef.current) {
|
|
717
|
+
await editorRef.current.save(!!exportPdf);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (!resolved?.arrayBuffer) return;
|
|
721
|
+
const b64 = arrayBufferToBase642(resolved.arrayBuffer);
|
|
722
|
+
props.onSave?.(b64, { fileName: resolved.fileName, fileType: resolved.fileType, annotations: { sigPlacements } });
|
|
723
|
+
}
|
|
724
|
+
const canSave = mode === "edit" || mode === "create";
|
|
725
|
+
const canExportPdf = (mode === "edit" || mode === "create") && (resolved?.fileType === "docx" || resolved?.fileType === "md" || resolved?.fileType === "txt" || resolved?.fileType === "xlsx");
|
|
726
|
+
return /* @__PURE__ */ jsxs9("div", { className: `hv-root`, "data-hv-theme": theme, children: [
|
|
727
|
+
/* @__PURE__ */ jsx9(
|
|
728
|
+
Toolbar,
|
|
729
|
+
{
|
|
730
|
+
locale,
|
|
731
|
+
mode,
|
|
732
|
+
fileType: resolved?.fileType,
|
|
733
|
+
layout,
|
|
734
|
+
onChangeLayout: setLayout,
|
|
735
|
+
showThumbnails,
|
|
736
|
+
onToggleThumbnails: () => setShowThumbnails((v) => !v),
|
|
737
|
+
showSignatures,
|
|
738
|
+
onToggleSignatures: () => setShowSignatures((v) => !v),
|
|
739
|
+
onSign: () => void handleSignRequest(),
|
|
740
|
+
allowSigning,
|
|
741
|
+
signingDisabled: signingBusy || !props.onSignRequest,
|
|
742
|
+
canSave,
|
|
743
|
+
onSave: () => void handleSave(false),
|
|
744
|
+
canExportPdf,
|
|
745
|
+
onExportPdf: () => void handleSave(true),
|
|
746
|
+
headerFooterEnabled,
|
|
747
|
+
showHeaderFooterToggle: (props.enableHeaderFooterToggle ?? true) && mode === "create",
|
|
748
|
+
onToggleHeaderFooter: () => setHeaderFooterEnabled((v) => !v)
|
|
749
|
+
}
|
|
750
|
+
),
|
|
751
|
+
error ? /* @__PURE__ */ jsxs9("div", { className: "hv-error", role: "alert", children: [
|
|
752
|
+
/* @__PURE__ */ jsx9("div", { className: "hv-error-title", children: locale["error.title"] ?? "Error" }),
|
|
753
|
+
/* @__PURE__ */ jsx9("div", { className: "hv-error-body", children: error })
|
|
754
|
+
] }) : null,
|
|
755
|
+
!resolved && !error ? /* @__PURE__ */ jsx9("div", { className: "hv-loading", "aria-busy": "true", children: locale["loading"] ?? "Loading\u2026" }) : null,
|
|
756
|
+
resolved ? /* @__PURE__ */ jsxs9("div", { className: "hv-shell", children: [
|
|
757
|
+
mode !== "create" ? /* @__PURE__ */ jsx9(
|
|
758
|
+
ThumbnailsSidebar,
|
|
759
|
+
{
|
|
760
|
+
locale,
|
|
761
|
+
thumbnails,
|
|
762
|
+
currentPage,
|
|
763
|
+
collapsed: !showThumbnails,
|
|
764
|
+
onToggle: () => setShowThumbnails((v) => !v),
|
|
765
|
+
onSelectPage: setCurrentPage
|
|
766
|
+
}
|
|
767
|
+
) : null,
|
|
768
|
+
/* @__PURE__ */ jsxs9("main", { className: "hv-main", children: [
|
|
769
|
+
resolved.fileType === "pdf" ? /* @__PURE__ */ jsx9(
|
|
770
|
+
PdfRenderer,
|
|
771
|
+
{
|
|
772
|
+
url: resolved.url,
|
|
773
|
+
arrayBuffer: resolved.arrayBuffer,
|
|
774
|
+
layout,
|
|
775
|
+
currentPage,
|
|
776
|
+
onCurrentPageChange: setCurrentPage,
|
|
777
|
+
onPageCount: (n) => {
|
|
778
|
+
setPageCount(n);
|
|
779
|
+
setThumbs((prev) => prev.length === n ? prev : Array.from({ length: n }, (_, i) => prev[i]));
|
|
780
|
+
},
|
|
781
|
+
onThumbs: (t) => setThumbs(t),
|
|
782
|
+
signatureStamp: armedSignatureUrl ? { imageUrl: armedSignatureUrl, armed: true, onPlaced: placeSignature } : void 0
|
|
783
|
+
}
|
|
784
|
+
) : null,
|
|
785
|
+
resolved.fileType === "docx" || resolved.fileType === "md" || resolved.fileType === "txt" ? /* @__PURE__ */ jsx9(
|
|
786
|
+
RichTextEditor,
|
|
787
|
+
{
|
|
788
|
+
ref: editorRef,
|
|
789
|
+
mode,
|
|
790
|
+
fileType: resolved.fileType,
|
|
791
|
+
fileName: resolved.fileName,
|
|
792
|
+
arrayBuffer: resolved.arrayBuffer,
|
|
793
|
+
headerComponent: props.headerComponent,
|
|
794
|
+
footerComponent: props.footerComponent,
|
|
795
|
+
headerFooterEnabled,
|
|
796
|
+
locale,
|
|
797
|
+
signatures: localSignatures,
|
|
798
|
+
signaturePlacements: sigPlacements,
|
|
799
|
+
onPageCount: (n) => {
|
|
800
|
+
setPageCount(n);
|
|
801
|
+
setThumbs((prev) => prev.length === n ? prev : Array.from({ length: n }, (_, i) => prev[i]));
|
|
802
|
+
},
|
|
803
|
+
onSave: (b64, meta) => props.onSave?.(b64, meta),
|
|
804
|
+
armedSignatureUrl,
|
|
805
|
+
onPlaceSignature: placeSignature
|
|
806
|
+
}
|
|
807
|
+
) : null,
|
|
808
|
+
resolved.fileType === "xlsx" ? /* @__PURE__ */ jsx9(
|
|
809
|
+
SpreadsheetEditor,
|
|
810
|
+
{
|
|
811
|
+
ref: editorRef,
|
|
812
|
+
mode,
|
|
813
|
+
fileName: resolved.fileName,
|
|
814
|
+
arrayBuffer: resolved.arrayBuffer,
|
|
815
|
+
locale,
|
|
816
|
+
onSave: (b64, meta) => props.onSave?.(b64, meta)
|
|
817
|
+
}
|
|
818
|
+
) : null,
|
|
819
|
+
resolved.fileType === "pptx" ? /* @__PURE__ */ jsx9(
|
|
820
|
+
PptxRenderer,
|
|
821
|
+
{
|
|
822
|
+
arrayBuffer: resolved.arrayBuffer,
|
|
823
|
+
layout,
|
|
824
|
+
currentPage,
|
|
825
|
+
onCurrentPageChange: setCurrentPage,
|
|
826
|
+
onSlideCount: (n) => {
|
|
827
|
+
setPageCount(n);
|
|
828
|
+
setThumbs((prev) => prev.length === n ? prev : Array.from({ length: n }, (_, i) => prev[i]));
|
|
829
|
+
},
|
|
830
|
+
onThumbs: (t) => setThumbs(t)
|
|
831
|
+
}
|
|
832
|
+
) : null,
|
|
833
|
+
resolved.fileType === "png" || resolved.fileType === "jpg" || resolved.fileType === "svg" ? /* @__PURE__ */ jsx9(ImageRenderer, { arrayBuffer: resolved.arrayBuffer, fileType: resolved.fileType, fileName: resolved.fileName }) : null
|
|
834
|
+
] }),
|
|
835
|
+
mode !== "create" && localSignatures.length ? /* @__PURE__ */ jsx9(
|
|
836
|
+
SignaturePanel,
|
|
837
|
+
{
|
|
838
|
+
locale,
|
|
839
|
+
signatures: localSignatures,
|
|
840
|
+
collapsed: !showSignatures,
|
|
841
|
+
onToggle: () => setShowSignatures((v) => !v)
|
|
842
|
+
}
|
|
843
|
+
) : null
|
|
844
|
+
] }) : null
|
|
845
|
+
] });
|
|
846
|
+
}
|
|
847
|
+
function arrayBufferToBase642(ab) {
|
|
848
|
+
const bytes = new Uint8Array(ab);
|
|
849
|
+
let binary = "";
|
|
850
|
+
const chunk = 32768;
|
|
851
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
852
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
853
|
+
}
|
|
854
|
+
return btoa(binary);
|
|
855
|
+
}
|
|
856
|
+
export {
|
|
857
|
+
DocumentViewer
|
|
858
|
+
};
|
|
859
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerohive/hive-viewer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"dompurify": "^3.1.7",
|
|
37
|
-
"html2canvas": "^1.4.1",
|
|
38
37
|
"html-to-docx": "^1.8.0",
|
|
38
|
+
"html2canvas": "^1.4.1",
|
|
39
39
|
"jszip": "^3.10.1",
|
|
40
40
|
"mammoth": "^1.8.0",
|
|
41
41
|
"markdown-it": "^14.1.0",
|
|
@@ -45,13 +45,13 @@
|
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/react": "^18.3.11",
|
|
47
47
|
"@types/react-dom": "^18.3.1",
|
|
48
|
-
"typescript": "^5.6.3",
|
|
49
|
-
"tsup": "^8.2.4",
|
|
50
48
|
"react": "^18.3.1",
|
|
51
|
-
"react-dom": "^18.3.1"
|
|
49
|
+
"react-dom": "^18.3.1",
|
|
50
|
+
"tsup": "^8.5.1",
|
|
51
|
+
"typescript": "^5.6.3"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
|
-
"build": "tsup
|
|
54
|
+
"build": "tsup && cp dist/index.js dist/index.mjs && cp src/styles/hiveviewer.css dist/styles.css",
|
|
55
55
|
"dev": "tsup src/index.tsx --format esm --watch"
|
|
56
56
|
}
|
|
57
|
-
}
|
|
57
|
+
}
|