Add interactive signature buttons to PDF viewer

Introduced functionality to render interactive signature buttons on the PDF viewer. Added support for fetching and displaying signature data (`SignatureDto`) dynamically based on the current page.

- Added `@using` directives in `EnvelopeViewer.razor` for required namespaces.
- Introduced `_signatures` field to store signature data.
- Updated `OnInitializedAsync` to fetch and process signatures.
- Implemented `RenderSignatureButtonsAsync` to dynamically render buttons.
- Added `[JSInvokable]` method `OnSignatureButtonClick` for button events.
- Updated CSS to style `pdf-signature-layer` and `signature-button`.
- Enhanced `pdf-viewer.js` with methods to render and clear buttons.
- Ensured buttons respond to zoom and page navigation changes.
- Added error handling and logging for signature rendering.

These changes improve user interaction by enabling signing functionality directly on the PDF viewer.
This commit is contained in:
2026-06-07 12:43:36 +02:00
parent b888c85937
commit 2f73e4f6da
3 changed files with 205 additions and 2 deletions

View File

@@ -1,4 +1,6 @@
@page "/envelope/{EnvelopeKey}" @page "/envelope/{EnvelopeKey}"
@using EnvelopeGenerator.ReceiverUI.Models
@using EnvelopeGenerator.ReceiverUI.Models.Constants
@using EnvelopeGenerator.ReceiverUI.Services @using EnvelopeGenerator.ReceiverUI.Services
@using Microsoft.Extensions.Options @using Microsoft.Extensions.Options
@using EnvelopeGenerator.ReceiverUI.Options @using EnvelopeGenerator.ReceiverUI.Options
@@ -155,6 +157,7 @@
<div class="pdf-page-container"> <div class="pdf-page-container">
<canvas id="pdf-canvas" class="pdf-canvas"></canvas> <canvas id="pdf-canvas" class="pdf-canvas"></canvas>
<div id="pdf-text-layer" class="pdf-text-layer"></div> <div id="pdf-text-layer" class="pdf-text-layer"></div>
<div id="pdf-signature-layer" class="pdf-signature-layer"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -186,6 +189,7 @@ int _totalPages = 0;
int _currentZoom = 150; int _currentZoom = 150;
bool _showThumbnails = true; bool _showThumbnails = true;
DotNetObjectReference<EnvelopeViewer>? _dotNetRef; DotNetObjectReference<EnvelopeViewer>? _dotNetRef;
IReadOnlyList<SignatureDto> _signatures = [];
// Resizable splitter state // Resizable splitter state
int _thumbnailWidth = 260; int _thumbnailWidth = 260;
@@ -195,7 +199,7 @@ int _resizeStartWidth = 0;
const int MinThumbnailWidth = 150; const int MinThumbnailWidth = 150;
const int MaxThumbnailWidth = 400; const int MaxThumbnailWidth = 400;
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {
if (string.IsNullOrWhiteSpace(EnvelopeKey)) { if (string.IsNullOrWhiteSpace(EnvelopeKey)) {
_errorMessage = "Envelope-Schlüssel fehlt."; _errorMessage = "Envelope-Schlüssel fehlt.";
_isLoading = false; _isLoading = false;
@@ -213,8 +217,9 @@ protected override async Task OnInitializedAsync() {
} }
var signatures = await SignatureService.GetAsync(EnvelopeKey); var signatures = await SignatureService.GetAsync(EnvelopeKey);
_signatures = signatures.Convert(UnitOfLength.Point);
await JSRuntime.InvokeVoidAsync("console.log", signatures); await JSRuntime.InvokeVoidAsync("console.log", "Loaded signatures:", _signatures);
} catch (Exception ex) { } catch (Exception ex) {
_errorMessage = $"Fehler: {ex.Message}"; _errorMessage = $"Fehler: {ex.Message}";
@@ -269,11 +274,15 @@ protected override async Task OnInitializedAsync() {
// Attach resize listeners // Attach resize listeners
await JSRuntime.InvokeVoidAsync("pdfViewer.attachResizeListeners", _dotNetRef); await JSRuntime.InvokeVoidAsync("pdfViewer.attachResizeListeners", _dotNetRef);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
// Wait for DOM to be ready, then render thumbnails // Wait for DOM to be ready, then render thumbnails
await Task.Delay(100); await Task.Delay(100);
await RenderThumbnailsAsync(); await RenderThumbnailsAsync();
// Render signature buttons
await RenderSignatureButtonsAsync();
} }
} catch (Exception ex) { } catch (Exception ex) {
_errorMessage = $"PDF.js Fehler: {ex.Message}"; _errorMessage = $"PDF.js Fehler: {ex.Message}";
@@ -287,17 +296,23 @@ protected override async Task OnInitializedAsync() {
{ {
_currentZoom = (int)(scale * 100); _currentZoom = (int)(scale * 100);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
// Re-render signature buttons when zoom changes
await Task.Delay(100);
await RenderSignatureButtonsAsync();
} }
async Task NextPage() { async Task NextPage() {
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.nextPage")) { if (await JSRuntime.InvokeAsync<bool>("pdfViewer.nextPage")) {
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage"); _currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
await RenderSignatureButtonsAsync();
} }
} }
async Task PreviousPage() { async Task PreviousPage() {
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.previousPage")) { if (await JSRuntime.InvokeAsync<bool>("pdfViewer.previousPage")) {
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage"); _currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
await RenderSignatureButtonsAsync();
} }
} }
@@ -355,9 +370,25 @@ protected override async Task OnInitializedAsync() {
async Task GoToPageFromThumbnail(int pageNum) { async Task GoToPageFromThumbnail(int pageNum) {
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.goToPage", pageNum)) { if (await JSRuntime.InvokeAsync<bool>("pdfViewer.goToPage", pageNum)) {
_currentPage = pageNum; _currentPage = pageNum;
await RenderSignatureButtonsAsync();
} }
} }
async Task RenderSignatureButtonsAsync() {
if (_signatures.Count == 0 || !_pdfLoaded) return;
try {
await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons", _signatures, _currentPage, _dotNetRef);
} catch (Exception ex) {
System.Diagnostics.Debug.WriteLine($"Signature button rendering error: {ex.Message}");
}
}
[JSInvokable]
public void OnSignatureButtonClick(int signatureId) {
Console.WriteLine($"Signature #{signatureId} signed");
}
async Task RenderThumbnailsAsync() { async Task RenderThumbnailsAsync() {
try { try {
var delay = PdfViewerOptions.Value.ThumbnailRenderDelay; var delay = PdfViewerOptions.Value.ThumbnailRenderDelay;

View File

@@ -480,6 +480,34 @@ body.resizing {
background: rgba(126, 34, 206, 0.3); background: rgba(126, 34, 206, 0.3);
} }
.pdf-signature-layer {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: visible;
pointer-events: none;
z-index: 20;
}
.pdf-signature-layer .signature-button {
pointer-events: auto;
}
.signature-button {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.signature-button:focus {
outline: 2px solid #7e22ce;
outline-offset: 2px;
}
.signature-button:active {
transform: scale(0.98);
}
.error-container { .error-container {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -451,6 +451,150 @@ window.pdfViewer = {
startResize() { startResize() {
this.isResizing = true; this.isResizing = true;
},
// Signature button functionality
signatureButtons: [],
/**
* 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
this.clearSignatureButtons();
if (!this.pdfDoc || !signatures || signatures.length === 0) {
return;
}
this.dotNetReference = dotNetRef;
try {
// Filter signatures for current page
const pageSignatures = signatures.filter(sig => sig.page === currentPageNum);
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`;
// Create button for each 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);
// 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 = '150px';
button.style.height = '60px';
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 = 'Sign';
textDiv.style.fontSize = '18px';
textDiv.style.fontWeight = '700';
// Add SVG icon
const svgNS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(svgNS, 'svg');
svg.setAttribute('width', '24');
svg.setAttribute('height', '24');
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);
}
},
/**
* Clears all signature buttons from the canvas.
*/
clearSignatureButtons() {
this.signatureButtons.forEach(button => {
if (button.parentNode) {
button.parentNode.removeChild(button);
}
});
this.signatureButtons = [];
} }
}; };