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

View File

@@ -480,6 +480,34 @@ body.resizing {
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 {
display: flex;
align-items: center;

View File

@@ -451,6 +451,150 @@ window.pdfViewer = {
startResize() {
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 = [];
}
};