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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user