@zerohive/hive-viewer 0.2.2 → 0.2.4
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.cjs +688 -504
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +672 -488
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +672 -488
- package/dist/styles.css +383 -22
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,386 +1,18 @@
|
|
|
1
1
|
// src/components/DocumentViewer.tsx
|
|
2
2
|
import { useEffect as useEffect6, useMemo as useMemo6, useRef as useRef3, useState as useState6 } from "react";
|
|
3
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 = [
|
|
30
|
-
"pdf",
|
|
31
|
-
"md",
|
|
32
|
-
"docx",
|
|
33
|
-
"xlsx",
|
|
34
|
-
"pptx",
|
|
35
|
-
"txt",
|
|
36
|
-
"png",
|
|
37
|
-
"jpg",
|
|
38
|
-
"svg"
|
|
39
|
-
];
|
|
40
|
-
return allowed.includes(ext) ? ext : "txt";
|
|
41
|
-
}
|
|
42
|
-
function arrayBufferToBase64(buf) {
|
|
43
|
-
const bytes = new Uint8Array(buf);
|
|
44
|
-
let binary = "";
|
|
45
|
-
const chunk = 32768;
|
|
46
|
-
for (let i = 0; i < bytes.length; i += chunk) {
|
|
47
|
-
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
48
|
-
}
|
|
49
|
-
return btoa(binary);
|
|
50
|
-
}
|
|
51
|
-
async function base64ToArrayBuffer(b64) {
|
|
52
|
-
const bin = atob(b64);
|
|
53
|
-
const len = bin.length;
|
|
54
|
-
const bytes = new Uint8Array(len);
|
|
55
|
-
for (let i = 0; i < len; i++) bytes[i] = bin.charCodeAt(i);
|
|
56
|
-
return bytes.buffer;
|
|
57
|
-
}
|
|
58
|
-
async function resolveSource(args) {
|
|
59
|
-
const fileType = guessFileType(args.fileName, args.fileType);
|
|
60
|
-
const fileName = args.fileName ?? `document.${fileType}`;
|
|
61
|
-
if (args.blob) {
|
|
62
|
-
const ab = await args.blob.arrayBuffer();
|
|
63
|
-
const url = URL.createObjectURL(args.blob);
|
|
64
|
-
return { fileType, fileName, arrayBuffer: ab, url };
|
|
65
|
-
}
|
|
66
|
-
if (args.base64) {
|
|
67
|
-
const ab = await base64ToArrayBuffer(args.base64);
|
|
68
|
-
return { fileType, fileName, arrayBuffer: ab };
|
|
69
|
-
}
|
|
70
|
-
if (!args.fileUrl)
|
|
71
|
-
throw new Error("No file source provided. Use fileUrl, blob, or base64.");
|
|
72
|
-
const res = await fetch(args.fileUrl);
|
|
73
|
-
if (!res.ok) throw new Error(`Failed to fetch file (${res.status})`);
|
|
74
|
-
const total = Number(res.headers.get("content-length") || "") || void 0;
|
|
75
|
-
if (!res.body) {
|
|
76
|
-
const ab = await res.arrayBuffer();
|
|
77
|
-
args.onProgress?.(ab.byteLength, total);
|
|
78
|
-
return { fileType, fileName, arrayBuffer: ab, url: args.fileUrl };
|
|
79
|
-
}
|
|
80
|
-
const reader = res.body.getReader();
|
|
81
|
-
const chunks = [];
|
|
82
|
-
let loaded = 0;
|
|
83
|
-
while (true) {
|
|
84
|
-
const { done, value } = await reader.read();
|
|
85
|
-
if (done) break;
|
|
86
|
-
if (value) {
|
|
87
|
-
chunks.push(value);
|
|
88
|
-
loaded += value.length;
|
|
89
|
-
args.onProgress?.(loaded, total);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const out = new Uint8Array(loaded);
|
|
93
|
-
let offset = 0;
|
|
94
|
-
for (const c of chunks) {
|
|
95
|
-
out.set(c, offset);
|
|
96
|
-
offset += c.length;
|
|
97
|
-
}
|
|
98
|
-
return { fileType, fileName, arrayBuffer: out.buffer, url: args.fileUrl };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// src/components/Toolbar.tsx
|
|
102
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
103
|
-
function Toolbar(props) {
|
|
104
|
-
const t = (k, fallback) => props.locale[k] ?? fallback;
|
|
105
|
-
return /* @__PURE__ */ jsxs("div", { className: "hv-toolbar", role: "toolbar", "aria-label": t("a11y.toolbar", "Document toolbar"), children: [
|
|
106
|
-
/* @__PURE__ */ jsxs("div", { className: "hv-toolbar__left", children: [
|
|
107
|
-
/* @__PURE__ */ jsx("button", { type: "button", className: "hv-btn", onClick: props.onToggleThumbnails, "aria-pressed": props.showThumbnails, children: t("toolbar.thumbs", "Thumbnails") }),
|
|
108
|
-
props.mode !== "create" && /* @__PURE__ */ jsx("button", { type: "button", className: "hv-btn", onClick: props.onToggleSignatures, "aria-pressed": props.showSignatures, children: t("toolbar.signatures", "Signatures") }),
|
|
109
|
-
/* @__PURE__ */ jsx("span", { className: "hv-sep" }),
|
|
110
|
-
/* @__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") }),
|
|
111
|
-
/* @__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") })
|
|
112
|
-
] }),
|
|
113
|
-
/* @__PURE__ */ jsxs("div", { className: "hv-toolbar__right", children: [
|
|
114
|
-
props.showHeaderFooterToggle && /* @__PURE__ */ jsxs("label", { className: "hv-toggle", children: [
|
|
115
|
-
/* @__PURE__ */ jsx("input", { type: "checkbox", checked: props.headerFooterEnabled, onChange: props.onToggleHeaderFooter }),
|
|
116
|
-
/* @__PURE__ */ jsx("span", { children: t("toolbar.letterhead", "Letterhead") })
|
|
117
|
-
] }),
|
|
118
|
-
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") }),
|
|
119
|
-
props.canExportPdf && /* @__PURE__ */ jsx("button", { type: "button", className: "hv-btn", onClick: props.onExportPdf, children: t("toolbar.exportPdf", "Export as PDF") }),
|
|
120
|
-
props.canSave && /* @__PURE__ */ jsx("button", { type: "button", className: "hv-btn hv-btn--primary", onClick: props.onSave, children: t("toolbar.save", "Save") })
|
|
121
|
-
] })
|
|
122
|
-
] });
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// src/components/ThumbnailsSidebar.tsx
|
|
126
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
127
|
-
function ThumbnailsSidebar(props) {
|
|
128
|
-
const t = props.locale["thumbnails.title"] ?? "Thumbnails";
|
|
129
|
-
return /* @__PURE__ */ jsxs2("aside", { className: props.collapsed ? "hv-thumbs hv-thumbs--collapsed" : "hv-thumbs", "aria-label": t, children: [
|
|
130
|
-
/* @__PURE__ */ jsxs2("div", { className: "hv-thumbs__header", children: [
|
|
131
|
-
/* @__PURE__ */ jsx2(
|
|
132
|
-
"button",
|
|
133
|
-
{
|
|
134
|
-
type: "button",
|
|
135
|
-
className: "hv-icon",
|
|
136
|
-
onClick: props.onToggle,
|
|
137
|
-
"aria-label": props.collapsed ? props.locale["thumbnails.open"] ?? "Open thumbnails" : props.locale["thumbnails.close"] ?? "Close thumbnails",
|
|
138
|
-
children: props.collapsed ? "\u25B8" : "\u25BE"
|
|
139
|
-
}
|
|
140
|
-
),
|
|
141
|
-
!props.collapsed ? /* @__PURE__ */ jsx2("div", { className: "hv-thumbs__title", children: t }) : null
|
|
142
|
-
] }),
|
|
143
|
-
!props.collapsed ? /* @__PURE__ */ jsx2("div", { className: "hv-thumbs__list", role: "list", children: props.thumbnails.map((th, idx) => {
|
|
144
|
-
const p = idx + 1;
|
|
145
|
-
const active = p === props.currentPage;
|
|
146
|
-
return /* @__PURE__ */ jsxs2(
|
|
147
|
-
"button",
|
|
148
|
-
{
|
|
149
|
-
type: "button",
|
|
150
|
-
role: "listitem",
|
|
151
|
-
className: active ? "hv-thumb hv-thumb--active" : "hv-thumb",
|
|
152
|
-
onClick: () => props.onSelectPage(p),
|
|
153
|
-
"aria-current": active ? "page" : void 0,
|
|
154
|
-
children: [
|
|
155
|
-
/* @__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" }) }),
|
|
156
|
-
/* @__PURE__ */ jsx2("div", { className: "hv-thumb__label", children: th.label })
|
|
157
|
-
]
|
|
158
|
-
},
|
|
159
|
-
th.id
|
|
160
|
-
);
|
|
161
|
-
}) }) : null
|
|
162
|
-
] });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// src/components/SignaturePanel.tsx
|
|
166
|
-
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
167
|
-
function SignaturePanel(props) {
|
|
168
|
-
const title = props.locale["signatures.title"] ?? "Signatures";
|
|
169
|
-
return /* @__PURE__ */ jsxs3("aside", { className: props.collapsed ? "hv-side hv-side--collapsed" : "hv-side", "aria-label": title, children: [
|
|
170
|
-
/* @__PURE__ */ jsxs3("div", { className: "hv-sidebar-header", children: [
|
|
171
|
-
/* @__PURE__ */ jsx3("button", { type: "button", className: "hv-icon", onClick: props.onToggle, "aria-label": props.locale["toolbar.signatures"] ?? "Signatures", children: "\u270D" }),
|
|
172
|
-
/* @__PURE__ */ jsx3("div", { className: "hv-sidebar-title", children: title })
|
|
173
|
-
] }),
|
|
174
|
-
/* @__PURE__ */ jsx3("div", { className: "hv-sidebar-body", children: props.signatures.map((s, idx) => /* @__PURE__ */ jsxs3("div", { className: "hv-signature-card", children: [
|
|
175
|
-
/* @__PURE__ */ jsx3("img", { src: s.signatureImageUrl, alt: `Signature by ${s.signedBy}`, className: "hv-signature-img" }),
|
|
176
|
-
/* @__PURE__ */ jsxs3("div", { className: "hv-signature-meta", children: [
|
|
177
|
-
/* @__PURE__ */ jsx3("div", { className: "hv-signature-name", children: s.signedBy }),
|
|
178
|
-
/* @__PURE__ */ jsx3("div", { className: "hv-signature-date", children: new Date(s.dateSigned).toLocaleString() }),
|
|
179
|
-
s.comment ? /* @__PURE__ */ jsx3("div", { className: "hv-signature-comment", children: s.comment }) : null
|
|
180
|
-
] })
|
|
181
|
-
] }, `${s.signedBy}-${s.dateSigned}-${idx}`)) })
|
|
182
|
-
] });
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// src/renderers/PdfRenderer.tsx
|
|
186
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
187
|
-
import {
|
|
188
|
-
GlobalWorkerOptions,
|
|
189
|
-
getDocument
|
|
190
|
-
} from "pdfjs-dist";
|
|
191
|
-
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
192
|
-
function PdfRenderer(props) {
|
|
193
|
-
const { url, arrayBuffer } = props;
|
|
194
|
-
const [doc, setDoc] = useState(null);
|
|
195
|
-
const [pageCount, setPageCount] = useState(0);
|
|
196
|
-
const [rendered, setRendered] = useState(
|
|
197
|
-
/* @__PURE__ */ new Map()
|
|
198
|
-
);
|
|
199
|
-
const [thumbs, setThumbs] = useState([]);
|
|
200
|
-
const [size, setSize] = useState({ w: 840, h: 1188 });
|
|
201
|
-
const [error, setError] = useState(null);
|
|
202
|
-
const [loading, setLoading] = useState(false);
|
|
203
|
-
const containerRef = useRef(null);
|
|
204
|
-
useEffect(() => {
|
|
205
|
-
try {
|
|
206
|
-
GlobalWorkerOptions.workerSrc = new URL(
|
|
207
|
-
"pdfjs-dist/build/pdf.worker.min.mjs",
|
|
208
|
-
import.meta.url
|
|
209
|
-
).toString();
|
|
210
|
-
} catch {
|
|
211
|
-
}
|
|
212
|
-
}, []);
|
|
213
|
-
useEffect(() => {
|
|
214
|
-
let cancel = false;
|
|
215
|
-
setError(null);
|
|
216
|
-
setLoading(true);
|
|
217
|
-
(async () => {
|
|
218
|
-
setDoc(null);
|
|
219
|
-
setRendered(/* @__PURE__ */ new Map());
|
|
220
|
-
setThumbs([]);
|
|
221
|
-
if (!url && !arrayBuffer) {
|
|
222
|
-
setError("No PDF source provided.");
|
|
223
|
-
setLoading(false);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
try {
|
|
227
|
-
const task = getDocument(
|
|
228
|
-
url ? { url, rangeChunkSize: 512 * 1024 } : { data: arrayBuffer }
|
|
229
|
-
);
|
|
230
|
-
const pdf = await task.promise;
|
|
231
|
-
if (cancel) return;
|
|
232
|
-
setDoc(pdf);
|
|
233
|
-
setPageCount(pdf.numPages);
|
|
234
|
-
props.onPageCount(pdf.numPages);
|
|
235
|
-
setThumbs(Array.from({ length: pdf.numPages }));
|
|
236
|
-
const p1 = await pdf.getPage(1);
|
|
237
|
-
const base = p1.getViewport({ scale: 1 });
|
|
238
|
-
const w = Math.min(980, Math.max(640, base.width));
|
|
239
|
-
const s = w / base.width;
|
|
240
|
-
const vp = p1.getViewport({ scale: s });
|
|
241
|
-
setSize({ w: Math.round(vp.width), h: Math.round(vp.height) });
|
|
242
|
-
} catch (e) {
|
|
243
|
-
setError(
|
|
244
|
-
"Failed to load PDF. " + (e instanceof Error ? e.message : "")
|
|
245
|
-
);
|
|
246
|
-
} finally {
|
|
247
|
-
setLoading(false);
|
|
248
|
-
}
|
|
249
|
-
})();
|
|
250
|
-
return () => {
|
|
251
|
-
cancel = true;
|
|
252
|
-
};
|
|
253
|
-
}, [url, arrayBuffer]);
|
|
254
|
-
useEffect(() => {
|
|
255
|
-
props.onThumbs(thumbs);
|
|
256
|
-
}, [thumbs]);
|
|
257
|
-
const pagesToShow = useMemo(() => {
|
|
258
|
-
if (props.layout === "side-by-side") {
|
|
259
|
-
const left = props.currentPage;
|
|
260
|
-
const right = Math.min(pageCount || left + 1, left + 1);
|
|
261
|
-
return [left, right];
|
|
262
|
-
}
|
|
263
|
-
return [props.currentPage];
|
|
264
|
-
}, [props.currentPage, props.layout, pageCount]);
|
|
265
|
-
useEffect(() => {
|
|
266
|
-
if (!doc) return;
|
|
267
|
-
let cancel = false;
|
|
268
|
-
(async () => {
|
|
269
|
-
for (const p of pagesToShow) {
|
|
270
|
-
if (rendered.has(p)) continue;
|
|
271
|
-
try {
|
|
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
|
-
if (!thumbs[p - 1]) {
|
|
289
|
-
const thumbCanvas = document.createElement("canvas");
|
|
290
|
-
const thumbScale = 120 / vp.width;
|
|
291
|
-
thumbCanvas.width = Math.round(vp.width * thumbScale);
|
|
292
|
-
thumbCanvas.height = Math.round(vp.height * thumbScale);
|
|
293
|
-
const thumbCtx = thumbCanvas.getContext("2d", { alpha: false });
|
|
294
|
-
if (thumbCtx) {
|
|
295
|
-
thumbCtx.drawImage(
|
|
296
|
-
canvas,
|
|
297
|
-
0,
|
|
298
|
-
0,
|
|
299
|
-
thumbCanvas.width,
|
|
300
|
-
thumbCanvas.height
|
|
301
|
-
);
|
|
302
|
-
setThumbs((prev) => {
|
|
303
|
-
const arr = prev.slice();
|
|
304
|
-
arr[p - 1] = thumbCanvas.toDataURL("image/png");
|
|
305
|
-
return arr;
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
} catch {
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
})();
|
|
313
|
-
return () => {
|
|
314
|
-
cancel = true;
|
|
315
|
-
};
|
|
316
|
-
}, [doc, pagesToShow, size.w, rendered, thumbs]);
|
|
317
|
-
function onWheel(e) {
|
|
318
|
-
if (!pageCount) return;
|
|
319
|
-
if (Math.abs(e.deltaY) < 10) return;
|
|
320
|
-
const dir = e.deltaY > 0 ? 1 : -1;
|
|
321
|
-
const step = props.layout === "side-by-side" ? 2 : 1;
|
|
322
|
-
const next = Math.max(
|
|
323
|
-
1,
|
|
324
|
-
Math.min(pageCount, props.currentPage + dir * step)
|
|
325
|
-
);
|
|
326
|
-
props.onCurrentPageChange(next);
|
|
327
|
-
}
|
|
328
|
-
function clickPlace(e, page) {
|
|
329
|
-
const stamp = props.signatureStamp;
|
|
330
|
-
if (!stamp?.armed) return;
|
|
331
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
332
|
-
const x = (e.clientX - rect.left) / rect.width;
|
|
333
|
-
const y = (e.clientY - rect.top) / rect.height;
|
|
334
|
-
stamp.onPlaced({ page, x, y, w: 0.22, h: 0.08 });
|
|
335
|
-
}
|
|
336
|
-
return /* @__PURE__ */ jsxs4("div", { className: "hv-doc", ref: containerRef, onWheel, children: [
|
|
337
|
-
!doc ? /* @__PURE__ */ jsx4("div", { className: "hv-loading", children: "Loading PDF\u2026" }) : null,
|
|
338
|
-
doc ? /* @__PURE__ */ jsx4(
|
|
339
|
-
"div",
|
|
340
|
-
{
|
|
341
|
-
className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages",
|
|
342
|
-
children: pagesToShow.map((p) => {
|
|
343
|
-
const c = rendered.get(p);
|
|
344
|
-
return /* @__PURE__ */ jsx4(
|
|
345
|
-
"div",
|
|
346
|
-
{
|
|
347
|
-
className: "hv-page",
|
|
348
|
-
style: { width: size.w, height: size.h },
|
|
349
|
-
onClick: (e) => clickPlace(e, p),
|
|
350
|
-
children: c ? /* @__PURE__ */ jsx4(
|
|
351
|
-
"canvas",
|
|
352
|
-
{
|
|
353
|
-
className: "hv-canvas",
|
|
354
|
-
width: c.width,
|
|
355
|
-
height: c.height,
|
|
356
|
-
ref: (node) => {
|
|
357
|
-
if (!node) return;
|
|
358
|
-
const ctx = node.getContext("2d");
|
|
359
|
-
if (ctx) ctx.drawImage(c, 0, 0);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
) : /* @__PURE__ */ jsx4("div", { className: "hv-loading", children: "Rendering\u2026" })
|
|
363
|
-
},
|
|
364
|
-
p
|
|
365
|
-
);
|
|
366
|
-
})
|
|
367
|
-
}
|
|
368
|
-
) : null
|
|
369
|
-
] });
|
|
370
|
-
}
|
|
371
|
-
|
|
372
4
|
// src/editors/RichTextEditor.tsx
|
|
5
|
+
import html2canvas from "html2canvas";
|
|
6
|
+
import mammoth from "mammoth";
|
|
7
|
+
import MarkdownIt from "markdown-it";
|
|
373
8
|
import {
|
|
374
9
|
forwardRef,
|
|
375
|
-
useEffect
|
|
10
|
+
useEffect,
|
|
376
11
|
useImperativeHandle,
|
|
377
|
-
useMemo
|
|
378
|
-
useRef
|
|
379
|
-
useState
|
|
12
|
+
useMemo,
|
|
13
|
+
useRef,
|
|
14
|
+
useState
|
|
380
15
|
} from "react";
|
|
381
|
-
import mammoth from "mammoth";
|
|
382
|
-
import MarkdownIt from "markdown-it";
|
|
383
|
-
import html2canvas from "html2canvas";
|
|
384
16
|
|
|
385
17
|
// src/utils/sanitize.ts
|
|
386
18
|
import DOMPurify from "dompurify";
|
|
@@ -392,44 +24,53 @@ function sanitizeHtml(html) {
|
|
|
392
24
|
}
|
|
393
25
|
|
|
394
26
|
// src/editors/RichTextEditor.tsx
|
|
395
|
-
import { jsx
|
|
27
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
396
28
|
var PAGE_H = 1122;
|
|
397
29
|
var RichTextEditor = forwardRef((props, ref) => {
|
|
398
30
|
const readOnly = props.mode === "view";
|
|
399
|
-
const md =
|
|
31
|
+
const md = useMemo(
|
|
400
32
|
() => new MarkdownIt({ html: false, linkify: true, breaks: true }),
|
|
401
33
|
[]
|
|
402
34
|
);
|
|
403
|
-
const scrollerRef =
|
|
404
|
-
const editorRef =
|
|
405
|
-
const captureRef =
|
|
406
|
-
const [html, setHtml] =
|
|
407
|
-
|
|
35
|
+
const scrollerRef = useRef(null);
|
|
36
|
+
const editorRef = useRef(null);
|
|
37
|
+
const captureRef = useRef(null);
|
|
38
|
+
const [html, setHtml] = useState("<p><br/></p>");
|
|
39
|
+
useEffect(() => {
|
|
408
40
|
let cancelled = false;
|
|
409
41
|
(async () => {
|
|
410
42
|
if (props.mode === "create") {
|
|
411
43
|
setHtml("<p><br/></p>");
|
|
412
44
|
return;
|
|
413
45
|
}
|
|
414
|
-
if (!props.arrayBuffer)
|
|
46
|
+
if (!props.arrayBuffer) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
415
49
|
if (props.fileType === "docx") {
|
|
416
50
|
const res = await mammoth.convertToHtml({
|
|
417
51
|
arrayBuffer: props.arrayBuffer
|
|
418
52
|
});
|
|
419
|
-
if (!cancelled)
|
|
53
|
+
if (!cancelled) {
|
|
54
|
+
setHtml(sanitizeHtml(res.value || "<p><br/></p>"));
|
|
55
|
+
}
|
|
420
56
|
} else {
|
|
421
57
|
const text = new TextDecoder().decode(props.arrayBuffer);
|
|
422
|
-
if (props.fileType === "md")
|
|
423
|
-
|
|
58
|
+
if (props.fileType === "md") {
|
|
59
|
+
setHtml(sanitizeHtml(md.render(text)));
|
|
60
|
+
} else {
|
|
61
|
+
setHtml(`<pre>${escapeHtml(text)}</pre>`);
|
|
62
|
+
}
|
|
424
63
|
}
|
|
425
64
|
})();
|
|
426
65
|
return () => {
|
|
427
66
|
cancelled = true;
|
|
428
67
|
};
|
|
429
68
|
}, [props.arrayBuffer, props.fileType, props.mode, md]);
|
|
430
|
-
|
|
69
|
+
useEffect(() => {
|
|
431
70
|
const el = scrollerRef.current;
|
|
432
|
-
if (!el)
|
|
71
|
+
if (!el) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
433
74
|
const recompute = () => props.onPageCount(Math.max(1, Math.ceil(el.scrollHeight / PAGE_H)));
|
|
434
75
|
recompute();
|
|
435
76
|
const ro = new ResizeObserver(recompute);
|
|
@@ -437,13 +78,19 @@ var RichTextEditor = forwardRef((props, ref) => {
|
|
|
437
78
|
return () => ro.disconnect();
|
|
438
79
|
}, [html, props.headerFooterEnabled]);
|
|
439
80
|
function exec(cmd) {
|
|
440
|
-
if (readOnly)
|
|
81
|
+
if (readOnly) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
441
84
|
document.execCommand(cmd);
|
|
442
85
|
}
|
|
443
86
|
function onClick(e) {
|
|
444
|
-
if (!props.armedSignatureUrl)
|
|
87
|
+
if (!props.armedSignatureUrl) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
445
90
|
const scroller = scrollerRef.current;
|
|
446
|
-
if (!scroller)
|
|
91
|
+
if (!scroller) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
447
94
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
448
95
|
const absY = scroller.scrollTop + (e.clientY - rect.top);
|
|
449
96
|
const page = Math.max(1, Math.floor(absY / PAGE_H) + 1);
|
|
@@ -455,7 +102,9 @@ var RichTextEditor = forwardRef((props, ref) => {
|
|
|
455
102
|
async function requestThumbnail(index) {
|
|
456
103
|
const scroller = scrollerRef.current;
|
|
457
104
|
const capture = captureRef.current;
|
|
458
|
-
if (!scroller || !capture)
|
|
105
|
+
if (!scroller || !capture) {
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
459
108
|
const old = scroller.scrollTop;
|
|
460
109
|
scroller.scrollTop = index * PAGE_H;
|
|
461
110
|
await new Promise((r) => requestAnimationFrame(() => r(null)));
|
|
@@ -495,7 +144,9 @@ var RichTextEditor = forwardRef((props, ref) => {
|
|
|
495
144
|
fileName: replaceExt(props.fileName, "docx")
|
|
496
145
|
})
|
|
497
146
|
});
|
|
498
|
-
if (!response.ok)
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
throw new Error("Failed to generate DOCX");
|
|
149
|
+
}
|
|
499
150
|
const blob = await response.blob();
|
|
500
151
|
const url = window.URL.createObjectURL(blob);
|
|
501
152
|
const a = document.createElement("a");
|
|
@@ -523,9 +174,9 @@ var RichTextEditor = forwardRef((props, ref) => {
|
|
|
523
174
|
});
|
|
524
175
|
}
|
|
525
176
|
useImperativeHandle(ref, () => ({ save, requestThumbnail }));
|
|
526
|
-
return /* @__PURE__ */
|
|
527
|
-
/* @__PURE__ */
|
|
528
|
-
/* @__PURE__ */
|
|
177
|
+
return /* @__PURE__ */ jsxs("div", { className: "hv-doc", children: [
|
|
178
|
+
/* @__PURE__ */ jsxs("div", { className: "hv-ribbon", role: "toolbar", children: [
|
|
179
|
+
/* @__PURE__ */ jsx(
|
|
529
180
|
"button",
|
|
530
181
|
{
|
|
531
182
|
className: "hv-btn",
|
|
@@ -534,7 +185,7 @@ var RichTextEditor = forwardRef((props, ref) => {
|
|
|
534
185
|
children: "B"
|
|
535
186
|
}
|
|
536
187
|
),
|
|
537
|
-
/* @__PURE__ */
|
|
188
|
+
/* @__PURE__ */ jsx(
|
|
538
189
|
"button",
|
|
539
190
|
{
|
|
540
191
|
className: "hv-btn",
|
|
@@ -543,7 +194,7 @@ var RichTextEditor = forwardRef((props, ref) => {
|
|
|
543
194
|
children: "I"
|
|
544
195
|
}
|
|
545
196
|
),
|
|
546
|
-
/* @__PURE__ */
|
|
197
|
+
/* @__PURE__ */ jsx(
|
|
547
198
|
"button",
|
|
548
199
|
{
|
|
549
200
|
className: "hv-btn",
|
|
@@ -552,11 +203,11 @@ var RichTextEditor = forwardRef((props, ref) => {
|
|
|
552
203
|
children: "U"
|
|
553
204
|
}
|
|
554
205
|
),
|
|
555
|
-
props.armedSignatureUrl ? /* @__PURE__ */
|
|
206
|
+
props.armedSignatureUrl ? /* @__PURE__ */ jsx("div", { className: "hv-hint", children: "Click to place signature" }) : null
|
|
556
207
|
] }),
|
|
557
|
-
/* @__PURE__ */
|
|
558
|
-
props.headerFooterEnabled && props.headerComponent ? /* @__PURE__ */
|
|
559
|
-
/* @__PURE__ */
|
|
208
|
+
/* @__PURE__ */ jsx("div", { className: "hv-scroll", ref: scrollerRef, onClick, children: /* @__PURE__ */ jsxs("div", { className: "hv-pageStage", ref: captureRef, children: [
|
|
209
|
+
props.headerFooterEnabled && props.headerComponent ? /* @__PURE__ */ jsx("div", { className: "hv-letterhead", children: props.headerComponent }) : null,
|
|
210
|
+
/* @__PURE__ */ jsx(
|
|
560
211
|
"div",
|
|
561
212
|
{
|
|
562
213
|
ref: editorRef,
|
|
@@ -567,9 +218,9 @@ var RichTextEditor = forwardRef((props, ref) => {
|
|
|
567
218
|
dangerouslySetInnerHTML: { __html: html }
|
|
568
219
|
}
|
|
569
220
|
),
|
|
570
|
-
props.headerFooterEnabled && props.footerComponent ? /* @__PURE__ */
|
|
571
|
-
props.mode === "create" && props.signatures.length ? /* @__PURE__ */
|
|
572
|
-
/* @__PURE__ */
|
|
221
|
+
props.headerFooterEnabled && props.footerComponent ? /* @__PURE__ */ jsx("div", { className: "hv-letterhead hv-letterhead--footer", children: props.footerComponent }) : null,
|
|
222
|
+
props.mode === "create" && props.signatures.length ? /* @__PURE__ */ jsx("div", { className: "hv-signatures-inline", children: props.signatures.map((s, i) => /* @__PURE__ */ jsxs("div", { className: "hv-sign-inline", children: [
|
|
223
|
+
/* @__PURE__ */ jsx(
|
|
573
224
|
"img",
|
|
574
225
|
{
|
|
575
226
|
src: s.signatureImageUrl,
|
|
@@ -577,9 +228,9 @@ var RichTextEditor = forwardRef((props, ref) => {
|
|
|
577
228
|
className: "hv-sign-img"
|
|
578
229
|
}
|
|
579
230
|
),
|
|
580
|
-
/* @__PURE__ */
|
|
581
|
-
/* @__PURE__ */
|
|
582
|
-
/* @__PURE__ */
|
|
231
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
232
|
+
/* @__PURE__ */ jsx("div", { className: "hv-sign-name", children: s.signedBy }),
|
|
233
|
+
/* @__PURE__ */ jsx("div", { className: "hv-sign-date", children: new Date(s.dateSigned).toLocaleString() })
|
|
583
234
|
] })
|
|
584
235
|
] }, i)) }) : null
|
|
585
236
|
] }) })
|
|
@@ -594,14 +245,103 @@ function escapeHtml(s) {
|
|
|
594
245
|
}
|
|
595
246
|
|
|
596
247
|
// src/editors/SpreadsheetEditor.tsx
|
|
597
|
-
import { forwardRef as forwardRef2, useEffect as
|
|
248
|
+
import { forwardRef as forwardRef2, useEffect as useEffect2, useImperativeHandle as useImperativeHandle2, useMemo as useMemo2, useState as useState2 } from "react";
|
|
598
249
|
import * as XLSX from "xlsx";
|
|
599
|
-
|
|
250
|
+
|
|
251
|
+
// src/utils/fileSource.ts
|
|
252
|
+
function guessFileType(name, explicit) {
|
|
253
|
+
if (explicit) {
|
|
254
|
+
return explicit;
|
|
255
|
+
}
|
|
256
|
+
const ext = (name?.split(".").pop() || "").toLowerCase();
|
|
257
|
+
const allowed = [
|
|
258
|
+
"pdf",
|
|
259
|
+
"md",
|
|
260
|
+
"docx",
|
|
261
|
+
"xlsx",
|
|
262
|
+
"pptx",
|
|
263
|
+
"txt",
|
|
264
|
+
"png",
|
|
265
|
+
"jpg",
|
|
266
|
+
"svg"
|
|
267
|
+
];
|
|
268
|
+
return allowed.includes(ext) ? ext : "txt";
|
|
269
|
+
}
|
|
270
|
+
function arrayBufferToBase64(buf) {
|
|
271
|
+
const bytes = new Uint8Array(buf);
|
|
272
|
+
let binary = "";
|
|
273
|
+
const chunk = 32768;
|
|
274
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
275
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
276
|
+
}
|
|
277
|
+
return btoa(binary);
|
|
278
|
+
}
|
|
279
|
+
async function base64ToArrayBuffer(b64) {
|
|
280
|
+
const bin = atob(b64);
|
|
281
|
+
const len = bin.length;
|
|
282
|
+
const bytes = new Uint8Array(len);
|
|
283
|
+
for (let i = 0; i < len; i++) {
|
|
284
|
+
bytes[i] = bin.charCodeAt(i);
|
|
285
|
+
}
|
|
286
|
+
return bytes.buffer;
|
|
287
|
+
}
|
|
288
|
+
async function resolveSource(args) {
|
|
289
|
+
const fileType = guessFileType(args.fileName, args.fileType);
|
|
290
|
+
const fileName = args.fileName ?? `document.${fileType}`;
|
|
291
|
+
if (args.blob) {
|
|
292
|
+
const ab = await args.blob.arrayBuffer();
|
|
293
|
+
const url = URL.createObjectURL(args.blob);
|
|
294
|
+
return { fileType, fileName, arrayBuffer: ab, url };
|
|
295
|
+
}
|
|
296
|
+
if (args.base64) {
|
|
297
|
+
const ab = await base64ToArrayBuffer(args.base64);
|
|
298
|
+
return { fileType, fileName, arrayBuffer: ab };
|
|
299
|
+
}
|
|
300
|
+
if (!args.fileUrl) {
|
|
301
|
+
throw new Error("No file source provided. Use fileUrl, blob, or base64.");
|
|
302
|
+
}
|
|
303
|
+
const res = await fetch(args.fileUrl);
|
|
304
|
+
if (!res.ok) {
|
|
305
|
+
throw new Error(`Failed to fetch file (${res.status})`);
|
|
306
|
+
}
|
|
307
|
+
const total = Number(res.headers.get("content-length") || "") || void 0;
|
|
308
|
+
if (!res.body) {
|
|
309
|
+
const ab = await res.arrayBuffer();
|
|
310
|
+
args.onProgress?.(ab.byteLength, total);
|
|
311
|
+
return { fileType, fileName, arrayBuffer: ab, url: args.fileUrl };
|
|
312
|
+
}
|
|
313
|
+
const reader = res.body.getReader();
|
|
314
|
+
const chunks = [];
|
|
315
|
+
let loaded = 0;
|
|
316
|
+
while (true) {
|
|
317
|
+
const { done, value } = await reader.read();
|
|
318
|
+
if (done) {
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
if (value) {
|
|
322
|
+
chunks.push(value);
|
|
323
|
+
loaded += value.length;
|
|
324
|
+
args.onProgress?.(loaded, total);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const out = new Uint8Array(loaded);
|
|
328
|
+
let offset = 0;
|
|
329
|
+
for (const c of chunks) {
|
|
330
|
+
out.set(c, offset);
|
|
331
|
+
offset += c.length;
|
|
332
|
+
}
|
|
333
|
+
return { fileType, fileName, arrayBuffer: out.buffer, url: args.fileUrl };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/editors/SpreadsheetEditor.tsx
|
|
337
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
600
338
|
var SpreadsheetEditor = forwardRef2(function SpreadsheetEditor2(props, ref) {
|
|
601
339
|
const readonly = props.mode === "view";
|
|
602
|
-
const [grid, setGrid] =
|
|
603
|
-
|
|
604
|
-
if (!props.arrayBuffer)
|
|
340
|
+
const [grid, setGrid] = useState2(() => Array.from({ length: 30 }, () => Array.from({ length: 12 }, () => "")));
|
|
341
|
+
useEffect2(() => {
|
|
342
|
+
if (!props.arrayBuffer) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
605
345
|
try {
|
|
606
346
|
const wb = XLSX.read(props.arrayBuffer, { type: "array" });
|
|
607
347
|
const name = wb.SheetNames[0];
|
|
@@ -629,20 +369,20 @@ var SpreadsheetEditor = forwardRef2(function SpreadsheetEditor2(props, ref) {
|
|
|
629
369
|
save,
|
|
630
370
|
requestThumbnails: async () => void 0
|
|
631
371
|
}));
|
|
632
|
-
const cols =
|
|
633
|
-
return /* @__PURE__ */
|
|
634
|
-
/* @__PURE__ */
|
|
635
|
-
/* @__PURE__ */
|
|
636
|
-
!readonly ? /* @__PURE__ */
|
|
372
|
+
const cols = useMemo2(() => Array.from({ length: grid[0]?.length ?? 0 }, (_, i) => String.fromCharCode(65 + i % 26)), [grid]);
|
|
373
|
+
return /* @__PURE__ */ jsxs2("div", { className: "hv-sheet", children: [
|
|
374
|
+
/* @__PURE__ */ jsxs2("div", { className: "hv-sheetbar", children: [
|
|
375
|
+
/* @__PURE__ */ jsx2("div", { className: "hv-sheetbar-title", children: props.fileName }),
|
|
376
|
+
!readonly ? /* @__PURE__ */ jsx2("button", { className: "hv-btn", type: "button", onClick: () => void save(false), children: props.locale["toolbar.save"] ?? "Save" }) : null
|
|
637
377
|
] }),
|
|
638
|
-
/* @__PURE__ */
|
|
639
|
-
/* @__PURE__ */
|
|
640
|
-
/* @__PURE__ */
|
|
641
|
-
cols.map((c, i) => /* @__PURE__ */
|
|
378
|
+
/* @__PURE__ */ jsxs2("div", { className: "hv-sheetgrid", role: "table", "aria-label": "Spreadsheet", children: [
|
|
379
|
+
/* @__PURE__ */ jsxs2("div", { className: "hv-sheetrow hv-sheetrow--header", role: "row", children: [
|
|
380
|
+
/* @__PURE__ */ jsx2("div", { className: "hv-sheetcell hv-sheetcell--corner", role: "columnheader" }),
|
|
381
|
+
cols.map((c, i) => /* @__PURE__ */ jsx2("div", { className: "hv-sheetcell hv-sheetcell--header", role: "columnheader", children: c }, i))
|
|
642
382
|
] }),
|
|
643
|
-
grid.map((row, r) => /* @__PURE__ */
|
|
644
|
-
/* @__PURE__ */
|
|
645
|
-
row.map((val, c) => /* @__PURE__ */
|
|
383
|
+
grid.map((row, r) => /* @__PURE__ */ jsxs2("div", { className: "hv-sheetrow", role: "row", children: [
|
|
384
|
+
/* @__PURE__ */ jsx2("div", { className: "hv-sheetcell hv-sheetcell--header", role: "rowheader", children: r + 1 }),
|
|
385
|
+
row.map((val, c) => /* @__PURE__ */ jsx2(
|
|
646
386
|
"div",
|
|
647
387
|
{
|
|
648
388
|
className: "hv-sheetcell",
|
|
@@ -671,29 +411,33 @@ function ensureExt(name, ext) {
|
|
|
671
411
|
}
|
|
672
412
|
|
|
673
413
|
// src/renderers/ImageRenderer.tsx
|
|
674
|
-
import { useEffect as
|
|
675
|
-
import { jsx as
|
|
414
|
+
import { useEffect as useEffect3, useMemo as useMemo3, useState as useState3 } from "react";
|
|
415
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
676
416
|
function ImageRenderer({
|
|
677
417
|
arrayBuffer,
|
|
678
418
|
fileType,
|
|
679
419
|
fileName
|
|
680
420
|
}) {
|
|
681
|
-
const [zoom, setZoom] =
|
|
682
|
-
const url =
|
|
683
|
-
if (!arrayBuffer)
|
|
421
|
+
const [zoom, setZoom] = useState3(1);
|
|
422
|
+
const url = useMemo3(() => {
|
|
423
|
+
if (!arrayBuffer) {
|
|
424
|
+
return void 0;
|
|
425
|
+
}
|
|
684
426
|
const mime = fileType === "svg" ? "image/svg+xml" : fileType === "png" ? "image/png" : "image/jpeg";
|
|
685
427
|
return URL.createObjectURL(new Blob([arrayBuffer], { type: mime }));
|
|
686
428
|
}, [arrayBuffer, fileType]);
|
|
687
|
-
|
|
429
|
+
useEffect3(() => {
|
|
688
430
|
return () => {
|
|
689
|
-
if (url)
|
|
431
|
+
if (url) {
|
|
432
|
+
URL.revokeObjectURL(url);
|
|
433
|
+
}
|
|
690
434
|
};
|
|
691
435
|
}, [url]);
|
|
692
|
-
return /* @__PURE__ */
|
|
693
|
-
/* @__PURE__ */
|
|
694
|
-
/* @__PURE__ */
|
|
695
|
-
/* @__PURE__ */
|
|
696
|
-
/* @__PURE__ */
|
|
436
|
+
return /* @__PURE__ */ jsxs3("div", { className: "hv-doc", children: [
|
|
437
|
+
/* @__PURE__ */ jsxs3("div", { className: "hv-mini-toolbar", children: [
|
|
438
|
+
/* @__PURE__ */ jsx3("div", { className: "hv-title", children: fileName }),
|
|
439
|
+
/* @__PURE__ */ jsx3("div", { className: "hv-spacer" }),
|
|
440
|
+
/* @__PURE__ */ jsx3(
|
|
697
441
|
"button",
|
|
698
442
|
{
|
|
699
443
|
type: "button",
|
|
@@ -702,11 +446,11 @@ function ImageRenderer({
|
|
|
702
446
|
children: "-"
|
|
703
447
|
}
|
|
704
448
|
),
|
|
705
|
-
/* @__PURE__ */
|
|
449
|
+
/* @__PURE__ */ jsxs3("div", { className: "hv-zoom", children: [
|
|
706
450
|
Math.round(zoom * 100),
|
|
707
451
|
"%"
|
|
708
452
|
] }),
|
|
709
|
-
/* @__PURE__ */
|
|
453
|
+
/* @__PURE__ */ jsx3(
|
|
710
454
|
"button",
|
|
711
455
|
{
|
|
712
456
|
type: "button",
|
|
@@ -716,10 +460,10 @@ function ImageRenderer({
|
|
|
716
460
|
}
|
|
717
461
|
)
|
|
718
462
|
] }),
|
|
719
|
-
/* @__PURE__ */
|
|
720
|
-
!arrayBuffer && /* @__PURE__ */
|
|
721
|
-
arrayBuffer && !url && /* @__PURE__ */
|
|
722
|
-
url && /* @__PURE__ */
|
|
463
|
+
/* @__PURE__ */ jsxs3("div", { className: "hv-center", children: [
|
|
464
|
+
!arrayBuffer && /* @__PURE__ */ jsx3("div", { className: "hv-error", children: "No image data provided." }),
|
|
465
|
+
arrayBuffer && !url && /* @__PURE__ */ jsx3("div", { className: "hv-error", children: "Failed to load image." }),
|
|
466
|
+
url && /* @__PURE__ */ jsx3(
|
|
723
467
|
"img",
|
|
724
468
|
{
|
|
725
469
|
src: url,
|
|
@@ -732,10 +476,216 @@ function ImageRenderer({
|
|
|
732
476
|
] });
|
|
733
477
|
}
|
|
734
478
|
|
|
479
|
+
// src/renderers/PdfRenderer.tsx
|
|
480
|
+
import {
|
|
481
|
+
getDocument,
|
|
482
|
+
GlobalWorkerOptions
|
|
483
|
+
} from "pdfjs-dist";
|
|
484
|
+
import { useEffect as useEffect4, useMemo as useMemo4, useRef as useRef2, useState as useState4 } from "react";
|
|
485
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
486
|
+
function PdfRenderer(props) {
|
|
487
|
+
const { url, arrayBuffer } = props;
|
|
488
|
+
const [doc, setDoc] = useState4(null);
|
|
489
|
+
const [pageCount, setPageCount] = useState4(0);
|
|
490
|
+
const [rendered, setRendered] = useState4(
|
|
491
|
+
/* @__PURE__ */ new Map()
|
|
492
|
+
);
|
|
493
|
+
const [thumbs, setThumbs] = useState4([]);
|
|
494
|
+
const [size, setSize] = useState4({ w: 840, h: 1188 });
|
|
495
|
+
const [error, setError] = useState4(null);
|
|
496
|
+
const [loading, setLoading] = useState4(false);
|
|
497
|
+
const containerRef = useRef2(null);
|
|
498
|
+
useEffect4(() => {
|
|
499
|
+
try {
|
|
500
|
+
GlobalWorkerOptions.workerSrc = new URL(
|
|
501
|
+
"pdfjs-dist/build/pdf.worker.min.mjs",
|
|
502
|
+
import.meta.url
|
|
503
|
+
).toString();
|
|
504
|
+
} catch {
|
|
505
|
+
}
|
|
506
|
+
}, []);
|
|
507
|
+
useEffect4(() => {
|
|
508
|
+
let cancel = false;
|
|
509
|
+
setError(null);
|
|
510
|
+
setLoading(true);
|
|
511
|
+
(async () => {
|
|
512
|
+
setDoc(null);
|
|
513
|
+
setRendered(/* @__PURE__ */ new Map());
|
|
514
|
+
setThumbs([]);
|
|
515
|
+
if (!url && !arrayBuffer) {
|
|
516
|
+
setError("No PDF source provided.");
|
|
517
|
+
setLoading(false);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const task = getDocument(
|
|
522
|
+
url ? { url, rangeChunkSize: 512 * 1024 } : { data: arrayBuffer }
|
|
523
|
+
);
|
|
524
|
+
const pdf = await task.promise;
|
|
525
|
+
if (cancel) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
setDoc(pdf);
|
|
529
|
+
setPageCount(pdf.numPages);
|
|
530
|
+
props.onPageCount(pdf.numPages);
|
|
531
|
+
const p1 = await pdf.getPage(1);
|
|
532
|
+
const base = p1.getViewport({ scale: 1 });
|
|
533
|
+
const w = Math.min(980, Math.max(640, base.width));
|
|
534
|
+
const s = w / base.width;
|
|
535
|
+
const vp = p1.getViewport({ scale: s });
|
|
536
|
+
setSize({ w: Math.round(vp.width), h: Math.round(vp.height) });
|
|
537
|
+
const thumbWidth = 56;
|
|
538
|
+
const thumbsArr = [];
|
|
539
|
+
for (let i = 1; i <= pdf.numPages; i++) {
|
|
540
|
+
const page = await pdf.getPage(i);
|
|
541
|
+
const pageBase = page.getViewport({ scale: 1 });
|
|
542
|
+
const thumbScale = thumbWidth / pageBase.width;
|
|
543
|
+
const thumbVp = page.getViewport({ scale: thumbScale });
|
|
544
|
+
const thumbCanvas = document.createElement("canvas");
|
|
545
|
+
thumbCanvas.width = Math.round(thumbVp.width);
|
|
546
|
+
thumbCanvas.height = Math.round(thumbVp.height);
|
|
547
|
+
const thumbCtx = thumbCanvas.getContext("2d", { alpha: false });
|
|
548
|
+
if (thumbCtx) {
|
|
549
|
+
await page.render({ canvasContext: thumbCtx, viewport: thumbVp }).promise;
|
|
550
|
+
thumbsArr.push(thumbCanvas.toDataURL("image/png"));
|
|
551
|
+
} else {
|
|
552
|
+
thumbsArr.push(void 0);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
setThumbs(thumbsArr);
|
|
556
|
+
} catch (e) {
|
|
557
|
+
setError(
|
|
558
|
+
"Failed to load PDF. " + (e instanceof Error ? e.message : "")
|
|
559
|
+
);
|
|
560
|
+
} finally {
|
|
561
|
+
setLoading(false);
|
|
562
|
+
}
|
|
563
|
+
})();
|
|
564
|
+
return () => {
|
|
565
|
+
cancel = true;
|
|
566
|
+
};
|
|
567
|
+
}, [url, arrayBuffer]);
|
|
568
|
+
useEffect4(() => {
|
|
569
|
+
props.onThumbs(thumbs);
|
|
570
|
+
}, [thumbs]);
|
|
571
|
+
const pagesToShow = useMemo4(() => {
|
|
572
|
+
if (props.layout === "side-by-side" && pageCount > 1) {
|
|
573
|
+
const left = Math.max(1, Math.min(props.currentPage, pageCount));
|
|
574
|
+
const right = Math.max(1, Math.min(left + 1, pageCount));
|
|
575
|
+
return left === right ? [left] : [left, right];
|
|
576
|
+
}
|
|
577
|
+
return [Math.max(1, Math.min(props.currentPage, pageCount))];
|
|
578
|
+
}, [props.currentPage, props.layout, pageCount]);
|
|
579
|
+
useEffect4(() => {
|
|
580
|
+
if (!doc) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
let cancel = false;
|
|
584
|
+
(async () => {
|
|
585
|
+
for (const p of pagesToShow) {
|
|
586
|
+
if (rendered.has(p)) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
const page = await doc.getPage(p);
|
|
591
|
+
if (cancel) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const base = page.getViewport({ scale: 1 });
|
|
595
|
+
const vp = page.getViewport({ scale: size.w / base.width });
|
|
596
|
+
const canvas = document.createElement("canvas");
|
|
597
|
+
canvas.width = Math.round(vp.width);
|
|
598
|
+
canvas.height = Math.round(vp.height);
|
|
599
|
+
const ctx = canvas.getContext("2d", { alpha: false });
|
|
600
|
+
if (!ctx) {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
await page.render({ canvasContext: ctx, viewport: vp }).promise;
|
|
604
|
+
if (cancel) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
setRendered((prev) => {
|
|
608
|
+
const next = new Map(prev);
|
|
609
|
+
next.set(p, canvas);
|
|
610
|
+
return next;
|
|
611
|
+
});
|
|
612
|
+
} catch {
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
})();
|
|
616
|
+
return () => {
|
|
617
|
+
cancel = true;
|
|
618
|
+
};
|
|
619
|
+
}, [doc, pagesToShow, size.w, rendered]);
|
|
620
|
+
function onWheel(e) {
|
|
621
|
+
if (!pageCount) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (Math.abs(e.deltaY) < 10) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const dir = e.deltaY > 0 ? 1 : -1;
|
|
628
|
+
const step = props.layout === "side-by-side" ? 2 : 1;
|
|
629
|
+
const next = Math.max(
|
|
630
|
+
1,
|
|
631
|
+
Math.min(pageCount, props.currentPage + dir * step)
|
|
632
|
+
);
|
|
633
|
+
props.onCurrentPageChange(next);
|
|
634
|
+
}
|
|
635
|
+
function clickPlace(e, page) {
|
|
636
|
+
const stamp = props.signatureStamp;
|
|
637
|
+
if (!stamp?.armed) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
641
|
+
const x = (e.clientX - rect.left) / rect.width;
|
|
642
|
+
const y = (e.clientY - rect.top) / rect.height;
|
|
643
|
+
stamp.onPlaced({ page, x, y, w: 0.22, h: 0.08 });
|
|
644
|
+
}
|
|
645
|
+
return /* @__PURE__ */ jsxs4("div", { className: "hv-doc", ref: containerRef, onWheel, children: [
|
|
646
|
+
!doc ? /* @__PURE__ */ jsx4("div", { className: "hv-loading", children: "Loading PDF\u2026" }) : null,
|
|
647
|
+
doc ? /* @__PURE__ */ jsx4(
|
|
648
|
+
"div",
|
|
649
|
+
{
|
|
650
|
+
className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages",
|
|
651
|
+
children: pagesToShow.map((p) => {
|
|
652
|
+
const c = rendered.get(p);
|
|
653
|
+
return /* @__PURE__ */ jsx4(
|
|
654
|
+
"div",
|
|
655
|
+
{
|
|
656
|
+
className: "hv-page",
|
|
657
|
+
style: { width: size.w, height: size.h },
|
|
658
|
+
onClick: (e) => clickPlace(e, p),
|
|
659
|
+
children: c ? /* @__PURE__ */ jsx4(
|
|
660
|
+
"canvas",
|
|
661
|
+
{
|
|
662
|
+
className: "hv-canvas",
|
|
663
|
+
width: c.width,
|
|
664
|
+
height: c.height,
|
|
665
|
+
ref: (node) => {
|
|
666
|
+
if (!node) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const ctx = node.getContext("2d");
|
|
670
|
+
if (ctx) {
|
|
671
|
+
ctx.drawImage(c, 0, 0);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
) : /* @__PURE__ */ jsx4("div", { className: "hv-loading", children: "Rendering\u2026" })
|
|
676
|
+
},
|
|
677
|
+
p
|
|
678
|
+
);
|
|
679
|
+
})
|
|
680
|
+
}
|
|
681
|
+
) : null
|
|
682
|
+
] });
|
|
683
|
+
}
|
|
684
|
+
|
|
735
685
|
// src/renderers/PptxRenderer.tsx
|
|
736
686
|
import { useEffect as useEffect5, useMemo as useMemo5, useState as useState5 } from "react";
|
|
737
687
|
import JSZip from "jszip";
|
|
738
|
-
import { jsx as
|
|
688
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
739
689
|
function decodeXml(s) {
|
|
740
690
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
741
691
|
}
|
|
@@ -748,43 +698,39 @@ function PptxRenderer(props) {
|
|
|
748
698
|
const [error, setError] = useState5(null);
|
|
749
699
|
const [loading, setLoading] = useState5(false);
|
|
750
700
|
useEffect5(() => {
|
|
751
|
-
let
|
|
701
|
+
let cancel = false;
|
|
752
702
|
setError(null);
|
|
753
703
|
setLoading(true);
|
|
754
704
|
(async () => {
|
|
755
705
|
setSlides([]);
|
|
756
706
|
setThumbs([]);
|
|
757
707
|
if (!props.arrayBuffer) {
|
|
758
|
-
|
|
759
|
-
setSlides([{ index: 1, text: "No content" }]);
|
|
760
|
-
setError("No PPTX data provided.");
|
|
708
|
+
setError("No PPTX source provided.");
|
|
761
709
|
setLoading(false);
|
|
762
710
|
return;
|
|
763
711
|
}
|
|
764
712
|
try {
|
|
765
713
|
const zip = await JSZip.loadAsync(props.arrayBuffer);
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
const out = [];
|
|
772
|
-
for (let i = 0; i < files.length; i++) {
|
|
773
|
-
const xml = await zip.file(files[i]).async("string");
|
|
774
|
-
out.push({ index: i + 1, text: extractText(xml) });
|
|
714
|
+
const slidePaths = Object.keys(zip.files).filter((p) => /^ppt\/slides\/slide\d+\.xml$/.test(p)).sort();
|
|
715
|
+
const slidesOut = [];
|
|
716
|
+
for (let i = 0; i < slidePaths.length; i++) {
|
|
717
|
+
const xml = await zip.files[slidePaths[i]].async("string");
|
|
718
|
+
slidesOut.push({ index: i + 1, text: extractText(xml) });
|
|
775
719
|
}
|
|
776
|
-
if (
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
setSlides(out.length ? out : [{ index: 1, text: "(empty)" }]);
|
|
780
|
-
setThumbs(
|
|
781
|
-
Array.from(
|
|
782
|
-
{ length: count },
|
|
783
|
-
(_, i) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgThumb(i + 1))}`
|
|
784
|
-
)
|
|
720
|
+
if (cancel) return;
|
|
721
|
+
setSlides(
|
|
722
|
+
slidesOut.length ? slidesOut : [{ index: 1, text: "(empty)" }]
|
|
785
723
|
);
|
|
724
|
+
props.onSlideCount(slidesOut.length || 1);
|
|
725
|
+
const thumbWidth = 56;
|
|
726
|
+
const thumbsArr = [];
|
|
727
|
+
for (let i = 0; i < (slidesOut.length || 1); i++) {
|
|
728
|
+
thumbsArr.push(
|
|
729
|
+
`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgThumb(i + 1))}`
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
setThumbs(thumbsArr);
|
|
786
733
|
} catch (e) {
|
|
787
|
-
props.onSlideCount(1);
|
|
788
734
|
setSlides([
|
|
789
735
|
{ index: 1, text: "Unable to render this .pptx in-browser." }
|
|
790
736
|
]);
|
|
@@ -797,42 +743,43 @@ function PptxRenderer(props) {
|
|
|
797
743
|
}
|
|
798
744
|
})();
|
|
799
745
|
return () => {
|
|
800
|
-
|
|
746
|
+
cancel = true;
|
|
801
747
|
};
|
|
802
748
|
}, [props.arrayBuffer]);
|
|
803
749
|
useEffect5(() => {
|
|
804
750
|
props.onThumbs(thumbs);
|
|
805
751
|
}, [thumbs]);
|
|
806
752
|
const pagesToShow = useMemo5(() => {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
];
|
|
812
|
-
|
|
753
|
+
const total = slides.length;
|
|
754
|
+
if (props.layout === "side-by-side" && total > 1) {
|
|
755
|
+
const left = Math.max(1, Math.min(props.currentPage, total));
|
|
756
|
+
const right = Math.max(1, Math.min(left + 1, total));
|
|
757
|
+
return left === right ? [left] : [left, right];
|
|
758
|
+
}
|
|
759
|
+
return [Math.max(1, Math.min(props.currentPage, total))];
|
|
813
760
|
}, [props.currentPage, props.layout, slides.length]);
|
|
814
|
-
return /* @__PURE__ */
|
|
815
|
-
loading && /* @__PURE__ */
|
|
816
|
-
error && /* @__PURE__ */
|
|
817
|
-
!loading && !error && (!slides || slides.length === 0) && /* @__PURE__ */
|
|
818
|
-
!error && slides && slides.length > 0 && /* @__PURE__ */
|
|
761
|
+
return /* @__PURE__ */ jsxs5("div", { className: "hv-doc", children: [
|
|
762
|
+
loading && /* @__PURE__ */ jsx5("div", { className: "hv-loading", children: "Loading PPTX\u2026" }),
|
|
763
|
+
error && /* @__PURE__ */ jsx5("div", { className: "hv-error", children: error }),
|
|
764
|
+
!loading && !error && (!slides || slides.length === 0) && /* @__PURE__ */ jsx5("div", { className: "hv-error", children: "No slides to display." }),
|
|
765
|
+
!error && slides && slides.length > 0 && /* @__PURE__ */ jsx5(
|
|
819
766
|
"div",
|
|
820
767
|
{
|
|
821
768
|
className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages",
|
|
822
769
|
children: pagesToShow.map((p) => {
|
|
823
770
|
const s = slides[p - 1];
|
|
824
|
-
return /* @__PURE__ */
|
|
771
|
+
return /* @__PURE__ */ jsxs5(
|
|
825
772
|
"div",
|
|
826
773
|
{
|
|
827
774
|
className: "hv-slide",
|
|
828
775
|
tabIndex: 0,
|
|
829
776
|
onFocus: () => props.onCurrentPageChange(p),
|
|
830
777
|
children: [
|
|
831
|
-
/* @__PURE__ */
|
|
778
|
+
/* @__PURE__ */ jsxs5("div", { className: "hv-slide-title", children: [
|
|
832
779
|
"Slide ",
|
|
833
780
|
p
|
|
834
781
|
] }),
|
|
835
|
-
/* @__PURE__ */
|
|
782
|
+
/* @__PURE__ */ jsx5("div", { className: "hv-slide-text", children: s?.text || "" })
|
|
836
783
|
]
|
|
837
784
|
},
|
|
838
785
|
p
|
|
@@ -846,6 +793,233 @@ function svgThumb(n) {
|
|
|
846
793
|
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>`;
|
|
847
794
|
}
|
|
848
795
|
|
|
796
|
+
// src/utils/locale.ts
|
|
797
|
+
var defaultLocale = {
|
|
798
|
+
"loading": "Loading\u2026",
|
|
799
|
+
"error.title": "Error",
|
|
800
|
+
"toolbar.layout.single": "Single page",
|
|
801
|
+
"toolbar.layout.two": "Side-by-side",
|
|
802
|
+
"toolbar.thumbs": "Thumbnails",
|
|
803
|
+
"toolbar.signatures": "Signatures",
|
|
804
|
+
"toolbar.sign": "Sign Document",
|
|
805
|
+
"toolbar.save": "Save",
|
|
806
|
+
"toolbar.exportPdf": "Export as PDF",
|
|
807
|
+
"thumbnails.title": "Thumbnails",
|
|
808
|
+
"thumbnails.page": "Page",
|
|
809
|
+
"signatures.title": "Signatures",
|
|
810
|
+
"signatures.empty": "No signatures",
|
|
811
|
+
"signatures.placeHint": "Click on the document to place the signature.",
|
|
812
|
+
"a11y.viewer": "Document viewer",
|
|
813
|
+
"a11y.ribbon": "Ribbon",
|
|
814
|
+
"a11y.editor": "Document editor"
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
// src/components/SignaturePanel.tsx
|
|
818
|
+
import React6 from "react";
|
|
819
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
820
|
+
function SignaturePanel(props) {
|
|
821
|
+
const title = props.locale["signatures.title"] ?? "Signatures";
|
|
822
|
+
const deduped = React6.useMemo(() => {
|
|
823
|
+
const seen = /* @__PURE__ */ new Set();
|
|
824
|
+
return props.signatures.filter((s) => {
|
|
825
|
+
const key = `${s.signedBy}|${s.dateSigned}|${s.signatureImageUrl}`;
|
|
826
|
+
if (seen.has(key)) return false;
|
|
827
|
+
seen.add(key);
|
|
828
|
+
return true;
|
|
829
|
+
});
|
|
830
|
+
}, [props.signatures]);
|
|
831
|
+
return /* @__PURE__ */ jsxs6(
|
|
832
|
+
"aside",
|
|
833
|
+
{
|
|
834
|
+
className: props.collapsed ? "hv-side hv-side--collapsed" : "hv-side",
|
|
835
|
+
"aria-label": title,
|
|
836
|
+
children: [
|
|
837
|
+
/* @__PURE__ */ jsxs6("div", { className: "hv-sidebar-header", children: [
|
|
838
|
+
/* @__PURE__ */ jsx6(
|
|
839
|
+
"button",
|
|
840
|
+
{
|
|
841
|
+
type: "button",
|
|
842
|
+
className: "hv-icon",
|
|
843
|
+
onClick: props.onToggle,
|
|
844
|
+
"aria-label": props.locale["toolbar.signatures"] ?? "Signatures",
|
|
845
|
+
children: /* @__PURE__ */ jsx6("span", { "aria-hidden": true, children: "\u270D" })
|
|
846
|
+
}
|
|
847
|
+
),
|
|
848
|
+
/* @__PURE__ */ jsx6("div", { className: "hv-sidebar-title", children: title })
|
|
849
|
+
] }),
|
|
850
|
+
/* @__PURE__ */ jsxs6("div", { className: "hv-sidebar-body", children: [
|
|
851
|
+
deduped.length === 0 && /* @__PURE__ */ jsx6("div", { className: "hv-signature-empty", "aria-live": "polite", children: props.locale["signatures.empty"] ?? "No signatures yet." }),
|
|
852
|
+
deduped.map((s, idx) => /* @__PURE__ */ jsxs6(
|
|
853
|
+
"div",
|
|
854
|
+
{
|
|
855
|
+
className: "hv-signature-card",
|
|
856
|
+
tabIndex: 0,
|
|
857
|
+
"aria-label": `Signature by ${s.signedBy}`,
|
|
858
|
+
children: [
|
|
859
|
+
/* @__PURE__ */ jsx6(
|
|
860
|
+
"img",
|
|
861
|
+
{
|
|
862
|
+
src: s.signatureImageUrl,
|
|
863
|
+
alt: props.locale["signatures.imgAlt"] ? props.locale["signatures.imgAlt"].replace(
|
|
864
|
+
"{name}",
|
|
865
|
+
s.signedBy
|
|
866
|
+
) : `Signature by ${s.signedBy}`,
|
|
867
|
+
className: "hv-signature-img"
|
|
868
|
+
}
|
|
869
|
+
),
|
|
870
|
+
/* @__PURE__ */ jsxs6("div", { className: "hv-signature-meta", children: [
|
|
871
|
+
/* @__PURE__ */ jsx6("div", { className: "hv-signature-name", children: s.signedBy }),
|
|
872
|
+
/* @__PURE__ */ jsx6("div", { className: "hv-signature-date", children: new Date(s.dateSigned).toLocaleString() }),
|
|
873
|
+
s.comment ? /* @__PURE__ */ jsx6("div", { className: "hv-signature-comment", children: s.comment }) : null
|
|
874
|
+
] })
|
|
875
|
+
]
|
|
876
|
+
},
|
|
877
|
+
`${s.signedBy}-${s.dateSigned}-${s.signatureImageUrl}`
|
|
878
|
+
))
|
|
879
|
+
] })
|
|
880
|
+
]
|
|
881
|
+
}
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/components/ThumbnailsSidebar.tsx
|
|
886
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
887
|
+
function ThumbnailsSidebar(props) {
|
|
888
|
+
const t = props.locale["thumbnails.title"] ?? "Thumbnails";
|
|
889
|
+
return /* @__PURE__ */ jsxs7(
|
|
890
|
+
"aside",
|
|
891
|
+
{
|
|
892
|
+
className: props.collapsed ? "hv-thumbs hv-thumbs--collapsed" : "hv-thumbs",
|
|
893
|
+
"aria-label": t,
|
|
894
|
+
children: [
|
|
895
|
+
/* @__PURE__ */ jsxs7("div", { className: "hv-thumbs-header", children: [
|
|
896
|
+
/* @__PURE__ */ jsx7(
|
|
897
|
+
"button",
|
|
898
|
+
{
|
|
899
|
+
type: "button",
|
|
900
|
+
className: "hv-thumbs-toggle",
|
|
901
|
+
onClick: props.onToggle,
|
|
902
|
+
"aria-label": props.collapsed ? props.locale["thumbnails.open"] ?? "Open thumbnails" : props.locale["thumbnails.close"] ?? "Close thumbnails",
|
|
903
|
+
children: /* @__PURE__ */ jsx7("span", { className: "hv-thumbs-toggle-icon", children: props.collapsed ? "\u25B8" : "\u25BE" })
|
|
904
|
+
}
|
|
905
|
+
),
|
|
906
|
+
!props.collapsed && /* @__PURE__ */ jsx7("div", { className: "hv-thumbs-title", children: t })
|
|
907
|
+
] }),
|
|
908
|
+
!props.collapsed && /* @__PURE__ */ jsx7("div", { className: "hv-thumbs-list", role: "list", children: props.thumbnails.map((th, idx) => {
|
|
909
|
+
const p = idx + 1;
|
|
910
|
+
const active = p === props.currentPage;
|
|
911
|
+
return /* @__PURE__ */ jsxs7(
|
|
912
|
+
"button",
|
|
913
|
+
{
|
|
914
|
+
type: "button",
|
|
915
|
+
role: "listitem",
|
|
916
|
+
className: active ? "hv-thumb hv-thumb--active" : "hv-thumb",
|
|
917
|
+
onClick: () => props.onSelectPage(p),
|
|
918
|
+
"aria-current": active ? "page" : void 0,
|
|
919
|
+
tabIndex: 0,
|
|
920
|
+
children: [
|
|
921
|
+
/* @__PURE__ */ jsx7("div", { className: "hv-thumb-img", "aria-hidden": true, children: th.dataUrl ? /* @__PURE__ */ jsx7("img", { src: th.dataUrl, alt: "" }) : /* @__PURE__ */ jsx7("div", { className: "hv-thumb-placeholder" }) }),
|
|
922
|
+
/* @__PURE__ */ jsx7("div", { className: "hv-thumb-label", children: th.label })
|
|
923
|
+
]
|
|
924
|
+
},
|
|
925
|
+
th.id
|
|
926
|
+
);
|
|
927
|
+
}) })
|
|
928
|
+
]
|
|
929
|
+
}
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/components/Toolbar.tsx
|
|
934
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
935
|
+
function Toolbar(props) {
|
|
936
|
+
const t = (k, fallback) => props.locale[k] ?? fallback;
|
|
937
|
+
return /* @__PURE__ */ jsxs8(
|
|
938
|
+
"div",
|
|
939
|
+
{
|
|
940
|
+
className: "hv-toolbar",
|
|
941
|
+
role: "toolbar",
|
|
942
|
+
"aria-label": t("a11y.toolbar", "Document toolbar"),
|
|
943
|
+
children: [
|
|
944
|
+
/* @__PURE__ */ jsxs8("div", { className: "hv-toolbar__left gap-2", children: [
|
|
945
|
+
/* @__PURE__ */ jsx8(
|
|
946
|
+
"button",
|
|
947
|
+
{
|
|
948
|
+
type: "button",
|
|
949
|
+
className: "hv-btn",
|
|
950
|
+
onClick: props.onToggleThumbnails,
|
|
951
|
+
"aria-pressed": props.showThumbnails,
|
|
952
|
+
children: t("toolbar.thumbs", "Thumbnails")
|
|
953
|
+
}
|
|
954
|
+
),
|
|
955
|
+
props.mode !== "create" && /* @__PURE__ */ jsx8(
|
|
956
|
+
"button",
|
|
957
|
+
{
|
|
958
|
+
type: "button",
|
|
959
|
+
className: "hv-btn",
|
|
960
|
+
onClick: props.onToggleSignatures,
|
|
961
|
+
"aria-pressed": props.showSignatures,
|
|
962
|
+
children: t("toolbar.signatures", "Signatures")
|
|
963
|
+
}
|
|
964
|
+
),
|
|
965
|
+
/* @__PURE__ */ jsx8("span", { className: "hv-sep" }),
|
|
966
|
+
/* @__PURE__ */ jsx8(
|
|
967
|
+
"button",
|
|
968
|
+
{
|
|
969
|
+
type: "button",
|
|
970
|
+
className: props.layout === "single" ? "hv-btn hv-btn--active" : "hv-btn",
|
|
971
|
+
onClick: () => props.onChangeLayout("single"),
|
|
972
|
+
children: t("toolbar.layout.single", "Single")
|
|
973
|
+
}
|
|
974
|
+
),
|
|
975
|
+
/* @__PURE__ */ jsx8(
|
|
976
|
+
"button",
|
|
977
|
+
{
|
|
978
|
+
type: "button",
|
|
979
|
+
className: props.layout === "side-by-side" ? "hv-btn hv-btn--active" : "hv-btn",
|
|
980
|
+
onClick: () => props.onChangeLayout("side-by-side"),
|
|
981
|
+
children: t("toolbar.layout.two", "Two")
|
|
982
|
+
}
|
|
983
|
+
)
|
|
984
|
+
] }),
|
|
985
|
+
/* @__PURE__ */ jsxs8("div", { className: "hv-toolbar__right", children: [
|
|
986
|
+
props.showHeaderFooterToggle && /* @__PURE__ */ jsxs8("label", { className: "hv-toggle", children: [
|
|
987
|
+
/* @__PURE__ */ jsx8(
|
|
988
|
+
"input",
|
|
989
|
+
{
|
|
990
|
+
type: "checkbox",
|
|
991
|
+
checked: props.headerFooterEnabled,
|
|
992
|
+
onChange: props.onToggleHeaderFooter
|
|
993
|
+
}
|
|
994
|
+
),
|
|
995
|
+
/* @__PURE__ */ jsx8("span", { children: t("toolbar.letterhead", "Letterhead") })
|
|
996
|
+
] }),
|
|
997
|
+
props.allowSigning && /* @__PURE__ */ jsx8(
|
|
998
|
+
"button",
|
|
999
|
+
{
|
|
1000
|
+
type: "button",
|
|
1001
|
+
className: "hv-btn hv-btn--primary",
|
|
1002
|
+
onClick: props.onSign,
|
|
1003
|
+
disabled: props.signingDisabled,
|
|
1004
|
+
children: t("toolbar.sign", "Sign Document")
|
|
1005
|
+
}
|
|
1006
|
+
),
|
|
1007
|
+
props.canExportPdf && /* @__PURE__ */ jsx8("button", { type: "button", className: "hv-btn", onClick: props.onExportPdf, children: t("toolbar.exportPdf", "Export as PDF") }),
|
|
1008
|
+
props.canSave && /* @__PURE__ */ jsx8(
|
|
1009
|
+
"button",
|
|
1010
|
+
{
|
|
1011
|
+
type: "button",
|
|
1012
|
+
className: "hv-btn hv-btn--primary",
|
|
1013
|
+
onClick: props.onSave,
|
|
1014
|
+
children: t("toolbar.save", "Save")
|
|
1015
|
+
}
|
|
1016
|
+
)
|
|
1017
|
+
] })
|
|
1018
|
+
]
|
|
1019
|
+
}
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
849
1023
|
// src/components/DocumentViewer.tsx
|
|
850
1024
|
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
851
1025
|
function DocumentViewer(props) {
|
|
@@ -891,10 +1065,14 @@ function DocumentViewer(props) {
|
|
|
891
1065
|
fileName: props.fileName,
|
|
892
1066
|
fileType: props.fileType
|
|
893
1067
|
});
|
|
894
|
-
if (cancelled)
|
|
1068
|
+
if (cancelled) {
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
895
1071
|
setResolved({ fileType: res.fileType, fileName: res.fileName, url: res.url, arrayBuffer: res.arrayBuffer });
|
|
896
1072
|
} catch (e) {
|
|
897
|
-
if (cancelled)
|
|
1073
|
+
if (cancelled) {
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
898
1076
|
setError(e instanceof Error ? e.message : String(e));
|
|
899
1077
|
}
|
|
900
1078
|
})();
|
|
@@ -911,7 +1089,9 @@ function DocumentViewer(props) {
|
|
|
911
1089
|
}));
|
|
912
1090
|
}, [pageCount, thumbs, locale]);
|
|
913
1091
|
async function handleSignRequest() {
|
|
914
|
-
if (!allowSigning || signingBusy || !props.onSignRequest)
|
|
1092
|
+
if (!allowSigning || signingBusy || !props.onSignRequest) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
915
1095
|
setSigningBusy(true);
|
|
916
1096
|
try {
|
|
917
1097
|
const sig = await props.onSignRequest();
|
|
@@ -922,7 +1102,9 @@ function DocumentViewer(props) {
|
|
|
922
1102
|
}
|
|
923
1103
|
}
|
|
924
1104
|
function placeSignature(p) {
|
|
925
|
-
if (!armedSignatureUrl)
|
|
1105
|
+
if (!armedSignatureUrl) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
926
1108
|
setSigPlacements((prev) => [...prev, { ...p, signatureImageUrl: armedSignatureUrl }]);
|
|
927
1109
|
setArmedSignatureUrl(null);
|
|
928
1110
|
}
|
|
@@ -931,7 +1113,9 @@ function DocumentViewer(props) {
|
|
|
931
1113
|
await editorRef.current.save(!!exportPdf);
|
|
932
1114
|
return;
|
|
933
1115
|
}
|
|
934
|
-
if (!resolved?.arrayBuffer)
|
|
1116
|
+
if (!resolved?.arrayBuffer) {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
935
1119
|
const b64 = arrayBufferToBase642(resolved.arrayBuffer);
|
|
936
1120
|
props.onSave?.(b64, { fileName: resolved.fileName, fileType: resolved.fileType, annotations: { sigPlacements } });
|
|
937
1121
|
}
|
|
@@ -966,7 +1150,7 @@ function DocumentViewer(props) {
|
|
|
966
1150
|
/* @__PURE__ */ jsx9("div", { className: "hv-error-title", children: locale["error.title"] ?? "Error" }),
|
|
967
1151
|
/* @__PURE__ */ jsx9("div", { className: "hv-error-body", children: error })
|
|
968
1152
|
] }) : null,
|
|
969
|
-
!resolved && !error ? /* @__PURE__ */ jsx9("div", { className: "hv-loading", "aria-busy": "true", children: locale
|
|
1153
|
+
!resolved && !error ? /* @__PURE__ */ jsx9("div", { className: "hv-loading", "aria-busy": "true", children: locale.loading ?? "Loading\u2026" }) : null,
|
|
970
1154
|
resolved ? /* @__PURE__ */ jsxs9("div", { className: "hv-shell", children: [
|
|
971
1155
|
mode !== "create" ? /* @__PURE__ */ jsx9(
|
|
972
1156
|
ThumbnailsSidebar,
|