diff --git a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor index 66af967d..38df32f6 100644 --- a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor +++ b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor @@ -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 @@
+
@@ -186,6 +189,7 @@ int _totalPages = 0; int _currentZoom = 150; bool _showThumbnails = true; DotNetObjectReference? _dotNetRef; +IReadOnlyList _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("pdfViewer.nextPage")) { _currentPage = await JSRuntime.InvokeAsync("pdfViewer.getCurrentPage"); + await RenderSignatureButtonsAsync(); } } async Task PreviousPage() { if (await JSRuntime.InvokeAsync("pdfViewer.previousPage")) { _currentPage = await JSRuntime.InvokeAsync("pdfViewer.getCurrentPage"); + await RenderSignatureButtonsAsync(); } } @@ -355,9 +370,25 @@ protected override async Task OnInitializedAsync() { async Task GoToPageFromThumbnail(int pageNum) { if (await JSRuntime.InvokeAsync("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; diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css b/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css index cfd7408a..ae201187 100644 --- a/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css +++ b/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css @@ -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; diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js b/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js index 92182b56..493deb70 100644 --- a/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js +++ b/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js @@ -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 = []; } }; +