Files
EnvelopeGenerator/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js
TekH c26ad9e1c2 Improve zoom control granularity and behavior
Updated the zoom slider in `EnvelopeViewer.razor` to allow finer adjustments by changing the step size from 25 to 1. Modified `pdf-viewer.js` to enable smoother zooming with 1% increments for `zoomIn` and `zoomOut` methods. Capped zoom levels between 0.5 and 3.0. Enhanced mouse wheel zoom behavior to adjust zoom in 1% steps and notify the .NET side of changes via `OnZoomChanged`. Ensured pages are re-rendered after each zoom adjustment.
2026-06-05 13:39:20 +02:00

285 lines
9.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 (1% ad?m)
this.scale = Math.min(this.scale + 0.01, 3.0);
this.queueRenderPage(this.pageNum);
if (this.dotNetReference) {
this.dotNetReference.invokeMethodAsync('OnZoomChanged', this.scale);
}
} else {
// Scroll down = Zoom Out (1% ad?m)
this.scale = Math.max(this.scale - 0.01, 0.5);
this.queueRenderPage(this.pageNum);
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() {
// 1% art??
this.scale = Math.min(this.scale + 0.01, 3.0);
this.queueRenderPage(this.pageNum);
},
zoomOut() {
// 1% azal??
this.scale = Math.max(this.scale - 0.01, 0.5);
this.queueRenderPage(this.pageNum);
},
setScale(scale) {
if (scale >= 0.5 && scale <= 3.0) {
this.scale = scale;
this.queueRenderPage(this.pageNum);
}
},
async fitToWidth() {
const container = this.canvas.closest('.pdf-frame');
if (!container || !this.pdfDoc) return;
try {
const page = await this.pdfDoc.getPage(this.pageNum);
const viewport = page.getViewport({ scale: 1.0 });
const containerWidth = container.clientWidth - 80;
const optimalScale = containerWidth / viewport.width;
this.scale = Math.min(Math.max(optimalScale, 0.5), 3.0);
this.queueRenderPage(this.pageNum);
} catch (error) {
console.error('Error fitting to width:', error);
}
},
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');
}
}
};