ng2-pdfjs-viewer 25.0.11 → 25.0.13

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.
@@ -1,1967 +1,1967 @@
1
- (function() {
2
- 'use strict';
3
-
4
- // This will be wrapped in diagnostic logs check after diagnosticLogs is set
5
-
6
- // #region URL Security Validation
7
- let originalFileUrl = null;
8
- let urlValidationEnabled = true;
9
-
10
- function initializeUrlValidation() {
11
- // Get the initial file URL from query parameters
12
- const urlParams = new URLSearchParams(window.location.search);
13
- const currentFileUrl = urlParams.get('file');
14
-
15
- // Check if we already have a stored original URL
16
- const storedOriginalUrl = sessionStorage.getItem('ng2-pdfjs-viewer-original-file-url');
17
-
18
- if (storedOriginalUrl) {
19
- // Use the stored original URL (from previous page load)
20
- originalFileUrl = storedOriginalUrl;
21
- } else if (currentFileUrl) {
22
- // Store the current URL as the original (first time loading)
23
- originalFileUrl = currentFileUrl;
24
- sessionStorage.setItem('ng2-pdfjs-viewer-original-file-url', originalFileUrl);
25
- }
26
- }
27
-
28
- function validateCurrentUrl() {
29
- if (!urlValidationEnabled || !originalFileUrl) {
30
- return true; // Skip validation if disabled or no original URL
31
- }
32
-
33
- const currentFileParam = new URLSearchParams(window.location.search).get('file');
34
-
35
- // Compare the file parameter specifically
36
- if (currentFileParam !== originalFileUrl) {
37
- console.error('🚨 SECURITY ALERT: File URL has been modified!');
38
- console.error('Original:', originalFileUrl);
39
- console.error('Current:', currentFileParam);
40
-
41
- // Prevent loading and show error
42
- showSecurityError('Unauthorized file access detected. The file URL has been tampered with.');
43
- return false;
44
- }
45
-
46
- return true;
47
- }
48
-
49
- function showSecurityError(message) {
50
- // Simple console warning (less intrusive)
51
- console.warn('🚨 Security Warning:', message);
52
-
53
- // Send notification to parent window for Angular template handling
54
- if (window.parent && window.parent !== window) {
55
- window.parent.postMessage({
56
- type: 'ng2-pdfjs-viewer-security-warning',
57
- message: message,
58
- originalUrl: originalFileUrl,
59
- currentUrl: new URLSearchParams(window.location.search).get('file')
60
- }, '*');
61
- }
62
- }
63
-
64
- // Initialize URL validation on page load
65
- initializeUrlValidation();
66
-
67
- // Add validation check before PDF.js loads
68
- window.addEventListener('DOMContentLoaded', () => {
69
- if (!validateCurrentUrl()) {
70
- // Prevent PDF.js from loading if URL validation fails
71
- return;
72
- }
73
- });
74
-
75
- // Validate on page load
76
- window.addEventListener('load', () => {
77
- validateCurrentUrl();
78
- });
79
-
80
- // Also validate on hash change (in case user tries to modify URL)
81
- window.addEventListener('hashchange', () => {
82
- validateCurrentUrl();
83
- });
84
-
85
- // Validate on popstate (back/forward navigation)
86
- window.addEventListener('popstate', () => {
87
- validateCurrentUrl();
88
- });
89
-
90
- // Run validation immediately (in case page is already loaded)
91
- validateCurrentUrl();
92
- // #endregion
93
-
94
- // Override the webviewerloaded event to dispatch to parent window
95
- // This allows us to capture the event outside the iframe and set locale before PDF.js initializes
96
- const originalDispatchEvent = document.dispatchEvent;
97
- document.dispatchEvent = function(event) {
98
- if (event.type === 'webviewerloaded') {
99
- // Dispatch to parent window so it can be captured outside the iframe
100
- if (window.parent && window.parent !== window) {
101
- window.parent.document.dispatchEvent(event);
102
- }
103
- }
104
- return originalDispatchEvent.call(this, event);
105
- };
106
-
107
- // #region Constants and Configuration
108
- // Command tracking for zoom to prevent infinite loops
109
- const ZoomCommandTracker = {
110
- activeZoomCommand: false,
111
-
112
- markZoomCommandStart() {
113
- this.activeZoomCommand = true;
114
- },
115
-
116
- markZoomCommandEnd() {
117
- this.activeZoomCommand = false;
118
- },
119
-
120
- isZoomCommandActive() {
121
- return this.activeZoomCommand;
122
- }
123
- };
124
-
125
- // Readiness state management
126
- const ViewerReadiness = {
127
- NOT_LOADED: 0,
128
- VIEWER_LOADED: 1, // PDFViewerApplication exists
129
- VIEWER_INITIALIZED: 2, // PDFViewerApplication.initialized = true
130
- EVENTBUS_READY: 3, // Event bus is available and ready
131
- COMPONENTS_READY: 4, // All required components available
132
- DOCUMENT_LOADED: 5 // PDF document fully loaded
133
- };
134
-
135
- // Pure event-driven architecture - no action categorization needed
136
- // All actions are dispatched at required readiness levels by Universal Dispatcher
137
- // #endregion
138
-
139
- // #region State Management
140
- let currentReadiness = ViewerReadiness.NOT_LOADED;
141
- let readinessCallbacks = [];
142
- let diagnosticLogs = false; // Default to false for production
143
- // #endregion
144
-
145
- // #region Utility Functions
146
- function log(message, level = 'info') {
147
- if (!diagnosticLogs) return; // Only log when diagnostic logs are enabled
148
-
149
- const timestamp = new Date().toISOString();
150
- const prefix = '[PostMessage]';
151
-
152
- switch (level) {
153
- case 'error':
154
- console.error(`${prefix} ${timestamp} ERROR: ${message}`);
155
- break;
156
- case 'warn':
157
- console.warn(`${prefix} ${timestamp} WARN: ${message}`);
158
- break;
159
- default:
160
- console.log(`${prefix} ${timestamp} INFO: ${message}`);
161
- }
162
- }
163
-
164
- // Set diagnostic logs mode
165
- function setDiagnosticLogs(enabled) {
166
- diagnosticLogs = enabled;
167
- }
168
-
169
- // Button visibility control helper (consolidated from separate module)
170
- function toggleButtonVisibility(primaryId, secondaryId, visible) {
171
- const primary = document.getElementById(primaryId);
172
- const secondary = secondaryId ? document.getElementById(secondaryId) : null;
173
- if (primary) primary.classList.toggle('hidden', !visible);
174
- if (secondary) secondary.classList.toggle('hidden', !visible);
175
- }
176
-
177
- // Generic element visibility control for toolbar/sidebar groups
178
- function toggleElementVisibilityById(elementId, visible) {
179
- const el = document.getElementById(elementId);
180
- if (!el) {
181
- log(`Element not found for visibility toggle: ${elementId}`, 'warn');
182
- return;
183
- }
184
- el.classList.toggle('hidden', !visible);
185
- }
186
-
187
- // Toolbar section visibility control that preserves layout
188
- function toggleToolbarSectionVisibility(elementId, visible) {
189
- const el = document.getElementById(elementId);
190
- if (!el) {
191
- log(`Toolbar section not found for visibility toggle: ${elementId}`, 'warn');
192
- return;
193
- }
194
-
195
- // Use CSS class instead of inline styles (CSP-safe)
196
- el.classList.toggle('ng2-hidden-section', !visible);
197
- }
198
-
199
- // Readiness state management
200
- function setReadiness(readiness) {
201
- if (readiness > currentReadiness) {
202
- log(`Readiness state changed: ${currentReadiness} → ${readiness}`);
203
- currentReadiness = readiness;
204
-
205
- // Execute callbacks for this readiness level
206
- readinessCallbacks.forEach(callback => {
207
- if (callback.readiness <= readiness && !callback.executed) {
208
- callback.executed = true;
209
- callback.callback();
210
- }
211
- });
212
- }
213
- }
214
-
215
- function onReadiness(readiness, callback) {
216
- if (currentReadiness >= readiness) {
217
- callback();
218
- } else {
219
- readinessCallbacks.push({ readiness, callback, executed: false });
220
- }
221
- }
222
-
223
- // Note: Density styles now loaded from external ng2-customization.css (CSP-safe)
224
-
225
- // Enhanced zoom transformation function
226
- function transformZoomFromViewer(scale, app) {
227
- if (typeof scale === 'string') {
228
- return scale;
229
- }
230
-
231
- if (app?.pdfViewer) {
232
- const viewer = app.pdfViewer;
233
-
234
- // Check for special zoom modes
235
- const specialModes = ['page-fit', 'page-width', 'page-actual', 'auto'];
236
- if (specialModes.includes(viewer.currentScaleValue)) {
237
- return viewer.currentScaleValue;
238
- }
239
-
240
- // Check predefined zoom levels
241
- const predefinedZooms = {
242
- 0.5: '0.5', 0.75: '0.75', 1: '1', 1.25: '1.25',
243
- 1.5: '1.5', 2: '2', 3: '3', 4: '4'
244
- };
245
-
246
- for (const [zoomLevel, zoomValue] of Object.entries(predefinedZooms)) {
247
- if (Math.abs(scale - parseFloat(zoomLevel)) < 0.01) {
248
- return zoomValue;
249
- }
250
- }
251
- }
252
-
253
- return typeof scale === 'number' ? `${Math.round(scale * 100)}%` : 'auto';
254
- }
255
-
256
- // Send state change notifications to Angular component
257
- function sendStateChangeNotification(property, value, source = 'user') {
258
- try {
259
- const message = {
260
- type: 'state-change',
261
- property: property,
262
- value: value,
263
- source: source,
264
- timestamp: Date.now()
265
- };
266
-
267
- window.parent.postMessage(message, '*');
268
- } catch (error) {
269
- log(`Failed to send state change notification: ${error.message}`, 'error');
270
- }
271
- }
272
-
273
- // Send event notifications to Angular component
274
- function sendEventNotification(eventName, eventData) {
275
- try {
276
- const message = {
277
- type: 'event-notification',
278
- eventName: eventName,
279
- eventData: eventData,
280
- timestamp: Date.now()
281
- };
282
-
283
- window.parent.postMessage(message, '*');
284
- } catch (error) {
285
- log(`Failed to send event notification: ${error.message}`, 'error');
286
- }
287
- }
288
-
289
- function sendResponse(id, response) {
290
- if (id && window.parent) {
291
- window.parent.postMessage({
292
- type: 'control-response',
293
- id: id,
294
- ...response,
295
- timestamp: Date.now()
296
- }, '*');
297
- }
298
- }
299
-
300
- function getState() {
301
- const app = PDFViewerApplication;
302
- if (!app) {
303
- return { ready: false };
304
- }
305
-
306
- return {
307
- ready: app.initialized,
308
- page: app.page,
309
- pagesCount: app.pagesCount,
310
- currentScale: app.pdfViewer ? app.pdfViewer.currentScale : null,
311
- currentScaleValue: app.pdfViewer ? app.pdfViewer.currentScaleValue : null,
312
- pagesRotation: app.pdfViewer ? app.pdfViewer.pagesRotation : null,
313
- scrollMode: app.pdfViewer ? app.pdfViewer.scrollMode : null,
314
- spreadMode: app.pdfViewer ? app.pdfViewer.spreadMode : null
315
- };
316
- }
317
-
318
- // Common control update utilities
319
- const ControlUtils = {
320
- // Common button visibility update pattern
321
- updateButtonVisibility: function(buttonId, secondaryId, visible) {
322
- const button = document.getElementById(buttonId);
323
- const secondaryButton = secondaryId ? document.getElementById(secondaryId) : null;
324
-
325
- if (button) {
326
- button.classList.toggle('hidden', !visible);
327
- }
328
- if (secondaryButton) {
329
- secondaryButton.classList.toggle('hidden', !visible);
330
- }
331
-
332
- },
333
-
334
- // Common mode update pattern with event bus
335
- updateModeViaEventBus: function(eventName, modeValue, modeMap, propertyName) {
336
- const app = PDFViewerApplication;
337
- if (!app?.eventBus) {
338
- log('EventBus not available for mode update', 'error');
339
- return;
340
- }
341
-
342
- try {
343
- const upperMode = typeof modeValue === 'string' ? modeValue.toUpperCase() : modeValue;
344
- const mode = modeMap[upperMode];
345
-
346
- if (mode !== undefined) {
347
- app.eventBus.dispatch(eventName, { mode });
348
-
349
- // NOTE: State change notifications are sent by the bidirectional event listeners
350
- // when the actual mode change occurs in PDF.js, not immediately when we dispatch
351
- // This prevents infinite loops and ensures we only notify on actual changes
352
- } else {
353
- log(`Unknown ${propertyName} mode: ${modeValue}`, 'warn');
354
- }
355
- } catch (error) {
356
- log(`Error updating ${propertyName} mode: ${error.message}`, 'error');
357
- }
358
- },
359
-
360
- // Common direct property update pattern
361
- updatePropertyDirectly: function(propertyPath, value, propertyName) {
362
- const app = PDFViewerApplication;
363
- if (!app?.pdfViewer) {
364
- log(`PDFViewer not available for ${propertyName} update`, 'warn');
365
- return;
366
- }
367
-
368
- try {
369
- // Navigate to nested property
370
- const pathParts = propertyPath.split('.');
371
- let target = app;
372
-
373
- for (let i = 0; i < pathParts.length - 1; i++) {
374
- target = target[pathParts[i]];
375
- if (!target) {
376
- log(`Property path ${propertyPath} not found for ${propertyName}`, 'warn');
377
- return;
378
- }
379
- }
380
-
381
- target[pathParts[pathParts.length - 1]] = value;
382
- } catch (error) {
383
- log(`Direct ${propertyName} update failed: ${error.message}`, 'warn');
384
- }
385
- },
386
-
387
- // Common validation pattern
388
- validateAndExecute: function(condition, action, errorMessage) {
389
- if (condition) {
390
- action();
391
- } else {
392
- log(errorMessage, 'warn');
393
- }
394
- }
395
- };
396
- // #endregion
397
-
398
- // #region Initialization and Setup
399
-
400
- // Enhanced readiness checking with proper async initialization handling
401
- function checkViewerReadiness() {
402
- const app = PDFViewerApplication;
403
-
404
- if (!app) {
405
- setReadiness(ViewerReadiness.NOT_LOADED);
406
- return;
407
- }
408
-
409
- setReadiness(ViewerReadiness.VIEWER_LOADED);
410
-
411
- // Handle async initialization properly
412
- if (app.initializedPromise) {
413
- app.initializedPromise.then(() => {
414
- setReadiness(ViewerReadiness.VIEWER_INITIALIZED);
415
-
416
- // Check if event bus is ready
417
- if (app.eventBus && typeof app.eventBus.dispatch === 'function') {
418
- setReadiness(ViewerReadiness.EVENTBUS_READY);
419
-
420
- // Check if key components are available
421
- if (app.pdfViewer && app.pdfCursorTools) {
422
- setReadiness(ViewerReadiness.COMPONENTS_READY);
423
-
424
- // CSS zoom setting is now handled via PostMessage configuration
425
- // No need to check global window variables
426
- }
427
- }
428
- }).catch(error => {
429
- log(`PDFViewerApplication initialization failed: ${error.message}`, 'error');
430
- });
431
- } else if (app.initialized) {
432
- // Fallback for synchronous check (though this should be async)
433
- setReadiness(ViewerReadiness.VIEWER_INITIALIZED);
434
-
435
- // Check if event bus is ready
436
- if (app.eventBus && typeof app.eventBus.dispatch === 'function') {
437
- setReadiness(ViewerReadiness.EVENTBUS_READY);
438
-
439
- // Check if key components are available
440
- if (app.pdfViewer && app.pdfCursorTools) {
441
- setReadiness(ViewerReadiness.COMPONENTS_READY);
442
-
443
- // CSS zoom setting is now handled via PostMessage configuration
444
- // No need to check global window variables
445
- }
446
- }
447
- }
448
- }
449
-
450
- // Wait for PDF.js viewer to be ready with event-driven approach
451
- function waitForViewer() {
452
- // Use MutationObserver to detect when PDFViewerApplication becomes available
453
- const observer = new MutationObserver((mutations, obs) => {
454
- if (typeof PDFViewerApplication !== 'undefined') {
455
- obs.disconnect(); // Stop observing
456
- checkViewerReadiness();
457
-
458
- // Set up event-driven readiness monitoring
459
- setupEventDrivenReadinessMonitoring();
460
- }
461
- });
462
-
463
- // Start observing DOM changes
464
- observer.observe(document, {
465
- childList: true,
466
- subtree: true
467
- });
468
-
469
- // Also check immediately in case it's already available
470
- if (typeof PDFViewerApplication !== 'undefined') {
471
- log('PDFViewerApplication already available, starting readiness check');
472
- observer.disconnect();
473
- checkViewerReadiness();
474
- setupEventDrivenReadinessMonitoring();
475
- }
476
- }
477
-
478
- // Set up event-driven readiness monitoring
479
- function setupEventDrivenReadinessMonitoring() {
480
- // Use a custom event system for readiness changes
481
- const readinessEventTarget = new EventTarget();
482
-
483
- // Override setReadiness to emit events
484
- const originalSetReadiness = setReadiness;
485
- setReadiness = function(readiness) {
486
- originalSetReadiness(readiness);
487
-
488
- // Emit custom event for readiness change
489
- readinessEventTarget.dispatchEvent(new CustomEvent('readiness-change', {
490
- detail: { readiness, previousReadiness: currentReadiness }
491
- }));
492
- };
493
-
494
- // Listen for readiness changes
495
- readinessEventTarget.addEventListener('readiness-change', (event) => {
496
- const { readiness } = event.detail;
497
-
498
- if (readiness >= ViewerReadiness.EVENTBUS_READY) {
499
-
500
- initializePostMessageAPI();
501
- }
502
- });
503
-
504
- // Check initial readiness
505
- if (currentReadiness >= ViewerReadiness.EVENTBUS_READY) {
506
-
507
- initializePostMessageAPI();
508
- }
509
- }
510
-
511
- function initializePostMessageAPI() {
512
- // Add message listener
513
- window.addEventListener('message', handleControlMessage);
514
-
515
- // Expose API for external access
516
- window.Ng2PdfJsViewerAPI = {
517
- updateControl: updateControl,
518
- getState: getState,
519
- isReady: () => currentReadiness >= ViewerReadiness.EVENTBUS_READY,
520
- getReadiness: () => currentReadiness
521
- };
522
-
523
- // Notify parent that PostMessage API is ready
524
- window.parent.postMessage({
525
- type: 'postmessage-ready',
526
- timestamp: Date.now(),
527
- readiness: currentReadiness
528
- }, '*');
529
-
530
- // Set up event listeners for readiness changes
531
- setupReadinessEventListeners();
532
- }
533
-
534
- function setupReadinessEventListeners() {
535
- const app = PDFViewerApplication;
536
- if (!app || !app.eventBus) return;
537
-
538
- // Set up error event listeners immediately (before document loads)
539
- setupErrorEventListeners();
540
-
541
- // Listen for document loaded event
542
- app.eventBus.on('documentloaded', () => {
543
- setReadiness(ViewerReadiness.DOCUMENT_LOADED);
544
-
545
- // Set up bidirectional event listeners once document is loaded
546
- setupBidirectionalEventListeners();
547
- });
548
-
549
- // Listen for pages loaded event
550
- app.eventBus.on('pagesloaded', () => {
551
- // Ensure we're at least at components ready level
552
- if (currentReadiness < ViewerReadiness.COMPONENTS_READY) {
553
- setReadiness(ViewerReadiness.COMPONENTS_READY);
554
- }
555
- });
556
- }
557
- // #endregion
558
-
559
- // #region Message Handling
560
- function handleControlMessage(event) {
561
- const { type, action, payload, id } = event.data;
562
-
563
- if (type === 'control-update') {
564
- try {
565
-
566
- // Universal Dispatcher has already verified readiness - just execute
567
- updateControl(action, payload);
568
- sendResponse(id, { success: true, action, payload });
569
- } catch (error) {
570
- const errorMsg = `Error processing ${action}: ${error.message}`;
571
- log(errorMsg, 'error');
572
- sendResponse(id, { success: false, error: errorMsg });
573
- }
574
- }
575
- }
576
- // #endregion
577
-
578
- // #region Control Update Functions
579
- function updateControl(action, payload) {
580
- const app = PDFViewerApplication;
581
-
582
- // Validate that PDFViewerApplication is available
583
- if (!app) {
584
- log('PDFViewerApplication not available', 'error');
585
- return;
586
- }
587
-
588
- try {
589
- // Note: Density styles loaded from external ng2-customization.css (CSP-safe)
590
- switch (action) {
591
- // Button visibility controls (using consolidated helper function)
592
- case 'show-download':
593
- toggleButtonVisibility('downloadButton', 'secondaryDownload', payload);
594
- break;
595
- case 'show-print':
596
- toggleButtonVisibility('printButton', 'secondaryPrint', payload);
597
- break;
598
- case 'show-fullscreen':
599
- toggleButtonVisibility('presentationMode', null, payload);
600
- break;
601
- case 'show-find':
602
- toggleButtonVisibility('viewFindButton', null, payload);
603
- break;
604
- case 'show-bookmark':
605
- toggleButtonVisibility('viewBookmark', null, payload);
606
- break;
607
- case 'show-openfile':
608
- toggleButtonVisibility('openFile', 'secondaryOpenFile', payload);
609
- break;
610
- case 'show-annotations':
611
- toggleElementVisibilityById('editorModeButtons', payload);
612
- break;
613
-
614
- case 'set-toolbar-density': {
615
- // Apply compact/comfortable density by toggling a class on #toolbarContainer
616
- const container = document.getElementById('toolbarContainer');
617
- if (container) {
618
- container.classList.remove('density-default','density-compact','density-comfortable');
619
- const density = typeof payload === 'string' ? payload : 'default';
620
- container.classList.add(`density-${density}`);
621
- }
622
- break;
623
- }
624
- case 'set-sidebar-width': {
625
- // Use CSS variable only (CSP-safe) - width applied via ng2-customization.css
626
- const outer = document.getElementById('outerContainer');
627
- if (outer && typeof payload === 'string' && payload.trim() !== '') {
628
- outer.style.setProperty('--sidebar-width', payload);
629
- }
630
- break;
631
- }
632
- case 'set-toolbar-position': {
633
- const outer = document.getElementById('outerContainer');
634
- if (outer) {
635
- outer.classList.remove('toolbar-bottom');
636
- if (payload === 'bottom') outer.classList.add('toolbar-bottom');
637
- }
638
- break;
639
- }
640
- case 'set-sidebar-position': {
641
- const outer = document.getElementById('outerContainer');
642
- if (outer) {
643
- outer.classList.remove('sidebar-right');
644
- if (payload === 'right') outer.classList.add('sidebar-right');
645
- }
646
- break;
647
- }
648
- case 'set-responsive-breakpoint': {
649
- const value = typeof payload === 'number' ? `${payload}px` : `${payload}`;
650
- const outer = document.getElementById('outerContainer');
651
- if (outer && value) {
652
- outer.style.setProperty('--ng2-responsive-breakpoint', value);
653
- }
654
- break;
655
- }
656
-
657
- case 'show-toolbar-left':
658
- toggleToolbarSectionVisibility('toolbarViewerLeft', payload);
659
- break;
660
- case 'show-toolbar-middle':
661
- toggleToolbarSectionVisibility('toolbarViewerMiddle', payload);
662
- break;
663
- case 'show-toolbar-right':
664
- toggleToolbarSectionVisibility('toolbarViewerRight', payload);
665
- break;
666
- case 'show-secondary-toolbar-toggle':
667
- toggleElementVisibilityById('secondaryToolbarToggle', payload);
668
- break;
669
- case 'show-sidebar':
670
- toggleElementVisibilityById('sidebarContainer', payload);
671
- break;
672
- case 'show-sidebar-left':
673
- toggleElementVisibilityById('toolbarSidebarLeft', payload);
674
- break;
675
- case 'show-sidebar-right':
676
- toggleElementVisibilityById('toolbarSidebarRight', payload);
677
- break;
678
-
679
- // Mode controls
680
- case 'set-zoom':
681
- updateZoom(payload);
682
- break;
683
- case 'set-cursor':
684
- updateCursor(payload);
685
- break;
686
- case 'set-scroll':
687
- ControlUtils.updateModeViaEventBus('switchscrollmode', payload,
688
- { 'VERTICAL': 0, 'V': 0, 'HORIZONTAL': 1, 'H': 1, 'WRAPPED': 2, 'W': 2, 'PAGE': 3, 'P': 3 },
689
- 'scroll mode');
690
- ControlUtils.updatePropertyDirectly('pdfViewer.scrollMode',
691
- ({ 'VERTICAL': 0, 'V': 0, 'HORIZONTAL': 1, 'H': 1, 'WRAPPED': 2, 'W': 2, 'PAGE': 3, 'P': 3 })[payload?.toUpperCase()] || 0,
692
- 'scroll mode');
693
- break;
694
- case 'set-spread':
695
- ControlUtils.updateModeViaEventBus('switchspreadmode', payload,
696
- { 'NONE': 0, 'N': 0, 'ODD': 1, 'O': 1, 'EVEN': 2, 'E': 2 },
697
- 'spread mode');
698
- ControlUtils.updatePropertyDirectly('pdfViewer.spreadMode',
699
- ({ 'NONE': 0, 'N': 0, 'ODD': 1, 'O': 1, 'EVEN': 2, 'E': 2 })[payload?.toUpperCase()] || 0,
700
- 'spread mode');
701
- break;
702
-
703
- // Navigation controls
704
- case 'set-page':
705
- // Universal Dispatcher guarantees PDFViewerApplication.initialized at readiness level 5
706
- const pageNumber = parseInt(payload, 10);
707
- if (pageNumber > 0 && pageNumber <= PDFViewerApplication.pagesCount) {
708
- PDFViewerApplication.page = pageNumber;
709
- } else {
710
- log(`Invalid page number: ${payload}`, 'warn');
711
- }
712
- break;
713
- case 'set-rotation':
714
- // Universal Dispatcher guarantees PDFViewerApplication.initialized at readiness level 5
715
- const rotation = parseInt(payload, 10);
716
- if ([0, 90, 180, 270].includes(rotation)) {
717
- // Set rotation on pdfViewer to trigger proper refresh
718
- PDFViewerApplication.pdfViewer.pagesRotation = rotation;
719
- // Note: No need to send state change notification here -
720
- // PDF.js will fire 'rotationchanging' event which we handle separately
721
- } else {
722
- log(`Invalid rotation: ${payload}`, 'warn');
723
- }
724
- break;
725
- case 'go-to-last-page':
726
- if (payload === true) {
727
- // Universal Dispatcher guarantees PDFViewerApplication.initialized at readiness level 5
728
- const lastPage = PDFViewerApplication.pagesCount;
729
- PDFViewerApplication.page = lastPage;
730
- }
731
- break;
732
- case 'go-to-named-dest':
733
- // Validate that payload is not empty or null before processing
734
- if (!payload || typeof payload !== 'string' || payload.trim() === '') {
735
- log(`Skipping invalid named destination: "${payload}"`);
736
- break;
737
- }
738
-
739
- // Universal Dispatcher guarantees PDFViewerApplication.pdfLinkService at readiness level 5
740
- PDFViewerApplication.pdfLinkService.goToDestination(payload);
741
- break;
742
- case 'update-page-mode':
743
- // Universal Dispatcher guarantees PDFViewerApplication.eventBus at readiness level 4
744
- const mode = payload ? payload.toLowerCase() : 'none';
745
- PDFViewerApplication.eventBus.dispatch('pagemode', { mode });
746
- break;
747
-
748
- // Configuration actions
749
- case 'set-download-filename':
750
- setDownloadFilename(payload);
751
- break;
752
-
753
- // Auto actions - Universal Dispatcher guarantees eventBus availability at readiness level 5
754
- case 'trigger-download':
755
- if (payload === true) {
756
- PDFViewerApplication.eventBus.dispatch('download');
757
- }
758
- break;
759
- case 'trigger-print':
760
- if (payload === true) {
761
- PDFViewerApplication.eventBus.dispatch('print');
762
- }
763
- break;
764
- case 'trigger-rotate-cw':
765
- if (payload === true) {
766
- PDFViewerApplication.eventBus.dispatch('rotatecw');
767
- }
768
- break;
769
- case 'trigger-rotate-ccw':
770
- if (payload === true) {
771
- PDFViewerApplication.eventBus.dispatch('rotateccw');
772
- }
773
- break;
774
-
775
- // Error handling
776
- case 'set-error-message':
777
- setErrorMessage(payload);
778
- break;
779
- case 'set-error-override':
780
- setErrorOverride(payload);
781
- break;
782
- case 'set-error-append':
783
- setErrorAppend(payload);
784
- break;
785
-
786
- // CSS zoom
787
- case 'set-css-zoom':
788
- setCssZoom(payload);
789
- break;
790
-
791
- // Event enablement
792
- case 'enable-before-print':
793
- enableBeforePrint(payload);
794
- break;
795
- case 'enable-after-print':
796
- enableAfterPrint(payload);
797
- break;
798
- case 'enable-pages-loaded':
799
- enablePagesLoaded(payload);
800
- break;
801
- case 'enable-page-change':
802
- enablePageChange(payload);
803
- break;
804
-
805
- // New high-value event enablement
806
- case 'enable-document-error':
807
- enableDocumentError(payload);
808
- break;
809
- case 'enable-document-init':
810
- enableDocumentInit(payload);
811
- break;
812
- case 'enable-pages-init':
813
- enablePagesInit(payload);
814
- break;
815
- case 'enable-presentation-mode-changed':
816
- enablePresentationModeChanged(payload);
817
- break;
818
- case 'enable-open-file':
819
- enableOpenFile(payload);
820
- break;
821
- case 'enable-find':
822
- enableFind(payload);
823
- break;
824
- case 'enable-update-find-matches-count':
825
- enableUpdateFindMatchesCount(payload);
826
- break;
827
- case 'enable-metadata-loaded':
828
- enableMetadataLoaded(payload);
829
- break;
830
- case 'enable-outline-loaded':
831
- enableOutlineLoaded(payload);
832
- break;
833
- case 'enable-page-rendered':
834
- enablePageRendered(payload);
835
- break;
836
-
837
- // New high-value events
838
- case 'enable-annotation-layer-rendered':
839
- enableAnnotationLayerRendered(payload);
840
- break;
841
- case 'enable-bookmark-click':
842
- enableBookmarkClick(payload);
843
- break;
844
- case 'enable-idle':
845
- enableIdle(payload);
846
- break;
847
-
848
- // Theme & Visual Customization Actions
849
- case 'set-theme':
850
- setTheme(payload);
851
- break;
852
- case 'set-primary-color':
853
- setPrimaryColor(payload);
854
- break;
855
- case 'set-background-color':
856
- setBackgroundColor(payload);
857
- break;
858
- case 'set-page-border-color':
859
- setPageBorderColor(payload);
860
- break;
861
- case 'set-page-spacing':
862
- setPageSpacing(payload.margin, payload.spreadMargin, payload.border);
863
- break;
864
- case 'set-toolbar-color':
865
- setToolbarColor(payload);
866
- break;
867
- case 'set-text-color':
868
- setTextColor(payload);
869
- break;
870
- case 'set-border-radius':
871
- setBorderRadius(payload);
872
- break;
873
- case 'set-custom-css':
874
- // Handle both old format (string) and new format (object with nonce)
875
- if (typeof payload === 'string') {
876
- setCustomCSS(payload, null);
877
- } else if (payload && typeof payload === 'object') {
878
- setCustomCSS(payload.css, payload.nonce);
879
- }
880
- break;
881
-
882
- case 'set-diagnostic-logs':
883
- setDiagnosticLogs(payload);
884
- break;
885
- case 'set-url-validation':
886
- urlValidationEnabled = payload === true;
887
- log(`URL validation ${urlValidationEnabled ? 'enabled' : 'disabled'}`);
888
- break;
889
-
890
- default:
891
- log(`Unknown action: ${action}`, 'warn');
892
- }
893
- } catch (error) {
894
- log(`Error in updateControl for action ${action}: ${error.message}`, 'error');
895
- throw error;
896
- }
897
- }
898
- // #endregion
899
-
900
- // #region Button Visibility Functions
901
- function updateDownloadButton(visible) {
902
- const button = document.getElementById('downloadButton');
903
- const secondaryButton = document.getElementById('secondaryDownload');
904
- if (button) {
905
- button.classList.toggle('hidden', !visible);
906
- }
907
- if (secondaryButton) {
908
- secondaryButton.classList.toggle('hidden', !visible);
909
- }
910
- }
911
-
912
- function updatePrintButton(visible) {
913
- const button = document.getElementById('printButton');
914
- const secondaryButton = document.getElementById('secondaryPrint');
915
- if (button) {
916
- button.classList.toggle('hidden', !visible);
917
- }
918
- if (secondaryButton) {
919
- secondaryButton.classList.toggle('hidden', !visible);
920
- }
921
- }
922
-
923
- function updateFullScreenButton(visible) {
924
- const button = document.getElementById('presentationMode');
925
- if (button) {
926
- button.classList.toggle('hidden', !visible);
927
- }
928
- }
929
-
930
- function updateFindButton(visible) {
931
- const button = document.getElementById('viewFindButton');
932
- if (button) {
933
- button.classList.toggle('hidden', !visible);
934
- }
935
- }
936
-
937
- function updateBookmarkButton(visible) {
938
- const button = document.getElementById('viewBookmark');
939
- if (button) {
940
- button.classList.toggle('hidden', !visible);
941
- }
942
- }
943
-
944
- function updateOpenFileButton(visible) {
945
- const button = document.getElementById('openFile');
946
- const secondaryButton = document.getElementById('secondaryOpenFile');
947
- if (button) {
948
- button.classList.toggle('hidden', !visible);
949
- }
950
- if (secondaryButton) {
951
- secondaryButton.classList.toggle('hidden', !visible);
952
- }
953
- }
954
- function updateAnnotationsButton(visible) {
955
- // Handle annotations button visibility
956
- // This might need to be implemented based on the specific annotation system
957
- log(`Annotations button visibility set to: ${visible}`);
958
- }
959
- // #endregion
960
-
961
- // #region Mode Control Functions
962
- function updateZoom(zoom) {
963
- const app = PDFViewerApplication;
964
- if (!app || !app.pdfViewer || !app.eventBus) {
965
- log('PDFViewerApplication, pdfViewer, or eventBus not ready for zoom update', 'warn');
966
- return;
967
- }
968
-
969
- // Mark zoom command as active to prevent infinite loop
970
- ZoomCommandTracker.markZoomCommandStart();
971
-
972
- try {
973
- // Acceptable values: "auto", "page-fit", "page-width", "page-actual", "page-height", or a number/string number like "1.25"
974
- const validStringZooms = ['auto', 'page-actual', 'page-fit', 'page-width', 'page-height'];
975
-
976
- // Check if it's a valid string zoom
977
- if (validStringZooms.includes(zoom)) {
978
- // Dispatch the scalechanged event exactly like the UI does
979
- app.eventBus.dispatch("scalechanged", {
980
- source: app.toolbar, // Use toolbar as source, just like the UI does
981
- value: zoom
982
- });
983
- return;
984
- }
985
-
986
- // Check if it's a valid numeric zoom (including decimal strings like "1.25")
987
- const numericZoom = Number(zoom);
988
- if (!isNaN(numericZoom) && numericZoom > 0) {
989
- // Dispatch the scalechanged event exactly like the UI does
990
- app.eventBus.dispatch("scalechanged", {
991
- source: app.toolbar, // Use toolbar as source, just like the UI does
992
- value: zoom
993
- });
994
- return;
995
- }
996
-
997
- // If we get here, it's invalid
998
- log(`Invalid zoom value: ${zoom}`, 'warn');
999
- } finally {
1000
- // Always clear the command flag, even if there's an error
1001
- ZoomCommandTracker.markZoomCommandEnd();
1002
- }
1003
- }
1004
-
1005
- function updateCursor(cursor) {
1006
- // Universal Dispatcher guarantees PDFViewerApplication availability at readiness level 4
1007
- const app = PDFViewerApplication;
1008
-
1009
- try {
1010
- const cursorTool = cursor ? cursor.toUpperCase() : 'SELECT';
1011
-
1012
- let toolId = 0; // Default to SELECT
1013
- switch (cursorTool) {
1014
- case 'HAND':
1015
- case 'H':
1016
- toolId = 1; // HAND
1017
- break;
1018
- case 'SELECT':
1019
- case 'S':
1020
- toolId = 0; // SELECT
1021
- break;
1022
- case 'ZOOM':
1023
- case 'Z':
1024
- toolId = 2; // ZOOM
1025
- break;
1026
- default:
1027
- log(`Unknown cursor tool: ${cursorTool}, defaulting to SELECT`, 'warn');
1028
- toolId = 0;
1029
- }
1030
-
1031
- // Update cursor using event bus dispatch (PDF.js v4.x)
1032
- if (app.eventBus && typeof app.eventBus.dispatch === 'function') {
1033
- app.eventBus.dispatch('switchcursortool', {
1034
- tool: toolId
1035
- });
1036
- } else {
1037
- log('EventBus not available for cursor update', 'warn');
1038
- }
1039
- } catch (error) {
1040
- log(`Error updating cursor: ${error.message}`, 'error');
1041
- }
1042
- }
1043
-
1044
- function updateScroll(scroll) {
1045
- // Universal Dispatcher guarantees PDFViewerApplication availability at readiness level 4
1046
-
1047
- try {
1048
- const scrollMode = scroll ? scroll.toUpperCase() : 'VERTICAL';
1049
- log(`Attempting to update scroll mode to: ${scrollMode}`);
1050
-
1051
- if (PDFViewerApplication.eventBus) {
1052
- // Use PDF.js v4.x event bus dispatch for scroll mode switching
1053
- switch (scrollMode) {
1054
- case 'VERTICAL':
1055
- case 'V':
1056
- log('Dispatching switchscrollmode with VERTICAL mode');
1057
- PDFViewerApplication.eventBus.dispatch('switchscrollmode', {
1058
- mode: 0 // ScrollMode.VERTICAL
1059
- });
1060
- break;
1061
- case 'HORIZONTAL':
1062
- case 'H':
1063
- log('Dispatching switchscrollmode with HORIZONTAL mode');
1064
- PDFViewerApplication.eventBus.dispatch('switchscrollmode', {
1065
- mode: 1 // ScrollMode.HORIZONTAL
1066
- });
1067
- break;
1068
- case 'WRAPPED':
1069
- case 'W':
1070
- log('Dispatching switchscrollmode with WRAPPED mode');
1071
- PDFViewerApplication.eventBus.dispatch('switchscrollmode', {
1072
- mode: 2 // ScrollMode.WRAPPED
1073
- });
1074
- break;
1075
- case 'PAGE':
1076
- case 'P':
1077
- log('Dispatching switchscrollmode with PAGE mode');
1078
- PDFViewerApplication.eventBus.dispatch('switchscrollmode', {
1079
- mode: 3 // ScrollMode.PAGE
1080
- });
1081
- break;
1082
- default:
1083
- log(`Unknown scroll mode: ${scrollMode}, defaulting to VERTICAL`, 'warn');
1084
- PDFViewerApplication.eventBus.dispatch('switchscrollmode', {
1085
- mode: 0
1086
- });
1087
- }
1088
- } else {
1089
- // Fallback to direct property setting if event bus not available
1090
- if (PDFViewerApplication.pdfViewer) {
1091
- switch (scrollMode) {
1092
- case 'VERTICAL':
1093
- case 'V':
1094
- PDFViewerApplication.pdfViewer.scrollMode = 0;
1095
- break;
1096
- case 'HORIZONTAL':
1097
- case 'H':
1098
- PDFViewerApplication.pdfViewer.scrollMode = 1;
1099
- break;
1100
- case 'WRAPPED':
1101
- case 'W':
1102
- PDFViewerApplication.pdfViewer.scrollMode = 2;
1103
- break;
1104
- case 'PAGE':
1105
- case 'P':
1106
- PDFViewerApplication.pdfViewer.scrollMode = 3;
1107
- break;
1108
- }
1109
- }
1110
- }
1111
-
1112
- log(`Scroll mode update completed for: ${scrollMode}`);
1113
- } catch (error) {
1114
- log(`Error updating scroll mode: ${error.message}`, 'error');
1115
- }
1116
- }
1117
-
1118
- function updateSpread(spread) {
1119
- // Universal Dispatcher guarantees PDFViewerApplication availability at readiness level 4
1120
-
1121
- try {
1122
- const spreadMode = spread ? spread.toUpperCase() : 'NONE';
1123
- log(`Attempting to update spread mode to: ${spreadMode}`);
1124
-
1125
- if (PDFViewerApplication.eventBus) {
1126
- // Use PDF.js v4.x event bus dispatch for spread mode switching
1127
- switch (spreadMode) {
1128
- case 'NONE':
1129
- case 'N':
1130
- log('Dispatching switchspreadmode with NONE mode');
1131
- PDFViewerApplication.eventBus.dispatch('switchspreadmode', {
1132
- mode: 0 // SpreadMode.NONE
1133
- });
1134
- break;
1135
- case 'ODD':
1136
- case 'O':
1137
- log('Dispatching switchspreadmode with ODD mode');
1138
- PDFViewerApplication.eventBus.dispatch('switchspreadmode', {
1139
- mode: 1 // SpreadMode.ODD
1140
- });
1141
- break;
1142
- case 'EVEN':
1143
- case 'E':
1144
- log('Dispatching switchspreadmode with EVEN mode');
1145
- PDFViewerApplication.eventBus.dispatch('switchspreadmode', {
1146
- mode: 2 // SpreadMode.EVEN
1147
- });
1148
- break;
1149
- default:
1150
- log(`Unknown spread mode: ${spreadMode}, defaulting to NONE`, 'warn');
1151
- PDFViewerApplication.eventBus.dispatch('switchspreadmode', {
1152
- mode: 0
1153
- });
1154
- }
1155
- } else {
1156
- // Fallback to direct property setting if event bus not available
1157
- if (PDFViewerApplication.pdfViewer) {
1158
- switch (spreadMode) {
1159
- case 'NONE':
1160
- case 'N':
1161
- PDFViewerApplication.pdfViewer.spreadMode = 0;
1162
- break;
1163
- case 'ODD':
1164
- case 'O':
1165
- PDFViewerApplication.pdfViewer.spreadMode = 1;
1166
- break;
1167
- case 'EVEN':
1168
- case 'E':
1169
- PDFViewerApplication.pdfViewer.spreadMode = 2;
1170
- break;
1171
- }
1172
- }
1173
- }
1174
-
1175
- log(`Spread mode update completed for: ${spreadMode}`);
1176
- } catch (error) {
1177
- log(`Error updating spread mode: ${error.message}`, 'error');
1178
- }
1179
- }
1180
- // #endregion
1181
-
1182
- // Note: Legacy navigation and auto-action functions removed.
1183
- // All functionality now handled through the main updateControl switch statement
1184
- // which trusts the Universal Dispatcher's readiness guarantees.
1185
-
1186
- // #region Error Handling Functions
1187
- // Error state is managed internally and communicated via PostMessage
1188
- // No need for global window variables - events drive the behavior
1189
- function setErrorMessage(message) {
1190
- // Error messages are sent directly via PostMessage when errors occur
1191
- // No need to store in global variables
1192
- }
1193
-
1194
- function setErrorOverride(override) {
1195
- // Error override is handled by the Angular component
1196
- // No need to store in global variables
1197
- }
1198
-
1199
- function setErrorAppend(append) {
1200
- // Error append is handled by the Angular component
1201
- // No need to store in global variables
1202
- }
1203
- // #endregion
1204
-
1205
- // #region CSS Zoom Functions
1206
-
1207
- function setCssZoom(useCssZoom) {
1208
- try {
1209
- if (PDFViewerApplication && PDFViewerApplication.pdfViewer) {
1210
- // Set CSS zoom mode
1211
- PDFViewerApplication.pdfViewer.useOnlyCssZoom = useCssZoom === true;
1212
- } else {
1213
- // CSS zoom will be set when viewer becomes ready via readiness-based dispatch
1214
- // No need to store in global variables
1215
- }
1216
- } catch (error) {
1217
- log(`Error setting CSS zoom: ${error.message}`, 'error');
1218
- }
1219
- }
1220
-
1221
- function setDownloadFilename(filename) {
1222
- try {
1223
- if (filename && PDFViewerApplication) {
1224
- // Ensure filename ends with .pdf if not already present
1225
- const processedFilename = filename.endsWith('.pdf') ? filename : `${filename}.pdf`;
1226
-
1227
- // Set the content disposition filename that PDF.js uses for downloads
1228
- PDFViewerApplication._contentDispositionFilename = processedFilename;
1229
- } else {
1230
- log('Cannot set download filename - invalid filename or PDFViewerApplication not available', 'warn');
1231
- }
1232
- } catch (error) {
1233
- log(`Error setting download filename: ${error.message}`, 'error');
1234
- }
1235
- }
1236
- // #endregion
1237
-
1238
- // #region Event Configuration Functions
1239
- // Event enablement state stored locally (not in global window variables)
1240
- // This follows v5-upgrade.md principles while maintaining functionality
1241
-
1242
- // Local state for event enablement
1243
- const eventEnablement = {
1244
- beforePrint: false,
1245
- afterPrint: false,
1246
- pagesLoaded: false,
1247
- pageChange: false,
1248
- documentError: false,
1249
- documentInit: false,
1250
- pagesInit: false,
1251
- presentationModeChanged: false,
1252
- openFile: false,
1253
- find: false,
1254
- updateFindMatchesCount: false,
1255
- metadataLoaded: false,
1256
- outlineLoaded: false,
1257
- pageRendered: false,
1258
- annotationLayerRendered: false,
1259
- bookmarkClick: false,
1260
- idle: false
1261
- };
1262
-
1263
- function enableBeforePrint(enable) { eventEnablement.beforePrint = enable === true; }
1264
- function enableAfterPrint(enable) { eventEnablement.afterPrint = enable === true; }
1265
- function enablePagesLoaded(enable) { eventEnablement.pagesLoaded = enable === true; }
1266
- function enablePageChange(enable) { eventEnablement.pageChange = enable === true; }
1267
- function enableDocumentError(enable) { eventEnablement.documentError = enable === true; }
1268
- function enableDocumentInit(enable) { eventEnablement.documentInit = enable === true; }
1269
- function enablePagesInit(enable) { eventEnablement.pagesInit = enable === true; }
1270
- function enablePresentationModeChanged(enable) { eventEnablement.presentationModeChanged = enable === true; }
1271
- function enableOpenFile(enable) { eventEnablement.openFile = enable === true; }
1272
- function enableFind(enable) { eventEnablement.find = enable === true; }
1273
- function enableUpdateFindMatchesCount(enable) { eventEnablement.updateFindMatchesCount = enable === true; }
1274
- function enableMetadataLoaded(enable) { eventEnablement.metadataLoaded = enable === true; }
1275
- function enableOutlineLoaded(enable) { eventEnablement.outlineLoaded = enable === true; }
1276
- function enablePageRendered(enable) { eventEnablement.pageRendered = enable === true; }
1277
- function enableAnnotationLayerRendered(enable) { eventEnablement.annotationLayerRendered = enable === true; }
1278
- function enableBookmarkClick(enable) { eventEnablement.bookmarkClick = enable === true; }
1279
- function enableIdle(enable) { eventEnablement.idle = enable === true; }
1280
- // #endregion
1281
-
1282
- // #region Theme & Visual Customization Functions
1283
-
1284
- // Theme management variables
1285
- let currentTheme = 'light';
1286
-
1287
- function setTheme(theme) {
1288
- currentTheme = theme || 'light';
1289
-
1290
- // Apply theme-specific CSS classes (CSP-safe)
1291
- const body = document.body;
1292
- if (body) {
1293
- // Remove ALL theme classes including active states
1294
- body.classList.remove('ng2-theme-light', 'ng2-theme-dark', 'ng2-theme-auto',
1295
- 'ng2-theme-dark-active', 'ng2-theme-light-active');
1296
-
1297
- // Add new theme class
1298
- body.classList.add(`ng2-theme-${currentTheme}`);
1299
-
1300
- // Apply auto theme detection
1301
- if (currentTheme === 'auto') {
1302
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
1303
- const activeClass = prefersDark ? 'ng2-theme-dark-active' : 'ng2-theme-light-active';
1304
- body.classList.add(activeClass);
1305
-
1306
- // Listen for theme changes
1307
- if (!window.ng2ThemeMediaListener) {
1308
- const mediaListener = window.matchMedia('(prefers-color-scheme: dark)');
1309
- mediaListener.addListener((e) => {
1310
- if (currentTheme === 'auto') {
1311
- body.classList.remove('ng2-theme-dark-active', 'ng2-theme-light-active');
1312
- const newActiveClass = e.matches ? 'ng2-theme-dark-active' : 'ng2-theme-light-active';
1313
- body.classList.add(newActiveClass);
1314
- }
1315
- });
1316
- window.ng2ThemeMediaListener = mediaListener;
1317
- }
1318
- }
1319
-
1320
- // Force style recalculation
1321
- body.offsetHeight; // Trigger reflow
1322
- }
1323
-
1324
- // Note: Theme styles now loaded from external ng2-customization.css (CSP-safe)
1325
- }
1326
-
1327
- function setPrimaryColor(color) {
1328
- setCSSVariable('--ng2-primary-color', color);
1329
- }
1330
-
1331
- function setBackgroundColor(color) {
1332
- setCSSVariable('--ng2-background-color', color);
1333
- }
1334
-
1335
- function setPageSpacing(margin, spreadMargin, border) {
1336
- if (margin !== undefined) {
1337
- setCSSVariable('--page-margin', margin);
1338
- }
1339
- if (spreadMargin !== undefined) {
1340
- setCSSVariable('--spreadHorizontalWrapped-margin-LR', spreadMargin);
1341
- }
1342
- if (border !== undefined) {
1343
- setCSSVariable('--page-border', border);
1344
- }
1345
- }
1346
-
1347
- function setPageBorderColor(color) {
1348
- setCSSVariable('--ng2-page-border-color', color);
1349
- }
1350
-
1351
- function setToolbarColor(color) {
1352
- setCSSVariable('--ng2-toolbar-color', color);
1353
- }
1354
-
1355
- function setTextColor(color) {
1356
- setCSSVariable('--ng2-text-color', color);
1357
- }
1358
-
1359
- function setBorderRadius(radius) {
1360
- setCSSVariable('--ng2-border-radius', radius);
1361
- }
1362
-
1363
- function setCustomCSS(css, nonce) {
1364
- // Remove existing custom CSS
1365
- const existingStyle = document.getElementById('ng2-custom-css');
1366
- if (existingStyle) {
1367
- existingStyle.remove();
1368
- }
1369
-
1370
- // Apply new custom CSS with optional nonce for CSP support
1371
- if (css) {
1372
- const style = document.createElement('style');
1373
- style.id = 'ng2-custom-css';
1374
- style.textContent = css;
1375
-
1376
- // Add nonce if provided (CSP support)
1377
- if (nonce) {
1378
- style.setAttribute('nonce', nonce);
1379
- }
1380
-
1381
- document.head.appendChild(style);
1382
- }
1383
- }
1384
-
1385
- // Helper function to set CSS custom properties
1386
- function setCSSVariable(property, value) {
1387
- if (value) {
1388
- document.documentElement.style.setProperty(property, value);
1389
- // Also set on body for better compatibility
1390
- document.body.style.setProperty(property, value);
1391
- } else {
1392
- document.documentElement.style.removeProperty(property);
1393
- document.body.style.removeProperty(property);
1394
- }
1395
- }
1396
-
1397
- // Note: Theme styles now loaded from external ng2-customization.css (CSP-safe)
1398
-
1399
- // #endregion
1400
-
1401
- // #region Error Event Listeners (Early Setup)
1402
- let errorListenersSetup = false; // Flag to prevent multiple setups
1403
-
1404
- function setupErrorEventListeners() {
1405
- // Prevent multiple setups
1406
- if (errorListenersSetup) {
1407
- return;
1408
- }
1409
-
1410
- const app = PDFViewerApplication;
1411
- if (!app || !app.eventBus) {
1412
- log('Cannot setup error listeners: EventBus not available', 'warn');
1413
- return;
1414
- }
1415
-
1416
- // Listen for document errors (including file origin errors)
1417
- app.eventBus.on('documenterror', (event) => {
1418
- log(`🔴 DOCUMENT ERROR EVENT FIRED: ${event.message}`, 'error');
1419
- // Always hide loading spinner on document error
1420
- sendStateChangeNotification('loading', false, 'system');
1421
-
1422
- // Send error state notification for custom error display
1423
- const errorMessage = event.message || 'An error occurred while loading the PDF.';
1424
- sendStateChangeNotification('error', errorMessage, 'system');
1425
-
1426
- // Send document error events only if enabled
1427
- if (eventEnablement.documentError) {
1428
- log(`Document error event received: ${event.message}`);
1429
- const errorData = {
1430
- message: event.message || 'Unknown document error',
1431
- source: event.source ? 'PDFViewerApplication' : 'unknown',
1432
- name: event.name || 'DocumentError'
1433
- };
1434
- sendEventNotification('documentError', errorData);
1435
- }
1436
- });
1437
-
1438
- // Listen for other potential error events
1439
- app.eventBus.on('loaderror', (event) => {
1440
- log(`🔴 LOAD ERROR EVENT FIRED: ${event.message}`, 'error');
1441
- // Hide loading spinner on load error
1442
- sendStateChangeNotification('loading', false, 'system');
1443
- });
1444
-
1445
- app.eventBus.on('error', (event) => {
1446
- log(`🔴 GENERIC ERROR EVENT FIRED: ${event.message}`, 'error');
1447
- // Hide loading spinner on any error
1448
- sendStateChangeNotification('loading', false, 'system');
1449
- });
1450
-
1451
- errorListenersSetup = true;
1452
- log('Error event listeners set up successfully', 'info');
1453
- }
1454
-
1455
- // #region Bidirectional Event Listeners
1456
- let bidirectionalListenersSetup = false; // Flag to prevent multiple setups
1457
-
1458
- function setupBidirectionalEventListeners() {
1459
- // Prevent multiple setups
1460
- if (bidirectionalListenersSetup) {
1461
- return;
1462
- }
1463
-
1464
- // Add global error handler for iframe-level errors
1465
- window.addEventListener('error', (event) => {
1466
- log(`🔴 GLOBAL ERROR: ${event.message}`, 'error');
1467
- // Hide loading spinner on any global error
1468
- sendStateChangeNotification('loading', false, 'system');
1469
- });
1470
-
1471
- window.addEventListener('unhandledrejection', (event) => {
1472
- log(`🔴 UNHANDLED PROMISE REJECTION: ${event.reason}`, 'error');
1473
- // Hide loading spinner on unhandled promise rejection
1474
- sendStateChangeNotification('loading', false, 'system');
1475
- });
1476
-
1477
- bidirectionalListenersSetup = true;
1478
- const app = PDFViewerApplication;
1479
- if (!app || !app.eventBus) {
1480
- log('Cannot setup bidirectional listeners: EventBus not available', 'warn');
1481
- return;
1482
- }
1483
-
1484
-
1485
- // Test sending a state change notification immediately
1486
- sendStateChangeNotification('test', 'initial-setup', 'system');
1487
-
1488
- // Cursor tool changes are handled via switchcursortool event listener only
1489
-
1490
- // Note: Scroll and spread mode changes are handled via event listeners
1491
- // No method interception needed for PDF.js v5.3.93
1492
- if (app.pdfViewer) {
1493
-
1494
- // Monitor scroll mode changes - setScrollMode doesn't exist in v5.3.93
1495
- // The scroll mode changes are handled via event listeners instead
1496
-
1497
- // Spread mode changes are handled via switchspreadmode event listener only
1498
-
1499
- // Monitor zoom changes via currentScale property
1500
- let lastKnownScale = app.pdfViewer.currentScale;
1501
- const originalSetCurrentScale = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(app.pdfViewer), 'currentScale');
1502
- if (originalSetCurrentScale && originalSetCurrentScale.set) {
1503
- Object.defineProperty(app.pdfViewer, 'currentScale', {
1504
- get: originalSetCurrentScale.get,
1505
- set: function(scale) {
1506
- const result = originalSetCurrentScale.set.call(this, scale);
1507
-
1508
- if (scale !== lastKnownScale) {
1509
- lastKnownScale = scale;
1510
-
1511
- // Use enhanced zoom transformation
1512
- const zoomValue = transformZoomFromViewer(scale, app);
1513
-
1514
- log(`Zoom changed via currentScale to: ${zoomValue} (scale: ${scale}, currentScaleValue: ${app.pdfViewer.currentScaleValue})`);
1515
- sendStateChangeNotification('zoom', zoomValue, 'user');
1516
- }
1517
-
1518
- return result;
1519
- },
1520
- configurable: true,
1521
- enumerable: true
1522
- });
1523
- } else {
1524
- log('pdfViewer.currentScale setter not found', 'warn');
1525
- }
1526
- } else {
1527
- log('pdfViewer not available', 'warn');
1528
- }
1529
-
1530
- // Listen for sidebar/page mode changes
1531
- if (app.pdfSidebar) {
1532
-
1533
- // Monitor sidebar view changes
1534
- const originalSetView = app.pdfSidebar.setView;
1535
- if (originalSetView) {
1536
- app.pdfSidebar.setView = function(view) {
1537
- const result = originalSetView.apply(this, arguments);
1538
-
1539
- // Map PDF.js sidebar view numbers to our page mode names
1540
- const pageModeMap = {
1541
- 0: 'none', // SidebarView.NONE
1542
- 1: 'thumbs', // SidebarView.THUMBS
1543
- 2: 'bookmarks', // SidebarView.OUTLINE
1544
- 3: 'attachments', // SidebarView.ATTACHMENTS
1545
- 4: 'layers' // SidebarView.LAYERS
1546
- };
1547
-
1548
- const pageModeName = pageModeMap[view] || 'none';
1549
- log(`Page mode changed to: ${pageModeName} (view: ${view})`);
1550
- sendStateChangeNotification('pageMode', pageModeName, 'user');
1551
-
1552
- return result;
1553
- };
1554
- } else {
1555
- }
1556
- } else {
1557
- log('pdfSidebar not available', 'warn');
1558
- }
1559
-
1560
- // Listen for additional PDF.js events that might indicate property changes
1561
- if (app.eventBus) {
1562
-
1563
- // Listen for scale changing events with intelligent user vs programmatic detection
1564
- app.eventBus.on('scalechanging', (event) => {
1565
-
1566
-
1567
- // Skip notification if this is from our own zoom command
1568
- if (ZoomCommandTracker.isZoomCommandActive()) {
1569
- log('Skipping zoom notification - programmatic change from our command');
1570
- return;
1571
- }
1572
-
1573
- // Use event context to determine if this is a user-initiated change
1574
- let isUserInitiated = false;
1575
- let zoomValue;
1576
-
1577
- if (event.presetValue && typeof event.presetValue === 'string') {
1578
- // User clicked zoom buttons (Page Fit, Page Width, Auto, etc.)
1579
-
1580
- isUserInitiated = true;
1581
- zoomValue = event.presetValue;
1582
- } else if (typeof event.scale === 'number' && !event.presetValue) {
1583
- // Could be mouse wheel (user) or other programmatic change
1584
- // For now, assume it's user-initiated (mouse wheel, etc.)
1585
- // Future enhancement: could add more sophisticated detection
1586
- log('Assuming user-initiated scale change (mouse wheel, etc.)');
1587
- isUserInitiated = true;
1588
- zoomValue = transformZoomFromViewer(event.scale, app);
1589
- }
1590
-
1591
- if (isUserInitiated) {
1592
-
1593
- sendStateChangeNotification('zoom', zoomValue, 'user');
1594
- } else {
1595
- log('Skipping zoom notification - not identified as user-initiated');
1596
- }
1597
- });
1598
-
1599
- // Listen for other relevant events
1600
- app.eventBus.on('switchscrollmode', (event) => {
1601
-
1602
- const scrollMap = {
1603
- 0: 'vertical', // ScrollMode.VERTICAL
1604
- 1: 'horizontal', // ScrollMode.HORIZONTAL
1605
- 2: 'wrapped', // ScrollMode.WRAPPED
1606
- 3: 'page' // ScrollMode.PAGE
1607
- };
1608
-
1609
- const scrollName = scrollMap[event.mode] || 'vertical';
1610
- log(`Scroll mode changed via event to: ${scrollName}`);
1611
- sendStateChangeNotification('scroll', scrollName, 'user');
1612
- });
1613
-
1614
- app.eventBus.on('switchspreadmode', (event) => {
1615
-
1616
- const spreadMap = {
1617
- 0: 'none', // SpreadMode.NONE
1618
- 1: 'odd', // SpreadMode.ODD
1619
- 2: 'even' // SpreadMode.EVEN
1620
- };
1621
-
1622
- const spreadName = spreadMap[event.mode] || 'none';
1623
- log(`Spread mode changed via event to: ${spreadName}`);
1624
- sendStateChangeNotification('spread', spreadName, 'user');
1625
- });
1626
-
1627
- app.eventBus.on('switchcursortool', (event) => {
1628
-
1629
- const cursorMap = {
1630
- 0: 'select', // CursorTool.SELECT
1631
- 1: 'hand', // CursorTool.HAND
1632
- 2: 'zoom' // CursorTool.ZOOM
1633
- };
1634
-
1635
- const cursorName = cursorMap[event.tool] || 'select';
1636
- log(`Cursor changed via event to: ${cursorName}`);
1637
- sendStateChangeNotification('cursor', cursorName, 'user');
1638
- });
1639
-
1640
- app.eventBus.on('sidebarviewchanged', (event) => {
1641
-
1642
- const pageModeMap = {
1643
- 0: 'none', // SidebarView.NONE
1644
- 1: 'thumbs', // SidebarView.THUMBS
1645
- 2: 'bookmarks', // SidebarView.OUTLINE
1646
- 3: 'attachments', // SidebarView.ATTACHMENTS
1647
- 4: 'layers' // SidebarView.LAYERS
1648
- };
1649
-
1650
- const pageModeName = pageModeMap[event.view] || 'none';
1651
- log(`Page mode changed via event to: ${pageModeName}`);
1652
- sendStateChangeNotification('pageMode', pageModeName, 'user');
1653
- });
1654
-
1655
- // Listen for rotation changes (when user rotates via UI or programmatically)
1656
- app.eventBus.on('rotationchanging', (event) => {
1657
- log(`Rotation changed via event to: ${event.pagesRotation}`);
1658
- sendStateChangeNotification('rotation', event.pagesRotation, 'user');
1659
- });
1660
-
1661
- // Note: Error event listeners are set up earlier in setupErrorEventListeners()
1662
-
1663
- app.eventBus.on('documentinit', (event) => {
1664
- // Send document init events only if enabled
1665
- if (eventEnablement.documentInit) {
1666
- sendEventNotification('documentInit', null);
1667
- }
1668
- });
1669
-
1670
- app.eventBus.on('pagesinit', (event) => {
1671
- // Send pages init events only if enabled
1672
- if (eventEnablement.pagesInit) {
1673
- const pageCount = app.pagesCount || 0;
1674
- const pagesData = {
1675
- pagesCount: pageCount
1676
- };
1677
- sendEventNotification('pagesInit', pagesData);
1678
- }
1679
- });
1680
-
1681
- app.eventBus.on('presentationmodechanged', (event) => {
1682
- // Send presentation mode events only if enabled
1683
- if (eventEnablement.presentationModeChanged) {
1684
- // Map PDF.js PresentationModeState to boolean
1685
- // FULLSCREEN = 3, CHANGING = 2, NORMAL = 1, UNKNOWN = 0
1686
- const isActive = event.state === 3; // PresentationModeState.FULLSCREEN
1687
- const isChanging = event.state === 2; // PresentationModeState.CHANGING
1688
- const presentationData = {
1689
- active: isActive,
1690
- switchInProgress: isChanging
1691
- };
1692
- sendEventNotification('presentationModeChanged', presentationData);
1693
- }
1694
- });
1695
-
1696
- app.eventBus.on('fileinputchange', (event) => {
1697
- // Send open file events only if enabled
1698
- if (eventEnablement.openFile) {
1699
- sendEventNotification('openFile', null);
1700
- }
1701
- });
1702
-
1703
- app.eventBus.on('find', (event) => {
1704
- // Send find events only if enabled
1705
- if (eventEnablement.find) {
1706
- const findData = {
1707
- query: event.query || '',
1708
- phraseSearch: false, // Not available in PDF.js v5.3.93
1709
- caseSensitive: event.caseSensitive || false,
1710
- entireWord: event.entireWord || false,
1711
- highlightAll: event.highlightAll || false,
1712
- findPrevious: event.findPrevious || false
1713
- };
1714
- sendEventNotification('find', findData);
1715
- }
1716
- });
1717
-
1718
- app.eventBus.on('updatefindmatchescount', (event) => {
1719
- // Send find matches count events only if enabled
1720
- if (eventEnablement.updateFindMatchesCount) {
1721
- const countData = {
1722
- current: event.matchesCount?.current || 0,
1723
- total: event.matchesCount?.total || 0
1724
- };
1725
- sendEventNotification('updateFindMatchesCount', countData);
1726
- }
1727
- });
1728
-
1729
- app.eventBus.on('metadataloaded', (event) => {
1730
- // Send metadata loaded events only if enabled
1731
- if (eventEnablement.metadataLoaded) {
1732
- const info = app.documentInfo;
1733
- const metadataData = {
1734
- title: info?.Title,
1735
- author: info?.Author,
1736
- subject: info?.Subject,
1737
- keywords: info?.Keywords,
1738
- creator: info?.Creator,
1739
- producer: info?.Producer,
1740
- creationDate: info?.CreationDate,
1741
- modificationDate: info?.ModDate,
1742
- pdfFormatVersion: info?.PDFFormatVersion,
1743
- isLinearized: info?.IsLinearized,
1744
- isAcroFormPresent: info?.IsAcroFormPresent,
1745
- isXFAPresent: info?.IsXFAPresent,
1746
- isCollectionPresent: info?.IsCollectionPresent
1747
- };
1748
- sendEventNotification('metadataLoaded', metadataData);
1749
- }
1750
- });
1751
-
1752
- app.eventBus.on('outlineloaded', (event) => {
1753
- // Send outline loaded events only if enabled
1754
- if (eventEnablement.outlineLoaded) {
1755
- const outlineData = {
1756
- items: [], // Outline items are not available in the event, but consumers can check hasOutline
1757
- hasOutline: (event.outlineCount || 0) > 0
1758
- };
1759
- sendEventNotification('outlineLoaded', outlineData);
1760
- }
1761
- });
1762
-
1763
- app.eventBus.on('pagerendered', (event) => {
1764
- // Send page rendered events only if enabled
1765
- if (eventEnablement.pageRendered) {
1766
- const renderData = {
1767
- pageNumber: event.pageNumber || 1,
1768
- // Don't include source as it contains canvas element that can't be cloned
1769
- timestamp: Date.now()
1770
- };
1771
- sendEventNotification('pageRendered', renderData);
1772
- }
1773
- });
1774
-
1775
- // Loading state integration for overlay control
1776
- // Event-driven only; idempotent notifications, no flags or polling
1777
- app.eventBus.on('documentinit', () => sendStateChangeNotification('loading', true, 'system'));
1778
- app.eventBus.on('pagesinit', () => sendStateChangeNotification('loading', true, 'system'));
1779
- app.eventBus.on('pagerendered', () => sendStateChangeNotification('loading', false, 'system'));
1780
- app.eventBus.on('pagesloaded', () => sendStateChangeNotification('loading', false, 'system'));
1781
-
1782
- // New high-value events
1783
-
1784
- // Annotation Layer Rendered - Native PDF.js event
1785
- app.eventBus.on('annotationlayerrendered', (event) => {
1786
- // Send annotation layer rendered events only if enabled
1787
- if (eventEnablement.annotationLayerRendered) {
1788
- const renderData = {
1789
- pageNumber: event.pageNumber || 1,
1790
- error: event.error || null,
1791
- timestamp: performance.now()
1792
- };
1793
- sendEventNotification('annotationLayerRendered', renderData);
1794
- }
1795
- });
1796
-
1797
- // Reset idle timer when PDF document changes (event-driven approach)
1798
- // Set up idle timer only if enabled
1799
- if (eventEnablement.idle) {
1800
- app.eventBus.on('documentloaded', () => {
1801
- if (typeof resetIdleTimer === 'function') {
1802
- resetIdleTimer();
1803
- log('Idle timer reset on document load');
1804
- }
1805
- });
1806
- }
1807
- }
1808
-
1809
- // Initialize custom event implementations
1810
- initializeCustomEvents(app);
1811
-
1812
-
1813
- }
1814
-
1815
- // Initialize custom event implementations
1816
- function initializeCustomEvents(app) {
1817
- setupIdleDetection();
1818
- interceptOutlineClicks(app);
1819
- }
1820
-
1821
- // Idle Detection - Custom activity tracking
1822
- let idleTimer = null;
1823
- let idleListenersSetup = false;
1824
- let idleCleanupListeners = []; // Track listeners for cleanup
1825
- const IDLE_TIMEOUT = 30000; // 30 seconds default
1826
-
1827
- function resetIdleTimer() {
1828
- if (idleTimer) {
1829
- clearTimeout(idleTimer);
1830
- idleTimer = null;
1831
- }
1832
- // Set up idle timer only if enabled
1833
- if (eventEnablement.idle) {
1834
- idleTimer = setTimeout(() => {
1835
- sendEventNotification('idle', null);
1836
- }, IDLE_TIMEOUT);
1837
- }
1838
- }
1839
-
1840
- function cleanupIdleDetection() {
1841
- // Clear timer
1842
- if (idleTimer) {
1843
- clearTimeout(idleTimer);
1844
- idleTimer = null;
1845
- }
1846
-
1847
- // Remove all tracked event listeners
1848
- idleCleanupListeners.forEach(({ element, event, handler, options }) => {
1849
- element.removeEventListener(event, handler, options);
1850
- });
1851
- idleCleanupListeners = [];
1852
- idleListenersSetup = false;
1853
-
1854
- log('Idle detection cleanup completed');
1855
- }
1856
-
1857
- function setupIdleDetection() {
1858
- // Prevent multiple setups
1859
- if (idleListenersSetup) {
1860
- log('Idle detection already setup, skipping');
1861
- return;
1862
- }
1863
-
1864
- idleListenersSetup = true;
1865
-
1866
- // Helper to add and track event listeners
1867
- const addTrackedListener = (element, event, handler, options) => {
1868
- element.addEventListener(event, handler, options);
1869
- idleCleanupListeners.push({ element, event, handler, options });
1870
- };
1871
-
1872
- // Track mouse activity
1873
- addTrackedListener(document, 'mousemove', resetIdleTimer, { passive: true });
1874
- addTrackedListener(document, 'mousedown', resetIdleTimer, { passive: true });
1875
- addTrackedListener(document, 'click', resetIdleTimer, { passive: true });
1876
-
1877
- // Track keyboard activity
1878
- addTrackedListener(document, 'keydown', resetIdleTimer, { passive: true });
1879
- addTrackedListener(document, 'keypress', resetIdleTimer, { passive: true });
1880
-
1881
- // Track scroll activity
1882
- addTrackedListener(document, 'scroll', resetIdleTimer, { passive: true });
1883
- addTrackedListener(document, 'wheel', resetIdleTimer, { passive: true });
1884
-
1885
- // Track touch activity
1886
- addTrackedListener(document, 'touchstart', resetIdleTimer, { passive: true });
1887
- addTrackedListener(document, 'touchmove', resetIdleTimer, { passive: true });
1888
-
1889
- // Start initial timer
1890
- resetIdleTimer();
1891
-
1892
- // Cleanup on document unload
1893
- const unloadHandler = () => {
1894
- cleanupIdleDetection();
1895
- };
1896
- addTrackedListener(window, 'beforeunload', unloadHandler, { passive: true });
1897
-
1898
-
1899
- }
1900
-
1901
- // Bookmark Click - Intercept outline item clicks
1902
- let bookmarkInterceptionSetup = false;
1903
-
1904
- function interceptOutlineClicks(app) {
1905
- // Prevent multiple setups
1906
- if (bookmarkInterceptionSetup) {
1907
- log('Bookmark click interception already setup, skipping');
1908
- return;
1909
- }
1910
-
1911
- // Check if PDF outline viewer is available immediately
1912
- if (app && app.pdfOutlineViewer && app.pdfOutlineViewer._bindLink) {
1913
- const originalBindLink = app.pdfOutlineViewer._bindLink;
1914
-
1915
- app.pdfOutlineViewer._bindLink = function(element, params) {
1916
- // Call original bind method first
1917
- originalBindLink.call(this, element, params);
1918
-
1919
- // Intercept bookmark clicks only if enabled
1920
- if (eventEnablement.bookmarkClick) {
1921
- const originalOnClick = element.onclick;
1922
- element.onclick = function(evt) {
1923
- // Extract bookmark data
1924
- const bookmarkData = {
1925
- title: element.textContent?.trim() || 'Unknown',
1926
- dest: params.dest || null,
1927
- action: params.action || undefined,
1928
- url: params.url || undefined,
1929
- pageNumber: undefined, // Will be resolved by linkService
1930
- isCurrentItem: element.classList.contains('currentTreeItem')
1931
- };
1932
-
1933
- log(`Bookmark clicked: ${bookmarkData.title}`);
1934
- sendEventNotification('bookmarkClick', bookmarkData);
1935
-
1936
- // Call original handler to preserve navigation functionality
1937
- if (originalOnClick) {
1938
- return originalOnClick.call(this, evt);
1939
- }
1940
- return false;
1941
- };
1942
- }
1943
- };
1944
-
1945
- bookmarkInterceptionSetup = true;
1946
-
1947
- } else {
1948
- // If outline viewer is not available yet, defer setup using event-driven approach
1949
- // Hook into outlineloaded event to retry when outline becomes available
1950
- if (app && app.eventBus) {
1951
- const outlineLoadedHandler = () => {
1952
- if (!bookmarkInterceptionSetup) {
1953
- interceptOutlineClicks(app); // Retry when outline is loaded
1954
- }
1955
- };
1956
- app.eventBus.on('outlineloaded', outlineLoadedHandler);
1957
- log('Bookmark click interception deferred until outline loads');
1958
- } else {
1959
- log('Unable to setup bookmark click interception - event bus not available', 'warn');
1960
- }
1961
- }
1962
- }
1963
- // #endregion
1964
-
1965
- // Start the initialization process
1966
- waitForViewer();
1
+ (function() {
2
+ 'use strict';
3
+
4
+ // This will be wrapped in diagnostic logs check after diagnosticLogs is set
5
+
6
+ // #region URL Security Validation
7
+ let originalFileUrl = null;
8
+ let urlValidationEnabled = true;
9
+
10
+ function initializeUrlValidation() {
11
+ // Get the initial file URL from query parameters
12
+ const urlParams = new URLSearchParams(window.location.search);
13
+ const currentFileUrl = urlParams.get('file');
14
+
15
+ // Check if we already have a stored original URL
16
+ const storedOriginalUrl = sessionStorage.getItem('ng2-pdfjs-viewer-original-file-url');
17
+
18
+ if (storedOriginalUrl) {
19
+ // Use the stored original URL (from previous page load)
20
+ originalFileUrl = storedOriginalUrl;
21
+ } else if (currentFileUrl) {
22
+ // Store the current URL as the original (first time loading)
23
+ originalFileUrl = currentFileUrl;
24
+ sessionStorage.setItem('ng2-pdfjs-viewer-original-file-url', originalFileUrl);
25
+ }
26
+ }
27
+
28
+ function validateCurrentUrl() {
29
+ if (!urlValidationEnabled || !originalFileUrl) {
30
+ return true; // Skip validation if disabled or no original URL
31
+ }
32
+
33
+ const currentFileParam = new URLSearchParams(window.location.search).get('file');
34
+
35
+ // Compare the file parameter specifically
36
+ if (currentFileParam !== originalFileUrl) {
37
+ console.error('🚨 SECURITY ALERT: File URL has been modified!');
38
+ console.error('Original:', originalFileUrl);
39
+ console.error('Current:', currentFileParam);
40
+
41
+ // Prevent loading and show error
42
+ showSecurityError('Unauthorized file access detected. The file URL has been tampered with.');
43
+ return false;
44
+ }
45
+
46
+ return true;
47
+ }
48
+
49
+ function showSecurityError(message) {
50
+ // Simple console warning (less intrusive)
51
+ console.warn('🚨 Security Warning:', message);
52
+
53
+ // Send notification to parent window for Angular template handling
54
+ if (window.parent && window.parent !== window) {
55
+ window.parent.postMessage({
56
+ type: 'ng2-pdfjs-viewer-security-warning',
57
+ message: message,
58
+ originalUrl: originalFileUrl,
59
+ currentUrl: new URLSearchParams(window.location.search).get('file')
60
+ }, '*');
61
+ }
62
+ }
63
+
64
+ // Initialize URL validation on page load
65
+ initializeUrlValidation();
66
+
67
+ // Add validation check before PDF.js loads
68
+ window.addEventListener('DOMContentLoaded', () => {
69
+ if (!validateCurrentUrl()) {
70
+ // Prevent PDF.js from loading if URL validation fails
71
+ return;
72
+ }
73
+ });
74
+
75
+ // Validate on page load
76
+ window.addEventListener('load', () => {
77
+ validateCurrentUrl();
78
+ });
79
+
80
+ // Also validate on hash change (in case user tries to modify URL)
81
+ window.addEventListener('hashchange', () => {
82
+ validateCurrentUrl();
83
+ });
84
+
85
+ // Validate on popstate (back/forward navigation)
86
+ window.addEventListener('popstate', () => {
87
+ validateCurrentUrl();
88
+ });
89
+
90
+ // Run validation immediately (in case page is already loaded)
91
+ validateCurrentUrl();
92
+ // #endregion
93
+
94
+ // Override the webviewerloaded event to dispatch to parent window
95
+ // This allows us to capture the event outside the iframe and set locale before PDF.js initializes
96
+ const originalDispatchEvent = document.dispatchEvent;
97
+ document.dispatchEvent = function(event) {
98
+ if (event.type === 'webviewerloaded') {
99
+ // Dispatch to parent window so it can be captured outside the iframe
100
+ if (window.parent && window.parent !== window) {
101
+ window.parent.document.dispatchEvent(event);
102
+ }
103
+ }
104
+ return originalDispatchEvent.call(this, event);
105
+ };
106
+
107
+ // #region Constants and Configuration
108
+ // Command tracking for zoom to prevent infinite loops
109
+ const ZoomCommandTracker = {
110
+ activeZoomCommand: false,
111
+
112
+ markZoomCommandStart() {
113
+ this.activeZoomCommand = true;
114
+ },
115
+
116
+ markZoomCommandEnd() {
117
+ this.activeZoomCommand = false;
118
+ },
119
+
120
+ isZoomCommandActive() {
121
+ return this.activeZoomCommand;
122
+ }
123
+ };
124
+
125
+ // Readiness state management
126
+ const ViewerReadiness = {
127
+ NOT_LOADED: 0,
128
+ VIEWER_LOADED: 1, // PDFViewerApplication exists
129
+ VIEWER_INITIALIZED: 2, // PDFViewerApplication.initialized = true
130
+ EVENTBUS_READY: 3, // Event bus is available and ready
131
+ COMPONENTS_READY: 4, // All required components available
132
+ DOCUMENT_LOADED: 5 // PDF document fully loaded
133
+ };
134
+
135
+ // Pure event-driven architecture - no action categorization needed
136
+ // All actions are dispatched at required readiness levels by Universal Dispatcher
137
+ // #endregion
138
+
139
+ // #region State Management
140
+ let currentReadiness = ViewerReadiness.NOT_LOADED;
141
+ let readinessCallbacks = [];
142
+ let diagnosticLogs = false; // Default to false for production
143
+ // #endregion
144
+
145
+ // #region Utility Functions
146
+ function log(message, level = 'info') {
147
+ if (!diagnosticLogs) return; // Only log when diagnostic logs are enabled
148
+
149
+ const timestamp = new Date().toISOString();
150
+ const prefix = '[PostMessage]';
151
+
152
+ switch (level) {
153
+ case 'error':
154
+ console.error(`${prefix} ${timestamp} ERROR: ${message}`);
155
+ break;
156
+ case 'warn':
157
+ console.warn(`${prefix} ${timestamp} WARN: ${message}`);
158
+ break;
159
+ default:
160
+ console.log(`${prefix} ${timestamp} INFO: ${message}`);
161
+ }
162
+ }
163
+
164
+ // Set diagnostic logs mode
165
+ function setDiagnosticLogs(enabled) {
166
+ diagnosticLogs = enabled;
167
+ }
168
+
169
+ // Button visibility control helper (consolidated from separate module)
170
+ function toggleButtonVisibility(primaryId, secondaryId, visible) {
171
+ const primary = document.getElementById(primaryId);
172
+ const secondary = secondaryId ? document.getElementById(secondaryId) : null;
173
+ if (primary) primary.classList.toggle('hidden', !visible);
174
+ if (secondary) secondary.classList.toggle('hidden', !visible);
175
+ }
176
+
177
+ // Generic element visibility control for toolbar/sidebar groups
178
+ function toggleElementVisibilityById(elementId, visible) {
179
+ const el = document.getElementById(elementId);
180
+ if (!el) {
181
+ log(`Element not found for visibility toggle: ${elementId}`, 'warn');
182
+ return;
183
+ }
184
+ el.classList.toggle('hidden', !visible);
185
+ }
186
+
187
+ // Toolbar section visibility control that preserves layout
188
+ function toggleToolbarSectionVisibility(elementId, visible) {
189
+ const el = document.getElementById(elementId);
190
+ if (!el) {
191
+ log(`Toolbar section not found for visibility toggle: ${elementId}`, 'warn');
192
+ return;
193
+ }
194
+
195
+ // Use CSS class instead of inline styles (CSP-safe)
196
+ el.classList.toggle('ng2-hidden-section', !visible);
197
+ }
198
+
199
+ // Readiness state management
200
+ function setReadiness(readiness) {
201
+ if (readiness > currentReadiness) {
202
+ log(`Readiness state changed: ${currentReadiness} → ${readiness}`);
203
+ currentReadiness = readiness;
204
+
205
+ // Execute callbacks for this readiness level
206
+ readinessCallbacks.forEach(callback => {
207
+ if (callback.readiness <= readiness && !callback.executed) {
208
+ callback.executed = true;
209
+ callback.callback();
210
+ }
211
+ });
212
+ }
213
+ }
214
+
215
+ function onReadiness(readiness, callback) {
216
+ if (currentReadiness >= readiness) {
217
+ callback();
218
+ } else {
219
+ readinessCallbacks.push({ readiness, callback, executed: false });
220
+ }
221
+ }
222
+
223
+ // Note: Density styles now loaded from external ng2-customization.css (CSP-safe)
224
+
225
+ // Enhanced zoom transformation function
226
+ function transformZoomFromViewer(scale, app) {
227
+ if (typeof scale === 'string') {
228
+ return scale;
229
+ }
230
+
231
+ if (app?.pdfViewer) {
232
+ const viewer = app.pdfViewer;
233
+
234
+ // Check for special zoom modes
235
+ const specialModes = ['page-fit', 'page-width', 'page-actual', 'auto'];
236
+ if (specialModes.includes(viewer.currentScaleValue)) {
237
+ return viewer.currentScaleValue;
238
+ }
239
+
240
+ // Check predefined zoom levels
241
+ const predefinedZooms = {
242
+ 0.5: '0.5', 0.75: '0.75', 1: '1', 1.25: '1.25',
243
+ 1.5: '1.5', 2: '2', 3: '3', 4: '4'
244
+ };
245
+
246
+ for (const [zoomLevel, zoomValue] of Object.entries(predefinedZooms)) {
247
+ if (Math.abs(scale - parseFloat(zoomLevel)) < 0.01) {
248
+ return zoomValue;
249
+ }
250
+ }
251
+ }
252
+
253
+ return typeof scale === 'number' ? `${Math.round(scale * 100)}%` : 'auto';
254
+ }
255
+
256
+ // Send state change notifications to Angular component
257
+ function sendStateChangeNotification(property, value, source = 'user') {
258
+ try {
259
+ const message = {
260
+ type: 'state-change',
261
+ property: property,
262
+ value: value,
263
+ source: source,
264
+ timestamp: Date.now()
265
+ };
266
+
267
+ window.parent.postMessage(message, '*');
268
+ } catch (error) {
269
+ log(`Failed to send state change notification: ${error.message}`, 'error');
270
+ }
271
+ }
272
+
273
+ // Send event notifications to Angular component
274
+ function sendEventNotification(eventName, eventData) {
275
+ try {
276
+ const message = {
277
+ type: 'event-notification',
278
+ eventName: eventName,
279
+ eventData: eventData,
280
+ timestamp: Date.now()
281
+ };
282
+
283
+ window.parent.postMessage(message, '*');
284
+ } catch (error) {
285
+ log(`Failed to send event notification: ${error.message}`, 'error');
286
+ }
287
+ }
288
+
289
+ function sendResponse(id, response) {
290
+ if (id && window.parent) {
291
+ window.parent.postMessage({
292
+ type: 'control-response',
293
+ id: id,
294
+ ...response,
295
+ timestamp: Date.now()
296
+ }, '*');
297
+ }
298
+ }
299
+
300
+ function getState() {
301
+ const app = PDFViewerApplication;
302
+ if (!app) {
303
+ return { ready: false };
304
+ }
305
+
306
+ return {
307
+ ready: app.initialized,
308
+ page: app.page,
309
+ pagesCount: app.pagesCount,
310
+ currentScale: app.pdfViewer ? app.pdfViewer.currentScale : null,
311
+ currentScaleValue: app.pdfViewer ? app.pdfViewer.currentScaleValue : null,
312
+ pagesRotation: app.pdfViewer ? app.pdfViewer.pagesRotation : null,
313
+ scrollMode: app.pdfViewer ? app.pdfViewer.scrollMode : null,
314
+ spreadMode: app.pdfViewer ? app.pdfViewer.spreadMode : null
315
+ };
316
+ }
317
+
318
+ // Common control update utilities
319
+ const ControlUtils = {
320
+ // Common button visibility update pattern
321
+ updateButtonVisibility: function(buttonId, secondaryId, visible) {
322
+ const button = document.getElementById(buttonId);
323
+ const secondaryButton = secondaryId ? document.getElementById(secondaryId) : null;
324
+
325
+ if (button) {
326
+ button.classList.toggle('hidden', !visible);
327
+ }
328
+ if (secondaryButton) {
329
+ secondaryButton.classList.toggle('hidden', !visible);
330
+ }
331
+
332
+ },
333
+
334
+ // Common mode update pattern with event bus
335
+ updateModeViaEventBus: function(eventName, modeValue, modeMap, propertyName) {
336
+ const app = PDFViewerApplication;
337
+ if (!app?.eventBus) {
338
+ log('EventBus not available for mode update', 'error');
339
+ return;
340
+ }
341
+
342
+ try {
343
+ const upperMode = typeof modeValue === 'string' ? modeValue.toUpperCase() : modeValue;
344
+ const mode = modeMap[upperMode];
345
+
346
+ if (mode !== undefined) {
347
+ app.eventBus.dispatch(eventName, { mode });
348
+
349
+ // NOTE: State change notifications are sent by the bidirectional event listeners
350
+ // when the actual mode change occurs in PDF.js, not immediately when we dispatch
351
+ // This prevents infinite loops and ensures we only notify on actual changes
352
+ } else {
353
+ log(`Unknown ${propertyName} mode: ${modeValue}`, 'warn');
354
+ }
355
+ } catch (error) {
356
+ log(`Error updating ${propertyName} mode: ${error.message}`, 'error');
357
+ }
358
+ },
359
+
360
+ // Common direct property update pattern
361
+ updatePropertyDirectly: function(propertyPath, value, propertyName) {
362
+ const app = PDFViewerApplication;
363
+ if (!app?.pdfViewer) {
364
+ log(`PDFViewer not available for ${propertyName} update`, 'warn');
365
+ return;
366
+ }
367
+
368
+ try {
369
+ // Navigate to nested property
370
+ const pathParts = propertyPath.split('.');
371
+ let target = app;
372
+
373
+ for (let i = 0; i < pathParts.length - 1; i++) {
374
+ target = target[pathParts[i]];
375
+ if (!target) {
376
+ log(`Property path ${propertyPath} not found for ${propertyName}`, 'warn');
377
+ return;
378
+ }
379
+ }
380
+
381
+ target[pathParts[pathParts.length - 1]] = value;
382
+ } catch (error) {
383
+ log(`Direct ${propertyName} update failed: ${error.message}`, 'warn');
384
+ }
385
+ },
386
+
387
+ // Common validation pattern
388
+ validateAndExecute: function(condition, action, errorMessage) {
389
+ if (condition) {
390
+ action();
391
+ } else {
392
+ log(errorMessage, 'warn');
393
+ }
394
+ }
395
+ };
396
+ // #endregion
397
+
398
+ // #region Initialization and Setup
399
+
400
+ // Enhanced readiness checking with proper async initialization handling
401
+ function checkViewerReadiness() {
402
+ const app = PDFViewerApplication;
403
+
404
+ if (!app) {
405
+ setReadiness(ViewerReadiness.NOT_LOADED);
406
+ return;
407
+ }
408
+
409
+ setReadiness(ViewerReadiness.VIEWER_LOADED);
410
+
411
+ // Handle async initialization properly
412
+ if (app.initializedPromise) {
413
+ app.initializedPromise.then(() => {
414
+ setReadiness(ViewerReadiness.VIEWER_INITIALIZED);
415
+
416
+ // Check if event bus is ready
417
+ if (app.eventBus && typeof app.eventBus.dispatch === 'function') {
418
+ setReadiness(ViewerReadiness.EVENTBUS_READY);
419
+
420
+ // Check if key components are available
421
+ if (app.pdfViewer && app.pdfCursorTools) {
422
+ setReadiness(ViewerReadiness.COMPONENTS_READY);
423
+
424
+ // CSS zoom setting is now handled via PostMessage configuration
425
+ // No need to check global window variables
426
+ }
427
+ }
428
+ }).catch(error => {
429
+ log(`PDFViewerApplication initialization failed: ${error.message}`, 'error');
430
+ });
431
+ } else if (app.initialized) {
432
+ // Fallback for synchronous check (though this should be async)
433
+ setReadiness(ViewerReadiness.VIEWER_INITIALIZED);
434
+
435
+ // Check if event bus is ready
436
+ if (app.eventBus && typeof app.eventBus.dispatch === 'function') {
437
+ setReadiness(ViewerReadiness.EVENTBUS_READY);
438
+
439
+ // Check if key components are available
440
+ if (app.pdfViewer && app.pdfCursorTools) {
441
+ setReadiness(ViewerReadiness.COMPONENTS_READY);
442
+
443
+ // CSS zoom setting is now handled via PostMessage configuration
444
+ // No need to check global window variables
445
+ }
446
+ }
447
+ }
448
+ }
449
+
450
+ // Wait for PDF.js viewer to be ready with event-driven approach
451
+ function waitForViewer() {
452
+ // Use MutationObserver to detect when PDFViewerApplication becomes available
453
+ const observer = new MutationObserver((mutations, obs) => {
454
+ if (typeof PDFViewerApplication !== 'undefined') {
455
+ obs.disconnect(); // Stop observing
456
+ checkViewerReadiness();
457
+
458
+ // Set up event-driven readiness monitoring
459
+ setupEventDrivenReadinessMonitoring();
460
+ }
461
+ });
462
+
463
+ // Start observing DOM changes
464
+ observer.observe(document, {
465
+ childList: true,
466
+ subtree: true
467
+ });
468
+
469
+ // Also check immediately in case it's already available
470
+ if (typeof PDFViewerApplication !== 'undefined') {
471
+ log('PDFViewerApplication already available, starting readiness check');
472
+ observer.disconnect();
473
+ checkViewerReadiness();
474
+ setupEventDrivenReadinessMonitoring();
475
+ }
476
+ }
477
+
478
+ // Set up event-driven readiness monitoring
479
+ function setupEventDrivenReadinessMonitoring() {
480
+ // Use a custom event system for readiness changes
481
+ const readinessEventTarget = new EventTarget();
482
+
483
+ // Override setReadiness to emit events
484
+ const originalSetReadiness = setReadiness;
485
+ setReadiness = function(readiness) {
486
+ originalSetReadiness(readiness);
487
+
488
+ // Emit custom event for readiness change
489
+ readinessEventTarget.dispatchEvent(new CustomEvent('readiness-change', {
490
+ detail: { readiness, previousReadiness: currentReadiness }
491
+ }));
492
+ };
493
+
494
+ // Listen for readiness changes
495
+ readinessEventTarget.addEventListener('readiness-change', (event) => {
496
+ const { readiness } = event.detail;
497
+
498
+ if (readiness >= ViewerReadiness.EVENTBUS_READY) {
499
+
500
+ initializePostMessageAPI();
501
+ }
502
+ });
503
+
504
+ // Check initial readiness
505
+ if (currentReadiness >= ViewerReadiness.EVENTBUS_READY) {
506
+
507
+ initializePostMessageAPI();
508
+ }
509
+ }
510
+
511
+ function initializePostMessageAPI() {
512
+ // Add message listener
513
+ window.addEventListener('message', handleControlMessage);
514
+
515
+ // Expose API for external access
516
+ window.Ng2PdfJsViewerAPI = {
517
+ updateControl: updateControl,
518
+ getState: getState,
519
+ isReady: () => currentReadiness >= ViewerReadiness.EVENTBUS_READY,
520
+ getReadiness: () => currentReadiness
521
+ };
522
+
523
+ // Notify parent that PostMessage API is ready
524
+ window.parent.postMessage({
525
+ type: 'postmessage-ready',
526
+ timestamp: Date.now(),
527
+ readiness: currentReadiness
528
+ }, '*');
529
+
530
+ // Set up event listeners for readiness changes
531
+ setupReadinessEventListeners();
532
+ }
533
+
534
+ function setupReadinessEventListeners() {
535
+ const app = PDFViewerApplication;
536
+ if (!app || !app.eventBus) return;
537
+
538
+ // Set up error event listeners immediately (before document loads)
539
+ setupErrorEventListeners();
540
+
541
+ // Listen for document loaded event
542
+ app.eventBus.on('documentloaded', () => {
543
+ setReadiness(ViewerReadiness.DOCUMENT_LOADED);
544
+
545
+ // Set up bidirectional event listeners once document is loaded
546
+ setupBidirectionalEventListeners();
547
+ });
548
+
549
+ // Listen for pages loaded event
550
+ app.eventBus.on('pagesloaded', () => {
551
+ // Ensure we're at least at components ready level
552
+ if (currentReadiness < ViewerReadiness.COMPONENTS_READY) {
553
+ setReadiness(ViewerReadiness.COMPONENTS_READY);
554
+ }
555
+ });
556
+ }
557
+ // #endregion
558
+
559
+ // #region Message Handling
560
+ function handleControlMessage(event) {
561
+ const { type, action, payload, id } = event.data;
562
+
563
+ if (type === 'control-update') {
564
+ try {
565
+
566
+ // Universal Dispatcher has already verified readiness - just execute
567
+ updateControl(action, payload);
568
+ sendResponse(id, { success: true, action, payload });
569
+ } catch (error) {
570
+ const errorMsg = `Error processing ${action}: ${error.message}`;
571
+ log(errorMsg, 'error');
572
+ sendResponse(id, { success: false, error: errorMsg });
573
+ }
574
+ }
575
+ }
576
+ // #endregion
577
+
578
+ // #region Control Update Functions
579
+ function updateControl(action, payload) {
580
+ const app = PDFViewerApplication;
581
+
582
+ // Validate that PDFViewerApplication is available
583
+ if (!app) {
584
+ log('PDFViewerApplication not available', 'error');
585
+ return;
586
+ }
587
+
588
+ try {
589
+ // Note: Density styles loaded from external ng2-customization.css (CSP-safe)
590
+ switch (action) {
591
+ // Button visibility controls (using consolidated helper function)
592
+ case 'show-download':
593
+ toggleButtonVisibility('downloadButton', 'secondaryDownload', payload);
594
+ break;
595
+ case 'show-print':
596
+ toggleButtonVisibility('printButton', 'secondaryPrint', payload);
597
+ break;
598
+ case 'show-fullscreen':
599
+ toggleButtonVisibility('presentationMode', null, payload);
600
+ break;
601
+ case 'show-find':
602
+ toggleButtonVisibility('viewFindButton', null, payload);
603
+ break;
604
+ case 'show-bookmark':
605
+ toggleButtonVisibility('viewBookmark', null, payload);
606
+ break;
607
+ case 'show-openfile':
608
+ toggleButtonVisibility('openFile', 'secondaryOpenFile', payload);
609
+ break;
610
+ case 'show-annotations':
611
+ toggleElementVisibilityById('editorModeButtons', payload);
612
+ break;
613
+
614
+ case 'set-toolbar-density': {
615
+ // Apply compact/comfortable density by toggling a class on #toolbarContainer
616
+ const container = document.getElementById('toolbarContainer');
617
+ if (container) {
618
+ container.classList.remove('density-default','density-compact','density-comfortable');
619
+ const density = typeof payload === 'string' ? payload : 'default';
620
+ container.classList.add(`density-${density}`);
621
+ }
622
+ break;
623
+ }
624
+ case 'set-sidebar-width': {
625
+ // Use CSS variable only (CSP-safe) - width applied via ng2-customization.css
626
+ const outer = document.getElementById('outerContainer');
627
+ if (outer && typeof payload === 'string' && payload.trim() !== '') {
628
+ outer.style.setProperty('--sidebar-width', payload);
629
+ }
630
+ break;
631
+ }
632
+ case 'set-toolbar-position': {
633
+ const outer = document.getElementById('outerContainer');
634
+ if (outer) {
635
+ outer.classList.remove('toolbar-bottom');
636
+ if (payload === 'bottom') outer.classList.add('toolbar-bottom');
637
+ }
638
+ break;
639
+ }
640
+ case 'set-sidebar-position': {
641
+ const outer = document.getElementById('outerContainer');
642
+ if (outer) {
643
+ outer.classList.remove('sidebar-right');
644
+ if (payload === 'right') outer.classList.add('sidebar-right');
645
+ }
646
+ break;
647
+ }
648
+ case 'set-responsive-breakpoint': {
649
+ const value = typeof payload === 'number' ? `${payload}px` : `${payload}`;
650
+ const outer = document.getElementById('outerContainer');
651
+ if (outer && value) {
652
+ outer.style.setProperty('--ng2-responsive-breakpoint', value);
653
+ }
654
+ break;
655
+ }
656
+
657
+ case 'show-toolbar-left':
658
+ toggleToolbarSectionVisibility('toolbarViewerLeft', payload);
659
+ break;
660
+ case 'show-toolbar-middle':
661
+ toggleToolbarSectionVisibility('toolbarViewerMiddle', payload);
662
+ break;
663
+ case 'show-toolbar-right':
664
+ toggleToolbarSectionVisibility('toolbarViewerRight', payload);
665
+ break;
666
+ case 'show-secondary-toolbar-toggle':
667
+ toggleElementVisibilityById('secondaryToolbarToggle', payload);
668
+ break;
669
+ case 'show-sidebar':
670
+ toggleElementVisibilityById('sidebarContainer', payload);
671
+ break;
672
+ case 'show-sidebar-left':
673
+ toggleElementVisibilityById('toolbarSidebarLeft', payload);
674
+ break;
675
+ case 'show-sidebar-right':
676
+ toggleElementVisibilityById('toolbarSidebarRight', payload);
677
+ break;
678
+
679
+ // Mode controls
680
+ case 'set-zoom':
681
+ updateZoom(payload);
682
+ break;
683
+ case 'set-cursor':
684
+ updateCursor(payload);
685
+ break;
686
+ case 'set-scroll':
687
+ ControlUtils.updateModeViaEventBus('switchscrollmode', payload,
688
+ { 'VERTICAL': 0, 'V': 0, 'HORIZONTAL': 1, 'H': 1, 'WRAPPED': 2, 'W': 2, 'PAGE': 3, 'P': 3 },
689
+ 'scroll mode');
690
+ ControlUtils.updatePropertyDirectly('pdfViewer.scrollMode',
691
+ ({ 'VERTICAL': 0, 'V': 0, 'HORIZONTAL': 1, 'H': 1, 'WRAPPED': 2, 'W': 2, 'PAGE': 3, 'P': 3 })[payload?.toUpperCase()] || 0,
692
+ 'scroll mode');
693
+ break;
694
+ case 'set-spread':
695
+ ControlUtils.updateModeViaEventBus('switchspreadmode', payload,
696
+ { 'NONE': 0, 'N': 0, 'ODD': 1, 'O': 1, 'EVEN': 2, 'E': 2 },
697
+ 'spread mode');
698
+ ControlUtils.updatePropertyDirectly('pdfViewer.spreadMode',
699
+ ({ 'NONE': 0, 'N': 0, 'ODD': 1, 'O': 1, 'EVEN': 2, 'E': 2 })[payload?.toUpperCase()] || 0,
700
+ 'spread mode');
701
+ break;
702
+
703
+ // Navigation controls
704
+ case 'set-page':
705
+ // Universal Dispatcher guarantees PDFViewerApplication.initialized at readiness level 5
706
+ const pageNumber = parseInt(payload, 10);
707
+ if (pageNumber > 0 && pageNumber <= PDFViewerApplication.pagesCount) {
708
+ PDFViewerApplication.page = pageNumber;
709
+ } else {
710
+ log(`Invalid page number: ${payload}`, 'warn');
711
+ }
712
+ break;
713
+ case 'set-rotation':
714
+ // Universal Dispatcher guarantees PDFViewerApplication.initialized at readiness level 5
715
+ const rotation = parseInt(payload, 10);
716
+ if ([0, 90, 180, 270].includes(rotation)) {
717
+ // Set rotation on pdfViewer to trigger proper refresh
718
+ PDFViewerApplication.pdfViewer.pagesRotation = rotation;
719
+ // Note: No need to send state change notification here -
720
+ // PDF.js will fire 'rotationchanging' event which we handle separately
721
+ } else {
722
+ log(`Invalid rotation: ${payload}`, 'warn');
723
+ }
724
+ break;
725
+ case 'go-to-last-page':
726
+ if (payload === true) {
727
+ // Universal Dispatcher guarantees PDFViewerApplication.initialized at readiness level 5
728
+ const lastPage = PDFViewerApplication.pagesCount;
729
+ PDFViewerApplication.page = lastPage;
730
+ }
731
+ break;
732
+ case 'go-to-named-dest':
733
+ // Validate that payload is not empty or null before processing
734
+ if (!payload || typeof payload !== 'string' || payload.trim() === '') {
735
+ log(`Skipping invalid named destination: "${payload}"`);
736
+ break;
737
+ }
738
+
739
+ // Universal Dispatcher guarantees PDFViewerApplication.pdfLinkService at readiness level 5
740
+ PDFViewerApplication.pdfLinkService.goToDestination(payload);
741
+ break;
742
+ case 'update-page-mode':
743
+ // Universal Dispatcher guarantees PDFViewerApplication.eventBus at readiness level 4
744
+ const mode = payload ? payload.toLowerCase() : 'none';
745
+ PDFViewerApplication.eventBus.dispatch('pagemode', { mode });
746
+ break;
747
+
748
+ // Configuration actions
749
+ case 'set-download-filename':
750
+ setDownloadFilename(payload);
751
+ break;
752
+
753
+ // Auto actions - Universal Dispatcher guarantees eventBus availability at readiness level 5
754
+ case 'trigger-download':
755
+ if (payload === true) {
756
+ PDFViewerApplication.eventBus.dispatch('download');
757
+ }
758
+ break;
759
+ case 'trigger-print':
760
+ if (payload === true) {
761
+ PDFViewerApplication.eventBus.dispatch('print');
762
+ }
763
+ break;
764
+ case 'trigger-rotate-cw':
765
+ if (payload === true) {
766
+ PDFViewerApplication.eventBus.dispatch('rotatecw');
767
+ }
768
+ break;
769
+ case 'trigger-rotate-ccw':
770
+ if (payload === true) {
771
+ PDFViewerApplication.eventBus.dispatch('rotateccw');
772
+ }
773
+ break;
774
+
775
+ // Error handling
776
+ case 'set-error-message':
777
+ setErrorMessage(payload);
778
+ break;
779
+ case 'set-error-override':
780
+ setErrorOverride(payload);
781
+ break;
782
+ case 'set-error-append':
783
+ setErrorAppend(payload);
784
+ break;
785
+
786
+ // CSS zoom
787
+ case 'set-css-zoom':
788
+ setCssZoom(payload);
789
+ break;
790
+
791
+ // Event enablement
792
+ case 'enable-before-print':
793
+ enableBeforePrint(payload);
794
+ break;
795
+ case 'enable-after-print':
796
+ enableAfterPrint(payload);
797
+ break;
798
+ case 'enable-pages-loaded':
799
+ enablePagesLoaded(payload);
800
+ break;
801
+ case 'enable-page-change':
802
+ enablePageChange(payload);
803
+ break;
804
+
805
+ // New high-value event enablement
806
+ case 'enable-document-error':
807
+ enableDocumentError(payload);
808
+ break;
809
+ case 'enable-document-init':
810
+ enableDocumentInit(payload);
811
+ break;
812
+ case 'enable-pages-init':
813
+ enablePagesInit(payload);
814
+ break;
815
+ case 'enable-presentation-mode-changed':
816
+ enablePresentationModeChanged(payload);
817
+ break;
818
+ case 'enable-open-file':
819
+ enableOpenFile(payload);
820
+ break;
821
+ case 'enable-find':
822
+ enableFind(payload);
823
+ break;
824
+ case 'enable-update-find-matches-count':
825
+ enableUpdateFindMatchesCount(payload);
826
+ break;
827
+ case 'enable-metadata-loaded':
828
+ enableMetadataLoaded(payload);
829
+ break;
830
+ case 'enable-outline-loaded':
831
+ enableOutlineLoaded(payload);
832
+ break;
833
+ case 'enable-page-rendered':
834
+ enablePageRendered(payload);
835
+ break;
836
+
837
+ // New high-value events
838
+ case 'enable-annotation-layer-rendered':
839
+ enableAnnotationLayerRendered(payload);
840
+ break;
841
+ case 'enable-bookmark-click':
842
+ enableBookmarkClick(payload);
843
+ break;
844
+ case 'enable-idle':
845
+ enableIdle(payload);
846
+ break;
847
+
848
+ // Theme & Visual Customization Actions
849
+ case 'set-theme':
850
+ setTheme(payload);
851
+ break;
852
+ case 'set-primary-color':
853
+ setPrimaryColor(payload);
854
+ break;
855
+ case 'set-background-color':
856
+ setBackgroundColor(payload);
857
+ break;
858
+ case 'set-page-border-color':
859
+ setPageBorderColor(payload);
860
+ break;
861
+ case 'set-page-spacing':
862
+ setPageSpacing(payload.margin, payload.spreadMargin, payload.border);
863
+ break;
864
+ case 'set-toolbar-color':
865
+ setToolbarColor(payload);
866
+ break;
867
+ case 'set-text-color':
868
+ setTextColor(payload);
869
+ break;
870
+ case 'set-border-radius':
871
+ setBorderRadius(payload);
872
+ break;
873
+ case 'set-custom-css':
874
+ // Handle both old format (string) and new format (object with nonce)
875
+ if (typeof payload === 'string') {
876
+ setCustomCSS(payload, null);
877
+ } else if (payload && typeof payload === 'object') {
878
+ setCustomCSS(payload.css, payload.nonce);
879
+ }
880
+ break;
881
+
882
+ case 'set-diagnostic-logs':
883
+ setDiagnosticLogs(payload);
884
+ break;
885
+ case 'set-url-validation':
886
+ urlValidationEnabled = payload === true;
887
+ log(`URL validation ${urlValidationEnabled ? 'enabled' : 'disabled'}`);
888
+ break;
889
+
890
+ default:
891
+ log(`Unknown action: ${action}`, 'warn');
892
+ }
893
+ } catch (error) {
894
+ log(`Error in updateControl for action ${action}: ${error.message}`, 'error');
895
+ throw error;
896
+ }
897
+ }
898
+ // #endregion
899
+
900
+ // #region Button Visibility Functions
901
+ function updateDownloadButton(visible) {
902
+ const button = document.getElementById('downloadButton');
903
+ const secondaryButton = document.getElementById('secondaryDownload');
904
+ if (button) {
905
+ button.classList.toggle('hidden', !visible);
906
+ }
907
+ if (secondaryButton) {
908
+ secondaryButton.classList.toggle('hidden', !visible);
909
+ }
910
+ }
911
+
912
+ function updatePrintButton(visible) {
913
+ const button = document.getElementById('printButton');
914
+ const secondaryButton = document.getElementById('secondaryPrint');
915
+ if (button) {
916
+ button.classList.toggle('hidden', !visible);
917
+ }
918
+ if (secondaryButton) {
919
+ secondaryButton.classList.toggle('hidden', !visible);
920
+ }
921
+ }
922
+
923
+ function updateFullScreenButton(visible) {
924
+ const button = document.getElementById('presentationMode');
925
+ if (button) {
926
+ button.classList.toggle('hidden', !visible);
927
+ }
928
+ }
929
+
930
+ function updateFindButton(visible) {
931
+ const button = document.getElementById('viewFindButton');
932
+ if (button) {
933
+ button.classList.toggle('hidden', !visible);
934
+ }
935
+ }
936
+
937
+ function updateBookmarkButton(visible) {
938
+ const button = document.getElementById('viewBookmark');
939
+ if (button) {
940
+ button.classList.toggle('hidden', !visible);
941
+ }
942
+ }
943
+
944
+ function updateOpenFileButton(visible) {
945
+ const button = document.getElementById('openFile');
946
+ const secondaryButton = document.getElementById('secondaryOpenFile');
947
+ if (button) {
948
+ button.classList.toggle('hidden', !visible);
949
+ }
950
+ if (secondaryButton) {
951
+ secondaryButton.classList.toggle('hidden', !visible);
952
+ }
953
+ }
954
+ function updateAnnotationsButton(visible) {
955
+ // Handle annotations button visibility
956
+ // This might need to be implemented based on the specific annotation system
957
+ log(`Annotations button visibility set to: ${visible}`);
958
+ }
959
+ // #endregion
960
+
961
+ // #region Mode Control Functions
962
+ function updateZoom(zoom) {
963
+ const app = PDFViewerApplication;
964
+ if (!app || !app.pdfViewer || !app.eventBus) {
965
+ log('PDFViewerApplication, pdfViewer, or eventBus not ready for zoom update', 'warn');
966
+ return;
967
+ }
968
+
969
+ // Mark zoom command as active to prevent infinite loop
970
+ ZoomCommandTracker.markZoomCommandStart();
971
+
972
+ try {
973
+ // Acceptable values: "auto", "page-fit", "page-width", "page-actual", "page-height", or a number/string number like "1.25"
974
+ const validStringZooms = ['auto', 'page-actual', 'page-fit', 'page-width', 'page-height'];
975
+
976
+ // Check if it's a valid string zoom
977
+ if (validStringZooms.includes(zoom)) {
978
+ // Dispatch the scalechanged event exactly like the UI does
979
+ app.eventBus.dispatch("scalechanged", {
980
+ source: app.toolbar, // Use toolbar as source, just like the UI does
981
+ value: zoom
982
+ });
983
+ return;
984
+ }
985
+
986
+ // Check if it's a valid numeric zoom (including decimal strings like "1.25")
987
+ const numericZoom = Number(zoom);
988
+ if (!isNaN(numericZoom) && numericZoom > 0) {
989
+ // Dispatch the scalechanged event exactly like the UI does
990
+ app.eventBus.dispatch("scalechanged", {
991
+ source: app.toolbar, // Use toolbar as source, just like the UI does
992
+ value: zoom
993
+ });
994
+ return;
995
+ }
996
+
997
+ // If we get here, it's invalid
998
+ log(`Invalid zoom value: ${zoom}`, 'warn');
999
+ } finally {
1000
+ // Always clear the command flag, even if there's an error
1001
+ ZoomCommandTracker.markZoomCommandEnd();
1002
+ }
1003
+ }
1004
+
1005
+ function updateCursor(cursor) {
1006
+ // Universal Dispatcher guarantees PDFViewerApplication availability at readiness level 4
1007
+ const app = PDFViewerApplication;
1008
+
1009
+ try {
1010
+ const cursorTool = cursor ? cursor.toUpperCase() : 'SELECT';
1011
+
1012
+ let toolId = 0; // Default to SELECT
1013
+ switch (cursorTool) {
1014
+ case 'HAND':
1015
+ case 'H':
1016
+ toolId = 1; // HAND
1017
+ break;
1018
+ case 'SELECT':
1019
+ case 'S':
1020
+ toolId = 0; // SELECT
1021
+ break;
1022
+ case 'ZOOM':
1023
+ case 'Z':
1024
+ toolId = 2; // ZOOM
1025
+ break;
1026
+ default:
1027
+ log(`Unknown cursor tool: ${cursorTool}, defaulting to SELECT`, 'warn');
1028
+ toolId = 0;
1029
+ }
1030
+
1031
+ // Update cursor using event bus dispatch (PDF.js v4.x)
1032
+ if (app.eventBus && typeof app.eventBus.dispatch === 'function') {
1033
+ app.eventBus.dispatch('switchcursortool', {
1034
+ tool: toolId
1035
+ });
1036
+ } else {
1037
+ log('EventBus not available for cursor update', 'warn');
1038
+ }
1039
+ } catch (error) {
1040
+ log(`Error updating cursor: ${error.message}`, 'error');
1041
+ }
1042
+ }
1043
+
1044
+ function updateScroll(scroll) {
1045
+ // Universal Dispatcher guarantees PDFViewerApplication availability at readiness level 4
1046
+
1047
+ try {
1048
+ const scrollMode = scroll ? scroll.toUpperCase() : 'VERTICAL';
1049
+ log(`Attempting to update scroll mode to: ${scrollMode}`);
1050
+
1051
+ if (PDFViewerApplication.eventBus) {
1052
+ // Use PDF.js v4.x event bus dispatch for scroll mode switching
1053
+ switch (scrollMode) {
1054
+ case 'VERTICAL':
1055
+ case 'V':
1056
+ log('Dispatching switchscrollmode with VERTICAL mode');
1057
+ PDFViewerApplication.eventBus.dispatch('switchscrollmode', {
1058
+ mode: 0 // ScrollMode.VERTICAL
1059
+ });
1060
+ break;
1061
+ case 'HORIZONTAL':
1062
+ case 'H':
1063
+ log('Dispatching switchscrollmode with HORIZONTAL mode');
1064
+ PDFViewerApplication.eventBus.dispatch('switchscrollmode', {
1065
+ mode: 1 // ScrollMode.HORIZONTAL
1066
+ });
1067
+ break;
1068
+ case 'WRAPPED':
1069
+ case 'W':
1070
+ log('Dispatching switchscrollmode with WRAPPED mode');
1071
+ PDFViewerApplication.eventBus.dispatch('switchscrollmode', {
1072
+ mode: 2 // ScrollMode.WRAPPED
1073
+ });
1074
+ break;
1075
+ case 'PAGE':
1076
+ case 'P':
1077
+ log('Dispatching switchscrollmode with PAGE mode');
1078
+ PDFViewerApplication.eventBus.dispatch('switchscrollmode', {
1079
+ mode: 3 // ScrollMode.PAGE
1080
+ });
1081
+ break;
1082
+ default:
1083
+ log(`Unknown scroll mode: ${scrollMode}, defaulting to VERTICAL`, 'warn');
1084
+ PDFViewerApplication.eventBus.dispatch('switchscrollmode', {
1085
+ mode: 0
1086
+ });
1087
+ }
1088
+ } else {
1089
+ // Fallback to direct property setting if event bus not available
1090
+ if (PDFViewerApplication.pdfViewer) {
1091
+ switch (scrollMode) {
1092
+ case 'VERTICAL':
1093
+ case 'V':
1094
+ PDFViewerApplication.pdfViewer.scrollMode = 0;
1095
+ break;
1096
+ case 'HORIZONTAL':
1097
+ case 'H':
1098
+ PDFViewerApplication.pdfViewer.scrollMode = 1;
1099
+ break;
1100
+ case 'WRAPPED':
1101
+ case 'W':
1102
+ PDFViewerApplication.pdfViewer.scrollMode = 2;
1103
+ break;
1104
+ case 'PAGE':
1105
+ case 'P':
1106
+ PDFViewerApplication.pdfViewer.scrollMode = 3;
1107
+ break;
1108
+ }
1109
+ }
1110
+ }
1111
+
1112
+ log(`Scroll mode update completed for: ${scrollMode}`);
1113
+ } catch (error) {
1114
+ log(`Error updating scroll mode: ${error.message}`, 'error');
1115
+ }
1116
+ }
1117
+
1118
+ function updateSpread(spread) {
1119
+ // Universal Dispatcher guarantees PDFViewerApplication availability at readiness level 4
1120
+
1121
+ try {
1122
+ const spreadMode = spread ? spread.toUpperCase() : 'NONE';
1123
+ log(`Attempting to update spread mode to: ${spreadMode}`);
1124
+
1125
+ if (PDFViewerApplication.eventBus) {
1126
+ // Use PDF.js v4.x event bus dispatch for spread mode switching
1127
+ switch (spreadMode) {
1128
+ case 'NONE':
1129
+ case 'N':
1130
+ log('Dispatching switchspreadmode with NONE mode');
1131
+ PDFViewerApplication.eventBus.dispatch('switchspreadmode', {
1132
+ mode: 0 // SpreadMode.NONE
1133
+ });
1134
+ break;
1135
+ case 'ODD':
1136
+ case 'O':
1137
+ log('Dispatching switchspreadmode with ODD mode');
1138
+ PDFViewerApplication.eventBus.dispatch('switchspreadmode', {
1139
+ mode: 1 // SpreadMode.ODD
1140
+ });
1141
+ break;
1142
+ case 'EVEN':
1143
+ case 'E':
1144
+ log('Dispatching switchspreadmode with EVEN mode');
1145
+ PDFViewerApplication.eventBus.dispatch('switchspreadmode', {
1146
+ mode: 2 // SpreadMode.EVEN
1147
+ });
1148
+ break;
1149
+ default:
1150
+ log(`Unknown spread mode: ${spreadMode}, defaulting to NONE`, 'warn');
1151
+ PDFViewerApplication.eventBus.dispatch('switchspreadmode', {
1152
+ mode: 0
1153
+ });
1154
+ }
1155
+ } else {
1156
+ // Fallback to direct property setting if event bus not available
1157
+ if (PDFViewerApplication.pdfViewer) {
1158
+ switch (spreadMode) {
1159
+ case 'NONE':
1160
+ case 'N':
1161
+ PDFViewerApplication.pdfViewer.spreadMode = 0;
1162
+ break;
1163
+ case 'ODD':
1164
+ case 'O':
1165
+ PDFViewerApplication.pdfViewer.spreadMode = 1;
1166
+ break;
1167
+ case 'EVEN':
1168
+ case 'E':
1169
+ PDFViewerApplication.pdfViewer.spreadMode = 2;
1170
+ break;
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ log(`Spread mode update completed for: ${spreadMode}`);
1176
+ } catch (error) {
1177
+ log(`Error updating spread mode: ${error.message}`, 'error');
1178
+ }
1179
+ }
1180
+ // #endregion
1181
+
1182
+ // Note: Legacy navigation and auto-action functions removed.
1183
+ // All functionality now handled through the main updateControl switch statement
1184
+ // which trusts the Universal Dispatcher's readiness guarantees.
1185
+
1186
+ // #region Error Handling Functions
1187
+ // Error state is managed internally and communicated via PostMessage
1188
+ // No need for global window variables - events drive the behavior
1189
+ function setErrorMessage(message) {
1190
+ // Error messages are sent directly via PostMessage when errors occur
1191
+ // No need to store in global variables
1192
+ }
1193
+
1194
+ function setErrorOverride(override) {
1195
+ // Error override is handled by the Angular component
1196
+ // No need to store in global variables
1197
+ }
1198
+
1199
+ function setErrorAppend(append) {
1200
+ // Error append is handled by the Angular component
1201
+ // No need to store in global variables
1202
+ }
1203
+ // #endregion
1204
+
1205
+ // #region CSS Zoom Functions
1206
+
1207
+ function setCssZoom(useCssZoom) {
1208
+ try {
1209
+ if (PDFViewerApplication && PDFViewerApplication.pdfViewer) {
1210
+ // Set CSS zoom mode
1211
+ PDFViewerApplication.pdfViewer.useOnlyCssZoom = useCssZoom === true;
1212
+ } else {
1213
+ // CSS zoom will be set when viewer becomes ready via readiness-based dispatch
1214
+ // No need to store in global variables
1215
+ }
1216
+ } catch (error) {
1217
+ log(`Error setting CSS zoom: ${error.message}`, 'error');
1218
+ }
1219
+ }
1220
+
1221
+ function setDownloadFilename(filename) {
1222
+ try {
1223
+ if (filename && PDFViewerApplication) {
1224
+ // Ensure filename ends with .pdf if not already present
1225
+ const processedFilename = filename.endsWith('.pdf') ? filename : `${filename}.pdf`;
1226
+
1227
+ // Set the content disposition filename that PDF.js uses for downloads
1228
+ PDFViewerApplication._contentDispositionFilename = processedFilename;
1229
+ } else {
1230
+ log('Cannot set download filename - invalid filename or PDFViewerApplication not available', 'warn');
1231
+ }
1232
+ } catch (error) {
1233
+ log(`Error setting download filename: ${error.message}`, 'error');
1234
+ }
1235
+ }
1236
+ // #endregion
1237
+
1238
+ // #region Event Configuration Functions
1239
+ // Event enablement state stored locally (not in global window variables)
1240
+ // This follows v5-upgrade.md principles while maintaining functionality
1241
+
1242
+ // Local state for event enablement
1243
+ const eventEnablement = {
1244
+ beforePrint: false,
1245
+ afterPrint: false,
1246
+ pagesLoaded: false,
1247
+ pageChange: false,
1248
+ documentError: false,
1249
+ documentInit: false,
1250
+ pagesInit: false,
1251
+ presentationModeChanged: false,
1252
+ openFile: false,
1253
+ find: false,
1254
+ updateFindMatchesCount: false,
1255
+ metadataLoaded: false,
1256
+ outlineLoaded: false,
1257
+ pageRendered: false,
1258
+ annotationLayerRendered: false,
1259
+ bookmarkClick: false,
1260
+ idle: false
1261
+ };
1262
+
1263
+ function enableBeforePrint(enable) { eventEnablement.beforePrint = enable === true; }
1264
+ function enableAfterPrint(enable) { eventEnablement.afterPrint = enable === true; }
1265
+ function enablePagesLoaded(enable) { eventEnablement.pagesLoaded = enable === true; }
1266
+ function enablePageChange(enable) { eventEnablement.pageChange = enable === true; }
1267
+ function enableDocumentError(enable) { eventEnablement.documentError = enable === true; }
1268
+ function enableDocumentInit(enable) { eventEnablement.documentInit = enable === true; }
1269
+ function enablePagesInit(enable) { eventEnablement.pagesInit = enable === true; }
1270
+ function enablePresentationModeChanged(enable) { eventEnablement.presentationModeChanged = enable === true; }
1271
+ function enableOpenFile(enable) { eventEnablement.openFile = enable === true; }
1272
+ function enableFind(enable) { eventEnablement.find = enable === true; }
1273
+ function enableUpdateFindMatchesCount(enable) { eventEnablement.updateFindMatchesCount = enable === true; }
1274
+ function enableMetadataLoaded(enable) { eventEnablement.metadataLoaded = enable === true; }
1275
+ function enableOutlineLoaded(enable) { eventEnablement.outlineLoaded = enable === true; }
1276
+ function enablePageRendered(enable) { eventEnablement.pageRendered = enable === true; }
1277
+ function enableAnnotationLayerRendered(enable) { eventEnablement.annotationLayerRendered = enable === true; }
1278
+ function enableBookmarkClick(enable) { eventEnablement.bookmarkClick = enable === true; }
1279
+ function enableIdle(enable) { eventEnablement.idle = enable === true; }
1280
+ // #endregion
1281
+
1282
+ // #region Theme & Visual Customization Functions
1283
+
1284
+ // Theme management variables
1285
+ let currentTheme = 'light';
1286
+
1287
+ function setTheme(theme) {
1288
+ currentTheme = theme || 'light';
1289
+
1290
+ // Apply theme-specific CSS classes (CSP-safe)
1291
+ const body = document.body;
1292
+ if (body) {
1293
+ // Remove ALL theme classes including active states
1294
+ body.classList.remove('ng2-theme-light', 'ng2-theme-dark', 'ng2-theme-auto',
1295
+ 'ng2-theme-dark-active', 'ng2-theme-light-active');
1296
+
1297
+ // Add new theme class
1298
+ body.classList.add(`ng2-theme-${currentTheme}`);
1299
+
1300
+ // Apply auto theme detection
1301
+ if (currentTheme === 'auto') {
1302
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
1303
+ const activeClass = prefersDark ? 'ng2-theme-dark-active' : 'ng2-theme-light-active';
1304
+ body.classList.add(activeClass);
1305
+
1306
+ // Listen for theme changes
1307
+ if (!window.ng2ThemeMediaListener) {
1308
+ const mediaListener = window.matchMedia('(prefers-color-scheme: dark)');
1309
+ mediaListener.addListener((e) => {
1310
+ if (currentTheme === 'auto') {
1311
+ body.classList.remove('ng2-theme-dark-active', 'ng2-theme-light-active');
1312
+ const newActiveClass = e.matches ? 'ng2-theme-dark-active' : 'ng2-theme-light-active';
1313
+ body.classList.add(newActiveClass);
1314
+ }
1315
+ });
1316
+ window.ng2ThemeMediaListener = mediaListener;
1317
+ }
1318
+ }
1319
+
1320
+ // Force style recalculation
1321
+ body.offsetHeight; // Trigger reflow
1322
+ }
1323
+
1324
+ // Note: Theme styles now loaded from external ng2-customization.css (CSP-safe)
1325
+ }
1326
+
1327
+ function setPrimaryColor(color) {
1328
+ setCSSVariable('--ng2-primary-color', color);
1329
+ }
1330
+
1331
+ function setBackgroundColor(color) {
1332
+ setCSSVariable('--ng2-background-color', color);
1333
+ }
1334
+
1335
+ function setPageSpacing(margin, spreadMargin, border) {
1336
+ if (margin !== undefined) {
1337
+ setCSSVariable('--page-margin', margin);
1338
+ }
1339
+ if (spreadMargin !== undefined) {
1340
+ setCSSVariable('--spreadHorizontalWrapped-margin-LR', spreadMargin);
1341
+ }
1342
+ if (border !== undefined) {
1343
+ setCSSVariable('--page-border', border);
1344
+ }
1345
+ }
1346
+
1347
+ function setPageBorderColor(color) {
1348
+ setCSSVariable('--ng2-page-border-color', color);
1349
+ }
1350
+
1351
+ function setToolbarColor(color) {
1352
+ setCSSVariable('--ng2-toolbar-color', color);
1353
+ }
1354
+
1355
+ function setTextColor(color) {
1356
+ setCSSVariable('--ng2-text-color', color);
1357
+ }
1358
+
1359
+ function setBorderRadius(radius) {
1360
+ setCSSVariable('--ng2-border-radius', radius);
1361
+ }
1362
+
1363
+ function setCustomCSS(css, nonce) {
1364
+ // Remove existing custom CSS
1365
+ const existingStyle = document.getElementById('ng2-custom-css');
1366
+ if (existingStyle) {
1367
+ existingStyle.remove();
1368
+ }
1369
+
1370
+ // Apply new custom CSS with optional nonce for CSP support
1371
+ if (css) {
1372
+ const style = document.createElement('style');
1373
+ style.id = 'ng2-custom-css';
1374
+ style.textContent = css;
1375
+
1376
+ // Add nonce if provided (CSP support)
1377
+ if (nonce) {
1378
+ style.setAttribute('nonce', nonce);
1379
+ }
1380
+
1381
+ document.head.appendChild(style);
1382
+ }
1383
+ }
1384
+
1385
+ // Helper function to set CSS custom properties
1386
+ function setCSSVariable(property, value) {
1387
+ if (value) {
1388
+ document.documentElement.style.setProperty(property, value);
1389
+ // Also set on body for better compatibility
1390
+ document.body.style.setProperty(property, value);
1391
+ } else {
1392
+ document.documentElement.style.removeProperty(property);
1393
+ document.body.style.removeProperty(property);
1394
+ }
1395
+ }
1396
+
1397
+ // Note: Theme styles now loaded from external ng2-customization.css (CSP-safe)
1398
+
1399
+ // #endregion
1400
+
1401
+ // #region Error Event Listeners (Early Setup)
1402
+ let errorListenersSetup = false; // Flag to prevent multiple setups
1403
+
1404
+ function setupErrorEventListeners() {
1405
+ // Prevent multiple setups
1406
+ if (errorListenersSetup) {
1407
+ return;
1408
+ }
1409
+
1410
+ const app = PDFViewerApplication;
1411
+ if (!app || !app.eventBus) {
1412
+ log('Cannot setup error listeners: EventBus not available', 'warn');
1413
+ return;
1414
+ }
1415
+
1416
+ // Listen for document errors (including file origin errors)
1417
+ app.eventBus.on('documenterror', (event) => {
1418
+ log(`🔴 DOCUMENT ERROR EVENT FIRED: ${event.message}`, 'error');
1419
+ // Always hide loading spinner on document error
1420
+ sendStateChangeNotification('loading', false, 'system');
1421
+
1422
+ // Send error state notification for custom error display
1423
+ const errorMessage = event.message || 'An error occurred while loading the PDF.';
1424
+ sendStateChangeNotification('error', errorMessage, 'system');
1425
+
1426
+ // Send document error events only if enabled
1427
+ if (eventEnablement.documentError) {
1428
+ log(`Document error event received: ${event.message}`);
1429
+ const errorData = {
1430
+ message: event.message || 'Unknown document error',
1431
+ source: event.source ? 'PDFViewerApplication' : 'unknown',
1432
+ name: event.name || 'DocumentError'
1433
+ };
1434
+ sendEventNotification('documentError', errorData);
1435
+ }
1436
+ });
1437
+
1438
+ // Listen for other potential error events
1439
+ app.eventBus.on('loaderror', (event) => {
1440
+ log(`🔴 LOAD ERROR EVENT FIRED: ${event.message}`, 'error');
1441
+ // Hide loading spinner on load error
1442
+ sendStateChangeNotification('loading', false, 'system');
1443
+ });
1444
+
1445
+ app.eventBus.on('error', (event) => {
1446
+ log(`🔴 GENERIC ERROR EVENT FIRED: ${event.message}`, 'error');
1447
+ // Hide loading spinner on any error
1448
+ sendStateChangeNotification('loading', false, 'system');
1449
+ });
1450
+
1451
+ errorListenersSetup = true;
1452
+ log('Error event listeners set up successfully', 'info');
1453
+ }
1454
+
1455
+ // #region Bidirectional Event Listeners
1456
+ let bidirectionalListenersSetup = false; // Flag to prevent multiple setups
1457
+
1458
+ function setupBidirectionalEventListeners() {
1459
+ // Prevent multiple setups
1460
+ if (bidirectionalListenersSetup) {
1461
+ return;
1462
+ }
1463
+
1464
+ // Add global error handler for iframe-level errors
1465
+ window.addEventListener('error', (event) => {
1466
+ log(`🔴 GLOBAL ERROR: ${event.message}`, 'error');
1467
+ // Hide loading spinner on any global error
1468
+ sendStateChangeNotification('loading', false, 'system');
1469
+ });
1470
+
1471
+ window.addEventListener('unhandledrejection', (event) => {
1472
+ log(`🔴 UNHANDLED PROMISE REJECTION: ${event.reason}`, 'error');
1473
+ // Hide loading spinner on unhandled promise rejection
1474
+ sendStateChangeNotification('loading', false, 'system');
1475
+ });
1476
+
1477
+ bidirectionalListenersSetup = true;
1478
+ const app = PDFViewerApplication;
1479
+ if (!app || !app.eventBus) {
1480
+ log('Cannot setup bidirectional listeners: EventBus not available', 'warn');
1481
+ return;
1482
+ }
1483
+
1484
+
1485
+ // Test sending a state change notification immediately
1486
+ sendStateChangeNotification('test', 'initial-setup', 'system');
1487
+
1488
+ // Cursor tool changes are handled via switchcursortool event listener only
1489
+
1490
+ // Note: Scroll and spread mode changes are handled via event listeners
1491
+ // No method interception needed for PDF.js v5.3.93
1492
+ if (app.pdfViewer) {
1493
+
1494
+ // Monitor scroll mode changes - setScrollMode doesn't exist in v5.3.93
1495
+ // The scroll mode changes are handled via event listeners instead
1496
+
1497
+ // Spread mode changes are handled via switchspreadmode event listener only
1498
+
1499
+ // Monitor zoom changes via currentScale property
1500
+ let lastKnownScale = app.pdfViewer.currentScale;
1501
+ const originalSetCurrentScale = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(app.pdfViewer), 'currentScale');
1502
+ if (originalSetCurrentScale && originalSetCurrentScale.set) {
1503
+ Object.defineProperty(app.pdfViewer, 'currentScale', {
1504
+ get: originalSetCurrentScale.get,
1505
+ set: function(scale) {
1506
+ const result = originalSetCurrentScale.set.call(this, scale);
1507
+
1508
+ if (scale !== lastKnownScale) {
1509
+ lastKnownScale = scale;
1510
+
1511
+ // Use enhanced zoom transformation
1512
+ const zoomValue = transformZoomFromViewer(scale, app);
1513
+
1514
+ log(`Zoom changed via currentScale to: ${zoomValue} (scale: ${scale}, currentScaleValue: ${app.pdfViewer.currentScaleValue})`);
1515
+ sendStateChangeNotification('zoom', zoomValue, 'user');
1516
+ }
1517
+
1518
+ return result;
1519
+ },
1520
+ configurable: true,
1521
+ enumerable: true
1522
+ });
1523
+ } else {
1524
+ log('pdfViewer.currentScale setter not found', 'warn');
1525
+ }
1526
+ } else {
1527
+ log('pdfViewer not available', 'warn');
1528
+ }
1529
+
1530
+ // Listen for sidebar/page mode changes
1531
+ if (app.pdfSidebar) {
1532
+
1533
+ // Monitor sidebar view changes
1534
+ const originalSetView = app.pdfSidebar.setView;
1535
+ if (originalSetView) {
1536
+ app.pdfSidebar.setView = function(view) {
1537
+ const result = originalSetView.apply(this, arguments);
1538
+
1539
+ // Map PDF.js sidebar view numbers to our page mode names
1540
+ const pageModeMap = {
1541
+ 0: 'none', // SidebarView.NONE
1542
+ 1: 'thumbs', // SidebarView.THUMBS
1543
+ 2: 'bookmarks', // SidebarView.OUTLINE
1544
+ 3: 'attachments', // SidebarView.ATTACHMENTS
1545
+ 4: 'layers' // SidebarView.LAYERS
1546
+ };
1547
+
1548
+ const pageModeName = pageModeMap[view] || 'none';
1549
+ log(`Page mode changed to: ${pageModeName} (view: ${view})`);
1550
+ sendStateChangeNotification('pageMode', pageModeName, 'user');
1551
+
1552
+ return result;
1553
+ };
1554
+ } else {
1555
+ }
1556
+ } else {
1557
+ log('pdfSidebar not available', 'warn');
1558
+ }
1559
+
1560
+ // Listen for additional PDF.js events that might indicate property changes
1561
+ if (app.eventBus) {
1562
+
1563
+ // Listen for scale changing events with intelligent user vs programmatic detection
1564
+ app.eventBus.on('scalechanging', (event) => {
1565
+
1566
+
1567
+ // Skip notification if this is from our own zoom command
1568
+ if (ZoomCommandTracker.isZoomCommandActive()) {
1569
+ log('Skipping zoom notification - programmatic change from our command');
1570
+ return;
1571
+ }
1572
+
1573
+ // Use event context to determine if this is a user-initiated change
1574
+ let isUserInitiated = false;
1575
+ let zoomValue;
1576
+
1577
+ if (event.presetValue && typeof event.presetValue === 'string') {
1578
+ // User clicked zoom buttons (Page Fit, Page Width, Auto, etc.)
1579
+
1580
+ isUserInitiated = true;
1581
+ zoomValue = event.presetValue;
1582
+ } else if (typeof event.scale === 'number' && !event.presetValue) {
1583
+ // Could be mouse wheel (user) or other programmatic change
1584
+ // For now, assume it's user-initiated (mouse wheel, etc.)
1585
+ // Future enhancement: could add more sophisticated detection
1586
+ log('Assuming user-initiated scale change (mouse wheel, etc.)');
1587
+ isUserInitiated = true;
1588
+ zoomValue = transformZoomFromViewer(event.scale, app);
1589
+ }
1590
+
1591
+ if (isUserInitiated) {
1592
+
1593
+ sendStateChangeNotification('zoom', zoomValue, 'user');
1594
+ } else {
1595
+ log('Skipping zoom notification - not identified as user-initiated');
1596
+ }
1597
+ });
1598
+
1599
+ // Listen for other relevant events
1600
+ app.eventBus.on('switchscrollmode', (event) => {
1601
+
1602
+ const scrollMap = {
1603
+ 0: 'vertical', // ScrollMode.VERTICAL
1604
+ 1: 'horizontal', // ScrollMode.HORIZONTAL
1605
+ 2: 'wrapped', // ScrollMode.WRAPPED
1606
+ 3: 'page' // ScrollMode.PAGE
1607
+ };
1608
+
1609
+ const scrollName = scrollMap[event.mode] || 'vertical';
1610
+ log(`Scroll mode changed via event to: ${scrollName}`);
1611
+ sendStateChangeNotification('scroll', scrollName, 'user');
1612
+ });
1613
+
1614
+ app.eventBus.on('switchspreadmode', (event) => {
1615
+
1616
+ const spreadMap = {
1617
+ 0: 'none', // SpreadMode.NONE
1618
+ 1: 'odd', // SpreadMode.ODD
1619
+ 2: 'even' // SpreadMode.EVEN
1620
+ };
1621
+
1622
+ const spreadName = spreadMap[event.mode] || 'none';
1623
+ log(`Spread mode changed via event to: ${spreadName}`);
1624
+ sendStateChangeNotification('spread', spreadName, 'user');
1625
+ });
1626
+
1627
+ app.eventBus.on('switchcursortool', (event) => {
1628
+
1629
+ const cursorMap = {
1630
+ 0: 'select', // CursorTool.SELECT
1631
+ 1: 'hand', // CursorTool.HAND
1632
+ 2: 'zoom' // CursorTool.ZOOM
1633
+ };
1634
+
1635
+ const cursorName = cursorMap[event.tool] || 'select';
1636
+ log(`Cursor changed via event to: ${cursorName}`);
1637
+ sendStateChangeNotification('cursor', cursorName, 'user');
1638
+ });
1639
+
1640
+ app.eventBus.on('sidebarviewchanged', (event) => {
1641
+
1642
+ const pageModeMap = {
1643
+ 0: 'none', // SidebarView.NONE
1644
+ 1: 'thumbs', // SidebarView.THUMBS
1645
+ 2: 'bookmarks', // SidebarView.OUTLINE
1646
+ 3: 'attachments', // SidebarView.ATTACHMENTS
1647
+ 4: 'layers' // SidebarView.LAYERS
1648
+ };
1649
+
1650
+ const pageModeName = pageModeMap[event.view] || 'none';
1651
+ log(`Page mode changed via event to: ${pageModeName}`);
1652
+ sendStateChangeNotification('pageMode', pageModeName, 'user');
1653
+ });
1654
+
1655
+ // Listen for rotation changes (when user rotates via UI or programmatically)
1656
+ app.eventBus.on('rotationchanging', (event) => {
1657
+ log(`Rotation changed via event to: ${event.pagesRotation}`);
1658
+ sendStateChangeNotification('rotation', event.pagesRotation, 'user');
1659
+ });
1660
+
1661
+ // Note: Error event listeners are set up earlier in setupErrorEventListeners()
1662
+
1663
+ app.eventBus.on('documentinit', (event) => {
1664
+ // Send document init events only if enabled
1665
+ if (eventEnablement.documentInit) {
1666
+ sendEventNotification('documentInit', null);
1667
+ }
1668
+ });
1669
+
1670
+ app.eventBus.on('pagesinit', (event) => {
1671
+ // Send pages init events only if enabled
1672
+ if (eventEnablement.pagesInit) {
1673
+ const pageCount = app.pagesCount || 0;
1674
+ const pagesData = {
1675
+ pagesCount: pageCount
1676
+ };
1677
+ sendEventNotification('pagesInit', pagesData);
1678
+ }
1679
+ });
1680
+
1681
+ app.eventBus.on('presentationmodechanged', (event) => {
1682
+ // Send presentation mode events only if enabled
1683
+ if (eventEnablement.presentationModeChanged) {
1684
+ // Map PDF.js PresentationModeState to boolean
1685
+ // FULLSCREEN = 3, CHANGING = 2, NORMAL = 1, UNKNOWN = 0
1686
+ const isActive = event.state === 3; // PresentationModeState.FULLSCREEN
1687
+ const isChanging = event.state === 2; // PresentationModeState.CHANGING
1688
+ const presentationData = {
1689
+ active: isActive,
1690
+ switchInProgress: isChanging
1691
+ };
1692
+ sendEventNotification('presentationModeChanged', presentationData);
1693
+ }
1694
+ });
1695
+
1696
+ app.eventBus.on('fileinputchange', (event) => {
1697
+ // Send open file events only if enabled
1698
+ if (eventEnablement.openFile) {
1699
+ sendEventNotification('openFile', null);
1700
+ }
1701
+ });
1702
+
1703
+ app.eventBus.on('find', (event) => {
1704
+ // Send find events only if enabled
1705
+ if (eventEnablement.find) {
1706
+ const findData = {
1707
+ query: event.query || '',
1708
+ phraseSearch: false, // Not available in PDF.js v5.3.93
1709
+ caseSensitive: event.caseSensitive || false,
1710
+ entireWord: event.entireWord || false,
1711
+ highlightAll: event.highlightAll || false,
1712
+ findPrevious: event.findPrevious || false
1713
+ };
1714
+ sendEventNotification('find', findData);
1715
+ }
1716
+ });
1717
+
1718
+ app.eventBus.on('updatefindmatchescount', (event) => {
1719
+ // Send find matches count events only if enabled
1720
+ if (eventEnablement.updateFindMatchesCount) {
1721
+ const countData = {
1722
+ current: event.matchesCount?.current || 0,
1723
+ total: event.matchesCount?.total || 0
1724
+ };
1725
+ sendEventNotification('updateFindMatchesCount', countData);
1726
+ }
1727
+ });
1728
+
1729
+ app.eventBus.on('metadataloaded', (event) => {
1730
+ // Send metadata loaded events only if enabled
1731
+ if (eventEnablement.metadataLoaded) {
1732
+ const info = app.documentInfo;
1733
+ const metadataData = {
1734
+ title: info?.Title,
1735
+ author: info?.Author,
1736
+ subject: info?.Subject,
1737
+ keywords: info?.Keywords,
1738
+ creator: info?.Creator,
1739
+ producer: info?.Producer,
1740
+ creationDate: info?.CreationDate,
1741
+ modificationDate: info?.ModDate,
1742
+ pdfFormatVersion: info?.PDFFormatVersion,
1743
+ isLinearized: info?.IsLinearized,
1744
+ isAcroFormPresent: info?.IsAcroFormPresent,
1745
+ isXFAPresent: info?.IsXFAPresent,
1746
+ isCollectionPresent: info?.IsCollectionPresent
1747
+ };
1748
+ sendEventNotification('metadataLoaded', metadataData);
1749
+ }
1750
+ });
1751
+
1752
+ app.eventBus.on('outlineloaded', (event) => {
1753
+ // Send outline loaded events only if enabled
1754
+ if (eventEnablement.outlineLoaded) {
1755
+ const outlineData = {
1756
+ items: [], // Outline items are not available in the event, but consumers can check hasOutline
1757
+ hasOutline: (event.outlineCount || 0) > 0
1758
+ };
1759
+ sendEventNotification('outlineLoaded', outlineData);
1760
+ }
1761
+ });
1762
+
1763
+ app.eventBus.on('pagerendered', (event) => {
1764
+ // Send page rendered events only if enabled
1765
+ if (eventEnablement.pageRendered) {
1766
+ const renderData = {
1767
+ pageNumber: event.pageNumber || 1,
1768
+ // Don't include source as it contains canvas element that can't be cloned
1769
+ timestamp: Date.now()
1770
+ };
1771
+ sendEventNotification('pageRendered', renderData);
1772
+ }
1773
+ });
1774
+
1775
+ // Loading state integration for overlay control
1776
+ // Event-driven only; idempotent notifications, no flags or polling
1777
+ app.eventBus.on('documentinit', () => sendStateChangeNotification('loading', true, 'system'));
1778
+ app.eventBus.on('pagesinit', () => sendStateChangeNotification('loading', true, 'system'));
1779
+ app.eventBus.on('pagerendered', () => sendStateChangeNotification('loading', false, 'system'));
1780
+ app.eventBus.on('pagesloaded', () => sendStateChangeNotification('loading', false, 'system'));
1781
+
1782
+ // New high-value events
1783
+
1784
+ // Annotation Layer Rendered - Native PDF.js event
1785
+ app.eventBus.on('annotationlayerrendered', (event) => {
1786
+ // Send annotation layer rendered events only if enabled
1787
+ if (eventEnablement.annotationLayerRendered) {
1788
+ const renderData = {
1789
+ pageNumber: event.pageNumber || 1,
1790
+ error: event.error || null,
1791
+ timestamp: performance.now()
1792
+ };
1793
+ sendEventNotification('annotationLayerRendered', renderData);
1794
+ }
1795
+ });
1796
+
1797
+ // Reset idle timer when PDF document changes (event-driven approach)
1798
+ // Set up idle timer only if enabled
1799
+ if (eventEnablement.idle) {
1800
+ app.eventBus.on('documentloaded', () => {
1801
+ if (typeof resetIdleTimer === 'function') {
1802
+ resetIdleTimer();
1803
+ log('Idle timer reset on document load');
1804
+ }
1805
+ });
1806
+ }
1807
+ }
1808
+
1809
+ // Initialize custom event implementations
1810
+ initializeCustomEvents(app);
1811
+
1812
+
1813
+ }
1814
+
1815
+ // Initialize custom event implementations
1816
+ function initializeCustomEvents(app) {
1817
+ setupIdleDetection();
1818
+ interceptOutlineClicks(app);
1819
+ }
1820
+
1821
+ // Idle Detection - Custom activity tracking
1822
+ let idleTimer = null;
1823
+ let idleListenersSetup = false;
1824
+ let idleCleanupListeners = []; // Track listeners for cleanup
1825
+ const IDLE_TIMEOUT = 30000; // 30 seconds default
1826
+
1827
+ function resetIdleTimer() {
1828
+ if (idleTimer) {
1829
+ clearTimeout(idleTimer);
1830
+ idleTimer = null;
1831
+ }
1832
+ // Set up idle timer only if enabled
1833
+ if (eventEnablement.idle) {
1834
+ idleTimer = setTimeout(() => {
1835
+ sendEventNotification('idle', null);
1836
+ }, IDLE_TIMEOUT);
1837
+ }
1838
+ }
1839
+
1840
+ function cleanupIdleDetection() {
1841
+ // Clear timer
1842
+ if (idleTimer) {
1843
+ clearTimeout(idleTimer);
1844
+ idleTimer = null;
1845
+ }
1846
+
1847
+ // Remove all tracked event listeners
1848
+ idleCleanupListeners.forEach(({ element, event, handler, options }) => {
1849
+ element.removeEventListener(event, handler, options);
1850
+ });
1851
+ idleCleanupListeners = [];
1852
+ idleListenersSetup = false;
1853
+
1854
+ log('Idle detection cleanup completed');
1855
+ }
1856
+
1857
+ function setupIdleDetection() {
1858
+ // Prevent multiple setups
1859
+ if (idleListenersSetup) {
1860
+ log('Idle detection already setup, skipping');
1861
+ return;
1862
+ }
1863
+
1864
+ idleListenersSetup = true;
1865
+
1866
+ // Helper to add and track event listeners
1867
+ const addTrackedListener = (element, event, handler, options) => {
1868
+ element.addEventListener(event, handler, options);
1869
+ idleCleanupListeners.push({ element, event, handler, options });
1870
+ };
1871
+
1872
+ // Track mouse activity
1873
+ addTrackedListener(document, 'mousemove', resetIdleTimer, { passive: true });
1874
+ addTrackedListener(document, 'mousedown', resetIdleTimer, { passive: true });
1875
+ addTrackedListener(document, 'click', resetIdleTimer, { passive: true });
1876
+
1877
+ // Track keyboard activity
1878
+ addTrackedListener(document, 'keydown', resetIdleTimer, { passive: true });
1879
+ addTrackedListener(document, 'keypress', resetIdleTimer, { passive: true });
1880
+
1881
+ // Track scroll activity
1882
+ addTrackedListener(document, 'scroll', resetIdleTimer, { passive: true });
1883
+ addTrackedListener(document, 'wheel', resetIdleTimer, { passive: true });
1884
+
1885
+ // Track touch activity
1886
+ addTrackedListener(document, 'touchstart', resetIdleTimer, { passive: true });
1887
+ addTrackedListener(document, 'touchmove', resetIdleTimer, { passive: true });
1888
+
1889
+ // Start initial timer
1890
+ resetIdleTimer();
1891
+
1892
+ // Cleanup on document unload
1893
+ const unloadHandler = () => {
1894
+ cleanupIdleDetection();
1895
+ };
1896
+ addTrackedListener(window, 'beforeunload', unloadHandler, { passive: true });
1897
+
1898
+
1899
+ }
1900
+
1901
+ // Bookmark Click - Intercept outline item clicks
1902
+ let bookmarkInterceptionSetup = false;
1903
+
1904
+ function interceptOutlineClicks(app) {
1905
+ // Prevent multiple setups
1906
+ if (bookmarkInterceptionSetup) {
1907
+ log('Bookmark click interception already setup, skipping');
1908
+ return;
1909
+ }
1910
+
1911
+ // Check if PDF outline viewer is available immediately
1912
+ if (app && app.pdfOutlineViewer && app.pdfOutlineViewer._bindLink) {
1913
+ const originalBindLink = app.pdfOutlineViewer._bindLink;
1914
+
1915
+ app.pdfOutlineViewer._bindLink = function(element, params) {
1916
+ // Call original bind method first
1917
+ originalBindLink.call(this, element, params);
1918
+
1919
+ // Intercept bookmark clicks only if enabled
1920
+ if (eventEnablement.bookmarkClick) {
1921
+ const originalOnClick = element.onclick;
1922
+ element.onclick = function(evt) {
1923
+ // Extract bookmark data
1924
+ const bookmarkData = {
1925
+ title: element.textContent?.trim() || 'Unknown',
1926
+ dest: params.dest || null,
1927
+ action: params.action || undefined,
1928
+ url: params.url || undefined,
1929
+ pageNumber: undefined, // Will be resolved by linkService
1930
+ isCurrentItem: element.classList.contains('currentTreeItem')
1931
+ };
1932
+
1933
+ log(`Bookmark clicked: ${bookmarkData.title}`);
1934
+ sendEventNotification('bookmarkClick', bookmarkData);
1935
+
1936
+ // Call original handler to preserve navigation functionality
1937
+ if (originalOnClick) {
1938
+ return originalOnClick.call(this, evt);
1939
+ }
1940
+ return false;
1941
+ };
1942
+ }
1943
+ };
1944
+
1945
+ bookmarkInterceptionSetup = true;
1946
+
1947
+ } else {
1948
+ // If outline viewer is not available yet, defer setup using event-driven approach
1949
+ // Hook into outlineloaded event to retry when outline becomes available
1950
+ if (app && app.eventBus) {
1951
+ const outlineLoadedHandler = () => {
1952
+ if (!bookmarkInterceptionSetup) {
1953
+ interceptOutlineClicks(app); // Retry when outline is loaded
1954
+ }
1955
+ };
1956
+ app.eventBus.on('outlineloaded', outlineLoadedHandler);
1957
+ log('Bookmark click interception deferred until outline loads');
1958
+ } else {
1959
+ log('Unable to setup bookmark click interception - event bus not available', 'warn');
1960
+ }
1961
+ }
1962
+ }
1963
+ // #endregion
1964
+
1965
+ // Start the initialization process
1966
+ waitForViewer();
1967
1967
  })();