From 0a22e4e5cc952419ab0f8a5a13a55329c91b41ac Mon Sep 17 00:00:00 2001 From: TekH Date: Thu, 28 May 2026 14:39:38 +0200 Subject: [PATCH] Enhance signature popup with multiple input modes Refactored `@using` directives in `ReportViewer.razor` to use specific aliases for improved readability. Updated the signature popup to support three input modes: "Draw", "Text", and "Image", with a tabbed interface for switching between modes. Added font selection for typed signatures and improved state management for signature modes. Refactored JavaScript interop methods to handle initialization, clearing, and data retrieval for each signature mode. Enhanced `receiver-signature.js` to support typed and image-based signatures, including rendering text-based signatures and handling image uploads with scaling and centering. Improved maintainability by modularizing code and extracting reusable functions. Enhanced user experience with dynamic UI updates and better handling of signature state changes. --- .../Pages/ReportViewer.razor | 128 +++++++++++++++- .../wwwroot/js/receiver-signature.js | 139 +++++++++++++++++- 2 files changed, 257 insertions(+), 10 deletions(-) diff --git a/EnvelopeGenerator.ReceiverUI/Pages/ReportViewer.razor b/EnvelopeGenerator.ReceiverUI/Pages/ReportViewer.razor index c67077be..a33ed909 100644 --- a/EnvelopeGenerator.ReceiverUI/Pages/ReportViewer.razor +++ b/EnvelopeGenerator.ReceiverUI/Pages/ReportViewer.razor @@ -4,7 +4,12 @@ @using DevExpress.Utils @using DevExpress.XtraPrinting @using DevExpress.XtraPrinting.Drawing -@using DevExpress.XtraReports.UI; +@using XtraReport = DevExpress.XtraReports.UI.XtraReport +@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand +@using XRLabel = DevExpress.XtraReports.UI.XRLabel +@using XRPictureBox = DevExpress.XtraReports.UI.XRPictureBox +@using XRControl = DevExpress.XtraReports.UI.XRControl +@using ImageSizeMode = DevExpress.XtraPrinting.ImageSizeMode @using EnvelopeGenerator.ReceiverUI.Services; @inject IJSRuntime JSRuntime @inject InMemoryReportStorageWebExtension ReportStorage @@ -36,13 +41,47 @@ -

Bitte unterschreiben Sie im folgenden Feld.

- + + + @if(ActiveSignatureTab == SignatureTabDraw) { +

Bitte unterschreiben Sie im folgenden Feld.

+ + } else if(ActiveSignatureTab == SignatureTabText) { +

Geben Sie Ihre Unterschrift als Text ein und waehlen Sie eine Schriftart.

+
+
+ +
+
+ +
+
+ + } else { +

Laden Sie ein Bild Ihrer Unterschrift hoch.

+ + + } + @if(!string.IsNullOrWhiteSpace(PopupValidationMessage)) {
@PopupValidationMessage
} @@ -61,12 +100,31 @@ } @code { + const string SignatureTabDraw = "draw"; + const string SignatureTabText = "text"; + const string SignatureTabImage = "image"; + const string DrawCanvasId = "receiver-signature-pad"; + const string TypedCanvasId = "receiver-typed-signature-pad"; + const string ImageInputId = "receiver-signature-image-input"; + const string ImageCanvasId = "receiver-image-signature-pad"; + + readonly (string Text, string Value)[] TypedSignatureFonts = { + ("Brush Script", "'Brush Script MT', cursive"), + ("Segoe Script", "'Segoe Script', cursive"), + ("Lucida Handwriting", "'Lucida Handwriting', cursive"), + ("Comic Sans", "'Comic Sans MS', cursive"), + ("Cursive", "cursive") + }; + DxReportViewer reportViewer; XtraReport? Report; bool SignatureApplied; bool SignaturePopupVisible; string? SignatureValidationMessage; string? PopupValidationMessage; + string ActiveSignatureTab = SignatureTabDraw; + string TypedSignatureText = string.Empty; + string TypedSignatureFont = "'Brush Script MT', cursive"; int ViewerKey; protected override async Task OnInitializedAsync() { @@ -76,17 +134,45 @@ } async Task OpenSignaturePopupAsync() { + ActiveSignatureTab = SignatureTabDraw; SignaturePopupVisible = true; SignatureValidationMessage = null; PopupValidationMessage = null; await InvokeAsync(StateHasChanged); await Task.Delay(50); - await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", "receiver-signature-pad"); + await InitializeActiveSignatureTabAsync(); + } + + async Task SetSignatureTabAsync(string tab) { + ActiveSignatureTab = tab; + PopupValidationMessage = null; + await InvokeAsync(StateHasChanged); + await Task.Delay(50); + await InitializeActiveSignatureTabAsync(); + } + + async Task InitializeActiveSignatureTabAsync() { + if(ActiveSignatureTab == SignatureTabDraw) { + await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", DrawCanvasId); + } else if(ActiveSignatureTab == SignatureTabText) { + await JSRuntime.InvokeVoidAsync("receiverSignature.initializeTyped", TypedCanvasId); + await RenderTypedSignatureAsync(); + } else { + await JSRuntime.InvokeVoidAsync("receiverSignature.initializeImage", ImageInputId, ImageCanvasId); + } } async Task RenewSignatureAsync() { PopupValidationMessage = null; - await JSRuntime.InvokeVoidAsync("receiverSignature.clear", "receiver-signature-pad"); + + if(ActiveSignatureTab == SignatureTabDraw) { + await JSRuntime.InvokeVoidAsync("receiverSignature.clear", DrawCanvasId); + } else if(ActiveSignatureTab == SignatureTabText) { + TypedSignatureText = string.Empty; + await JSRuntime.InvokeVoidAsync("receiverSignature.clearTyped", TypedCanvasId); + } else { + await JSRuntime.InvokeVoidAsync("receiverSignature.clearImage", ImageInputId, ImageCanvasId); + } } void CloseSignaturePopup() { @@ -94,8 +180,22 @@ SignaturePopupVisible = false; } + async Task OnTypedSignatureChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) { + TypedSignatureText = args.Value?.ToString() ?? string.Empty; + await RenderTypedSignatureAsync(); + } + + async Task OnTypedSignatureFontChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) { + TypedSignatureFont = args.Value?.ToString() ?? TypedSignatureFont; + await RenderTypedSignatureAsync(); + } + + async Task RenderTypedSignatureAsync() { + await JSRuntime.InvokeVoidAsync("receiverSignature.renderTypedSignature", TypedCanvasId, TypedSignatureText, TypedSignatureFont); + } + async Task ApplySignatureAsync() { - var signatureDataUrl = await JSRuntime.InvokeAsync("receiverSignature.getDataUrl", "receiver-signature-pad"); + var signatureDataUrl = await GetActiveSignatureDataUrlAsync(); if(string.IsNullOrWhiteSpace(signatureDataUrl)) { PopupValidationMessage = "Die Unterschrift ist fuer den PDF-Export erforderlich."; @@ -110,6 +210,18 @@ ViewerKey++; } + async Task GetActiveSignatureDataUrlAsync() { + if(ActiveSignatureTab == SignatureTabDraw) + return await JSRuntime.InvokeAsync("receiverSignature.getDataUrl", DrawCanvasId); + + if(ActiveSignatureTab == SignatureTabText) { + await RenderTypedSignatureAsync(); + return await JSRuntime.InvokeAsync("receiverSignature.getTypedDataUrl", TypedCanvasId); + } + + return await JSRuntime.InvokeAsync("receiverSignature.getImageDataUrl", ImageCanvasId); + } + async Task ExportSignedPdfAsync() { if(!SignatureApplied || Report is null) { SignatureValidationMessage = "Bitte fuegen Sie die Unterschrift zuerst zum Bericht hinzu."; @@ -180,4 +292,4 @@ foreach(var control in controls) bottomMargin.Controls.Remove(control); } -} \ No newline at end of file +} diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/js/receiver-signature.js b/EnvelopeGenerator.ReceiverUI/wwwroot/js/receiver-signature.js index e30ba8cf..4b700448 100644 --- a/EnvelopeGenerator.ReceiverUI/wwwroot/js/receiver-signature.js +++ b/EnvelopeGenerator.ReceiverUI/wwwroot/js/receiver-signature.js @@ -1,5 +1,7 @@ window.receiverSignature = (() => { const pads = new Map(); + const typedSignatures = new Map(); + const imageSignatures = new Map(); function getPosition(canvas, event) { const rect = canvas.getBoundingClientRect(); @@ -10,6 +12,10 @@ window.receiverSignature = (() => { }; } + function clearCanvas(canvas) { + canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height); + } + function initialize(canvasId) { const canvas = document.getElementById(canvasId); if (!canvas || pads.has(canvasId)) @@ -59,16 +65,120 @@ window.receiverSignature = (() => { canvas.addEventListener('touchend', end, { passive: false }); } + function initializeTyped(canvasId) { + const canvas = document.getElementById(canvasId); + if (!canvas || typedSignatures.has(canvasId)) + return; + + typedSignatures.set(canvasId, { hasSignature: false }); + } + + function initializeImage(inputId, canvasId) { + const input = document.getElementById(inputId); + const canvas = document.getElementById(canvasId); + if (!input || !canvas || imageSignatures.has(canvasId)) + return; + + const state = { hasSignature: false }; + imageSignatures.set(canvasId, state); + + input.addEventListener('change', () => { + const file = input.files && input.files.length ? input.files[0] : null; + if (!file || !file.type.startsWith('image/')) { + clearCanvas(canvas); + state.hasSignature = false; + return; + } + + const reader = new FileReader(); + reader.onload = () => { + const image = new Image(); + image.onload = () => { + const context = canvas.getContext('2d'); + clearCanvas(canvas); + + const padding = 10; + const maxWidth = canvas.width - padding * 2; + const maxHeight = canvas.height - padding * 2; + const scale = Math.min(maxWidth / image.width, maxHeight / image.height, 1); + const width = image.width * scale; + const height = image.height * scale; + const x = (canvas.width - width) / 2; + const y = (canvas.height - height) / 2; + + context.drawImage(image, x, y, width, height); + state.hasSignature = true; + }; + image.src = reader.result; + }; + reader.readAsDataURL(file); + }); + } + function clear(canvasId) { const canvas = document.getElementById(canvasId); const state = pads.get(canvasId); if (!canvas || !state) return; - canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height); + clearCanvas(canvas); state.hasSignature = false; } + function clearTyped(canvasId) { + const canvas = document.getElementById(canvasId); + const state = typedSignatures.get(canvasId); + if (!canvas || !state) + return; + + clearCanvas(canvas); + state.hasSignature = false; + } + + function clearImage(inputId, canvasId) { + const input = document.getElementById(inputId); + const canvas = document.getElementById(canvasId); + const state = imageSignatures.get(canvasId); + if (!canvas || !state) + return; + + if (input) + input.value = ''; + + clearCanvas(canvas); + state.hasSignature = false; + } + + function renderTypedSignature(canvasId, text, fontFamily) { + const canvas = document.getElementById(canvasId); + const state = typedSignatures.get(canvasId); + if (!canvas || !state) + return; + + const value = (text || '').trim(); + clearCanvas(canvas); + + if (!value) { + state.hasSignature = false; + return; + } + + const context = canvas.getContext('2d'); + const maxWidth = canvas.width - 30; + let fontSize = 54; + + do { + context.font = `italic ${fontSize}px ${fontFamily || 'cursive'}`; + fontSize -= 2; + } while (context.measureText(value).width > maxWidth && fontSize > 24); + + context.fillStyle = '#111'; + context.textBaseline = 'middle'; + context.textAlign = 'center'; + context.fillText(value, canvas.width / 2, canvas.height / 2); + state.hasSignature = true; + } + function getDataUrl(canvasId) { const canvas = document.getElementById(canvasId); const state = pads.get(canvasId); @@ -78,9 +188,34 @@ window.receiverSignature = (() => { return canvas.toDataURL('image/png'); } + function getTypedDataUrl(canvasId) { + const canvas = document.getElementById(canvasId); + const state = typedSignatures.get(canvasId); + if (!canvas || !state || !state.hasSignature) + return null; + + return canvas.toDataURL('image/png'); + } + + function getImageDataUrl(canvasId) { + const canvas = document.getElementById(canvasId); + const state = imageSignatures.get(canvasId); + if (!canvas || !state || !state.hasSignature) + return null; + + return canvas.toDataURL('image/png'); + } + return { initialize, + initializeTyped, + initializeImage, clear, - getDataUrl + clearTyped, + clearImage, + renderTypedSignature, + getDataUrl, + getTypedDataUrl, + getImageDataUrl }; })();