Add interactive signature buttons to PDF viewer

Implemented a new feature to render clickable "Sign" buttons on the PDF canvas at signature field positions fetched from the database.

- Updated `EnvelopeViewer.razor` to fetch signature data, convert coordinates from inches to points, and invoke JavaScript for rendering.
- Added `renderSignatureButtons` and `clearSignatureButtons` functions in `pdf-viewer.js` to dynamically create and position buttons based on page and zoom level.
- Modified HTML to include a new `#pdf-signature-layer` overlay for buttons.
- Styled buttons with a purple gradient, hover/active effects, and focus outlines for accessibility.
- Defined rendering triggers for initial load, page changes, and zoom changes.
- Documented coordinate conversion flow from inches to points to pixels for accurate positioning.
- Enhanced accessibility with `tabindex="0"`, focus outlines, and semantic `<button>` elements.
This commit is contained in:
2026-06-07 12:55:21 +02:00
parent 2f73e4f6da
commit 89fb6f1452

View File

@@ -419,6 +419,14 @@ Our use case is **visual/image stamping** at specific page coordinates
| **11** | **2025-01-XX** | **Made zoom step configurable (buttons, Ctrl+Wheel, slider use same step)** |
| **11** | **2025-01-XX** | **Fixed thumbnail canvas alignment (object-fit: contain)** |
| **11** | **2025-01-XX** | **Fixed thumbnail re-rendering on sidebar toggle** |
| **12** | **2025-01-XX** | **Fixed coordinate system documentation: Database stores INCHES (not DX units)** |
| **12** | **2025-01-XX** | **Updated XML documentation in SignatureDto, AnnotationDto, AnnotationCreateDto** |
| **13** | **2025-01-XX** | **Added SenderAppType enum to SignatureDto for Legacy/Blazor app differentiation** |
| **13** | **2025-01-XX** | **Implemented signature overlay rendering in PDF.js viewer (INCHES ? normalized ? pixels)** |
| **13** | **2025-01-XX** | **Added automatic overlay re-rendering on page change, zoom, and initial load** |
| **13** | **2025-01-XX** | **Added clickable signature buttons on PDF canvas (Sign/Unterschreiben)** |
| **13** | **2025-01-XX** | **Signature buttons: 150px×60px, purple gradient, pen icon, hover effects** |
| **13** | **2025-01-XX** | **JavaScript: `pdfViewer.renderSignatureButtons()` and `pdfViewer.clearSignatureButtons()`** |
---
@@ -503,4 +511,154 @@ Our use case is **visual/image stamping** at specific page coordinates
- **Mobile (?768px)**: Flex column, thumbnails on top
- **Thumbnail toggle**: Controlled by `@if (_showThumbnails)` in Razor markup
---
## Signature Buttons in EnvelopeViewer — Interactive Overlay System
**Purpose:** Render clickable "Unterschreiben" (Sign) buttons on PDF canvas at signature field positions fetched from database.
### Architecture
**Blazor Component (`EnvelopeViewer.razor`):**
```csharp
IReadOnlyList<SignatureDto> _signatures = [];
protected override async Task OnInitializedAsync() {
var signatures = await SignatureService.GetAsync(EnvelopeKey);
_signatures = signatures.Convert(UnitOfLength.Point); // INCHES ? POINTS
}
async Task RenderSignatureButtonsAsync() {
await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons",
_signatures, _currentPage, _dotNetRef);
}
[JSInvokable]
public void OnSignatureButtonClick(int signatureId) {
Console.WriteLine($"Signature #{signatureId} signed");
}
```
**JavaScript (`pdf-viewer.js`):**
```javascript
renderSignatureButtons(signatures, currentPageNum, dotNetRef) {
this.clearSignatureButtons(); // Remove old buttons
const pageSignatures = signatures.filter(sig => sig.page === currentPageNum);
const signatureLayer = document.getElementById('pdf-signature-layer');
pageSignatures.forEach(sig => {
// Convert POINTS to display pixels
const xPx = sig.x * this.scale; // sig.x already in PDF POINTS
const yPx = sig.y * this.scale;
const button = document.createElement('button');
button.className = 'signature-button';
button.style.left = `${xPx}px`;
button.style.top = `${yPx}px`;
button.style.width = '150px';
button.style.height = '60px';
// German text + pen icon
button.innerHTML = `
<div>Unterschreiben</div>
<svg>...</svg>
`;
button.addEventListener('click', () => {
this.dotNetReference.invokeMethodAsync('OnSignatureButtonClick', sig.id);
});
signatureLayer.appendChild(button);
this.signatureButtons.push(button);
});
}
clearSignatureButtons() {
this.signatureButtons.forEach(btn => btn.parentNode?.removeChild(btn));
this.signatureButtons = [];
}
```
**HTML Structure:**
```html
<div class="pdf-page-container">
<canvas id="pdf-canvas"></canvas>
<div id="pdf-text-layer"></div>
<div id="pdf-signature-layer"></div> <!-- NEW -->
</div>
```
**CSS (`envelope-viewer.css`):**
```css
.pdf-signature-layer {
position: absolute;
left: 0; top: 0; right: 0; bottom: 0;
overflow: visible;
pointer-events: none; /* Pass clicks through to canvas */
z-index: 20;
}
.signature-button {
pointer-events: auto; /* Re-enable for buttons */
position: absolute;
width: 150px; height: 60px;
background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
/* Hover: scale(1.05), shadow, darker gradient */
}
```
### Rendering Triggers
1. **Initial Load:** After PDF renders (`OnAfterRenderAsync`)
2. **Page Change:** `NextPage()`, `PreviousPage()`, `GoToPageFromThumbnail()`
3. **Zoom Change:** `OnZoomChanged()` (re-calculates pixel positions)
### Coordinate Conversion Flow
```
Database (INCHES)
? SignatureService.GetAsync()
SignatureDto.X/Y (INCHES)
? .Convert(UnitOfLength.Point)
SignatureDto.X/Y (PDF POINTS, × 72)
? JavaScript
Display Pixels (× this.scale)
? CSS
button.style.left/top
```
**Example Calculation:**
- Database: `X = 1.5 inches, Y = 2.0 inches`
- After conversion: `X = 108 points, Y = 144 points` (× 72)
- At scale 1.5: `X = 162px, Y = 216px`
- Button positioned at `left: 162px, top: 216px`
### Button Design
**Visual Spec:**
- **Size:** 150px × 60px
- **Background:** Purple gradient `#4F46E5` ? `#4338CA` (hover: darker)
- **Text:** "Unterschreiben" (18px, bold, white)
- **Icon:** Pen SVG (24px white)
- **Effects:**
- Hover: `scale(1.05)` + shadow `0 4px 12px rgba(79, 70, 229, 0.4)`
- Active: `scale(0.98)`
- Focus: `2px solid #7e22ce` outline
**Accessibility:**
- `tabindex="0"` for keyboard navigation
- Focus outline for keyboard users
- Click handler with semantic button element
---