This commit replaces the existing PDF.js-based viewer with the DevExpress DxPdfViewer component, introducing significant improvements to the UI, state management, and signature handling. Key changes: - Integrated DevExpress.Blazor.PdfViewer and removed PDF.js dependencies. - Updated HTML structure to use `DxPdfViewer` and new overlay layers. - Refactored zoom and navigation logic to use DevExpress APIs. - Overhauled signature button rendering and positioning logic. - Added dynamic scaling for applied signatures based on zoom level. - Introduced `requestOverlayRefresh` for efficient overlay updates. - Added new CSS styles for the DevExpress viewer and overlays. - Refactored `pdf-viewer.js` to remove legacy PDF.js logic. - Improved performance with `requestAnimationFrame` and optimized event handling. - Added a `dispose` method for proper cleanup of resources. - Enhanced error handling and accessibility for signature buttons. - Removed redundant code and improved overall maintainability.
876 lines
30 KiB
JavaScript
876 lines
30 KiB
JavaScript
// PDF.js Helper for DevExpress Document Viewer
|
|
window.pdfViewer = {
|
|
pdfDoc: null,
|
|
totalPages: 0,
|
|
pageNum: 1,
|
|
currentZoom: 150,
|
|
dotNetReference: null,
|
|
viewerHostId: null,
|
|
overlayLayerId: null,
|
|
wheelHandler: null,
|
|
resizeObserver: null,
|
|
overlayRefreshHandle: 0,
|
|
retryRenderHandle: 0,
|
|
scrollBoundElements: new WeakSet(),
|
|
|
|
qualityOptions: {
|
|
thumbnailBaseScale: 0.75,
|
|
thumbnailEnableHiDPI: true,
|
|
thumbnailMaxDPR: 2.0,
|
|
mainCanvasEnableHiDPI: true,
|
|
mainCanvasMaxDPR: 2.0,
|
|
enableSmoothZoom: true,
|
|
zoomTransitionDuration: 150,
|
|
renderingOpacity: 0.85,
|
|
zoomStepPercentage: 5
|
|
},
|
|
|
|
signatureButtons: [],
|
|
appliedSignatures: [],
|
|
appliedSignatureElements: [],
|
|
_allSignatures: [],
|
|
_lastViewedSignatureId: null,
|
|
|
|
setQualityOptions(options) {
|
|
this.qualityOptions = { ...this.qualityOptions, ...options };
|
|
document.documentElement.style.setProperty('--zoom-transition-duration', `${options.zoomTransitionDuration}ms`);
|
|
document.documentElement.style.setProperty('--rendering-opacity', options.renderingOpacity);
|
|
},
|
|
|
|
async initialize(pdfDataUrl, viewerHostId, overlayLayerId, dotNetRef) {
|
|
try {
|
|
this.dotNetReference = dotNetRef;
|
|
this.viewerHostId = viewerHostId;
|
|
this.overlayLayerId = overlayLayerId;
|
|
|
|
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';
|
|
|
|
const uint8Array = this.base64ToUint8Array(pdfDataUrl);
|
|
const loadingTask = pdfjsLib.getDocument({ data: uint8Array });
|
|
this.pdfDoc = await loadingTask.promise;
|
|
this.totalPages = this.pdfDoc.numPages;
|
|
this.pageNum = 1;
|
|
this.currentZoom = 150;
|
|
return true;
|
|
} catch (error) {
|
|
console.error('PDF helper initialization failed:', error);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
setViewState(currentPage, currentZoom) {
|
|
this.pageNum = currentPage;
|
|
this.currentZoom = currentZoom;
|
|
this.requestOverlayRefresh();
|
|
},
|
|
|
|
attachViewerInteractionListeners(viewerHostId, dotNetRef) {
|
|
this.viewerHostId = viewerHostId;
|
|
this.dotNetReference = dotNetRef;
|
|
|
|
const host = this.getViewerHost();
|
|
if (!host) {
|
|
return;
|
|
}
|
|
|
|
if (!this.wheelHandler) {
|
|
this.wheelHandler = async (e) => {
|
|
if (!(e.ctrlKey || e.metaKey) || !this.dotNetReference) {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
const step = this.qualityOptions.zoomStepPercentage;
|
|
const nextZoom = e.deltaY < 0
|
|
? Math.min(this.currentZoom + step, 300)
|
|
: Math.max(this.currentZoom - step, 50);
|
|
|
|
if (nextZoom !== this.currentZoom) {
|
|
this.currentZoom = nextZoom;
|
|
await this.dotNetReference.invokeMethodAsync('OnZoomGestureRequested', nextZoom);
|
|
}
|
|
};
|
|
|
|
host.addEventListener('wheel', this.wheelHandler, { passive: false });
|
|
}
|
|
|
|
if (!this.resizeObserver && typeof ResizeObserver !== 'undefined') {
|
|
this.resizeObserver = new ResizeObserver(() => this.requestOverlayRefresh());
|
|
this.resizeObserver.observe(host);
|
|
}
|
|
|
|
this.ensureScrollBindings();
|
|
this.requestOverlayRefresh();
|
|
},
|
|
|
|
ensureScrollBindings() {
|
|
const host = this.getViewerHost();
|
|
if (!host) {
|
|
return;
|
|
}
|
|
|
|
const candidates = [host, ...host.querySelectorAll('*')];
|
|
candidates.forEach(element => {
|
|
if (this.scrollBoundElements.has(element)) {
|
|
return;
|
|
}
|
|
|
|
const style = window.getComputedStyle(element);
|
|
const overflowY = style.overflowY;
|
|
const overflowX = style.overflowX;
|
|
const isScrollable =
|
|
['auto', 'scroll', 'overlay'].includes(overflowY) ||
|
|
['auto', 'scroll', 'overlay'].includes(overflowX) ||
|
|
element.scrollHeight > element.clientHeight + 2 ||
|
|
element.scrollWidth > element.clientWidth + 2;
|
|
|
|
if (isScrollable) {
|
|
element.addEventListener('scroll', () => this.requestOverlayRefresh(), { passive: true });
|
|
this.scrollBoundElements.add(element);
|
|
}
|
|
});
|
|
},
|
|
|
|
requestOverlayRefresh() {
|
|
if (this.overlayRefreshHandle) {
|
|
cancelAnimationFrame(this.overlayRefreshHandle);
|
|
}
|
|
|
|
this.overlayRefreshHandle = requestAnimationFrame(() => {
|
|
this.overlayRefreshHandle = 0;
|
|
this.refreshOverlayLayout();
|
|
});
|
|
},
|
|
|
|
async refreshOverlayLayout() {
|
|
if (!this._allSignatures || this._allSignatures.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const metrics = await this.getCurrentPageMetrics(this.pageNum);
|
|
if (!metrics) {
|
|
return;
|
|
}
|
|
|
|
this.signatureButtons.forEach(button => {
|
|
const signatureId = parseInt(button.getAttribute('data-signature-id'));
|
|
const signature = this._allSignatures.find(s => s.id === signatureId);
|
|
if (signature) {
|
|
this.positionSignatureButton(button, signature, metrics);
|
|
}
|
|
});
|
|
|
|
this.updateAppliedSignaturePositions(metrics, this.pageNum);
|
|
},
|
|
|
|
getViewerHost() {
|
|
return this.viewerHostId ? document.getElementById(this.viewerHostId) : null;
|
|
},
|
|
|
|
getOverlayLayer() {
|
|
return this.overlayLayerId ? document.getElementById(this.overlayLayerId) : null;
|
|
},
|
|
|
|
getScrollContainer() {
|
|
const host = this.getViewerHost();
|
|
if (!host) {
|
|
return null;
|
|
}
|
|
|
|
let best = host;
|
|
let bestArea = host.clientWidth * host.clientHeight;
|
|
const elements = host.querySelectorAll('*');
|
|
|
|
elements.forEach(element => {
|
|
const isScrollable = element.scrollHeight > element.clientHeight + 2 || element.scrollWidth > element.clientWidth + 2;
|
|
const area = element.clientWidth * element.clientHeight;
|
|
if (isScrollable && area >= bestArea) {
|
|
best = element;
|
|
bestArea = area;
|
|
}
|
|
});
|
|
|
|
return best;
|
|
},
|
|
|
|
findPageSurface(host) {
|
|
const pickLargestVisible = (elements) => {
|
|
let best = null;
|
|
let bestArea = 0;
|
|
|
|
elements.forEach(element => {
|
|
if (element.id === this.overlayLayerId) {
|
|
return;
|
|
}
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
if (rect.width < 120 || rect.height < 120) {
|
|
return;
|
|
}
|
|
|
|
const style = window.getComputedStyle(element);
|
|
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity || '1') === 0) {
|
|
return;
|
|
}
|
|
|
|
const area = rect.width * rect.height;
|
|
if (area > bestArea) {
|
|
best = element;
|
|
bestArea = area;
|
|
}
|
|
});
|
|
|
|
return best;
|
|
};
|
|
|
|
const mediaCandidate = pickLargestVisible(Array.from(host.querySelectorAll('canvas, img, svg')));
|
|
if (mediaCandidate) {
|
|
return mediaCandidate;
|
|
}
|
|
|
|
return pickLargestVisible(Array.from(host.querySelectorAll('div')));
|
|
},
|
|
|
|
async getCurrentPageMetrics(pageNum) {
|
|
if (!this.pdfDoc) {
|
|
return null;
|
|
}
|
|
|
|
const host = this.getViewerHost();
|
|
const overlayLayer = this.getOverlayLayer();
|
|
if (!host || !overlayLayer) {
|
|
return null;
|
|
}
|
|
|
|
this.ensureScrollBindings();
|
|
|
|
const surface = this.findPageSurface(host);
|
|
if (!surface) {
|
|
return null;
|
|
}
|
|
|
|
const page = await this.pdfDoc.getPage(pageNum);
|
|
const viewport = page.getViewport({ scale: 1.0 });
|
|
const hostRect = host.getBoundingClientRect();
|
|
const pageRect = surface.getBoundingClientRect();
|
|
const scaleX = viewport.width > 0 ? pageRect.width / viewport.width : 1;
|
|
const scaleY = viewport.height > 0 ? pageRect.height / viewport.height : 1;
|
|
|
|
overlayLayer.style.width = `${host.clientWidth}px`;
|
|
overlayLayer.style.height = `${host.clientHeight}px`;
|
|
|
|
return {
|
|
left: pageRect.left - hostRect.left,
|
|
top: pageRect.top - hostRect.top,
|
|
width: pageRect.width,
|
|
height: pageRect.height,
|
|
scaleX,
|
|
scaleY,
|
|
scale: Math.min(scaleX, scaleY)
|
|
};
|
|
},
|
|
|
|
async renderThumbnail(pageNum, canvasId) {
|
|
if (!this.pdfDoc) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
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;
|
|
}
|
|
|
|
if (canvas._renderTask) {
|
|
try {
|
|
await canvas._renderTask;
|
|
} catch {
|
|
}
|
|
}
|
|
|
|
const page = await this.pdfDoc.getPage(pageNum);
|
|
const dpr = this.qualityOptions.thumbnailEnableHiDPI
|
|
? Math.min(window.devicePixelRatio || 1, this.qualityOptions.thumbnailMaxDPR)
|
|
: 1.0;
|
|
const viewport = page.getViewport({ scale: this.qualityOptions.thumbnailBaseScale * dpr });
|
|
|
|
canvas.width = viewport.width;
|
|
canvas.height = viewport.height;
|
|
canvas.style.width = '';
|
|
canvas.style.height = '';
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.imageSmoothingEnabled = true;
|
|
ctx.imageSmoothingQuality = 'high';
|
|
|
|
const renderTask = page.render({
|
|
canvasContext: ctx,
|
|
viewport
|
|
});
|
|
canvas._renderTask = renderTask.promise;
|
|
await renderTask.promise;
|
|
delete canvas._renderTask;
|
|
} catch {
|
|
}
|
|
},
|
|
|
|
getCurrentPage() {
|
|
return this.pageNum;
|
|
},
|
|
|
|
getTotalPages() {
|
|
return this.totalPages;
|
|
},
|
|
|
|
getScale() {
|
|
return this.currentZoom / 100;
|
|
},
|
|
|
|
nextPage() {
|
|
if (this.pageNum >= this.totalPages) {
|
|
return false;
|
|
}
|
|
this.pageNum++;
|
|
this.requestOverlayRefresh();
|
|
return true;
|
|
},
|
|
|
|
previousPage() {
|
|
if (this.pageNum <= 1) {
|
|
return false;
|
|
}
|
|
this.pageNum--;
|
|
this.requestOverlayRefresh();
|
|
return true;
|
|
},
|
|
|
|
goToPage(num) {
|
|
if (num < 1 || num > this.totalPages) {
|
|
return false;
|
|
}
|
|
this.pageNum = num;
|
|
this.requestOverlayRefresh();
|
|
return true;
|
|
},
|
|
|
|
async zoomIn() {
|
|
const nextZoom = Math.min(this.currentZoom + this.qualityOptions.zoomStepPercentage, 300);
|
|
this.currentZoom = nextZoom;
|
|
this.requestOverlayRefresh();
|
|
},
|
|
|
|
async zoomOut() {
|
|
const nextZoom = Math.max(this.currentZoom - this.qualityOptions.zoomStepPercentage, 50);
|
|
this.currentZoom = nextZoom;
|
|
this.requestOverlayRefresh();
|
|
},
|
|
|
|
setScale(scale) {
|
|
this.currentZoom = Math.max(50, Math.min(300, Math.round(scale * 100)));
|
|
this.requestOverlayRefresh();
|
|
},
|
|
|
|
async fitToWidth() {
|
|
this.currentZoom = 150;
|
|
this.requestOverlayRefresh();
|
|
},
|
|
|
|
getSignatureNavState() {
|
|
if (!this._allSignatures || this._allSignatures.length === 0) {
|
|
return {
|
|
total: 0,
|
|
signed: 0,
|
|
unsigned: 0,
|
|
currentIndex: 0,
|
|
canGoPrev: false,
|
|
canGoNext: false
|
|
};
|
|
}
|
|
|
|
const total = this._allSignatures.length;
|
|
const signed = this.appliedSignatures.length;
|
|
const unsigned = total - signed;
|
|
|
|
let currentIndex = 0;
|
|
if (this._lastViewedSignatureId) {
|
|
const index = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId);
|
|
currentIndex = index !== -1 ? index + 1 : 0;
|
|
}
|
|
|
|
return {
|
|
total,
|
|
signed,
|
|
unsigned,
|
|
currentIndex,
|
|
canGoPrev: total > 0,
|
|
canGoNext: total > 0
|
|
};
|
|
},
|
|
|
|
async goToNextSignature(dotNetRef) {
|
|
if (!this._allSignatures || this._allSignatures.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
let currentIndex = -1;
|
|
if (this._lastViewedSignatureId) {
|
|
currentIndex = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId);
|
|
}
|
|
|
|
let nextIndex = currentIndex + 1;
|
|
if (nextIndex >= this._allSignatures.length) {
|
|
nextIndex = 0;
|
|
}
|
|
|
|
const nextSignature = this._allSignatures[nextIndex];
|
|
await this.focusSignature(nextSignature, dotNetRef);
|
|
return true;
|
|
},
|
|
|
|
async goToPreviousSignature(dotNetRef) {
|
|
if (!this._allSignatures || this._allSignatures.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
let currentIndex = this._allSignatures.length;
|
|
if (this._lastViewedSignatureId) {
|
|
const foundIndex = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId);
|
|
currentIndex = foundIndex === -1 ? this._allSignatures.length : foundIndex;
|
|
}
|
|
|
|
let prevIndex = currentIndex - 1;
|
|
if (prevIndex < 0) {
|
|
prevIndex = this._allSignatures.length - 1;
|
|
}
|
|
|
|
const prevSignature = this._allSignatures[prevIndex];
|
|
await this.focusSignature(prevSignature, dotNetRef);
|
|
return true;
|
|
},
|
|
|
|
async focusSignature(signature, dotNetRef) {
|
|
if (signature.page !== this.pageNum) {
|
|
this.pageNum = signature.page;
|
|
if (dotNetRef) {
|
|
await dotNetRef.invokeMethodAsync('OnPageChangedBySignatureNav', this.pageNum);
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
}
|
|
|
|
this._lastViewedSignatureId = signature.id;
|
|
this.requestOverlayRefresh();
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
const appliedElement = document.querySelector(`.applied-signature[data-signature-id="${signature.id}"]`);
|
|
if (appliedElement) {
|
|
this.scrollToElement(appliedElement);
|
|
} else {
|
|
const button = this.signatureButtons.find(btn => parseInt(btn.getAttribute('data-signature-id')) === signature.id);
|
|
if (button) {
|
|
this.scrollToElement(button);
|
|
}
|
|
}
|
|
|
|
if (dotNetRef) {
|
|
await dotNetRef.invokeMethodAsync('OnSignatureNavChanged');
|
|
}
|
|
},
|
|
|
|
scrollToElement(element) {
|
|
const container = this.getScrollContainer();
|
|
if (!container || !element) {
|
|
return;
|
|
}
|
|
|
|
const elementRect = element.getBoundingClientRect();
|
|
const containerRect = container.getBoundingClientRect();
|
|
|
|
const left = container.scrollLeft + elementRect.left - containerRect.left - (containerRect.width / 2) + (elementRect.width / 2);
|
|
const top = container.scrollTop + elementRect.top - containerRect.top - (containerRect.height / 2) + (elementRect.height / 2);
|
|
|
|
container.scrollTo({
|
|
left,
|
|
top,
|
|
behavior: 'smooth'
|
|
});
|
|
},
|
|
|
|
clearSignatureButtons() {
|
|
this.signatureButtons.forEach(button => button.remove());
|
|
this.signatureButtons = [];
|
|
},
|
|
|
|
async renderSignatureButtons(signatures, currentPageNum, dotNetRef) {
|
|
this.clearSignatureButtons();
|
|
|
|
if (!this.pdfDoc || !signatures || signatures.length === 0) {
|
|
return;
|
|
}
|
|
|
|
this.dotNetReference = dotNetRef;
|
|
this._allSignatures = signatures;
|
|
this.pageNum = currentPageNum;
|
|
this.ensureScrollBindings();
|
|
|
|
const metrics = await this.getCurrentPageMetrics(currentPageNum);
|
|
if (!metrics) {
|
|
if (this.retryRenderHandle) {
|
|
clearTimeout(this.retryRenderHandle);
|
|
}
|
|
this.retryRenderHandle = setTimeout(() => {
|
|
this.retryRenderHandle = 0;
|
|
this.renderSignatureButtons(this._allSignatures, this.pageNum, this.dotNetReference);
|
|
}, 150);
|
|
return;
|
|
}
|
|
|
|
const overlayLayer = this.getOverlayLayer();
|
|
if (!overlayLayer) {
|
|
return;
|
|
}
|
|
|
|
this.updateAppliedSignaturePositions(metrics, currentPageNum);
|
|
|
|
const appliedIds = new Set(this.appliedSignatures.map(s => s.id));
|
|
const pageSignatures = signatures.filter(sig => sig.page === currentPageNum && !appliedIds.has(sig.id));
|
|
|
|
pageSignatures.forEach(signature => {
|
|
const button = this.createSignatureButton(signature, metrics);
|
|
overlayLayer.appendChild(button);
|
|
this.signatureButtons.push(button);
|
|
});
|
|
},
|
|
|
|
createSignatureButton(signature, metrics) {
|
|
const button = document.createElement('button');
|
|
button.className = 'signature-button';
|
|
button.setAttribute('data-signature-id', signature.id);
|
|
button.setAttribute('type', 'button');
|
|
button.setAttribute('tabindex', '0');
|
|
|
|
const textDiv = document.createElement('div');
|
|
textDiv.className = 'signature-button__text';
|
|
textDiv.textContent = 'Unterschreiben';
|
|
|
|
const svgNS = 'http://www.w3.org/2000/svg';
|
|
const svg = document.createElementNS(svgNS, 'svg');
|
|
svg.setAttribute('viewBox', '0 8 32 36');
|
|
svg.setAttribute('fill', 'none');
|
|
|
|
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);
|
|
|
|
this.positionSignatureButton(button, signature, metrics);
|
|
|
|
button.addEventListener('click', () => {
|
|
if (this.dotNetReference) {
|
|
this.dotNetReference.invokeMethodAsync('OnSignatureButtonClick', signature.id);
|
|
}
|
|
});
|
|
|
|
return button;
|
|
},
|
|
|
|
positionSignatureButton(button, signature, metrics) {
|
|
const scaleFactor = metrics.scale / 1.5;
|
|
const width = 150 * scaleFactor;
|
|
const height = 60 * scaleFactor;
|
|
const fontSize = Math.max(18 * scaleFactor, 10);
|
|
const iconSize = Math.max(24 * scaleFactor, 14);
|
|
|
|
button.style.position = 'absolute';
|
|
button.style.left = `${metrics.left + (signature.x * metrics.scaleX)}px`;
|
|
button.style.top = `${metrics.top + (signature.y * metrics.scaleY)}px`;
|
|
button.style.width = `${width}px`;
|
|
button.style.height = `${height}px`;
|
|
button.style.backgroundColor = '#4F46E5';
|
|
button.style.color = 'white';
|
|
button.style.border = 'none';
|
|
button.style.borderRadius = '8px';
|
|
button.style.cursor = 'pointer';
|
|
button.style.fontWeight = '600';
|
|
button.style.display = 'flex';
|
|
button.style.flexDirection = 'column';
|
|
button.style.alignItems = 'center';
|
|
button.style.justifyContent = 'center';
|
|
button.style.gap = `${Math.max(4 * scaleFactor, 2)}px`;
|
|
button.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
|
|
button.style.transition = 'all 0.2s ease';
|
|
button.style.zIndex = '100';
|
|
button.style.pointerEvents = 'auto';
|
|
|
|
const text = button.querySelector('.signature-button__text');
|
|
if (text) {
|
|
text.style.fontSize = `${fontSize}px`;
|
|
text.style.fontWeight = '700';
|
|
}
|
|
|
|
const icon = button.querySelector('svg');
|
|
if (icon) {
|
|
icon.setAttribute('width', iconSize);
|
|
icon.setAttribute('height', iconSize);
|
|
icon.style.filter = 'drop-shadow(0 1px 2px rgba(0,0,0,0.2))';
|
|
}
|
|
},
|
|
|
|
scaleAppliedSignature(container, metrics) {
|
|
const scaleFactor = metrics.scale / 1.5;
|
|
const width = 230 * scaleFactor;
|
|
const padding = 12 * scaleFactor;
|
|
const borderRadius = 6 * scaleFactor;
|
|
const fontSize = Math.max(9 * scaleFactor, 6);
|
|
const imageHeight = 70 * scaleFactor;
|
|
|
|
container.style.width = `${width}px`;
|
|
container.style.padding = `${padding}px`;
|
|
container.style.borderRadius = `${borderRadius}px`;
|
|
|
|
const infoContainer = container.querySelector('.signature-info-text');
|
|
if (infoContainer) {
|
|
infoContainer.style.fontSize = `${fontSize}px`;
|
|
}
|
|
|
|
const img = container.querySelector('img');
|
|
if (img) {
|
|
img.style.maxHeight = `${imageHeight}px`;
|
|
img.style.marginBottom = `${Math.max(6 * scaleFactor, 3)}px`;
|
|
}
|
|
|
|
const separator = container.querySelector('.applied-signature__separator');
|
|
if (separator) {
|
|
separator.style.marginBottom = `${Math.max(8 * scaleFactor, 4)}px`;
|
|
}
|
|
},
|
|
|
|
updateAppliedSignaturePositions(metrics, currentPageNum) {
|
|
this.appliedSignatureElements.forEach(container => {
|
|
const signatureId = parseInt(container.getAttribute('data-signature-id'));
|
|
const signature = this._allSignatures.find(s => s.id === signatureId);
|
|
if (!signature) {
|
|
return;
|
|
}
|
|
|
|
if (signature.page !== currentPageNum) {
|
|
container.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
container.style.display = '';
|
|
container.style.left = `${metrics.left + (signature.x * metrics.scaleX)}px`;
|
|
container.style.top = `${metrics.top + (signature.y * metrics.scaleY)}px`;
|
|
this.scaleAppliedSignature(container, metrics);
|
|
});
|
|
},
|
|
|
|
async applySignature(signatureId, signatureDataUrl, fullName, position, place) {
|
|
try {
|
|
const overlayLayer = this.getOverlayLayer();
|
|
if (!overlayLayer) {
|
|
return;
|
|
}
|
|
|
|
const buttonIndex = this.signatureButtons.findIndex(btn => btn.getAttribute('data-signature-id') == signatureId);
|
|
if (buttonIndex !== -1) {
|
|
this.signatureButtons[buttonIndex].remove();
|
|
this.signatureButtons.splice(buttonIndex, 1);
|
|
}
|
|
|
|
const signature = this._allSignatures.find(s => s.id === signatureId);
|
|
if (!signature) {
|
|
return;
|
|
}
|
|
|
|
if (!this.appliedSignatures.some(s => s.id === signatureId)) {
|
|
this.appliedSignatures.push({ id: signatureId, page: signature.page });
|
|
}
|
|
|
|
let container = this.appliedSignatureElements.find(el => parseInt(el.getAttribute('data-signature-id')) === signatureId);
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
container.className = 'applied-signature';
|
|
container.setAttribute('data-signature-id', signatureId);
|
|
container.style.position = 'absolute';
|
|
container.style.backgroundColor = '#f8f9fa';
|
|
container.style.border = '1px solid #dee2e6';
|
|
container.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
|
|
container.style.fontFamily = "'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
|
container.style.zIndex = '110';
|
|
container.style.pointerEvents = 'none';
|
|
|
|
const signatureImg = document.createElement('img');
|
|
signatureImg.src = signatureDataUrl;
|
|
signatureImg.alt = 'Unterschrift';
|
|
signatureImg.style.width = '100%';
|
|
signatureImg.style.height = 'auto';
|
|
signatureImg.style.display = 'block';
|
|
signatureImg.style.objectFit = 'contain';
|
|
|
|
const separator = document.createElement('div');
|
|
separator.className = 'applied-signature__separator';
|
|
separator.style.width = '100%';
|
|
separator.style.height = '1px';
|
|
separator.style.backgroundColor = '#495057';
|
|
|
|
const infoContainer = document.createElement('div');
|
|
infoContainer.className = 'signature-info-text';
|
|
infoContainer.style.lineHeight = '1.4';
|
|
infoContainer.style.color = '#495057';
|
|
infoContainer.style.fontWeight = '400';
|
|
|
|
const today = new Date();
|
|
const dateStr = today.toLocaleDateString('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
});
|
|
|
|
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>');
|
|
|
|
container.appendChild(signatureImg);
|
|
container.appendChild(separator);
|
|
container.appendChild(infoContainer);
|
|
overlayLayer.appendChild(container);
|
|
this.appliedSignatureElements.push(container);
|
|
}
|
|
|
|
this._lastViewedSignatureId = signatureId;
|
|
this.requestOverlayRefresh();
|
|
} catch (error) {
|
|
console.error('Error applying signature:', error);
|
|
}
|
|
},
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
},
|
|
|
|
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');
|
|
this.requestOverlayRefresh();
|
|
}
|
|
};
|
|
|
|
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;
|
|
},
|
|
|
|
isResizing: false,
|
|
resizeMouseMoveHandler: null,
|
|
resizeMouseUpHandler: null,
|
|
|
|
startResize() {
|
|
this.isResizing = true;
|
|
},
|
|
|
|
dispose() {
|
|
const host = this.getViewerHost();
|
|
if (host && this.wheelHandler) {
|
|
host.removeEventListener('wheel', this.wheelHandler);
|
|
this.wheelHandler = null;
|
|
}
|
|
|
|
if (this.resizeObserver) {
|
|
this.resizeObserver.disconnect();
|
|
this.resizeObserver = null;
|
|
}
|
|
|
|
if (this.overlayRefreshHandle) {
|
|
cancelAnimationFrame(this.overlayRefreshHandle);
|
|
this.overlayRefreshHandle = 0;
|
|
}
|
|
|
|
if (this.retryRenderHandle) {
|
|
clearTimeout(this.retryRenderHandle);
|
|
this.retryRenderHandle = 0;
|
|
}
|
|
|
|
this.detachResizeListeners();
|
|
this.clearSignatureButtons();
|
|
this.appliedSignatureElements.forEach(element => element.remove());
|
|
this.appliedSignatureElements = [];
|
|
this.appliedSignatures = [];
|
|
this._allSignatures = [];
|
|
this._lastViewedSignatureId = null;
|
|
this.scrollBoundElements = new WeakSet();
|
|
this.dotNetReference = null;
|
|
},
|
|
|
|
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) {
|
|
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;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|