diff --git a/examples/Cropper.Blazor.MAUI.Net6/Cropper.Blazor.MAUI.Net6.csproj b/examples/Cropper.Blazor.MAUI.Net6/Cropper.Blazor.MAUI.Net6.csproj index d394fa69..9ee658db 100644 --- a/examples/Cropper.Blazor.MAUI.Net6/Cropper.Blazor.MAUI.Net6.csproj +++ b/examples/Cropper.Blazor.MAUI.Net6/Cropper.Blazor.MAUI.Net6.csproj @@ -50,7 +50,7 @@ - + diff --git a/examples/Cropper.Blazor.MAUI.Net7/Cropper.Blazor.MAUI.Net7.csproj b/examples/Cropper.Blazor.MAUI.Net7/Cropper.Blazor.MAUI.Net7.csproj index cfcbcb7a..a88302a5 100644 --- a/examples/Cropper.Blazor.MAUI.Net7/Cropper.Blazor.MAUI.Net7.csproj +++ b/examples/Cropper.Blazor.MAUI.Net7/Cropper.Blazor.MAUI.Net7.csproj @@ -51,7 +51,7 @@ - + diff --git a/examples/Cropper.Blazor.Server.Net6/Cropper.Blazor.Server.Net6.csproj b/examples/Cropper.Blazor.Server.Net6/Cropper.Blazor.Server.Net6.csproj index 4566cee8..cc8a04b5 100644 --- a/examples/Cropper.Blazor.Server.Net6/Cropper.Blazor.Server.Net6.csproj +++ b/examples/Cropper.Blazor.Server.Net6/Cropper.Blazor.Server.Net6.csproj @@ -7,7 +7,7 @@ - + diff --git a/examples/Cropper.Blazor.Server.Net7/Cropper.Blazor.Server.Net7.csproj b/examples/Cropper.Blazor.Server.Net7/Cropper.Blazor.Server.Net7.csproj index 3ea12789..1a2efe86 100644 --- a/examples/Cropper.Blazor.Server.Net7/Cropper.Blazor.Server.Net7.csproj +++ b/examples/Cropper.Blazor.Server.Net7/Cropper.Blazor.Server.Net7.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -7,7 +7,7 @@ - + diff --git a/examples/Cropper.MVC.With.Blazor.Server.Net7/Cropper.MVC.With.Blazor.Server.Net7.csproj b/examples/Cropper.MVC.With.Blazor.Server.Net7/Cropper.MVC.With.Blazor.Server.Net7.csproj index 6b5a6dab..bf7f45f2 100644 --- a/examples/Cropper.MVC.With.Blazor.Server.Net7/Cropper.MVC.With.Blazor.Server.Net7.csproj +++ b/examples/Cropper.MVC.With.Blazor.Server.Net7/Cropper.MVC.With.Blazor.Server.Net7.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Cropper.Blazor/Client/Components/AspectRatioSettings.razor b/src/Cropper.Blazor/Client/Components/AspectRatioSettings.razor new file mode 100644 index 00000000..6f039b66 --- /dev/null +++ b/src/Cropper.Blazor/Client/Components/AspectRatioSettings.razor @@ -0,0 +1,19 @@ + + + Aspect Ratio Settings (only for FREE aspect ratio) + + + + + + Current aspect ratio: @AspectRatio + + + diff --git a/src/Cropper.Blazor/Client/Components/AspectRatioSettings.razor.cs b/src/Cropper.Blazor/Client/Components/AspectRatioSettings.razor.cs new file mode 100644 index 00000000..8d2535aa --- /dev/null +++ b/src/Cropper.Blazor/Client/Components/AspectRatioSettings.razor.cs @@ -0,0 +1,89 @@ +using System.ComponentModel.DataAnnotations; +using Cropper.Blazor.Client.Pages; +using Cropper.Blazor.Models; +using Microsoft.AspNetCore.Components; + +namespace Cropper.Blazor.Client.Components +{ + public partial class AspectRatioSettings + { + private decimal? maxAspectRatio; + private decimal? minAspectRatio; + private bool isEnableAspectRatioSettings; + + public decimal? MaxAspectRatio + { + get => maxAspectRatio; + set + { + maxAspectRatio = value; + ApplyAspectRatioRulesForCropperAsync(); + } + } + + public decimal? MinAspectRatio + { + get => minAspectRatio; + set + { + minAspectRatio = value; + ApplyAspectRatioRulesForCropperAsync(); + } + } + + [CascadingParameter(Name = "AspectRatio"), Required] + private decimal? AspectRatio { get; set; } + + [CascadingParameter(Name = "CropperDemo"), Required] + private CropperDemo CropperDemo { get; set; } = null!; + + [CascadingParameter(Name = "IsEnableAspectRatioSettings"), Required] + private bool IsEnableAspectRatioSettings + { + get => isEnableAspectRatioSettings; + set + { + if (!value) + { + minAspectRatio = null; + maxAspectRatio = null; + } + + isEnableAspectRatioSettings = value; + } + } + + public async Task ApplyAspectRatioRulesForCropperAsync() + { + if (minAspectRatio is not null || maxAspectRatio is not null) + { + ContainerData containerData = await CropperDemo.CropperComponent!.GetContainerDataAsync(); + CropBoxData cropBoxData = await CropperDemo.CropperComponent!.GetCropBoxDataAsync(); + + if (cropBoxData.Height != 0) + { + decimal aspectRatio = cropBoxData.Width / cropBoxData.Height; + + if (aspectRatio < minAspectRatio || aspectRatio > maxAspectRatio) + { + decimal? newCropBoxWidth = cropBoxData.Height * ((minAspectRatio + maxAspectRatio) / 2); + + CropperDemo.CropperComponent!.SetCropBoxData(new SetCropBoxDataOptions + { + Left = (containerData.Width - newCropBoxWidth) / 2, + Width = newCropBoxWidth, + }); + } + + SetUpAspectRatio(aspectRatio); + } + } + } + + public void SetUpAspectRatio(decimal? aspectRatio) + { + AspectRatio = aspectRatio; + StateHasChanged(); + } + } +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Client/Components/CroppedDimensionsSettings.razor b/src/Cropper.Blazor/Client/Components/CroppedDimensionsSettings.razor new file mode 100644 index 00000000..cebea3b0 --- /dev/null +++ b/src/Cropper.Blazor/Client/Components/CroppedDimensionsSettings.razor @@ -0,0 +1,26 @@ + + + + Dimensions Settings +
This option may not work with arbitrary 'Aspect Ratios' images, it is recommended to use a free Aspect Ratio for this option or calculate the allowed values yourself
+
+ + + + + + + + +
+
diff --git a/src/Cropper.Blazor/Client/Components/CroppedDimensionsSettings.razor.cs b/src/Cropper.Blazor/Client/Components/CroppedDimensionsSettings.razor.cs new file mode 100644 index 00000000..d0673efa --- /dev/null +++ b/src/Cropper.Blazor/Client/Components/CroppedDimensionsSettings.razor.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Components; + +namespace Cropper.Blazor.Client.Components +{ + public partial class CroppedDimensionsSettings + { + private decimal? minimumWidth = null; + private decimal? maximumWidth = null; + private decimal? minimumHeight = null; + private decimal? maximumHeight = null; + + [CascadingParameter(Name = "ResetCropperAction"), Required] + public Action ResetCropperAction { get; set; } = null!; + + public decimal? MinimumWidth { get => minimumWidth; set { minimumWidth = value; ResetCropperAction.Invoke(); } } + public decimal? MaximumWidth { get => maximumWidth; set { maximumWidth = value; ResetCropperAction.Invoke(); } } + public decimal? MinimumHeight { get => minimumHeight; set { minimumHeight = value; ResetCropperAction.Invoke(); } } + public decimal? MaximumHeight { get => maximumHeight; set { maximumHeight = value; ResetCropperAction.Invoke(); } } + } +} diff --git a/src/Cropper.Blazor/Client/Components/GetSetCropperData.razor b/src/Cropper.Blazor/Client/Components/GetSetCropperData.razor index fc8583f6..e91f3b37 100644 --- a/src/Cropper.Blazor/Client/Components/GetSetCropperData.razor +++ b/src/Cropper.Blazor/Client/Components/GetSetCropperData.razor @@ -1,8 +1,10 @@ @using Cropper.Blazor.Models -@*//---Cropper Data---//*@ - + @*//---Enable setup minimum and maximum cropped dimensions---//*@ + + @*//---Cropper Data---//*@ + Cropper Data @@ -70,8 +72,9 @@ @*//---Image Data---//*@ + - + Image Data @@ -95,7 +98,7 @@ @*//---Enable setup max/min zoom ratio---//*@ - + @*//---Canvas Data---//*@ diff --git a/src/Cropper.Blazor/Client/Components/GetSetCropperData.razor.cs b/src/Cropper.Blazor/Client/Components/GetSetCropperData.razor.cs index 363fbc28..35c3868b 100644 --- a/src/Cropper.Blazor/Client/Components/GetSetCropperData.razor.cs +++ b/src/Cropper.Blazor/Client/Components/GetSetCropperData.razor.cs @@ -24,12 +24,15 @@ public partial class GetSetCropperData [Parameter, Required] public Func> GetCanvasData { get; set; } = null!; + public AspectRatioSettings AspectRatioSettings = null!; + private CropBoxData CropBoxData = null!; private CropperData CropperData = null!; private ContainerData ContainerData = null!; private ImageData ImageData = null!; private CanvasData CanvasData = null!; - private ZoomRationSettings ZoomRationSettingszoomRationSettings = null!; + private ZoomRatioSettings ZoomRatioSettings = null!; + public CroppedDimensionsSettings CroppedDimensionsSettings = null!; protected override void OnInitialized() { @@ -42,7 +45,7 @@ protected override void OnInitialized() public void OnZoomEvent(ZoomEvent? zoomEvent) { - ZoomRationSettingszoomRationSettings!.OnZoomEvent(zoomEvent); + ZoomRatioSettings!.OnZoomEvent(zoomEvent); } public void SetCropBoxData(SetCropBoxDataOptions cropBoxDataOptions) diff --git a/src/Cropper.Blazor/Client/Components/ZoomRationSettings.razor b/src/Cropper.Blazor/Client/Components/ZoomRatioSettings.razor similarity index 63% rename from src/Cropper.Blazor/Client/Components/ZoomRationSettings.razor rename to src/Cropper.Blazor/Client/Components/ZoomRatioSettings.razor index d56743e2..cedda9e3 100644 --- a/src/Cropper.Blazor/Client/Components/ZoomRationSettings.razor +++ b/src/Cropper.Blazor/Client/Components/ZoomRatioSettings.razor @@ -1,5 +1,6 @@  - + + Zoom Ratio Settings - - - Apply zoom rules for Cropper - - diff --git a/src/Cropper.Blazor/Client/Components/ZoomRatioSettings.razor.cs b/src/Cropper.Blazor/Client/Components/ZoomRatioSettings.razor.cs new file mode 100644 index 00000000..120ad49d --- /dev/null +++ b/src/Cropper.Blazor/Client/Components/ZoomRatioSettings.razor.cs @@ -0,0 +1,49 @@ +using Cropper.Blazor.Events.ZoomEvent; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace Cropper.Blazor.Client.Components +{ + public partial class ZoomRatioSettings + { + private decimal? minZoomRatio = null; + private decimal? maxZoomRatio = null; + + private decimal? MinZoomRatio + { + get => minZoomRatio; + set + { + minZoomRatio = value; + ApplyZoomRulesForCropperAsync(); + } + } + private decimal? MaxZoomRatio + { + get => maxZoomRatio; + set + { + maxZoomRatio = value; + ApplyZoomRulesForCropperAsync(); + } + } + [Inject] private IJSRuntime? JSRuntime { get; set; } + + private decimal? OldRatio { get; set; } = null; + + private decimal? Ratio { get; set; } = null; + + public void OnZoomEvent(ZoomEvent? zoomEvent) + { + OldRatio = zoomEvent?.OldRatio; + Ratio = zoomEvent?.Ratio; + + StateHasChanged(); + } + + public async Task ApplyZoomRulesForCropperAsync() + { + await JSRuntime!.InvokeVoidAsync("window.overrideOnZoomCropperEvent", MinZoomRatio, MaxZoomRatio); + } + } +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Client/Components/ZoomRationSettings.razor.cs b/src/Cropper.Blazor/Client/Components/ZoomRationSettings.razor.cs deleted file mode 100644 index 318238dd..00000000 --- a/src/Cropper.Blazor/Client/Components/ZoomRationSettings.razor.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Cropper.Blazor.Events.ZoomEvent; -using Microsoft.AspNetCore.Components; -using Microsoft.JSInterop; - -namespace Cropper.Blazor.Client.Components -{ - public partial class ZoomRationSettings - { - private decimal? MinZoomRatio = null; - private decimal? MaxZoomRatio = null; - - [Inject] private IJSRuntime? JSRuntime { get; set; } - - private decimal? OldRatio { get; set; } = null; - - private decimal? Ratio { get; set; } = null; - - public void OnZoomEvent(ZoomEvent? zoomEvent) - { - OldRatio = zoomEvent?.OldRatio; - Ratio = zoomEvent?.Ratio; - - StateHasChanged(); - } - - public async Task ApplyZoomRulesForCropperAsync() - { - await JSRuntime!.InvokeVoidAsync("window.overrideCropperJsInteropModule", MinZoomRatio, MaxZoomRatio); - } - } -} diff --git a/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj b/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj index dd59915f..0ebddae2 100644 --- a/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj +++ b/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj @@ -1,159 +1,163 @@  - - - IncludeGeneratedStaticFiles; - $(ResolveStaticWebAssetsInputsDependsOn) - - - - - false - - - - net7.0 - enable - enable - service-worker-assets.js - - - - false - false - - - - - + + + IncludeGeneratedStaticFiles; + $(ResolveStaticWebAssetsInputsDependsOn) + + + + + false + + + + net7.0 + enable + enable + service-worker-assets.js + + + + false + false + + + + + - - - + + + - - - - - - - - - - <_ContentIncludedByDefault Remove="Pages\CroppedCanvasDialog.razor" /> - - - - - $(MSBuildThisFileDirectory)../ - - - - - - - $(SolutionDir)Cropper.Blazor.Client.Compiler/bin/Debug/netcoreapp3.1/Cropper.Blazor.Client.Compiler.dll - - - - - $(SolutionDir)Cropper.Blazor.Client.Compiler/bin/Debug/net7.0/Cropper.Blazor.Client.Compiler.dll - - - - - - - dotnet run --configuration release --project "$(SolutionDir)Cropper.Blazor.Client.Compiler/Cropper.Blazor.Client.Compiler.csproj" - - - - - - - - - - - - - - - - - - - - - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + <_ContentIncludedByDefault Remove="Pages\CroppedCanvasDialog.razor" /> + + + + + $(MSBuildThisFileDirectory)../ + + + + + + + $(SolutionDir)Cropper.Blazor.Client.Compiler/bin/Debug/netcoreapp3.1/Cropper.Blazor.Client.Compiler.dll + + + + + $(SolutionDir)Cropper.Blazor.Client.Compiler/bin/Debug/net7.0/Cropper.Blazor.Client.Compiler.dll + + + + + + + dotnet run --configuration release --project "$(SolutionDir)Cropper.Blazor.Client.Compiler/Cropper.Blazor.Client.Compiler.csproj" + + + + + + + + + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Cropper.Blazor/Client/Pages/CropperDemo.razor b/src/Cropper.Blazor/Client/Pages/CropperDemo.razor index 968f4f67..2f3bd147 100644 --- a/src/Cropper.Blazor/Client/Pages/CropperDemo.razor +++ b/src/Cropper.Blazor/Client/Pages/CropperDemo.razor @@ -183,31 +183,31 @@ + Style="@(AspectRatio == 1.7777777777777777m && !IsEnableAspectRatioSettings ? "background-color: var(--mud-palette-success-darken)" : "")" FullWidth="true"> 16:9 + Style="@(AspectRatio == 1.3333333333333333m && !IsEnableAspectRatioSettings ? "background-color: var(--mud-palette-success-darken)" : "")" FullWidth="true"> 4:3 + Style="@(AspectRatio == 1m && !IsEnableAspectRatioSettings ? "background-color: var(--mud-palette-success-darken)" : "")" FullWidth="true"> 1:1 + Style="@(AspectRatio == 0.6666666666666666m && !IsEnableAspectRatioSettings ? "background-color: var(--mud-palette-success-darken)" : "")" FullWidth="true"> 2:3 - + Free @@ -243,31 +243,31 @@ + Style="@(AspectRatio == 1.7777777777777777m && !IsEnableAspectRatioSettings ? "background-color: var(--mud-palette-success-darken)" : "")" FullWidth="true"> 16:9 + Style="@(AspectRatio == 1.3333333333333333m && !IsEnableAspectRatioSettings ? "background-color: var(--mud-palette-success-darken)" : "")" FullWidth="true"> 4:3 + Style="@(AspectRatio == 1m && !IsEnableAspectRatioSettings ? "background-color: var(--mud-palette-success-darken)" : "")" FullWidth="true"> 1:1 + Style="@(AspectRatio == 0.6666666666666666m && !IsEnableAspectRatioSettings ? "background-color: var(--mud-palette-success-darken)" : "")" FullWidth="true"> 2:3 - + Free @@ -321,14 +321,22 @@ @*//+---// Get/Set all cropper data //---+//*@ - + + + + + + + + + diff --git a/src/Cropper.Blazor/Client/Pages/CropperDemo.razor.cs b/src/Cropper.Blazor/Client/Pages/CropperDemo.razor.cs index b1a0791a..5cd905e8 100644 --- a/src/Cropper.Blazor/Client/Pages/CropperDemo.razor.cs +++ b/src/Cropper.Blazor/Client/Pages/CropperDemo.razor.cs @@ -25,13 +25,14 @@ public partial class CropperDemo : IDisposable [Inject] private IJSRuntime? JSRuntime { get; set; } - private CropperComponent? CropperComponent = null!; + public CropperComponent? CropperComponent = null!; private CropperDataPreview? CropperDataPreview = null!; private GetSetCropperData? GetSetCropperData = null!; private Options Options = null!; private decimal? ScaleXValue; private decimal? ScaleYValue; private decimal AspectRatio = 1.7777777777777777m; + private bool IsEnableAspectRatioSettings; private string Src = "https://fengyuanchen.github.io/cropperjs/v2/picture.jpg"; private bool IsErrorLoadImage { get; set; } = false; @@ -63,11 +64,34 @@ public async void OnCropEvent(JSEventData cropJSEvent) ScaleXValue = cropJSEvent.Detail.ScaleX; ScaleYValue = cropJSEvent.Detail.ScaleY; - await InvokeAsync(() => + decimal width = Math.Round(cropJSEvent.Detail.Width ?? 0); + decimal height = Math.Round(cropJSEvent.Detail.Height ?? 0); + + if (width < GetSetCropperData!.CroppedDimensionsSettings.MinimumWidth + || height < GetSetCropperData!.CroppedDimensionsSettings.MinimumHeight + || width > GetSetCropperData!.CroppedDimensionsSettings.MaximumWidth + || height > GetSetCropperData!.CroppedDimensionsSettings.MaximumHeight + ) { - //JSRuntime!.InvokeVoidAsync("console.log", $"CropJSEvent {JsonSerializer.Serialize(cropJSEvent)}"); - CropperDataPreview?.OnCropEvent(cropJSEvent.Detail); - }); + CropperComponent!.SetData(new SetDataOptions + { + Width = Math.Max( + GetSetCropperData!.CroppedDimensionsSettings.MinimumWidth ?? 0M, + Math.Min(GetSetCropperData!.CroppedDimensionsSettings.MaximumWidth ?? 0M, width)), + Height = Math.Max( + GetSetCropperData!.CroppedDimensionsSettings.MinimumHeight ?? 0M, + Math.Min(GetSetCropperData!.CroppedDimensionsSettings.MaximumHeight ?? 0M, height)), + + }); + } + else + { + await InvokeAsync(() => + { + //JSRuntime!.InvokeVoidAsync("console.log", $"CropJSEvent {JsonSerializer.Serialize(cropJSEvent)}"); + CropperDataPreview?.OnCropEvent(cropJSEvent.Detail); + }); + } } } @@ -132,6 +156,39 @@ public async void OnCropMoveEvent(JSEventData cropMoveJSEvent) // await JSRuntime!.InvokeVoidAsync("console.log", $"CropMoveJSEvent OriginalEvent clientX: {clientX}"); //} + CropBoxData cropBoxData = await CropperComponent!.GetCropBoxDataAsync(); + + if (cropBoxData.Height != 0) + { + decimal aspectRatio = cropBoxData.Width / cropBoxData.Height; + + AspectRatio = aspectRatio; + GetSetCropperData!.AspectRatioSettings.SetUpAspectRatio(aspectRatio); + + if (GetSetCropperData?.AspectRatioSettings?.MinAspectRatio is not null + || GetSetCropperData?.AspectRatioSettings?.MaxAspectRatio is not null) + { + if (aspectRatio < GetSetCropperData!.AspectRatioSettings!.MinAspectRatio) + { + CropperComponent!.SetCropBoxData(new SetCropBoxDataOptions + { + Width = cropBoxData.Height * GetSetCropperData!.AspectRatioSettings.MinAspectRatio + }); + } + else if (aspectRatio > GetSetCropperData!.AspectRatioSettings!.MaxAspectRatio) + { + CropperComponent!.SetCropBoxData(new SetCropBoxDataOptions + { + Width = cropBoxData.Height * GetSetCropperData!.AspectRatioSettings.MaxAspectRatio + }); + } + } + } + else + { + AspectRatio = 0; + GetSetCropperData!.AspectRatioSettings.SetUpAspectRatio(0); + } } public async void OnCropReadyEvent(JSEventData jSEventData) @@ -227,12 +284,20 @@ private void Destroy() CropperComponent?.RevokeObjectUrlAsync(Src); } - public void SetAspectRatio(decimal aspectRatio) + public void SetAspectRatio(decimal aspectRatio, bool isEnableAspectRatioSettings = false) { - this.AspectRatio = aspectRatio; + IsEnableAspectRatioSettings = isEnableAspectRatioSettings; + + if (aspectRatio != 0) + { + AspectRatio = aspectRatio; + } + CropperComponent?.SetAspectRatio(aspectRatio); } + public void SetFreeAspectRatio() => SetAspectRatio(0, true); + public void SetViewMode(ViewMode viewMode) { Options.ViewMode = viewMode; @@ -276,7 +341,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - var subscriptionResult = await BreakpointListener.Subscribe((breakpoint) => + var subscriptionResult = await BreakpointListener.SubscribeAsync((breakpoint) => { InvokeAsync(StateHasChanged); }); diff --git a/src/Cropper.Blazor/Client/Shared/CroppedCanvasDialog.razor b/src/Cropper.Blazor/Client/Shared/CroppedCanvasDialog.razor index 8fd81c47..9759bb92 100644 --- a/src/Cropper.Blazor/Client/Shared/CroppedCanvasDialog.razor +++ b/src/Cropper.Blazor/Client/Shared/CroppedCanvasDialog.razor @@ -2,7 +2,9 @@ - + + + Cropped canvas data @@ -12,13 +14,8 @@ - + + Download image by link + - -@code { - [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = null!; - - [Parameter] - [System.ComponentModel.DataAnnotations.Required] - public string Src { get; set; } = null!; -} \ No newline at end of file + \ No newline at end of file diff --git a/src/Cropper.Blazor/Client/Shared/CroppedCanvasDialog.razor.cs b/src/Cropper.Blazor/Client/Shared/CroppedCanvasDialog.razor.cs new file mode 100644 index 00000000..8fe26d08 --- /dev/null +++ b/src/Cropper.Blazor/Client/Shared/CroppedCanvasDialog.razor.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace Cropper.Blazor.Client.Shared +{ + public partial class CroppedCanvasDialog + { + [Parameter] + [System.ComponentModel.DataAnnotations.Required] + public string Src { get; set; } = null!; + + [Inject] private IJSRuntime? JSRuntime { get; set; } + + public async Task DownloadImageSrcAsync() + { + await JSRuntime!.InvokeVoidAsync( + "downloadFromUrl", + new { Url = Src, FileName = $"{Guid.NewGuid()}.png" }); + } + } +} diff --git a/src/Cropper.Blazor/Client/Shared/MainLayout.razor.cs b/src/Cropper.Blazor/Client/Shared/MainLayout.razor.cs index 2666d839..34fab34e 100644 --- a/src/Cropper.Blazor/Client/Shared/MainLayout.razor.cs +++ b/src/Cropper.Blazor/Client/Shared/MainLayout.razor.cs @@ -25,7 +25,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - SubscriptionId = await ResizeService.Subscribe((size) => + SubscriptionId = await ResizeService.SubscribeAsync((size) => { if (size.Width > 960) { @@ -48,7 +48,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) await base.OnAfterRenderAsync(firstRender); } - public async ValueTask DisposeAsync() => await ResizeService.Unsubscribe(SubscriptionId); + public async ValueTask DisposeAsync() => await ResizeService.UnsubscribeAsync(SubscriptionId); private async Task ApplyUserPreferences() { diff --git a/src/Cropper.Blazor/Client/wwwroot/helper.js b/src/Cropper.Blazor/Client/wwwroot/helper.js new file mode 100644 index 00000000..b4af1b2a --- /dev/null +++ b/src/Cropper.Blazor/Client/wwwroot/helper.js @@ -0,0 +1,7 @@ +window.downloadFromUrl = (options) => { + const anchorElement = document.createElement('a'); + anchorElement.href = options.url; + anchorElement.download = options.fileName ?? ''; + anchorElement.click(); + anchorElement.remove(); +}; \ No newline at end of file diff --git a/src/Cropper.Blazor/Client/wwwroot/index.html b/src/Cropper.Blazor/Client/wwwroot/index.html index e8caaf14..ca7e8df9 100644 --- a/src/Cropper.Blazor/Client/wwwroot/index.html +++ b/src/Cropper.Blazor/Client/wwwroot/index.html @@ -130,6 +130,7 @@ + diff --git a/src/Cropper.Blazor/Client/wwwroot/overrideCropperJsInteropModule.js b/src/Cropper.Blazor/Client/wwwroot/overrideCropperJsInteropModule.js index f4173780..103ad7f7 100644 --- a/src/Cropper.Blazor/Client/wwwroot/overrideCropperJsInteropModule.js +++ b/src/Cropper.Blazor/Client/wwwroot/overrideCropperJsInteropModule.js @@ -1,4 +1,4 @@ -window.overrideCropperJsInteropModule = (minZoomRatio, maxZoomRatio) => { +window.overrideOnZoomCropperEvent = (minZoomRatio, maxZoomRatio) => { window.cropper.onZoom = function (imageObject, event, correlationId) { const jSEventData = this.getJSEventData(event, correlationId); const isApplyPreventZoomRatio = minZoomRatio != null || maxZoomRatio != null; @@ -10,4 +10,4 @@ imageObject.invokeMethodAsync('CropperIsZoomed', jSEventData); } }; -}; +}; \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor.Testing/Cropper.Blazor.Testing.csproj b/src/Cropper.Blazor/Cropper.Blazor.Testing/Cropper.Blazor.Testing.csproj index 2964c3b7..913eae3d 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.Testing/Cropper.Blazor.Testing.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor.Testing/Cropper.Blazor.Testing.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Cropper.Blazor/Cropper.Blazor.Testing/ServiceCollectionMock.cs b/src/Cropper.Blazor/Cropper.Blazor.Testing/ServiceCollectionMock.cs index cf53ebd7..76177d64 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.Testing/ServiceCollectionMock.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.Testing/ServiceCollectionMock.cs @@ -12,19 +12,24 @@ public ServiceCollectionMock(Mock serviceCollectionMock) _serviceCollectionVerifier = new ServiceCollectionVerifier(serviceCollectionMock); } + public void TryContainsSingletonService() + { + _serviceCollectionVerifier.TryContainsSingletonService(); + } + public void ContainsSingletonService() { _serviceCollectionVerifier.ContainsSingletonService(); } - public void ContainsTransientService() + public void TryContainsTransientService() { - _serviceCollectionVerifier.ContainsTransientService(); + _serviceCollectionVerifier.TryContainsTransientService(); } - public void ContainsScopedService() + public void TryContainsScopedService() { - _serviceCollectionVerifier.ContainsTransientService(); + _serviceCollectionVerifier.TryContainsScopedService(); } } } diff --git a/src/Cropper.Blazor/Cropper.Blazor.Testing/ServiceCollectionVerifier.cs b/src/Cropper.Blazor/Cropper.Blazor.Testing/ServiceCollectionVerifier.cs index 53758538..ee27b5cc 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.Testing/ServiceCollectionVerifier.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.Testing/ServiceCollectionVerifier.cs @@ -12,19 +12,31 @@ public ServiceCollectionVerifier(Mock serviceCollectionMock) _serviceCollectionMock = serviceCollectionMock; } + public void TryContainsSingletonService() + { + TryIsRegistered(ServiceLifetime.Singleton); + } + public void ContainsSingletonService() { IsRegistered(ServiceLifetime.Singleton); } - public void ContainsTransientService() + public void TryContainsTransientService() { - IsRegistered(ServiceLifetime.Transient); + TryIsRegistered(ServiceLifetime.Transient); } - public void ContainsScopedService() + public void TryContainsScopedService() { - IsRegistered(ServiceLifetime.Scoped); + TryIsRegistered(ServiceLifetime.Scoped); + } + + private void TryIsRegistered(ServiceLifetime lifetime) + { + _serviceCollectionMock + .Verify(serviceCollection => serviceCollection.Add( + It.Is(serviceDescriptor => serviceDescriptor.TryIs(lifetime)))); } private void IsRegistered(ServiceLifetime lifetime) @@ -32,7 +44,6 @@ private void IsRegistered(ServiceLifetime lifetime) _serviceCollectionMock .Verify(serviceCollection => serviceCollection.Add( It.Is(serviceDescriptor => serviceDescriptor.Is(lifetime)))); - } } } diff --git a/src/Cropper.Blazor/Cropper.Blazor.Testing/ServiceDescriptionExtensions.cs b/src/Cropper.Blazor/Cropper.Blazor.Testing/ServiceDescriptionExtensions.cs index efffdb22..c74ee646 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.Testing/ServiceDescriptionExtensions.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.Testing/ServiceDescriptionExtensions.cs @@ -4,7 +4,7 @@ namespace Cropper.Blazor.Testing { public static class ServiceDescriptionExtensions { - public static bool Is( + public static bool TryIs( this ServiceDescriptor serviceDescriptor, ServiceLifetime lifetime) { @@ -12,5 +12,13 @@ public static bool Is( && serviceDescriptor.ImplementationType == typeof(TInstance) && serviceDescriptor.Lifetime == lifetime; } + + public static bool Is( + this ServiceDescriptor serviceDescriptor, + ServiceLifetime lifetime) + { + return serviceDescriptor.ServiceType == typeof(TService) + && serviceDescriptor.Lifetime == lifetime; + } } } diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Components/CropperComponent_Should.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Components/CropperComponent_Should.cs index f95a7cbd..9dcf36de 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Components/CropperComponent_Should.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Components/CropperComponent_Should.cs @@ -127,6 +127,8 @@ public async Task Should_Render_CropperComponent_SuccessfulAsync() decimal ratio = faker.Random.Decimal(); decimal pivotX = faker.Random.Decimal(); decimal pivotY = faker.Random.Decimal(); + string newUrlImage = faker.Random.Word(); + bool hasSameSize = faker.Random.Bool(); Action? onLoadImageHandler = () => { @@ -350,6 +352,9 @@ await cropperComponent.InvokeAsync(async () => expectedImageData.Should().Be(imageData); _mockCropperJsInterop.Verify(c => c.GetImageDataAsync(cancellationToken), Times.Once()); + await cropperComponent.Instance.ReplaceAsync(newUrlImage, hasSameSize); + _mockCropperJsInterop.Verify(c => c.ReplaceAsync(newUrlImage, hasSameSize, cancellationToken), Times.Once()); + string image = await cropperComponent.Instance.GetImageUsingStreamingAsync(imageFile, maxAllowedSize, cancellationToken); expectedImage.Should().Be(image); _mockCropperJsInterop.Verify(c => c.GetImageUsingStreamingAsync(imageFile, maxAllowedSize, cancellationToken), Times.Once()); @@ -480,8 +485,8 @@ public void Verify_Method_To_Be_Invokable_From_JS(string methodName) public void Dispose() { - _testContext.Dispose(); _testContext.DisposeComponents(); + _testContext.Dispose(); GC.SuppressFinalize(this); } } diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Cropper.Blazor.UnitTests.csproj b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Cropper.Blazor.UnitTests.csproj index b41322a0..7626347e 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Cropper.Blazor.UnitTests.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Cropper.Blazor.UnitTests.csproj @@ -9,20 +9,20 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -33,7 +33,7 @@ - + diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/ServiceCollectionExtensions_Should.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/ServiceCollectionExtensions_Should.cs index 7fe675c4..9cc17931 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/ServiceCollectionExtensions_Should.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/ServiceCollectionExtensions_Should.cs @@ -1,4 +1,6 @@ -using Cropper.Blazor.Extensions; +using System.Collections.Generic; +using Cropper.Blazor.Extensions; +using Cropper.Blazor.ModuleOptions; using Cropper.Blazor.Services; using Cropper.Blazor.Testing; using Microsoft.Extensions.DependencyInjection; @@ -9,20 +11,33 @@ namespace Cropper.Blazor.UnitTests.Extensions { public class ServiceCollectionExtensions_Should { - private readonly ServiceCollectionMock _serviceCollectionMock; + private ServiceCollectionMock ServiceCollectionMock = null!; + private readonly Mock ServiceCollection = new(); - public ServiceCollectionExtensions_Should() + [Theory, MemberData(nameof(TestData_AddCropper_Service))] + public void Verify_Cropper_Service_Is_Registered(CropperJsInteropOptions? cropperJsInteropOptions) { - Mock serviceCollection = new(); - serviceCollection.Object.AddCropper(); - _serviceCollectionMock = new ServiceCollectionMock(serviceCollection); + // act + ServiceCollection.Object.AddCropper(cropperJsInteropOptions); + + // assert + ServiceCollectionMock = new(ServiceCollection); + ServiceCollectionMock.ContainsSingletonService(); + ServiceCollectionMock.TryContainsTransientService(); } - [Fact] - public void Verify_Cropper_Service_Is_Registered() + public static IEnumerable TestData_AddCropper_Service() { - // assert - _serviceCollectionMock.ContainsTransientService(); + yield return WrapArgs(null); + + yield return WrapArgs(new CropperJsInteropOptions()); + + static object[] WrapArgs( + CropperJsInteropOptions? cropperJsInteropOptions) + => new object[] + { + cropperJsInteropOptions + }; } } } diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/UriExtensions_Should.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/UriExtensions_Should.cs new file mode 100644 index 00000000..8ef640c9 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/UriExtensions_Should.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using Cropper.Blazor.Extensions; +using FluentAssertions; +using Xunit; + +namespace Cropper.Blazor.UnitTests.Extensions +{ + public class UriExtensions_Should + { + [Theory, MemberData(nameof(TestData_ToHostName))] + public void ToHostName(Uri uri, string expectedHostName) + { + // act + string hostName = uri.GetHostName(); + + // assert + hostName.Should().Be(expectedHostName); + } + + public static IEnumerable TestData_ToHostName() + { + yield return WrapArgs(new Uri("http://localhost"), "http:localhost"); + + yield return WrapArgs(new Uri("https://localhost"), "https:localhost"); + + yield return WrapArgs(new Uri("http://localhost:5000"), "http:localhost:5000"); + + yield return WrapArgs(new Uri("https://localhost:5001"), "https:localhost:5001"); + + yield return WrapArgs(new Uri("http://localhost/"), "http:localhost"); + + yield return WrapArgs(new Uri("https://localhost/"), "https:localhost"); + + yield return WrapArgs(new Uri("http://localhost:5000/"), "http:localhost:5000"); + + yield return WrapArgs(new Uri("https://localhost:5001/"), "https:localhost:5001"); + + yield return WrapArgs(new Uri("http://localhost/testpath"), "http://localhost"); + + yield return WrapArgs(new Uri("https://localhost/testpath"), "https://localhost"); + + yield return WrapArgs(new Uri("http://localhost:5000/testpath"), "http://localhost:5000"); + + yield return WrapArgs(new Uri("https://localhost:5001/testpath"), "https://localhost:5001"); + + yield return WrapArgs(new Uri("http://localhost/testpath"), "http://localhost"); + + yield return WrapArgs(new Uri("https://localhost/testpath"), "https://localhost"); + + yield return WrapArgs(new Uri("http://localhost:5000/testpath"), "http://localhost:5000"); + + yield return WrapArgs(new Uri("https://localhost:5001/testpath"), "https://localhost:5001"); + + yield return WrapArgs(new Uri("http://localhost/testpath?name=123"), "http://localhost"); + + yield return WrapArgs(new Uri("https://localhost/testpath?name=123"), "https://localhost"); + + yield return WrapArgs(new Uri("http://localhost:5000/testpath?name=123"), "http://localhost:5000"); + + yield return WrapArgs(new Uri("https://localhost:5001/testpath?name=123"), "https://localhost:5001"); + + static object[] WrapArgs( + Uri uri, + string expectedHostName) + => new object[] + { + uri, + expectedHostName, + }; + } + } +} diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/CropperJsInterop_Should.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/CropperJsInterop_Should.cs index bf8f6d36..988968b0 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/CropperJsInterop_Should.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/CropperJsInterop_Should.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; @@ -10,6 +9,7 @@ using Cropper.Blazor.Base; using Cropper.Blazor.Extensions; using Cropper.Blazor.Models; +using Cropper.Blazor.ModuleOptions; using Cropper.Blazor.Services; using FluentAssertions; using Microsoft.AspNetCore.Components; @@ -26,7 +26,8 @@ public class CropperJsInterop_Should : IDisposable private readonly Faker _faker; private readonly TestContext _testContext; private readonly ICropperJsInterop _cropperJsInterop; - private string DefaultPathToCropperModule => Path.Combine("http:localhost", CropperJsInterop.PathToCropperModule); + private const string PathToCropperModule = "_content/Cropper.Blazor/cropperJsInterop.min.js"; + private static string DefaultPathToCropperModule => Path.Combine("http:localhost", PathToCropperModule); public CropperJsInterop_Should() { @@ -37,26 +38,10 @@ public CropperJsInterop_Should() FakeNavigationManager fakeNavigationManager = _testContext.Services.GetRequiredService(); _cropperJsInterop = new Faker() - .CustomInstantiator(f => new CropperJsInterop(_testContext.JSInterop.JSRuntime, fakeNavigationManager)) + .CustomInstantiator(f => new CropperJsInterop(_testContext.JSInterop.JSRuntime, fakeNavigationManager, new CropperJsInteropOptions())) .Generate(); } - [Theory, MemberData(nameof(TestData_LoadCropperModule))] - public async Task Verify_LoadCropperModuleAsync( - string pathToCropperModule, - string expectedPathToCropperModule) - { - // arrange - FakeNavigationManager fakeNavigationManager = _testContext.Services.GetRequiredService(); - fakeNavigationManager.NavigateTo(pathToCropperModule); - - // assert - VerifyLoadCropperModule(expectedPathToCropperModule); - - // act - await _cropperJsInterop.LoadModuleAsync(); - } - [Fact] public async Task Verify_InitCropperAsync() { @@ -679,7 +664,7 @@ public async Task Verify_DisposeAsync() { // arrange FakeNavigationManager fakeNavigationManager = _testContext.Services.GetRequiredService(); - CropperJsInterop cropperJsInterop = new(_testContext.JSInterop.JSRuntime, fakeNavigationManager); + CropperJsInterop cropperJsInterop = new(_testContext.JSInterop.JSRuntime, fakeNavigationManager, new CropperJsInteropOptions()); // assert VerifyLoadCropperModule(DefaultPathToCropperModule); @@ -689,43 +674,6 @@ public async Task Verify_DisposeAsync() await cropperJsInterop.DisposeAsync(); } - public static IEnumerable TestData_LoadCropperModule() - { - yield return WrapArgs("http://localhost", Path.Combine("http:localhost", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("http://localhost/", Path.Combine("http:localhost", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("http://localhost/testPath", Path.Combine("http:localhost", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("http://localhost/testPath/", Path.Combine("http:localhost", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://localhost", Path.Combine("https:localhost", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://localhost/", Path.Combine("https:localhost", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://localhost/testPath", Path.Combine("https:localhost", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://localhost/testPath/", Path.Combine("https:localhost", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://localhost:5001", Path.Combine("https:localhost:5001", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://localhost:5001/", Path.Combine("https:localhost:5001", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://localhost:5001/testPath", Path.Combine("https:localhost:5001", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://localhost:5001/testPath/", Path.Combine("https:localhost:5001", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("http://cropperblazor.github.io", Path.Combine("http:cropperblazor.github.io", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("http://cropperblazor.github.io/", Path.Combine("http:cropperblazor.github.io", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("http://cropperblazor.github.io/testPath", Path.Combine("http:cropperblazor.github.io", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("http://cropperblazor.github.io/testPath/", Path.Combine("http:cropperblazor.github.io", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://cropperblazor.github.io", Path.Combine("https:cropperblazor.github.io", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://cropperblazor.github.io/", Path.Combine("https:cropperblazor.github.io", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://cropperblazor.github.io/testPath", Path.Combine("https:cropperblazor.github.io", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://cropperblazor.github.io/testPath/", Path.Combine("https:cropperblazor.github.io", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://cropperblazor.github.io:5001", Path.Combine("https:cropperblazor.github.io:5001", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://cropperblazor.github.io:5001/", Path.Combine("https:cropperblazor.github.io:5001", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://cropperblazor.github.io:5001/testPath", Path.Combine("https:cropperblazor.github.io:5001", CropperJsInterop.PathToCropperModule)); - yield return WrapArgs("https://cropperblazor.github.io:5001/testPath/", Path.Combine("https:cropperblazor.github.io:5001", CropperJsInterop.PathToCropperModule)); - - static object[] WrapArgs( - string pathToCropperModule, - string expectedPathToCropperModule) - => new object[] - { - pathToCropperModule, - expectedPathToCropperModule - }; - } - private void VerifyLoadCropperModule( string pathToCropperModule) { diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/LoadCropperModule_Should.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/LoadCropperModule_Should.cs new file mode 100644 index 00000000..b9321143 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/LoadCropperModule_Should.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Bogus; +using Bunit; +using Bunit.TestDoubles; +using Cropper.Blazor.ModuleOptions; +using Cropper.Blazor.Services; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Cropper.Blazor.UnitTests.Services +{ + public class LoadCropperModule_Should : IDisposable + { + private readonly TestContext _testContext; + private ICropperJsInterop _cropperJsInterop; + private const string PathToCropperModule = "_content/Cropper.Blazor/cropperJsInterop.min.js"; + private static string DefaultPathToCropperModule => Path.Combine("http:localhost", PathToCropperModule); + + public LoadCropperModule_Should() + { + _testContext = new Faker() + .Generate(); + } + + [Theory, MemberData(nameof(TestData_LoadCropperModule))] + public async Task Verify_LoadCropperModuleAsync( + string pathToCropperModule, + CropperJsInteropOptions cropperJsInteropOptions, + string expectedPathToCropperModule) + { + // arrange + FakeNavigationManager fakeNavigationManager = _testContext.Services.GetRequiredService(); + + _cropperJsInterop = new Faker() + .CustomInstantiator(f => new CropperJsInterop(_testContext.JSInterop.JSRuntime, fakeNavigationManager, cropperJsInteropOptions)) + .Generate(); + + fakeNavigationManager.NavigateTo(pathToCropperModule); + + // assert + VerifyLoadCropperModule(expectedPathToCropperModule); + + // act + await _cropperJsInterop.LoadModuleAsync(); + } + + public static IEnumerable TestData_LoadCropperModule() + { + CropperJsInteropOptions cropperJsInteropOptions = new(); + + yield return WrapArgs("http://localhost", cropperJsInteropOptions, Path.Combine("http:localhost", PathToCropperModule)); + yield return WrapArgs("http://localhost/", cropperJsInteropOptions, Path.Combine("http:localhost", PathToCropperModule)); + yield return WrapArgs("http://localhost/testPath", cropperJsInteropOptions, Path.Combine("http:localhost", PathToCropperModule)); + yield return WrapArgs("http://localhost/testPath/", cropperJsInteropOptions, Path.Combine("http:localhost", PathToCropperModule)); + yield return WrapArgs("https://localhost", cropperJsInteropOptions, Path.Combine("https:localhost", PathToCropperModule)); + yield return WrapArgs("https://localhost/", cropperJsInteropOptions, Path.Combine("https:localhost", PathToCropperModule)); + yield return WrapArgs("https://localhost/testPath", cropperJsInteropOptions, Path.Combine("https:localhost", PathToCropperModule)); + yield return WrapArgs("https://localhost/testPath/", cropperJsInteropOptions, Path.Combine("https:localhost", PathToCropperModule)); + yield return WrapArgs("https://localhost:5001", cropperJsInteropOptions, Path.Combine("https:localhost:5001", PathToCropperModule)); + yield return WrapArgs("https://localhost:5001/", cropperJsInteropOptions, Path.Combine("https:localhost:5001", PathToCropperModule)); + yield return WrapArgs("https://localhost:5001/testPath", cropperJsInteropOptions, Path.Combine("https:localhost:5001", PathToCropperModule)); + yield return WrapArgs("https://localhost:5001/testPath/", cropperJsInteropOptions, Path.Combine("https:localhost:5001", PathToCropperModule)); + yield return WrapArgs("http://cropperblazor.github.io", cropperJsInteropOptions, Path.Combine("http:cropperblazor.github.io", PathToCropperModule)); + yield return WrapArgs("http://cropperblazor.github.io/", cropperJsInteropOptions, Path.Combine("http:cropperblazor.github.io", PathToCropperModule)); + yield return WrapArgs("http://cropperblazor.github.io/testPath", cropperJsInteropOptions, Path.Combine("http:cropperblazor.github.io", PathToCropperModule)); + yield return WrapArgs("http://cropperblazor.github.io/testPath/", cropperJsInteropOptions, Path.Combine("http:cropperblazor.github.io", PathToCropperModule)); + yield return WrapArgs("https://cropperblazor.github.io", cropperJsInteropOptions, Path.Combine("https:cropperblazor.github.io", PathToCropperModule)); + yield return WrapArgs("https://cropperblazor.github.io/", cropperJsInteropOptions, Path.Combine("https:cropperblazor.github.io", PathToCropperModule)); + yield return WrapArgs("https://cropperblazor.github.io/testPath", cropperJsInteropOptions, Path.Combine("https:cropperblazor.github.io", PathToCropperModule)); + yield return WrapArgs("https://cropperblazor.github.io/testPath/", cropperJsInteropOptions, Path.Combine("https:cropperblazor.github.io", PathToCropperModule)); + yield return WrapArgs("https://cropperblazor.github.io:5001", cropperJsInteropOptions, Path.Combine("https:cropperblazor.github.io:5001", PathToCropperModule)); + yield return WrapArgs("https://cropperblazor.github.io:5001/", cropperJsInteropOptions, Path.Combine("https:cropperblazor.github.io:5001", PathToCropperModule)); + yield return WrapArgs("https://cropperblazor.github.io:5001/testPath", cropperJsInteropOptions, Path.Combine("https:cropperblazor.github.io:5001", PathToCropperModule)); + yield return WrapArgs("https://cropperblazor.github.io:5001/testPath/", cropperJsInteropOptions, Path.Combine("https:cropperblazor.github.io:5001", PathToCropperModule)); + + cropperJsInteropOptions = new(); + cropperJsInteropOptions.IsActiveGlobalPath = true; + cropperJsInteropOptions.GlobalPathToCropperModule = "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"; + + yield return WrapArgs("http://localhost", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("http://localhost/", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("http://localhost/testPath", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("http://localhost/testPath/", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://localhost", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://localhost/", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://localhost/testPath", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://localhost/testPath/", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://localhost:5001", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://localhost:5001/", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://localhost:5001/testPath", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://localhost:5001/testPath/", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("http://cropperblazor.github.io", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("http://cropperblazor.github.io/", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("http://cropperblazor.github.io/testPath", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("http://cropperblazor.github.io/testPath/", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://cropperblazor.github.io", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://cropperblazor.github.io/", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://cropperblazor.github.io/testPath", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://cropperblazor.github.io/testPath/", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://cropperblazor.github.io:5001", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://cropperblazor.github.io:5001/", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://cropperblazor.github.io:5001/testPath", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + yield return WrapArgs("https://cropperblazor.github.io:5001/testPath/", cropperJsInteropOptions, "https://cropperblazor.github.io:5001/testPath/_content/Cropper.Blazor/cropperJsInterop.min.js"); + + static object[] WrapArgs( + string pathToCropperModule, + CropperJsInteropOptions cropperJsInteropOptions, + string expectedPathToCropperModule) + => new object[] + { + pathToCropperModule, + cropperJsInteropOptions, + expectedPathToCropperModule + }; + } + + private void VerifyLoadCropperModule( + string pathToCropperModule) + { + _testContext.JSInterop + .SetupModule(pathToCropperModule); + } + + public void Dispose() + { + _testContext.Dispose(); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.cs b/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.cs index 800494d0..2d0c19ab 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.cs @@ -481,6 +481,21 @@ public async ValueTask GetContainerDataAsync(CancellationToken ca return await CropperJsIntertop!.GetContainerDataAsync(cancellationToken); } + /// + /// Replace the image's src and rebuild the cropper. + /// + /// The new URL. + /// If the new image has the same size as the old one, then it will not rebuild the cropper and only update the URLs of all related images. This can be used for applying filters. + /// The used to propagate notifications that the operation should be canceled. + /// A representing any asynchronous operation. + public async ValueTask ReplaceAsync( + string url, + bool hasSameSize, + CancellationToken cancellationToken = default) + { + await CropperJsIntertop!.ReplaceAsync(url, hasSameSize, cancellationToken); + } + /// /// Output the image position, size and other related data. /// diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 2f389a1f..5e652df5 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -14,7 +14,7 @@ - 1.2.0 + 1.2.1 LICENSE NuGet.png Cropper.Blazor diff --git a/src/Cropper.Blazor/Cropper.Blazor/Extensions/ServiceCollectionExtensions.cs b/src/Cropper.Blazor/Cropper.Blazor/Extensions/ServiceCollectionExtensions.cs index c784ac77..e8db01df 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Extensions/ServiceCollectionExtensions.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Cropper.Blazor.Services; +using Cropper.Blazor.ModuleOptions; +using Cropper.Blazor.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -13,10 +14,15 @@ public static class ServiceCollectionExtensions /// Adds a see as a Transient instance. /// /// Continues the chain. + /// Continues the chain. + /// When option is default (null) then uses internal path with default cropper JavaScript interop options. /// Continues the chain. - public static IServiceCollection AddCropper(this IServiceCollection services) + public static IServiceCollection AddCropper(this IServiceCollection services, CropperJsInteropOptions? cropperJsInteropOptions = null) { + services.AddSingleton(services => cropperJsInteropOptions ?? new CropperJsInteropOptions()); + services.TryAddTransient(); + return services; } } diff --git a/src/Cropper.Blazor/Cropper.Blazor/Extensions/UriExtensions.cs b/src/Cropper.Blazor/Cropper.Blazor/Extensions/UriExtensions.cs new file mode 100644 index 00000000..5001f5e3 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/Extensions/UriExtensions.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Cropper.Blazor.UnitTests")] + +namespace Cropper.Blazor.Extensions +{ + static internal class UriExtensions + { + static internal string GetHostName(this Uri baseUri) + { + string redundantPath = baseUri.PathAndQuery; + + return baseUri.ToString().Replace(redundantPath, string.Empty); + } + } +} diff --git a/src/Cropper.Blazor/Cropper.Blazor/ModuleOptions/CropperJsInteropOptions.cs b/src/Cropper.Blazor/Cropper.Blazor/ModuleOptions/CropperJsInteropOptions.cs new file mode 100644 index 00000000..cd5abe1f --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/ModuleOptions/CropperJsInteropOptions.cs @@ -0,0 +1,23 @@ +namespace Cropper.Blazor.ModuleOptions +{ + /// + /// Contains cropper JavaScript interop options. + /// + public class CropperJsInteropOptions : ICropperJsInteropOptions + { + /// + /// Represents an internal (default) path to cropper js interop module. + /// + public string DefaultInternalPathToCropperModule { get; set; } = "_content/Cropper.Blazor/cropperJsInterop.min.js"; + + /// + /// Represents state regarding using global path to cropper js interop module instead of internal (default). + /// + public bool IsActiveGlobalPath { get; set; } = false; + + /// + /// Represents a global (conclusive) path to cropper js interop module. + /// + public string GlobalPathToCropperModule { get; set; } = string.Empty; + } +} diff --git a/src/Cropper.Blazor/Cropper.Blazor/ModuleOptions/ICropperJsInteropOptions.cs b/src/Cropper.Blazor/Cropper.Blazor/ModuleOptions/ICropperJsInteropOptions.cs new file mode 100644 index 00000000..e3672efa --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/ModuleOptions/ICropperJsInteropOptions.cs @@ -0,0 +1,23 @@ +namespace Cropper.Blazor.ModuleOptions +{ + /// + /// Contains cropper JavaScript interop options. + /// + public interface ICropperJsInteropOptions + { + /// + /// Represents an internal (default) path to cropper js interop module. + /// + public string DefaultInternalPathToCropperModule { get; set; } + + /// + /// Represents state regarding using global path to cropper js interop module instead of internal (default). + /// + public bool IsActiveGlobalPath { get; set; } + + /// + /// Represents a global (conclusive) path to cropper js interop module. + /// + public string GlobalPathToCropperModule { get; set; } + } +} diff --git a/src/Cropper.Blazor/Cropper.Blazor/Services/CropperJsInterop.cs b/src/Cropper.Blazor/Cropper.Blazor/Services/CropperJsInterop.cs index 983ae6ee..02f0a7d7 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Services/CropperJsInterop.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Services/CropperJsInterop.cs @@ -6,6 +6,7 @@ using Cropper.Blazor.Base; using Cropper.Blazor.Extensions; using Cropper.Blazor.Models; +using Cropper.Blazor.ModuleOptions; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; using Microsoft.JSInterop; @@ -18,23 +19,24 @@ namespace Cropper.Blazor.Services public class CropperJsInterop : ICropperJsInterop, IAsyncDisposable { private readonly NavigationManager _navigationManager; + private readonly ICropperJsInteropOptions _cropperJsInteropOptions; private readonly IJSRuntime _jsRuntime; private IJSObjectReference? Module = null; - /// - /// Path to cropper js interop module. - /// - public const string PathToCropperModule = "_content/Cropper.Blazor/cropperJsInterop.min.js"; - /// /// Implementation of the constructor. /// /// The . /// The . - public CropperJsInterop(IJSRuntime jsRuntime, NavigationManager navigationManager) + /// The . + public CropperJsInterop( + IJSRuntime jsRuntime, + NavigationManager navigationManager, + ICropperJsInteropOptions cropperJsInteropOptions) { _jsRuntime = jsRuntime; _navigationManager = navigationManager; + _cropperJsInteropOptions = cropperJsInteropOptions; } /// @@ -46,15 +48,27 @@ public CropperJsInterop(IJSRuntime jsRuntime, NavigationManager navigationManage /// A representing any asynchronous operation. public async Task LoadModuleAsync(CancellationToken cancellationToken = default) { - Uri baseUri = new Uri(_navigationManager.BaseUri); - string pathAndQuery = baseUri.PathAndQuery; - string hostName = baseUri.ToString().Replace(pathAndQuery, string.Empty); - string globalPathToCropperModule = Path.Combine(hostName, PathToCropperModule); + string globalPathToCropperModule = GetGlobalPathToCropperModule(); Module = await _jsRuntime.InvokeAsync( "import", cancellationToken, globalPathToCropperModule); } + private string GetGlobalPathToCropperModule() + { + if (_cropperJsInteropOptions.IsActiveGlobalPath) + { + return _cropperJsInteropOptions.GlobalPathToCropperModule; + } + else + { + Uri baseUri = new(_navigationManager.BaseUri); + string hostName = baseUri.GetHostName(); + + return Path.Combine(hostName, _cropperJsInteropOptions.DefaultInternalPathToCropperModule); + } + } + /// /// Initializes cropper. /// @@ -315,12 +329,12 @@ public async ValueTask MoveToAsync( /// Replace the image's src and rebuild the cropper. /// /// The new URL. - /// Indicate if the new image has the same size as the old one. + /// If the new image has the same size as the old one, then it will not rebuild the cropper and only update the URLs of all related images. This can be used for applying filters. /// The used to propagate notifications that the operation should be canceled. /// A representing any asynchronous operation. public async ValueTask ReplaceAsync( string url, - bool onlyColorChanged, + bool hasSameSize, CancellationToken cancellationToken = default) { if (Module is null) @@ -328,7 +342,7 @@ public async ValueTask ReplaceAsync( await LoadModuleAsync(cancellationToken); } - await _jsRuntime!.InvokeVoidAsync("cropper.replace", cancellationToken, url, onlyColorChanged); + await _jsRuntime!.InvokeVoidAsync("cropper.replace", cancellationToken, url, hasSameSize); } /// diff --git a/src/Cropper.Blazor/Cropper.Blazor/Services/ICropperJsInterop.cs b/src/Cropper.Blazor/Cropper.Blazor/Services/ICropperJsInterop.cs index e6ce4421..f137fc5e 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Services/ICropperJsInterop.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Services/ICropperJsInterop.cs @@ -136,12 +136,12 @@ ValueTask MoveToAsync( /// Replace the image's src and rebuild the cropper. /// /// The new URL. - /// Indicate if the new image has the same size as the old one. + /// If the new image has the same size as the old one, then it will not rebuild the cropper and only update the URLs of all related images. This can be used for applying filters. /// The used to propagate notifications that the operation should be canceled. /// A representing any asynchronous operation. ValueTask ReplaceAsync( string url, - bool onlyColorChanged, + bool hasSameSize, CancellationToken cancellationToken = default); /// diff --git a/src/Cropper.Blazor/Server/Cropper.Blazor.Server.csproj b/src/Cropper.Blazor/Server/Cropper.Blazor.Server.csproj index c982aec6..bc31deaa 100644 --- a/src/Cropper.Blazor/Server/Cropper.Blazor.Server.csproj +++ b/src/Cropper.Blazor/Server/Cropper.Blazor.Server.csproj @@ -8,7 +8,7 @@ - +