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