Files
EnvelopeGenerator/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js
TekH 9535c7dd6b Improve signature scaling and responsiveness in PDF viewer
Reduced delay in `OnZoomChanged` to improve responsiveness when rendering signature buttons. Added calls to `RenderSignatureButtonsAsync` in zoom-related methods to ensure signature overlays update dynamically.

Refactored `pdf-viewer.js` to introduce `appliedSignatureElements` for better management of applied signatures. Added `scaleAppliedSignature` and `updateAppliedSignaturePositions` methods to dynamically scale and position applied signatures based on zoom level and page.

Enhanced signature button rendering by scaling dimensions (width, height, font size, icon size) proportionally with zoom. Added attributes to store base values for applied signature containers to facilitate scaling.

Improved handling of applied signatures to ensure proper scaling, positioning, and visibility during zoom and page navigation. These changes enhance user experience and maintain consistency across zoom levels.
2026-06-08 11:39:17 +02:00

1137 lines
43 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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,
_renderLock: false, // Lock to prevent concurrent renders
// Quality options (configurable from appsettings.json)
qualityOptions: {
thumbnailBaseScale: 0.75,
thumbnailEnableHiDPI: true,
thumbnailMaxDPR: 2.0,
mainCanvasEnableHiDPI: true,
mainCanvasMaxDPR: 2.0,
enableSmoothZoom: true,
zoomTransitionDuration: 150,
renderingOpacity: 0.85,
zoomStepPercentage: 5
},
setQualityOptions(options) {
this.qualityOptions = { ...this.qualityOptions, ...options };
// Apply CSS variables for dynamic styling
document.documentElement.style.setProperty('--zoom-transition-duration', `${options.zoomTransitionDuration}ms`);
document.documentElement.style.setProperty('--rendering-opacity', options.renderingOpacity);
},
async initialize(canvasId, pdfDataUrl, dotNetRef) {
try {
this.dotNetReference = dotNetRef;
if (typeof window.pdfjsLib === 'undefined') {
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) {
return false;
}
this.ctx = this.canvas.getContext('2d');
this.attachWheelEvent();
const uint8Array = this.base64ToUint8Array(pdfDataUrl);
const loadingTask = pdfjsLib.getDocument({ data: uint8Array });
this.pdfDoc = await loadingTask.promise;
this.totalPages = this.pdfDoc.numPages;
await this.renderPage(this.pageNum);
return true;
} catch (error) {
console.error('PDF viewer initialization failed:', 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();
const step = this.qualityOptions.zoomStepPercentage / 100; // Convert to decimal
if (e.deltaY < 0) {
// Scroll up = Zoom In
this.scale = Math.min(this.scale + step, 3.0);
this.queueRenderPage(this.pageNum);
if (this.dotNetReference) {
this.dotNetReference.invokeMethodAsync('OnZoomChanged', this.scale);
}
} else {
// Scroll down = Zoom Out
this.scale = Math.max(this.scale - step, 0.5);
this.queueRenderPage(this.pageNum);
if (this.dotNetReference) {
this.dotNetReference.invokeMethodAsync('OnZoomChanged', this.scale);
}
}
}
}, { passive: false });
this.wheelEventAttached = true;
},
async waitForPdfJs() {
for (let i = 0; i < 50; i++) {
if (typeof window.pdfjsLib !== 'undefined') {
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) {
// CRITICAL: Single render at a time - use a lock
if (this._renderLock) {
// Another render is in progress, queue it
this.pageNumPending = num;
return;
}
this._renderLock = true;
// Cancel any existing render task
if (this.currentRenderTask) {
try {
this.currentRenderTask.cancel();
await new Promise(resolve => setTimeout(resolve, 100));
} catch (e) {
// Ignore cancellation errors
}
this.currentRenderTask = null;
}
this.pageRendering = true;
// Add rendering class for smooth transition (if enabled)
if (this.qualityOptions.enableSmoothZoom) {
this.canvas.classList.add('rendering');
}
try {
// Get scroll container
const container = this.canvas.closest('.pdf-canvas-wrapper');
// 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);
// HiDPI support for main canvas (configurable)
const dpr = this.qualityOptions.mainCanvasEnableHiDPI
? Math.min(window.devicePixelRatio || 1, this.qualityOptions.mainCanvasMaxDPR)
: 1.0;
const viewport = page.getViewport({ scale: this.scale * dpr });
// Set internal canvas resolution (high quality)
this.canvas.width = viewport.width;
this.canvas.height = viewport.height;
// Set CSS display size (visual size)
this.canvas.style.width = `${viewport.width / dpr}px`;
this.canvas.style.height = `${viewport.height / dpr}px`;
const renderContext = {
canvasContext: this.ctx,
viewport: viewport
};
// Enable high-quality rendering
this.ctx.imageSmoothingEnabled = true;
this.ctx.imageSmoothingQuality = 'high';
// Start new render task
this.currentRenderTask = page.render(renderContext);
await this.currentRenderTask.promise;
// Render text layer for copy-paste functionality
await this.renderTextLayer(page, viewport, dpr);
// 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;
}
// Remove rendering class after completion
if (this.qualityOptions.enableSmoothZoom) {
this.canvas.classList.remove('rendering');
}
this.currentRenderTask = null;
this.pageRendering = false;
} catch (error) {
if (error.name !== 'RenderingCancelledException') {
console.error('Render error:', error);
}
this.canvas.classList.remove('rendering');
this.currentRenderTask = null;
this.pageRendering = false;
} finally {
// Always release lock
this._renderLock = false;
// Process pending render
if (this.pageNumPending !== null) {
const pendingPage = this.pageNumPending;
this.pageNumPending = null;
this.renderPage(pendingPage);
}
}
},
async renderTextLayer(page, viewport, dpr) {
try {
const textLayerDiv = document.getElementById('pdf-text-layer');
if (!textLayerDiv) {
return;
}
// Clear previous text layer
textLayerDiv.innerHTML = '';
// Set text layer dimensions to match canvas display size
textLayerDiv.style.width = `${viewport.width / dpr}px`;
textLayerDiv.style.height = `${viewport.height / dpr}px`;
// Set --scale-factor CSS variable required by PDF.js
textLayerDiv.style.setProperty('--scale-factor', this.scale);
// Get text content from PDF
const textContent = await page.getTextContent();
// Create viewport for text layer (without DPR scaling for positioning)
const textViewport = page.getViewport({ scale: this.scale });
// Render text layer using PDF.js built-in function
const pdfjsLib = window.pdfjsLib;
await pdfjsLib.renderTextLayer({
textContentSource: textContent,
container: textLayerDiv,
viewport: textViewport,
textDivs: []
}).promise;
} catch (error) {
// Silently handle text layer errors (non-critical feature)
}
},
queueRenderPage(num) {
// Always use pending mechanism to avoid race conditions
if (this.pageRendering || this.currentRenderTask) {
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;
},
async zoomIn() {
const step = this.qualityOptions.zoomStepPercentage / 100;
this.scale = Math.min(this.scale + step, 3.0);
await this.renderPage(this.pageNum);
},
async zoomOut() {
const step = this.qualityOptions.zoomStepPercentage / 100;
this.scale = Math.max(this.scale - step, 0.5);
await this.renderPage(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);
}
},
async renderThumbnail(pageNum, canvasId) {
if (!this.pdfDoc) {
return;
}
try {
// Wait for canvas to be in DOM
let canvas = document.getElementById(canvasId);
let retries = 0;
while (!canvas && retries < 10) {
await new Promise(resolve => setTimeout(resolve, 100));
canvas = document.getElementById(canvasId);
retries++;
}
if (!canvas) {
return;
}
// Check if canvas is already being used by a render task
if (canvas._renderTask) {
try {
await canvas._renderTask;
} catch (e) {
// Ignore cancellation errors
}
}
const page = await this.pdfDoc.getPage(pageNum);
// High-quality rendering with HiDPI support (configurable)
const dpr = this.qualityOptions.thumbnailEnableHiDPI
? Math.min(window.devicePixelRatio || 1, this.qualityOptions.thumbnailMaxDPR)
: 1.0;
const baseScale = this.qualityOptions.thumbnailBaseScale;
const scale = baseScale * dpr;
const viewport = page.getViewport({ scale: scale });
// Set actual canvas pixel dimensions (internal resolution)
canvas.width = viewport.width;
canvas.height = viewport.height;
// Remove any inline styles - let CSS handle display size
canvas.style.width = '';
canvas.style.height = '';
const ctx = canvas.getContext('2d');
// Enable maximum quality rendering
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
const renderContext = {
canvasContext: ctx,
viewport: viewport
};
// Store render task on canvas to track active renders
const renderTask = page.render(renderContext);
canvas._renderTask = renderTask.promise;
await renderTask.promise;
// Clear the task when done
delete canvas._renderTask;
} catch (error) {
// Silently handle thumbnail errors (non-critical)
}
},
getCurrentPage() {
return this.pageNum;
},
getTotalPages() {
return this.totalPages;
},
getScale() {
return this.scale;
},
dispose() {
if (this.wheelEventAttached) {
this.wheelEventAttached = false;
this.dotNetReference = null;
}
this.detachResizeListeners();
},
// Resizable splitter functionality
isResizing: false,
resizeMouseMoveHandler: null,
resizeMouseUpHandler: null,
attachResizeListeners(dotNetRef) {
this.dotNetReference = dotNetRef;
this.resizeMouseMoveHandler = (e) => {
if (this.isResizing && this.dotNetReference) {
this.dotNetReference.invokeMethodAsync('OnSplitterMouseMove', e.clientX);
}
};
this.resizeMouseUpHandler = () => {
if (this.isResizing && this.dotNetReference) {
this.isResizing = false;
this.dotNetReference.invokeMethodAsync('OnSplitterMouseUp');
}
};
document.addEventListener('mousemove', this.resizeMouseMoveHandler);
document.addEventListener('mouseup', this.resizeMouseUpHandler);
},
detachResizeListeners() {
if (this.resizeMouseMoveHandler) {
document.removeEventListener('mousemove', this.resizeMouseMoveHandler);
this.resizeMouseMoveHandler = null;
}
if (this.resizeMouseUpHandler) {
document.removeEventListener('mouseup', this.resizeMouseUpHandler);
this.resizeMouseUpHandler = null;
}
this.isResizing = false;
},
startResize() {
this.isResizing = true;
},
// Signature button functionality
signatureButtons: [],
appliedSignatures: [], // Track which signatures have been applied (ID list)
appliedSignatureElements: [], // ✅ NEW: Track applied signature DOM elements
_lastViewedSignatureId: null, // Track last viewed signature for navigation
/**
* Gets signature navigation state (for toolbar display)
* @returns {object} { total, signed, unsigned, currentIndex, canGoPrev, canGoNext }
*/
getSignatureNavState() {
// Global imza listesi yoksa bo? state d<>n
if (!this._allSignatures || this._allSignatures.length === 0) {
return {
total: 0,
signed: 0,
unsigned: 0,
currentIndex: -1,
canGoPrev: false,
canGoNext: false
};
}
// T<>M sayfalardaki imzalar? say (database'den gelen global liste)
const total = this._allSignatures.length; // Global: Toplam imza say?s?
const signed = this.appliedSignatures.length; // ?mzalananlar
const unsigned = total - signed; // Hesaplanan: ?mzalanmayanlar
// Mevcut görüntülenen imzanın sıra numarasını bul
let currentIndex = 0;
if (this._lastViewedSignatureId) {
const index = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId);
currentIndex = index !== -1 ? index + 1 : 0; // 1-based index (kullanıcıya göstermek için)
}
return {
total: total,
signed: signed,
unsigned: unsigned,
currentIndex: currentIndex, // Şu an hangi imzada (1-5 arası)
canGoPrev: total > 0, // Her zaman aktif (e?er imza varsa)
canGoNext: total > 0 // Her zaman aktif (e?er imza varsa)
};
},
/**
* Navigates to the next unsigned signature button.
* Scrolls to button position and changes page if necessary.
* Cross-page navigation: searches ALL pages for next unsigned signature.
*/
async goToNextSignature(dotNetRef) {
// Global imza listesi yoksa <20>?k
if (!this._allSignatures || this._allSignatures.length === 0) {
return false;
}
// Mevcut g<>r<EFBFBD>nt<6E>lenen imzan?n index'ini bul
let currentIndex = -1;
if (this._lastViewedSignatureId) {
currentIndex = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId);
}
// Bir sonraki imzay? al (imzalanm?? olup olmad???na bakmadan)
let nextIndex = currentIndex + 1;
// Sonsuz d<>ng<6E>: Son imzadaysa ilk imzaya d<>n
if (nextIndex >= this._allSignatures.length) {
nextIndex = 0; // ?lk imzaya d<>n
}
const nextSignature = this._allSignatures[nextIndex];
// Farkl? sayfadaysa sayfa de?i?tir
if (nextSignature.page !== this.pageNum) {
// Sayfa de?i?tir
this.pageNum = nextSignature.page;
this.queueRenderPage(this.pageNum);
// Render tamamlanana kadar bekle
let waitCount = 0;
while (this.pageRendering && waitCount < 20) {
await new Promise(resolve => setTimeout(resolve, 100));
waitCount++;
}
// Blazor'a haber ver - signature butonlar?n? yeniden ?iz
if (dotNetRef) {
await dotNetRef.invokeMethodAsync('OnPageChangedBySignatureNav', this.pageNum);
}
// Butonlar?n DOM'a eklenmesini bekle
await new Promise(resolve => setTimeout(resolve, 150));
}
// Son g<>r<EFBFBD>nt<6E>lenen imzay? kaydet
this._lastViewedSignatureId = nextSignature.id;
// ?mza imzalanm?? m? kontrol et
const isApplied = this.appliedSignatures.some(s => s.id === nextSignature.id);
if (isApplied) {
// ?mzalanm?? - overlay container'? bul ve scroll yap
const container = document.querySelector(`.applied-signature[data-signature-id="${nextSignature.id}"]`);
if (container) {
this.scrollToElement(container);
}
} else {
// ?mzalanmam?? - butonu bul ve scroll yap
const button = this.signatureButtons.find(btn =>
parseInt(btn.getAttribute('data-signature-id')) === nextSignature.id
);
if (button) {
this.scrollToButton(button);
}
}
// Counter'? g<>ncelle (Blazor'a bildir)
if (dotNetRef) {
dotNetRef.invokeMethodAsync('OnSignatureNavChanged');
}
return true;
},
/**
* Navigates to the previous signature (last applied one).
* Scrolls to signature position and changes page if necessary.
*/
async goToPreviousSignature(dotNetRef) {
if (!this._allSignatures || this._allSignatures.length === 0) {
return false;
}
// Mevcut g<>r<EFBFBD>nt<6E>lenen imzan?n index'ini bul
let currentIndex = this._allSignatures.length; // Varsay?lan: son imzadan sonra
if (this._lastViewedSignatureId) {
currentIndex = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId);
}
// Bir <20>nceki imzay? al
let prevIndex = currentIndex - 1;
// Sonsuz d<>ng<6E>: ?lk imzadaysa son imzaya git
if (prevIndex < 0) {
prevIndex = this._allSignatures.length - 1; // Son imzaya git
}
const prevSignature = this._allSignatures[prevIndex];
// Change page if needed
if (prevSignature.page !== this.pageNum) {
// Sayfa de?i?tir
this.pageNum = prevSignature.page;
this.queueRenderPage(this.pageNum);
// Render tamamlanana kadar bekle
let waitCount = 0;
while (this.pageRendering && waitCount < 20) {
await new Promise(resolve => setTimeout(resolve, 100));
waitCount++;
}
// Blazor'a haber ver - signature butonlar?n? yeniden <20>iz
if (dotNetRef) {
await dotNetRef.invokeMethodAsync('OnPageChangedBySignatureNav', this.pageNum);
}
// DOM g<>ncellenmesini bekle
await new Promise(resolve => setTimeout(resolve, 150));
}
// Son g<>r<EFBFBD>nt<6E>lenen imzay? kaydet
this._lastViewedSignatureId = prevSignature.id;
// ?mza imzalanm?? m? kontrol et
const isApplied = this.appliedSignatures.some(s => s.id === prevSignature.id);
if (isApplied) {
// ?mzalanm?? - overlay container'? bul ve scroll yap
const container = document.querySelector(`.applied-signature[data-signature-id="${prevSignature.id}"]`);
if (container) {
this.scrollToElement(container);
}
} else {
// ?mzalanmam?? - butonu bul ve scroll yap
const button = this.signatureButtons.find(btn =>
parseInt(btn.getAttribute('data-signature-id')) === prevSignature.id
);
if (button) {
this.scrollToButton(button);
}
}
// Notify Blazor
if (dotNetRef) {
dotNetRef.invokeMethodAsync('OnSignatureNavChanged');
}
return true;
},
/**
* Scrolls to center a button in the viewport
*/
scrollToButton(button) {
const wrapper = this.canvas.closest('.pdf-canvas-wrapper');
if (!wrapper) return;
const buttonRect = button.getBoundingClientRect();
const wrapperRect = wrapper.getBoundingClientRect();
// Calculate scroll to center button
const scrollLeft = wrapper.scrollLeft + buttonRect.left - wrapperRect.left - (wrapperRect.width / 2) + (buttonRect.width / 2);
const scrollTop = wrapper.scrollTop + buttonRect.top - wrapperRect.top - (wrapperRect.height / 2) + (buttonRect.height / 2);
wrapper.scrollTo({
left: scrollLeft,
top: scrollTop,
behavior: 'smooth'
});
},
/**
* Scrolls to center an element in the viewport
*/
scrollToElement(element) {
const wrapper = this.canvas.closest('.pdf-canvas-wrapper');
if (!wrapper) return;
const elemRect = element.getBoundingClientRect();
const wrapperRect = wrapper.getBoundingClientRect();
const scrollLeft = wrapper.scrollLeft + elemRect.left - wrapperRect.left - (wrapperRect.width / 2) + (elemRect.width / 2);
const scrollTop = wrapper.scrollTop + elemRect.top - wrapperRect.top - (wrapperRect.height / 2) + (elemRect.height / 2);
wrapper.scrollTo({
left: scrollLeft,
top: scrollTop,
behavior: 'smooth'
});
},
/**
* Renders clickable signature buttons on the PDF canvas.
* @param {Array} signatures - Array of SignatureDto objects with x, y coordinates in PDF POINTS
* @param {number} currentPageNum - Current page number (1-based)
* @param {object} dotNetRef - .NET reference for callbacks
*/
async renderSignatureButtons(signatures, currentPageNum, dotNetRef) {
// Clear existing buttons (NOT applied signatures!)
this.clearSignatureButtons();
if (!this.pdfDoc || !signatures || signatures.length === 0) {
return;
}
this.dotNetReference = dotNetRef;
this._allSignatures = signatures; // Store for navigation
try {
// CRITICAL: Filter OUT already applied signatures!
const appliedIds = new Set(this.appliedSignatures.map(s => s.id));
const pageSignatures = signatures.filter(sig =>
sig.page === currentPageNum && !appliedIds.has(sig.id) // ? Skip applied ones!
);
if (pageSignatures.length === 0) {
return;
}
// Get current page and viewport
const page = await this.pdfDoc.getPage(currentPageNum);
const dpr = this.qualityOptions.mainCanvasEnableHiDPI
? Math.min(window.devicePixelRatio || 1, this.qualityOptions.mainCanvasMaxDPR)
: 1.0;
const viewport = page.getViewport({ scale: this.scale * dpr });
// Get signature layer container
const signatureLayer = document.getElementById('pdf-signature-layer');
if (!signatureLayer) {
console.warn('Signature layer not found');
return;
}
// Set signature layer dimensions to match canvas display size
signatureLayer.style.width = `${viewport.width / dpr}px`;
signatureLayer.style.height = `${viewport.height / dpr}px`;
// Update applied signature coordinates for current zoom level
this.updateAppliedSignaturePositions(signatureLayer, currentPageNum);
// Create button for each UNSIGNED signature
pageSignatures.forEach(sig => {
// Coordinates are in PDF POINTS - convert to display pixels
const xPx = (sig.x * this.scale);
const yPx = (sig.y * this.scale);
// ✅ FIXED: Scale button size proportionally with zoom
const baseScale = 1.5; // Reference scale (initial load)
const scaleFactor = this.scale / baseScale;
const baseWidth = 150;
const baseHeight = 60;
const baseFontSize = 18;
const baseIconSize = 24;
const scaledWidth = baseWidth * scaleFactor;
const scaledHeight = baseHeight * scaleFactor;
const scaledFontSize = Math.max(baseFontSize * scaleFactor, 10); // Min 10px
const scaledIconSize = baseIconSize * scaleFactor;
// Create button element
const button = document.createElement('button');
button.className = 'signature-button';
button.setAttribute('data-signature-id', sig.id);
button.setAttribute('type', 'button');
button.setAttribute('tabindex', '0');
button.style.position = 'absolute';
button.style.left = `${xPx}px`;
button.style.top = `${yPx}px`;
button.style.width = `${scaledWidth}px`; // ✅ Scaled
button.style.height = `${scaledHeight}px`; // ✅ Scaled
button.style.backgroundColor = '#4F46E5';
button.style.color = 'white';
button.style.border = 'none';
button.style.borderRadius = '8px';
button.style.cursor = 'pointer';
button.style.fontSize = '16px';
button.style.fontWeight = '600';
button.style.display = 'flex';
button.style.flexDirection = 'column';
button.style.alignItems = 'center';
button.style.justifyContent = 'center';
button.style.gap = '4px';
button.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
button.style.transition = 'all 0.2s ease';
button.style.zIndex = '100';
// Add text
const textDiv = document.createElement('div');
textDiv.textContent = 'Unterschreiben';
textDiv.style.fontSize = `${scaledFontSize}px`; // ✅ Scaled
textDiv.style.fontWeight = '700';
// Add SVG icon
const svgNS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(svgNS, 'svg');
svg.setAttribute('width', scaledIconSize); // ✅ Scaled
svg.setAttribute('height', scaledIconSize); // ✅ Scaled
svg.setAttribute('viewBox', '0 8 32 36');
svg.setAttribute('fill', 'none');
svg.style.filter = 'drop-shadow(0 1px 2px rgba(0,0,0,0.2))';
const path = document.createElementNS(svgNS, 'path');
path.setAttribute('fill-rule', 'evenodd');
path.setAttribute('clip-rule', 'evenodd');
path.setAttribute('d', 'M25.061 6.90625L23.7115 8.25503C23.2861 8.05188 22.8241 7.9503 22.3621 7.9503C21.5605 7.9503 20.7589 8.25613 20.1483 8.86778L8.18147 20.8336L6.70565 26.7379H6.70557V27.7817H26.5372V26.7379H6.70671L12.6102 25.2623L24.576 13.2955C25.5404 12.3318 25.7445 10.8952 25.1882 9.73146L26.5369 8.38214L25.061 6.90625ZM23.174 10.27C22.9569 10.0539 22.6688 9.93388 22.362 9.93388C22.0551 9.93388 21.767 10.0539 21.5499 10.27L13.5323 18.2876L15.1564 19.9117L23.174 11.8941C23.6218 11.4463 23.6218 10.7177 23.174 10.27ZM14.4922 20.5759L12.868 18.9518L9.97241 21.8475L9.43069 24.0133L11.5965 23.4716L14.4922 20.5759Z');
path.setAttribute('fill', 'white');
svg.appendChild(path);
button.appendChild(textDiv);
button.appendChild(svg);
// Add hover effect
button.addEventListener('mouseenter', () => {
button.style.backgroundColor = '#4338CA';
button.style.transform = 'scale(1.05)';
button.style.boxShadow = '0 4px 12px rgba(79, 70, 229, 0.4)';
});
button.addEventListener('mouseleave', () => {
button.style.backgroundColor = '#4F46E5';
button.style.transform = 'scale(1)';
button.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
});
// Add click handler
button.addEventListener('click', () => {
if (this.dotNetReference) {
this.dotNetReference.invokeMethodAsync('OnSignatureButtonClick', sig.id);
}
});
signatureLayer.appendChild(button);
this.signatureButtons.push(button);
});
} catch (error) {
console.error('Error rendering signature buttons:', error);
}
},
/**
* Scales an applied signature container based on current zoom level
* @param {HTMLElement} container - Applied signature container
* @param {number} currentScale - Current PDF zoom scale
*/
scaleAppliedSignature(container, currentScale) {
const baseScale = 1.5; // Reference scale (initial load)
const scaleFactor = currentScale / baseScale;
// Scale width
const baseWidth = parseInt(container.getAttribute('data-base-width') || 230);
container.style.width = `${baseWidth * scaleFactor}px`;
// Scale padding
const basePadding = parseInt(container.getAttribute('data-base-padding') || 12);
container.style.padding = `${basePadding * scaleFactor}px`;
// Scale border radius (subtle detail)
const baseBorderRadius = parseInt(container.getAttribute('data-base-border-radius') || 6);
container.style.borderRadius = `${baseBorderRadius * scaleFactor}px`;
// Scale font size (with min 6px for readability)
const infoContainer = container.querySelector('.signature-info-text');
if (infoContainer) {
const baseFontSize = parseInt(infoContainer.getAttribute('data-base-font-size') || 9);
const scaledFontSize = Math.max(baseFontSize * scaleFactor, 6);
infoContainer.style.fontSize = `${scaledFontSize}px`;
}
// Scale image max height
const baseImgHeight = parseInt(container.getAttribute('data-base-img-height') || 70);
const img = container.querySelector('img');
if (img) {
img.style.maxHeight = `${baseImgHeight * scaleFactor}px`;
}
// Scale separator line margin
const baseSeparatorMargin = 6;
const separators = container.querySelectorAll('div[style*="border-top"]');
separators.forEach(sep => {
sep.style.marginTop = `${baseSeparatorMargin * scaleFactor}px`;
sep.style.marginBottom = `${(baseSeparatorMargin + 2) * scaleFactor}px`;
});
},
/**
* Updates applied signature positions based on current zoom level
* @param {HTMLElement} signatureLayer - Signature layer container
* @param {number} currentPageNum - Current page number
*/
updateAppliedSignaturePositions(signatureLayer, currentPageNum) {
if (!signatureLayer || !this._allSignatures) return;
const appliedContainers = signatureLayer.querySelectorAll('.applied-signature');
appliedContainers.forEach(container => {
const signatureId = parseInt(container.getAttribute('data-signature-id'));
const signature = this._allSignatures.find(s => s.id === signatureId);
if (signature) {
// ✅ Position calculation (same as renderSignatureButtons)
const xPx = signature.x * this.scale;
const yPx = signature.y * this.scale;
container.style.left = `${xPx}px`;
container.style.top = `${yPx}px`;
// ✅ FIXED: Apply comprehensive scaling using helper method
this.scaleAppliedSignature(container, this.scale);
// Show/hide based on current page
container.style.display = (signature.page === currentPageNum) ? '' : 'none';
}
});
},
/**
* Clears all signature buttons from the canvas.
* Also hides applied signatures that don't belong to current page.
*/
clearSignatureButtons() {
// Remove unsigned signature buttons
this.signatureButtons.forEach(button => {
if (button.parentNode) {
button.parentNode.removeChild(button);
}
});
this.signatureButtons = [];
// ✅ FIXED: Update applied signatures (position + scaling)
this.appliedSignatureElements.forEach(container => {
const signatureId = parseInt(container.getAttribute('data-signature-id'));
const signature = this._allSignatures?.find(s => s.id === signatureId);
if (signature) {
// Update position
const xPx = signature.x * this.scale;
const yPx = signature.y * this.scale;
container.style.left = `${xPx}px`;
container.style.top = `${yPx}px`;
// Update scaling
this.scaleAppliedSignature(container, this.scale);
// Show/hide based on current page
container.style.display = (signature.page === this.pageNum) ? '' : 'none';
}
});
},
/**
* Applies a signature to a specific signature field, removing the button and rendering the signature.
* German standard: Signature image + Name, Position, Place, Date
* @param {number} signatureId - ID of the signature field
* @param {string} signatureDataUrl - Base64 PNG data URL of signature
* @param {string} fullName - Signer's full name
* @param {string} position - Signer's position (optional, can be empty)
* @param {string} place - Signing place
*/
async applySignature(signatureId, signatureDataUrl, fullName, position, place) {
try {
// Find and remove the button
const buttonIndex = this.signatureButtons.findIndex(btn => {
return btn.getAttribute('data-signature-id') == signatureId;
});
if (buttonIndex === -1) {
console.warn(`Signature button #${signatureId} not found`);
return;
}
const button = this.signatureButtons[buttonIndex];
const signatureLayer = document.getElementById('pdf-signature-layer');
if (!signatureLayer) {
console.error('Signature layer not found');
return;
}
// Get button position before removing it
const left = button.style.left;
const top = button.style.top;
// Find signature data for tracking
const signature = this._allSignatures?.find(s => s.id === signatureId);
// Remove button
if (button.parentNode) {
button.parentNode.removeChild(button);
}
this.signatureButtons.splice(buttonIndex, 1);
// Track applied signature
if (signature) {
this.appliedSignatures.push({
id: signatureId,
page: signature.page
});
}
// Create signature container
const signatureContainer = document.createElement('div');
signatureContainer.className = 'applied-signature';
signatureContainer.setAttribute('data-signature-id', signatureId);
// ✅ FIXED: Store base values for scaling
signatureContainer.setAttribute('data-base-width', '230');
signatureContainer.setAttribute('data-base-padding', '12');
signatureContainer.setAttribute('data-base-font-size', '9');
signatureContainer.setAttribute('data-base-img-height', '70');
signatureContainer.setAttribute('data-base-border-radius', '6');
signatureContainer.style.position = 'absolute';
signatureContainer.style.left = left;
signatureContainer.style.top = top;
signatureContainer.style.width = '230px';
signatureContainer.style.backgroundColor = '#f8f9fa';
signatureContainer.style.border = '1px solid #dee2e6';
signatureContainer.style.borderRadius = '6px';
signatureContainer.style.padding = '12px';
signatureContainer.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
signatureContainer.style.fontFamily = "'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
// Signature image
const signatureImg = document.createElement('img');
signatureImg.src = signatureDataUrl;
signatureImg.alt = 'Unterschrift';
signatureImg.style.width = '100%';
signatureImg.style.height = 'auto';
signatureImg.style.maxHeight = '70px';
signatureImg.style.display = 'block';
signatureImg.style.objectFit = 'contain';
signatureImg.style.marginBottom = '6px';
// Separator line (German standard)
const separator = document.createElement('div');
separator.style.width = '100%';
separator.style.height = '1px';
separator.style.backgroundColor = '#495057';
separator.style.marginBottom = '8px';
// Text information container
const infoContainer = document.createElement('div');
infoContainer.className = 'signature-info-text'; // ✅ Class ekle (querySelector için)
infoContainer.setAttribute('data-base-font-size', '9'); // ✅ Base font size sakla
infoContainer.style.fontSize = '9px';
infoContainer.style.lineHeight = '1.4';
infoContainer.style.color = '#495057';
infoContainer.style.fontWeight = '400';
// Format date (German style: dd.MM.yyyy)
const today = new Date();
const dateStr = today.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
// Build text lines (German standard format)
const lines = [];
lines.push(`<strong style="font-weight: 600; color: #212529;">${this.escapeHtml(fullName)}</strong>`);
if (position && position.trim() !== '') {
lines.push(`${this.escapeHtml(position)}`);
}
lines.push(`${this.escapeHtml(place)}, ${dateStr}`);
infoContainer.innerHTML = lines.join('<br>');
// Assemble container
signatureContainer.appendChild(signatureImg);
signatureContainer.appendChild(separator);
signatureContainer.appendChild(infoContainer);
// Add to signature layer
signatureLayer.appendChild(signatureContainer);
// ✅ FIXED: Track applied signature element for zoom updates
this.appliedSignatureElements.push(signatureContainer);
// ✅ FIXED: Apply initial scaling based on current zoom
this.scaleAppliedSignature(signatureContainer, this.scale);
console.log(`Signature #${signatureId} applied successfully`);
} catch (error) {
console.error('Error applying signature:', error);
}
},
/**
* Escapes HTML to prevent XSS attacks
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
};