Enhanced signature navigation and rendering logic in `pdf-viewer.js`: - Added `_renderLock` to prevent concurrent page renders. - Refactored `renderPage` and `queueRenderPage` for stability. - Updated `goToNextSignature` to support cross-page navigation. - Filtered out applied signatures during rendering and navigation. - Improved handling of applied signatures visibility per page. Updated `EnvelopeViewer.razor`: - Added `OnPageChangedBySignatureNav` to handle page changes triggered by signature navigation. Improved code readability, added comments, and removed outdated logic to ensure smooth transitions and better user experience.
808 lines
39 KiB
Plaintext
808 lines
39 KiB
Plaintext
@page "/envelope/{EnvelopeKey}"
|
||
@using EnvelopeGenerator.ReceiverUI.Models
|
||
@using EnvelopeGenerator.ReceiverUI.Models.Constants
|
||
@using EnvelopeGenerator.ReceiverUI.Services
|
||
@using Microsoft.Extensions.Options
|
||
@using EnvelopeGenerator.ReceiverUI.Options
|
||
@using Microsoft.JSInterop
|
||
@using DevExpress.Blazor
|
||
@inject DocumentService DocumentService
|
||
@inject NavigationManager Navigation
|
||
@inject IOptions<ApiOptions> AppOptions
|
||
@inject IOptions<PdfViewerOptions> PdfViewerOptions
|
||
@inject IJSRuntime JSRuntime
|
||
@inject SignatureService SignatureService
|
||
@implements IAsyncDisposable
|
||
|
||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||
<link href="css/envelope-viewer.css" rel="stylesheet" />
|
||
<link href="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css" rel="stylesheet" />
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||
<script src="js/pdf-viewer.js"></script>
|
||
<script src="js/receiver-signature.js"></script>
|
||
|
||
<div class="envelope-viewer-layout">
|
||
<div class="envelope-action-bar">
|
||
<div class="envelope-action-bar__inner">
|
||
<div class="d-flex align-items-center gap-3">
|
||
<div class="envelope-logo">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<div class="envelope-title">Dokumentenansicht</div>
|
||
<div class="envelope-key">ID: @EnvelopeKey</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="envelope-content">
|
||
@if (_isLoading) {
|
||
<div class="d-flex justify-content-center align-items-center h-100">
|
||
<div class="text-center">
|
||
<div class="spinner-border text-white mb-3" style="width: 3.5rem; height: 3.5rem;" role="status">
|
||
<span class="visually-hidden">L<>dt...</span>
|
||
</div>
|
||
<p class="text-white fw-semibold">Dokument wird geladen...</p>
|
||
</div>
|
||
</div>
|
||
} else if (_errorMessage is not null) {
|
||
<div class="error-container">
|
||
<div class="alert alert-danger shadow-lg">
|
||
<div class="d-flex align-items-start">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3 flex-shrink-0" viewBox="0 0 16 16">
|
||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
|
||
</svg>
|
||
<div>
|
||
<h5 class="mb-2">Fehler beim Laden des Dokuments</h5>
|
||
<p class="mb-0">@_errorMessage</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
} else if (!string.IsNullOrWhiteSpace(_pdfDataUrl)) {
|
||
<div class="pdf-viewer-container">
|
||
@if (_pdfLoaded) {
|
||
<div class="pdf-toolbar">
|
||
<div class="pdf-toolbar__section">
|
||
<button class="pdf-toolbar__btn pdf-toolbar__btn--toggle" @onclick="ToggleThumbnails" title="@(_showThumbnails ? "Seitenleiste ausblenden" : "Seitenleiste einblenden")">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zm8 0A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm-8 8A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm8 0A1.5 1.5 0 0 1 10.5 9h3a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 13.5v-3z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="pdf-toolbar__divider"></div>
|
||
|
||
<div class="pdf-toolbar__section">
|
||
<button class="pdf-toolbar__btn" @onclick="PreviousPage" disabled="@(_currentPage <= 1)" title="Vorherige Seite">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
|
||
</svg>
|
||
</button>
|
||
<div class="pdf-toolbar__page-input-group">
|
||
<input type="number" class="pdf-toolbar__page-input" min="1" max="@_totalPages" value="@_currentPage" @onchange="OnPageInputChanged" />
|
||
<span class="pdf-toolbar__page-total">/ @_totalPages</span>
|
||
</div>
|
||
<button class="pdf-toolbar__btn" @onclick="NextPage" disabled="@(_currentPage >= _totalPages)" title="Nächste Seite">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="pdf-toolbar__divider"></div>
|
||
|
||
<div class="pdf-toolbar__section pdf-toolbar__zoom-section">
|
||
<button class="pdf-toolbar__btn" @onclick="ZoomOut" disabled="@(_currentZoom <= 50)" title="Verkleinern">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0zM4 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1H4z"/>
|
||
</svg>
|
||
</button>
|
||
<div class="pdf-toolbar__zoom-slider-container">
|
||
<input type="range" class="pdf-toolbar__zoom-slider" min="50" max="300" step="@(PdfViewerOptions.Value.ZoomStepPercentage)" value="@_currentZoom" @oninput="OnZoomSliderChanged" title="@(_currentZoom)%" />
|
||
<div class="pdf-toolbar__zoom-label">@(_currentZoom)%</div>
|
||
</div>
|
||
<button class="pdf-toolbar__btn" @onclick="ZoomIn" disabled="@(_currentZoom >= 300)" title="Vergrößern">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0zM6.5 3a.5.5 0 0 0-1 0v2.5H3a.5.5 0 0 0 0 1h2.5V9a.5.5 0 0 0 1 0V6.5H9a.5.5 0 0 0 0-1H6.5V3z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="pdf-toolbar__divider"></div>
|
||
|
||
@if (_totalSignatures > 0) {
|
||
<div class="pdf-toolbar__section pdf-toolbar__signature-nav">
|
||
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-nav"
|
||
@onclick="GoToPreviousSignature"
|
||
disabled="@(_signedSignatures == 0)"
|
||
title="Vorherige Unterschrift">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
|
||
</svg>
|
||
</button>
|
||
|
||
<div class="pdf-toolbar__signature-counter">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||
</svg>
|
||
<span class="pdf-toolbar__signature-counter-text">
|
||
<strong style="color: @(_unsignedSignatures > 0 ? "#4F46E5" : "#10b981");">@_signedSignatures</strong>
|
||
<span style="opacity: 0.6;"> / </span>
|
||
<span>@_totalSignatures</span>
|
||
</span>
|
||
@if (_unsignedSignatures > 0) {
|
||
<span class="pdf-toolbar__signature-badge">@_unsignedSignatures offen</span>
|
||
} else {
|
||
<span class="pdf-toolbar__signature-badge pdf-toolbar__signature-badge--complete">✓ Komplett</span>
|
||
}
|
||
</div>
|
||
|
||
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-nav"
|
||
@onclick="GoToNextSignature"
|
||
disabled="@(_unsignedSignatures == 0)"
|
||
title="Nächste Unterschrift">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
<div class="pdf-frame">
|
||
@if (_pdfLoaded && _showThumbnails) {
|
||
<!-- PDF Thumbnail Sidebar -->
|
||
<div class="pdf-thumbnails" style="width: @(_thumbnailWidth)px">
|
||
<div class="pdf-thumbnails__content">
|
||
@for (int i = 1; i <= _totalPages; i++) {
|
||
var pageNum = i;
|
||
<div class="pdf-thumbnail @(pageNum == _currentPage ? "pdf-thumbnail--active" : "")" @onclick="() => GoToPageFromThumbnail(pageNum)">
|
||
<div class="pdf-thumbnail__preview">
|
||
<canvas id="thumb-canvas-@pageNum" class="pdf-thumbnail__canvas"></canvas>
|
||
</div>
|
||
<div class="pdf-thumbnail__label">@pageNum</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-page-container">
|
||
<canvas id="pdf-canvas" class="pdf-canvas"></canvas>
|
||
<div id="pdf-text-layer" class="pdf-text-layer"></div>
|
||
<div id="pdf-signature-layer" class="pdf-signature-layer"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
} else {
|
||
<div class="error-container">
|
||
<div class="alert alert-warning shadow-lg">
|
||
<div class="d-flex align-items-center">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3" viewBox="0 0 16 16">
|
||
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||
</svg>
|
||
<span class="fs-5">Dokument konnte nicht geladen werden.</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<DxPopup @bind-Visible="_signaturePopupVisible"
|
||
HeaderText="Unterschrift erstellen"
|
||
Width="620px"
|
||
MaxWidth="95vw"
|
||
ShowFooter="true"
|
||
CloseOnOutsideClick="false"
|
||
ShowCloseButton="false"
|
||
CloseOnEscape="false"
|
||
Shown="OnPopupShownAsync">
|
||
<BodyContentTemplate>
|
||
<ul class="nav nav-tabs mb-3" style="border-bottom: 2px solid #e9ecef;">
|
||
<li class="nav-item">
|
||
<button type="button"
|
||
class="nav-link @(_activeSignatureTab == SignatureTabDraw ? "active" : "")"
|
||
style="@(_activeSignatureTab == SignatureTabDraw ? "border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;" : "color: #6c757d;")"
|
||
@onclick="() => SetSignatureTabAsync(SignatureTabDraw)">
|
||
Zeichnen
|
||
</button>
|
||
</li>
|
||
<li class="nav-item">
|
||
<button type="button"
|
||
class="nav-link @(_activeSignatureTab == SignatureTabText ? "active" : "")"
|
||
style="@(_activeSignatureTab == SignatureTabText ? "border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;" : "color: #6c757d;")"
|
||
@onclick="() => SetSignatureTabAsync(SignatureTabText)">
|
||
Text
|
||
</button>
|
||
</li>
|
||
<li class="nav-item">
|
||
<button type="button"
|
||
class="nav-link @(_activeSignatureTab == SignatureTabImage ? "active" : "")"
|
||
style="@(_activeSignatureTab == SignatureTabImage ? "border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;" : "color: #6c757d;")"
|
||
@onclick="() => SetSignatureTabAsync(SignatureTabImage)">
|
||
Bild
|
||
</button>
|
||
</li>
|
||
</ul>
|
||
|
||
@if(_activeSignatureTab == SignatureTabDraw) {
|
||
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.75rem;">Bitte unterschreiben Sie im folgenden Feld.</p>
|
||
<canvas id="envelope-signature-pad"
|
||
width="560"
|
||
height="180"
|
||
style="border: 2px solid #e9ecef; border-radius: 8px; background: white; width: 100%; max-width: 560px; touch-action: none; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"></canvas>
|
||
} else if(_activeSignatureTab == SignatureTabText) {
|
||
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.75rem;">Geben Sie Ihre Unterschrift als Text ein und wählen Sie eine Schriftart.</p>
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-12 col-md-7">
|
||
<input class="form-control"
|
||
placeholder="Ihre Unterschrift"
|
||
value="@_typedSignatureText"
|
||
@oninput="OnTypedSignatureChanged"
|
||
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
|
||
</div>
|
||
<div class="col-12 col-md-5">
|
||
<select class="form-select"
|
||
value="@_typedSignatureFont"
|
||
@onchange="OnTypedSignatureFontChanged"
|
||
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;">
|
||
@foreach(var font in TypedSignatureFonts) {
|
||
<option value="@font.Value" style="font-family: @font.Value">@font.Text</option>
|
||
}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<canvas id="envelope-typed-signature-pad"
|
||
width="560"
|
||
height="180"
|
||
style="border: 2px solid #e9ecef; border-radius: 8px; background: white; width: 100%; max-width: 560px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"></canvas>
|
||
} else {
|
||
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.75rem;">Laden Sie ein Bild Ihrer Unterschrift hoch.</p>
|
||
<input id="envelope-signature-image-input"
|
||
class="form-control mb-3"
|
||
type="file"
|
||
accept="image/png,image/jpeg,image/webp"
|
||
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
|
||
<canvas id="envelope-image-signature-pad"
|
||
width="560"
|
||
height="180"
|
||
style="border: 2px solid #e9ecef; border-radius: 8px; background: white; width: 100%; max-width: 560px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"></canvas>
|
||
}
|
||
|
||
<div style="border-top: 2px solid #e9ecef; margin-top: 1.5rem; padding-top: 1.5rem;">
|
||
<div class="row g-3">
|
||
<div class="col-12 col-md-6">
|
||
<label class="form-label" for="envelope-signer-name" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
|
||
Vor- und Nachname <span style="color: #dc3545;">*</span>
|
||
</label>
|
||
<input id="envelope-signer-name"
|
||
class="form-control"
|
||
value="@_signerFullName"
|
||
@oninput="args => _signerFullName = args.Value?.ToString() ?? string.Empty"
|
||
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<label class="form-label" for="envelope-signer-position" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
|
||
Position <span style="color: #6c757d; font-weight: 400;">(optional)</span>
|
||
</label>
|
||
<input id="envelope-signer-position"
|
||
class="form-control"
|
||
value="@_signerPosition"
|
||
@oninput="args => _signerPosition = args.Value?.ToString() ?? string.Empty"
|
||
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<label class="form-label" for="envelope-signature-place" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
|
||
Ort <span style="color: #dc3545;">*</span>
|
||
</label>
|
||
<input id="envelope-signature-place"
|
||
class="form-control"
|
||
value="@_signaturePlace"
|
||
@oninput="args => _signaturePlace = args.Value?.ToString() ?? string.Empty"
|
||
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@if(!string.IsNullOrWhiteSpace(_popupValidationMessage)) {
|
||
<div style="background: #fee; border-left: 4px solid #dc3545; padding: 0.75rem 1rem; margin-top: 1rem; border-radius: 4px;">
|
||
<span style="color: #dc3545; font-size: 0.875rem; font-weight: 500;">@_popupValidationMessage</span>
|
||
</div>
|
||
}
|
||
</BodyContentTemplate>
|
||
<FooterContentTemplate>
|
||
<div class="d-flex gap-2 justify-content-between w-100" style="padding: 0.5rem 0;">
|
||
<button class="btn btn-outline-secondary"
|
||
@onclick="RenewSignatureAsync"
|
||
style="border-radius: 6px; padding: 0.625rem 1.25rem; font-weight: 500;">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
|
||
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
||
</svg>
|
||
Erneuern
|
||
</button>
|
||
<button class="btn btn-primary"
|
||
@onclick="SaveSignatureAsync"
|
||
style="background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%); border: none; border-radius: 6px; padding: 0.625rem 2rem; font-weight: 600; box-shadow: 0 2px 4px rgba(79, 70, 229, 0.3);">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
||
</svg>
|
||
Speichern
|
||
</button>
|
||
</div>
|
||
</FooterContentTemplate>
|
||
</DxPopup>
|
||
|
||
@code {
|
||
// Signature tab constants
|
||
const string SignatureTabDraw = "draw";
|
||
const string SignatureTabText = "text";
|
||
const string SignatureTabImage = "image";
|
||
const string DrawCanvasId = "envelope-signature-pad";
|
||
const string TypedCanvasId = "envelope-typed-signature-pad";
|
||
const string ImageInputId = "envelope-signature-image-input";
|
||
const string ImageCanvasId = "envelope-image-signature-pad";
|
||
|
||
readonly (string Text, string Value)[] TypedSignatureFonts = {
|
||
("Brush Script", "'Brush Script MT', cursive"),
|
||
("Segoe Script", "'Segoe Script', cursive"),
|
||
("Lucida Handwriting", "'Lucida Handwriting', cursive"),
|
||
("Comic Sans", "'Comic Sans MS', cursive"),
|
||
("Cursive", "cursive")
|
||
};
|
||
|
||
[Parameter] public string? EnvelopeKey { get; set; }
|
||
|
||
bool _isLoading = true;
|
||
string? _errorMessage;
|
||
string? _pdfDataUrl;
|
||
bool _pdfLoaded = false;
|
||
int _currentPage = 1;
|
||
int _totalPages = 0;
|
||
int _currentZoom = 150;
|
||
bool _showThumbnails = true;
|
||
DotNetObjectReference<EnvelopeViewer>? _dotNetRef;
|
||
IReadOnlyList<SignatureDto> _signatures = [];
|
||
|
||
// Signature navigation state
|
||
int _totalSignatures = 0;
|
||
int _signedSignatures = 0;
|
||
int _unsignedSignatures = 0;
|
||
|
||
// Signature state
|
||
record SignatureCapture(string DataUrl, string FullName, string Position, string Place);
|
||
SignatureCapture? _capturedSignature;
|
||
bool _signaturePopupVisible = false;
|
||
string? _popupValidationMessage;
|
||
string _activeSignatureTab = SignatureTabDraw;
|
||
string _typedSignatureText = string.Empty;
|
||
string _typedSignatureFont = "'Brush Script MT', cursive";
|
||
string _signerFullName = string.Empty;
|
||
string _signerPosition = string.Empty;
|
||
string _signaturePlace = string.Empty;
|
||
|
||
// 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() {
|
||
if (string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||
_errorMessage = "Envelope-Schlüssel fehlt.";
|
||
_isLoading = false;
|
||
return;
|
||
}
|
||
|
||
try {
|
||
var (pdfBytes, statusCode) = await DocumentService.GetDocumentAsync(EnvelopeKey);
|
||
|
||
if (pdfBytes is { Length: > 0 }) {
|
||
var base64 = Convert.ToBase64String(pdfBytes);
|
||
_pdfDataUrl = $"data:application/pdf;base64,{base64}";
|
||
} else {
|
||
_errorMessage = $"Dokument konnte nicht geladen werden. HTTP Status: {statusCode}";
|
||
}
|
||
|
||
var signatures = await SignatureService.GetAsync(EnvelopeKey);
|
||
_signatures = signatures.Convert(UnitOfLength.Point);
|
||
|
||
await JSRuntime.InvokeVoidAsync("console.log", "Loaded signatures:", _signatures);
|
||
|
||
// Open signature popup on page load
|
||
_activeSignatureTab = SignatureTabDraw;
|
||
_signaturePopupVisible = true;
|
||
_popupValidationMessage = null;
|
||
|
||
} catch (Exception ex) {
|
||
_errorMessage = $"Fehler: {ex.Message}";
|
||
}
|
||
|
||
_isLoading = false;
|
||
await InvokeAsync(StateHasChanged);
|
||
}
|
||
|
||
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)) {
|
||
await Task.Delay(500);
|
||
|
||
try {
|
||
_dotNetRef = DotNetObjectReference.Create(this);
|
||
|
||
// Send quality options to JavaScript
|
||
var options = PdfViewerOptions.Value;
|
||
await JSRuntime.InvokeVoidAsync("pdfViewer.setQualityOptions", new
|
||
{
|
||
options.ThumbnailBaseScale,
|
||
options.ThumbnailEnableHiDPI,
|
||
options.ThumbnailMaxDPR,
|
||
options.MainCanvasEnableHiDPI,
|
||
options.MainCanvasMaxDPR,
|
||
options.EnableSmoothZoom,
|
||
options.ZoomTransitionDuration,
|
||
options.RenderingOpacity,
|
||
options.ZoomStepPercentage
|
||
});
|
||
|
||
var success = await JSRuntime.InvokeAsync<bool>("pdfViewer.initialize", "pdf-canvas", _pdfDataUrl, _dotNetRef);
|
||
|
||
if (success) {
|
||
_pdfLoaded = true;
|
||
_totalPages = await JSRuntime.InvokeAsync<int>("pdfViewer.getTotalPages");
|
||
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
|
||
|
||
// Attach resize listeners
|
||
await JSRuntime.InvokeVoidAsync("pdfViewer.attachResizeListeners", _dotNetRef);
|
||
|
||
|
||
await InvokeAsync(StateHasChanged);
|
||
|
||
// Wait for DOM to be ready, then render thumbnails
|
||
await Task.Delay(100);
|
||
await RenderThumbnailsAsync();
|
||
|
||
// Render signature buttons
|
||
await RenderSignatureButtonsAsync();
|
||
}
|
||
} catch (Exception ex) {
|
||
_errorMessage = $"PDF.js Fehler: {ex.Message}";
|
||
await InvokeAsync(StateHasChanged);
|
||
}
|
||
}
|
||
}
|
||
|
||
[JSInvokable]
|
||
public async Task OnZoomChanged(double scale)
|
||
{
|
||
_currentZoom = (int)(scale * 100);
|
||
await InvokeAsync(StateHasChanged);
|
||
|
||
// Re-render signature buttons when zoom changes
|
||
await Task.Delay(100);
|
||
await RenderSignatureButtonsAsync();
|
||
}
|
||
|
||
async Task NextPage() {
|
||
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.nextPage")) {
|
||
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
|
||
await RenderSignatureButtonsAsync();
|
||
}
|
||
}
|
||
|
||
async Task PreviousPage() {
|
||
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.previousPage")) {
|
||
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
|
||
await RenderSignatureButtonsAsync();
|
||
}
|
||
}
|
||
|
||
async Task ZoomIn() {
|
||
if (_currentZoom >= 300) return;
|
||
await JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn");
|
||
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
|
||
_currentZoom = (int)(scale * 100);
|
||
}
|
||
|
||
async Task ZoomOut() {
|
||
if (_currentZoom <= 50) return;
|
||
await JSRuntime.InvokeVoidAsync("pdfViewer.zoomOut");
|
||
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
|
||
_currentZoom = (int)(scale * 100);
|
||
}
|
||
|
||
async Task SetZoom(int percentage) {
|
||
var scale = percentage / 100.0;
|
||
await JSRuntime.InvokeVoidAsync("pdfViewer.setScale", scale);
|
||
_currentZoom = percentage;
|
||
}
|
||
|
||
async Task OnZoomSliderChanged(ChangeEventArgs e) {
|
||
if (int.TryParse(e.Value?.ToString(), out var zoom)) {
|
||
await SetZoom(zoom);
|
||
}
|
||
}
|
||
|
||
async Task OnPageInputChanged(ChangeEventArgs e) {
|
||
if (int.TryParse(e.Value?.ToString(), out var pageNum) && pageNum >= 1 && pageNum <= _totalPages) {
|
||
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.goToPage", pageNum)) {
|
||
_currentPage = pageNum;
|
||
}
|
||
}
|
||
}
|
||
|
||
async Task FitToWidth() {
|
||
await JSRuntime.InvokeVoidAsync("pdfViewer.fitToWidth");
|
||
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
|
||
_currentZoom = (int)(scale * 100);
|
||
}
|
||
|
||
async Task ToggleThumbnails() {
|
||
_showThumbnails = !_showThumbnails;
|
||
|
||
// Re-render thumbnails when showing them
|
||
if (_showThumbnails && _pdfLoaded) {
|
||
await InvokeAsync(StateHasChanged); // Force UI update first
|
||
await Task.Delay(150); // Wait for DOM to render canvas elements
|
||
await RenderThumbnailsAsync();
|
||
}
|
||
}
|
||
|
||
async Task GoToPageFromThumbnail(int pageNum) {
|
||
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.goToPage", pageNum)) {
|
||
_currentPage = pageNum;
|
||
await RenderSignatureButtonsAsync();
|
||
}
|
||
}
|
||
|
||
async Task RenderSignatureButtonsAsync() {
|
||
if (_signatures.Count == 0 || !_pdfLoaded) return;
|
||
|
||
try {
|
||
await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons", _signatures, _currentPage, _dotNetRef);
|
||
await UpdateSignatureCounterAsync();
|
||
} catch (Exception ex) {
|
||
System.Diagnostics.Debug.WriteLine($"Signature button rendering error: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
[JSInvokable]
|
||
public async Task OnSignatureButtonClick(int signatureId) {
|
||
if (_capturedSignature == null) {
|
||
// No signature captured yet - should not happen as popup is shown on page load
|
||
return;
|
||
}
|
||
|
||
// Apply signature to PDF canvas
|
||
await JSRuntime.InvokeVoidAsync("pdfViewer.applySignature",
|
||
signatureId,
|
||
_capturedSignature.DataUrl,
|
||
_capturedSignature.FullName,
|
||
_capturedSignature.Position,
|
||
_capturedSignature.Place);
|
||
|
||
// Update counter
|
||
await UpdateSignatureCounterAsync();
|
||
}
|
||
|
||
[JSInvokable]
|
||
public async Task OnSignatureNavChanged() {
|
||
await UpdateSignatureCounterAsync();
|
||
}
|
||
|
||
[JSInvokable]
|
||
public async Task OnPageChangedBySignatureNav(int newPage) {
|
||
_currentPage = newPage;
|
||
await RenderSignatureButtonsAsync();
|
||
}
|
||
|
||
async Task UpdateSignatureCounterAsync() {
|
||
try {
|
||
var state = await JSRuntime.InvokeAsync<SignatureNavState>("pdfViewer.getSignatureNavState");
|
||
_totalSignatures = state.Total;
|
||
_signedSignatures = state.Signed;
|
||
_unsignedSignatures = state.Unsigned;
|
||
await InvokeAsync(StateHasChanged);
|
||
} catch {
|
||
// Ignore errors during counter update
|
||
}
|
||
}
|
||
|
||
async Task GoToPreviousSignature() {
|
||
await JSRuntime.InvokeVoidAsync("pdfViewer.goToPreviousSignature", _dotNetRef);
|
||
}
|
||
|
||
async Task GoToNextSignature() {
|
||
await JSRuntime.InvokeVoidAsync("pdfViewer.goToNextSignature", _dotNetRef);
|
||
}
|
||
|
||
record SignatureNavState(int Total, int Signed, int Unsigned, int CurrentIndex, bool CanGoPrev, bool CanGoNext);
|
||
|
||
// Signature popup methods
|
||
void OpenSignaturePopup() {
|
||
_activeSignatureTab = SignatureTabDraw;
|
||
_signaturePopupVisible = true;
|
||
_popupValidationMessage = null;
|
||
}
|
||
|
||
async Task OnPopupShownAsync() {
|
||
await InitializeActiveSignatureTabAsync();
|
||
}
|
||
|
||
async Task SetSignatureTabAsync(string tab) {
|
||
_activeSignatureTab = tab;
|
||
_popupValidationMessage = null;
|
||
await InvokeAsync(StateHasChanged);
|
||
await Task.Delay(50);
|
||
await InitializeActiveSignatureTabAsync();
|
||
}
|
||
|
||
async Task InitializeActiveSignatureTabAsync() {
|
||
if(_activeSignatureTab == SignatureTabDraw) {
|
||
await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", DrawCanvasId);
|
||
} else if(_activeSignatureTab == SignatureTabText) {
|
||
await JSRuntime.InvokeVoidAsync("receiverSignature.initializeTyped", TypedCanvasId);
|
||
await RenderTypedSignatureAsync();
|
||
} else {
|
||
await JSRuntime.InvokeVoidAsync("receiverSignature.initializeImage", ImageInputId, ImageCanvasId);
|
||
}
|
||
}
|
||
|
||
async Task RenewSignatureAsync() {
|
||
_popupValidationMessage = null;
|
||
|
||
if(_activeSignatureTab == SignatureTabDraw) {
|
||
await JSRuntime.InvokeVoidAsync("receiverSignature.clear", DrawCanvasId);
|
||
} else if(_activeSignatureTab == SignatureTabText) {
|
||
_typedSignatureText = string.Empty;
|
||
await JSRuntime.InvokeVoidAsync("receiverSignature.clearTyped", TypedCanvasId);
|
||
} else {
|
||
await JSRuntime.InvokeVoidAsync("receiverSignature.clearImage", ImageInputId, ImageCanvasId);
|
||
}
|
||
}
|
||
|
||
async Task OnTypedSignatureChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) {
|
||
_typedSignatureText = args.Value?.ToString() ?? string.Empty;
|
||
await RenderTypedSignatureAsync();
|
||
}
|
||
|
||
async Task OnTypedSignatureFontChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) {
|
||
_typedSignatureFont = args.Value?.ToString() ?? _typedSignatureFont;
|
||
await RenderTypedSignatureAsync();
|
||
}
|
||
|
||
async Task RenderTypedSignatureAsync() {
|
||
await JSRuntime.InvokeVoidAsync("receiverSignature.renderTypedSignature", TypedCanvasId, _typedSignatureText, _typedSignatureFont);
|
||
}
|
||
|
||
async Task SaveSignatureAsync() {
|
||
if (string.IsNullOrWhiteSpace(_signerFullName)) {
|
||
_popupValidationMessage = "Bitte geben Sie Vor- und Nachname ein.";
|
||
return;
|
||
}
|
||
if (string.IsNullOrWhiteSpace(_signaturePlace)) {
|
||
_popupValidationMessage = "Bitte geben Sie den Ort ein.";
|
||
return;
|
||
}
|
||
var signatureDataUrl = await GetActiveSignatureDataUrlAsync();
|
||
if (string.IsNullOrWhiteSpace(signatureDataUrl)) {
|
||
_popupValidationMessage = "Die Unterschrift ist erforderlich.";
|
||
return;
|
||
}
|
||
|
||
_popupValidationMessage = null;
|
||
_capturedSignature = new(signatureDataUrl, _signerFullName.Trim(), _signerPosition.Trim(), _signaturePlace.Trim());
|
||
_signaturePopupVisible = false;
|
||
|
||
await InvokeAsync(StateHasChanged);
|
||
Console.WriteLine($"Signature saved: {_signerFullName}, {_signaturePlace}");
|
||
}
|
||
|
||
async Task<string?> GetActiveSignatureDataUrlAsync() {
|
||
if(_activeSignatureTab == SignatureTabDraw)
|
||
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getDataUrl", DrawCanvasId);
|
||
|
||
if(_activeSignatureTab == SignatureTabText) {
|
||
await RenderTypedSignatureAsync();
|
||
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getTypedDataUrl", TypedCanvasId);
|
||
}
|
||
|
||
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getImageDataUrl", ImageCanvasId);
|
||
}
|
||
|
||
async Task RenderThumbnailsAsync() {
|
||
try {
|
||
var delay = PdfViewerOptions.Value.ThumbnailRenderDelay;
|
||
|
||
// Sequential rendering to avoid overwhelming the browser
|
||
for (int i = 1; i <= _totalPages; i++) {
|
||
await JSRuntime.InvokeVoidAsync("pdfViewer.renderThumbnail", i, $"thumb-canvas-{i}");
|
||
|
||
// Configurable delay between renders
|
||
if (i < _totalPages) {
|
||
await Task.Delay(delay);
|
||
}
|
||
}
|
||
} catch (Exception ex) {
|
||
// Thumbnail rendering is not critical
|
||
System.Diagnostics.Debug.WriteLine($"Thumbnail rendering error: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// 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() {
|
||
if (_pdfLoaded) {
|
||
try {
|
||
await JSRuntime.InvokeVoidAsync("pdfViewer.dispose");
|
||
} catch {
|
||
// Ignore errors during disposal
|
||
}
|
||
}
|
||
_dotNetRef?.Dispose();
|
||
}
|
||
}
|