Files
EnvelopeGenerator/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js
TekH 34b620e749 Preserve scroll position during PDF page rendering
Added functionality to maintain the scroll position and viewport
center of the `.pdf-frame` container during PDF page rendering.
This ensures the user's view remains centered on the same content
after re-rendering, even if the canvas dimensions change.

Implemented logic to store the scroll position and viewport center
before rendering and restore them afterward using scaling factors
calculated from the canvas's old and new dimensions.
2026-06-05 12:55:23 +02:00

258 lines
8.0 KiB
JavaScript

// PDF.js Viewer for Blazor WASM
window.pdfViewer = {
pdfDoc: null,
pageNum: 1,
pageRendering: false,
pageNumPending: null,
scale: 1.5,
canvas: null,
ctx: null,
totalPages: 0,
currentRenderTask: null,
dotNetReference: null,
wheelEventAttached: false,
async initialize(canvasId, pdfDataUrl, dotNetRef) {
try {
console.log('PDF.js initialization started for canvas:', canvasId);
// Store .NET reference for callbacks
this.dotNetReference = dotNetRef;
// Wait for PDF.js to load
if (typeof window.pdfjsLib === 'undefined') {
console.error('PDF.js library not loaded, waiting...');
await this.waitForPdfJs();
}
const pdfjsLib = window.pdfjsLib;
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
this.canvas = document.getElementById(canvasId);
if (!this.canvas) {
console.error('Canvas element not found:', canvasId);
return false;
}
console.log('Canvas element found, loading PDF...');
this.ctx = this.canvas.getContext('2d');
// Attach mouse wheel event listener
this.attachWheelEvent();
// Load PDF from data URL
const uint8Array = this.base64ToUint8Array(pdfDataUrl);
console.log('PDF data converted to Uint8Array, size:', uint8Array.length);
const loadingTask = pdfjsLib.getDocument({ data: uint8Array });
this.pdfDoc = await loadingTask.promise;
this.totalPages = this.pdfDoc.numPages;
console.log('PDF loaded successfully, total pages:', this.totalPages);
// Render first page
await this.renderPage(this.pageNum);
return true;
} catch (error) {
console.error('Error initializing PDF viewer:', error);
return false;
}
},
attachWheelEvent() {
if (this.wheelEventAttached) return;
// Attach to the entire document body for global zoom control
document.body.addEventListener('wheel', (e) => {
// Check if Ctrl key is pressed (zoom gesture)
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
if (e.deltaY < 0) {
// Scroll up = Zoom In
this.zoomIn();
if (this.dotNetReference) {
this.dotNetReference.invokeMethodAsync('OnZoomChanged', this.scale);
}
} else {
// Scroll down = Zoom Out
this.zoomOut();
if (this.dotNetReference) {
this.dotNetReference.invokeMethodAsync('OnZoomChanged', this.scale);
}
}
}
}, { passive: false });
this.wheelEventAttached = true;
console.log('Wheel event listener attached to document body');
},
async waitForPdfJs() {
for (let i = 0; i < 50; i++) {
if (typeof window.pdfjsLib !== 'undefined') {
console.log('PDF.js loaded after', i * 100, 'ms');
return;
}
await new Promise(resolve => setTimeout(resolve, 100));
}
throw new Error('PDF.js failed to load after 5 seconds');
},
base64ToUint8Array(base64) {
// Remove data URL prefix if present
const base64String = base64.includes(',') ? base64.split(',')[1] : base64;
const raw = atob(base64String);
const uint8Array = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) {
uint8Array[i] = raw.charCodeAt(i);
}
return uint8Array;
},
async renderPage(num) {
this.pageRendering = true;
try {
// Get scroll container
const container = this.canvas.closest('.pdf-frame');
// Store scroll position and viewport center BEFORE rendering
let scrollLeft = 0, scrollTop = 0;
let centerX = 0, centerY = 0;
let oldWidth = this.canvas.width;
let oldHeight = this.canvas.height;
if (container) {
scrollLeft = container.scrollLeft;
scrollTop = container.scrollTop;
centerX = scrollLeft + container.clientWidth / 2;
centerY = scrollTop + container.clientHeight / 2;
}
const page = await this.pdfDoc.getPage(num);
const viewport = page.getViewport({ scale: this.scale });
console.log('Rendering page:', num, 'Viewport:', viewport.width, 'x', viewport.height);
this.canvas.height = viewport.height;
this.canvas.width = viewport.width;
const renderContext = {
canvasContext: this.ctx,
viewport: viewport
};
if (this.currentRenderTask) {
console.log('Cancelling previous render task');
this.currentRenderTask.cancel();
}
this.currentRenderTask = page.render(renderContext);
await this.currentRenderTask.promise;
console.log('Page rendered successfully');
// Restore viewport center position AFTER rendering
if (container && oldWidth > 0 && oldHeight > 0) {
const scaleX = this.canvas.width / oldWidth;
const scaleY = this.canvas.height / oldHeight;
const newCenterX = centerX * scaleX;
const newCenterY = centerY * scaleY;
container.scrollLeft = newCenterX - container.clientWidth / 2;
container.scrollTop = newCenterY - container.clientHeight / 2;
}
this.currentRenderTask = null;
this.pageRendering = false;
if (this.pageNumPending !== null) {
this.renderPage(this.pageNumPending);
this.pageNumPending = null;
}
} catch (error) {
if (error.name === 'RenderingCancelledException') {
console.log('Rendering cancelled, will render pending page');
} else {
console.error('Error rendering page:', error);
}
this.currentRenderTask = null;
this.pageRendering = false;
}
},
queueRenderPage(num) {
if (this.pageRendering) {
this.pageNumPending = num;
} else {
this.renderPage(num);
}
},
nextPage() {
if (this.pageNum >= this.totalPages) {
return false;
}
this.pageNum++;
this.queueRenderPage(this.pageNum);
return true;
},
previousPage() {
if (this.pageNum <= 1) {
return false;
}
this.pageNum--;
this.queueRenderPage(this.pageNum);
return true;
},
goToPage(num) {
if (num < 1 || num > this.totalPages) {
return false;
}
this.pageNum = num;
this.queueRenderPage(this.pageNum);
return true;
},
zoomIn() {
this.scale += 0.25;
this.queueRenderPage(this.pageNum);
},
zoomOut() {
if (this.scale > 0.5) {
this.scale -= 0.25;
this.queueRenderPage(this.pageNum);
}
},
getCurrentPage() {
return this.pageNum;
},
getTotalPages() {
return this.totalPages;
},
getScale() {
return this.scale;
},
dispose() {
// Clean up event listeners
if (this.wheelEventAttached) {
// Note: We can't remove the exact listener without keeping a reference
// but we can at least mark it as disposed
this.wheelEventAttached = false;
this.dotNetReference = null;
console.log('PDF viewer disposed');
}
}
};