Add zoom functionality to SenderPdfImageEditor

Introduced zoom controls to the `SenderPdfImageEditor` component, including zoom in/out, dynamic zoom levels, and a slider in the toolbar.

- Added zoom-related properties and methods to manage zoom levels.
- Updated the toolbar with zoom buttons and a slider for precise control.
- Enhanced JavaScript to handle zoom overrides and smooth scrolling.
- Modified UI elements to dynamically apply zoom styles.
- Improved accessibility and usability with appropriate attributes.
- Refactored code to integrate zoom functionality seamlessly.

These changes enhance the user experience by providing greater control over PDF viewing and editing.
This commit is contained in:
2026-09-30 16:41:57 +02:00
parent 8386b85592
commit e2322b41cd
3 changed files with 172 additions and 11 deletions

View File

@@ -1,6 +1,7 @@
@using EnvelopeGenerator.Application.Resources
@using EnvelopeGenerator.Application.Common.Dto
@using System.Globalization
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<Resource> UiLocalizer
@inject IJSRuntime JSRuntime
@@ -11,18 +12,26 @@
<div class="sender-image-editor__toolbar-host">
<SenderPdfImageEditorToolbar CurrentPage="@_currentPage"
TotalPages="@PageCount"
ZoomPercent="112"
ZoomPercent="@_zoomPercent"
MinZoomPercent="@MinZoomPercent"
MaxZoomPercent="@MaxZoomPercent"
GoFirstPage="OnGoFirstPageAsync"
GoPreviousPage="OnGoPreviousPageAsync"
GoNextPage="OnGoNextPageAsync"
GoLastPage="OnGoLastPageAsync"
GoToPage="OnGoToPageAsync" />
GoToPage="OnGoToPageAsync"
ZoomOut="OnZoomOutAsync"
ZoomIn="OnZoomInAsync"
SetZoomPercent="OnSetZoomPercentAsync" />
</div>
}
<div class="pdf-editor-wrapper sender-image-editor sender-editor-pdf-viewer"
style="cursor: @(IsPlacementMode ? "crosshair" : (_isDragging ? "grabbing" : "default"));"
tabindex="0"
@onclick="HandleClickAsync"
@onwheel="OnEditorWheelAsync"
@onwheel:preventDefault="true"
@onscroll="OnEditorScrollAsync"
@onmousemove="OnEditorMouseMoveAsync"
@onmouseup="OnEditorMouseUp"
@@ -35,14 +44,14 @@
{
@if (Pages.Count == 0 && !string.IsNullOrWhiteSpace(PdfDataUrl))
{
<div class="sender-image-editor__native-preview">
<div class="sender-image-editor__native-preview" style="@GetZoomStyle()">
<iframe class="sender-image-editor__native-frame"
src="@PdfDataUrl"
title="PDF Preview"></iframe>
</div>
}
<div class="sender-image-editor__pages">
<div class="sender-image-editor__pages" style="@GetZoomStyle()">
@for (var pageIndex = 0; pageIndex < PageCount; pageIndex++)
{
var pagePreview = pageIndex < Pages.Count ? Pages[pageIndex] : null;
@@ -128,6 +137,12 @@
private double _grabOffsetXPt;
private double _grabOffsetYPt;
private int _currentPage = 1;
private int _zoomPercent = 112;
private bool _zoomOverrideInitialized;
private const int MinZoomPercent = 50;
private const int MaxZoomPercent = 300;
private const int ZoomStepPercent = 5;
private static string GetFieldStyle(SenderPdfFieldOverlay field, double pageWidthPt, double pageHeightPt)
{
@@ -170,6 +185,12 @@
return $"aspect-ratio:{ratioText};cursor:{cursor};";
}
private string GetZoomStyle()
{
var factor = (_zoomPercent / 100.0).ToString("F4", CultureInfo.InvariantCulture);
return $"zoom:{factor};";
}
protected override void OnParametersSet()
{
if (PageCount <= 0)
@@ -181,6 +202,18 @@
_currentPage = Math.Max(1, Math.Min(_currentPage, PageCount));
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (_zoomOverrideInitialized)
return;
await JSRuntime.InvokeVoidAsync(
"envelopeEditor.ensureZoomOverride",
"sender-editor-pdf-viewer");
_zoomOverrideInitialized = true;
}
private async Task HandleClickAsync(MouseEventArgs args)
{
if (EditorClick.HasDelegate)
@@ -296,6 +329,47 @@
await InvokeAsync(StateHasChanged);
}
private async Task OnZoomOutAsync()
{
SetZoomPercent(_zoomPercent - ZoomStepPercent);
await InvokeAsync(StateHasChanged);
}
private async Task OnZoomInAsync()
{
SetZoomPercent(_zoomPercent + ZoomStepPercent);
await InvokeAsync(StateHasChanged);
}
private async Task OnSetZoomPercentAsync(int zoomPercent)
{
SetZoomPercent(zoomPercent);
await InvokeAsync(StateHasChanged);
}
private async Task OnEditorWheelAsync(WheelEventArgs args)
{
if (args.CtrlKey)
{
if (args.DeltaY < 0)
await OnZoomInAsync();
else if (args.DeltaY > 0)
await OnZoomOutAsync();
return;
}
await JSRuntime.InvokeVoidAsync(
"envelopeEditor.scrollPdfViewerByDelta",
"sender-editor-pdf-viewer",
args.DeltaY);
}
private void SetZoomPercent(int value)
{
_zoomPercent = Math.Max(MinZoomPercent, Math.Min(value, MaxZoomPercent));
}
private void OnEditorMouseUp(MouseEventArgs args)
{
_draggedFieldIndex = null;

View File

@@ -58,7 +58,7 @@
</div>
<div class="sender-image-toolbar__group sender-image-toolbar__group--zoom" role="group" aria-label="Zoom controls">
<button type="button" class="sender-image-toolbar__icon-btn" title="Zoom out" disabled>
<button type="button" class="sender-image-toolbar__icon-btn" title="Zoom out" disabled="@(!CanZoomOut)" @onclick="ZoomOutAsync">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M6 12H18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
@@ -68,13 +68,16 @@
<span>@ZoomPercentDisplay%</span>
</div>
<button type="button" class="sender-image-toolbar__icon-btn sender-image-toolbar__icon-btn--caret" title="Zoom presets" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<input type="range"
class="sender-image-toolbar__zoom-slider"
min="@MinZoomPercent"
max="@MaxZoomPercent"
step="@ZoomStepPercent"
value="@ZoomPercentDisplay"
@oninput="OnZoomSliderInputAsync"
aria-label="Zoom slider" />
<button type="button" class="sender-image-toolbar__icon-btn" title="Zoom in" disabled>
<button type="button" class="sender-image-toolbar__icon-btn" title="Zoom in" disabled="@(!CanZoomIn)" @onclick="ZoomInAsync">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 6V18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
<path d="M6 12H18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
@@ -111,6 +114,15 @@
[Parameter]
public int ZoomPercent { get; set; } = 112;
[Parameter]
public int MinZoomPercent { get; set; } = 50;
[Parameter]
public int MaxZoomPercent { get; set; } = 300;
[Parameter]
public int ZoomStepPercent { get; set; } = 5;
[Parameter]
public EventCallback GoFirstPage { get; set; }
@@ -126,11 +138,22 @@
[Parameter]
public EventCallback<int> GoToPage { get; set; }
[Parameter]
public EventCallback ZoomOut { get; set; }
[Parameter]
public EventCallback ZoomIn { get; set; }
[Parameter]
public EventCallback<int> SetZoomPercent { get; set; }
private int CurrentPageDisplay => CurrentPage > 0 ? CurrentPage : 0;
private int TotalPagesDisplay => TotalPages > 0 ? TotalPages : 0;
private int ZoomPercentDisplay => ZoomPercent > 0 ? ZoomPercent : 100;
private bool CanGoPrevious => CurrentPageDisplay > 1;
private bool CanGoNext => TotalPagesDisplay > 0 && CurrentPageDisplay < TotalPagesDisplay;
private bool CanZoomOut => ZoomPercentDisplay > MinZoomPercent;
private bool CanZoomIn => ZoomPercentDisplay < MaxZoomPercent;
private async Task GoToFirstAsync()
{
@@ -179,4 +202,28 @@
page = Math.Max(1, Math.Min(page, Math.Max(1, TotalPagesDisplay)));
await GoToPage.InvokeAsync(page);
}
private async Task ZoomOutAsync()
{
if (ZoomOut.HasDelegate)
await ZoomOut.InvokeAsync();
}
private async Task ZoomInAsync()
{
if (ZoomIn.HasDelegate)
await ZoomIn.InvokeAsync();
}
private async Task OnZoomSliderInputAsync(ChangeEventArgs args)
{
if (!SetZoomPercent.HasDelegate)
return;
if (!int.TryParse(args.Value?.ToString(), out var zoomPercent))
return;
zoomPercent = Math.Max(MinZoomPercent, Math.Min(zoomPercent, MaxZoomPercent));
await SetZoomPercent.InvokeAsync(zoomPercent);
}
}

View File

@@ -185,5 +185,45 @@ window.envelopeEditor = {
const raw = bestPage.getAttribute('data-editor-page');
const parsed = Number.parseInt(raw || '0', 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
},
scrollPdfViewerByDelta: function (viewerCssClass, deltaY) {
const viewer = document.querySelector('.' + viewerCssClass);
if (!viewer) {
return;
}
viewer.scrollTop += Number(deltaY) || 0;
},
ensureZoomOverride: function (viewerCssClass) {
const viewer = document.querySelector('.' + viewerCssClass);
if (!viewer) {
return;
}
if (viewer.dataset.zoomOverrideAttached === '1') {
return;
}
viewer.dataset.zoomOverrideAttached = '1';
viewer.addEventListener('wheel', function (event) {
event.preventDefault();
}, { passive: false });
viewer.addEventListener('keydown', function (event) {
if (!event.ctrlKey) {
return;
}
const key = event.key;
if (key === '+' || key === '=' || key === '-' || key === '0') {
event.preventDefault();
}
});
viewer.addEventListener('touchmove', function () {
}, { passive: true });
}
};