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)`
|
- Fetches PDF via `DocumentService.GetDocumentAsync(EnvelopeKey)`
|
||||||
- Converts to base64 data URL: `data:application/pdf;base64,{base64}`
|
- Converts to base64 data URL: `data:application/pdf;base64,{base64}`
|
||||||
- Initializes PDF.js viewer via JSInterop with `DotNetObjectReference` for callbacks
|
- 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`
|
- CSS externalized to `envelope-viewer.css`
|
||||||
|
|
||||||
**JavaScript (`pdf-viewer.js`):**
|
**JavaScript (`pdf-viewer.js`):**
|
||||||
@@ -78,10 +79,15 @@ Conversions:
|
|||||||
window.pdfViewer = {
|
window.pdfViewer = {
|
||||||
pdfDoc, canvas, ctx, scale, currentRenderTask,
|
pdfDoc, canvas, ctx, scale, currentRenderTask,
|
||||||
dotNetReference, wheelEventAttached,
|
dotNetReference, wheelEventAttached,
|
||||||
|
isResizing, resizeMouseMoveHandler, resizeMouseUpHandler,
|
||||||
|
|
||||||
initialize(canvasId, pdfDataUrl, dotNetRef),
|
initialize(canvasId, pdfDataUrl, dotNetRef),
|
||||||
renderPage(num),
|
renderPage(num),
|
||||||
|
renderThumbnail(pageNum, canvasId),
|
||||||
attachWheelEvent(), // Global Ctrl+Wheel zoom
|
attachWheelEvent(), // Global Ctrl+Wheel zoom
|
||||||
|
attachResizeListeners(dotNetRef), // Splitter resize
|
||||||
|
detachResizeListeners(),
|
||||||
|
startResize(),
|
||||||
zoomIn(), zoomOut(),
|
zoomIn(), zoomOut(),
|
||||||
nextPage(), previousPage(),
|
nextPage(), previousPage(),
|
||||||
dispose()
|
dispose()
|
||||||
@@ -91,7 +97,10 @@ window.pdfViewer = {
|
|||||||
**CSS (`envelope-viewer.css`):**
|
**CSS (`envelope-viewer.css`):**
|
||||||
- `.envelope-viewer-layout`: Full-height gradient background
|
- `.envelope-viewer-layout`: Full-height gradient background
|
||||||
- `.envelope-action-bar`: Top bar with logo, title, controls (sticky)
|
- `.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
|
- `.pdf-canvas`: `display: inline-block`, unlimited zoom, scrollable when exceeds frame
|
||||||
- Modern glassmorphism design with gradients and shadows
|
- Modern glassmorphism design with gradients and shadows
|
||||||
|
|
||||||
@@ -113,9 +122,30 @@ window.pdfViewer = {
|
|||||||
- Catches `RenderingCancelledException` to avoid console errors
|
- Catches `RenderingCancelledException` to avoid console errors
|
||||||
- Queue system (`pageNumPending`) for rapid page changes
|
- 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
|
- Desktop: 90% width, 1200px max
|
||||||
- Mobile: 95% width, adjusted heights
|
- Mobile: 95% width, adjusted heights, thumbnails collapse to top
|
||||||
- Adaptive padding and font sizes
|
- Adaptive padding and font sizes
|
||||||
|
|
||||||
### Flow
|
### Flow
|
||||||
@@ -127,21 +157,28 @@ window.pdfViewer = {
|
|||||||
- Convert to base64 data URL
|
- Convert to base64 data URL
|
||||||
- Set _isLoading = false
|
- Set _isLoading = false
|
||||||
|
|
||||||
OnAfterRenderAsync():
|
OnAfterRenderAsync(firstRender):
|
||||||
|
- Load saved thumbnail width from localStorage
|
||||||
- Create DotNetObjectReference
|
- Create DotNetObjectReference
|
||||||
- JSRuntime.InvokeAsync("pdfViewer.initialize", canvasId, pdfDataUrl, dotNetRef)
|
- JSRuntime.InvokeAsync("pdfViewer.initialize", canvasId, pdfDataUrl, dotNetRef)
|
||||||
|
- Attach resize listeners for splitter
|
||||||
- Update _totalPages, _currentPage, _pdfLoaded
|
- Update _totalPages, _currentPage, _pdfLoaded
|
||||||
|
- Render thumbnails sequentially (50ms delay between pages)
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **User Interaction:**
|
2. **User Interaction:**
|
||||||
- Button clicks ? `ZoomIn()`/`ZoomOut()` ? `JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn")`
|
- Button clicks ? `ZoomIn()`/`ZoomOut()` ? `JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn")`
|
||||||
- Ctrl+Wheel ? JS `attachWheelEvent()` ? `dotNetRef.invokeMethodAsync('OnZoomChanged')`
|
- Ctrl+Wheel ? JS `attachWheelEvent()` ? `dotNetRef.invokeMethodAsync('OnZoomChanged')`
|
||||||
- Page buttons ? `NextPage()`/`PreviousPage()` ? `JSRuntime.InvokeAsync("pdfViewer.nextPage")`
|
- 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:**
|
3. **Cleanup:**
|
||||||
```csharp
|
```csharp
|
||||||
DisposeAsync():
|
DisposeAsync():
|
||||||
- JSRuntime.InvokeVoidAsync("pdfViewer.dispose")
|
- JSRuntime.InvokeVoidAsync("pdfViewer.dispose")
|
||||||
|
- Detach resize listeners
|
||||||
- _dotNetRef?.Dispose()
|
- _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** |
|
| **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** |
|
| **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** |
|
| **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** | **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** | **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** | **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** | **Refactored layout: Moved thumbnails inside `pdf-frame` for flex side-by-side design** |
|
||||||
| **11** | **2025-01-XX** | **Updated COPILOT_CONTEXT_EN.md: EnvelopeViewer replaces ReportViewer for read-only viewing** |
|
| **11** | **2025-01-XX** | **Removed thumbnail header (title + close button) to maximize thumbnail space** |
|
||||||
| **11** | **2025-01-XX** | **?? UNRESOLVED: Infinite render loop causing blank screen — Canvas not found error repeating, `_pdfLoaded` never becomes true** |
|
| **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>
|
||||||
|
|
||||||
**Symptoms:**
|
<div class="pdf-frame">
|
||||||
- Browser console shows: `Canvas not found: pdf-canvas` (repeating 20+ times)
|
@if (_showThumbnails) {
|
||||||
- UI displays error: "Fehler beim Laden des Dokuments - PDF konnte nicht initialisiert werden"
|
<div class="pdf-thumbnails" style="width: @(_thumbnailWidth)px">
|
||||||
- Blank purple gradient screen, no PDF or thumbnails visible
|
<!-- Page previews -->
|
||||||
- `OnAfterRenderAsync` triggers continuously in loop
|
</div>
|
||||||
|
<div class="pdf-splitter" @onmousedown="OnSplitterMouseDown">
|
||||||
**Root Cause:**
|
<!-- Resizable divider -->
|
||||||
- `OnAfterRenderAsync` runs on every render cycle (not just `firstRender`)
|
</div>
|
||||||
- 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**
|
|
||||||
|
|
||||||
**Attempted Fixes (Failed):**
|
|
||||||
1. **Adding `firstRender` check:**
|
|
||||||
```csharp
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
|
||||||
if (firstRender && !_pdfLoaded && ...) {
|
|
||||||
// Initialize PDF
|
|
||||||
}
|
}
|
||||||
|
<div class="pdf-canvas-wrapper">
|
||||||
|
<canvas id="pdf-canvas" class="pdf-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Flexbox Layout
|
||||||
|
```css
|
||||||
|
.pdf-frame {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row; /* Side-by-side */
|
||||||
|
align-items: stretch; /* Same height */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-thumbnails {
|
||||||
|
flex-shrink: 0; /* Fixed width */
|
||||||
|
width: 260px; /* Dynamic via inline style */
|
||||||
|
border-right: none; /* Seamless join with splitter */
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-splitter {
|
||||||
|
width: 4px;
|
||||||
|
cursor: col-resize;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-canvas-wrapper {
|
||||||
|
flex: 1; /* Fill remaining space */
|
||||||
|
overflow: auto; /* Scrollable for zoom */
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
**Result:** Didn't stop the loop, still blank screen
|
|
||||||
|
|
||||||
2. **PDF.js availability check:**
|
### Resizable Splitter Workflow
|
||||||
```csharp
|
|
||||||
var pdfJsLoaded = await JSRuntime.InvokeAsync<bool>("eval", "typeof window.pdfjsLib !== 'undefined'");
|
|
||||||
```
|
```
|
||||||
**Result:** Didn't resolve canvas not found issue
|
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()
|
||||||
|
|
||||||
3. **Increased delays:**
|
3. Mouse move (global) ? OnSplitterMouseMove(clientX)
|
||||||
- `Task.Delay(300)` before initialize
|
- Calculate delta = clientX - startX
|
||||||
- `Task.Delay(200)` before thumbnails
|
- newWidth = startWidth + delta
|
||||||
**Result:** No improvement
|
- Clamp to 150-400px range
|
||||||
|
- Update _thumbnailWidth
|
||||||
|
- StateHasChanged() for reactive UI
|
||||||
|
|
||||||
4. **JavaScript validation:**
|
4. Mouse up (global) ? OnSplitterMouseUp()
|
||||||
- Added checks for `uint8Array.length`, `totalPages > 0`
|
- _isResizing = false
|
||||||
**Result:** Didn't prevent initialization failure
|
- Remove 'resizing' class
|
||||||
|
- Save to localStorage("envelopeViewer_thumbnailWidth")
|
||||||
|
```
|
||||||
|
|
||||||
**Possible Next Steps:**
|
### Responsive Behavior
|
||||||
1. **DOM Ready Strategy:**
|
- **Desktop (>768px)**: Flex row, side-by-side
|
||||||
- Wait for specific element existence before initialize
|
- **Mobile (?768px)**: Flex column, thumbnails on top
|
||||||
- Use `MutationObserver` in JS to detect canvas availability
|
- **Thumbnail toggle**: Controlled by `@if (_showThumbnails)` in Razor markup
|
||||||
- Try `IntersectionObserver` to ensure canvas is in viewport
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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