@xom11/whiteboard 0.9.1 → 0.10.0
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/{chunk-KEYZ5EZT.mjs → chunk-G7FR3AIV.mjs} +44 -5
- package/dist/chunk-G7FR3AIV.mjs.map +1 -0
- package/dist/{chunk-DU3RHKT5.mjs → chunk-PDKKDZ4H.mjs} +4 -4
- package/dist/{chunk-DU3RHKT5.mjs.map → chunk-PDKKDZ4H.mjs.map} +1 -1
- package/dist/chunk-PWIMZIB6.mjs +62 -0
- package/dist/chunk-PWIMZIB6.mjs.map +1 -0
- package/dist/chunk-WQOABS6N.mjs +197 -0
- package/dist/chunk-WQOABS6N.mjs.map +1 -0
- package/dist/geometry-2d.js +96 -12
- package/dist/geometry-2d.js.map +1 -1
- package/dist/geometry-2d.mjs +2 -2
- package/dist/geometry-3d.js +152 -93
- package/dist/geometry-3d.js.map +1 -1
- package/dist/geometry-3d.mjs +2 -2
- package/dist/{host-VDNAJMLC.mjs → host-DJETSFCG.mjs} +56 -12
- package/dist/host-DJETSFCG.mjs.map +1 -0
- package/dist/{host-PIIDSMVE.mjs → host-N6ACNJKI.mjs} +51 -12
- package/dist/host-N6ACNJKI.mjs.map +1 -0
- package/dist/index.d.mts +127 -1
- package/dist/index.d.ts +127 -1
- package/dist/index.js +1265 -174
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +991 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -1
- package/dist/chunk-DU2NFHRR.mjs +0 -103
- package/dist/chunk-DU2NFHRR.mjs.map +0 -1
- package/dist/chunk-IUVV52HO.mjs +0 -144
- package/dist/chunk-IUVV52HO.mjs.map +0 -1
- package/dist/chunk-KEYZ5EZT.mjs.map +0 -1
- package/dist/host-PIIDSMVE.mjs.map +0 -1
- package/dist/host-VDNAJMLC.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import './index.css';
|
|
3
|
-
import { geometryStamp } from './chunk-
|
|
4
|
-
export { geometryStamp } from './chunk-
|
|
5
|
-
import { geometry3dStamp } from './chunk-
|
|
6
|
-
export { geometry3dStamp } from './chunk-
|
|
3
|
+
import { geometryStamp } from './chunk-PDKKDZ4H.mjs';
|
|
4
|
+
export { geometryStamp } from './chunk-PDKKDZ4H.mjs';
|
|
5
|
+
import { geometry3dStamp } from './chunk-PWIMZIB6.mjs';
|
|
6
|
+
export { geometry3dStamp } from './chunk-PWIMZIB6.mjs';
|
|
7
7
|
import { latexStamp } from './chunk-7P7SQFOW.mjs';
|
|
8
8
|
export { latexStamp } from './chunk-7P7SQFOW.mjs';
|
|
9
9
|
import { graph2dStamp } from './chunk-ZVN356JZ.mjs';
|
|
10
10
|
export { graph2dStamp } from './chunk-ZVN356JZ.mjs';
|
|
11
11
|
export { isGraph2DCustomData } from './chunk-74VEEZBV.mjs';
|
|
12
|
-
export { isGeometryCustomData } from './chunk-
|
|
12
|
+
export { isGeometryCustomData } from './chunk-G7FR3AIV.mjs';
|
|
13
13
|
export { isLatexCustomData } from './chunk-X5R72SSJ.mjs';
|
|
14
|
-
export { isGeometry3DCustomData } from './chunk-
|
|
14
|
+
export { isGeometry3DCustomData } from './chunk-WQOABS6N.mjs';
|
|
15
15
|
import './chunk-HTBLO5JO.mjs';
|
|
16
16
|
import './chunk-C6SCVOMC.mjs';
|
|
17
17
|
import './chunk-BJTO5JO5.mjs';
|
|
@@ -147,20 +147,28 @@ function ToolbarInjector({
|
|
|
147
147
|
};
|
|
148
148
|
return createPortal(
|
|
149
149
|
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
150
|
-
stamps.map((stamp) =>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
150
|
+
stamps.map((stamp) => {
|
|
151
|
+
const { displayLabel, shortcut } = splitTitleAndShortcut(
|
|
152
|
+
stamp.toolbarTitle,
|
|
153
|
+
stamp.toolbarLabel
|
|
154
|
+
);
|
|
155
|
+
return /* @__PURE__ */ jsx(
|
|
156
|
+
StampMenuItem,
|
|
157
|
+
{
|
|
158
|
+
icon: stamp.toolbarIcon,
|
|
159
|
+
label: displayLabel,
|
|
160
|
+
ariaLabel: stamp.toolbarTitle,
|
|
161
|
+
shortcut,
|
|
162
|
+
active: activeStampKind === stamp.kind,
|
|
163
|
+
onClick: () => {
|
|
164
|
+
onToggle(stamp.kind);
|
|
165
|
+
closePopover();
|
|
166
|
+
},
|
|
167
|
+
dataTestId: stamp.toolbarTestId
|
|
159
168
|
},
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
)),
|
|
169
|
+
stamp.kind
|
|
170
|
+
);
|
|
171
|
+
}),
|
|
164
172
|
/* @__PURE__ */ jsx(
|
|
165
173
|
"div",
|
|
166
174
|
{
|
|
@@ -176,7 +184,22 @@ function ToolbarInjector({
|
|
|
176
184
|
menuMount
|
|
177
185
|
);
|
|
178
186
|
}
|
|
179
|
-
function
|
|
187
|
+
function splitTitleAndShortcut(title, fallbackShortcut) {
|
|
188
|
+
const match = title.match(/^(.*?)\s*\(([^()]+)\)\s*$/);
|
|
189
|
+
if (match) {
|
|
190
|
+
return { displayLabel: match[1].trim(), shortcut: match[2].trim() };
|
|
191
|
+
}
|
|
192
|
+
return { displayLabel: title, shortcut: fallbackShortcut };
|
|
193
|
+
}
|
|
194
|
+
function StampMenuItem({
|
|
195
|
+
icon,
|
|
196
|
+
label,
|
|
197
|
+
ariaLabel,
|
|
198
|
+
shortcut,
|
|
199
|
+
active,
|
|
200
|
+
onClick,
|
|
201
|
+
dataTestId
|
|
202
|
+
}) {
|
|
180
203
|
const className = [
|
|
181
204
|
"dropdown-menu-item",
|
|
182
205
|
"dropdown-menu-item-base",
|
|
@@ -187,39 +210,15 @@ function StampMenuItem({ icon, label, active, onClick, dataTestId }) {
|
|
|
187
210
|
{
|
|
188
211
|
type: "button",
|
|
189
212
|
onClick,
|
|
190
|
-
|
|
213
|
+
title: ariaLabel,
|
|
214
|
+
"aria-label": ariaLabel,
|
|
191
215
|
"aria-pressed": active,
|
|
192
216
|
"data-testid": dataTestId,
|
|
193
217
|
className,
|
|
194
|
-
style: {
|
|
195
|
-
display: "flex",
|
|
196
|
-
alignItems: "center",
|
|
197
|
-
columnGap: "0.625rem",
|
|
198
|
-
width: "100%",
|
|
199
|
-
boxSizing: "border-box",
|
|
200
|
-
background: "transparent",
|
|
201
|
-
border: "1px solid transparent",
|
|
202
|
-
cursor: "pointer",
|
|
203
|
-
fontFamily: "inherit",
|
|
204
|
-
fontSize: "0.875rem",
|
|
205
|
-
color: "var(--color-on-surface)"
|
|
206
|
-
},
|
|
207
218
|
children: [
|
|
208
|
-
/* @__PURE__ */ jsx(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
"aria-hidden": "true",
|
|
212
|
-
style: {
|
|
213
|
-
display: "inline-flex",
|
|
214
|
-
alignItems: "center",
|
|
215
|
-
justifyContent: "center",
|
|
216
|
-
width: "1rem",
|
|
217
|
-
height: "1rem"
|
|
218
|
-
},
|
|
219
|
-
children: icon
|
|
220
|
-
}
|
|
221
|
-
),
|
|
222
|
-
/* @__PURE__ */ jsx("span", { children: label })
|
|
219
|
+
/* @__PURE__ */ jsx("div", { className: "dropdown-menu-item__icon", "aria-hidden": "true", children: icon }),
|
|
220
|
+
/* @__PURE__ */ jsx("div", { className: "dropdown-menu-item__text", children: label }),
|
|
221
|
+
shortcut ? /* @__PURE__ */ jsx("div", { className: "dropdown-menu-item__shortcut", children: shortcut }) : null
|
|
223
222
|
]
|
|
224
223
|
}
|
|
225
224
|
);
|
|
@@ -253,6 +252,843 @@ function useShortcuts({
|
|
|
253
252
|
return () => window.removeEventListener("keydown", handler, { capture: true });
|
|
254
253
|
}, [enabled, onToggle, stamps]);
|
|
255
254
|
}
|
|
255
|
+
var WRAPPER_ID = "pdf-import-portal-wrapper";
|
|
256
|
+
var POPOVER_SELECTOR2 = ".App-toolbar__extra-tools-dropdown .dropdown-menu-container";
|
|
257
|
+
function PdfImporterButton({ enabled, onPick }) {
|
|
258
|
+
const [mount, setMount] = useState(null);
|
|
259
|
+
const mountRef = useRef(null);
|
|
260
|
+
const inputRef = useRef(null);
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (!enabled) {
|
|
263
|
+
mountRef.current = null;
|
|
264
|
+
setMount(null);
|
|
265
|
+
document.getElementById(WRAPPER_ID)?.remove();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
let cancelled = false;
|
|
269
|
+
let observer = null;
|
|
270
|
+
let rafId = null;
|
|
271
|
+
let observedRoot = null;
|
|
272
|
+
const apply = (next) => {
|
|
273
|
+
if (cancelled || mountRef.current === next) return;
|
|
274
|
+
mountRef.current = next;
|
|
275
|
+
queueMicrotask(() => {
|
|
276
|
+
if (!cancelled) setMount(next);
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
const findMenu = () => {
|
|
280
|
+
if (cancelled) return;
|
|
281
|
+
const container = document.querySelector(POPOVER_SELECTOR2);
|
|
282
|
+
if (!container) {
|
|
283
|
+
apply(null);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
let wrapper = container.querySelector("#" + WRAPPER_ID);
|
|
287
|
+
if (!wrapper) {
|
|
288
|
+
wrapper = document.createElement("div");
|
|
289
|
+
wrapper.id = WRAPPER_ID;
|
|
290
|
+
wrapper.setAttribute("data-pdf-import", "true");
|
|
291
|
+
wrapper.style.display = "contents";
|
|
292
|
+
container.appendChild(wrapper);
|
|
293
|
+
}
|
|
294
|
+
apply(wrapper);
|
|
295
|
+
};
|
|
296
|
+
const attachObserver = () => {
|
|
297
|
+
if (cancelled) return;
|
|
298
|
+
const excalidraw = document.querySelector(".excalidraw");
|
|
299
|
+
const nextRoot = excalidraw ?? document.body;
|
|
300
|
+
if (observedRoot === nextRoot) return;
|
|
301
|
+
observer?.disconnect();
|
|
302
|
+
observedRoot = nextRoot;
|
|
303
|
+
observer = new MutationObserver(onMutation);
|
|
304
|
+
observer.observe(nextRoot, { childList: true, subtree: true });
|
|
305
|
+
};
|
|
306
|
+
const onMutation = () => {
|
|
307
|
+
if (rafId != null) return;
|
|
308
|
+
rafId = requestAnimationFrame(() => {
|
|
309
|
+
rafId = null;
|
|
310
|
+
if (cancelled) return;
|
|
311
|
+
if (observedRoot !== document.querySelector(".excalidraw")) {
|
|
312
|
+
attachObserver();
|
|
313
|
+
}
|
|
314
|
+
findMenu();
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
findMenu();
|
|
318
|
+
attachObserver();
|
|
319
|
+
return () => {
|
|
320
|
+
cancelled = true;
|
|
321
|
+
if (rafId != null) cancelAnimationFrame(rafId);
|
|
322
|
+
observer?.disconnect();
|
|
323
|
+
document.getElementById(WRAPPER_ID)?.remove();
|
|
324
|
+
};
|
|
325
|
+
}, [enabled]);
|
|
326
|
+
const closePopover = () => {
|
|
327
|
+
const trigger = document.querySelector(
|
|
328
|
+
".App-toolbar__extra-tools-trigger"
|
|
329
|
+
);
|
|
330
|
+
trigger?.click();
|
|
331
|
+
};
|
|
332
|
+
const handleClick = () => {
|
|
333
|
+
inputRef.current?.click();
|
|
334
|
+
};
|
|
335
|
+
const handleFileChange = (e) => {
|
|
336
|
+
const file = e.target.files?.[0];
|
|
337
|
+
if (file) onPick(file);
|
|
338
|
+
e.target.value = "";
|
|
339
|
+
closePopover();
|
|
340
|
+
};
|
|
341
|
+
if (!enabled || !mount) {
|
|
342
|
+
return /* @__PURE__ */ jsx(
|
|
343
|
+
"input",
|
|
344
|
+
{
|
|
345
|
+
ref: inputRef,
|
|
346
|
+
type: "file",
|
|
347
|
+
accept: "application/pdf,.pdf",
|
|
348
|
+
style: { display: "none" },
|
|
349
|
+
onChange: handleFileChange
|
|
350
|
+
}
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
354
|
+
/* @__PURE__ */ jsx(
|
|
355
|
+
"input",
|
|
356
|
+
{
|
|
357
|
+
ref: inputRef,
|
|
358
|
+
type: "file",
|
|
359
|
+
accept: "application/pdf,.pdf",
|
|
360
|
+
style: { display: "none" },
|
|
361
|
+
onChange: handleFileChange
|
|
362
|
+
}
|
|
363
|
+
),
|
|
364
|
+
createPortal(
|
|
365
|
+
/* @__PURE__ */ jsxs(
|
|
366
|
+
"button",
|
|
367
|
+
{
|
|
368
|
+
type: "button",
|
|
369
|
+
onClick: handleClick,
|
|
370
|
+
title: "Ch\xE8n PDF (P)",
|
|
371
|
+
"aria-label": "Ch\xE8n PDF",
|
|
372
|
+
"data-testid": "pdf-import-button",
|
|
373
|
+
className: "dropdown-menu-item dropdown-menu-item-base",
|
|
374
|
+
children: [
|
|
375
|
+
/* @__PURE__ */ jsx("div", { className: "dropdown-menu-item__icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx(PdfIcon, {}) }),
|
|
376
|
+
/* @__PURE__ */ jsx("div", { className: "dropdown-menu-item__text", children: "Ch\xE8n PDF" }),
|
|
377
|
+
/* @__PURE__ */ jsx("div", { className: "dropdown-menu-item__shortcut", children: "P" })
|
|
378
|
+
]
|
|
379
|
+
}
|
|
380
|
+
),
|
|
381
|
+
mount
|
|
382
|
+
)
|
|
383
|
+
] });
|
|
384
|
+
}
|
|
385
|
+
function PdfIcon() {
|
|
386
|
+
return /* @__PURE__ */ jsxs(
|
|
387
|
+
"svg",
|
|
388
|
+
{
|
|
389
|
+
width: "18",
|
|
390
|
+
height: "18",
|
|
391
|
+
viewBox: "0 0 24 24",
|
|
392
|
+
fill: "none",
|
|
393
|
+
stroke: "currentColor",
|
|
394
|
+
strokeWidth: "1.6",
|
|
395
|
+
strokeLinecap: "round",
|
|
396
|
+
strokeLinejoin: "round",
|
|
397
|
+
"aria-hidden": "true",
|
|
398
|
+
children: [
|
|
399
|
+
/* @__PURE__ */ jsx("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" }),
|
|
400
|
+
/* @__PURE__ */ jsx("path", { d: "M14 3v5h5" }),
|
|
401
|
+
/* @__PURE__ */ jsx("text", { x: "7.5", y: "17", fontSize: "6", fontFamily: "sans-serif", fontWeight: "700", stroke: "none", fill: "currentColor", children: "PDF" })
|
|
402
|
+
]
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/pdf/parseRange.ts
|
|
408
|
+
function parsePageRange(input, totalPages) {
|
|
409
|
+
if (!Number.isInteger(totalPages) || totalPages <= 0) {
|
|
410
|
+
throw new Error("S\u1ED1 trang ph\u1EA3i l\xE0 s\u1ED1 nguy\xEAn d\u01B0\u01A1ng.");
|
|
411
|
+
}
|
|
412
|
+
const trimmed = input.trim();
|
|
413
|
+
if (trimmed === "") return [];
|
|
414
|
+
const tokens = trimmed.split(/[,\s]+/).map((t) => t.trim()).filter((t) => t.length > 0);
|
|
415
|
+
const set = /* @__PURE__ */ new Set();
|
|
416
|
+
for (const token of tokens) {
|
|
417
|
+
if (token.includes("-")) {
|
|
418
|
+
const parts = token.split("-");
|
|
419
|
+
if (parts.length !== 2) {
|
|
420
|
+
throw new Error(`Kho\u1EA3ng trang kh\xF4ng h\u1EE3p l\u1EC7: "${token}".`);
|
|
421
|
+
}
|
|
422
|
+
const start = parseStrictInt(parts[0]);
|
|
423
|
+
const end = parseStrictInt(parts[1]);
|
|
424
|
+
if (start === null || end === null) {
|
|
425
|
+
throw new Error(`Kho\u1EA3ng trang kh\xF4ng h\u1EE3p l\u1EC7: "${token}".`);
|
|
426
|
+
}
|
|
427
|
+
if (start > end) {
|
|
428
|
+
throw new Error(`Kho\u1EA3ng trang ng\u01B0\u1EE3c: "${token}" (\u0111\u1EA7u > cu\u1ED1i).`);
|
|
429
|
+
}
|
|
430
|
+
if (start < 1 || end > totalPages) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`Kho\u1EA3ng trang v\u01B0\u1EE3t gi\u1EDBi h\u1EA1n: "${token}". PDF c\xF3 ${totalPages} trang.`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
for (let i = start; i <= end; i++) set.add(i);
|
|
436
|
+
} else {
|
|
437
|
+
const n = parseStrictInt(token);
|
|
438
|
+
if (n === null) {
|
|
439
|
+
throw new Error(`S\u1ED1 trang kh\xF4ng h\u1EE3p l\u1EC7: "${token}".`);
|
|
440
|
+
}
|
|
441
|
+
if (n < 1 || n > totalPages) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
`S\u1ED1 trang v\u01B0\u1EE3t gi\u1EDBi h\u1EA1n: ${n}. PDF c\xF3 ${totalPages} trang.`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
set.add(n);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return Array.from(set).sort((a, b) => a - b);
|
|
450
|
+
}
|
|
451
|
+
function parseStrictInt(s) {
|
|
452
|
+
if (!/^-?\d+$/.test(s)) return null;
|
|
453
|
+
const n = Number(s);
|
|
454
|
+
return Number.isInteger(n) ? n : null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/pdf/rasterize.ts
|
|
458
|
+
var workerSrcOverride = null;
|
|
459
|
+
var pdfjsCache = null;
|
|
460
|
+
function configurePdfWorker(workerSrc) {
|
|
461
|
+
workerSrcOverride = workerSrc;
|
|
462
|
+
if (pdfjsCache) {
|
|
463
|
+
pdfjsCache.GlobalWorkerOptions.workerSrc = workerSrc;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async function loadPdfjs() {
|
|
467
|
+
if (pdfjsCache) return pdfjsCache;
|
|
468
|
+
const mod = await import('pdfjs-dist');
|
|
469
|
+
const workerSrc = workerSrcOverride ?? `https://cdn.jsdelivr.net/npm/pdfjs-dist@${mod.version}/build/pdf.worker.min.mjs`;
|
|
470
|
+
mod.GlobalWorkerOptions.workerSrc = workerSrc;
|
|
471
|
+
pdfjsCache = mod;
|
|
472
|
+
return mod;
|
|
473
|
+
}
|
|
474
|
+
async function loadPdfDocument(source) {
|
|
475
|
+
const pdfjs = await loadPdfjs();
|
|
476
|
+
const data = source instanceof ArrayBuffer ? source : await source.arrayBuffer();
|
|
477
|
+
const task = pdfjs.getDocument({ data: new Uint8Array(data) });
|
|
478
|
+
return task.promise;
|
|
479
|
+
}
|
|
480
|
+
async function closePdfDocument(doc) {
|
|
481
|
+
try {
|
|
482
|
+
await doc.cleanup();
|
|
483
|
+
await doc.destroy();
|
|
484
|
+
} catch {
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async function rasterizePdf(doc, options = {}) {
|
|
488
|
+
const scale = options.scale ?? 2;
|
|
489
|
+
const total = doc.numPages;
|
|
490
|
+
const pages = options.pages ?? Array.from({ length: total }, (_, i) => i + 1);
|
|
491
|
+
const signal = options.signal;
|
|
492
|
+
const result = [];
|
|
493
|
+
for (let i = 0; i < pages.length; i++) {
|
|
494
|
+
if (signal?.aborted) {
|
|
495
|
+
throw new DOMException("Rasterize PDF b\u1ECB hu\u1EF7.", "AbortError");
|
|
496
|
+
}
|
|
497
|
+
const pageNum = pages[i];
|
|
498
|
+
const page = await doc.getPage(pageNum);
|
|
499
|
+
try {
|
|
500
|
+
const rendered = await renderPageToPng(page, scale);
|
|
501
|
+
result.push({ pageNumber: pageNum, mimeType: "image/png", ...rendered });
|
|
502
|
+
} finally {
|
|
503
|
+
page.cleanup();
|
|
504
|
+
}
|
|
505
|
+
options.onProgress?.(i + 1, pages.length);
|
|
506
|
+
}
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
async function renderPageToPng(page, scale) {
|
|
510
|
+
const viewport = page.getViewport({ scale });
|
|
511
|
+
const width = Math.ceil(viewport.width);
|
|
512
|
+
const height = Math.ceil(viewport.height);
|
|
513
|
+
const canvas = document.createElement("canvas");
|
|
514
|
+
canvas.width = width;
|
|
515
|
+
canvas.height = height;
|
|
516
|
+
const ctx = canvas.getContext("2d");
|
|
517
|
+
if (!ctx) throw new Error("Kh\xF4ng l\u1EA5y \u0111\u01B0\u1EE3c 2D context c\u1EE7a canvas.");
|
|
518
|
+
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
|
|
519
|
+
const dataURL = canvas.toDataURL("image/png");
|
|
520
|
+
return { dataURL, width, height };
|
|
521
|
+
}
|
|
522
|
+
async function renderPageThumbnail(page, scale = 0.3, quality = 0.7) {
|
|
523
|
+
const viewport = page.getViewport({ scale });
|
|
524
|
+
const width = Math.ceil(viewport.width);
|
|
525
|
+
const height = Math.ceil(viewport.height);
|
|
526
|
+
const canvas = document.createElement("canvas");
|
|
527
|
+
canvas.width = width;
|
|
528
|
+
canvas.height = height;
|
|
529
|
+
const ctx = canvas.getContext("2d");
|
|
530
|
+
if (!ctx) throw new Error("Kh\xF4ng l\u1EA5y \u0111\u01B0\u1EE3c 2D context c\u1EE7a canvas.");
|
|
531
|
+
ctx.fillStyle = "#ffffff";
|
|
532
|
+
ctx.fillRect(0, 0, width, height);
|
|
533
|
+
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
|
|
534
|
+
const dataURL = canvas.toDataURL("image/jpeg", quality);
|
|
535
|
+
return { dataURL, width, height };
|
|
536
|
+
}
|
|
537
|
+
async function renderAllThumbnails(doc, onEach, options = {}) {
|
|
538
|
+
const total = doc.numPages;
|
|
539
|
+
const scale = options.scale ?? 0.3;
|
|
540
|
+
const quality = options.quality ?? 0.7;
|
|
541
|
+
const concurrency = Math.max(1, options.concurrency ?? 3);
|
|
542
|
+
const signal = options.signal;
|
|
543
|
+
let next = 1;
|
|
544
|
+
async function worker() {
|
|
545
|
+
while (true) {
|
|
546
|
+
if (signal?.aborted) return;
|
|
547
|
+
const pageNum = next++;
|
|
548
|
+
if (pageNum > total) return;
|
|
549
|
+
const page = await doc.getPage(pageNum);
|
|
550
|
+
try {
|
|
551
|
+
if (signal?.aborted) return;
|
|
552
|
+
const { dataURL, width, height } = await renderPageThumbnail(page, scale, quality);
|
|
553
|
+
if (signal?.aborted) return;
|
|
554
|
+
onEach(pageNum, dataURL, width, height);
|
|
555
|
+
} finally {
|
|
556
|
+
page.cleanup();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
await Promise.all(
|
|
561
|
+
Array.from({ length: Math.min(concurrency, total) }, () => worker())
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
function serializeSelection(pages) {
|
|
565
|
+
if (pages.length === 0) return "";
|
|
566
|
+
const sorted = [...pages].sort((a, b) => a - b);
|
|
567
|
+
const groups = [];
|
|
568
|
+
let start = sorted[0];
|
|
569
|
+
let prev = start;
|
|
570
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
571
|
+
const n = sorted[i];
|
|
572
|
+
if (n === prev + 1) {
|
|
573
|
+
prev = n;
|
|
574
|
+
} else {
|
|
575
|
+
groups.push(start === prev ? `${start}` : `${start}-${prev}`);
|
|
576
|
+
start = n;
|
|
577
|
+
prev = n;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
groups.push(start === prev ? `${start}` : `${start}-${prev}`);
|
|
581
|
+
return groups.join(",");
|
|
582
|
+
}
|
|
583
|
+
function PageRangeDialog({ doc, fileName, onConfirm, onCancel }) {
|
|
584
|
+
const totalPages = doc.numPages;
|
|
585
|
+
const defaultPages = useMemo(
|
|
586
|
+
() => Array.from({ length: totalPages }, (_, i) => i + 1),
|
|
587
|
+
[totalPages]
|
|
588
|
+
);
|
|
589
|
+
const [selectedSet, setSelectedSet] = useState(
|
|
590
|
+
() => new Set(defaultPages)
|
|
591
|
+
);
|
|
592
|
+
const [inputValue, setInputValue] = useState(serializeSelection(defaultPages));
|
|
593
|
+
const [inputError, setInputError] = useState(null);
|
|
594
|
+
const [thumbs, setThumbs] = useState({});
|
|
595
|
+
const [thumbProgress, setThumbProgress] = useState(0);
|
|
596
|
+
const inputRef = useRef(null);
|
|
597
|
+
useEffect(() => {
|
|
598
|
+
const ctrl = new AbortController();
|
|
599
|
+
void renderAllThumbnails(
|
|
600
|
+
doc,
|
|
601
|
+
(pageNum, dataURL, width, height) => {
|
|
602
|
+
setThumbs((prev) => ({ ...prev, [pageNum]: { dataURL, width, height } }));
|
|
603
|
+
setThumbProgress((prev) => prev + 1);
|
|
604
|
+
},
|
|
605
|
+
{ scale: 0.3, quality: 0.7, concurrency: 3, signal: ctrl.signal }
|
|
606
|
+
).catch((err) => {
|
|
607
|
+
if (ctrl.signal.aborted) return;
|
|
608
|
+
console.warn("[PageRangeDialog] render thumbnails l\u1ED7i:", err);
|
|
609
|
+
});
|
|
610
|
+
return () => ctrl.abort();
|
|
611
|
+
}, [doc]);
|
|
612
|
+
useEffect(() => {
|
|
613
|
+
const onKey = (e) => {
|
|
614
|
+
if (e.key === "Escape") {
|
|
615
|
+
e.preventDefault();
|
|
616
|
+
e.stopPropagation();
|
|
617
|
+
onCancel();
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
window.addEventListener("keydown", onKey, { capture: true });
|
|
621
|
+
return () => window.removeEventListener("keydown", onKey, { capture: true });
|
|
622
|
+
}, [onCancel]);
|
|
623
|
+
const handleInputChange = (next) => {
|
|
624
|
+
setInputValue(next);
|
|
625
|
+
try {
|
|
626
|
+
const pages = parsePageRange(next, totalPages);
|
|
627
|
+
setInputError(null);
|
|
628
|
+
setSelectedSet(new Set(pages));
|
|
629
|
+
} catch (e) {
|
|
630
|
+
setInputError(e.message);
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
const toggleThumb = (pageNum) => {
|
|
634
|
+
setSelectedSet((prev) => {
|
|
635
|
+
const next = new Set(prev);
|
|
636
|
+
if (next.has(pageNum)) next.delete(pageNum);
|
|
637
|
+
else next.add(pageNum);
|
|
638
|
+
const serialized = serializeSelection([...next]);
|
|
639
|
+
setInputValue(serialized);
|
|
640
|
+
setInputError(null);
|
|
641
|
+
return next;
|
|
642
|
+
});
|
|
643
|
+
};
|
|
644
|
+
const selectAll = () => {
|
|
645
|
+
setSelectedSet(new Set(defaultPages));
|
|
646
|
+
setInputValue(serializeSelection(defaultPages));
|
|
647
|
+
setInputError(null);
|
|
648
|
+
};
|
|
649
|
+
const clearAll = () => {
|
|
650
|
+
setSelectedSet(/* @__PURE__ */ new Set());
|
|
651
|
+
setInputValue("");
|
|
652
|
+
setInputError(null);
|
|
653
|
+
};
|
|
654
|
+
const canSubmit = inputError === null && selectedSet.size > 0;
|
|
655
|
+
const sortedSelected = useMemo(
|
|
656
|
+
() => [...selectedSet].sort((a, b) => a - b),
|
|
657
|
+
[selectedSet]
|
|
658
|
+
);
|
|
659
|
+
const handleSubmit = () => {
|
|
660
|
+
if (!canSubmit) return;
|
|
661
|
+
onConfirm(sortedSelected);
|
|
662
|
+
};
|
|
663
|
+
return createPortal(
|
|
664
|
+
/* @__PURE__ */ jsx(
|
|
665
|
+
"div",
|
|
666
|
+
{
|
|
667
|
+
role: "dialog",
|
|
668
|
+
"aria-modal": "true",
|
|
669
|
+
"aria-labelledby": "pdf-range-title",
|
|
670
|
+
style: {
|
|
671
|
+
position: "fixed",
|
|
672
|
+
inset: 0,
|
|
673
|
+
background: "rgba(0,0,0,0.55)",
|
|
674
|
+
display: "flex",
|
|
675
|
+
alignItems: "center",
|
|
676
|
+
justifyContent: "center",
|
|
677
|
+
zIndex: 1e4
|
|
678
|
+
},
|
|
679
|
+
onClick: (e) => {
|
|
680
|
+
if (e.target === e.currentTarget) onCancel();
|
|
681
|
+
},
|
|
682
|
+
children: /* @__PURE__ */ jsxs(
|
|
683
|
+
"div",
|
|
684
|
+
{
|
|
685
|
+
style: {
|
|
686
|
+
background: "var(--popup-bg-color, #fff)",
|
|
687
|
+
color: "var(--text-primary-color, #1b1b1f)",
|
|
688
|
+
borderRadius: 12,
|
|
689
|
+
padding: "20px 22px",
|
|
690
|
+
width: "min(880px, 92vw)",
|
|
691
|
+
maxHeight: "88vh",
|
|
692
|
+
boxShadow: "0 12px 40px rgba(0,0,0,0.3)",
|
|
693
|
+
fontFamily: "inherit",
|
|
694
|
+
display: "flex",
|
|
695
|
+
flexDirection: "column",
|
|
696
|
+
gap: 12
|
|
697
|
+
},
|
|
698
|
+
onClick: (e) => e.stopPropagation(),
|
|
699
|
+
children: [
|
|
700
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
701
|
+
/* @__PURE__ */ jsx(
|
|
702
|
+
"h2",
|
|
703
|
+
{
|
|
704
|
+
id: "pdf-range-title",
|
|
705
|
+
style: { margin: 0, fontSize: 16, fontWeight: 600, lineHeight: 1.3 },
|
|
706
|
+
children: "Ch\xE8n PDF"
|
|
707
|
+
}
|
|
708
|
+
),
|
|
709
|
+
/* @__PURE__ */ jsxs("p", { style: { margin: "4px 0 0", fontSize: 12, opacity: 0.7 }, children: [
|
|
710
|
+
fileName,
|
|
711
|
+
" \u2014 ",
|
|
712
|
+
totalPages,
|
|
713
|
+
" trang",
|
|
714
|
+
thumbProgress < totalPages && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
715
|
+
" \xB7 \u0111ang t\u1EA3i preview ",
|
|
716
|
+
thumbProgress,
|
|
717
|
+
"/",
|
|
718
|
+
totalPages,
|
|
719
|
+
"\u2026"
|
|
720
|
+
] })
|
|
721
|
+
] })
|
|
722
|
+
] }),
|
|
723
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "flex-start", gap: 10 }, children: [
|
|
724
|
+
/* @__PURE__ */ jsxs("div", { style: { flex: 1 }, children: [
|
|
725
|
+
/* @__PURE__ */ jsx(
|
|
726
|
+
"label",
|
|
727
|
+
{
|
|
728
|
+
style: { display: "block", fontSize: 12, marginBottom: 4, opacity: 0.75 },
|
|
729
|
+
children: "Trang c\u1EA7n ch\xE8n (vd: 1,3,5-10) \u2014 ho\u1EB7c click thumbnail b\xEAn d\u01B0\u1EDBi"
|
|
730
|
+
}
|
|
731
|
+
),
|
|
732
|
+
/* @__PURE__ */ jsx(
|
|
733
|
+
"input",
|
|
734
|
+
{
|
|
735
|
+
ref: inputRef,
|
|
736
|
+
type: "text",
|
|
737
|
+
value: inputValue,
|
|
738
|
+
onChange: (e) => handleInputChange(e.target.value),
|
|
739
|
+
onKeyDown: (e) => {
|
|
740
|
+
if (e.key === "Enter") {
|
|
741
|
+
e.preventDefault();
|
|
742
|
+
handleSubmit();
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
style: {
|
|
746
|
+
width: "100%",
|
|
747
|
+
boxSizing: "border-box",
|
|
748
|
+
padding: "8px 10px",
|
|
749
|
+
fontSize: 14,
|
|
750
|
+
borderRadius: 6,
|
|
751
|
+
border: `1px solid ${inputError ? "#dc2626" : "rgba(0,0,0,0.2)"}`,
|
|
752
|
+
outline: "none",
|
|
753
|
+
background: "var(--input-bg-color, #fff)",
|
|
754
|
+
color: "inherit",
|
|
755
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace"
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
)
|
|
759
|
+
] }),
|
|
760
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, paddingTop: 18 }, children: [
|
|
761
|
+
/* @__PURE__ */ jsx(
|
|
762
|
+
"button",
|
|
763
|
+
{
|
|
764
|
+
type: "button",
|
|
765
|
+
onClick: selectAll,
|
|
766
|
+
style: quickBtnStyle,
|
|
767
|
+
title: "Ch\u1ECDn t\u1EA5t c\u1EA3 trang",
|
|
768
|
+
children: "T\u1EA5t c\u1EA3"
|
|
769
|
+
}
|
|
770
|
+
),
|
|
771
|
+
/* @__PURE__ */ jsx(
|
|
772
|
+
"button",
|
|
773
|
+
{
|
|
774
|
+
type: "button",
|
|
775
|
+
onClick: clearAll,
|
|
776
|
+
style: quickBtnStyle,
|
|
777
|
+
title: "B\u1ECF ch\u1ECDn t\u1EA5t c\u1EA3",
|
|
778
|
+
children: "B\u1ECF h\u1EBFt"
|
|
779
|
+
}
|
|
780
|
+
)
|
|
781
|
+
] })
|
|
782
|
+
] }),
|
|
783
|
+
/* @__PURE__ */ jsx("div", { style: { minHeight: 18, fontSize: 12 }, "data-testid": "pdf-range-status", children: inputError ? /* @__PURE__ */ jsx("span", { style: { color: "#dc2626" }, children: inputError }) : /* @__PURE__ */ jsxs("span", { style: { opacity: 0.75 }, children: [
|
|
784
|
+
"\u0110\xE3 ch\u1ECDn ",
|
|
785
|
+
/* @__PURE__ */ jsx("strong", { children: selectedSet.size }),
|
|
786
|
+
" / ",
|
|
787
|
+
totalPages,
|
|
788
|
+
" trang"
|
|
789
|
+
] }) }),
|
|
790
|
+
/* @__PURE__ */ jsx(
|
|
791
|
+
"div",
|
|
792
|
+
{
|
|
793
|
+
style: {
|
|
794
|
+
flex: 1,
|
|
795
|
+
minHeight: 240,
|
|
796
|
+
maxHeight: "60vh",
|
|
797
|
+
overflow: "auto",
|
|
798
|
+
padding: 8,
|
|
799
|
+
background: "rgba(0,0,0,0.04)",
|
|
800
|
+
borderRadius: 8,
|
|
801
|
+
display: "grid",
|
|
802
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))",
|
|
803
|
+
gap: 10,
|
|
804
|
+
alignContent: "start"
|
|
805
|
+
},
|
|
806
|
+
children: Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNum) => {
|
|
807
|
+
const thumb = thumbs[pageNum];
|
|
808
|
+
const selected = selectedSet.has(pageNum);
|
|
809
|
+
return /* @__PURE__ */ jsx(
|
|
810
|
+
ThumbnailItem,
|
|
811
|
+
{
|
|
812
|
+
pageNum,
|
|
813
|
+
thumb,
|
|
814
|
+
selected,
|
|
815
|
+
onToggle: () => toggleThumb(pageNum)
|
|
816
|
+
},
|
|
817
|
+
pageNum
|
|
818
|
+
);
|
|
819
|
+
})
|
|
820
|
+
}
|
|
821
|
+
),
|
|
822
|
+
/* @__PURE__ */ jsxs(
|
|
823
|
+
"div",
|
|
824
|
+
{
|
|
825
|
+
style: {
|
|
826
|
+
display: "flex",
|
|
827
|
+
justifyContent: "flex-end",
|
|
828
|
+
gap: 8,
|
|
829
|
+
paddingTop: 4
|
|
830
|
+
},
|
|
831
|
+
children: [
|
|
832
|
+
/* @__PURE__ */ jsx(
|
|
833
|
+
"button",
|
|
834
|
+
{
|
|
835
|
+
type: "button",
|
|
836
|
+
onClick: onCancel,
|
|
837
|
+
style: {
|
|
838
|
+
padding: "8px 14px",
|
|
839
|
+
fontSize: 13,
|
|
840
|
+
borderRadius: 6,
|
|
841
|
+
border: "1px solid rgba(0,0,0,0.15)",
|
|
842
|
+
background: "transparent",
|
|
843
|
+
color: "inherit",
|
|
844
|
+
cursor: "pointer"
|
|
845
|
+
},
|
|
846
|
+
children: "Hu\u1EF7"
|
|
847
|
+
}
|
|
848
|
+
),
|
|
849
|
+
/* @__PURE__ */ jsxs(
|
|
850
|
+
"button",
|
|
851
|
+
{
|
|
852
|
+
type: "button",
|
|
853
|
+
onClick: handleSubmit,
|
|
854
|
+
disabled: !canSubmit,
|
|
855
|
+
style: {
|
|
856
|
+
padding: "8px 16px",
|
|
857
|
+
fontSize: 13,
|
|
858
|
+
borderRadius: 6,
|
|
859
|
+
border: "none",
|
|
860
|
+
background: canSubmit ? "#4f46e5" : "rgba(0,0,0,0.15)",
|
|
861
|
+
color: "#fff",
|
|
862
|
+
cursor: canSubmit ? "pointer" : "not-allowed",
|
|
863
|
+
fontWeight: 500
|
|
864
|
+
},
|
|
865
|
+
children: [
|
|
866
|
+
"Ch\xE8n ",
|
|
867
|
+
selectedSet.size > 0 ? `${selectedSet.size} trang` : ""
|
|
868
|
+
]
|
|
869
|
+
}
|
|
870
|
+
)
|
|
871
|
+
]
|
|
872
|
+
}
|
|
873
|
+
)
|
|
874
|
+
]
|
|
875
|
+
}
|
|
876
|
+
)
|
|
877
|
+
}
|
|
878
|
+
),
|
|
879
|
+
document.body
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
var quickBtnStyle = {
|
|
883
|
+
padding: "7px 10px",
|
|
884
|
+
fontSize: 12,
|
|
885
|
+
borderRadius: 6,
|
|
886
|
+
border: "1px solid rgba(0,0,0,0.15)",
|
|
887
|
+
background: "transparent",
|
|
888
|
+
color: "inherit",
|
|
889
|
+
cursor: "pointer",
|
|
890
|
+
whiteSpace: "nowrap"
|
|
891
|
+
};
|
|
892
|
+
function ThumbnailItem({ pageNum, thumb, selected, onToggle }) {
|
|
893
|
+
const aspect = thumb ? thumb.width / thumb.height : 0.77;
|
|
894
|
+
return /* @__PURE__ */ jsxs(
|
|
895
|
+
"button",
|
|
896
|
+
{
|
|
897
|
+
type: "button",
|
|
898
|
+
onClick: onToggle,
|
|
899
|
+
"aria-pressed": selected,
|
|
900
|
+
"aria-label": `Trang ${pageNum}${selected ? " (\u0111\xE3 ch\u1ECDn)" : ""}`,
|
|
901
|
+
title: `Trang ${pageNum}`,
|
|
902
|
+
style: {
|
|
903
|
+
position: "relative",
|
|
904
|
+
padding: 0,
|
|
905
|
+
background: "#fff",
|
|
906
|
+
border: `2px solid ${selected ? "#4f46e5" : "rgba(0,0,0,0.12)"}`,
|
|
907
|
+
borderRadius: 6,
|
|
908
|
+
overflow: "hidden",
|
|
909
|
+
cursor: "pointer",
|
|
910
|
+
boxShadow: selected ? "0 0 0 3px rgba(79,70,229,0.18)" : "none",
|
|
911
|
+
transition: "border-color 80ms ease, box-shadow 80ms ease"
|
|
912
|
+
},
|
|
913
|
+
children: [
|
|
914
|
+
/* @__PURE__ */ jsx(
|
|
915
|
+
"div",
|
|
916
|
+
{
|
|
917
|
+
style: {
|
|
918
|
+
width: "100%",
|
|
919
|
+
aspectRatio: aspect.toString(),
|
|
920
|
+
background: "#f5f5f5",
|
|
921
|
+
display: "flex",
|
|
922
|
+
alignItems: "center",
|
|
923
|
+
justifyContent: "center"
|
|
924
|
+
},
|
|
925
|
+
children: thumb ? (
|
|
926
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
927
|
+
/* @__PURE__ */ jsx(
|
|
928
|
+
"img",
|
|
929
|
+
{
|
|
930
|
+
src: thumb.dataURL,
|
|
931
|
+
alt: "",
|
|
932
|
+
style: { width: "100%", height: "100%", display: "block", objectFit: "contain" },
|
|
933
|
+
draggable: false
|
|
934
|
+
}
|
|
935
|
+
)
|
|
936
|
+
) : /* @__PURE__ */ jsx("div", { style: { fontSize: 11, opacity: 0.5 }, children: "\u2026" })
|
|
937
|
+
}
|
|
938
|
+
),
|
|
939
|
+
/* @__PURE__ */ jsx(
|
|
940
|
+
"div",
|
|
941
|
+
{
|
|
942
|
+
style: {
|
|
943
|
+
position: "absolute",
|
|
944
|
+
bottom: 4,
|
|
945
|
+
left: 4,
|
|
946
|
+
fontSize: 10,
|
|
947
|
+
fontWeight: 600,
|
|
948
|
+
padding: "2px 6px",
|
|
949
|
+
borderRadius: 4,
|
|
950
|
+
background: selected ? "#4f46e5" : "rgba(0,0,0,0.6)",
|
|
951
|
+
color: "#fff"
|
|
952
|
+
},
|
|
953
|
+
children: pageNum
|
|
954
|
+
}
|
|
955
|
+
),
|
|
956
|
+
selected && /* @__PURE__ */ jsx(
|
|
957
|
+
"div",
|
|
958
|
+
{
|
|
959
|
+
"aria-hidden": "true",
|
|
960
|
+
style: {
|
|
961
|
+
position: "absolute",
|
|
962
|
+
top: 4,
|
|
963
|
+
right: 4,
|
|
964
|
+
width: 18,
|
|
965
|
+
height: 18,
|
|
966
|
+
borderRadius: "50%",
|
|
967
|
+
background: "#4f46e5",
|
|
968
|
+
color: "#fff",
|
|
969
|
+
display: "flex",
|
|
970
|
+
alignItems: "center",
|
|
971
|
+
justifyContent: "center",
|
|
972
|
+
fontSize: 11,
|
|
973
|
+
fontWeight: 700,
|
|
974
|
+
boxShadow: "0 1px 3px rgba(0,0,0,0.3)"
|
|
975
|
+
},
|
|
976
|
+
children: "\u2713"
|
|
977
|
+
}
|
|
978
|
+
)
|
|
979
|
+
]
|
|
980
|
+
}
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// src/pdf/insertPdfPages.ts
|
|
985
|
+
var PAGE_GAP = 24;
|
|
986
|
+
var DEFAULT_SCALE = 2;
|
|
987
|
+
function insertRasterizedPagesIntoScene(api, rendered, options) {
|
|
988
|
+
if (!api) throw new Error("Excalidraw API ch\u01B0a s\u1EB5n s\xE0ng.");
|
|
989
|
+
if (rendered.length === 0) return { insertedElementIds: [], fileIds: [] };
|
|
990
|
+
const { scale } = options;
|
|
991
|
+
const filesPayload = rendered.map((p) => ({
|
|
992
|
+
id: generateFileId(),
|
|
993
|
+
dataURL: p.dataURL,
|
|
994
|
+
mimeType: p.mimeType,
|
|
995
|
+
created: Date.now()
|
|
996
|
+
}));
|
|
997
|
+
api.addFiles(filesPayload);
|
|
998
|
+
const origin = options.origin ?? getViewportCenter(api);
|
|
999
|
+
const sceneSizes = rendered.map((p) => pixelsToSceneSize(p.width, p.height, scale));
|
|
1000
|
+
const maxSceneWidth = Math.max(...sceneSizes.map((s) => s.width));
|
|
1001
|
+
const baseX = origin.x - maxSceneWidth / 2;
|
|
1002
|
+
let cursorY = origin.y - sceneSizes[0].height / 2;
|
|
1003
|
+
const newElements = rendered.map((_, i) => {
|
|
1004
|
+
const { width, height } = sceneSizes[i];
|
|
1005
|
+
const x = baseX + (maxSceneWidth - width) / 2;
|
|
1006
|
+
const y = cursorY;
|
|
1007
|
+
cursorY = y + height + PAGE_GAP;
|
|
1008
|
+
return buildPdfImageElement(filesPayload[i].id, x, y, width, height);
|
|
1009
|
+
});
|
|
1010
|
+
const existing = api.getSceneElements();
|
|
1011
|
+
api.updateScene({
|
|
1012
|
+
elements: [...existing, ...newElements],
|
|
1013
|
+
appState: { selectedElementIds: {}, croppingElementId: null }
|
|
1014
|
+
});
|
|
1015
|
+
return {
|
|
1016
|
+
insertedElementIds: newElements.map((e) => e.id),
|
|
1017
|
+
fileIds: filesPayload.map((f) => f.id)
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
function pixelsToSceneSize(pxWidth, pxHeight, scale) {
|
|
1021
|
+
return { width: pxWidth / scale, height: pxHeight / scale };
|
|
1022
|
+
}
|
|
1023
|
+
function buildPdfImageElement(fileId, x, y, width, height) {
|
|
1024
|
+
return {
|
|
1025
|
+
type: "image",
|
|
1026
|
+
id: "pdf_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8),
|
|
1027
|
+
x,
|
|
1028
|
+
y,
|
|
1029
|
+
width,
|
|
1030
|
+
height,
|
|
1031
|
+
fileId,
|
|
1032
|
+
angle: 0,
|
|
1033
|
+
strokeColor: "transparent",
|
|
1034
|
+
backgroundColor: "transparent",
|
|
1035
|
+
fillStyle: "solid",
|
|
1036
|
+
strokeWidth: 1,
|
|
1037
|
+
strokeStyle: "solid",
|
|
1038
|
+
roughness: 0,
|
|
1039
|
+
opacity: 100,
|
|
1040
|
+
groupIds: [],
|
|
1041
|
+
roundness: null,
|
|
1042
|
+
seed: Math.floor(Math.random() * 1e9),
|
|
1043
|
+
versionNonce: 0,
|
|
1044
|
+
version: 1,
|
|
1045
|
+
isDeleted: false,
|
|
1046
|
+
boundElements: null,
|
|
1047
|
+
updated: Date.now(),
|
|
1048
|
+
link: null,
|
|
1049
|
+
locked: false,
|
|
1050
|
+
status: "saved",
|
|
1051
|
+
scale: [1, 1]
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
function generateFileId() {
|
|
1055
|
+
return "pdf_" + Date.now() + "_" + Math.random().toString(36).slice(2, 10);
|
|
1056
|
+
}
|
|
1057
|
+
function getViewportCenter(api) {
|
|
1058
|
+
const appState = api?.getAppState?.() ?? {
|
|
1059
|
+
scrollX: 0,
|
|
1060
|
+
scrollY: 0,
|
|
1061
|
+
width: 800,
|
|
1062
|
+
height: 600,
|
|
1063
|
+
zoom: { value: 1 }
|
|
1064
|
+
};
|
|
1065
|
+
const zoom = appState.zoom?.value ?? 1;
|
|
1066
|
+
return {
|
|
1067
|
+
x: appState.scrollX + (appState.width ?? 800) / 2 / zoom,
|
|
1068
|
+
y: appState.scrollY + (appState.height ?? 600) / 2 / zoom
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
async function insertPdfPages(api, source, options = {}) {
|
|
1072
|
+
if (!api) throw new Error("Excalidraw API ch\u01B0a s\u1EB5n s\xE0ng.");
|
|
1073
|
+
const scale = options.scale ?? DEFAULT_SCALE;
|
|
1074
|
+
const doc = await loadPdfDocument(source);
|
|
1075
|
+
let rendered;
|
|
1076
|
+
try {
|
|
1077
|
+
rendered = await rasterizePdf(doc, {
|
|
1078
|
+
pages: options.pages,
|
|
1079
|
+
scale,
|
|
1080
|
+
onProgress: options.onProgress,
|
|
1081
|
+
signal: options.signal
|
|
1082
|
+
});
|
|
1083
|
+
} finally {
|
|
1084
|
+
void closePdfDocument(doc);
|
|
1085
|
+
}
|
|
1086
|
+
const { insertedElementIds } = insertRasterizedPagesIntoScene(api, rendered, {
|
|
1087
|
+
scale,
|
|
1088
|
+
origin: options.origin
|
|
1089
|
+
});
|
|
1090
|
+
return { insertedElementIds, pages: rendered };
|
|
1091
|
+
}
|
|
256
1092
|
var DOUBLE_CLICK_MS = 400;
|
|
257
1093
|
function useStampDoubleClick({ enabled, stamps, onOpen }) {
|
|
258
1094
|
const lastClickRef = useRef({
|
|
@@ -721,6 +1557,8 @@ function Whiteboard({
|
|
|
721
1557
|
activeStampRef.current = activeStamp;
|
|
722
1558
|
const [editingElement, setEditingElement] = useState(null);
|
|
723
1559
|
const hostRef = useRef(null);
|
|
1560
|
+
const [pdfPending, setPdfPending] = useState(null);
|
|
1561
|
+
const [pdfBusy, setPdfBusy] = useState(false);
|
|
724
1562
|
const handledCropIdRef = useRef(null);
|
|
725
1563
|
const prevExcalidrawToolRef = useRef("selection");
|
|
726
1564
|
const stampByKind = useMemo(() => {
|
|
@@ -1009,6 +1847,80 @@ function Whiteboard({
|
|
|
1009
1847
|
return () => window.removeEventListener("keydown", onKey, { capture: true });
|
|
1010
1848
|
}, [activeStamp, closeStamp]);
|
|
1011
1849
|
useStampClickOutside({ activeStamp, hostRef, onClose: closeStamp });
|
|
1850
|
+
const handlePdfPick = useCallback(
|
|
1851
|
+
async (file) => {
|
|
1852
|
+
if (readOnly || pdfBusy) return;
|
|
1853
|
+
setPdfBusy(true);
|
|
1854
|
+
try {
|
|
1855
|
+
const doc = await loadPdfDocument(file);
|
|
1856
|
+
setPdfPending({ doc, fileName: file.name, totalPages: doc.numPages });
|
|
1857
|
+
} catch (err) {
|
|
1858
|
+
console.warn("[whiteboard] \u0110\u1ECDc PDF th\u1EA5t b\u1EA1i:", err);
|
|
1859
|
+
window.alert("Kh\xF4ng \u0111\u1ECDc \u0111\u01B0\u1EE3c PDF. File c\xF3 th\u1EC3 \u0111\xE3 h\u1ECFng ho\u1EB7c b\u1ECB m\u1EADt kh\u1EA9u b\u1EA3o v\u1EC7.");
|
|
1860
|
+
} finally {
|
|
1861
|
+
setPdfBusy(false);
|
|
1862
|
+
}
|
|
1863
|
+
},
|
|
1864
|
+
[readOnly, pdfBusy]
|
|
1865
|
+
);
|
|
1866
|
+
const handlePdfConfirm = useCallback(
|
|
1867
|
+
async (pages) => {
|
|
1868
|
+
if (!pdfPending || !api) return;
|
|
1869
|
+
const { doc } = pdfPending;
|
|
1870
|
+
setPdfPending(null);
|
|
1871
|
+
setPdfBusy(true);
|
|
1872
|
+
const scale = 2;
|
|
1873
|
+
try {
|
|
1874
|
+
const rendered = await rasterizePdf(doc, { pages, scale });
|
|
1875
|
+
await closePdfDocument(doc);
|
|
1876
|
+
insertRasterizedPagesIntoScene(api, rendered, { scale });
|
|
1877
|
+
} catch (err) {
|
|
1878
|
+
console.warn("[whiteboard] Ch\xE8n PDF th\u1EA5t b\u1EA1i:", err);
|
|
1879
|
+
window.alert("Ch\xE8n PDF th\u1EA5t b\u1EA1i. Xem console \u0111\u1EC3 bi\u1EBFt chi ti\u1EBFt.");
|
|
1880
|
+
} finally {
|
|
1881
|
+
setPdfBusy(false);
|
|
1882
|
+
}
|
|
1883
|
+
},
|
|
1884
|
+
[pdfPending, api]
|
|
1885
|
+
);
|
|
1886
|
+
const handlePdfCancel = useCallback(() => {
|
|
1887
|
+
if (pdfPending) {
|
|
1888
|
+
void closePdfDocument(pdfPending.doc);
|
|
1889
|
+
}
|
|
1890
|
+
setPdfPending(null);
|
|
1891
|
+
}, [pdfPending]);
|
|
1892
|
+
useEffect(() => {
|
|
1893
|
+
if (readOnly) return;
|
|
1894
|
+
const root = document.querySelector(".excalidraw");
|
|
1895
|
+
if (!root) return;
|
|
1896
|
+
const onDragOver = (e) => {
|
|
1897
|
+
const items = e.dataTransfer?.items;
|
|
1898
|
+
if (!items) return;
|
|
1899
|
+
for (let i = 0; i < items.length; i++) {
|
|
1900
|
+
if (items[i].kind === "file" && items[i].type === "application/pdf") {
|
|
1901
|
+
e.preventDefault();
|
|
1902
|
+
e.stopPropagation();
|
|
1903
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
};
|
|
1908
|
+
const onDrop = (e) => {
|
|
1909
|
+
const files = e.dataTransfer?.files;
|
|
1910
|
+
if (!files || files.length === 0) return;
|
|
1911
|
+
const pdf = Array.from(files).find((f) => f.type === "application/pdf");
|
|
1912
|
+
if (!pdf) return;
|
|
1913
|
+
e.preventDefault();
|
|
1914
|
+
e.stopPropagation();
|
|
1915
|
+
void handlePdfPick(pdf);
|
|
1916
|
+
};
|
|
1917
|
+
root.addEventListener("dragover", onDragOver, { capture: true });
|
|
1918
|
+
root.addEventListener("drop", onDrop, { capture: true });
|
|
1919
|
+
return () => {
|
|
1920
|
+
root.removeEventListener("dragover", onDragOver, { capture: true });
|
|
1921
|
+
root.removeEventListener("drop", onDrop, { capture: true });
|
|
1922
|
+
};
|
|
1923
|
+
}, [readOnly, handlePdfPick, api]);
|
|
1012
1924
|
return /* @__PURE__ */ jsxs("div", { className: `relative h-full w-full${isDarkTheme ? " theme--dark" : ""}`, children: [
|
|
1013
1925
|
/* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(ExcalidrawLoadingFallback, {}), children: /* @__PURE__ */ jsx(
|
|
1014
1926
|
Excalidraw,
|
|
@@ -1043,6 +1955,35 @@ function Whiteboard({
|
|
|
1043
1955
|
stamps
|
|
1044
1956
|
}
|
|
1045
1957
|
),
|
|
1958
|
+
/* @__PURE__ */ jsx(PdfImporterButton, { enabled: !readOnly, onPick: handlePdfPick }),
|
|
1959
|
+
pdfPending && /* @__PURE__ */ jsx(
|
|
1960
|
+
PageRangeDialog,
|
|
1961
|
+
{
|
|
1962
|
+
doc: pdfPending.doc,
|
|
1963
|
+
fileName: pdfPending.fileName,
|
|
1964
|
+
onConfirm: handlePdfConfirm,
|
|
1965
|
+
onCancel: handlePdfCancel
|
|
1966
|
+
}
|
|
1967
|
+
),
|
|
1968
|
+
pdfBusy && !pdfPending && /* @__PURE__ */ jsx(
|
|
1969
|
+
"div",
|
|
1970
|
+
{
|
|
1971
|
+
"aria-live": "polite",
|
|
1972
|
+
role: "status",
|
|
1973
|
+
style: {
|
|
1974
|
+
position: "fixed",
|
|
1975
|
+
bottom: 16,
|
|
1976
|
+
right: 16,
|
|
1977
|
+
padding: "8px 14px",
|
|
1978
|
+
background: "rgba(0,0,0,0.75)",
|
|
1979
|
+
color: "#fff",
|
|
1980
|
+
borderRadius: 6,
|
|
1981
|
+
fontSize: 12,
|
|
1982
|
+
zIndex: 1e4
|
|
1983
|
+
},
|
|
1984
|
+
children: "\u0110ang x\u1EED l\xFD PDF\u2026"
|
|
1985
|
+
}
|
|
1986
|
+
),
|
|
1046
1987
|
HostComponent && /* @__PURE__ */ jsx(
|
|
1047
1988
|
HostComponent,
|
|
1048
1989
|
{
|
|
@@ -1056,6 +1997,6 @@ function Whiteboard({
|
|
|
1056
1997
|
] });
|
|
1057
1998
|
}
|
|
1058
1999
|
|
|
1059
|
-
export { ALL_STAMPS, DEFAULT_STAMPS, EXPERIMENTAL_STAMPS, STABLE_STAMPS, Whiteboard, findStampForCustomData, isStampElement, pickSyncableAppState, restoreMissingStampFiles };
|
|
2000
|
+
export { ALL_STAMPS, DEFAULT_STAMPS, EXPERIMENTAL_STAMPS, STABLE_STAMPS, Whiteboard, closePdfDocument, configurePdfWorker, findStampForCustomData, insertPdfPages, insertRasterizedPagesIntoScene, isStampElement, loadPdfDocument, parsePageRange, pickSyncableAppState, rasterizePdf, restoreMissingStampFiles };
|
|
1060
2001
|
//# sourceMappingURL=index.mjs.map
|
|
1061
2002
|
//# sourceMappingURL=index.mjs.map
|