diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 25835c2d..0089655d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,4 +4,4 @@ updates: directory: "/src/Cropper.Blazor" target-branch: "dev" # Location of package manifests schedule: - interval: "weekly" + interval: "monthly" diff --git a/README.md b/README.md index f280a439..bf6c64c4 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,11 @@ - [CropperBlazor.github.io/demo](https://CropperBlazor.github.io/demo) ## Prerequisites -- Supported .NET versions - - [.NET 7.0](https://dotnet.microsoft.com/download/dotnet/7.0) for versions greater than v1.1.0 - - [.NET 6.0](https://dotnet.microsoft.com/download/dotnet/6.0) for v1.0.x +- Supported .NET 7.0, .NET 6.0 versions for these web platforms: + - Blazor WebAssembly + - Blazor Server + - Blazor Server Hybrid with MVC + - MAUI Blazor Hybrid ## Installation diff --git a/src/Cropper.Blazor/Client/.config/dotnet-tools.json b/src/Cropper.Blazor/Client/.config/dotnet-tools.json index 43057ad8..d8410c8d 100644 --- a/src/Cropper.Blazor/Client/.config/dotnet-tools.json +++ b/src/Cropper.Blazor/Client/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "excubo.webcompiler": { - "version": "3.5.20", + "version": "3.5.54", "commands": [ "webcompiler" ] diff --git a/src/Cropper.Blazor/Client/Components/Docs/SectionContent.razor b/src/Cropper.Blazor/Client/Components/Docs/SectionContent.razor index 0bf6ce6a..31be8b04 100644 --- a/src/Cropper.Blazor/Client/Components/Docs/SectionContent.razor +++ b/src/Cropper.Blazor/Client/Components/Docs/SectionContent.razor @@ -14,20 +14,31 @@ @if (Codes != null || ChildContent != null) { + @if(Codes != null) { @foreach (var codefile in Codes) { - @codefile.title + + @codefile.title + } } @if (HasCode && ChildContent != null) { - - @(ShowCode ? "Hide code" : "Show code") + + @(ShowCode ? "Hide code" : "Show code") } + } @if (ChildContent != null) @@ -44,6 +55,11 @@
@CodeComponent(ActiveCode)
- + } \ No newline at end of file diff --git a/src/Cropper.Blazor/Client/Components/Docs/SectionContent.razor.cs b/src/Cropper.Blazor/Client/Components/Docs/SectionContent.razor.cs index 4d3314e1..6d927df3 100644 --- a/src/Cropper.Blazor/Client/Components/Docs/SectionContent.razor.cs +++ b/src/Cropper.Blazor/Client/Components/Docs/SectionContent.razor.cs @@ -2,6 +2,7 @@ using Cropper.Blazor.Client.Models; using Microsoft.AspNetCore.Components; using MudBlazor; +using MudBlazor.Services; using MudBlazor.Utilities; namespace Cropper.Blazor.Client.Components.Docs; @@ -9,6 +10,7 @@ namespace Cropper.Blazor.Client.Components.Docs; public partial class SectionContent { [Inject] protected IJsApiService? JsApiService { get; set; } + [Inject] IBreakpointService BreakpointService { get; set; } = null!; protected string Classname => new CssBuilder("docs-section-content") @@ -17,6 +19,7 @@ public partial class SectionContent .AddClass("show-code", HasCode && ShowCode) .AddClass(Class) .Build(); + protected string ToolbarClassname => new CssBuilder("docs-section-content-toolbar") .AddClass($"outlined", Outlined && ChildContent != null) @@ -50,7 +53,9 @@ public partial class SectionContent [Parameter] public RenderFragment ChildContent { get; set; } private bool HasCode; - private string ActiveCode; + public string ActiveCode; + + private bool IsVerticalAlign = false; protected override void OnParametersSet() { @@ -59,13 +64,27 @@ protected override void OnParametersSet() HasCode = true; ActiveCode = Codes.FirstOrDefault().code; } - else if (!String.IsNullOrWhiteSpace(Code)) + else if (!string.IsNullOrWhiteSpace(Code)) { HasCode = true; ActiveCode = Code; } } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await BreakpointService!.SubscribeAsync((br) => + { + IsVerticalAlign = BreakpointService!.IsMediaSize(br, Breakpoint.Xs); + InvokeAsync(StateHasChanged); + }); + } + + await base.OnAfterRenderAsync(firstRender); + } + public void OnShowCode() { ShowCode = !ShowCode; @@ -88,9 +107,9 @@ private string GetActiveCode(string value) } } - private async Task CopyTextToClipboard() + private async Task CopyTextToClipboardAsync() { - await JsApiService.CopyToClipboardAsync(Snippets.GetCode(string.IsNullOrWhiteSpace(Code) ? ActiveCode : Code)); + await JsApiService!.CopyToClipboardAsync(Snippets.GetCode(string.IsNullOrWhiteSpace(Code) ? ActiveCode : Code)); } RenderFragment CodeComponent(string code) => builder => diff --git a/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj b/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj index 0ebddae2..ee7b3d0a 100644 --- a/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj +++ b/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj @@ -31,7 +31,7 @@ - + @@ -105,6 +105,7 @@ + @@ -123,6 +124,7 @@ + @@ -132,6 +134,7 @@ + @@ -139,6 +142,7 @@ + diff --git a/src/Cropper.Blazor/Client/Enums/CropperFace.cs b/src/Cropper.Blazor/Client/Enums/CropperFace.cs new file mode 100644 index 00000000..f3a700bd --- /dev/null +++ b/src/Cropper.Blazor/Client/Enums/CropperFace.cs @@ -0,0 +1,11 @@ +namespace Cropper.Blazor.Client.Enums +{ + public enum CropperFace + { + Default, + Close, + Pentagon, + Circle, + Arrow + } +} diff --git a/src/Cropper.Blazor/Client/Extensions/DocsVeiewExtension.cs b/src/Cropper.Blazor/Client/Extensions/DocsVeiewExtension.cs index f2e9eab1..3ff7de2b 100644 --- a/src/Cropper.Blazor/Client/Extensions/DocsVeiewExtension.cs +++ b/src/Cropper.Blazor/Client/Extensions/DocsVeiewExtension.cs @@ -11,13 +11,13 @@ public static void TryAddDocsViewServices(this IServiceCollection services) { services.AddMudServices(config => { - config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft; + config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomRight; config.SnackbarConfiguration.PreventDuplicates = false; config.SnackbarConfiguration.NewestOnTop = false; config.SnackbarConfiguration.ShowCloseIcon = true; config.SnackbarConfiguration.VisibleStateDuration = 10000; - config.SnackbarConfiguration.HideTransitionDuration = 500; - config.SnackbarConfiguration.ShowTransitionDuration = 500; + config.SnackbarConfiguration.HideTransitionDuration = 200; + config.SnackbarConfiguration.ShowTransitionDuration = 100; config.SnackbarConfiguration.SnackbarVariant = Variant.Filled; }); diff --git a/src/Cropper.Blazor/Client/Pages/CropperDemo.razor b/src/Cropper.Blazor/Client/Pages/CropperDemo.razor index 868706a2..9577a519 100644 --- a/src/Cropper.Blazor/Client/Pages/CropperDemo.razor +++ b/src/Cropper.Blazor/Client/Pages/CropperDemo.razor @@ -9,7 +9,7 @@ @*//---Cropper Component---//*@ -
+
-
+
-
+
-
-
+
+
@@ -160,20 +160,26 @@ Scale (-2, -1) - + - Get Cropped Canvas + Get Cropped Canvas by URL + + + + + Get Cropped Canvas by element - + 320×180 - - + + 640×360 @@ -243,6 +249,56 @@ + + + + + + + + + + + + + + + + + + + GET + + + @@ -303,6 +359,56 @@ + + + + + + + + + + + + + + + + + + + GET + + + diff --git a/src/Cropper.Blazor/Client/Pages/CropperDemo.razor.cs b/src/Cropper.Blazor/Client/Pages/CropperDemo.razor.cs index 51280a39..208dfc1b 100644 --- a/src/Cropper.Blazor/Client/Pages/CropperDemo.razor.cs +++ b/src/Cropper.Blazor/Client/Pages/CropperDemo.razor.cs @@ -1,6 +1,7 @@ using System.Reflection; using System.Text.Json; using Cropper.Blazor.Client.Components; +using Cropper.Blazor.Client.Enums; using Cropper.Blazor.Components; using Cropper.Blazor.Events; using Cropper.Blazor.Events.CropEndEvent; @@ -29,6 +30,7 @@ public partial class CropperDemo : IDisposable private CropperDataPreview? CropperDataPreview = null!; private GetSetCropperData? GetSetCropperData = null!; private Options Options = null!; + private CropperFace CropperFace = CropperFace.Default; private decimal? ScaleXValue; private decimal? ScaleYValue; private decimal AspectRatio = 1.7777777777777777m; @@ -40,6 +42,10 @@ public partial class CropperDemo : IDisposable private readonly string _errorLoadImageSrc = "not-found-image.jpg"; private Breakpoint Start; private Guid SubscriptionId; + private ElementReference ElementReferencePreviewLg; + private ElementReference ElementReferencePreviewMd; + private ElementReference ElementReferencePreviewSm; + private ElementReference ElementReferencePreviewXs; public Dictionary InputAttributes { get; set; } = new Dictionary() @@ -52,12 +58,26 @@ protected override void OnInitialized() { Options = new Options() { - Preview = ".img-preview", + //Preview = ".img-preview", AspectRatio = (decimal)16 / 9, ViewMode = ViewMode.Vm0 }; } + protected override void OnAfterRender(bool firstRender) + { + if (firstRender) + { + Options.Preview = new ElementReference[] + { + ElementReferencePreviewXs, + ElementReferencePreviewSm, + ElementReferencePreviewMd, + ElementReferencePreviewLg + }; + } + } + public async void OnCropEvent(JSEventData cropJSEvent) { if (cropJSEvent?.Detail is not null) @@ -313,16 +333,40 @@ private void Reset() public async void GetCroppedCanvasDataURL(GetCroppedCanvasOptions getCroppedCanvasOptions) { - //CroppedCanvas croppedCanvas = await cropperComponent!.GetCroppedCanvasAsync(getCroppedCanvasOptions); - //string croppedCanvasDataURL = await croppedCanvas!.JSRuntimeObjectRef.InvokeAsync("toDataURL"); - string croppedCanvasDataURL = await CropperComponent!.GetCroppedCanvasDataURLAsync(getCroppedCanvasOptions); - DialogParameters parameters = new() + + OpenCroppedCanvasDialog(croppedCanvasDataURL); + } + + public async void GetCroppedCanvasData(GetCroppedCanvasOptions getCroppedCanvasOptions) + { + CroppedCanvas croppedCanvas = await CropperComponent!.GetCroppedCanvasAsync(getCroppedCanvasOptions); + string croppedCanvasDataURL = await croppedCanvas!.JSRuntimeObjectRef.InvokeAsync("toDataURL", "image/png", 1); + + OpenCroppedCanvasDialog(croppedCanvasDataURL); + } + + public async void GetCroppedCanvasDataByPolygonFilter(GetCroppedCanvasOptions getCroppedCanvasOptions) + { + CroppedCanvas croppedCanvas = await CropperComponent!.GetCroppedCanvasAsync(getCroppedCanvasOptions); + string croppedCanvasDataURL; + + if (CropperFace == CropperFace.Default) { - { "Src", croppedCanvasDataURL } - }; - var options = new DialogOptions() { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true, DisableBackdropClick = true }; - _dialogService.Show("CroppedCanvasDialog", parameters, options); + croppedCanvasDataURL = await croppedCanvas!.JSRuntimeObjectRef.InvokeAsync("toDataURL", "image/png", 1); + } + else if (CropperFace == CropperFace.Circle) + { + croppedCanvasDataURL = await JSRuntime!.InvokeAsync("window.addClipPathEllipse", croppedCanvas!.JSRuntimeObjectRef); + } + else + { + IEnumerable croppedPathToCanvasCropper = GetCroppedPathToCanvasCropper(); + + croppedCanvasDataURL = await JSRuntime!.InvokeAsync("window.addClipPathPolygon", croppedCanvas!.JSRuntimeObjectRef, croppedPathToCanvasCropper); + } + + OpenCroppedCanvasDialog(croppedCanvasDataURL); } public async Task InputFileChangeAsync(InputFileChangeEventArgs inputFileChangeEventArgs) @@ -443,6 +487,50 @@ public void Dispose() GC.SuppressFinalize(this); } + public void SetCropperFace(CropperFace cropperFace) + { + CropperFace = cropperFace; + } + + public string GetClassNameCropper() => + "img-container" + CropperFace switch + { + CropperFace.Default => string.Empty, + CropperFace.Close => " cropper-face-close", + CropperFace.Pentagon => " cropper-face-pentagon", + CropperFace.Circle => " cropper-face-circle", + CropperFace.Arrow => " cropper-face-arrow", + _ => string.Empty, + }; + + public IEnumerable GetCroppedPathToCanvasCropper() => + CropperFace switch + { + // That enumerable is equivalent css like that (the same for another paths) - clip-path: polygon(20% 0%, 0% 20%, 30% 50%, 0% 80%, 20% 100%, 50% 70%, 80% 100%, 100% 80%, 70% 50%, 100% 20%, 80% 0%, 50% 30%); + CropperFace.Close => new List { 20, 0, 0, 20, 30, 50, 0, 80, 20, 100, 50, 70, 80, 100, 100, 80, 70, 50, 100, 20, 80, 0, 50, 30 }, + CropperFace.Pentagon => new List { 50, 0, 100, 38, 82, 100, 18, 100, 0, 38 }, + CropperFace.Arrow => new List { 40, 0, 40, 40, 100, 40, 100, 60, 40, 60, 40, 100, 0, 50 }, + _ => throw new InvalidOperationException() + }; + + private void OpenCroppedCanvasDialog(string croppedCanvasDataURL) + { + DialogParameters parameters = new() + { + { "Src", croppedCanvasDataURL } + }; + + DialogOptions options = new() + { + CloseButton = true, + MaxWidth = MaxWidth.Medium, + FullWidth = true, + DisableBackdropClick = true + }; + + _dialogService.Show("CroppedCanvasDialog", parameters, options); + } + protected virtual void Dispose(bool disposing) { if (disposing) diff --git a/src/Cropper.Blazor/Client/Pages/Examples/InstallServicesForBlazorServerExample.razor b/src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallServicesForBlazorServerExample.razor similarity index 100% rename from src/Cropper.Blazor/Client/Pages/Examples/InstallServicesForBlazorServerExample.razor rename to src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallServicesForBlazorServerExample.razor diff --git a/src/Cropper.Blazor/Client/Pages/Examples/InstallServicesForBlazorServerExampleCode.html b/src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallServicesForBlazorServerExampleCode.html similarity index 100% rename from src/Cropper.Blazor/Client/Pages/Examples/InstallServicesForBlazorServerExampleCode.html rename to src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallServicesForBlazorServerExampleCode.html diff --git a/src/Cropper.Blazor/Client/Pages/Examples/InstallationManualComponentsExample.razor b/src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualComponentsExample.razor similarity index 100% rename from src/Cropper.Blazor/Client/Pages/Examples/InstallationManualComponentsExample.razor rename to src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualComponentsExample.razor diff --git a/src/Cropper.Blazor/Client/Pages/Examples/InstallationManualComponentsExampleCode.html b/src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualComponentsExampleCode.html similarity index 75% rename from src/Cropper.Blazor/Client/Pages/Examples/InstallationManualComponentsExampleCode.html rename to src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualComponentsExampleCode.html index f497f1ce..9b30e4fc 100644 --- a/src/Cropper.Blazor/Client/Pages/Examples/InstallationManualComponentsExampleCode.html +++ b/src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualComponentsExampleCode.html @@ -1,5 +1,6 @@ 
-
+    
+
 @using Cropper.Blazor.Components;
 
 <CropperComponent 
@@ -7,5 +8,18 @@
     Src="cropperblazor.png"
     Options="Blazor.Models.Options()"
 />
-
+ +
+
+
+ // Add this style for cropper. +
+
+.cropper-example {
+    max-height: 300px;
+    width: 100%;
+}
+
+        
+
diff --git a/src/Cropper.Blazor/Client/Pages/Examples/InstallationManualCssFontsExample.razor b/src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualCssFontsExample.razor similarity index 100% rename from src/Cropper.Blazor/Client/Pages/Examples/InstallationManualCssFontsExample.razor rename to src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualCssFontsExample.razor diff --git a/src/Cropper.Blazor/Client/Pages/Examples/InstallationManualCssFontsExampleCode.html b/src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualCssFontsExampleCode.html similarity index 100% rename from src/Cropper.Blazor/Client/Pages/Examples/InstallationManualCssFontsExampleCode.html rename to src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualCssFontsExampleCode.html diff --git a/src/Cropper.Blazor/Client/Pages/Examples/InstallationManualImportsExample.razor b/src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualImportsExample.razor similarity index 100% rename from src/Cropper.Blazor/Client/Pages/Examples/InstallationManualImportsExample.razor rename to src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualImportsExample.razor diff --git a/src/Cropper.Blazor/Client/Pages/Examples/InstallationManualImportsExampleCode.html b/src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualImportsExampleCode.html similarity index 100% rename from src/Cropper.Blazor/Client/Pages/Examples/InstallationManualImportsExampleCode.html rename to src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualImportsExampleCode.html diff --git a/src/Cropper.Blazor/Client/Pages/Examples/InstallationManualPackageExample.razor b/src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualPackageExample.razor similarity index 100% rename from src/Cropper.Blazor/Client/Pages/Examples/InstallationManualPackageExample.razor rename to src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualPackageExample.razor diff --git a/src/Cropper.Blazor/Client/Pages/Examples/InstallationManualPackageExampleCode.html b/src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualPackageExampleCode.html similarity index 100% rename from src/Cropper.Blazor/Client/Pages/Examples/InstallationManualPackageExampleCode.html rename to src/Cropper.Blazor/Client/Pages/Examples/Installation/InstallationManualPackageExampleCode.html diff --git a/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromElementReferenceSelectorComponentsExample.razor b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromElementReferenceSelectorComponentsExample.razor new file mode 100644 index 00000000..5730dad3 --- /dev/null +++ b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromElementReferenceSelectorComponentsExample.razor @@ -0,0 +1,25 @@ +@using Cropper.Blazor.Components + +
+ +
+
+ +@code { + private Blazor.Models.Options Options = new(); + private ElementReference ElementReference; + + protected override void OnAfterRender(bool firstRender) + { + if (firstRender) + { + Options.Preview = new ElementReference[] + { + ElementReference + }; + } + } +} diff --git a/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromElementReferenceSelectorComponentsExample.razor.css b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromElementReferenceSelectorComponentsExample.razor.css new file mode 100644 index 00000000..515e4e8c --- /dev/null +++ b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromElementReferenceSelectorComponentsExample.razor.css @@ -0,0 +1,14 @@ +.cropper-example { + max-height: 300px; + width: 100%; +} + +.img-example-preview { + width: 100%; + height: 300px; + overflow: hidden; +} + +.img-example-preview > ::deep img { + max-width: 100%; +} diff --git a/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromElementReferenceSelectorComponentsExampleCode.html b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromElementReferenceSelectorComponentsExampleCode.html new file mode 100644 index 00000000..343478c0 --- /dev/null +++ b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromElementReferenceSelectorComponentsExampleCode.html @@ -0,0 +1,54 @@ +
+
+
+@using Cropper.Blazor.Components;
+
+<div class="img-container"
+        <CropperComponent 
+            Class="cropper-example"
+            Src="cropperblazor.png"
+            Options="Options"
+        />
+</div>
+<div @ref="ElementReference" class="img-example-preview" />
+
+
@code {
+    private Blazor.Models.Options Options = new();
+    private Microsoft.AspNetCore.Components.ElementReference ElementReference;
+
+    protected override void OnAfterRender(bool firstRender)
+    {
+        if (firstRender)
+        {
+            Options.Preview = new ElementReference[]
+            {
+                ElementReference
+            };
+        }
+    }
+}
+
+
+
+
+ // Add this style for cropper and preview image. +
+
+.cropper-example {
+    max-height: 300px;
+    width: 100%;
+}
+
+.img-example-preview {
+    width: 100%;
+    height: 300px;
+    overflow: hidden;
+}
+
+.img-example-preview > ::deep img {
+    max-width: 100%;
+}
+
+        
+
+
diff --git a/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromMultipleElementReferenceSelectorComponentsExample.razor b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromMultipleElementReferenceSelectorComponentsExample.razor new file mode 100644 index 00000000..04395890 --- /dev/null +++ b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromMultipleElementReferenceSelectorComponentsExample.razor @@ -0,0 +1,25 @@ +@using Cropper.Blazor.Components + +
+ +
+
+
+ +@code { + private Blazor.Models.Options Options = new(); + private ElementReference FirstElementReference; + private ElementReference SecondElementReference; + + protected override void OnAfterRender(bool firstRender) + { + if (firstRender) + { + Options.Preview = new ElementReference[] + { + FirstElementReference, + SecondElementReference + }; + } + } +} diff --git a/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromMultipleElementReferenceSelectorComponentsExample.razor.css b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromMultipleElementReferenceSelectorComponentsExample.razor.css new file mode 100644 index 00000000..515e4e8c --- /dev/null +++ b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromMultipleElementReferenceSelectorComponentsExample.razor.css @@ -0,0 +1,14 @@ +.cropper-example { + max-height: 300px; + width: 100%; +} + +.img-example-preview { + width: 100%; + height: 300px; + overflow: hidden; +} + +.img-example-preview > ::deep img { + max-width: 100%; +} diff --git a/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromMultipleElementReferenceSelectorComponentsExampleCode.html b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromMultipleElementReferenceSelectorComponentsExampleCode.html new file mode 100644 index 00000000..97293bd9 --- /dev/null +++ b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromMultipleElementReferenceSelectorComponentsExampleCode.html @@ -0,0 +1,57 @@ +
+
+
+@using Cropper.Blazor.Components;
+
+<div class="img-container"
+        <CropperComponent 
+            Class="cropper-example"
+            Src="cropperblazor.png"
+            Options="Options"
+        />
+</div>
+<div @ref="FirstElementReference" class="img-example-preview" />
+<div @ref="SecondElementReference" class="img-example-preview" />
+
+
@code {
+    private Blazor.Models.Options Options = new();
+    private Microsoft.AspNetCore.Components.ElementReference FirstElementReference;
+    private Microsoft.AspNetCore.Components.ElementReference SecondElementReference;
+
+    protected override void OnAfterRender(bool firstRender)
+    {
+        if (firstRender)
+        {
+            Options.Preview = new ElementReference[]
+            {
+                FirstElementReference,
+                SecondElementReference
+            };
+        }
+    }
+}
+
+
+
+
+ // Add this style for cropper and preview image. +
+
+.cropper-example {
+    max-height: 300px;
+    width: 100%;
+}
+
+.img-example-preview {
+    width: 100%;
+    height: 300px;
+    overflow: hidden;
+}
+
+.img-example-preview > ::deep img {
+    max-width: 100%;
+}
+
+        
+
+
diff --git a/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromStringSelectorComponentsExample.razor b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromStringSelectorComponentsExample.razor new file mode 100644 index 00000000..13f4d5a9 --- /dev/null +++ b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromStringSelectorComponentsExample.razor @@ -0,0 +1,16 @@ +@using Cropper.Blazor.Components + +
+ +
+
+ +@code { + private Blazor.Models.Options Options = new Blazor.Models.Options + { + Preview = ".img-example-preview" + }; +} diff --git a/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromStringSelectorComponentsExample.razor.css b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromStringSelectorComponentsExample.razor.css new file mode 100644 index 00000000..515e4e8c --- /dev/null +++ b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromStringSelectorComponentsExample.razor.css @@ -0,0 +1,14 @@ +.cropper-example { + max-height: 300px; + width: 100%; +} + +.img-example-preview { + width: 100%; + height: 300px; + overflow: hidden; +} + +.img-example-preview > ::deep img { + max-width: 100%; +} diff --git a/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromStringSelectorComponentsExampleCode.html b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromStringSelectorComponentsExampleCode.html new file mode 100644 index 00000000..11f76130 --- /dev/null +++ b/src/Cropper.Blazor/Client/Pages/Examples/Uses/Preview/UsesPreviewFromStringSelectorComponentsExampleCode.html @@ -0,0 +1,45 @@ +
+
+
+@using Cropper.Blazor.Components;
+
+<div class="img-container"
+        <CropperComponent 
+            Class="cropper-example"
+            Src="cropperblazor.png"
+            Options="Options"
+        />
+</div>
+<div class="img-example-preview" />
+
+
@code {
+    private Blazor.Models.Options Options = new Blazor.Models.Options
+    {
+        Preview = ".img-example-preview"
+    };
+}
+
+
+
+
+ // Add this style for cropper and preview image. +
+
+.cropper-example {
+    max-height: 300px;
+    width: 100%;
+}
+
+.img-example-preview {
+    width: 100%;
+    height: 300px;
+    overflow: hidden;
+}
+
+.img-example-preview > ::deep img {
+    max-width: 100%;
+}
+
+        
+
+
diff --git a/src/Cropper.Blazor/Client/Pages/Index.razor b/src/Cropper.Blazor/Client/Pages/Index.razor index 7defb1f2..7610889f 100644 --- a/src/Cropper.Blazor/Client/Pages/Index.razor +++ b/src/Cropper.Blazor/Client/Pages/Index.razor @@ -2,7 +2,8 @@ @using Cropper.Blazor.Client.Components @using Cropper.Blazor.Client.Components.Docs @using Cropper.Blazor.Client.Models; -@using Cropper.Blazor.Client.Pages.Examples +@using Cropper.Blazor.Client.Pages.Examples.Installation +@using Cropper.Blazor.Client.Pages.Examples.Uses.Preview @@ -22,8 +23,8 @@ - Cropper.Blazor - is a component element that wraps around + Cropper.Blazor + is a component element that wraps around Cropper.js @@ -113,6 +114,27 @@ + + + Add the following components to your MainLayout.razor + + Notes for Preview Option: + + + + + + + + + + diff --git a/src/Cropper.Blazor/Client/Pages/Index.razor.cs b/src/Cropper.Blazor/Client/Pages/Index.razor.cs new file mode 100644 index 00000000..d027320c --- /dev/null +++ b/src/Cropper.Blazor/Client/Pages/Index.razor.cs @@ -0,0 +1,34 @@ +using Cropper.Blazor.Client.Components.Docs; +using Cropper.Blazor.Client.Pages.Examples.Uses.Preview; +using Microsoft.AspNetCore.Components; + +namespace Cropper.Blazor.Client.Pages +{ + public partial class Index + { + SectionContent ActivePreviewActive = null!; + + RenderFragment PreviewActiveRenderFragment => builder => + { + if (ActivePreviewActive.ActiveCode == nameof(UsesPreviewFromStringSelectorComponentsExample)) + { + builder.OpenComponent(1); + builder.CloseComponent(); + } + else if (ActivePreviewActive.ActiveCode == nameof(UsesPreviewFromElementReferenceSelectorComponentsExample)) + { + builder.OpenComponent(1); + builder.CloseComponent(); + } + else if (ActivePreviewActive.ActiveCode == nameof(UsesPreviewFromMultipleElementReferenceSelectorComponentsExample)) + { + builder.OpenComponent(1); + builder.CloseComponent(); + } + else + { + throw new InvalidOperationException(); + } + }; + } +} diff --git a/src/Cropper.Blazor/Client/Shared/MainLayout.razor b/src/Cropper.Blazor/Client/Shared/MainLayout.razor index 15be5a99..7d03b584 100644 --- a/src/Cropper.Blazor/Client/Shared/MainLayout.razor +++ b/src/Cropper.Blazor/Client/Shared/MainLayout.razor @@ -4,6 +4,7 @@ + diff --git a/src/Cropper.Blazor/Client/Shared/UpdateAvailableDetector.razor b/src/Cropper.Blazor/Client/Shared/UpdateAvailableDetector.razor new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/Cropper.Blazor/Client/Shared/UpdateAvailableDetector.razor @@ -0,0 +1 @@ + diff --git a/src/Cropper.Blazor/Client/Shared/UpdateAvailableDetector.razor.cs b/src/Cropper.Blazor/Client/Shared/UpdateAvailableDetector.razor.cs new file mode 100644 index 00000000..3d7dd439 --- /dev/null +++ b/src/Cropper.Blazor/Client/Shared/UpdateAvailableDetector.razor.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using MudBlazor; + +namespace Cropper.Blazor.Client.Shared +{ + public partial class UpdateAvailableDetector + { + [Inject] private IJSRuntime? JSRuntime { get; set; } + [Inject] private ISnackbar? Snackbar { get; set; } + [Inject] private NavigationManager? Navigation { get; set; } + + protected override async Task OnInitializedAsync() + { + await RegisterForUpdateAvailableNotification(); + } + + private async Task RegisterForUpdateAvailableNotification() + { + await JSRuntime!.InvokeAsync( + identifier: "registerForUpdateAvailableNotification", + DotNetObjectReference.Create(this), + nameof(OnUpdateAvailable)); + } + + [JSInvokable(nameof(OnUpdateAvailable))] + public void OnUpdateAvailable() + { + Snackbar!.Add("A new version of the application is available.", Severity.Warning, config => + { + config.Action = "Reload"; + config.IconColor = Color.Error; + config.ActionColor = Color.Error; + config.Icon = Icons.Material.Filled.BrowserUpdated; + config.RequireInteraction = true; + config.Onclick = snackbar => + { + Navigation!.NavigateTo(Navigation.Uri, true); + + return Task.CompletedTask; + }; + }); + } + } +} diff --git a/src/Cropper.Blazor/Client/Styles/Cropper.Blazor.Client.scss b/src/Cropper.Blazor/Client/Styles/Cropper.Blazor.Client.scss index 09abc5ab..6179f399 100644 --- a/src/Cropper.Blazor/Client/Styles/Cropper.Blazor.Client.scss +++ b/src/Cropper.Blazor/Client/Styles/Cropper.Blazor.Client.scss @@ -1,5 +1,6 @@ @import 'layout/_mainlayout'; @import 'layout/_markdown'; +@import 'layout/_updateAvaibleDetector.scss'; @import 'components/docssection.scss'; .no-select { @@ -117,3 +118,23 @@ html, body { .cropped-canvas-dialog .mud-dialog-title { padding-bottom: 0; } + +.cropper-face { + opacity: 25%; +} + +.img-container.cropper-face-close .cropper-container .cropper-crop-box .cropper-face { + clip-path: polygon(20% 0%, 0% 20%, 30% 50%, 0% 80%, 20% 100%, 50% 70%, 80% 100%, 100% 80%, 70% 50%, 100% 20%, 80% 0%, 50% 30%); +} + +.img-container.cropper-face-arrow .cropper-container .cropper-crop-box .cropper-face { + clip-path: polygon(40% 0%, 40% 40%, 100% 40%, 100% 60%, 40% 60%, 40% 100%, 0% 50%); +} + +.img-container.cropper-face-circle .cropper-container .cropper-crop-box .cropper-face { + border-radius: 50%; +} + +.img-container.cropper-face-pentagon .cropper-container .cropper-crop-box .cropper-face { + clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%); +} diff --git a/src/Cropper.Blazor/Client/Styles/layout/_markdown.scss b/src/Cropper.Blazor/Client/Styles/layout/_markdown.scss index ed250cab..3f0602b2 100644 --- a/src/Cropper.Blazor/Client/Styles/layout/_markdown.scss +++ b/src/Cropper.Blazor/Client/Styles/layout/_markdown.scss @@ -109,6 +109,16 @@ direction: ltr; } + & .css { + & .property { + color: hsl(76, 21%, 52%); + } + + & .comment { + color: #57a64a; + } + } + & .csharp { & .atSign { color: var(--mud-palette-text-primary); diff --git a/src/Cropper.Blazor/Client/Styles/layout/_updateAvaibleDetector.scss b/src/Cropper.Blazor/Client/Styles/layout/_updateAvaibleDetector.scss new file mode 100644 index 00000000..2291b4c9 --- /dev/null +++ b/src/Cropper.Blazor/Client/Styles/layout/_updateAvaibleDetector.scss @@ -0,0 +1,5 @@ +@media (min-width: 0) and (max-width: 510px) { + .mud-snackbar-location-bottom-right { + right: 0 !important; + } +} diff --git a/src/Cropper.Blazor/Client/wwwroot/helper.js b/src/Cropper.Blazor/Client/wwwroot/helper.js index b4af1b2a..59ad08a5 100644 --- a/src/Cropper.Blazor/Client/wwwroot/helper.js +++ b/src/Cropper.Blazor/Client/wwwroot/helper.js @@ -4,4 +4,50 @@ anchorElement.download = options.fileName ?? ''; anchorElement.click(); anchorElement.remove(); -}; \ No newline at end of file +}; + +window.addClipPathPolygon = (sourceCanvas, path) => { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + const width = sourceCanvas.width, + height = sourceCanvas.height; + + canvas.width = width; + canvas.height = height; + context.imageSmoothingEnabled = true; + + context.beginPath(); + context.moveTo(path[0] * width / 100, path[1] * height / 100); + context.fillStyle = "rgba(255, 255, 255, 0)"; + + for (let i = 2; i < path.length; i += 2) { + context.lineTo(path[i] * width / 100, path[i + 1] * height / 100); + } + + context.closePath(); + context.clip(); + context.fill(); + context.globalCompositeOperation = 'lighter'; + context.drawImage(sourceCanvas, 0, 0, width, height); + + return canvas.toDataURL("image/png", 1); +} + +window.addClipPathEllipse = (sourceCanvas) => { + const createdCanvas = document.createElement('canvas'); + const contextCanvas = createdCanvas.getContext('2d'); + const widthCanvas = sourceCanvas.width, + heightCanvas = sourceCanvas.height; + + createdCanvas.width = widthCanvas; + createdCanvas.height = heightCanvas; + contextCanvas.imageSmoothingEnabled = true; + + contextCanvas.drawImage(sourceCanvas, 0, 0, widthCanvas, heightCanvas); + contextCanvas.globalCompositeOperation = 'destination-in'; + contextCanvas.beginPath(); + contextCanvas.ellipse(widthCanvas / 2, heightCanvas / 2, widthCanvas / 2, heightCanvas / 2, 0 * Math.PI, 0, 180 * Math.PI, true); + contextCanvas.fill(); + + return createdCanvas.toDataURL("image/png", 1); +} diff --git a/src/Cropper.Blazor/Client/wwwroot/index.html b/src/Cropper.Blazor/Client/wwwroot/index.html index ca7e8df9..15d27955 100644 --- a/src/Cropper.Blazor/Client/wwwroot/index.html +++ b/src/Cropper.Blazor/Client/wwwroot/index.html @@ -131,7 +131,7 @@ - + diff --git a/src/Cropper.Blazor/Client/wwwroot/sw-registrator.js b/src/Cropper.Blazor/Client/wwwroot/sw-registrator.js new file mode 100644 index 00000000..782e60b2 --- /dev/null +++ b/src/Cropper.Blazor/Client/wwwroot/sw-registrator.js @@ -0,0 +1,38 @@ +window.updateAvailable = new Promise((resolve, reject) => { + if (!('serviceWorker' in navigator)) { + const errorMessage = `This browser doesn't support service workers`; + console.error(errorMessage); + reject(errorMessage); + return; + } + + navigator.serviceWorker.register('/service-worker.min.js') + .then(registration => { + console.info(`Service worker registration successful (scope: ${registration.scope})`); + + setInterval(() => { + registration.update(); + }, 60 * 1000); // 60000ms -> check each minute + + registration.onupdatefound = () => { + const installingServiceWorker = registration.installing; + installingServiceWorker.onstatechange = () => { + if (installingServiceWorker.state === 'installed') { + resolve(!!navigator.serviceWorker.controller); + } + } + }; + }) + .catch(error => { + console.error('Service worker registration failed with error:', error); + reject(error); + }); +}); + +window.registerForUpdateAvailableNotification = (caller, methodName) => { + window.updateAvailable.then(isUpdateAvailable => { + if (isUpdateAvailable) { + caller.invokeMethodAsync(methodName).then(); + } + }); +}; \ No newline at end of file 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 0854704c..4ee8b62a 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Components/CropperComponent_Should.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Components/CropperComponent_Should.cs @@ -45,6 +45,48 @@ public CropperComponent_Should() _testContext.Services.AddSingleton(_mockCropperJsInterop.Object); } + [Theory] + [InlineData(-100)] + [InlineData(-1)] + [InlineData(-0.99)] + [InlineData(1.11)] + [InlineData(111)] + public async Task Throw_Exception_Because_Of_Invalid_NumberAsync(float numberImageQuality) + { + // arrange + Faker faker = new(); + CancellationToken cancellationToken = new(); + + GetCroppedCanvasOptions getCroppedCanvasOptions = new Faker() + .Generate(); + string imageFormatType = faker.Random.Word(); + + IRenderedComponent cropperComponent = _testContext + .RenderComponent(); + + await cropperComponent.InvokeAsync(async () => + { + // act + Func func = async () => await cropperComponent.Instance.GetCroppedCanvasDataURLAsync( + getCroppedCanvasOptions, + imageFormatType, + numberImageQuality, + cancellationToken); + + // assert + await func + .Should() + .ThrowAsync() + .WithMessage($"The given number should be between 0 and 1 for indication the image quality, but found {numberImageQuality}. (Parameter 'number')"); + + _mockCropperJsInterop.Verify(c => c.GetCroppedCanvasDataURLAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never()); + }); + } + [Fact] public async Task Should_Render_CropperComponent_SuccessfulAsync() { @@ -95,7 +137,7 @@ public async Task Should_Render_CropperComponent_SuccessfulAsync() .Generate(); GetCroppedCanvasOptions getCroppedCanvasOptions = new Faker() .Generate(); - Mock mockIJSObjectReference = new Mock(); + Mock mockIJSObjectReference = new(); CroppedCanvas expectedCroppedCanvas = new Faker() .CustomInstantiator(c => new CroppedCanvas(mockIJSObjectReference.Object)); CropperData expectedCropperData = new Faker() @@ -129,6 +171,8 @@ public async Task Should_Render_CropperComponent_SuccessfulAsync() decimal pivotY = faker.Random.Decimal(); string newUrlImage = faker.Random.Word(); bool hasSameSize = faker.Random.Bool(); + string imageFormatType = faker.Random.Word(); + float numberImageQuality = faker.Random.Float(0, 1); Action? onLoadImageHandler = () => { @@ -236,7 +280,11 @@ public async Task Should_Render_CropperComponent_SuccessfulAsync() .ReturnsAsync(expectedCroppedCanvas); _mockCropperJsInterop - .Setup(c => c.GetCroppedCanvasDataURLAsync(getCroppedCanvasOptions, cancellationToken)) + .Setup(c => c.GetCroppedCanvasDataURLAsync( + getCroppedCanvasOptions, + imageFormatType, + numberImageQuality, + cancellationToken)) .ReturnsAsync(expectedCroppedCanvasDataURL); _mockCropperJsInterop @@ -344,9 +392,9 @@ await cropperComponent.InvokeAsync(async () => expectedCroppedCanvas.Should().BeEquivalentTo(croppedCanvas); _mockCropperJsInterop.Verify(c => c.GetCroppedCanvasAsync(getCroppedCanvasOptions, cancellationToken), Times.Once()); - string croppedCanvasDataURL = await cropperComponent.Instance.GetCroppedCanvasDataURLAsync(getCroppedCanvasOptions); + string croppedCanvasDataURL = await cropperComponent.Instance.GetCroppedCanvasDataURLAsync(getCroppedCanvasOptions, imageFormatType, numberImageQuality); expectedCroppedCanvasDataURL.Should().BeEquivalentTo(croppedCanvasDataURL); - _mockCropperJsInterop.Verify(c => c.GetCroppedCanvasDataURLAsync(getCroppedCanvasOptions, cancellationToken), Times.Once()); + _mockCropperJsInterop.Verify(c => c.GetCroppedCanvasDataURLAsync(getCroppedCanvasOptions, imageFormatType, numberImageQuality, cancellationToken), Times.Once()); CropperData cropperData = await cropperComponent.Instance.GetDataAsync(isRounded); expectedCropperData.Should().BeEquivalentTo(cropperData); 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 7626347e..1fc65ae7 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Cropper.Blazor.UnitTests.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Cropper.Blazor.UnitTests.csproj @@ -9,16 +9,16 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all 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 9cc17931..7536026c 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/ServiceCollectionExtensions_Should.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/ServiceCollectionExtensions_Should.cs @@ -36,7 +36,7 @@ static object[] WrapArgs( CropperJsInteropOptions? cropperJsInteropOptions) => new object[] { - cropperJsInteropOptions + cropperJsInteropOptions! }; } } diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Models/Options_Should.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Models/Options_Should.cs new file mode 100644 index 00000000..45741cb9 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Models/Options_Should.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using Cropper.Blazor.Models; +using FluentAssertions; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using Microsoft.JSInterop.Infrastructure; +using Xunit; + +namespace Cropper.Blazor.UnitTests.Models +{ + public class Options_Should + { + private readonly Options _options; + private readonly InternalJSRuntime _internalJSRuntime; + + public class InternalJSRuntime : JSRuntime + { + protected override void BeginInvokeJS(long taskId, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId) + { + throw new System.NotImplementedException(); + } + + protected override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) + { + throw new System.NotImplementedException(); + } + + public JsonSerializerOptions GetJsonSerializerOptions => JsonSerializerOptions; + } + + public class CustomElementReferenceContext : ElementReferenceContext + { + + } + + public Options_Should() + { + _options = new(); + _internalJSRuntime = new(); + } + + [Theory, MemberData(nameof(TestData_Setup_Options_Preview))] + public void Verify_Setup_Options_Preview(object? preview, string expectedObject) + { + // arrange + _options.Preview = preview; + + // act + string resultObj = JsonSerializer.Serialize( + _options, + _internalJSRuntime.GetJsonSerializerOptions); + + // assert + _options.Preview.Should().BeEquivalentTo(preview); + + resultObj.Should().BeEquivalentTo(expectedObject); + } + + [Theory, MemberData(nameof(TestData_Throw_ArgumentException_When_Setup_Options_Preview))] + public void Throw_ArgumentException_When_Setup_Options_Preview(object? preview, string expectedMessage) + { + // arrange + Action act = () => _options.Preview = preview; + + // act & assert + act + .Should() + .Throw() + .WithMessage(expectedMessage); + } + + public static IEnumerable TestData_Setup_Options_Preview() + { + yield return WrapArgs( + null, + "{\"correlationId\":\"Cropper.Blazor\"}"); + + yield return WrapArgs( + ".testClass", + "{\"preview\":\".testClass\",\"correlationId\":\"Cropper.Blazor\"}"); + + yield return WrapArgs( + new ElementReference("ElementReferenceId"), + "{\"preview\":{\"id\":\"ElementReferenceId\",\"context\":null},\"correlationId\":\"Cropper.Blazor\"}"); + + yield return WrapArgs( + new ElementReference("ElementReferenceId", new CustomElementReferenceContext()), + "{\"preview\":{\"id\":\"ElementReferenceId\",\"context\":{}},\"correlationId\":\"Cropper.Blazor\"}"); + + yield return WrapArgs( + new ElementReference[] + { + new ElementReference("ElementReferenceId"), + new ElementReference("ElementReferenceId", new CustomElementReferenceContext()) + }, + "{\"preview\":[{\"id\":\"ElementReferenceId\",\"context\":null},{\"id\":\"ElementReferenceId\",\"context\":{}}],\"correlationId\":\"Cropper.Blazor\"}"); + + static object[] WrapArgs( + object? preview, + string expectedObject) + => new object[] + { + preview!, + expectedObject + }; + } + + public static IEnumerable TestData_Throw_ArgumentException_When_Setup_Options_Preview() + { + yield return WrapArgs( + Array.Empty(), + "'Preview' should be not an empty collection of ElementReference."); + + yield return WrapArgs( + new ElementReference(), + "'Preview' must not contain an empty Reference element."); + + yield return WrapArgs( + new ElementReference[] + { + new ElementReference(), + new ElementReference("ElementReferenceId"), + new ElementReference("ElementReferenceId", new CustomElementReferenceContext()) + }, + "'Preview' must not contain an empty Reference element in the ElementReference collection."); + + yield return WrapArgs( + "", + "'Preview' should be not an empty or include white spaces in the string."); + + yield return WrapArgs( + " ", + "'Preview' should be not an empty or include white spaces in the string."); + + yield return WrapArgs( + new object(), + "'Preview' is only available for string, ElementReference, IEnumerable types, but found 'System.Object' type."); + + yield return WrapArgs( + new CustomElementReferenceContext(), + "'Preview' is only available for string, ElementReference, IEnumerable types, but found 'Cropper.Blazor.UnitTests.Models.Options_Should+CustomElementReferenceContext' type."); + + static object[] WrapArgs( + object? preview, + string expectedMessage) + => new object[] + { + preview!, + expectedMessage + }; + } + } +} 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 988968b0..55d90ea4 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/CropperJsInterop_Should.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/CropperJsInterop_Should.cs @@ -230,21 +230,23 @@ public async Task Verify_GetCroppedCanvasAsync() } [Fact] - public async Task Verify_GetCroppedCanvasDataURLAsync() + public async Task Verify_GetCroppedCanvasDataURL_By_TypeAndNumber_Async() { // arrange string expectedCroppedCanvasURL = _faker.Random.Word(); + string type = _faker.Random.Word(); + float number = _faker.Random.Float(); GetCroppedCanvasOptions getCroppedCanvasOptions = new Faker(); _testContext.JSInterop - .Setup("cropper.getCroppedCanvasDataURL", getCroppedCanvasOptions) + .Setup("cropper.getCroppedCanvasDataURL", getCroppedCanvasOptions, type, number) .SetResult(expectedCroppedCanvasURL); // assert VerifyLoadCropperModule(DefaultPathToCropperModule); // act - string croppedCanvasURL = await _cropperJsInterop.GetCroppedCanvasDataURLAsync(getCroppedCanvasOptions); + string croppedCanvasURL = await _cropperJsInterop.GetCroppedCanvasDataURLAsync(getCroppedCanvasOptions, type, number); // assert expectedCroppedCanvasURL.Should().BeEquivalentTo(croppedCanvasURL); diff --git a/src/Cropper.Blazor/Cropper.Blazor.sln b/src/Cropper.Blazor/Cropper.Blazor.sln index 29d818d7..027871bb 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.sln +++ b/src/Cropper.Blazor/Cropper.Blazor.sln @@ -32,7 +32,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pipelines", "pipelines", "{ ProjectSection(SolutionItems) = preProject ..\..\.github\workflows\cd.yml = ..\..\.github\workflows\cd.yml ..\..\.github\workflows\ci.yml = ..\..\.github\workflows\ci.yml - ..\..\.github\codecov.yml = ..\..\.github\codecov.yml + ..\..\.github\dependabot.yml = ..\..\.github\dependabot.yml ..\..\.github\workflows\release.yml = ..\..\.github\workflows\release.yml EndProjectSection EndProject diff --git a/src/Cropper.Blazor/Cropper.Blazor/.config/dotnet-tools.json b/src/Cropper.Blazor/Cropper.Blazor/.config/dotnet-tools.json index 873ad451..d8410c8d 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/.config/dotnet-tools.json +++ b/src/Cropper.Blazor/Cropper.Blazor/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "excubo.webcompiler": { - "version": "2.7.14", + "version": "3.5.54", "commands": [ "webcompiler" ] diff --git a/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.cs b/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.cs index 50a66659..ea97b3a4 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using Cropper.Blazor.Base; @@ -569,11 +570,28 @@ public async ValueTask GetCroppedCanvasAsync(GetCroppedCanvasOpti /// Get a canvas drawn from the cropped image (lossy compression). If it is not cropped, then returns a canvas drawn the whole image. /// /// Options for getting cropped canvas. + /// A string indicating the image format. The default type is image/png; this image format will be also used if the specified type is not supported. + /// A number between 0 and 1 indicating the image quality to be used when creating images using file formats that support lossy compression (such as image/jpeg or image/webp). + /// Different browsers have different image encoder compression, usually it is 92 or 80 percent of the full image quality. + /// /// The used to propagate notifications that the operation should be canceled. /// A representing canvas drawn the cropped image in URL format asynchronous operation. - public async ValueTask GetCroppedCanvasDataURLAsync(GetCroppedCanvasOptions getCroppedCanvasOptions, CancellationToken cancellationToken = default) + public async ValueTask GetCroppedCanvasDataURLAsync( + GetCroppedCanvasOptions getCroppedCanvasOptions, + string type = "image/png", + float number = 1, + CancellationToken cancellationToken = default) { - return await CropperJsIntertop!.GetCroppedCanvasDataURLAsync(getCroppedCanvasOptions, cancellationToken); + + return number switch + { + < 0 or > 1 => throw new ArgumentException($"The given number should be between 0 and 1 for indication the image quality, but found {number}.", nameof(number)), + _ => await CropperJsIntertop!.GetCroppedCanvasDataURLAsync( + getCroppedCanvasOptions, + type, + number, + cancellationToken) + }; } /// diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 2bbe6ea5..ecb17ad9 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.2 + 1.2.3 LICENSE NuGet.png Cropper.Blazor diff --git a/src/Cropper.Blazor/Cropper.Blazor/Models/Options.cs b/src/Cropper.Blazor/Cropper.Blazor/Models/Options.cs index 9643e0f4..4529fc33 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Models/Options.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Models/Options.cs @@ -1,5 +1,9 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Components; namespace Cropper.Blazor.Models { @@ -8,6 +12,8 @@ namespace Cropper.Blazor.Models /// public class Options { + private object? preview = null; + /// /// Define the fixed aspect ratio of the crop box. /// @@ -20,14 +26,74 @@ public class Options /// /// Add extra elements (containers) for preview. - /// An element or an array of elements or a node list object or a valid selector for Document.querySelectorAll. + /// An element or an array of elements or a list or a valid string selector for Document.querySelectorAll. /// /// - /// Default: '' + /// Default: null. + ///
+ /// Notes: + ///
+ /// The maximum width is the initial width of the preview container. + ///
+ /// The maximum height is the initial height of the preview container. + ///
+ /// If you set an aspectRatio option, be sure to set the same aspect ratio to the preview container. + ///
+ /// If the preview does not display correctly, set the overflow: hidden style to the preview container. ///
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("preview")] - public string Preview { get; set; } = string.Empty; + public object? Preview + { + get => preview; + set + { + if (value is ElementReference elementReference) + { + if (elementReference.Equals(default(ElementReference))) + { + throw new ArgumentException("'Preview' must not contain an empty Reference element."); + } + + preview = value; + } + else if (value is string classValue) + { + if (!string.IsNullOrWhiteSpace(classValue)) + { + preview = value; + } + else + { + throw new ArgumentException("'Preview' should be not an empty or include white spaces in the string."); + } + } + else if (value is IEnumerable listElementReferences) + { + if (listElementReferences.Any(elementReference => elementReference.Equals(default(ElementReference)))) + { + throw new ArgumentException("'Preview' must not contain an empty Reference element in the ElementReference collection."); + } + + if (listElementReferences.Any()) + { + preview = value; + } + else + { + throw new ArgumentException("'Preview' should be not an empty collection of ElementReference."); + } + } + else if (value is null) + { + preview = value; + } + else + { + throw new ArgumentException($"'Preview' is only available for string, ElementReference, IEnumerable types, but found '{value!.GetType()}' type."); + } + } + } /// /// Enable to crop the image automatically when initialized. diff --git a/src/Cropper.Blazor/Cropper.Blazor/Services/CropperJsInterop.cs b/src/Cropper.Blazor/Cropper.Blazor/Services/CropperJsInterop.cs index 02f0a7d7..fdb30e4b 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Services/CropperJsInterop.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Services/CropperJsInterop.cs @@ -240,10 +240,16 @@ public async ValueTask GetCroppedCanvasAsync( /// Get a canvas drawn the cropped image. /// /// The config options. + /// A string indicating the image format. The default type is image/png; this image format will be also used if the specified type is not supported. + /// A number between 0 and 1 indicating the image quality to be used when creating images using file formats that support lossy compression (such as image/jpeg or image/webp). A user agent will use its default quality value if this option is not specified, or if the number is outside the allowed range. + /// Different browsers have different image encoder compression, usually it is 92 or 80 percent of the full image quality. + /// /// The used to propagate notifications that the operation should be canceled. /// A representing URL result canvas asynchronous operation. public async ValueTask GetCroppedCanvasDataURLAsync( GetCroppedCanvasOptions getCroppedCanvasOptions, + string type, + float number, CancellationToken cancellationToken = default) { if (Module is null) @@ -251,7 +257,12 @@ public async ValueTask GetCroppedCanvasDataURLAsync( await LoadModuleAsync(cancellationToken); } - return await _jsRuntime!.InvokeAsync("cropper.getCroppedCanvasDataURL", cancellationToken, getCroppedCanvasOptions); + return await _jsRuntime!.InvokeAsync( + "cropper.getCroppedCanvasDataURL", + cancellationToken, + getCroppedCanvasOptions, + type, + number); } /// diff --git a/src/Cropper.Blazor/Cropper.Blazor/Services/ICropperJsInterop.cs b/src/Cropper.Blazor/Cropper.Blazor/Services/ICropperJsInterop.cs index f137fc5e..0c781b42 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Services/ICropperJsInterop.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Services/ICropperJsInterop.cs @@ -308,10 +308,16 @@ ValueTask GetCroppedCanvasAsync( /// Get a canvas drawn the cropped image. /// /// The config options. + /// A string indicating the image format. The default type is image/png; this image format will be also used if the specified type is not supported. + /// A number between 0 and 1 indicating the image quality to be used when creating images using file formats that support lossy compression (such as image/jpeg or image/webp). A user agent will use its default quality value if this option is not specified, or if the number is outside the allowed range. + /// Different browsers have different image encoder compression, usually it is 92 or 80 percent of the full image quality. + /// /// The used to propagate notifications that the operation should be canceled. /// A representing URL result canvas asynchronous operation. ValueTask GetCroppedCanvasDataURLAsync( GetCroppedCanvasOptions getCroppedCanvasOptions, + string type, + float number, CancellationToken cancellationToken = default); /// diff --git a/src/Cropper.Blazor/Cropper.Blazor/excubowebcompiler.json b/src/Cropper.Blazor/Cropper.Blazor/excubowebcompiler.json index 852c2d92..4e46967c 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/excubowebcompiler.json +++ b/src/Cropper.Blazor/Cropper.Blazor/excubowebcompiler.json @@ -42,8 +42,7 @@ "Sass": { "IndentType": "Space", "IndentWidth": 2, - "OutputStyle": "Nested", - "Precision": 5, + "OutputStyle": "Expanded", "RelativeUrls": true, "LineFeed": "Lf", "SourceMap": false diff --git a/src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropperJsInterop.js b/src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropperJsInterop.js index 7d9e89b3..07f35864 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropperJsInterop.js +++ b/src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropperJsInterop.js @@ -40,8 +40,8 @@ class CropperDecorator { return this.cropperInstance.getCroppedCanvas(options); } - getCroppedCanvasDataURL(options) { - return this.cropperInstance.getCroppedCanvas(options).toDataURL(); + getCroppedCanvasDataURL(options, type, encoderOptions) { + return this.cropperInstance.getCroppedCanvas(options).toDataURL(type, encoderOptions); } getData(rounded) {