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.Resources
|
||||||
@using EnvelopeGenerator.Application.Common.Dto
|
@using EnvelopeGenerator.Application.Common.Dto
|
||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
@using Microsoft.Extensions.Localization
|
@using Microsoft.Extensions.Localization
|
||||||
@inject IStringLocalizer<Resource> UiLocalizer
|
@inject IStringLocalizer<Resource> UiLocalizer
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@@ -11,18 +12,26 @@
|
|||||||
<div class="sender-image-editor__toolbar-host">
|
<div class="sender-image-editor__toolbar-host">
|
||||||
<SenderPdfImageEditorToolbar CurrentPage="@_currentPage"
|
<SenderPdfImageEditorToolbar CurrentPage="@_currentPage"
|
||||||
TotalPages="@PageCount"
|
TotalPages="@PageCount"
|
||||||
ZoomPercent="112"
|
ZoomPercent="@_zoomPercent"
|
||||||
|
MinZoomPercent="@MinZoomPercent"
|
||||||
|
MaxZoomPercent="@MaxZoomPercent"
|
||||||
GoFirstPage="OnGoFirstPageAsync"
|
GoFirstPage="OnGoFirstPageAsync"
|
||||||
GoPreviousPage="OnGoPreviousPageAsync"
|
GoPreviousPage="OnGoPreviousPageAsync"
|
||||||
GoNextPage="OnGoNextPageAsync"
|
GoNextPage="OnGoNextPageAsync"
|
||||||
GoLastPage="OnGoLastPageAsync"
|
GoLastPage="OnGoLastPageAsync"
|
||||||
GoToPage="OnGoToPageAsync" />
|
GoToPage="OnGoToPageAsync"
|
||||||
|
ZoomOut="OnZoomOutAsync"
|
||||||
|
ZoomIn="OnZoomInAsync"
|
||||||
|
SetZoomPercent="OnSetZoomPercentAsync" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="pdf-editor-wrapper sender-image-editor sender-editor-pdf-viewer"
|
<div class="pdf-editor-wrapper sender-image-editor sender-editor-pdf-viewer"
|
||||||
style="cursor: @(IsPlacementMode ? "crosshair" : (_isDragging ? "grabbing" : "default"));"
|
style="cursor: @(IsPlacementMode ? "crosshair" : (_isDragging ? "grabbing" : "default"));"
|
||||||
|
tabindex="0"
|
||||||
@onclick="HandleClickAsync"
|
@onclick="HandleClickAsync"
|
||||||
|
@onwheel="OnEditorWheelAsync"
|
||||||
|
@onwheel:preventDefault="true"
|
||||||
@onscroll="OnEditorScrollAsync"
|
@onscroll="OnEditorScrollAsync"
|
||||||
@onmousemove="OnEditorMouseMoveAsync"
|
@onmousemove="OnEditorMouseMoveAsync"
|
||||||
@onmouseup="OnEditorMouseUp"
|
@onmouseup="OnEditorMouseUp"
|
||||||
@@ -35,14 +44,14 @@
|
|||||||
{
|
{
|
||||||
@if (Pages.Count == 0 && !string.IsNullOrWhiteSpace(PdfDataUrl))
|
@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"
|
<iframe class="sender-image-editor__native-frame"
|
||||||
src="@PdfDataUrl"
|
src="@PdfDataUrl"
|
||||||
title="PDF Preview"></iframe>
|
title="PDF Preview"></iframe>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="sender-image-editor__pages">
|
<div class="sender-image-editor__pages" style="@GetZoomStyle()">
|
||||||
@for (var pageIndex = 0; pageIndex < PageCount; pageIndex++)
|
@for (var pageIndex = 0; pageIndex < PageCount; pageIndex++)
|
||||||
{
|
{
|
||||||
var pagePreview = pageIndex < Pages.Count ? Pages[pageIndex] : null;
|
var pagePreview = pageIndex < Pages.Count ? Pages[pageIndex] : null;
|
||||||
@@ -128,6 +137,12 @@
|
|||||||
private double _grabOffsetXPt;
|
private double _grabOffsetXPt;
|
||||||
private double _grabOffsetYPt;
|
private double _grabOffsetYPt;
|
||||||
private int _currentPage = 1;
|
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)
|
private static string GetFieldStyle(SenderPdfFieldOverlay field, double pageWidthPt, double pageHeightPt)
|
||||||
{
|
{
|
||||||
@@ -170,6 +185,12 @@
|
|||||||
return $"aspect-ratio:{ratioText};cursor:{cursor};";
|
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()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
if (PageCount <= 0)
|
if (PageCount <= 0)
|
||||||
@@ -181,6 +202,18 @@
|
|||||||
_currentPage = Math.Max(1, Math.Min(_currentPage, PageCount));
|
_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)
|
private async Task HandleClickAsync(MouseEventArgs args)
|
||||||
{
|
{
|
||||||
if (EditorClick.HasDelegate)
|
if (EditorClick.HasDelegate)
|
||||||
@@ -296,6 +329,47 @@
|
|||||||
await InvokeAsync(StateHasChanged);
|
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)
|
private void OnEditorMouseUp(MouseEventArgs args)
|
||||||
{
|
{
|
||||||
_draggedFieldIndex = null;
|
_draggedFieldIndex = null;
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sender-image-toolbar__group sender-image-toolbar__group--zoom" role="group" aria-label="Zoom controls">
|
<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">
|
<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" />
|
<path d="M6 12H18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -68,13 +68,16 @@
|
|||||||
<span>@ZoomPercentDisplay%</span>
|
<span>@ZoomPercentDisplay%</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" class="sender-image-toolbar__icon-btn sender-image-toolbar__icon-btn--caret" title="Zoom presets" disabled>
|
<input type="range"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
class="sender-image-toolbar__zoom-slider"
|
||||||
<path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
min="@MinZoomPercent"
|
||||||
</svg>
|
max="@MaxZoomPercent"
|
||||||
</button>
|
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">
|
<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="M12 6V18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
<path d="M6 12H18" 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]
|
[Parameter]
|
||||||
public int ZoomPercent { get; set; } = 112;
|
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]
|
[Parameter]
|
||||||
public EventCallback GoFirstPage { get; set; }
|
public EventCallback GoFirstPage { get; set; }
|
||||||
|
|
||||||
@@ -126,11 +138,22 @@
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<int> GoToPage { get; set; }
|
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 CurrentPageDisplay => CurrentPage > 0 ? CurrentPage : 0;
|
||||||
private int TotalPagesDisplay => TotalPages > 0 ? TotalPages : 0;
|
private int TotalPagesDisplay => TotalPages > 0 ? TotalPages : 0;
|
||||||
private int ZoomPercentDisplay => ZoomPercent > 0 ? ZoomPercent : 100;
|
private int ZoomPercentDisplay => ZoomPercent > 0 ? ZoomPercent : 100;
|
||||||
private bool CanGoPrevious => CurrentPageDisplay > 1;
|
private bool CanGoPrevious => CurrentPageDisplay > 1;
|
||||||
private bool CanGoNext => TotalPagesDisplay > 0 && CurrentPageDisplay < TotalPagesDisplay;
|
private bool CanGoNext => TotalPagesDisplay > 0 && CurrentPageDisplay < TotalPagesDisplay;
|
||||||
|
private bool CanZoomOut => ZoomPercentDisplay > MinZoomPercent;
|
||||||
|
private bool CanZoomIn => ZoomPercentDisplay < MaxZoomPercent;
|
||||||
|
|
||||||
private async Task GoToFirstAsync()
|
private async Task GoToFirstAsync()
|
||||||
{
|
{
|
||||||
@@ -179,4 +202,28 @@
|
|||||||
page = Math.Max(1, Math.Min(page, Math.Max(1, TotalPagesDisplay)));
|
page = Math.Max(1, Math.Min(page, Math.Max(1, TotalPagesDisplay)));
|
||||||
await GoToPage.InvokeAsync(page);
|
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 raw = bestPage.getAttribute('data-editor-page');
|
||||||
const parsed = Number.parseInt(raw || '0', 10);
|
const parsed = Number.parseInt(raw || '0', 10);
|
||||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
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