Add thumbnail sidebar and resizable splitter to viewer

Enhanced EnvelopeViewer with a thumbnail sidebar for page previews and a resizable splitter (150px-400px range) for improved navigation. Updated layout to use a flexbox design for side-by-side thumbnails and PDF canvas.

Externalized CSS for maintainability and added responsive behavior for mobile devices. Improved Blazor lifecycle handling with `firstRender` checks and sequential thumbnail rendering. Addressed known issues like vertical alignment and infinite render loops.

Introduced localStorage persistence for user preferences and enhanced zoom/navigation interactivity with global mouse events.
This commit is contained in:
2026-06-06 00:46:10 +02:00
parent 9fa8ef29d8
commit 51ea93200e

View File

@@ -70,7 +70,8 @@ Conversions:
- Fetches PDF via `DocumentService.GetDocumentAsync(EnvelopeKey)`
- Converts to base64 data URL: `data:application/pdf;base64,{base64}`
- Initializes PDF.js viewer via JSInterop with `DotNetObjectReference` for callbacks
- Displays controls: Zoom In/Out, Page Navigation, Zoom percentage
- Displays controls: Zoom In/Out, Page Navigation, Zoom percentage, Thumbnail toggle
- Thumbnail sidebar with resizable splitter (150px-400px range)
- CSS externalized to `envelope-viewer.css`
**JavaScript (`pdf-viewer.js`):**
@@ -78,10 +79,15 @@ Conversions:
window.pdfViewer = {
pdfDoc, canvas, ctx, scale, currentRenderTask,
dotNetReference, wheelEventAttached,
isResizing, resizeMouseMoveHandler, resizeMouseUpHandler,
initialize(canvasId, pdfDataUrl, dotNetRef),
renderPage(num),
renderThumbnail(pageNum, canvasId),
attachWheelEvent(), // Global Ctrl+Wheel zoom
attachResizeListeners(dotNetRef), // Splitter resize
detachResizeListeners(),
startResize(),
zoomIn(), zoomOut(),
nextPage(), previousPage(),
dispose()
@@ -91,7 +97,10 @@ window.pdfViewer = {
**CSS (`envelope-viewer.css`):**
- `.envelope-viewer-layout`: Full-height gradient background
- `.envelope-action-bar`: Top bar with logo, title, controls (sticky)
- `.pdf-frame`: Fixed-size white container (`calc(100vh - 200px)` × 90% width, max 1200px)
- `.pdf-frame`: Flex container (row) with thumbnails + canvas side-by-side
- `.pdf-thumbnails`: Left sidebar (260px default, resizable 150-400px), no header
- `.pdf-splitter`: 4px resizable divider with `col-resize` cursor
- `.pdf-canvas-wrapper`: Flex-grow container with scroll, padding, centered canvas
- `.pdf-canvas`: `display: inline-block`, unlimited zoom, scrollable when exceeds frame
- Modern glassmorphism design with gradients and shadows
@@ -113,9 +122,30 @@ window.pdfViewer = {
- Catches `RenderingCancelledException` to avoid console errors
- Queue system (`pageNumPending`) for rapid page changes
4. **Responsive Design:**
4. **Thumbnail Sidebar:**
- Left panel with page previews (sequential rendering, 50ms delay)
- Click to navigate to specific page
- Active page highlighted with gradient border
- No header/title (maximizes thumbnail space)
- Toggle button in toolbar to show/hide
5. **Resizable Splitter:**
- 4px draggable divider between thumbnails and canvas
- Min width: 150px, Max width: 400px
- Visual feedback: gradient on hover/active
- User preference saved to localStorage (`envelopeViewer_thumbnailWidth`)
- Global mouse events (works anywhere during drag)
- `col-resize` cursor (?) for intuitive UX
6. **Flex Layout:**
- Thumbnails and canvas in same container (`.pdf-frame`)
- `display: flex, flex-direction: row, align-items: stretch`
- Perfect vertical alignment (same top/bottom position)
- Responsive: column layout on mobile (<768px)
7. **Responsive Design:**
- Desktop: 90% width, 1200px max
- Mobile: 95% width, adjusted heights
- Mobile: 95% width, adjusted heights, thumbnails collapse to top
- Adaptive padding and font sizes
### Flow
@@ -127,21 +157,28 @@ window.pdfViewer = {
- Convert to base64 data URL
- Set _isLoading = false
OnAfterRenderAsync():
OnAfterRenderAsync(firstRender):
- Load saved thumbnail width from localStorage
- Create DotNetObjectReference
- JSRuntime.InvokeAsync("pdfViewer.initialize", canvasId, pdfDataUrl, dotNetRef)
- Attach resize listeners for splitter
- Update _totalPages, _currentPage, _pdfLoaded
- Render thumbnails sequentially (50ms delay between pages)
```
2. **User Interaction:**
- Button clicks ? `ZoomIn()`/`ZoomOut()` ? `JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn")`
- Ctrl+Wheel ? JS `attachWheelEvent()` ? `dotNetRef.invokeMethodAsync('OnZoomChanged')`
- Page buttons ? `NextPage()`/`PreviousPage()` ? `JSRuntime.InvokeAsync("pdfViewer.nextPage")`
- Thumbnail click ? `GoToPageFromThumbnail(pageNum)` ? `pdfViewer.goToPage(pageNum)`
- Toggle button ? `ToggleThumbnails()` ? `_showThumbnails = !_showThumbnails`
- Splitter drag ? `OnSplitterMouseDown()` ? JS global mouse events ? `OnSplitterMouseMove(clientX)` ? width update ? `OnSplitterMouseUp()` ? save to localStorage
3. **Cleanup:**
```csharp
DisposeAsync():
- JSRuntime.InvokeVoidAsync("pdfViewer.dispose")
- Detach resize listeners
- _dotNetRef?.Dispose()
```
@@ -269,6 +306,8 @@ return report;
| **Mouse wheel on `.pdf-frame` only** | **Only works when mouse over PDF; should work anywhere on page** |
| **OnAfterRenderAsync without `firstRender` guard** | **Creates infinite loop when `StateHasChanged` is called repeatedly** |
| **Conditional rendering with `@if (_pdfLoaded)` wrapping canvas** | **Canvas not in DOM when initialize called, causing perpetual failure** |
| **Thumbnail sidebar with `position: absolute`** | **Independent from PDF canvas, breaks alignment on screen resize** |
| **Thumbnail header with title/close button** | **Wastes valuable space; toolbar toggle is sufficient** |
---
@@ -303,81 +342,93 @@ Our use case is **visual/image stamping** at specific page coordinates
| **11** | **2025-01-XX** | **Added PDF thumbnail sidebar (left panel) with page previews and navigation** |
| **11** | **2025-01-XX** | **Implemented thumbnail rendering system with sequential loading (50ms delay between pages)** |
| **11** | **2025-01-XX** | **Fixed thumbnail rendering: retry logic (10x 100ms) for canvas availability** |
| **11** | **2025-01-XX** | **Refactored layout: Side-by-side flex design (thumbnails left, PDF right), responsive mobile (horizontal scroll thumbnails)** |
| **11** | **2025-01-XX** | **Updated COPILOT_CONTEXT_EN.md: EnvelopeViewer replaces ReportViewer for read-only viewing** |
| **11** | **2025-01-XX** | **?? UNRESOLVED: Infinite render loop causing blank screen — Canvas not found error repeating, `_pdfLoaded` never becomes true** |
| **11** | **2025-01-XX** | **Refactored layout: Moved thumbnails inside `pdf-frame` for flex side-by-side design** |
| **11** | **2025-01-XX** | **Removed thumbnail header (title + close button) to maximize thumbnail space** |
| **11** | **2025-01-XX** | **Added resizable splitter: 4px draggable divider, 150-400px range, localStorage persistence** |
| **11** | **2025-01-XX** | **Fixed vertical alignment: `align-items: stretch` ensures thumbnails and canvas have same height** |
| **11** | **2025-01-XX** | **Updated COPILOT_CONTEXT_EN.md: Documented resizable splitter and layout refactoring** |
---
## Known Issues
## Layout Architecture (EnvelopeViewer)
### EnvelopeViewer — Blank Screen / Infinite Loop (UNRESOLVED)
### HTML Structure
```html
<div class="pdf-viewer-container">
<div class="pdf-toolbar">
<!-- Zoom, page navigation, thumbnail toggle -->
</div>
<div class="pdf-frame">
@if (_showThumbnails) {
<div class="pdf-thumbnails" style="width: @(_thumbnailWidth)px">
<!-- Page previews -->
</div>
<div class="pdf-splitter" @onmousedown="OnSplitterMouseDown">
<!-- Resizable divider -->
</div>
}
<div class="pdf-canvas-wrapper">
<canvas id="pdf-canvas" class="pdf-canvas"></canvas>
</div>
</div>
</div>
```
**Symptoms:**
- Browser console shows: `Canvas not found: pdf-canvas` (repeating 20+ times)
- UI displays error: "Fehler beim Laden des Dokuments - PDF konnte nicht initialisiert werden"
- Blank purple gradient screen, no PDF or thumbnails visible
- `OnAfterRenderAsync` triggers continuously in loop
### CSS Flexbox Layout
```css
.pdf-frame {
display: flex;
flex-direction: row; /* Side-by-side */
align-items: stretch; /* Same height */
overflow: hidden;
}
**Root Cause:**
- `OnAfterRenderAsync` runs on every render cycle (not just `firstRender`)
- PDF canvas element is not in DOM when `pdfViewer.initialize` is called
- Because `_pdfLoaded = false`, thumbnail/toolbar sections don't render (`@if (_pdfLoaded)` condition)
- Each failed initialize triggers `StateHasChanged` ? new render ? `OnAfterRenderAsync` again ? **infinite loop**
.pdf-thumbnails {
flex-shrink: 0; /* Fixed width */
width: 260px; /* Dynamic via inline style */
border-right: none; /* Seamless join with splitter */
}
**Attempted Fixes (Failed):**
1. **Adding `firstRender` check:**
```csharp
protected override async Task OnAfterRenderAsync(bool firstRender) {
if (firstRender && !_pdfLoaded && ...) {
// Initialize PDF
}
}
```
**Result:** Didn't stop the loop, still blank screen
.pdf-splitter {
width: 4px;
cursor: col-resize;
flex-shrink: 0;
}
2. **PDF.js availability check:**
```csharp
var pdfJsLoaded = await JSRuntime.InvokeAsync<bool>("eval", "typeof window.pdfjsLib !== 'undefined'");
```
**Result:** Didn't resolve canvas not found issue
.pdf-canvas-wrapper {
flex: 1; /* Fill remaining space */
overflow: auto; /* Scrollable for zoom */
padding: 2rem;
text-align: center;
}
```
3. **Increased delays:**
- `Task.Delay(300)` before initialize
- `Task.Delay(200)` before thumbnails
**Result:** No improvement
### Resizable Splitter Workflow
```
1. User hovers splitter ? cursor: col-resize (?)
2. Mouse down ? OnSplitterMouseDown(e)
- _isResizing = true
- Store start position (clientX) and width
- Add 'resizing' class to body (prevent text selection)
- Call pdfViewer.startResize()
4. **JavaScript validation:**
- Added checks for `uint8Array.length`, `totalPages > 0`
**Result:** Didn't prevent initialization failure
3. Mouse move (global) ? OnSplitterMouseMove(clientX)
- Calculate delta = clientX - startX
- newWidth = startWidth + delta
- Clamp to 150-400px range
- Update _thumbnailWidth
- StateHasChanged() for reactive UI
**Possible Next Steps:**
1. **DOM Ready Strategy:**
- Wait for specific element existence before initialize
- Use `MutationObserver` in JS to detect canvas availability
- Try `IntersectionObserver` to ensure canvas is in viewport
4. Mouse up (global) ? OnSplitterMouseUp()
- _isResizing = false
- Remove 'resizing' class
- Save to localStorage("envelopeViewer_thumbnailWidth")
```
2. **Conditional Rendering:**
- Always render canvas element (even before `_pdfLoaded`)
- Move toolbar/thumbnails outside `@if (_pdfLoaded)` block
- Use CSS `visibility: hidden` instead of conditional rendering
### Responsive Behavior
- **Desktop (>768px)**: Flex row, side-by-side
- **Mobile (?768px)**: Flex column, thumbnails on top
- **Thumbnail toggle**: Controlled by `@if (_showThumbnails)` in Razor markup
3. **Blazor Lifecycle:**
- Try `OnAfterRenderAsync` with `IJSRuntime` timeout guard
- Use `Task.Run` with cancellation token to prevent overlapping calls
- Investigate if WASM-specific render cycle differs from Server
4. **Debugging:**
- Add `Console.WriteLine` in C# to track render count
- Log `firstRender`, `_pdfLoaded`, `_pdfDataUrl` state on each call
- Check if PDF data is actually loaded (`_pdfDataUrl` not null/empty)
- Verify PDF.js CDN loads successfully (Network tab)
**Test Case:**
- Route: `/envelope/NTE3Ym15SyUtNjA4M...` (valid envelope key)
- Expected: PDF loads, thumbnails appear, toolbar shows
- Actual: Blank screen, console error spam, no PDF rendering
**Workaround:**
- Use legacy `ReportViewer.razor` (`/receiver/{key}`) for now
- EnvelopeViewer development paused until root cause identified