@skyramp/skyramp 1.3.9 → 1.3.11
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/package.json +5 -3
- package/scripts/build-pdf-bundle.js +141 -0
- package/scripts/pdf-execution-helpers.js +340 -0
- package/src/classes/MockV2.d.ts +9 -17
- package/src/classes/MockV2.js +20 -22
- package/src/classes/SkyrampClient.d.ts +2 -0
- package/src/classes/SkyrampClient.js +4 -3
- package/src/classes/SmartPlaywright.js +369 -13
- package/src/pdfViewer/bundle.d.ts +11 -0
- package/src/pdfViewer/bundle.js +1349 -0
- package/src/pdfViewer/index.d.ts +8 -0
- package/src/pdfViewer/index.js +14 -0
- package/src/pdfViewer/validator.d.ts +25 -0
- package/src/pdfViewer/validator.js +119 -0
- package/src/utils.d.ts +2 -2
- package/src/utils.js +14 -3
|
@@ -0,0 +1,1349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Skyramp Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Bundled PDF.js viewer for execution-time injection
|
|
5
|
+
* This is a self-contained JavaScript string that can be injected via page.addInitScript()
|
|
6
|
+
*
|
|
7
|
+
* AUTO-GENERATED from Playwright sources by scripts/build-pdf-bundle.js
|
|
8
|
+
* DO NOT EDIT THIS FILE DIRECTLY - edit pdfJsViewer.ts instead and regenerate
|
|
9
|
+
*
|
|
10
|
+
* Source files:
|
|
11
|
+
* - PdfJsViewer class: /Users/chelfim/code/playwright/packages/injected/src/recorder/skyramp/pdfJsViewer.ts
|
|
12
|
+
* - Execution helpers: /Users/chelfim/Desktop/code/skyramp/libs/npm/scripts/pdf-execution-helpers.js
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Self-contained PDF viewer injection script
|
|
17
|
+
* This script will be injected into pages during test execution to replace Chrome's PDF plugin with PDF.js
|
|
18
|
+
*/
|
|
19
|
+
const PDF_VIEWER_INJECTION_SCRIPT = `
|
|
20
|
+
(function() {
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
console.log('[PW-PDF] PDF viewer injection script initializing...');
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// PDF.js Viewer Class (auto-generated from pdfJsViewer.ts)
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
class PdfJsViewer {
|
|
30
|
+
constructor(container) {
|
|
31
|
+
this._pdfDoc = null;
|
|
32
|
+
this._canvasElements = [];
|
|
33
|
+
this._thumbnailElements = [];
|
|
34
|
+
this._isRendering = false;
|
|
35
|
+
this._toolbar = null;
|
|
36
|
+
this._sidebar = null;
|
|
37
|
+
this._mainContent = null;
|
|
38
|
+
this._currentPage = 1;
|
|
39
|
+
this._pageDisplay = null;
|
|
40
|
+
this._zoomDisplay = null;
|
|
41
|
+
this._currentZoom = 1.0; // 100%
|
|
42
|
+
this._rotation = 0; // 0, 90, 180, 270 degrees
|
|
43
|
+
this._pdfDataUrl = ''; // Store for download
|
|
44
|
+
this._filename = 'Document.pdf';
|
|
45
|
+
this._moreMenuElement = null;
|
|
46
|
+
this._moreMenuCleanup = null;
|
|
47
|
+
this._twoPageView = false;
|
|
48
|
+
this._annotationsVisible = true;
|
|
49
|
+
this._container = container;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Loads PDF.js library from CDN if not already loaded
|
|
53
|
+
*/
|
|
54
|
+
static async loadPdfJs() {
|
|
55
|
+
if (window.pdfjsLib) {
|
|
56
|
+
return; // Already loaded
|
|
57
|
+
}
|
|
58
|
+
console.log('[PDF.js] Loading PDF.js library from CDN...');
|
|
59
|
+
// Load PDF.js from CDN
|
|
60
|
+
const script = document.createElement('script');
|
|
61
|
+
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
script.onload = () => {
|
|
64
|
+
if (!window.pdfjsLib) {
|
|
65
|
+
reject(new Error('PDF.js loaded but pdfjsLib not available'));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Configure worker
|
|
69
|
+
window.pdfjsLib.GlobalWorkerOptions.workerSrc =
|
|
70
|
+
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
|
71
|
+
console.log('[PDF.js] ✅ PDF.js library loaded successfully');
|
|
72
|
+
resolve();
|
|
73
|
+
};
|
|
74
|
+
script.onerror = () => reject(new Error('Failed to load PDF.js from CDN'));
|
|
75
|
+
document.head.appendChild(script);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Renders a PDF from a data URL
|
|
80
|
+
*/
|
|
81
|
+
async renderPdf(config) {
|
|
82
|
+
const { pdfDataUrl, filename, onReady, onError } = config;
|
|
83
|
+
try {
|
|
84
|
+
// Store for later use (download, etc.)
|
|
85
|
+
this._pdfDataUrl = pdfDataUrl;
|
|
86
|
+
this._filename = filename || 'Document.pdf';
|
|
87
|
+
// Ensure PDF.js is loaded
|
|
88
|
+
await PdfJsViewer.loadPdfJs();
|
|
89
|
+
console.log('[PDF.js] Rendering PDF...');
|
|
90
|
+
this._isRendering = true;
|
|
91
|
+
// Load the PDF document
|
|
92
|
+
const loadingTask = window.pdfjsLib.getDocument(pdfDataUrl);
|
|
93
|
+
this._pdfDoc = await loadingTask.promise;
|
|
94
|
+
console.log(\`[PDF.js] PDF loaded: \${this._pdfDoc.numPages} pages\`);
|
|
95
|
+
// Let the body grow with the PDF content so Playwright's fullPage screenshot
|
|
96
|
+
// can capture all pages by scrolling the document (not an inner div).
|
|
97
|
+
if (document.documentElement) {
|
|
98
|
+
document.documentElement.style.height = 'auto';
|
|
99
|
+
}
|
|
100
|
+
if (document.body) {
|
|
101
|
+
document.body.style.margin = '0';
|
|
102
|
+
document.body.style.padding = '0';
|
|
103
|
+
document.body.style.width = '100%';
|
|
104
|
+
document.body.style.height = 'auto';
|
|
105
|
+
document.body.style.overflow = 'auto';
|
|
106
|
+
}
|
|
107
|
+
// Clear container
|
|
108
|
+
this._container.innerHTML = '';
|
|
109
|
+
this._canvasElements = [];
|
|
110
|
+
this._thumbnailElements = [];
|
|
111
|
+
// Container in normal document flow so all pages contribute to scroll height
|
|
112
|
+
this._container.style.cssText = \`
|
|
113
|
+
width: 100%;
|
|
114
|
+
min-height: 100vh;
|
|
115
|
+
display: flex;
|
|
116
|
+
flex-direction: column;
|
|
117
|
+
background-color: #525252;
|
|
118
|
+
margin: 0;
|
|
119
|
+
padding: 0;
|
|
120
|
+
\`;
|
|
121
|
+
// Create toolbar at top (match Chrome PDF viewer toolbar exactly)
|
|
122
|
+
this._toolbar = document.createElement('div');
|
|
123
|
+
this._toolbar.style.cssText = \`
|
|
124
|
+
width: 100%;
|
|
125
|
+
height: 56px;
|
|
126
|
+
background-color: #4a4a4a;
|
|
127
|
+
color: #e8eaed;
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
justify-content: space-between;
|
|
131
|
+
padding: 0 8px;
|
|
132
|
+
box-sizing: border-box;
|
|
133
|
+
font-family: 'Roboto', Arial, sans-serif;
|
|
134
|
+
font-size: 14px;
|
|
135
|
+
flex-shrink: 0;
|
|
136
|
+
border-bottom: 1px solid #2a2a2a;
|
|
137
|
+
position: sticky;
|
|
138
|
+
top: 0;
|
|
139
|
+
z-index: 10;
|
|
140
|
+
\`;
|
|
141
|
+
// Left section: menu + filename
|
|
142
|
+
const leftSection = document.createElement('div');
|
|
143
|
+
leftSection.style.cssText = 'display: flex; align-items: center; gap: 12px;';
|
|
144
|
+
// Menu button (hamburger)
|
|
145
|
+
const menuBtn = this._createToolbarButton('≡', 'Menu', () => {
|
|
146
|
+
// Toggle sidebar visibility
|
|
147
|
+
if (this._sidebar) {
|
|
148
|
+
const isHidden = this._sidebar.style.display === 'none';
|
|
149
|
+
this._sidebar.style.display = isHidden ? 'block' : 'none';
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
menuBtn.style.fontSize = '24px';
|
|
153
|
+
leftSection.appendChild(menuBtn);
|
|
154
|
+
// Filename
|
|
155
|
+
const filenameDisplay = document.createElement('div');
|
|
156
|
+
filenameDisplay.textContent = filename || 'Document.pdf';
|
|
157
|
+
filenameDisplay.style.cssText = \`
|
|
158
|
+
color: #e8eaed;
|
|
159
|
+
font-size: 14px;
|
|
160
|
+
font-weight: 400;
|
|
161
|
+
margin-left: 4px;
|
|
162
|
+
\`;
|
|
163
|
+
leftSection.appendChild(filenameDisplay);
|
|
164
|
+
// Center section: page navigation + zoom controls
|
|
165
|
+
const centerSection = document.createElement('div');
|
|
166
|
+
centerSection.style.cssText = 'display: flex; align-items: center; gap: 12px;';
|
|
167
|
+
// Page navigation
|
|
168
|
+
const pageNav = document.createElement('div');
|
|
169
|
+
pageNav.style.cssText = 'display: flex; align-items: center; gap: 8px;';
|
|
170
|
+
const pageDisplay = document.createElement('span');
|
|
171
|
+
pageDisplay.textContent = \`1 / \${this._pdfDoc.numPages}\`;
|
|
172
|
+
pageDisplay.style.cssText = 'color: #e8eaed; font-size: 13px; min-width: 50px; text-align: center;';
|
|
173
|
+
pageNav.appendChild(pageDisplay);
|
|
174
|
+
centerSection.appendChild(pageNav);
|
|
175
|
+
// Divider
|
|
176
|
+
const divider1 = document.createElement('div');
|
|
177
|
+
divider1.style.cssText = 'width: 1px; height: 24px; background-color: #5f5f5f;';
|
|
178
|
+
centerSection.appendChild(divider1);
|
|
179
|
+
// Zoom controls
|
|
180
|
+
const zoomControls = document.createElement('div');
|
|
181
|
+
zoomControls.style.cssText = 'display: flex; align-items: center; gap: 8px;';
|
|
182
|
+
const zoomOutBtn = this._createToolbarButton('−', 'Zoom out', () => {
|
|
183
|
+
this._zoom(this._currentZoom - 0.1);
|
|
184
|
+
});
|
|
185
|
+
zoomControls.appendChild(zoomOutBtn);
|
|
186
|
+
const zoomDisplay = document.createElement('span');
|
|
187
|
+
zoomDisplay.textContent = '100%';
|
|
188
|
+
zoomDisplay.style.cssText = 'color: #e8eaed; font-size: 13px; min-width: 45px; text-align: center; cursor: pointer;';
|
|
189
|
+
zoomDisplay.title = 'Reset zoom to 100%';
|
|
190
|
+
zoomDisplay.addEventListener('click', () => {
|
|
191
|
+
this._zoom(1.0);
|
|
192
|
+
});
|
|
193
|
+
zoomControls.appendChild(zoomDisplay);
|
|
194
|
+
const zoomInBtn = this._createToolbarButton('+', 'Zoom in', () => {
|
|
195
|
+
this._zoom(this._currentZoom + 0.1);
|
|
196
|
+
});
|
|
197
|
+
zoomControls.appendChild(zoomInBtn);
|
|
198
|
+
centerSection.appendChild(zoomControls);
|
|
199
|
+
// Divider
|
|
200
|
+
const divider2 = document.createElement('div');
|
|
201
|
+
divider2.style.cssText = 'width: 1px; height: 24px; background-color: #5f5f5f;';
|
|
202
|
+
centerSection.appendChild(divider2);
|
|
203
|
+
// Fit to page button
|
|
204
|
+
const fitBtn = this._createToolbarButton('⊡', 'Fit to page', () => {
|
|
205
|
+
this._fitToPage();
|
|
206
|
+
});
|
|
207
|
+
fitBtn.style.fontSize = '18px';
|
|
208
|
+
centerSection.appendChild(fitBtn);
|
|
209
|
+
// Rotate button
|
|
210
|
+
const rotateBtn = this._createToolbarButton('↻', 'Rotate clockwise', () => {
|
|
211
|
+
this._rotate();
|
|
212
|
+
});
|
|
213
|
+
rotateBtn.style.fontSize = '18px';
|
|
214
|
+
centerSection.appendChild(rotateBtn);
|
|
215
|
+
// Right section: download + print + more
|
|
216
|
+
const rightSection = document.createElement('div');
|
|
217
|
+
rightSection.style.cssText = 'display: flex; align-items: center; gap: 8px;';
|
|
218
|
+
// Download button
|
|
219
|
+
const downloadBtn = this._createToolbarButton('⬇', 'Download', () => {
|
|
220
|
+
this._download();
|
|
221
|
+
});
|
|
222
|
+
downloadBtn.style.fontSize = '18px';
|
|
223
|
+
rightSection.appendChild(downloadBtn);
|
|
224
|
+
// Print button
|
|
225
|
+
const printBtn = this._createToolbarButton('🖨', 'Print', () => {
|
|
226
|
+
this._print();
|
|
227
|
+
});
|
|
228
|
+
printBtn.style.fontSize = '16px';
|
|
229
|
+
rightSection.appendChild(printBtn);
|
|
230
|
+
// More options button
|
|
231
|
+
let moreBtn;
|
|
232
|
+
moreBtn = this._createToolbarButton('⋮', 'More options', () => {
|
|
233
|
+
this._toggleMoreMenu(moreBtn);
|
|
234
|
+
});
|
|
235
|
+
moreBtn.style.fontSize = '20px';
|
|
236
|
+
rightSection.appendChild(moreBtn);
|
|
237
|
+
// Assemble toolbar
|
|
238
|
+
this._toolbar.appendChild(leftSection);
|
|
239
|
+
this._toolbar.appendChild(centerSection);
|
|
240
|
+
this._toolbar.appendChild(rightSection);
|
|
241
|
+
// Store references for updates
|
|
242
|
+
this._pageDisplay = pageDisplay;
|
|
243
|
+
this._zoomDisplay = zoomDisplay;
|
|
244
|
+
// Create content wrapper for sidebar and main content
|
|
245
|
+
const contentWrapper = document.createElement('div');
|
|
246
|
+
contentWrapper.style.cssText = \`
|
|
247
|
+
width: 100%;
|
|
248
|
+
flex: 1;
|
|
249
|
+
display: flex;
|
|
250
|
+
\`;
|
|
251
|
+
// Sidebar sticks to the left as the document scrolls
|
|
252
|
+
this._sidebar = document.createElement('div');
|
|
253
|
+
this._sidebar.style.cssText = \`
|
|
254
|
+
width: 294px;
|
|
255
|
+
height: calc(100vh - 56px);
|
|
256
|
+
overflow-y: auto;
|
|
257
|
+
overflow-x: hidden;
|
|
258
|
+
background-color: #3f3f3f;
|
|
259
|
+
border-right: 1px solid #2a2a2a;
|
|
260
|
+
padding: 20px 35px 20px 70px;
|
|
261
|
+
box-sizing: border-box;
|
|
262
|
+
flex-shrink: 0;
|
|
263
|
+
position: sticky;
|
|
264
|
+
top: 56px;
|
|
265
|
+
align-self: flex-start;
|
|
266
|
+
\`;
|
|
267
|
+
// Main content flows in the document — all pages contribute to scroll height
|
|
268
|
+
this._mainContent = document.createElement('div');
|
|
269
|
+
this._mainContent.style.cssText = \`
|
|
270
|
+
flex: 1;
|
|
271
|
+
overflow: visible;
|
|
272
|
+
background-color: #525252;
|
|
273
|
+
position: relative;
|
|
274
|
+
padding: 0;
|
|
275
|
+
box-sizing: border-box;
|
|
276
|
+
\`;
|
|
277
|
+
this._container.appendChild(this._toolbar);
|
|
278
|
+
contentWrapper.appendChild(this._sidebar);
|
|
279
|
+
contentWrapper.appendChild(this._mainContent);
|
|
280
|
+
this._container.appendChild(contentWrapper);
|
|
281
|
+
// Render all pages and thumbnails
|
|
282
|
+
for (let pageNum = 1; pageNum <= this._pdfDoc.numPages; pageNum++) {
|
|
283
|
+
await this._renderPage(pageNum);
|
|
284
|
+
await this._renderThumbnail(pageNum);
|
|
285
|
+
}
|
|
286
|
+
// Setup scroll sync
|
|
287
|
+
this._setupScrollSync();
|
|
288
|
+
this._isRendering = false;
|
|
289
|
+
console.log('[PDF.js] ✅ All pages rendered successfully');
|
|
290
|
+
if (onReady) {
|
|
291
|
+
onReady();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
this._isRendering = false;
|
|
296
|
+
console.error('[PDF.js] ❌ Failed to render PDF:', error);
|
|
297
|
+
if (onError) {
|
|
298
|
+
onError(error);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Injects the minimal CSS required by PDF.js renderTextLayer (once per document).
|
|
304
|
+
* PDF.js relies on external CSS for \`position: absolute\` and \`transform-origin\` on
|
|
305
|
+
* text layer spans — without it the spans are in normal flow and misaligned.
|
|
306
|
+
*/
|
|
307
|
+
static _injectTextLayerCss() {
|
|
308
|
+
if (document.getElementById('pw-pdf-text-layer-styles'))
|
|
309
|
+
return;
|
|
310
|
+
const style = document.createElement('style');
|
|
311
|
+
style.id = 'pw-pdf-text-layer-styles';
|
|
312
|
+
style.textContent = \`
|
|
313
|
+
div[data-pw-pdf-text-layer] {
|
|
314
|
+
line-height: 1;
|
|
315
|
+
-webkit-text-size-adjust: none;
|
|
316
|
+
-moz-text-size-adjust: none;
|
|
317
|
+
text-size-adjust: none;
|
|
318
|
+
forced-color-adjust: none;
|
|
319
|
+
transform-origin: 0 0;
|
|
320
|
+
}
|
|
321
|
+
div[data-pw-pdf-text-layer] :is(span, br) {
|
|
322
|
+
color: transparent;
|
|
323
|
+
position: absolute;
|
|
324
|
+
white-space: pre;
|
|
325
|
+
cursor: text;
|
|
326
|
+
transform-origin: 0% 0%;
|
|
327
|
+
pointer-events: none;
|
|
328
|
+
}
|
|
329
|
+
div[data-pw-pdf-text-layer] span.markedContent {
|
|
330
|
+
top: 0;
|
|
331
|
+
height: 0;
|
|
332
|
+
}
|
|
333
|
+
\`;
|
|
334
|
+
document.head.appendChild(style);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Renders a single page in the main content area
|
|
338
|
+
*/
|
|
339
|
+
async _renderPage(pageNum) {
|
|
340
|
+
if (!this._mainContent)
|
|
341
|
+
return;
|
|
342
|
+
const page = await this._pdfDoc.getPage(pageNum);
|
|
343
|
+
const viewport = page.getViewport({ scale: 1.0 });
|
|
344
|
+
const containerWidth = this._mainContent.clientWidth || 800;
|
|
345
|
+
let scale;
|
|
346
|
+
let wrapperMargin;
|
|
347
|
+
if (this._twoPageView) {
|
|
348
|
+
// Each page gets half the container width
|
|
349
|
+
const availableWidth = (containerWidth / 2 - 32) * 0.95;
|
|
350
|
+
scale = (availableWidth / viewport.width) * this._currentZoom;
|
|
351
|
+
wrapperMargin = '16px auto';
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
const availableWidth = (containerWidth - 80) * 0.89;
|
|
355
|
+
scale = (availableWidth / viewport.width) * this._currentZoom;
|
|
356
|
+
wrapperMargin = '16px 20px 16px 80px';
|
|
357
|
+
}
|
|
358
|
+
const scaledViewport = page.getViewport({ scale });
|
|
359
|
+
// Create canvas wrapper — position:relative so the text layer can overlay it
|
|
360
|
+
const canvasWrapper = document.createElement('div');
|
|
361
|
+
canvasWrapper.setAttribute('data-page-number', pageNum.toString());
|
|
362
|
+
canvasWrapper.style.cssText = \`
|
|
363
|
+
position: relative;
|
|
364
|
+
margin: \${wrapperMargin};
|
|
365
|
+
background: white;
|
|
366
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 4px 8px rgba(0, 0, 0, 0.15);
|
|
367
|
+
width: \${scaledViewport.width}px;
|
|
368
|
+
height: \${scaledViewport.height}px;
|
|
369
|
+
box-sizing: border-box;
|
|
370
|
+
\`;
|
|
371
|
+
// Create canvas
|
|
372
|
+
const canvas = document.createElement('canvas');
|
|
373
|
+
canvas.width = scaledViewport.width;
|
|
374
|
+
canvas.height = scaledViewport.height;
|
|
375
|
+
canvas.style.cssText = \`
|
|
376
|
+
display: block;
|
|
377
|
+
width: 100%;
|
|
378
|
+
height: 100%;
|
|
379
|
+
\`;
|
|
380
|
+
canvasWrapper.appendChild(canvas);
|
|
381
|
+
this._mainContent.appendChild(canvasWrapper);
|
|
382
|
+
this._canvasElements.push(canvas);
|
|
383
|
+
// Render page on canvas
|
|
384
|
+
const context = canvas.getContext('2d');
|
|
385
|
+
if (!context) {
|
|
386
|
+
throw new Error('Failed to get canvas 2D context');
|
|
387
|
+
}
|
|
388
|
+
const renderContext = {
|
|
389
|
+
canvasContext: context,
|
|
390
|
+
viewport: scaledViewport
|
|
391
|
+
};
|
|
392
|
+
await page.render(renderContext).promise;
|
|
393
|
+
// Render invisible text layer so Playwright text assertions can find PDF text
|
|
394
|
+
try {
|
|
395
|
+
PdfJsViewer._injectTextLayerCss();
|
|
396
|
+
const textContent = await page.getTextContent();
|
|
397
|
+
const textLayer = document.createElement('div');
|
|
398
|
+
textLayer.setAttribute('data-pw-pdf-text-layer', pageNum.toString());
|
|
399
|
+
// width/height must match the canvas so span coordinates align correctly.
|
|
400
|
+
// opacity:0 hides the layer visually but keeps spans interactive for mouse events.
|
|
401
|
+
textLayer.style.cssText = \`
|
|
402
|
+
position: absolute;
|
|
403
|
+
top: 0;
|
|
404
|
+
left: 0;
|
|
405
|
+
width: \${scaledViewport.width}px;
|
|
406
|
+
height: \${scaledViewport.height}px;
|
|
407
|
+
overflow: hidden;
|
|
408
|
+
opacity: 0;
|
|
409
|
+
\`;
|
|
410
|
+
// --scale-factor is used by PDF.js CSS for font-size calculations
|
|
411
|
+
textLayer.style.setProperty('--scale-factor', String(scale));
|
|
412
|
+
canvasWrapper.appendChild(textLayer);
|
|
413
|
+
const renderTask = window.pdfjsLib.renderTextLayer({
|
|
414
|
+
textContentSource: textContent,
|
|
415
|
+
container: textLayer,
|
|
416
|
+
viewport: scaledViewport,
|
|
417
|
+
textDivs: [],
|
|
418
|
+
});
|
|
419
|
+
await renderTask.promise;
|
|
420
|
+
}
|
|
421
|
+
catch (e) {
|
|
422
|
+
console.warn(\`[PDF.js] Text layer render failed for page \${pageNum}:\`, e);
|
|
423
|
+
}
|
|
424
|
+
console.log(\`[PDF.js] Rendered page \${pageNum}/\${this._pdfDoc.numPages}\`);
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Renders a thumbnail for the sidebar
|
|
428
|
+
*/
|
|
429
|
+
async _renderThumbnail(pageNum) {
|
|
430
|
+
if (!this._sidebar)
|
|
431
|
+
return;
|
|
432
|
+
const page = await this._pdfDoc.getPage(pageNum);
|
|
433
|
+
// Thumbnail scale (match Chrome PDF viewer: ~118px wide thumbnails)
|
|
434
|
+
const viewport = page.getViewport({ scale: 1.0 });
|
|
435
|
+
const thumbnailWidth = 118;
|
|
436
|
+
const scale = thumbnailWidth / viewport.width;
|
|
437
|
+
const scaledViewport = page.getViewport({ scale });
|
|
438
|
+
// Create thumbnail wrapper (match Chrome PDF viewer style with generous spacing)
|
|
439
|
+
const thumbWrapper = document.createElement('div');
|
|
440
|
+
thumbWrapper.setAttribute('data-page-number', pageNum.toString());
|
|
441
|
+
thumbWrapper.style.cssText = \`
|
|
442
|
+
margin: 18px auto;
|
|
443
|
+
background: white;
|
|
444
|
+
cursor: pointer;
|
|
445
|
+
border: 3px solid transparent;
|
|
446
|
+
box-sizing: border-box;
|
|
447
|
+
transition: border-color 0.15s;
|
|
448
|
+
width: fit-content;
|
|
449
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
450
|
+
\`;
|
|
451
|
+
// Highlight first page (match Chrome's blue highlight)
|
|
452
|
+
if (pageNum === 1) {
|
|
453
|
+
thumbWrapper.style.borderColor = '#1a73e8';
|
|
454
|
+
}
|
|
455
|
+
// Create thumbnail canvas
|
|
456
|
+
const canvas = document.createElement('canvas');
|
|
457
|
+
canvas.width = scaledViewport.width;
|
|
458
|
+
canvas.height = scaledViewport.height;
|
|
459
|
+
canvas.style.cssText = 'display: block; width: 100%; height: auto;';
|
|
460
|
+
// Page number label (match Chrome style)
|
|
461
|
+
const label = document.createElement('div');
|
|
462
|
+
label.textContent = pageNum.toString();
|
|
463
|
+
label.style.cssText = \`
|
|
464
|
+
text-align: center;
|
|
465
|
+
color: #dadce0;
|
|
466
|
+
font-size: 13px;
|
|
467
|
+
padding: 5px;
|
|
468
|
+
background: #3f3f3f;
|
|
469
|
+
font-family: 'Roboto', Arial, sans-serif;
|
|
470
|
+
\`;
|
|
471
|
+
thumbWrapper.appendChild(canvas);
|
|
472
|
+
thumbWrapper.appendChild(label);
|
|
473
|
+
this._sidebar.appendChild(thumbWrapper);
|
|
474
|
+
this._thumbnailElements.push(thumbWrapper);
|
|
475
|
+
// Click handler to jump to page
|
|
476
|
+
thumbWrapper.addEventListener('click', () => {
|
|
477
|
+
this._scrollToPage(pageNum);
|
|
478
|
+
});
|
|
479
|
+
// Render thumbnail
|
|
480
|
+
const context = canvas.getContext('2d');
|
|
481
|
+
if (!context)
|
|
482
|
+
return;
|
|
483
|
+
await page.render({
|
|
484
|
+
canvasContext: context,
|
|
485
|
+
viewport: scaledViewport
|
|
486
|
+
}).promise;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Scrolls to a specific page
|
|
490
|
+
*/
|
|
491
|
+
_scrollToPage(pageNum) {
|
|
492
|
+
if (!this._mainContent)
|
|
493
|
+
return;
|
|
494
|
+
const pageElement = this._mainContent.querySelector(\`[data-page-number="\${pageNum}"]\`);
|
|
495
|
+
if (pageElement) {
|
|
496
|
+
const rect = pageElement.getBoundingClientRect();
|
|
497
|
+
// Offset by toolbar height (56px) so the page isn't hidden under the sticky toolbar
|
|
498
|
+
window.scrollBy({ top: rect.top - 56, behavior: 'smooth' });
|
|
499
|
+
this._updateCurrentPage(pageNum);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Updates the current page highlight in sidebar (match Chrome's blue highlight)
|
|
504
|
+
*/
|
|
505
|
+
_updateCurrentPage(pageNum) {
|
|
506
|
+
if (this._currentPage === pageNum)
|
|
507
|
+
return;
|
|
508
|
+
// Remove highlight from old page
|
|
509
|
+
if (this._thumbnailElements[this._currentPage - 1]) {
|
|
510
|
+
this._thumbnailElements[this._currentPage - 1].style.borderColor = 'transparent';
|
|
511
|
+
}
|
|
512
|
+
// Add highlight to new page (match Chrome's blue: #1a73e8)
|
|
513
|
+
if (this._thumbnailElements[pageNum - 1]) {
|
|
514
|
+
this._thumbnailElements[pageNum - 1].style.borderColor = '#1a73e8';
|
|
515
|
+
}
|
|
516
|
+
this._currentPage = pageNum;
|
|
517
|
+
// Update toolbar page display
|
|
518
|
+
if (this._pageDisplay) {
|
|
519
|
+
this._pageDisplay.textContent = \`\${pageNum} / \${this._pdfDoc.numPages}\`;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Creates a toolbar button with consistent styling
|
|
524
|
+
*/
|
|
525
|
+
_createToolbarButton(icon, title, onClick) {
|
|
526
|
+
const button = document.createElement('button');
|
|
527
|
+
button.textContent = icon;
|
|
528
|
+
button.title = title;
|
|
529
|
+
button.style.cssText = \`
|
|
530
|
+
background: transparent;
|
|
531
|
+
border: none;
|
|
532
|
+
color: #e8eaed;
|
|
533
|
+
cursor: pointer;
|
|
534
|
+
padding: 6px 8px;
|
|
535
|
+
border-radius: 4px;
|
|
536
|
+
font-size: 16px;
|
|
537
|
+
line-height: 1;
|
|
538
|
+
display: flex;
|
|
539
|
+
align-items: center;
|
|
540
|
+
justify-content: center;
|
|
541
|
+
min-width: 32px;
|
|
542
|
+
height: 32px;
|
|
543
|
+
transition: background-color 0.2s;
|
|
544
|
+
\`;
|
|
545
|
+
// Hover effect
|
|
546
|
+
button.addEventListener('mouseenter', () => {
|
|
547
|
+
button.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
|
|
548
|
+
});
|
|
549
|
+
button.addEventListener('mouseleave', () => {
|
|
550
|
+
button.style.backgroundColor = 'transparent';
|
|
551
|
+
});
|
|
552
|
+
// Click handler
|
|
553
|
+
button.addEventListener('click', (e) => {
|
|
554
|
+
e.preventDefault();
|
|
555
|
+
onClick();
|
|
556
|
+
});
|
|
557
|
+
return button;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Zoom to a specific level (1.0 = 100%)
|
|
561
|
+
*/
|
|
562
|
+
async _zoom(newZoom) {
|
|
563
|
+
if (!this._pdfDoc || !this._mainContent) {
|
|
564
|
+
console.warn('[PDF.js] Cannot zoom: PDF not loaded');
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
this._currentZoom = Math.max(0.25, Math.min(4.0, newZoom));
|
|
568
|
+
if (this._zoomDisplay) {
|
|
569
|
+
this._zoomDisplay.textContent = \`\${Math.round(this._currentZoom * 100)}%\`;
|
|
570
|
+
}
|
|
571
|
+
console.log(\`[PDF.js] Zooming to \${Math.round(this._currentZoom * 100)}%...\`);
|
|
572
|
+
await this._rerenderPages();
|
|
573
|
+
console.log('[PDF.js] ✅ Zoom complete');
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Re-renders all pages (used by zoom and two-page view toggle)
|
|
577
|
+
*/
|
|
578
|
+
async _rerenderPages() {
|
|
579
|
+
if (!this._pdfDoc || !this._mainContent)
|
|
580
|
+
return;
|
|
581
|
+
const scrollPercentage = window.scrollY / (document.body.scrollHeight || 1);
|
|
582
|
+
if (this._twoPageView) {
|
|
583
|
+
this._mainContent.style.cssText = \`
|
|
584
|
+
flex: 1;
|
|
585
|
+
overflow: visible;
|
|
586
|
+
background-color: #525252;
|
|
587
|
+
position: relative;
|
|
588
|
+
padding: 0;
|
|
589
|
+
box-sizing: border-box;
|
|
590
|
+
display: grid;
|
|
591
|
+
grid-template-columns: 1fr 1fr;
|
|
592
|
+
align-items: start;
|
|
593
|
+
\`;
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
this._mainContent.style.cssText = \`
|
|
597
|
+
flex: 1;
|
|
598
|
+
overflow: visible;
|
|
599
|
+
background-color: #525252;
|
|
600
|
+
position: relative;
|
|
601
|
+
padding: 0;
|
|
602
|
+
box-sizing: border-box;
|
|
603
|
+
\`;
|
|
604
|
+
}
|
|
605
|
+
this._mainContent.innerHTML = '';
|
|
606
|
+
this._canvasElements = [];
|
|
607
|
+
for (let pageNum = 1; pageNum <= this._pdfDoc.numPages; pageNum++) {
|
|
608
|
+
await this._renderPage(pageNum);
|
|
609
|
+
}
|
|
610
|
+
setTimeout(() => {
|
|
611
|
+
window.scrollTo(0, scrollPercentage * document.body.scrollHeight);
|
|
612
|
+
}, 100);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Fit page to available width
|
|
616
|
+
*/
|
|
617
|
+
_fitToPage() {
|
|
618
|
+
if (!this._mainContent)
|
|
619
|
+
return;
|
|
620
|
+
// Reset zoom to fit width (100% is already calculated to fit)
|
|
621
|
+
this._zoom(1.0);
|
|
622
|
+
console.log('[PDF.js] Fit to page');
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Rotate PDF pages clockwise by 90 degrees
|
|
626
|
+
*/
|
|
627
|
+
_rotate() {
|
|
628
|
+
this._rotation = (this._rotation + 90) % 360;
|
|
629
|
+
// Apply rotation transform to all pages
|
|
630
|
+
const pages = this._mainContent?.querySelectorAll('[data-page-number]');
|
|
631
|
+
if (pages) {
|
|
632
|
+
pages.forEach((page) => {
|
|
633
|
+
page.style.transform = \`rotate(\${this._rotation}deg)\`;
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
console.log(\`[PDF.js] Rotated to \${this._rotation} degrees\`);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Download the PDF file
|
|
640
|
+
*/
|
|
641
|
+
_download() {
|
|
642
|
+
if (!this._pdfDataUrl) {
|
|
643
|
+
console.error('[PDF.js] No PDF data URL available for download');
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
// Create a temporary download link
|
|
647
|
+
const link = document.createElement('a');
|
|
648
|
+
link.href = this._pdfDataUrl;
|
|
649
|
+
link.download = this._filename;
|
|
650
|
+
link.style.display = 'none';
|
|
651
|
+
document.body.appendChild(link);
|
|
652
|
+
link.click();
|
|
653
|
+
document.body.removeChild(link);
|
|
654
|
+
console.log(\`[PDF.js] Downloaded: \${this._filename}\`);
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Print the PDF by rendering all canvas pages into a new window
|
|
658
|
+
*/
|
|
659
|
+
_print() {
|
|
660
|
+
if (!this._canvasElements.length) {
|
|
661
|
+
console.error('[PDF.js] No pages rendered to print');
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const printWindow = window.open('', '_blank');
|
|
665
|
+
if (!printWindow) {
|
|
666
|
+
console.warn('[PDF.js] Print window blocked by browser');
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const doc = printWindow.document;
|
|
670
|
+
doc.write(\`<!DOCTYPE html><html><head>
|
|
671
|
+
<title>\${this._filename}</title>
|
|
672
|
+
<style>
|
|
673
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
674
|
+
body { background: white; }
|
|
675
|
+
img { display: block; width: 100%; page-break-after: always; page-break-inside: avoid; }
|
|
676
|
+
img:last-child { page-break-after: avoid; }
|
|
677
|
+
</style>
|
|
678
|
+
</head><body>\`);
|
|
679
|
+
for (const canvas of this._canvasElements) {
|
|
680
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
681
|
+
doc.write(\`<img src="\${dataUrl}">\`);
|
|
682
|
+
}
|
|
683
|
+
doc.write('</body></html>');
|
|
684
|
+
doc.close();
|
|
685
|
+
// Trigger print once the window has loaded
|
|
686
|
+
printWindow.onload = () => {
|
|
687
|
+
printWindow.print();
|
|
688
|
+
printWindow.close();
|
|
689
|
+
};
|
|
690
|
+
// Fallback in case onload doesn't fire (e.g. data already loaded synchronously)
|
|
691
|
+
setTimeout(() => {
|
|
692
|
+
if (!printWindow.closed) {
|
|
693
|
+
printWindow.print();
|
|
694
|
+
printWindow.close();
|
|
695
|
+
}
|
|
696
|
+
}, 1500);
|
|
697
|
+
console.log('[PDF.js] Print window opened');
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Toggles the "more options" dropdown menu matching Chrome's PDF viewer
|
|
701
|
+
*/
|
|
702
|
+
_toggleMoreMenu(anchorElement) {
|
|
703
|
+
if (this._moreMenuElement) {
|
|
704
|
+
this._closeMoreMenu();
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const menu = document.createElement('div');
|
|
708
|
+
this._moreMenuElement = menu;
|
|
709
|
+
menu.style.cssText = \`
|
|
710
|
+
position: fixed;
|
|
711
|
+
background: #202124;
|
|
712
|
+
border-radius: 4px;
|
|
713
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.6);
|
|
714
|
+
z-index: 2147483648;
|
|
715
|
+
min-width: 220px;
|
|
716
|
+
padding: 4px 0;
|
|
717
|
+
font-family: 'Roboto', Arial, sans-serif;
|
|
718
|
+
font-size: 14px;
|
|
719
|
+
color: #e8eaed;
|
|
720
|
+
\`;
|
|
721
|
+
const rect = anchorElement.getBoundingClientRect();
|
|
722
|
+
menu.style.top = \`\${rect.bottom + 4}px\`;
|
|
723
|
+
menu.style.right = \`\${window.innerWidth - rect.right}px\`;
|
|
724
|
+
const addMenuItem = (text, checked, onClick) => {
|
|
725
|
+
const item = document.createElement('div');
|
|
726
|
+
item.style.cssText = \`
|
|
727
|
+
padding: 10px 16px 10px 44px;
|
|
728
|
+
cursor: pointer;
|
|
729
|
+
position: relative;
|
|
730
|
+
white-space: nowrap;
|
|
731
|
+
\`;
|
|
732
|
+
if (checked !== null) {
|
|
733
|
+
const checkEl = document.createElement('span');
|
|
734
|
+
checkEl.textContent = checked ? '✓' : '';
|
|
735
|
+
checkEl.style.cssText = \`
|
|
736
|
+
position: absolute;
|
|
737
|
+
left: 16px;
|
|
738
|
+
top: 50%;
|
|
739
|
+
transform: translateY(-50%);
|
|
740
|
+
font-size: 14px;
|
|
741
|
+
\`;
|
|
742
|
+
item.appendChild(checkEl);
|
|
743
|
+
}
|
|
744
|
+
const label = document.createElement('span');
|
|
745
|
+
label.textContent = text;
|
|
746
|
+
item.appendChild(label);
|
|
747
|
+
item.addEventListener('mouseenter', () => {
|
|
748
|
+
item.style.backgroundColor = 'rgba(255,255,255,0.1)';
|
|
749
|
+
});
|
|
750
|
+
item.addEventListener('mouseleave', () => {
|
|
751
|
+
item.style.backgroundColor = 'transparent';
|
|
752
|
+
});
|
|
753
|
+
item.addEventListener('click', () => {
|
|
754
|
+
this._closeMoreMenu();
|
|
755
|
+
onClick();
|
|
756
|
+
});
|
|
757
|
+
menu.appendChild(item);
|
|
758
|
+
return item;
|
|
759
|
+
};
|
|
760
|
+
const addDivider = () => {
|
|
761
|
+
const d = document.createElement('div');
|
|
762
|
+
d.style.cssText = 'height: 1px; background: rgba(255,255,255,0.15); margin: 4px 0;';
|
|
763
|
+
menu.appendChild(d);
|
|
764
|
+
};
|
|
765
|
+
addMenuItem('Two page view', this._twoPageView, () => {
|
|
766
|
+
this._twoPageView = !this._twoPageView;
|
|
767
|
+
this._rerenderPages();
|
|
768
|
+
});
|
|
769
|
+
addMenuItem('Annotations', this._annotationsVisible, () => {
|
|
770
|
+
this._annotationsVisible = !this._annotationsVisible;
|
|
771
|
+
console.log(\`[PDF.js] Annotations \${this._annotationsVisible ? 'shown' : 'hidden'}\`);
|
|
772
|
+
});
|
|
773
|
+
addDivider();
|
|
774
|
+
addMenuItem('Present', null, () => {
|
|
775
|
+
this._present();
|
|
776
|
+
});
|
|
777
|
+
addMenuItem('Document properties', null, () => {
|
|
778
|
+
this._showDocumentProperties();
|
|
779
|
+
});
|
|
780
|
+
// Close on outside click — registered after current event loop to avoid immediately closing
|
|
781
|
+
const onOutsideClick = (e) => {
|
|
782
|
+
if (!menu.contains(e.target) && e.target !== anchorElement) {
|
|
783
|
+
this._closeMoreMenu();
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
setTimeout(() => {
|
|
787
|
+
document.addEventListener('mousedown', onOutsideClick, true);
|
|
788
|
+
this._moreMenuCleanup = () => document.removeEventListener('mousedown', onOutsideClick, true);
|
|
789
|
+
}, 0);
|
|
790
|
+
document.body.appendChild(menu);
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Closes the more options dropdown menu
|
|
794
|
+
*/
|
|
795
|
+
_closeMoreMenu() {
|
|
796
|
+
if (this._moreMenuElement) {
|
|
797
|
+
this._moreMenuElement.remove();
|
|
798
|
+
this._moreMenuElement = null;
|
|
799
|
+
}
|
|
800
|
+
if (this._moreMenuCleanup) {
|
|
801
|
+
this._moreMenuCleanup();
|
|
802
|
+
this._moreMenuCleanup = null;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Present mode — not yet implemented.
|
|
807
|
+
*/
|
|
808
|
+
_present() {
|
|
809
|
+
console.log('[PDF.js] Present: not yet implemented');
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Shows a dialog with document metadata matching Chrome's "Document properties"
|
|
813
|
+
*/
|
|
814
|
+
async _showDocumentProperties() {
|
|
815
|
+
if (!this._pdfDoc)
|
|
816
|
+
return;
|
|
817
|
+
let info = {};
|
|
818
|
+
try {
|
|
819
|
+
const metadata = await this._pdfDoc.getMetadata();
|
|
820
|
+
info = metadata.info || {};
|
|
821
|
+
}
|
|
822
|
+
catch (e) {
|
|
823
|
+
// metadata unavailable
|
|
824
|
+
}
|
|
825
|
+
// Calculate file size from data URL base64
|
|
826
|
+
let fileSize = '-';
|
|
827
|
+
if (this._pdfDataUrl) {
|
|
828
|
+
try {
|
|
829
|
+
const base64 = this._pdfDataUrl.split(',')[1];
|
|
830
|
+
if (base64) {
|
|
831
|
+
const bytes = Math.ceil((base64.length * 3) / 4);
|
|
832
|
+
fileSize = bytes >= 1024 * 1024
|
|
833
|
+
? \`\${(bytes / (1024 * 1024)).toFixed(1)} MB\`
|
|
834
|
+
: \`\${(bytes / 1024).toFixed(1)} KB\`;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
catch (e) { /* ignore */ }
|
|
838
|
+
}
|
|
839
|
+
// Parse PDF date string (D:YYYYMMDDHHmmss...) to locale format
|
|
840
|
+
const formatPdfDate = (raw) => {
|
|
841
|
+
if (!raw)
|
|
842
|
+
return '-';
|
|
843
|
+
const m = raw.match(/^D:(\\d{4})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})/);
|
|
844
|
+
if (!m)
|
|
845
|
+
return raw;
|
|
846
|
+
const date = new Date(\`\${m[1]}-\${m[2]}-\${m[3]}T\${m[4]}:\${m[5]}:\${m[6]}\`);
|
|
847
|
+
return isNaN(date.getTime()) ? raw : date.toLocaleString();
|
|
848
|
+
};
|
|
849
|
+
// Get page dimensions from first page and convert points → inches
|
|
850
|
+
let pageSize = '-';
|
|
851
|
+
try {
|
|
852
|
+
const firstPage = await this._pdfDoc.getPage(1);
|
|
853
|
+
const vp = firstPage.getViewport({ scale: 1.0 });
|
|
854
|
+
const wIn = (vp.width / 72).toFixed(2);
|
|
855
|
+
const hIn = (vp.height / 72).toFixed(2);
|
|
856
|
+
const orientation = vp.width > vp.height ? 'landscape' : 'portrait';
|
|
857
|
+
pageSize = \`\${wIn} × \${hIn} in (\${orientation})\`;
|
|
858
|
+
}
|
|
859
|
+
catch (e) { /* ignore */ }
|
|
860
|
+
// Grouped sections matching Chrome's layout exactly
|
|
861
|
+
const sections = [
|
|
862
|
+
[
|
|
863
|
+
['File name:', this._filename],
|
|
864
|
+
['File size:', fileSize],
|
|
865
|
+
],
|
|
866
|
+
[
|
|
867
|
+
['Title:', info['Title'] || '-'],
|
|
868
|
+
['Author:', info['Author'] || '-'],
|
|
869
|
+
['Subject:', info['Subject'] || '-'],
|
|
870
|
+
['Keywords:', info['Keywords'] || '-'],
|
|
871
|
+
['Created:', formatPdfDate(info['CreationDate'] || '')],
|
|
872
|
+
['Modified:', formatPdfDate(info['ModDate'] || '')],
|
|
873
|
+
['Application:', info['Creator'] || '-'],
|
|
874
|
+
],
|
|
875
|
+
[
|
|
876
|
+
['PDF producer:', info['Producer'] || '-'],
|
|
877
|
+
['PDF version:', info['PDFFormatVersion'] || '-'],
|
|
878
|
+
['Page count:', \`\${this._pdfDoc.numPages}\`],
|
|
879
|
+
['Page size:', pageSize],
|
|
880
|
+
],
|
|
881
|
+
[
|
|
882
|
+
['Fast web view:', 'No'],
|
|
883
|
+
],
|
|
884
|
+
];
|
|
885
|
+
const overlay = document.createElement('div');
|
|
886
|
+
overlay.style.cssText = \`
|
|
887
|
+
position: fixed;
|
|
888
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
889
|
+
background: rgba(0,0,0,0.5);
|
|
890
|
+
z-index: 2147483649;
|
|
891
|
+
display: flex;
|
|
892
|
+
align-items: center;
|
|
893
|
+
justify-content: center;
|
|
894
|
+
\`;
|
|
895
|
+
const dialog = document.createElement('div');
|
|
896
|
+
dialog.style.cssText = \`
|
|
897
|
+
background: #3c4043;
|
|
898
|
+
border-radius: 12px;
|
|
899
|
+
padding: 24px 24px 16px;
|
|
900
|
+
min-width: 380px;
|
|
901
|
+
max-width: 500px;
|
|
902
|
+
color: #e8eaed;
|
|
903
|
+
font-family: 'Roboto', Arial, sans-serif;
|
|
904
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
|
905
|
+
\`;
|
|
906
|
+
const titleEl = document.createElement('h3');
|
|
907
|
+
titleEl.textContent = 'Document properties';
|
|
908
|
+
titleEl.style.cssText = 'margin: 0 0 16px; font-size: 18px; font-weight: 500;';
|
|
909
|
+
dialog.appendChild(titleEl);
|
|
910
|
+
const addRow = (label, value) => {
|
|
911
|
+
const row = document.createElement('div');
|
|
912
|
+
row.style.cssText = 'display: flex; padding: 7px 0; font-size: 13px;';
|
|
913
|
+
const labelEl = document.createElement('span');
|
|
914
|
+
labelEl.textContent = label;
|
|
915
|
+
labelEl.style.cssText = 'min-width: 140px; flex-shrink: 0;';
|
|
916
|
+
const valueEl = document.createElement('span');
|
|
917
|
+
valueEl.textContent = value;
|
|
918
|
+
valueEl.style.wordBreak = 'break-all';
|
|
919
|
+
row.appendChild(labelEl);
|
|
920
|
+
row.appendChild(valueEl);
|
|
921
|
+
dialog.appendChild(row);
|
|
922
|
+
};
|
|
923
|
+
const addDivider = () => {
|
|
924
|
+
const d = document.createElement('div');
|
|
925
|
+
d.style.cssText = 'height: 1px; background: rgba(255,255,255,0.15); margin: 6px 0;';
|
|
926
|
+
dialog.appendChild(d);
|
|
927
|
+
};
|
|
928
|
+
for (let i = 0; i < sections.length; i++) {
|
|
929
|
+
for (const [label, value] of sections[i]) {
|
|
930
|
+
addRow(label, value);
|
|
931
|
+
}
|
|
932
|
+
if (i < sections.length - 1) {
|
|
933
|
+
addDivider();
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
const closeBtn = document.createElement('button');
|
|
937
|
+
closeBtn.textContent = 'Close';
|
|
938
|
+
closeBtn.style.cssText = \`
|
|
939
|
+
display: block;
|
|
940
|
+
margin: 20px 0 0 auto;
|
|
941
|
+
padding: 10px 28px;
|
|
942
|
+
background: #8ab4f8;
|
|
943
|
+
border: none;
|
|
944
|
+
border-radius: 24px;
|
|
945
|
+
color: #202124;
|
|
946
|
+
font-size: 14px;
|
|
947
|
+
font-weight: 500;
|
|
948
|
+
cursor: pointer;
|
|
949
|
+
font-family: 'Roboto', Arial, sans-serif;
|
|
950
|
+
\`;
|
|
951
|
+
closeBtn.addEventListener('click', () => overlay.remove());
|
|
952
|
+
dialog.appendChild(closeBtn);
|
|
953
|
+
overlay.appendChild(dialog);
|
|
954
|
+
overlay.addEventListener('click', (e) => {
|
|
955
|
+
if (e.target === overlay)
|
|
956
|
+
overlay.remove();
|
|
957
|
+
});
|
|
958
|
+
document.body.appendChild(overlay);
|
|
959
|
+
console.log('[PDF.js] Document properties dialog opened');
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Sets up scroll synchronization between the document scroll and the sidebar thumbnail highlight.
|
|
963
|
+
* Pages are in the document flow so we listen on window, not on _mainContent.
|
|
964
|
+
*/
|
|
965
|
+
_setupScrollSync() {
|
|
966
|
+
if (!this._mainContent)
|
|
967
|
+
return;
|
|
968
|
+
window.addEventListener('scroll', () => {
|
|
969
|
+
if (!this._mainContent)
|
|
970
|
+
return;
|
|
971
|
+
const pages = this._mainContent.querySelectorAll('[data-page-number]');
|
|
972
|
+
const toolbarBottom = 56; // toolbar height
|
|
973
|
+
for (let i = 0; i < pages.length; i++) {
|
|
974
|
+
const pageElement = pages[i];
|
|
975
|
+
const rect = pageElement.getBoundingClientRect();
|
|
976
|
+
if (rect.top <= toolbarBottom + 100 && rect.bottom > toolbarBottom) {
|
|
977
|
+
const pageNum = parseInt(pageElement.getAttribute('data-page-number') || '1');
|
|
978
|
+
this._updateCurrentPage(pageNum);
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}, { passive: true });
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Cleanup resources
|
|
986
|
+
*/
|
|
987
|
+
cleanup() {
|
|
988
|
+
if (this._pdfDoc) {
|
|
989
|
+
this._pdfDoc.destroy();
|
|
990
|
+
this._pdfDoc = null;
|
|
991
|
+
}
|
|
992
|
+
this._closeMoreMenu();
|
|
993
|
+
this._canvasElements = [];
|
|
994
|
+
this._thumbnailElements = [];
|
|
995
|
+
this._toolbar = null;
|
|
996
|
+
this._sidebar = null;
|
|
997
|
+
this._mainContent = null;
|
|
998
|
+
this._currentPage = 1;
|
|
999
|
+
this._container.innerHTML = '';
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// =============================================================================
|
|
1004
|
+
// Helper Functions (execution-specific)
|
|
1005
|
+
// =============================================================================
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Fetches a PDF from a URL and converts it to a data URL
|
|
1009
|
+
* Uses fetch() which will be intercepted by route handler during execution
|
|
1010
|
+
*/
|
|
1011
|
+
async function fetchPdfAsDataUrl(pdfUrl) {
|
|
1012
|
+
try {
|
|
1013
|
+
console.log('[PW-PDF] Fetching PDF:', pdfUrl.substring(0, 100));
|
|
1014
|
+
|
|
1015
|
+
// During execution, this fetch will be intercepted by Playwright's route handler
|
|
1016
|
+
const response = await fetch(pdfUrl);
|
|
1017
|
+
|
|
1018
|
+
if (!response.ok) {
|
|
1019
|
+
throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1023
|
+
const base64 = btoa(
|
|
1024
|
+
new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), '')
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
const dataUrl = \`data:application/pdf;base64,\${base64}\`;
|
|
1028
|
+
console.log('[PW-PDF] ✅ PDF fetched successfully');
|
|
1029
|
+
|
|
1030
|
+
return dataUrl;
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
console.error('[PW-PDF] ❌ Failed to fetch PDF:', error);
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Checks if the current page itself is a PDF document
|
|
1039
|
+
*/
|
|
1040
|
+
function isCurrentPagePdf() {
|
|
1041
|
+
const url = window.location.href;
|
|
1042
|
+
|
|
1043
|
+
// Check URL ends with .pdf
|
|
1044
|
+
if (url.toLowerCase().endsWith('.pdf')) {
|
|
1045
|
+
console.log('[PW-PDF] URL ends with .pdf:', url);
|
|
1046
|
+
return true;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Check for PDF plugin embed that takes up the whole page
|
|
1050
|
+
const fullPageEmbed = document.querySelector('embed[type="application/pdf"]');
|
|
1051
|
+
if (fullPageEmbed && document.body.children.length === 1) {
|
|
1052
|
+
console.log('[PW-PDF] Found full-page PDF embed');
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Check if URL contains .pdf (including S3 URLs with .pdf)
|
|
1057
|
+
if (url.toLowerCase().includes('.pdf')) {
|
|
1058
|
+
console.log('[PW-PDF] PDF URL detected:', url);
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Replaces the entire page with PDF.js viewer when the page itself is a PDF
|
|
1067
|
+
*/
|
|
1068
|
+
async function replacePdfPage() {
|
|
1069
|
+
try {
|
|
1070
|
+
// Get PDF URL from current page URL
|
|
1071
|
+
const pdfUrl = window.location.href;
|
|
1072
|
+
console.log('[PW-PDF] Replacing full-page PDF with PDF.js viewer:', pdfUrl.substring(0, 100));
|
|
1073
|
+
|
|
1074
|
+
// Fetch PDF
|
|
1075
|
+
const dataUrl = await fetchPdfAsDataUrl(pdfUrl);
|
|
1076
|
+
if (!dataUrl) {
|
|
1077
|
+
throw new Error('Failed to fetch PDF');
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Clear the document body
|
|
1081
|
+
document.body.innerHTML = '';
|
|
1082
|
+
|
|
1083
|
+
// Create container for PDF.js viewer
|
|
1084
|
+
const container = document.createElement('div');
|
|
1085
|
+
container.setAttribute('data-pw-pdf-viewer', 'true');
|
|
1086
|
+
container.id = 'pw-pdf-viewer-container';
|
|
1087
|
+
container.style.cssText = \`
|
|
1088
|
+
position: fixed;
|
|
1089
|
+
top: 0;
|
|
1090
|
+
left: 0;
|
|
1091
|
+
width: 100%;
|
|
1092
|
+
height: 100%;
|
|
1093
|
+
z-index: 2147483647;
|
|
1094
|
+
background: #525252;
|
|
1095
|
+
\`;
|
|
1096
|
+
|
|
1097
|
+
document.body.appendChild(container);
|
|
1098
|
+
|
|
1099
|
+
// Extract filename from URL
|
|
1100
|
+
let filename = 'Document.pdf';
|
|
1101
|
+
try {
|
|
1102
|
+
const url = new URL(pdfUrl);
|
|
1103
|
+
const pathname = url.pathname;
|
|
1104
|
+
const lastSlash = pathname.lastIndexOf('/');
|
|
1105
|
+
if (lastSlash !== -1) {
|
|
1106
|
+
filename = pathname.substring(lastSlash + 1);
|
|
1107
|
+
filename = decodeURIComponent(filename);
|
|
1108
|
+
}
|
|
1109
|
+
} catch (e) {
|
|
1110
|
+
console.warn('[PW-PDF] Failed to extract filename from URL:', e);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Create PDF.js viewer
|
|
1114
|
+
const viewer = new PdfJsViewer(container);
|
|
1115
|
+
|
|
1116
|
+
// Render the PDF
|
|
1117
|
+
await viewer.renderPdf({
|
|
1118
|
+
pdfDataUrl: dataUrl,
|
|
1119
|
+
filename,
|
|
1120
|
+
onReady: () => {
|
|
1121
|
+
console.log('[PW-PDF] ✅ Full-page PDF replaced with PDF.js viewer');
|
|
1122
|
+
},
|
|
1123
|
+
onError: (error) => {
|
|
1124
|
+
console.error('[PW-PDF] ❌ Failed to render full-page PDF:', error);
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
return true;
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
console.error('[PW-PDF] Error replacing full-page PDF:', error);
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Replaces a single PDF embed with PDF.js viewer
|
|
1137
|
+
*/
|
|
1138
|
+
async function replacePdfEmbed(embed) {
|
|
1139
|
+
try {
|
|
1140
|
+
// Get PDF URL
|
|
1141
|
+
let pdfUrl = embed.getAttribute('src');
|
|
1142
|
+
|
|
1143
|
+
// If src is about:blank, use the page URL (the page itself is the PDF)
|
|
1144
|
+
if (!pdfUrl || pdfUrl === 'about:blank') {
|
|
1145
|
+
console.log('[PW-PDF] Embed has src="about:blank", using page URL as PDF URL...');
|
|
1146
|
+
pdfUrl = window.location.href;
|
|
1147
|
+
|
|
1148
|
+
// Verify it looks like a PDF URL
|
|
1149
|
+
if (!pdfUrl.includes('.pdf')) {
|
|
1150
|
+
console.log('[PW-PDF] Page URL does not appear to be a PDF, skipping');
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
console.log('[PW-PDF] Using page URL as PDF:', pdfUrl.substring(0, 100));
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
console.log('[PW-PDF] Replacing PDF embed with PDF.js viewer:', pdfUrl.substring(0, 100));
|
|
1158
|
+
|
|
1159
|
+
// Fetch PDF
|
|
1160
|
+
const dataUrl = await fetchPdfAsDataUrl(pdfUrl);
|
|
1161
|
+
if (!dataUrl) {
|
|
1162
|
+
throw new Error('Failed to fetch PDF');
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Create container for PDF.js viewer
|
|
1166
|
+
const container = document.createElement('div');
|
|
1167
|
+
container.setAttribute('data-pw-pdf-viewer', 'true');
|
|
1168
|
+
container.style.cssText = \`
|
|
1169
|
+
position: absolute;
|
|
1170
|
+
top: \${embed.offsetTop}px;
|
|
1171
|
+
left: \${embed.offsetLeft}px;
|
|
1172
|
+
width: \${embed.offsetWidth || 800}px;
|
|
1173
|
+
height: \${embed.offsetHeight || 600}px;
|
|
1174
|
+
z-index: 2147483647;
|
|
1175
|
+
background: #525252;
|
|
1176
|
+
\`;
|
|
1177
|
+
|
|
1178
|
+
// Insert container next to embed
|
|
1179
|
+
const parent = embed.parentNode;
|
|
1180
|
+
if (!parent) return false;
|
|
1181
|
+
|
|
1182
|
+
parent.insertBefore(container, embed);
|
|
1183
|
+
|
|
1184
|
+
// Hide original embed
|
|
1185
|
+
embed.style.display = 'none';
|
|
1186
|
+
|
|
1187
|
+
// Extract filename from URL
|
|
1188
|
+
let filename = 'Document.pdf';
|
|
1189
|
+
try {
|
|
1190
|
+
const url = new URL(pdfUrl);
|
|
1191
|
+
const pathname = url.pathname;
|
|
1192
|
+
const lastSlash = pathname.lastIndexOf('/');
|
|
1193
|
+
if (lastSlash !== -1) {
|
|
1194
|
+
filename = pathname.substring(lastSlash + 1);
|
|
1195
|
+
filename = decodeURIComponent(filename);
|
|
1196
|
+
}
|
|
1197
|
+
} catch (e) {
|
|
1198
|
+
console.warn('[PW-PDF] Failed to extract filename from URL:', e);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Create PDF.js viewer
|
|
1202
|
+
const viewer = new PdfJsViewer(container);
|
|
1203
|
+
|
|
1204
|
+
// Render the PDF
|
|
1205
|
+
await viewer.renderPdf({
|
|
1206
|
+
pdfDataUrl: dataUrl,
|
|
1207
|
+
filename,
|
|
1208
|
+
onReady: () => {
|
|
1209
|
+
console.log('[PW-PDF] ✅ PDF embed replaced successfully');
|
|
1210
|
+
},
|
|
1211
|
+
onError: (error) => {
|
|
1212
|
+
console.error('[PW-PDF] ❌ Failed to render PDF:', error);
|
|
1213
|
+
// Restore original embed on error
|
|
1214
|
+
embed.style.display = '';
|
|
1215
|
+
if (container.parentNode) {
|
|
1216
|
+
container.parentNode.removeChild(container);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
return true;
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
console.error('[PW-PDF] Error replacing PDF embed:', error);
|
|
1224
|
+
return false;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* Detects and replaces PDF embeds with PDF.js viewers
|
|
1230
|
+
*/
|
|
1231
|
+
async function detectAndReplacePdfs() {
|
|
1232
|
+
console.log('[PW-PDF] Scanning for PDF embeds...');
|
|
1233
|
+
|
|
1234
|
+
// Check if we've already replaced a full-page PDF (prevent infinite loop)
|
|
1235
|
+
if (window.__pwPdfPageReplaced) {
|
|
1236
|
+
console.log('[PW-PDF] PDF page already replaced, skipping detection');
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Check if the current page itself is a PDF document
|
|
1241
|
+
const isPdfPage = isCurrentPagePdf();
|
|
1242
|
+
if (isPdfPage) {
|
|
1243
|
+
console.log('[PW-PDF] Current page is a PDF document, replacing with PDF.js viewer...');
|
|
1244
|
+
window.__pwPdfPageReplaced = true;
|
|
1245
|
+
await replacePdfPage();
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// Find all embed and iframe elements
|
|
1250
|
+
const embeds = document.querySelectorAll('embed[type="application/pdf"], iframe[src*=".pdf"]');
|
|
1251
|
+
|
|
1252
|
+
if (embeds.length === 0) {
|
|
1253
|
+
console.log('[PW-PDF] No PDF embeds found');
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
console.log(\`[PW-PDF] Found \${embeds.length} PDF embed(s)\`);
|
|
1258
|
+
|
|
1259
|
+
for (const embed of embeds) {
|
|
1260
|
+
await replacePdfEmbed(embed);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// =============================================================================
|
|
1265
|
+
// Window.open() Interception (for headless mode)
|
|
1266
|
+
// =============================================================================
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Sets up window.open() interception to detect PDF URLs
|
|
1270
|
+
*/
|
|
1271
|
+
function setupPdfPopupInterception() {
|
|
1272
|
+
const originalOpen = window.open;
|
|
1273
|
+
window.open = function(url, target, features) {
|
|
1274
|
+
// Check if URL is a PDF (ends with .pdf or contains PDF indicators)
|
|
1275
|
+
const isPdf = url && url.toLowerCase().includes('.pdf');
|
|
1276
|
+
|
|
1277
|
+
if (isPdf) {
|
|
1278
|
+
console.log('[PW-PDF] Intercepted window.open() to PDF:', url);
|
|
1279
|
+
|
|
1280
|
+
// Let the popup open normally (to about:blank or target URL)
|
|
1281
|
+
const popup = originalOpen.call(this, url, target, features);
|
|
1282
|
+
|
|
1283
|
+
// If popup opened successfully, store PDF URL for it to access
|
|
1284
|
+
if (popup && !popup.closed) {
|
|
1285
|
+
popup.__pw_pdfUrl = url;
|
|
1286
|
+
|
|
1287
|
+
// The popup will detect __pw_pdfUrl and render with PDF.js
|
|
1288
|
+
setTimeout(() => {
|
|
1289
|
+
if (popup.__pw_detectAndReplacePdfs) {
|
|
1290
|
+
popup.__pw_detectAndReplacePdfs();
|
|
1291
|
+
}
|
|
1292
|
+
}, 100);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
return popup;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Not a PDF, let window.open() work normally
|
|
1299
|
+
return originalOpen.apply(this, arguments);
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// =============================================================================
|
|
1304
|
+
// Expose Functions and Classes Globally
|
|
1305
|
+
// =============================================================================
|
|
1306
|
+
|
|
1307
|
+
window.PdfJsViewer = PdfJsViewer; // Expose class for direct instantiation
|
|
1308
|
+
window.__pw_detectAndReplacePdfs = detectAndReplacePdfs;
|
|
1309
|
+
window.__pw_setupPdfPopupInterception = setupPdfPopupInterception;
|
|
1310
|
+
|
|
1311
|
+
// =============================================================================
|
|
1312
|
+
// Auto-detect PDFs on page load
|
|
1313
|
+
// =============================================================================
|
|
1314
|
+
|
|
1315
|
+
// Auto-detect if current page is a PDF popup that needs rendering
|
|
1316
|
+
if (window.__pw_pdfUrl && !window.__pwPdfRendered) {
|
|
1317
|
+
window.__pwPdfRendered = true;
|
|
1318
|
+
|
|
1319
|
+
// Wait for DOM ready
|
|
1320
|
+
if (document.readyState === 'loading') {
|
|
1321
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1322
|
+
window.__pw_detectAndReplacePdfs();
|
|
1323
|
+
});
|
|
1324
|
+
} else {
|
|
1325
|
+
window.__pw_detectAndReplacePdfs();
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// Also auto-detect on every page load (for direct navigation to PDFs)
|
|
1330
|
+
// This runs after the page has fully loaded
|
|
1331
|
+
if (document.readyState === 'loading') {
|
|
1332
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1333
|
+
console.log('[PW-PDF] DOMContentLoaded - checking for PDFs...');
|
|
1334
|
+
window.__pw_detectAndReplacePdfs();
|
|
1335
|
+
});
|
|
1336
|
+
} else if (document.readyState === 'interactive' || document.readyState === 'complete') {
|
|
1337
|
+
// If DOM is already ready, run detection immediately
|
|
1338
|
+
console.log('[PW-PDF] DOM already ready - checking for PDFs...');
|
|
1339
|
+
setTimeout(() => {
|
|
1340
|
+
window.__pw_detectAndReplacePdfs();
|
|
1341
|
+
}, 100); // Small delay to let browser plugin initialize
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
console.log('[PW-PDF] PDF viewer injection script loaded');
|
|
1346
|
+
})();
|
|
1347
|
+
`;
|
|
1348
|
+
|
|
1349
|
+
module.exports = { PDF_VIEWER_INJECTION_SCRIPT };
|