From 0c81d79a9bb252d3183dc88e977aaa8f96d6c910 Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 30 Sep 2026 09:08:17 +0200 Subject: [PATCH] Add drag-and-drop support for PDF field repositioning Introduced drag-and-drop functionality for fields in the PDF editor: - Added `SenderPdfFieldMoveRequest` record to encapsulate field movement data. - Enhanced `SenderPdfImageEditor.razor` with drag-and-drop event handlers (`OnFieldDragStart`, `OnPageDropAsync`) and updated field structure. - Updated `SenderEditorPage.razor` to handle field movement via the `OnMoveFieldAsync` method. - Improved CSS styles for draggable fields and added a delete button with hover effects. - Added `getDropCoordsOnPage` function in `envelope-editor.js` to calculate normalized drop coordinates. These changes improve the user experience by enabling intuitive field placement and management in the PDF editor. --- .../PdfEditor/SenderPdfFieldMoveRequest.cs | 7 ++ .../PdfEditor/SenderPdfImageEditor.razor | 75 ++++++++++++++++--- .../Pages/SenderEditorPage.razor | 18 +++++ .../wwwroot/css/envelope-viewer.css | 38 +++++++++- .../wwwroot/js/envelope-editor.js | 26 +++++++ 5 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/PdfEditor/SenderPdfFieldMoveRequest.cs diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/PdfEditor/SenderPdfFieldMoveRequest.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/PdfEditor/SenderPdfFieldMoveRequest.cs new file mode 100644 index 00000000..3df34e5a --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/PdfEditor/SenderPdfFieldMoveRequest.cs @@ -0,0 +1,7 @@ +namespace EnvelopeGenerator.Server.Client.Components.PdfEditor; + +public sealed record SenderPdfFieldMoveRequest( + int Index, + int Page, + double XPt, + double YPt); diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/PdfEditor/SenderPdfImageEditor.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/PdfEditor/SenderPdfImageEditor.razor index cc3a1555..3cc34bb7 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/PdfEditor/SenderPdfImageEditor.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/PdfEditor/SenderPdfImageEditor.razor @@ -2,6 +2,7 @@ @using EnvelopeGenerator.Application.Common.Dto @using Microsoft.Extensions.Localization @inject IStringLocalizer UiLocalizer +@inject IJSRuntime JSRuntime
-
+
@if (!string.IsNullOrWhiteSpace(pagePreview?.ImageDataUrl)) { f.Page == pageNumber)) { - +
+ @field.ReceiverName + +
}
@@ -86,6 +104,11 @@ [Parameter] public EventCallback RemoveField { get; set; } + [Parameter] + public EventCallback MoveField { get; set; } + + private int? _draggedFieldIndex; + private static string GetFieldStyle(SenderPdfFieldOverlay field, double pageWidthPt, double pageHeightPt) { var leftPercent = (field.XPt / pageWidthPt) * 100.0; @@ -108,6 +131,40 @@ await EditorClick.InvokeAsync(args); } + private void OnFieldDragStart(int fieldIndex) + { + _draggedFieldIndex = fieldIndex; + } + + private async Task OnPageDropAsync(DragEventArgs args, int pageNumber, double pageWidthPt, double pageHeightPt) + { + if (_draggedFieldIndex is null || !MoveField.HasDelegate) + return; + + var coords = await JSRuntime.InvokeAsync( + "envelopeEditor.getDropCoordsOnPage", + "sender-editor-pdf-viewer", + pageNumber, + args.ClientX, + args.ClientY); + + if (coords is null) + return; + + const double sigWidthPt = 1.77 * 72; + const double sigHeightPt = 1.96 * 72; + + var xPt = coords.NormX * pageWidthPt; + var yPt = coords.NormY * pageHeightPt; + xPt = Math.Max(0, Math.Min(xPt, pageWidthPt - sigWidthPt)); + yPt = Math.Max(0, Math.Min(yPt, pageHeightPt - sigHeightPt)); + + await MoveField.InvokeAsync(new SenderPdfFieldMoveRequest(_draggedFieldIndex.Value, pageNumber, xPt, yPt)); + _draggedFieldIndex = null; + } + + private sealed record DropCoords(double NormX, double NormY); + private string L(string key, string fallback) { var value = UiLocalizer[key]; diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderEditorPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderEditorPage.razor index 70dc8a0e..368444c8 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderEditorPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderEditorPage.razor @@ -5,6 +5,7 @@ @using EnvelopeGenerator.Application.Common.Dto @using EnvelopeGenerator.Application.Envelopes.Commands @using EnvelopeGenerator.Application.Resources +@using EnvelopeGenerator.Server.Client.Components.PdfEditor @using EnvelopeGenerator.Server.Client.Services @using Microsoft.AspNetCore.WebUtilities @using Microsoft.AspNetCore.Components.Forms @@ -336,6 +337,7 @@ PdfDataUrl="@PdfDataUrl" Fields="@FieldOverlays" EditorClick="OnPdfAreaClickAsync" + MoveField="OnMoveFieldAsync" RemoveField="OnRemoveFieldByIndexAsync" /> }
@@ -806,6 +808,22 @@ await Task.CompletedTask; } + async Task OnMoveFieldAsync(SenderPdfFieldMoveRequest request) + { + if (request.Index < 0 || request.Index >= _signatureFields.Count) + return; + + var field = _signatureFields[request.Index]; + _signatureFields[request.Index] = field with + { + XPt = request.XPt, + YPt = request.YPt, + Page = request.Page + }; + + await Task.CompletedTask; + } + // ── Clear all fields ── async Task ClearAllFieldsAsync() { diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css index 50768729..4bff7f79 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css @@ -134,13 +134,47 @@ padding: 0.2rem 0.3rem; text-align: left; line-height: 1.1; - cursor: pointer; + cursor: grab; display: flex; align-items: flex-start; - justify-content: flex-start; + justify-content: space-between; + gap: 0.35rem; overflow: hidden; } +.sender-image-editor__field:active { + cursor: grabbing; +} + +.sender-image-editor__field-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sender-image-editor__field-remove { + border: none; + background: rgba(15, 23, 42, 0.78); + color: #ffffff; + width: 1.1rem; + height: 1.1rem; + min-width: 1.1rem; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + cursor: pointer; + line-height: 1; + margin-left: auto; + flex-shrink: 0; +} + +.sender-image-editor__field-remove:hover { + background: rgba(220, 38, 38, 0.9); +} + .sender-image-editor__field:hover { filter: brightness(0.96); } diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/envelope-editor.js b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/envelope-editor.js index 5dabd3ab..ddd4cee3 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/envelope-editor.js +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/envelope-editor.js @@ -92,5 +92,31 @@ window.envelopeEditor = { normY: normY, pageIndex: targetIndex }; + }, + + getDropCoordsOnPage: function (viewerCssClass, pageNumber, clientX, clientY) { + const viewer = document.querySelector('.' + viewerCssClass); + if (!viewer) { + console.warn('[envelopeEditor] viewer not found for class:', viewerCssClass); + return null; + } + + const page = viewer.querySelector('[data-editor-page="' + pageNumber + '"]'); + if (!page) { + console.warn('[envelopeEditor] page not found:', pageNumber); + return null; + } + + const rect = page.getBoundingClientRect(); + const clampedX = Math.max(rect.left, Math.min(clientX, rect.right)); + const clampedY = Math.max(rect.top, Math.min(clientY, rect.bottom)); + + const normX = (clampedX - rect.left) / rect.width; + const normY = (clampedY - rect.top) / rect.height; + + return { + normX: normX, + normY: normY + }; } };