From 9fa8ef29d8e0853498cf647fd18dacc10b5266d0 Mon Sep 17 00:00:00 2001 From: TekH Date: Sat, 6 Jun 2026 00:38:27 +0200 Subject: [PATCH] Add resizable thumbnail sidebar to EnvelopeViewer Introduced a resizable splitter for the PDF thumbnail sidebar, allowing users to dynamically adjust its width. Added `_thumbnailWidth` property with min/max constraints and implemented mouse event handlers (`OnSplitterMouseDown`, `OnSplitterMouseMove`, `OnSplitterMouseUp`) to manage resizing. Integrated JavaScript interop to attach/detach resize event listeners and save user preferences to `localStorage`. Updated `pdf-viewer.js` to handle resizing state and cleanup. Styled the splitter in `envelope-viewer.css` with hover/active states and ensured smooth interaction. Persisted thumbnail width across sessions and added error handling for `localStorage`. Enhanced user experience with intuitive resizing and improved UI flexibility. --- .../Pages/EnvelopeViewer.razor | 93 ++++++++++++++++--- .../wwwroot/css/envelope-viewer.css | 40 ++++++++ .../wwwroot/js/pdf-viewer.js | 42 +++++++++ 3 files changed, 163 insertions(+), 12 deletions(-) diff --git a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor index 8253d971..9efca85d 100644 --- a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor +++ b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor @@ -130,7 +130,7 @@
@if (_pdfLoaded && _showThumbnails) { -
+
@for (int i = 1; i <= _totalPages; i++) { var pageNum = i; @@ -143,6 +143,11 @@ }
+ +
+
}
@@ -165,19 +170,27 @@
@code { - [Parameter] public string? EnvelopeKey { get; set; } +[Parameter] public string? EnvelopeKey { get; set; } - bool _isLoading = true; - string? _errorMessage; - string? _pdfDataUrl; - bool _pdfLoaded = false; - int _currentPage = 1; - int _totalPages = 0; - int _currentZoom = 150; - bool _showThumbnails = true; - DotNetObjectReference? _dotNetRef; +bool _isLoading = true; +string? _errorMessage; +string? _pdfDataUrl; +bool _pdfLoaded = false; +int _currentPage = 1; +int _totalPages = 0; +int _currentZoom = 150; +bool _showThumbnails = true; +DotNetObjectReference? _dotNetRef; - protected override async Task OnInitializedAsync() { +// Resizable splitter state +int _thumbnailWidth = 260; +bool _isResizing = false; +int _resizeStartX = 0; +int _resizeStartWidth = 0; +const int MinThumbnailWidth = 150; +const int MaxThumbnailWidth = 400; + +protected override async Task OnInitializedAsync() { if (string.IsNullOrWhiteSpace(EnvelopeKey)) { _errorMessage = "Envelope-Schlüssel fehlt."; _isLoading = false; @@ -205,6 +218,19 @@ } protected override async Task OnAfterRenderAsync(bool firstRender) { + if (firstRender) { + // Load saved thumbnail width from localStorage + try { + var savedWidth = await JSRuntime.InvokeAsync("localStorage.getItem", "envelopeViewer_thumbnailWidth"); + if (!string.IsNullOrEmpty(savedWidth) && int.TryParse(savedWidth, out var width)) { + _thumbnailWidth = Math.Clamp(width, MinThumbnailWidth, MaxThumbnailWidth); + await InvokeAsync(StateHasChanged); + } + } catch { + // Ignore localStorage errors + } + } + if (!_pdfLoaded && !string.IsNullOrWhiteSpace(_pdfDataUrl)) { await Task.Delay(500); @@ -216,6 +242,10 @@ _pdfLoaded = true; _totalPages = await JSRuntime.InvokeAsync("pdfViewer.getTotalPages"); _currentPage = await JSRuntime.InvokeAsync("pdfViewer.getCurrentPage"); + + // Attach resize listeners + await JSRuntime.InvokeVoidAsync("pdfViewer.attachResizeListeners", _dotNetRef); + await InvokeAsync(StateHasChanged); // Wait for DOM to be ready, then render thumbnails @@ -315,6 +345,45 @@ } } + // Resizable splitter methods + void OnSplitterMouseDown(MouseEventArgs e) { + _isResizing = true; + _resizeStartX = (int)e.ClientX; + _resizeStartWidth = _thumbnailWidth; + + // Add resizing class to body to prevent text selection + _ = JSRuntime.InvokeVoidAsync("eval", "document.body.classList.add('resizing')"); + _ = JSRuntime.InvokeVoidAsync("pdfViewer.startResize"); + } + + [JSInvokable] + public async Task OnSplitterMouseMove(int clientX) { + if (!_isResizing) return; + + var delta = clientX - _resizeStartX; + var newWidth = _resizeStartWidth + delta; + + // Clamp to min/max + _thumbnailWidth = Math.Clamp(newWidth, MinThumbnailWidth, MaxThumbnailWidth); + + await InvokeAsync(StateHasChanged); + } + + [JSInvokable] + public async Task OnSplitterMouseUp() { + if (!_isResizing) return; + + _isResizing = false; + + // Remove resizing class from body + await JSRuntime.InvokeVoidAsync("eval", "document.body.classList.remove('resizing')"); + + // Save preference to localStorage + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "envelopeViewer_thumbnailWidth", _thumbnailWidth.ToString()); + + await InvokeAsync(StateHasChanged); + } + public async ValueTask DisposeAsync() { if (_pdfLoaded) { try { diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css b/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css index 1b532cfb..82179019 100644 --- a/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css +++ b/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css @@ -104,6 +104,46 @@ background: linear-gradient(135deg, #6b1cb0 0%, #1e3a72 100%); } +.pdf-splitter { + width: 4px; + background: transparent; + cursor: col-resize; + flex-shrink: 0; + position: relative; + transition: background 0.2s ease; + z-index: 10; + user-select: none; +} + +.pdf-splitter::before { + content: ''; + position: absolute; + left: -4px; + right: -4px; + top: 0; + bottom: 0; + /* Enlarged hitbox for easier grabbing */ +} + +.pdf-splitter:hover, +.pdf-splitter.resizing { + background: linear-gradient(90deg, + rgba(126, 34, 206, 0.4) 0%, + rgba(42, 82, 152, 0.4) 100%); +} + +.pdf-splitter:active { + background: linear-gradient(90deg, + rgba(126, 34, 206, 0.6) 0%, + rgba(42, 82, 152, 0.6) 100%); +} + +/* Prevent text selection during resize */ +body.resizing { + user-select: none; + cursor: col-resize !important; +} + .pdf-thumbnail { cursor: pointer; border-radius: 8px; diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js b/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js index 91c8f650..815a66e2 100644 --- a/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js +++ b/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js @@ -289,6 +289,48 @@ window.pdfViewer = { this.wheelEventAttached = false; this.dotNetReference = null; } + this.detachResizeListeners(); + }, + + // Resizable splitter functionality + isResizing: false, + resizeMouseMoveHandler: null, + resizeMouseUpHandler: null, + + attachResizeListeners(dotNetRef) { + this.dotNetReference = dotNetRef; + + this.resizeMouseMoveHandler = (e) => { + if (this.isResizing && this.dotNetReference) { + this.dotNetReference.invokeMethodAsync('OnSplitterMouseMove', e.clientX); + } + }; + + this.resizeMouseUpHandler = () => { + if (this.isResizing && this.dotNetReference) { + this.isResizing = false; + this.dotNetReference.invokeMethodAsync('OnSplitterMouseUp'); + } + }; + + document.addEventListener('mousemove', this.resizeMouseMoveHandler); + document.addEventListener('mouseup', this.resizeMouseUpHandler); + }, + + detachResizeListeners() { + if (this.resizeMouseMoveHandler) { + document.removeEventListener('mousemove', this.resizeMouseMoveHandler); + this.resizeMouseMoveHandler = null; + } + if (this.resizeMouseUpHandler) { + document.removeEventListener('mouseup', this.resizeMouseUpHandler); + this.resizeMouseUpHandler = null; + } + this.isResizing = false; + }, + + startResize() { + this.isResizing = true; } };