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.
This commit is contained in:
@@ -130,7 +130,7 @@
|
|||||||
<div class="pdf-frame">
|
<div class="pdf-frame">
|
||||||
@if (_pdfLoaded && _showThumbnails) {
|
@if (_pdfLoaded && _showThumbnails) {
|
||||||
<!-- PDF Thumbnail Sidebar -->
|
<!-- PDF Thumbnail Sidebar -->
|
||||||
<div class="pdf-thumbnails">
|
<div class="pdf-thumbnails" style="width: @(_thumbnailWidth)px">
|
||||||
<div class="pdf-thumbnails__content">
|
<div class="pdf-thumbnails__content">
|
||||||
@for (int i = 1; i <= _totalPages; i++) {
|
@for (int i = 1; i <= _totalPages; i++) {
|
||||||
var pageNum = i;
|
var pageNum = i;
|
||||||
@@ -143,6 +143,11 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Resizable Splitter -->
|
||||||
|
<div class="pdf-splitter @(_isResizing ? "resizing" : "")"
|
||||||
|
@onmousedown="OnSplitterMouseDown"
|
||||||
|
@onmousedown:preventDefault="true">
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
<div class="pdf-canvas-wrapper">
|
<div class="pdf-canvas-wrapper">
|
||||||
<canvas id="pdf-canvas" class="pdf-canvas"></canvas>
|
<canvas id="pdf-canvas" class="pdf-canvas"></canvas>
|
||||||
@@ -177,6 +182,14 @@
|
|||||||
bool _showThumbnails = true;
|
bool _showThumbnails = true;
|
||||||
DotNetObjectReference<EnvelopeViewer>? _dotNetRef;
|
DotNetObjectReference<EnvelopeViewer>? _dotNetRef;
|
||||||
|
|
||||||
|
// 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() {
|
protected override async Task OnInitializedAsync() {
|
||||||
if (string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
if (string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||||||
_errorMessage = "Envelope-Schlüssel fehlt.";
|
_errorMessage = "Envelope-Schlüssel fehlt.";
|
||||||
@@ -205,6 +218,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||||||
|
if (firstRender) {
|
||||||
|
// Load saved thumbnail width from localStorage
|
||||||
|
try {
|
||||||
|
var savedWidth = await JSRuntime.InvokeAsync<string>("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)) {
|
if (!_pdfLoaded && !string.IsNullOrWhiteSpace(_pdfDataUrl)) {
|
||||||
await Task.Delay(500);
|
await Task.Delay(500);
|
||||||
|
|
||||||
@@ -216,6 +242,10 @@
|
|||||||
_pdfLoaded = true;
|
_pdfLoaded = true;
|
||||||
_totalPages = await JSRuntime.InvokeAsync<int>("pdfViewer.getTotalPages");
|
_totalPages = await JSRuntime.InvokeAsync<int>("pdfViewer.getTotalPages");
|
||||||
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
|
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
|
||||||
|
|
||||||
|
// Attach resize listeners
|
||||||
|
await JSRuntime.InvokeVoidAsync("pdfViewer.attachResizeListeners", _dotNetRef);
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
// Wait for DOM to be ready, then render thumbnails
|
// 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() {
|
public async ValueTask DisposeAsync() {
|
||||||
if (_pdfLoaded) {
|
if (_pdfLoaded) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -104,6 +104,46 @@
|
|||||||
background: linear-gradient(135deg, #6b1cb0 0%, #1e3a72 100%);
|
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 {
|
.pdf-thumbnail {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
@@ -289,6 +289,48 @@ window.pdfViewer = {
|
|||||||
this.wheelEventAttached = false;
|
this.wheelEventAttached = false;
|
||||||
this.dotNetReference = null;
|
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user