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