omni-viewer-core 0.3.0 → 0.5.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/README.md +28 -0
- package/dist/host/index.d.ts +14 -2
- package/dist/i18n/catalog.en.js +8 -0
- package/dist/i18n/catalog.ja.js +9 -1
- package/dist/i18n/catalog.ko.js +8 -0
- package/dist/i18n/catalog.zh-cn.js +8 -1
- package/dist/parsers/pptx/index.d.ts +1 -0
- package/dist/parsers/pptx/index.js +1 -1
- package/dist/parsers/yaml/index.js +14 -2
- package/dist/parsers/yaml/self-loading.js +26 -14
- package/dist/registry/index.js +1 -1
- package/dist/styles/archive.css +1 -1
- package/dist/styles/pdf.css +26 -0
- package/dist/styles/toml.css +1 -1
- package/dist/styles/yaml.css +1 -1
- package/dist/viewers/archive/index.js +60 -11
- package/dist/viewers/archive/styles.d.ts +1 -1
- package/dist/viewers/archive/styles.js +1 -1
- package/dist/viewers/json/controller.js +13 -24
- package/dist/viewers/json/index.js +5 -1
- package/dist/viewers/markdown/index.d.ts +3 -2
- package/dist/viewers/markdown/index.js +29 -14
- package/dist/viewers/pdf/controller.d.ts +14 -2
- package/dist/viewers/pdf/controller.js +28 -10
- package/dist/viewers/pdf/index.d.ts +79 -8
- package/dist/viewers/pdf/index.js +363 -81
- package/dist/viewers/pdf/self-loading.d.ts +3 -3
- package/dist/viewers/pdf/styles.d.ts +1 -1
- package/dist/viewers/pdf/styles.js +26 -0
- package/dist/viewers/ppt/index.js +3 -3
- package/dist/viewers/structured-styles.d.ts +1 -1
- package/dist/viewers/structured-styles.js +1 -1
- package/dist/viewers/structured.d.ts +1 -0
- package/dist/viewers/structured.js +5 -3
- package/dist/viewers/yaml/controller.js +48 -5
- package/package.json +2 -1
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
// alert / confirm — those are blocked inside the VS Code webview, so text
|
|
11
11
|
// annotations use an inline input overlay instead.
|
|
12
12
|
import { MountAbortedError, VIEWER_ROOT_CLASS } from '../types.js';
|
|
13
|
-
import { createPdfController, mergeHighlightRects
|
|
14
|
-
import { buildSavedPdf, mergePdfBytes, parseLayer, savedPdfName, SIDECAR_LAYER_NAME, SIDECAR_BASE_NAME } from './editing.js';
|
|
13
|
+
import { createPdfController, mergeHighlightRects } from './controller.js';
|
|
14
|
+
import { buildEditedPdf, buildSavedPdf, mergePdfBytes, parseLayer, savedPdfName, SIDECAR_LAYER_NAME, SIDECAR_BASE_NAME } from './editing.js';
|
|
15
15
|
import { pdfViewerCss } from './styles.js';
|
|
16
|
-
export { createPdfController, PDF_ZOOM_LEVELS } from './controller.js';
|
|
16
|
+
export { createPdfController, PDF_MAX_ZOOM, PDF_MIN_ZOOM, PDF_ZOOM_LEVELS } from './controller.js';
|
|
17
17
|
export { pdfViewerCss } from './styles.js';
|
|
18
|
-
export { buildEditedPdf, mergePdfBytes, savedPdfName } from './editing.js';
|
|
18
|
+
export { buildEditedPdf, buildSavedPdf, mergePdfBytes, parseLayer, savedPdfName, SIDECAR_BASE_NAME, SIDECAR_LAYER_NAME } from './editing.js';
|
|
19
|
+
/** Asset key passed to `HostContext.assets.resolveAssetUrl` by default. */
|
|
20
|
+
export const PDF_WORKER_ASSET_KEY = 'assets/pdfjs/pdf.worker.min.mjs';
|
|
19
21
|
/** Viewer metadata — single source for the registry codegen (DESIGN.md §7). */
|
|
20
22
|
export const PDF_VIEWER_META = {
|
|
21
23
|
id: 'pdf',
|
|
@@ -32,6 +34,13 @@ function isPasswordError(error) {
|
|
|
32
34
|
error !== null &&
|
|
33
35
|
error.name === 'PasswordException');
|
|
34
36
|
}
|
|
37
|
+
export function containPdfSignatureSize(width, height, maxWidth = 120, maxHeight = 60) {
|
|
38
|
+
if (!(width > 0) || !(height > 0) || !(maxWidth > 0) || !(maxHeight > 0)) {
|
|
39
|
+
return { width: 0, height: 0 };
|
|
40
|
+
}
|
|
41
|
+
const scale = Math.min(maxWidth / width, maxHeight / height);
|
|
42
|
+
return { width: width * scale, height: height * scale };
|
|
43
|
+
}
|
|
35
44
|
export async function mountPdfViewer(input, container, ctx, deps, options = {}) {
|
|
36
45
|
if (options.signal?.aborted)
|
|
37
46
|
throw new MountAbortedError();
|
|
@@ -81,13 +90,16 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
81
90
|
status.setAttribute('aria-live', 'polite');
|
|
82
91
|
frame.append(header, zoomBar, body, status);
|
|
83
92
|
root.appendChild(frame);
|
|
93
|
+
const customActionCleanups = [];
|
|
84
94
|
// ------------------------------------------------------------- lifecycle
|
|
85
95
|
let disposed = false;
|
|
86
96
|
let doc;
|
|
87
97
|
let loadingTask;
|
|
88
98
|
let pageObserver;
|
|
89
99
|
let thumbObserver;
|
|
100
|
+
let thumbTasks = [];
|
|
90
101
|
let records = [];
|
|
102
|
+
let thumbRecords = [];
|
|
91
103
|
let renderEpoch = 0;
|
|
92
104
|
let currentPage = 1;
|
|
93
105
|
let tool = 'view';
|
|
@@ -98,15 +110,27 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
98
110
|
let pageLayout = 'single';
|
|
99
111
|
let matchThemeColors = false;
|
|
100
112
|
let sourceBytes = input.data;
|
|
113
|
+
const saveMode = options.saveMode ?? 'hybrid';
|
|
114
|
+
const processingMode = options.processingMode ?? 'auto';
|
|
115
|
+
const controllerOptions = {
|
|
116
|
+
...(options.zoomLevels === undefined ? {} : { zoomLevels: options.zoomLevels }),
|
|
117
|
+
...(options.minZoom === undefined ? {} : { minZoom: options.minZoom }),
|
|
118
|
+
...(options.maxZoom === undefined ? {} : { maxZoom: options.maxZoom })
|
|
119
|
+
};
|
|
101
120
|
/** A merge changed the document without going through the edit stack. */
|
|
102
121
|
let mergedSinceSave = false;
|
|
103
122
|
let pdfLib;
|
|
104
123
|
let unsubscribe;
|
|
105
|
-
let controller = createPdfController(0);
|
|
124
|
+
let controller = createPdfController(0, undefined, controllerOptions);
|
|
125
|
+
let operationState = { status: 'idle' };
|
|
126
|
+
let operationController;
|
|
127
|
+
let cancelPasswordPrompt;
|
|
106
128
|
/** Layout inputs of the last rebuild — annotation-only changes skip it. */
|
|
107
129
|
let layoutKey = '';
|
|
108
130
|
const abortListener = () => {
|
|
109
|
-
loadingTask?.destroy();
|
|
131
|
+
void loadingTask?.destroy();
|
|
132
|
+
operationController?.abort();
|
|
133
|
+
cancelPasswordPrompt?.();
|
|
110
134
|
};
|
|
111
135
|
options.signal?.addEventListener('abort', abortListener, { once: true });
|
|
112
136
|
function throwIfAborted() {
|
|
@@ -120,14 +144,23 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
120
144
|
document.removeEventListener('keydown', keyHandler);
|
|
121
145
|
document.removeEventListener('selectionchange', syncMarkupAvailability);
|
|
122
146
|
options.signal?.removeEventListener('abort', abortListener);
|
|
147
|
+
operationController?.abort();
|
|
148
|
+
operationController = undefined;
|
|
149
|
+
cancelPasswordPrompt?.();
|
|
150
|
+
for (const cleanup of customActionCleanups.splice(0))
|
|
151
|
+
cleanup();
|
|
123
152
|
unsubscribe?.();
|
|
124
153
|
pageObserver?.disconnect();
|
|
125
154
|
thumbObserver?.disconnect();
|
|
155
|
+
for (const task of thumbTasks)
|
|
156
|
+
task.cancel();
|
|
157
|
+
thumbTasks = [];
|
|
126
158
|
for (const record of records) {
|
|
127
159
|
record.task?.cancel();
|
|
128
160
|
record.textLayer?.cancel();
|
|
129
161
|
}
|
|
130
162
|
records = [];
|
|
163
|
+
thumbRecords = [];
|
|
131
164
|
void doc?.destroy();
|
|
132
165
|
doc = undefined;
|
|
133
166
|
root.replaceChildren();
|
|
@@ -156,23 +189,49 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
156
189
|
};
|
|
157
190
|
document.addEventListener('keydown', keyHandler);
|
|
158
191
|
// -------------------------------------------------------------- pdf.js
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
192
|
+
let pdfjs;
|
|
193
|
+
try {
|
|
194
|
+
pdfjs = await deps.loadPdfjs();
|
|
195
|
+
throwIfAborted();
|
|
196
|
+
pdfjs.GlobalWorkerOptions.workerSrc = options.workerSrc
|
|
197
|
+
?? await ctx.assets.resolveAssetUrl(PDF_WORKER_ASSET_KEY);
|
|
198
|
+
throwIfAborted();
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (error instanceof MountAbortedError)
|
|
202
|
+
throw error;
|
|
203
|
+
ctx.logger.log('error', `pdf worker asset failed: ${String(error)}`);
|
|
204
|
+
status.textContent = t('pdf.workerLoadFailed');
|
|
205
|
+
status.classList.add('omni-pdf__status--error');
|
|
206
|
+
return {
|
|
207
|
+
get controller() { return controller; },
|
|
208
|
+
get operation() { return operationState; },
|
|
209
|
+
isDirty: () => false,
|
|
210
|
+
cancelOperation: () => undefined,
|
|
211
|
+
refreshToolbarActions: () => undefined,
|
|
212
|
+
dispose: teardown
|
|
213
|
+
};
|
|
214
|
+
}
|
|
163
215
|
async function loadDocument(bytes, password) {
|
|
164
216
|
const request = {
|
|
165
217
|
// pdf.js transfers the buffer to its worker; keep ours intact.
|
|
166
|
-
data: bytes.slice()
|
|
218
|
+
data: bytes.slice(),
|
|
219
|
+
isEvalSupported: options.isEvalSupported ?? false
|
|
167
220
|
};
|
|
168
221
|
if (password !== undefined)
|
|
169
222
|
request.password = password;
|
|
170
|
-
|
|
223
|
+
const task = pdfjs.getDocument(request);
|
|
224
|
+
loadingTask = task;
|
|
171
225
|
try {
|
|
172
|
-
return await
|
|
226
|
+
return await task.promise;
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
await task.destroy();
|
|
230
|
+
throw error;
|
|
173
231
|
}
|
|
174
232
|
finally {
|
|
175
|
-
loadingTask
|
|
233
|
+
if (loadingTask === task)
|
|
234
|
+
loadingTask = undefined;
|
|
176
235
|
}
|
|
177
236
|
}
|
|
178
237
|
/**
|
|
@@ -224,10 +283,16 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
224
283
|
panel.append(inputEl, actions);
|
|
225
284
|
overlay.appendChild(panel);
|
|
226
285
|
frame.appendChild(overlay);
|
|
286
|
+
let finished = false;
|
|
227
287
|
const finish = (value) => {
|
|
288
|
+
if (finished)
|
|
289
|
+
return;
|
|
290
|
+
finished = true;
|
|
291
|
+
cancelPasswordPrompt = undefined;
|
|
228
292
|
overlay.remove();
|
|
229
293
|
resolve(value);
|
|
230
294
|
};
|
|
295
|
+
cancelPasswordPrompt = () => finish(null);
|
|
231
296
|
submit.addEventListener('click', () => finish(inputEl.value));
|
|
232
297
|
cancel.addEventListener('click', () => finish(null));
|
|
233
298
|
inputEl.addEventListener('keydown', (e) => {
|
|
@@ -240,7 +305,11 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
240
305
|
});
|
|
241
306
|
}
|
|
242
307
|
// --------------------------------------------------------------- toolbar
|
|
243
|
-
const
|
|
308
|
+
const canUseBrowserProcessing = processingMode !== 'host' && typeof deps.loadPdfLib === 'function';
|
|
309
|
+
const canDelegateBuild = processingMode !== 'browser' && typeof deps.processing?.buildPdf === 'function';
|
|
310
|
+
const canDelegateMerge = processingMode !== 'browser' && typeof deps.processing?.mergePdfs === 'function';
|
|
311
|
+
const editingAvailable = canUseBrowserProcessing || canDelegateBuild;
|
|
312
|
+
const mergeAvailable = canUseBrowserProcessing || canDelegateMerge;
|
|
244
313
|
function toolButton(labelKey, onClick, parent = toolbar) {
|
|
245
314
|
const b = el('button', 'omni-pdf__tool', t(labelKey));
|
|
246
315
|
b.type = 'button';
|
|
@@ -254,6 +323,9 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
254
323
|
button.title = t(reasonKey);
|
|
255
324
|
}
|
|
256
325
|
toolbar.appendChild(pageInfo);
|
|
326
|
+
const saveModeBadge = el('span', 'omni-pdf__save-mode', t(saveMode === 'hybrid' ? 'pdf.saveModeHybrid' : 'pdf.saveModeFlattened'));
|
|
327
|
+
saveModeBadge.title = t(saveMode === 'hybrid' ? 'pdf.saveModeHybridTitle' : 'pdf.saveModeFlattenedTitle');
|
|
328
|
+
toolbar.appendChild(saveModeBadge);
|
|
257
329
|
const textBtn = toolButton('pdf.annotationText', () => {
|
|
258
330
|
tool = tool === 'text' ? 'view' : 'text';
|
|
259
331
|
syncToolButtons();
|
|
@@ -341,6 +413,28 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
341
413
|
const saveBtn = toolButton('pdf.save', () => void save());
|
|
342
414
|
const saveAsBtn = toolButton('pdf.saveAs', () => void saveAs());
|
|
343
415
|
const mergeBtn = toolButton('pdf.merge', () => void merge());
|
|
416
|
+
const customActionButtons = (options.toolbarActions ?? []).map((action) => {
|
|
417
|
+
const button = el('button', 'omni-pdf__tool omni-pdf__host-action', action.label);
|
|
418
|
+
button.type = 'button';
|
|
419
|
+
button.dataset.actionId = action.id;
|
|
420
|
+
if (action.title !== undefined)
|
|
421
|
+
button.title = action.title;
|
|
422
|
+
button.setAttribute('aria-label', action.ariaLabel ?? action.label);
|
|
423
|
+
const listener = () => {
|
|
424
|
+
try {
|
|
425
|
+
void Promise.resolve(action.onClick()).catch((error) => {
|
|
426
|
+
ctx.logger.log('error', `pdf toolbar action ${action.id} failed: ${String(error)}`);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
ctx.logger.log('error', `pdf toolbar action ${action.id} failed: ${String(error)}`);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
button.addEventListener('click', listener);
|
|
434
|
+
customActionCleanups.push(() => button.removeEventListener('click', listener));
|
|
435
|
+
toolbar.appendChild(button);
|
|
436
|
+
return { action, button };
|
|
437
|
+
});
|
|
344
438
|
function syncToolButtons() {
|
|
345
439
|
textBtn.setAttribute('aria-pressed', String(tool === 'text'));
|
|
346
440
|
signatureBtn.setAttribute('aria-pressed', String(tool === 'signature'));
|
|
@@ -349,7 +443,7 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
349
443
|
}
|
|
350
444
|
syncToolButtons();
|
|
351
445
|
if (!editingAvailable) {
|
|
352
|
-
for (const b of [textBtn, signatureBtn, highlightBtn, markupMenuBtn, highlightChoiceBtn, underlineBtn, strikeoutBtn, resetBtn, saveBtn, saveAsBtn
|
|
446
|
+
for (const b of [textBtn, signatureBtn, highlightBtn, markupMenuBtn, highlightChoiceBtn, underlineBtn, strikeoutBtn, resetBtn, saveBtn, saveAsBtn]) {
|
|
353
447
|
degrade(b, 'pdf.editingUnavailable');
|
|
354
448
|
}
|
|
355
449
|
highlightColorInput.disabled = true;
|
|
@@ -361,9 +455,11 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
361
455
|
degrade(saveBtn, 'common.noWriteback');
|
|
362
456
|
if (!ctx.save)
|
|
363
457
|
degrade(saveAsBtn, 'common.noFileSave');
|
|
364
|
-
if (!ctx.filePick)
|
|
365
|
-
degrade(mergeBtn, 'pdf.noFilePick');
|
|
366
458
|
}
|
|
459
|
+
if (!ctx.filePick)
|
|
460
|
+
degrade(mergeBtn, 'pdf.noFilePick');
|
|
461
|
+
else if (!mergeAvailable)
|
|
462
|
+
degrade(mergeBtn, 'pdf.editingUnavailable');
|
|
367
463
|
function syncMarkupAvailability() {
|
|
368
464
|
if (!editingAvailable)
|
|
369
465
|
return;
|
|
@@ -544,9 +640,9 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
544
640
|
page: position.page,
|
|
545
641
|
x: position.x,
|
|
546
642
|
y: position.y,
|
|
547
|
-
width:
|
|
548
|
-
height:
|
|
549
|
-
dataUrl: image
|
|
643
|
+
width: image.width,
|
|
644
|
+
height: image.height,
|
|
645
|
+
dataUrl: image.dataUrl
|
|
550
646
|
}
|
|
551
647
|
});
|
|
552
648
|
});
|
|
@@ -575,7 +671,8 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
575
671
|
crop.width = maxX - minX + 1 + pad * 2;
|
|
576
672
|
crop.height = maxY - minY + 1 + pad * 2;
|
|
577
673
|
crop.getContext('2d')?.drawImage(signatureCanvas, minX, minY, maxX - minX + 1, maxY - minY + 1, pad, pad, maxX - minX + 1, maxY - minY + 1);
|
|
578
|
-
|
|
674
|
+
const size = containPdfSignatureSize(crop.width, crop.height);
|
|
675
|
+
return { dataUrl: crop.toDataURL('image/png'), ...size };
|
|
579
676
|
}
|
|
580
677
|
/** Rasterize user-added text for portable PDF flattening. The sidecar
|
|
581
678
|
* keeps the original string, size and color, so Omni can still edit it. */
|
|
@@ -800,6 +897,7 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
800
897
|
continue;
|
|
801
898
|
}
|
|
802
899
|
const overlay = el('div', 'omni-pdf__annotation');
|
|
900
|
+
overlay.dataset.annotationId = annotation.id;
|
|
803
901
|
overlay.tabIndex = 0;
|
|
804
902
|
overlay.setAttribute('role', 'button');
|
|
805
903
|
overlay.setAttribute('aria-label', annotation.kind === 'text' ? annotation.text : t('pdf.signature'));
|
|
@@ -870,6 +968,47 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
870
968
|
record.wrapper.appendChild(overlay);
|
|
871
969
|
}
|
|
872
970
|
}
|
|
971
|
+
/** Mirror the page annotations onto a thumbnail as non-interactive,
|
|
972
|
+
* scaled-down overlays so thumbs stay in sync with markup edits. */
|
|
973
|
+
function renderThumbOverlays(thumbRecord) {
|
|
974
|
+
for (const node of thumbRecord.holder.querySelectorAll('.omni-pdf__thumb-annotation')) {
|
|
975
|
+
node.remove();
|
|
976
|
+
}
|
|
977
|
+
const scale = thumbRecord.scale;
|
|
978
|
+
for (const annotation of controller.state.annotations) {
|
|
979
|
+
if (annotation.page !== thumbRecord.pageNumber)
|
|
980
|
+
continue;
|
|
981
|
+
if (annotation.kind === 'highlight' || annotation.kind === 'underline' || annotation.kind === 'strikeout') {
|
|
982
|
+
for (const rect of mergeHighlightRects(annotation.rects)) {
|
|
983
|
+
const box = el('div', `omni-pdf__thumb-annotation omni-pdf__${annotation.kind}`);
|
|
984
|
+
box.style.left = `${rect.x * scale}px`;
|
|
985
|
+
box.style.top = `${rect.y * scale}px`;
|
|
986
|
+
box.style.width = `${rect.width * scale}px`;
|
|
987
|
+
box.style.height = `${rect.height * scale}px`;
|
|
988
|
+
box.style.setProperty('--omni-hl-color', annotation.color);
|
|
989
|
+
thumbRecord.holder.appendChild(box);
|
|
990
|
+
}
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const overlay = el('div', 'omni-pdf__thumb-annotation');
|
|
994
|
+
overlay.style.left = `${annotation.x * scale}px`;
|
|
995
|
+
overlay.style.top = `${annotation.y * scale}px`;
|
|
996
|
+
if (annotation.kind === 'text') {
|
|
997
|
+
overlay.textContent = annotation.text;
|
|
998
|
+
overlay.style.fontSize = `${annotation.size * scale}px`;
|
|
999
|
+
overlay.style.color = annotation.color;
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
const image = el('img');
|
|
1003
|
+
image.src = annotation.dataUrl;
|
|
1004
|
+
image.alt = '';
|
|
1005
|
+
image.style.width = `${annotation.width * scale}px`;
|
|
1006
|
+
image.style.height = `${annotation.height * scale}px`;
|
|
1007
|
+
overlay.appendChild(image);
|
|
1008
|
+
}
|
|
1009
|
+
thumbRecord.holder.appendChild(overlay);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
873
1012
|
// -------------------------------------------------------------- render
|
|
874
1013
|
async function renderPage(record, epoch) {
|
|
875
1014
|
if (disposed || epoch !== renderEpoch || record.rendered || record.rendering)
|
|
@@ -880,7 +1019,7 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
880
1019
|
canvas.width = Math.floor(viewport.width);
|
|
881
1020
|
canvas.height = Math.floor(viewport.height);
|
|
882
1021
|
canvas.setAttribute('role', 'img');
|
|
883
|
-
canvas.setAttribute('aria-label', t('common.page', { page: record.
|
|
1022
|
+
canvas.setAttribute('aria-label', t('common.page', { page: record.displayNumber, pages: controller.state.pageOrder.length }));
|
|
884
1023
|
const canvasContext = canvas.getContext('2d');
|
|
885
1024
|
if (!canvasContext) {
|
|
886
1025
|
record.rendering = false;
|
|
@@ -941,21 +1080,24 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
941
1080
|
}
|
|
942
1081
|
function setCurrentPage(pageNumber) {
|
|
943
1082
|
currentPage = pageNumber;
|
|
944
|
-
|
|
1083
|
+
const displayNumber = records.findIndex((record) => record.pageNumber === currentPage) + 1;
|
|
1084
|
+
pageInput.value = String(Math.max(1, displayNumber));
|
|
1085
|
+
pageInput.max = String(records.length);
|
|
945
1086
|
pageTotal.textContent = `/ ${records.length}`;
|
|
946
1087
|
records.forEach((record, index) => {
|
|
947
1088
|
thumbs.children[index]?.toggleAttribute('aria-current', record.pageNumber === currentPage);
|
|
948
1089
|
});
|
|
949
1090
|
}
|
|
950
1091
|
function goToPageInput() {
|
|
951
|
-
const
|
|
952
|
-
const record =
|
|
1092
|
+
const displayNumber = Number(pageInput.value);
|
|
1093
|
+
const record = Number.isInteger(displayNumber) ? records[displayNumber - 1] : undefined;
|
|
953
1094
|
if (!record) {
|
|
954
|
-
|
|
1095
|
+
const currentDisplay = records.findIndex((item) => item.pageNumber === currentPage) + 1;
|
|
1096
|
+
pageInput.value = String(Math.max(1, currentDisplay));
|
|
955
1097
|
return;
|
|
956
1098
|
}
|
|
957
1099
|
scrollToPage(record);
|
|
958
|
-
setCurrentPage(pageNumber);
|
|
1100
|
+
setCurrentPage(record.pageNumber);
|
|
959
1101
|
}
|
|
960
1102
|
pageInput.addEventListener('keydown', (event) => {
|
|
961
1103
|
if (event.key !== 'Enter')
|
|
@@ -1026,26 +1168,32 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
1026
1168
|
const epoch = ++renderEpoch;
|
|
1027
1169
|
pageObserver?.disconnect();
|
|
1028
1170
|
thumbObserver?.disconnect();
|
|
1171
|
+
for (const task of thumbTasks)
|
|
1172
|
+
task.cancel();
|
|
1173
|
+
thumbTasks = [];
|
|
1029
1174
|
for (const record of records) {
|
|
1030
1175
|
record.task?.cancel();
|
|
1031
1176
|
record.textLayer?.cancel();
|
|
1032
1177
|
}
|
|
1033
1178
|
records = [];
|
|
1179
|
+
thumbRecords = [];
|
|
1034
1180
|
pages.replaceChildren();
|
|
1035
1181
|
thumbs.replaceChildren();
|
|
1036
1182
|
const scale = controller.state.zoom / 100;
|
|
1037
1183
|
const pageTotal = controller.state.pageOrder.length;
|
|
1038
1184
|
const thumbRenderers = [];
|
|
1039
|
-
for (const pageNumber of controller.state.pageOrder) {
|
|
1185
|
+
for (const [displayIndex, pageNumber] of controller.state.pageOrder.entries()) {
|
|
1186
|
+
const displayNumber = displayIndex + 1;
|
|
1040
1187
|
const page = await doc.getPage(pageNumber);
|
|
1041
1188
|
if (disposed || epoch !== renderEpoch)
|
|
1042
1189
|
return;
|
|
1043
1190
|
const viewport = page.getViewport({ scale });
|
|
1044
1191
|
const wrapper = el('div', 'omni-pdf__page');
|
|
1045
1192
|
wrapper.dataset.pageNumber = String(pageNumber);
|
|
1193
|
+
wrapper.dataset.displayNumber = String(displayNumber);
|
|
1046
1194
|
wrapper.style.width = `${viewport.width}px`;
|
|
1047
1195
|
wrapper.style.height = `${viewport.height}px`;
|
|
1048
|
-
wrapper.appendChild(el('div', 'omni-pdf__placeholder', t('common.page', { page:
|
|
1196
|
+
wrapper.appendChild(el('div', 'omni-pdf__placeholder', t('common.page', { page: displayNumber, pages: pageTotal })));
|
|
1049
1197
|
wrapper.addEventListener('click', (event) => {
|
|
1050
1198
|
const target = event.target;
|
|
1051
1199
|
if (target instanceof Element && target.closest('.omni-pdf__annotation'))
|
|
@@ -1080,6 +1228,7 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
1080
1228
|
pages.appendChild(wrapper);
|
|
1081
1229
|
const record = {
|
|
1082
1230
|
pageNumber,
|
|
1231
|
+
displayNumber,
|
|
1083
1232
|
page,
|
|
1084
1233
|
wrapper,
|
|
1085
1234
|
rendered: false,
|
|
@@ -1091,11 +1240,10 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
1091
1240
|
// --- thumbnail (lazy-rendered — docs §5: never all at once) ---
|
|
1092
1241
|
const thumb = el('button', 'omni-pdf__thumb');
|
|
1093
1242
|
thumb.type = 'button';
|
|
1094
|
-
thumb.setAttribute('aria-label', t('common.page', { page:
|
|
1243
|
+
thumb.setAttribute('aria-label', t('common.page', { page: displayNumber, pages: pageTotal }));
|
|
1095
1244
|
const thumbCanvas = el('canvas');
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
});
|
|
1245
|
+
const thumbScale = Math.min(0.18, 90 / (viewport.width / scale || 1));
|
|
1246
|
+
const thumbViewport = page.getViewport({ scale: thumbScale });
|
|
1099
1247
|
thumbCanvas.width = Math.floor(thumbViewport.width);
|
|
1100
1248
|
thumbCanvas.height = Math.floor(thumbViewport.height);
|
|
1101
1249
|
let thumbRendered = false;
|
|
@@ -1106,13 +1254,22 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
1106
1254
|
const thumbContext = thumbCanvas.getContext('2d');
|
|
1107
1255
|
if (!thumbContext)
|
|
1108
1256
|
return;
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1257
|
+
const task = page.render({ canvasContext: thumbContext, viewport: thumbViewport });
|
|
1258
|
+
thumbTasks.push(task);
|
|
1259
|
+
void task.promise
|
|
1260
|
+
.catch(() => undefined)
|
|
1261
|
+
.finally(() => {
|
|
1262
|
+
thumbTasks = thumbTasks.filter((candidate) => candidate !== task);
|
|
1263
|
+
});
|
|
1112
1264
|
};
|
|
1113
1265
|
thumbRenderers.push({ element: thumb, render: renderThumb });
|
|
1114
|
-
const label = el('span', 'omni-pdf__thumb-label', String(
|
|
1115
|
-
|
|
1266
|
+
const label = el('span', 'omni-pdf__thumb-label', String(displayNumber));
|
|
1267
|
+
const thumbHolder = el('div', 'omni-pdf__thumb-page');
|
|
1268
|
+
thumbHolder.appendChild(thumbCanvas);
|
|
1269
|
+
thumb.append(thumbHolder, label);
|
|
1270
|
+
const thumbRecord = { pageNumber, holder: thumbHolder, scale: thumbScale };
|
|
1271
|
+
thumbRecords.push(thumbRecord);
|
|
1272
|
+
renderThumbOverlays(thumbRecord);
|
|
1116
1273
|
thumb.addEventListener('click', () => {
|
|
1117
1274
|
// Keep thumbnail navigation inside the PDF scroll container.
|
|
1118
1275
|
scrollToPage(record);
|
|
@@ -1211,10 +1368,20 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
1211
1368
|
function refreshChrome() {
|
|
1212
1369
|
const state = controller.state;
|
|
1213
1370
|
zoomLevel.textContent = `${state.zoom}%`;
|
|
1214
|
-
zoomOutBtn.disabled = state.zoom
|
|
1215
|
-
zoomInBtn.disabled = state.zoom
|
|
1371
|
+
zoomOutBtn.disabled = state.zoom <= controller.minZoom;
|
|
1372
|
+
zoomInBtn.disabled = state.zoom >= controller.maxZoom;
|
|
1373
|
+
const busy = operationState.status === 'running';
|
|
1216
1374
|
if (editingAvailable && ctx.writeback) {
|
|
1217
|
-
saveBtn.disabled = !(state.dirty || mergedSinceSave);
|
|
1375
|
+
saveBtn.disabled = busy || !(state.dirty || mergedSinceSave);
|
|
1376
|
+
}
|
|
1377
|
+
if (editingAvailable && ctx.save)
|
|
1378
|
+
saveAsBtn.disabled = busy;
|
|
1379
|
+
if (mergeAvailable && ctx.filePick)
|
|
1380
|
+
mergeBtn.disabled = busy;
|
|
1381
|
+
for (const { action, button } of customActionButtons) {
|
|
1382
|
+
button.disabled = typeof action.disabled === 'function'
|
|
1383
|
+
? action.disabled()
|
|
1384
|
+
: action.disabled ?? false;
|
|
1218
1385
|
}
|
|
1219
1386
|
}
|
|
1220
1387
|
/** Full rebuild only when zoom / page structure changed; annotation-only
|
|
@@ -1232,6 +1399,8 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
1232
1399
|
if (record.rendered)
|
|
1233
1400
|
renderOverlays(record);
|
|
1234
1401
|
}
|
|
1402
|
+
for (const thumbRecord of thumbRecords)
|
|
1403
|
+
renderThumbOverlays(thumbRecord);
|
|
1235
1404
|
}
|
|
1236
1405
|
function adoptController(next) {
|
|
1237
1406
|
unsubscribe?.();
|
|
@@ -1261,80 +1430,164 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
1261
1430
|
})
|
|
1262
1431
|
};
|
|
1263
1432
|
}
|
|
1433
|
+
function isOperationCancelled(error, signal) {
|
|
1434
|
+
return signal.aborted || (typeof error === 'object' && error !== null
|
|
1435
|
+
&& error.name === 'AbortError');
|
|
1436
|
+
}
|
|
1437
|
+
async function runOperation(kind, work) {
|
|
1438
|
+
const abortController = new AbortController();
|
|
1439
|
+
operationController = abortController;
|
|
1440
|
+
operationState = { status: 'running', kind };
|
|
1441
|
+
refreshChrome();
|
|
1442
|
+
const control = {
|
|
1443
|
+
signal: abortController.signal,
|
|
1444
|
+
onProgress(progress) {
|
|
1445
|
+
if (operationController !== abortController || abortController.signal.aborted)
|
|
1446
|
+
return;
|
|
1447
|
+
const normalized = progress === undefined
|
|
1448
|
+
? undefined
|
|
1449
|
+
: Math.max(0, Math.min(1, progress));
|
|
1450
|
+
operationState = normalized === undefined
|
|
1451
|
+
? { status: 'running', kind }
|
|
1452
|
+
: { status: 'running', kind, progress: normalized };
|
|
1453
|
+
const base = t(kind === 'merge' ? 'pdf.merging' : 'pdf.saving');
|
|
1454
|
+
status.textContent = normalized === undefined
|
|
1455
|
+
? base
|
|
1456
|
+
: `${base} ${Math.round(normalized * 100)}%`;
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
control.onProgress(undefined);
|
|
1460
|
+
try {
|
|
1461
|
+
const value = await work(control);
|
|
1462
|
+
if (abortController.signal.aborted || disposed) {
|
|
1463
|
+
throw new DOMException('PDF operation cancelled', 'AbortError');
|
|
1464
|
+
}
|
|
1465
|
+
operationState = { status: 'succeeded', kind };
|
|
1466
|
+
return { status: 'succeeded', value };
|
|
1467
|
+
}
|
|
1468
|
+
catch (error) {
|
|
1469
|
+
if (isOperationCancelled(error, abortController.signal)) {
|
|
1470
|
+
operationState = { status: 'cancelled', kind };
|
|
1471
|
+
status.textContent = t('pdf.operationCancelled');
|
|
1472
|
+
return { status: 'cancelled' };
|
|
1473
|
+
}
|
|
1474
|
+
operationState = { status: 'failed', kind, error: String(error) };
|
|
1475
|
+
return { status: 'failed' };
|
|
1476
|
+
}
|
|
1477
|
+
finally {
|
|
1478
|
+
if (operationController === abortController)
|
|
1479
|
+
operationController = undefined;
|
|
1480
|
+
refreshChrome();
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
async function buildOutput(state, control) {
|
|
1484
|
+
if (canDelegateBuild) {
|
|
1485
|
+
return deps.processing.buildPdf({ source: sourceBytes, state, mode: saveMode }, control);
|
|
1486
|
+
}
|
|
1487
|
+
if (!canUseBrowserProcessing)
|
|
1488
|
+
throw new Error('PDF processing service unavailable');
|
|
1489
|
+
if (control.signal.aborted)
|
|
1490
|
+
throw new DOMException('PDF operation cancelled', 'AbortError');
|
|
1491
|
+
const lib = await ensurePdfLib();
|
|
1492
|
+
const output = saveMode === 'hybrid'
|
|
1493
|
+
? await buildSavedPdf(lib, sourceBytes, state)
|
|
1494
|
+
: await buildEditedPdf(lib, sourceBytes, state);
|
|
1495
|
+
if (control.signal.aborted)
|
|
1496
|
+
throw new DOMException('PDF operation cancelled', 'AbortError');
|
|
1497
|
+
return output;
|
|
1498
|
+
}
|
|
1499
|
+
async function mergeOutput(second, control) {
|
|
1500
|
+
if (canDelegateMerge) {
|
|
1501
|
+
return deps.processing.mergePdfs({ first: sourceBytes, second }, control);
|
|
1502
|
+
}
|
|
1503
|
+
if (!canUseBrowserProcessing)
|
|
1504
|
+
throw new Error('PDF merge service unavailable');
|
|
1505
|
+
if (control.signal.aborted)
|
|
1506
|
+
throw new DOMException('PDF operation cancelled', 'AbortError');
|
|
1507
|
+
const lib = await ensurePdfLib();
|
|
1508
|
+
const output = await mergePdfBytes(lib, sourceBytes, second);
|
|
1509
|
+
if (control.signal.aborted)
|
|
1510
|
+
throw new DOMException('PDF operation cancelled', 'AbortError');
|
|
1511
|
+
return output;
|
|
1512
|
+
}
|
|
1264
1513
|
async function save() {
|
|
1265
1514
|
if (!ctx.writeback || saveBtn.disabled)
|
|
1266
1515
|
return;
|
|
1267
|
-
|
|
1268
|
-
const
|
|
1269
|
-
// Flatten from the pristine base (not the on-disk flattened copy),
|
|
1270
|
-
// embedding the sidecar so overlays stay removable on reopen. The
|
|
1271
|
-
// in-memory session keeps `sourceBytes` pristine + the live layer;
|
|
1272
|
-
// we only rebaseline the dirty marker.
|
|
1273
|
-
const data = await buildSavedPdf(lib, sourceBytes, stateForSave());
|
|
1516
|
+
const result = await runOperation('save', async (control) => {
|
|
1517
|
+
const data = await buildOutput(stateForSave(), control);
|
|
1274
1518
|
await ctx.writeback.write(data);
|
|
1519
|
+
});
|
|
1520
|
+
if (result.status === 'succeeded') {
|
|
1275
1521
|
mergedSinceSave = false;
|
|
1276
1522
|
controller.dispatch({ type: 'mark-saved' });
|
|
1277
1523
|
refreshChrome();
|
|
1278
1524
|
status.textContent = t('common.savedToOriginal');
|
|
1279
1525
|
}
|
|
1280
|
-
|
|
1281
|
-
ctx.logger.log('error', `pdf save failed: ${
|
|
1526
|
+
else if (result.status === 'failed') {
|
|
1527
|
+
ctx.logger.log('error', `pdf save failed: ${operationState.status === 'failed' ? operationState.error : ''}`);
|
|
1282
1528
|
status.textContent = t('common.saveFailed');
|
|
1283
1529
|
}
|
|
1284
1530
|
}
|
|
1285
1531
|
async function saveAs() {
|
|
1286
1532
|
if (!ctx.save || saveAsBtn.disabled)
|
|
1287
1533
|
return;
|
|
1288
|
-
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1534
|
+
const result = await runOperation('save-as', async (control) => {
|
|
1535
|
+
const data = await buildOutput(stateForSave(), control);
|
|
1536
|
+
const saveResult = await ctx.save.saveFile(savedPdfName(input.fileName), data, 'application/pdf');
|
|
1537
|
+
await options.onSaveAsComplete?.(saveResult);
|
|
1538
|
+
if (saveResult?.status === 'cancelled') {
|
|
1539
|
+
throw new DOMException('Save As cancelled', 'AbortError');
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
if (result.status === 'succeeded') {
|
|
1295
1543
|
status.textContent = t('common.saved', { name: savedPdfName(input.fileName) });
|
|
1296
1544
|
}
|
|
1297
|
-
|
|
1298
|
-
ctx.logger.log('error', `pdf save-as failed: ${
|
|
1545
|
+
else if (result.status === 'failed') {
|
|
1546
|
+
ctx.logger.log('error', `pdf save-as failed: ${operationState.status === 'failed' ? operationState.error : ''}`);
|
|
1299
1547
|
status.textContent = t('common.saveFailed');
|
|
1300
1548
|
}
|
|
1301
1549
|
}
|
|
1302
1550
|
async function merge() {
|
|
1303
1551
|
if (!ctx.filePick || mergeBtn.disabled)
|
|
1304
1552
|
return;
|
|
1305
|
-
const picked = await ctx.filePick.pickFile({
|
|
1553
|
+
const picked = await ctx.filePick.pickFile({
|
|
1554
|
+
accept: ['application/pdf', '.pdf'],
|
|
1555
|
+
...(options.maxMergeBytes === undefined ? {} : { maxBytes: options.maxMergeBytes })
|
|
1556
|
+
});
|
|
1306
1557
|
if (!picked || disposed)
|
|
1307
1558
|
return;
|
|
1308
|
-
|
|
1309
|
-
status.textContent = t('pdf.merging');
|
|
1310
|
-
const lib = await ensurePdfLib();
|
|
1559
|
+
const result = await runOperation('merge', async (control) => {
|
|
1311
1560
|
// `sourceBytes` is the pristine base while the controller holds
|
|
1312
1561
|
// the live reorder/delete/annotation layer. Keep that layer and
|
|
1313
1562
|
// append the new source pages to its current visible order.
|
|
1314
1563
|
const previousState = controller.state;
|
|
1315
|
-
const previousSourcePageCount = doc?.numPages ??
|
|
1316
|
-
const merged = await
|
|
1317
|
-
await
|
|
1318
|
-
doc = await loadDocument(merged);
|
|
1564
|
+
const previousSourcePageCount = doc?.numPages ?? 0;
|
|
1565
|
+
const merged = await mergeOutput(picked.data, control);
|
|
1566
|
+
const candidate = await loadDocument(merged);
|
|
1319
1567
|
if (disposed) {
|
|
1320
|
-
void
|
|
1321
|
-
|
|
1568
|
+
void candidate.destroy();
|
|
1569
|
+
throw new DOMException('PDF operation cancelled', 'AbortError');
|
|
1322
1570
|
}
|
|
1571
|
+
const previousDocument = doc;
|
|
1572
|
+
doc = candidate;
|
|
1323
1573
|
sourceBytes = merged;
|
|
1324
1574
|
currentPage = 1;
|
|
1325
1575
|
// The merged pages exist only in memory until the user saves.
|
|
1326
1576
|
mergedSinceSave = true;
|
|
1327
1577
|
const appendedPages = Array.from({ length: Math.max(0, doc.numPages - previousSourcePageCount) }, (_, index) => previousSourcePageCount + index + 1);
|
|
1328
|
-
adoptController(createPdfController(
|
|
1578
|
+
adoptController(createPdfController(candidate.numPages, {
|
|
1329
1579
|
pageOrder: [...previousState.pageOrder, ...appendedPages],
|
|
1330
1580
|
annotations: previousState.annotations
|
|
1331
|
-
}));
|
|
1581
|
+
}, controllerOptions));
|
|
1582
|
+
await previousDocument?.destroy();
|
|
1332
1583
|
refreshChrome();
|
|
1333
1584
|
await rebuildLayout();
|
|
1585
|
+
});
|
|
1586
|
+
if (result.status === 'succeeded') {
|
|
1334
1587
|
status.textContent = t('pdf.mergeComplete');
|
|
1335
1588
|
}
|
|
1336
|
-
|
|
1337
|
-
ctx.logger.log('error', `pdf merge failed: ${
|
|
1589
|
+
else if (result.status === 'failed') {
|
|
1590
|
+
ctx.logger.log('error', `pdf merge failed: ${operationState.status === 'failed' ? operationState.error : ''}`);
|
|
1338
1591
|
status.textContent = t('pdf.mergeFailed');
|
|
1339
1592
|
}
|
|
1340
1593
|
}
|
|
@@ -1362,12 +1615,17 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
1362
1615
|
}
|
|
1363
1616
|
status.classList.add('omni-pdf__status--error');
|
|
1364
1617
|
// Stable failed handle: one empty controller, normal dispose.
|
|
1365
|
-
const failedController = createPdfController(0);
|
|
1618
|
+
const failedController = createPdfController(0, undefined, controllerOptions);
|
|
1366
1619
|
return {
|
|
1367
1620
|
get controller() {
|
|
1368
1621
|
return failedController;
|
|
1369
1622
|
},
|
|
1623
|
+
get operation() {
|
|
1624
|
+
return operationState;
|
|
1625
|
+
},
|
|
1370
1626
|
isDirty: () => false,
|
|
1627
|
+
cancelOperation: () => operationController?.abort(),
|
|
1628
|
+
refreshToolbarActions: refreshChrome,
|
|
1371
1629
|
dispose: teardown
|
|
1372
1630
|
};
|
|
1373
1631
|
}
|
|
@@ -1379,14 +1637,33 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
1379
1637
|
// overlays as a removable layer.
|
|
1380
1638
|
const sidecar = await readSidecar(doc);
|
|
1381
1639
|
if (sidecar) {
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1640
|
+
let baseDocument;
|
|
1641
|
+
try {
|
|
1642
|
+
baseDocument = await loadDocument(sidecar.base, password);
|
|
1643
|
+
throwIfAborted();
|
|
1644
|
+
}
|
|
1645
|
+
catch (error) {
|
|
1646
|
+
throwIfAborted();
|
|
1647
|
+
ctx.logger.log('warn', `pdf sidecar base ignored: ${String(error)}`);
|
|
1648
|
+
}
|
|
1649
|
+
if (baseDocument) {
|
|
1650
|
+
const flattenedDocument = doc;
|
|
1651
|
+
doc = baseDocument;
|
|
1652
|
+
sourceBytes = sidecar.base;
|
|
1653
|
+
adoptController(createPdfController(doc.numPages, sidecar.layer, controllerOptions));
|
|
1654
|
+
try {
|
|
1655
|
+
await flattenedDocument.destroy();
|
|
1656
|
+
}
|
|
1657
|
+
catch (error) {
|
|
1658
|
+
ctx.logger.log('warn', `pdf flattened document cleanup failed: ${String(error)}`);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
else {
|
|
1662
|
+
adoptController(createPdfController(doc.numPages, undefined, controllerOptions));
|
|
1663
|
+
}
|
|
1387
1664
|
}
|
|
1388
1665
|
else {
|
|
1389
|
-
adoptController(createPdfController(doc.numPages));
|
|
1666
|
+
adoptController(createPdfController(doc.numPages, undefined, controllerOptions));
|
|
1390
1667
|
}
|
|
1391
1668
|
refreshChrome();
|
|
1392
1669
|
await rebuildLayout();
|
|
@@ -1395,7 +1672,12 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
1395
1672
|
get controller() {
|
|
1396
1673
|
return controller;
|
|
1397
1674
|
},
|
|
1675
|
+
get operation() {
|
|
1676
|
+
return operationState;
|
|
1677
|
+
},
|
|
1398
1678
|
isDirty: () => controller.state.dirty || mergedSinceSave,
|
|
1679
|
+
cancelOperation: () => operationController?.abort(),
|
|
1680
|
+
refreshToolbarActions: refreshChrome,
|
|
1399
1681
|
dispose: teardown
|
|
1400
1682
|
};
|
|
1401
1683
|
}
|