diff --git a/.github/linters/.jscpd.json b/.github/linters/.jscpd.json index 8e44c666..cf640ee8 100644 --- a/.github/linters/.jscpd.json +++ b/.github/linters/.jscpd.json @@ -2,8 +2,20 @@ "threshold": 0, "reporters": ["consoleFull"], "ignore": [ + "**/src/Cropper.Blazor/Client/**", + "**/src/Cropper.Blazor/Client.V1/**", + "**/src/Cropper.Blazor/Client.V2/**", + "**/src/Cropper.Blazor/.vs/**", + "**/.idea/**", + "**/src/Cropper.Blazor/Cropper.Blazor.Tests/**", "**/src/Cropper.Blazor/Cropper.Blazor.UnitTests/**", + "**/src/Cropper.Blazor/Cropper.Blazor.Codecov/**", + "**/src/Cropper.Blazor/Cropper.Blazor.SuperLinter/**", + "**/*.coverage.test.ts", "**/examples/**", + "**/src/Cropper.Blazor/**/coverage/**", + "**/src/Cropper.Blazor/cropperjs/**", + "**/wwwroot/**", "**/*.md", "**/*excubowebcompiler.json", "**/bin/**", diff --git a/src/Cropper.Blazor/.github/copilot-instructions.md b/src/Cropper.Blazor/.github/copilot-instructions.md new file mode 100644 index 00000000..892352be --- /dev/null +++ b/src/Cropper.Blazor/.github/copilot-instructions.md @@ -0,0 +1,15 @@ +# Copilot Instructions + +## Project Guidelines +- For Cropper.Blazor Cropper.js v2 migration: use the existing Cropper.Blazor architecture, add unit-related tests, update examples, and keep separate v1 and v2 example URLs (/v1 and /v2). +- For the docs site, use Cropper.Blazor v1 from NuGet with the same v1 docs, create separate pages (including home) for v2, and keep the releases page shared between both versions. Ensure that the documentation versioning is handled within one application using different routes, rather than separate client apps. +- Inspect the Blazorise.Cropper project locally when comparing Cropper viewer behavior to ensure consistency and functionality. +- Split large god-object option classes into understandable, element-specific option classes for Cropper.Blazor configuration. Prefer enum-backed option APIs instead of plain strings for related Cropper.js option values. +- Verify that event correlation IDs are used correctly. +- For selection snapshots in the Cropper demo, use Cropper preview/viewer elements instead of saved canvas data URLs and avoid redundant snapshot save logic. + +## Development Practices +- Run the Super-Linter pipeline configuration locally before completion to ensure code quality and adherence to standards. +- Use conventional Arrange-Act-Assert naming/structure patterns for tests where applicable. +- Place all TypeScript tests in the Vitest project. The default move handle action should be select, allowing for a selection count of zero. +- All TypeScript tests in this repository should be placed in the Vitest project. \ No newline at end of file diff --git a/src/Cropper.Blazor/Blazorise.Cropper/Blazorise.Cropper.csproj b/src/Cropper.Blazor/Blazorise.Cropper/Blazorise.Cropper.csproj new file mode 100644 index 00000000..53a54dab --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/Blazorise.Cropper.csproj @@ -0,0 +1,9 @@ + + + net10.0 + false + false + false + false + + diff --git a/src/Cropper.Blazor/Blazorise.Cropper/Cropper.razor b/src/Cropper.Blazor/Blazorise.Cropper/Cropper.razor new file mode 100644 index 00000000..c7d44d6c --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/Cropper.razor @@ -0,0 +1,3 @@ +@namespace Blazorise.Cropper +@inherits BaseComponent +
\ No newline at end of file diff --git a/src/Cropper.Blazor/Blazorise.Cropper/Cropper.razor.cs b/src/Cropper.Blazor/Blazorise.Cropper/Cropper.razor.cs new file mode 100644 index 00000000..635d0e3e --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/Cropper.razor.cs @@ -0,0 +1,370 @@ +#region Using directives +using System; +using System.Threading.Tasks; +using Blazorise.Extensions; +using Blazorise.Utilities; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +#endregion + +namespace Blazorise.Cropper; + +/// +/// Blazorise image cropper component based on CropperJS. +/// +public partial class Cropper : BaseComponent, IAsyncDisposable +{ + #region Members + + private DotNetObjectReference adapter; + + #endregion + + #region Methods + + /// + public override async Task SetParametersAsync( ParameterView parameters ) + { + if ( Rendered ) + { + var sourceChanged = parameters.TryGetValue( nameof( Source ), out var paramSource ) && paramSource != Source; + var altChanged = parameters.TryGetValue( nameof( Alt ), out var paramAlt ) && paramAlt != Alt; + var crossoriginChanged = parameters.TryGetValue( nameof( CrossOrigin ), out var paramCrossOrigin ) && paramCrossOrigin != CrossOrigin; + var imageOptionsChanged = parameters.TryGetValue( nameof( ImageOptions ), out var paramImageOptions ) && paramImageOptions != ImageOptions; + var selectionOptionsChanged = parameters.TryGetValue( nameof( SelectionOptions ), out var paramSelectionOptions ) && paramSelectionOptions != SelectionOptions; + var gridOptionsChanged = parameters.TryGetValue( nameof( GridOptions ), out var paramGridOptions ) && paramGridOptions != GridOptions; + var enabledChanged = parameters.TryGetValue( nameof( Enabled ), out var paramEnabled ) && paramEnabled != Enabled; + if ( sourceChanged + || altChanged + || crossoriginChanged + || imageOptionsChanged + || selectionOptionsChanged + || gridOptionsChanged + || enabledChanged ) + { + ExecuteAfterRender( async () => await JSModule.UpdateOptions( ElementRef, ElementId, new() + { + Source = new( sourceChanged, paramSource ), + Alt = new( altChanged, paramAlt ), + CrossOrigin = new( crossoriginChanged, paramCrossOrigin ), + Image = new( imageOptionsChanged, new CropperImageOptions + { + Rotatable = paramImageOptions?.Rotatable ?? true, + Scalable = paramImageOptions?.Scalable ?? true, + Skewable = paramImageOptions?.Skewable ?? true, + Translatable = paramImageOptions?.Translatable ?? true, + } ), + Selection = new( selectionOptionsChanged, new CropperSelectionJSOptions + { + AspectRatio = paramSelectionOptions?.AspectRatio.Value, + InitialAspectRatio = paramSelectionOptions?.InitialAspectRatio.Value, + InitialCoverage = paramSelectionOptions?.InitialCoverage, + Movable = paramSelectionOptions?.Movable ?? false, + Resizable = paramSelectionOptions?.Resizable ?? false, + Zoomable = paramSelectionOptions?.Zoomable ?? false, + Keyboard = paramSelectionOptions?.Keyboard ?? false, + Outlined = paramSelectionOptions?.Outlined ?? false + } ), + Grid = new( gridOptionsChanged, new CropperGridOptions + { + Rows = paramGridOptions?.Rows ?? 3, + Columns = paramGridOptions?.Columns ?? 3, + Bordered = paramGridOptions?.Bordered ?? false, + Covered = paramGridOptions?.Covered ?? false, + } ), + Enabled = new( enabledChanged, paramEnabled ) + } ) ); + } + } + + await base.SetParametersAsync( parameters ); + } + + /// + protected override async Task OnAfterRenderAsync( bool firstRender ) + { + await base.OnAfterRenderAsync( firstRender ); + + if ( firstRender ) + { + JSModule ??= new JSCropperModule( JSRuntime, VersionProvider, BlazoriseOptions ); + adapter ??= DotNetObjectReference.Create( new CropperAdapter( this ) ); + + await JSModule.Initialize( adapter, ElementRef, ElementId, new() + { + Source = Source, + Alt = Alt, + Enabled = Enabled, + ShowBackground = ShowBackground, + Image = new() + { + Rotatable = ImageOptions?.Rotatable ?? true, + Scalable = ImageOptions?.Scalable ?? true, + Skewable = ImageOptions?.Skewable ?? true, + Translatable = ImageOptions?.Translatable ?? true, + }, + Selection = new() + { + AspectRatio = SelectionOptions?.AspectRatio.Value, + InitialAspectRatio = SelectionOptions?.InitialAspectRatio.Value, + InitialCoverage = SelectionOptions?.InitialCoverage, + Movable = SelectionOptions?.Movable ?? false, + Resizable = SelectionOptions?.Resizable ?? false, + Zoomable = SelectionOptions?.Zoomable ?? false, + Keyboard = SelectionOptions?.Keyboard ?? false, + Outlined = SelectionOptions?.Outlined ?? false + }, + Grid = new() + { + Rows = GridOptions?.Rows ?? 3, + Columns = GridOptions?.Columns ?? 3, + Bordered = GridOptions?.Bordered ?? false, + Covered = GridOptions?.Covered ?? false, + } + } ); + + if ( CropperState is not null ) + { + await CropperState.CropperInitialized.InvokeCallbackAsync( this ); + } + } + } + + /// + protected override void BuildClasses( ClassBuilder builder ) + { + builder.Append( "b-cropper-container" ); + + base.BuildClasses( builder ); + } + + /// + protected override async ValueTask DisposeAsync( bool disposing ) + { + if ( disposing && Rendered ) + { + await JSModule.SafeDestroy( ElementRef, ElementId ); + + await JSModule.SafeDisposeAsync(); + + if ( adapter != null ) + { + adapter.Dispose(); + adapter = null; + } + } + + await base.DisposeAsync( disposing ); + } + + /// + /// Get the cropped image as Base64 image. + /// + /// the cropping options. + /// the cropped image. + public ValueTask CropAsBase64ImageAsync( CropperCropOptions options ) + => JSModule.CropBase64( ElementRef, ElementId, options ); + + /// + /// Moves the image. + /// + /// The moving distance in the horizontal direction. + /// The moving distance in the vertical direction. + public ValueTask Move( int x, int y ) + => JSModule.Move( ElementRef, ElementId, x, y ); + + /// + /// Moves the image to a specific position. + /// + /// The new position in the horizontal direction. + /// The new position in the vertical direction. + public ValueTask MoveTo( int x, int y ) + => JSModule.MoveTo( ElementRef, ElementId, x, y ); + + /// + /// Zooms the image. + /// + /// The zoom factor. Positive numbers for zooming in, and negative numbers for zooming out. + public ValueTask Zoom( double scale ) + => JSModule.Zoom( ElementRef, ElementId, scale ); + + /// + /// Rotates the image. + /// + /// The rotation angle. + public ValueTask Rotate( double angle ) + => JSModule.Rotate( ElementRef, ElementId, angle ); + + /// + /// Scale the image. + /// + /// The scaling factor in the horizontal direction. + /// The scaling factor in the vertical direction. + /// + public ValueTask Scale( int x, int y ) + => JSModule.Scale( ElementRef, ElementId, x, y ); + + /// + /// Center the image. + /// + /// The size factor: null, contain or cover. + /// + public ValueTask Center( string size ) + => JSModule.Center( ElementRef, ElementId, size ); + + /// + /// Resets the selection to its initial position and size. + /// + /// + public ValueTask ResetSelection() + => JSModule.ResetSelection( ElementRef, ElementId ); + + internal async Task NotifyCropStart() + { + if ( CropStarted is not null ) + await CropStarted.Invoke(); + } + + internal async Task NotifyCropMove() + { + if ( CropMoved is not null ) + await CropMoved.Invoke(); + } + + internal async Task NotifyCropEnd() + { + if ( CropEnded is not null ) + await CropEnded.Invoke(); + } + + internal async Task NotifyCrop( double startX, double startY, double endX, double endY ) + { + if ( Cropped is not null ) + await Cropped.Invoke( new CropperCroppedEventArgs( startX, startY, endX, endY ) ); + } + + internal async Task NotifyZoom( double scale ) + { + if ( Zoomed is not null ) + await Zoomed.Invoke( new CropperZoomedEventArgs( scale ) ); + } + + internal async Task NotifySelectionChanged( double x, double y, double width, double height ) + { + if ( SelectionChanged is not null ) + await SelectionChanged.Invoke( new CropperSelectionChangedEventArgs( x, y, width, height ) ); + } + + internal async Task NotifyImageReady() + { + if ( ImageReady is not null ) + await ImageReady.Invoke(); + } + + internal async Task NotifyImageLoadingFailed( string errorMessage ) + { + if ( ImageLoadingFailed is not null ) + await ImageLoadingFailed.Invoke( errorMessage ); + } + + #endregion + + #region Properties + + internal JSCropperModule JSModule { get; set; } + + [Inject] private IJSRuntime JSRuntime { get; set; } + + [Inject] private IVersionProvider VersionProvider { get; set; } + [Inject] private BlazoriseOptions BlazoriseOptions { get; set; } + + /// + protected override bool ShouldAutoGenerateId => true; + + /// + /// The source of the image. + /// + [Parameter, EditorRequired] public string Source { get; set; } + + /// + /// The alt text of the image. + /// + [Parameter] public string Alt { get; set; } + + /// + /// The cross-origin attribute of the image. + /// + [Parameter] public string CrossOrigin { get; set; } + + /// + /// This event fires when the canvas (image wrapper) or the crop box starts to change. + /// + [Parameter] public Func CropStarted { get; set; } + + /// + /// This event fires when the canvas (image wrapper) or the crop box is changing. + /// + [Parameter] public Func CropMoved { get; set; } + + /// + /// This event fires when the canvas (image wrapper) or the crop box stops changing. + /// + [Parameter] public Func CropEnded { get; set; } + + /// + /// This event fires when the canvas (image wrapper) or the crop box changes. + /// + [Parameter] public Func Cropped { get; set; } + + /// + /// This event fires when a cropper instance starts to zoom in or zoom out its canvas (image wrapper). + /// + [Parameter] public Func Zoomed { get; set; } + + /// + /// The event is fired when the position or size of the selection is going to change. + /// + [Parameter] public Func SelectionChanged { get; set; } + + /// + /// This event fires when the image is ready / loaded. + /// + [Parameter] public Func ImageReady { get; set; } + + /// + /// This event fires when the image cannot be loaded. Usually because of 404 or being null. Returns an error message as a parameter. + /// + [Parameter] public Func ImageLoadingFailed { get; set; } + + /// + /// Indicates whether this element is disabled. + /// + [Parameter] public bool Enabled { get; set; } = true; + + /// + /// Indicates whether this element has a grid background. + /// + [Parameter] public bool ShowBackground { get; set; } = true; + + /// + /// Provides properties for manipulating the layout and presentation of image elements. + /// + [Parameter] public CropperImageOptions ImageOptions { get; set; } = new CropperImageOptions(); + + /// + /// Provides properties for manipulating the layout and presentation. + /// + [Parameter] public CropperSelectionOptions SelectionOptions { get; set; } = new CropperSelectionOptions(); + + /// + /// Provides properties for manipulating the layout and presentation of selection grid elements. + /// + [Parameter] public CropperGridOptions GridOptions { get; set; } = new CropperGridOptions(); + + /// + /// Provides a shared state and syncronization context between the cropper and cropper viewer. + /// + [Parameter] public CropperState CropperState { get; set; } + + #endregion +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Blazorise.Cropper/CropperAdapter.cs b/src/Cropper.Blazor/Blazorise.Cropper/CropperAdapter.cs new file mode 100644 index 00000000..908d41df --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/CropperAdapter.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using Microsoft.JSInterop; + +namespace Blazorise.Cropper; + +internal class CropperAdapter +{ + private readonly Cropper cropper; + + public CropperAdapter( Cropper cropper ) + { + this.cropper = cropper; + } + + [JSInvokable( "CropStart" )] + public async ValueTask CropStart() => await cropper.NotifyCropStart(); + + [JSInvokable( "CropMove" )] + public async ValueTask CropMove() => await cropper.NotifyCropMove(); + + [JSInvokable( "CropEnd" )] + public async ValueTask CropEnd() => await cropper.NotifyCropEnd(); + + [JSInvokable( "Crop" )] + public async ValueTask Crop( double startX, double startY, double endX, double endY ) => await cropper.NotifyCrop( startX, startY, endX, endY ); + + [JSInvokable( "Zoom" )] + public async ValueTask Zoom( double scale ) => await cropper.NotifyZoom( scale ); + + [JSInvokable( "SelectionChanged" )] + public async ValueTask SelectionChanged( double x, double y, double width, double height ) => await cropper.NotifySelectionChanged( x, y, width, height ); + + [JSInvokable( "ImageReady" )] + public async ValueTask ImageReady() => await cropper.NotifyImageReady(); + + [JSInvokable( "ImageLoadingFailed" )] + public async ValueTask ImageLoadingFailed( string errorMessage ) => await cropper.NotifyImageLoadingFailed( errorMessage ); +} diff --git a/src/Cropper.Blazor/Blazorise.Cropper/CropperAspectRatio.cs b/src/Cropper.Blazor/Blazorise.Cropper/CropperAspectRatio.cs new file mode 100644 index 00000000..10b4ae98 --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/CropperAspectRatio.cs @@ -0,0 +1,42 @@ +namespace Blazorise.Cropper; + +/// +/// Cropper aspect ratio. +/// +public record struct CropperAspectRatio( double? Value ) +{ + /// + /// Create a new aspect ratio based on width and height + /// + /// ratio width + /// ratio height + public CropperAspectRatio( int width, int height ) + : this( (double)width / height ) + { + } + + /// + /// 16:9 aspect ratio + /// + public static readonly CropperAspectRatio Is16x9 = new( 16, 9 ); + + /// + /// 4:3 aspect ratio + /// + public static readonly CropperAspectRatio Is4x3 = new( 4, 3 ); + + /// + /// 1:1 aspect ratio + /// + public static readonly CropperAspectRatio Is1x1 = new( 1 ); + + /// + /// 2:3 aspect ratio + /// + public static readonly CropperAspectRatio Is2x3 = new( 2, 3 ); + + /// + /// Free aspect ratio + /// + public static readonly CropperAspectRatio IsFree = new( null ); +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Blazorise.Cropper/CropperCropOptions.cs b/src/Cropper.Blazor/Blazorise.Cropper/CropperCropOptions.cs new file mode 100644 index 00000000..84ce1b28 --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/CropperCropOptions.cs @@ -0,0 +1,30 @@ +namespace Blazorise.Cropper; + +/// +/// Image Cropper crop options. +/// +public class CropperCropOptions +{ + /// + /// The destination width of the output canvas. + /// + public int Width { get; set; } + + /// + /// The destination height of the output canvas. + /// + public int Height { get; set; } + + /// + /// 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. + /// + public string ImageType { get; set; } = "image/png"; + + /// + /// 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. + /// + public double? ImageQuality { get; set; } = 1d; +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Blazorise.Cropper/CropperJSOptions.cs b/src/Cropper.Blazor/Blazorise.Cropper/CropperJSOptions.cs new file mode 100644 index 00000000..cd868893 --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/CropperJSOptions.cs @@ -0,0 +1,131 @@ +using Blazorise.Modules; + +namespace Blazorise.Cropper; + +/// +/// Represents JavaScript options for initializing an image cropper component. +/// +public class CropperJSOptions +{ + /// + /// Gets or sets the source URL of the image to be cropped. + /// + public string Source { get; set; } + + /// + /// Gets or sets the alternate text for the image. + /// + public string Alt { get; set; } + + /// + /// Gets or sets a value indicating whether the cropper is enabled. + /// + public bool Enabled { get; set; } + + /// + /// Gets or sets a value indicating whether to show a background around the cropping area. + /// + public bool ShowBackground { get; set; } + + /// + /// Gets or sets options related to the image settings for the cropper. + /// + public CropperImageOptions Image { get; set; } + + /// + /// Gets or sets options for the cropping selection area. + /// + public CropperSelectionJSOptions Selection { get; set; } + + /// + /// Gets or sets options for the grid overlay in the cropper. + /// + public CropperGridOptions Grid { get; set; } +} + +/// +/// Represents JavaScript options for updating specific settings of an image cropper component dynamically. +/// +public class CropperUpdateJSOptions +{ + /// + /// Gets or sets the option for updating the source URL of the image. + /// + public JSOptionChange Source { get; set; } + + /// + /// Gets or sets the option for updating the alternate text for the image. + /// + public JSOptionChange Alt { get; set; } + + /// + /// Gets or sets the option for updating the cross-origin attribute of the image. + /// + public JSOptionChange CrossOrigin { get; set; } + + /// + /// Gets or sets the option for updating the image settings in the cropper. + /// + public JSOptionChange Image { get; set; } + + /// + /// Gets or sets the option for updating the selection area in the cropper. + /// + public JSOptionChange Selection { get; set; } + + /// + /// Gets or sets the option for updating the grid overlay settings in the cropper. + /// + public JSOptionChange Grid { get; set; } + + /// + /// Gets or sets the option for enabling or disabling the cropper. + /// + public JSOptionChange Enabled { get; set; } +} + +/// +/// Represents JavaScript options for configuring the selection area within an image cropper component. +/// +public class CropperSelectionJSOptions +{ + /// + /// Gets or sets the aspect ratio for the selection area. A value of null allows any aspect ratio. + /// + public double? AspectRatio { get; set; } + + /// + /// Gets or sets the initial aspect ratio for the selection area. + /// + public double? InitialAspectRatio { get; set; } + + /// + /// Gets or sets a value indicating whether the selection should initially cover the entire image. + /// + public double? InitialCoverage { get; set; } + + /// + /// Gets or sets a value indicating whether the selection area is movable within the cropper. + /// + public bool Movable { get; set; } + + /// + /// Gets or sets a value indicating whether the selection area is resizable. + /// + public bool Resizable { get; set; } + + /// + /// Gets or sets a value indicating whether zooming is allowed on the selection area. + /// + public bool Zoomable { get; set; } + + /// + /// Gets or sets a value indicating whether keyboard navigation is enabled within the cropper. + /// + public bool Keyboard { get; set; } + + /// + /// Gets or sets a value indicating whether the selection area has an outline displayed. + /// + public bool Outlined { get; set; } +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Blazorise.Cropper/CropperState.cs b/src/Cropper.Blazor/Blazorise.Cropper/CropperState.cs new file mode 100644 index 00000000..2e53ca8a --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/CropperState.cs @@ -0,0 +1,11 @@ +using Blazorise.Infrastructure; + +namespace Blazorise.Cropper; + +/// +/// Provides a shared state and syncronization context between the cropper and cropper viewer. +/// +public class CropperState +{ + internal EventCallbackSubscribable CropperInitialized { get; } = new(); +} diff --git a/src/Cropper.Blazor/Blazorise.Cropper/CropperViewer.razor b/src/Cropper.Blazor/Blazorise.Cropper/CropperViewer.razor new file mode 100644 index 00000000..c7d44d6c --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/CropperViewer.razor @@ -0,0 +1,3 @@ +@namespace Blazorise.Cropper +@inherits BaseComponent +
\ No newline at end of file diff --git a/src/Cropper.Blazor/Blazorise.Cropper/CropperViewer.razor.cs b/src/Cropper.Blazor/Blazorise.Cropper/CropperViewer.razor.cs new file mode 100644 index 00000000..d469b1ea --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/CropperViewer.razor.cs @@ -0,0 +1,75 @@ +#region Using directives +using System; +using System.Threading.Tasks; +using Blazorise.Infrastructure; +using Microsoft.AspNetCore.Components; +#endregion + +namespace Blazorise.Cropper +{ + /// + /// The CropperViewer interface provides properties and methods for manipulating the layout and presentation of cropper elements. + /// + public partial class CropperViewer : BaseComponent, IDisposable + { + #region Members + + private readonly EventCallbackSubscriber cropperInitialized; + + #endregion + + #region Constructors + + /// + /// A default constructor. + /// + public CropperViewer() + { + cropperInitialized = new EventCallbackSubscriber( EventCallback.Factory.Create( this, OnCropperInitialized ) ); + } + + #endregion + + #region Methods + + /// + protected override Task OnParametersSetAsync() + { + cropperInitialized.SubscribeOrReplace( CropperState?.CropperInitialized ); + + return base.OnParametersSetAsync(); + } + + /// + protected override void Dispose( bool disposing ) + { + if ( disposing ) + { + cropperInitialized.Dispose(); + } + + base.Dispose( disposing ); + } + + private async Task OnCropperInitialized( Cropper cropper ) + { + if ( cropper?.JSModule != null ) + { + await cropper.JSModule.InitializeViewer( cropper.ElementRef, cropper.ElementId, ElementRef, ElementId, new + { + } ); + } + } + + #endregion + + #region Properties + + /// + /// Provides a shared state and syncronization context between the cropper and cropper viewer. + /// + [Parameter] public CropperState CropperState { get; set; } + + #endregion + } +} diff --git a/src/Cropper.Blazor/Blazorise.Cropper/EventArgs/CropperCroppedEventArgs.cs b/src/Cropper.Blazor/Blazorise.Cropper/EventArgs/CropperCroppedEventArgs.cs new file mode 100644 index 00000000..e9718c22 --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/EventArgs/CropperCroppedEventArgs.cs @@ -0,0 +1,42 @@ +namespace Blazorise.Cropper; + +/// +/// Provides all the information for the cropper crop event. +/// +public class CropperCroppedEventArgs +{ + /// + /// A default constructor. + /// + /// The starting pageX value. + /// The starting pageY value. + /// The ending pageX value. + /// The ending pageY value. + public CropperCroppedEventArgs( double startX, double startY, double endX, double endY ) + { + StartX = startX; + StartY = startY; + EndX = endX; + EndY = endY; + } + + /// + /// Gets the starting pageX value. + /// + public double StartX { get; } + + /// + /// Gets the starting pageY value. + /// + public double StartY { get; } + + /// + /// Gets the ending pageX value. + /// + public double EndX { get; } + + /// + /// Gets the ending pageY value. + /// + public double EndY { get; } +} diff --git a/src/Cropper.Blazor/Blazorise.Cropper/EventArgs/CropperSelectionChangedEventArgs.cs b/src/Cropper.Blazor/Blazorise.Cropper/EventArgs/CropperSelectionChangedEventArgs.cs new file mode 100644 index 00000000..5a915229 --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/EventArgs/CropperSelectionChangedEventArgs.cs @@ -0,0 +1,42 @@ +namespace Blazorise.Cropper; + +/// +/// Defines the position and size data of the selection. +/// +public class CropperSelectionChangedEventArgs +{ + /// + /// A default constructor. + /// + /// The x-axis coordinate of the selection. + /// The y-axis coordinate of the selection. + /// The width of the selection. + /// The height of the selection. + public CropperSelectionChangedEventArgs( double x, double y, double width, double height ) + { + X = x; + Y = y; + Width = width; + Height = height; + } + + /// + /// Gets the x-axis coordinate of the selection. + /// + public double X { get; } + + /// + /// Gets the y-axis coordinate of the selection. + /// + public double Y { get; } + + /// + /// Gets the width of the selection. + /// + public double Width { get; } + + /// + /// Gets the height of the selection. + /// + public double Height { get; } +} diff --git a/src/Cropper.Blazor/Blazorise.Cropper/EventArgs/CropperZoomedEventArgs.cs b/src/Cropper.Blazor/Blazorise.Cropper/EventArgs/CropperZoomedEventArgs.cs new file mode 100644 index 00000000..2c92af96 --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/EventArgs/CropperZoomedEventArgs.cs @@ -0,0 +1,21 @@ +namespace Blazorise.Cropper; + +/// +/// Provides all the information for the cropper zoom event. +/// +public class CropperZoomedEventArgs +{ + /// + /// A default constructor. + /// + /// The scaling factor. + public CropperZoomedEventArgs( double scale ) + { + Scale = scale; + } + + /// + /// Gets the scaling factor. + /// + public double Scale { get; } +} diff --git a/src/Cropper.Blazor/Blazorise.Cropper/JSCropperModule.cs b/src/Cropper.Blazor/Blazorise.Cropper/JSCropperModule.cs new file mode 100644 index 00000000..74d3bf26 --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/JSCropperModule.cs @@ -0,0 +1,65 @@ +#region Using directives +using System.Threading.Tasks; +using Blazorise.Modules; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +#endregion + +namespace Blazorise.Cropper; + +internal class JSCropperModule : BaseJSModule, IJSDestroyableModule +{ + public JSCropperModule( IJSRuntime jsRuntime, IVersionProvider versionProvider, BlazoriseOptions options ) + : base( jsRuntime, versionProvider, options ) + { + } + + public ValueTask Initialize( DotNetObjectReference adapterReference, ElementReference elementRef, string elementId, CropperJSOptions options ) + => InvokeSafeVoidAsync( "initialize", adapterReference, elementRef, elementId, options ); + + public ValueTask Destroy( ElementReference elementRef, string elementId ) + => InvokeSafeVoidAsync( "destroy", elementRef, elementId ); + + public ValueTask UpdateOptions( ElementReference elementRef, string elementId, CropperUpdateJSOptions options ) + => InvokeSafeVoidAsync( "updateOptions", elementRef, elementId, options ); + + public ValueTask InitializeViewer( ElementReference cropperElementRef, string cropperElementId, ElementReference elementRef, string elementId, object options ) + => InvokeSafeVoidAsync( "initializeViewer", cropperElementRef, cropperElementId, elementRef, elementId, options ); + + public ValueTask CropBase64( ElementReference elementRef, string elementId, CropperCropOptions options ) + { + var cropOptions = new + { + width = options.Width, + height = options.Height, + imageType = options.ImageType, + imageQuality = options.ImageQuality, + }; + + return InvokeSafeAsync( "cropBase64", elementRef, elementId, cropOptions ); + } + + public ValueTask Move( ElementReference elementRef, string elementId, int x, int y ) + => InvokeSafeVoidAsync( "move", elementRef, elementId, x, y ); + + public ValueTask MoveTo( ElementReference elementRef, string elementId, int x, int y ) + => InvokeSafeVoidAsync( "moveTo", elementRef, elementId, x, y ); + + public ValueTask Zoom( ElementReference elementRef, string elementId, double scale ) + => InvokeSafeVoidAsync( "zoom", elementRef, elementId, scale ); + + public ValueTask Rotate( ElementReference elementRef, string elementId, double angle ) + => InvokeSafeVoidAsync( "rotate", elementRef, elementId, angle ); + + public ValueTask Scale( ElementReference elementRef, string elementId, int x, int y ) + => InvokeSafeVoidAsync( "scale", elementRef, elementId, x, y ); + + public ValueTask Center( ElementReference elementRef, string elementId, string size ) + => InvokeSafeVoidAsync( "center", elementRef, elementId, size ); + + public ValueTask ResetSelection( ElementReference elementRef, string elementId ) + => InvokeSafeVoidAsync( "resetSelection", elementRef, elementId ); + + /// + public override string ModuleFileName => $"./_content/Blazorise.Cropper/blazorise.cropper.js?v={VersionProvider.Version}"; +} diff --git a/src/Cropper.Blazor/Blazorise.Cropper/Options/CropperGridOptions.cs b/src/Cropper.Blazor/Blazorise.Cropper/Options/CropperGridOptions.cs new file mode 100644 index 00000000..48c9a446 --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/Options/CropperGridOptions.cs @@ -0,0 +1,27 @@ +namespace Blazorise.Cropper; + +/// +/// Provides properties for manipulating the layout and presentation of selection grid elements. +/// +public record CropperGridOptions +{ + /// + /// Indicates the number of the rows. + /// + public int Rows { get; init; } = 3; + + /// + /// Indicates the number of the columns. + /// + public int Columns { get; init; } = 3; + + /// + /// Indicates whether this element is bordered. + /// + public bool Bordered { get; init; } = true; + + /// + /// Indicates whether this element covers its parent element. + /// + public bool Covered { get; init; } = true; +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Blazorise.Cropper/Options/CropperImageOptions.cs b/src/Cropper.Blazor/Blazorise.Cropper/Options/CropperImageOptions.cs new file mode 100644 index 00000000..5dea72e6 --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/Options/CropperImageOptions.cs @@ -0,0 +1,27 @@ +namespace Blazorise.Cropper; + +/// +/// Provides properties for manipulating the layout and presentation of image elements. +/// +public record CropperImageOptions +{ + /// + /// Indicates whether this element is rotatable. + /// + public bool Rotatable { get; init; } = true; + + /// + /// Indicates whether this element is scalable. + /// + public bool Scalable { get; init; } = true; + + /// + /// Indicates whether this element is skewable. + /// + public bool Skewable { get; init; } + + /// + /// Indicates whether this element is translatable. + /// + public bool Translatable { get; init; } = true; +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Blazorise.Cropper/Options/CropperSelectionOptions.cs b/src/Cropper.Blazor/Blazorise.Cropper/Options/CropperSelectionOptions.cs new file mode 100644 index 00000000..71ce51a1 --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/Options/CropperSelectionOptions.cs @@ -0,0 +1,47 @@ +namespace Blazorise.Cropper; + +/// +/// The Cropper selection interface provides properties for manipulating the layout and presentation. +/// +public record CropperSelectionOptions +{ + /// + /// Indicates the aspect ratio of the selection, must a positive number. + /// + public CropperAspectRatio AspectRatio { get; init; } = CropperAspectRatio.Is1x1; + + /// + /// Indicates the initial aspect ratio of the selection, must a positive number. + /// + public CropperAspectRatio InitialAspectRatio { get; init; } = CropperAspectRatio.Is1x1; + + /// + /// Indicates the initial coverage of the selection, must a positive number between 0 (0%) and 1 (100%). + /// + public double? InitialCoverage { get; init; } + + /// + /// Indicates whether the selection is movable. + /// + public bool Movable { get; init; } = true; + + /// + /// Indicates whether the selection is resizable. + /// + public bool Resizable { get; init; } = true; + + /// + /// Indicates whether the selection is zoomable. + /// + public bool Zoomable { get; init; } = true; + + /// + /// Indicates whether keyboard control is supported. + /// + public bool Keyboard { get; init; } = true; + + /// + /// Indicates whether show the outlined or not. + /// + public bool Outlined { get; init; } = true; +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Blazorise.Cropper/_Imports.razor b/src/Cropper.Blazor/Blazorise.Cropper/_Imports.razor new file mode 100644 index 00000000..c3615efc --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/_Imports.razor @@ -0,0 +1 @@ +@using Microsoft.AspNetCore.Components.Web \ No newline at end of file diff --git a/src/Cropper.Blazor/Blazorise.Cropper/wwwroot/blazorise.cropper.css b/src/Cropper.Blazor/Blazorise.Cropper/wwwroot/blazorise.cropper.css new file mode 100644 index 00000000..271ad182 --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/wwwroot/blazorise.cropper.css @@ -0,0 +1,13 @@ +.b-cropper-container { + width: 100%; + height: 100%; + min-height: 100px; +} + + .b-cropper-container cropper-canvas { + display: block; + width: 100%; + min-width: 100%; + height: 100%; + min-height: 100px; + } diff --git a/src/Cropper.Blazor/Blazorise.Cropper/wwwroot/blazorise.cropper.js b/src/Cropper.Blazor/Blazorise.Cropper/wwwroot/blazorise.cropper.js new file mode 100644 index 00000000..5b58b467 --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/wwwroot/blazorise.cropper.js @@ -0,0 +1,420 @@ +import Cropper, { CropperViewer } from "./vendors/cropper.js?v=2.1.3.0"; + +import { getRequiredElement, registerDisconnectCleanup, unregisterDisconnectCleanup } from "../Blazorise/utilities.js?v=2.1.3.0"; + +document.getElementsByTagName("head")[0].insertAdjacentHTML("beforeend", ""); + +const _instances = []; + +export function initialize(dotNetAdapter, element, elementId, options) { + element = getRequiredElement(element, elementId); + + if (!element) + return; + + const instance = { + options: options, + adapter: dotNetAdapter, + cropper: null, + destroyed: false, + disconnectCleanupId: null, + }; + + const image = new Image(); + + image.src = options.source; + image.alt = options.alt; + image.crossOrigin = options.crossOrigin; + + const template = ( + ` + + + + + + + + + + + + + + + + + ` + ); + + const cropper = new Cropper(image, { + container: element, + template: template + }); + + instance.cropper = cropper; + + const cropperCanvas = cropper.getCropperCanvas(); + const cropperSelection = cropper.getCropperSelection(); + const cropperImage = cropper.getCropperImage(); + + manageCropperImageReady(cropperImage, cropperCanvas, instance); + + registerEvents(cropperCanvas, cropperSelection); + + instance.disconnectCleanupId = registerDisconnectCleanup(element, () => destroy(null, elementId, false)); + _instances[elementId] = instance; +} + +export function initializeViewer(cropperElementRef, cropperElementId, element, elementId, options) { + const instance = _instances[cropperElementId]; + + if (!instance) + return; + + element = getRequiredElement(element, elementId); + + if (!element) + return; + + const cropperViewer = new CropperViewer(); + + cropperViewer.selection = `#cropper-selection-${cropperElementId}`; + + element.appendChild(cropperViewer); +} + +export function updateOptions(element, elementId, options) { + const instance = _instances[elementId]; + + if (!instance) + return; + + if (instance.cropper) { + const cropper = instance.cropper; + const cropperCanvas = cropper.getCropperCanvas(); + const cropperImage = cropper.getCropperImage(); + const cropperSelection = cropper.getCropperSelection(); + + if (cropperCanvas) { + if (options.enabled.changed) { + cropperCanvas.disabled = !options.enabled.value; + } + } + + if (cropperImage) { + if (options.source.changed) { + cropperImage.src = options.source.value; + + // Callback needs to be setup again after each source changed. + manageCropperImageReady(cropperImage, cropperCanvas, instance); + } + + if (options.alt.changed) { + cropperImage.alt = options.alt.value; + } + + if (options.crossOrigin.changed) { + cropperImage.crossOrigin = options.crossOrigin.value; + } + + if (options.image.changed) { + const image = options.image.value; + + cropperImage.rotatable = image.rotatable || true; + cropperImage.scalable = image.scalable || true; + cropperImage.skewable = image.skewable || true; + cropperImage.translatable = image.translatable || true; + } + } + + if (cropperSelection && options.selection.changed) { + const selection = options.selection.value; + + cropperSelection.aspectRatio = selection.aspectRatio || NaN; + cropperSelection.initialAspectRatio = selection.initialAspectRatio || NaN; + cropperSelection.initialCoverage = selection.initialCoverage || NaN; + cropperSelection.movable = selection.movable || false; + cropperSelection.resizable = selection.resizable || false; + cropperSelection.zoomable = selection.zoomable || false; + cropperSelection.keyboard = selection.keyboard || false; + cropperSelection.outlined = selection.outlined || false; + + cropperSelection.$move(1); + cropperSelection.$move(-1); + } + } +} + +export function destroy(element, elementId, unregisterCleanup = true) { + const instances = _instances || {}; + + let instance = instances[elementId]; + + if (instance) { + instance.destroyed = true; + + if (unregisterCleanup) { + unregisterDisconnectCleanup(instance.disconnectCleanupId); + } + + const cropper = instance.cropper; + const cropperCanvas = cropper?.getCropperCanvas(); + const cropperSelection = cropper?.getCropperSelection(); + + unregisterEvents(cropperCanvas, cropperSelection); + + if (cropper && typeof cropper.destroy === "function") + cropper.destroy(); + + instance.cropper = null; + instance.disconnectCleanupId = null; + } + + delete instances[elementId]; +} + +export async function cropBase64(element, elementId, options) { + const instance = _instances[elementId]; + + if (instance && instance.cropper) { + const cropper = instance.cropper; + const cropperSelection = cropper.getCropperSelection(); + + if (cropperSelection) { + const croppedCanvas = cropperSelection.$toCanvas(options); + + return await croppedCanvas.then((canvas) => { + return canvas.toDataURL(options.imageType, options.imageQuality); + }); + } + } + + return ""; +} + +export function move(element, elementId, x, y) { + const instance = _instances[elementId]; + + if (!instance) + return; + + if (instance.cropper) { + const cropper = instance.cropper; + const cropperImage = cropper.getCropperImage(); + + if (cropperImage) { + cropperImage.$move(x, y); + } + } +} + +export function moveTo(element, elementId, x, y) { + const instance = _instances[elementId]; + + if (!instance) + return; + + if (instance.cropper) { + const cropper = instance.cropper; + const cropperImage = cropper.getCropperImage(); + + if (cropperImage) { + cropperImage.$moveTo(x, y); + } + } +} + +export function zoom(element, elementId, scale) { + const instance = _instances[elementId]; + + if (!instance) + return; + + if (instance.cropper) { + const cropper = instance.cropper; + const cropperImage = cropper.getCropperImage(); + + if (cropperImage) { + cropperImage.$zoom(scale); + } + } +} + +export function rotate(element, elementId, angle) { + const instance = _instances[elementId]; + + if (!instance) + return; + + if (instance.cropper) { + const cropper = instance.cropper; + const cropperImage = cropper.getCropperImage(); + + if (cropperImage) { + cropperImage.$rotate(`${angle}deg`); + } + } +} + +export function scale(element, elementId, x, y) { + const instance = _instances[elementId]; + + if (!instance) + return; + + if (instance.cropper) { + const cropper = instance.cropper; + const cropperImage = cropper.getCropperImage(); + + if (cropperImage) { + cropperImage.$scale(x, y); + } + } +} + +export function center(element, elementId, size) { + const instance = _instances[elementId]; + + if (!instance) + return; + + if (instance.cropper) { + const cropper = instance.cropper; + const cropperImage = cropper.getCropperImage(); + + if (cropperImage) { + if (size) { + cropperImage.$center(size); + } + else { + cropperImage.$center(); + } + } + } +} + +export function resetSelection(element, elementId) { + const instance = _instances[elementId]; + + if (instance && instance.cropper) { + const cropper = instance.cropper; + const cropperSelection = cropper.getCropperSelection(); + + if (cropperSelection) { + cropperSelection.$reset(); + } + } +} + +function manageCropperImageReady(cropperImage, cropperCanvas, instance) { + cropperImage.$ready((image) => { + if (instance.destroyed) { + return; + } + + if (instance.loadFailed) { + cropperCanvas.disabled = instance.disabledBeforeImageLoadFailed; + instance.loadFailed = false; + } + invokeDotNetMethodAsync(instance.adapter, "ImageReady"); + }) + .catch((err) => { + if (instance.destroyed) { + return; + } + + invokeDotNetMethodAsync(instance.adapter, "ImageLoadingFailed", err.message); + instance.disabledBeforeImageLoadFailed = cropperCanvas.disabled; + instance.loadFailed = true; + cropperCanvas.disabled = true; + }); +} + + +function onCropperStartHandler(event) { + let parentElementId = event.srcElement.parentElement.id; + + const instance = _instances[parentElementId]; + + if (instance) { + invokeDotNetMethodAsync(instance.adapter, "CropStart"); + } +} + +function onCropperMoveHandler(event) { + let parentElementId = event.srcElement.parentElement.id; + + const instance = _instances[parentElementId]; + + if (instance) { + invokeDotNetMethodAsync(instance.adapter, "CropMove"); + } +} + +function onCropperEndHandler(event) { + let parentElementId = event.srcElement.parentElement.id; + + const instance = _instances[parentElementId]; + + if (instance) { + invokeDotNetMethodAsync(instance.adapter, "CropMove"); + } +} + +function onCropperActionHandler(event) { + let parentElementId = event.srcElement.parentElement.id; + + const instance = _instances[parentElementId]; + + if (instance) { + if (event.detail.action !== "scale") { + invokeDotNetMethodAsync(instance.adapter, "Crop", event.detail.startX, event.detail.startY, event.detail.endX, event.detail.endY); + } else if (event.detail.action === "scale") { + invokeDotNetMethodAsync(instance.adapter, "Zoom", event.detail.scale); + } + } +} + +function onCropperSelectionHandler(event) { + let parentElementId = event.srcElement.parentElement.parentElement.id; + const instance = _instances[parentElementId]; + + if (instance) { + invokeDotNetMethodAsync(instance.adapter, "SelectionChanged", event.detail.x, event.detail.y, event.detail.width, event.detail.height); + } +} + + +function registerEvents(cropperCanvas, cropperSelection) { + if (cropperCanvas) { + cropperCanvas.addEventListener('actionstart', onCropperStartHandler); + cropperCanvas.addEventListener('actionmove', onCropperMoveHandler); + cropperCanvas.addEventListener('actionend', onCropperEndHandler); + cropperCanvas.addEventListener('action', onCropperActionHandler); + } + + if (cropperSelection) { + cropperSelection.addEventListener('change', onCropperSelectionHandler); + } +} + +function unregisterEvents(cropperCanvas, cropperSelection) { + if (cropperCanvas) { + cropperCanvas.removeEventListener('actionstart', onCropperStartHandler); + cropperCanvas.removeEventListener('actionmove', onCropperMoveHandler); + cropperCanvas.removeEventListener('actionend', onCropperEndHandler); + cropperCanvas.removeEventListener('action', onCropperActionHandler); + } + + if (cropperSelection) { + cropperSelection.removeEventListener('change', onCropperSelectionHandler); + } +} + +function invokeDotNetMethodAsync(dotNetAdapter, methodName, ...args) { + dotNetAdapter.invokeMethodAsync(methodName, ...args) + .catch((reason) => { + console.error(reason); + }); +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Blazorise.Cropper/wwwroot/vendors/cropper.js b/src/Cropper.Blazor/Blazorise.Cropper/wwwroot/vendors/cropper.js new file mode 100644 index 00000000..9e815746 --- /dev/null +++ b/src/Cropper.Blazor/Blazorise.Cropper/wwwroot/vendors/cropper.js @@ -0,0 +1,2 @@ +/*! Cropper.js v2.0.0 | (c) 2015-present Chen Fengyuan | MIT */ +const t="undefined"!=typeof window&&void 0!==window.document,e=t?window:{},i=!!t&&"ontouchstart"in e.document.documentElement,s=!!t&&"PointerEvent"in e,n="cropper",a=`${n}-canvas`,o=`${n}-crosshair`,r=`${n}-grid`,h=`${n}-handle`,c=`${n}-image`,l=`${n}-selection`,d=`${n}-shade`,u=`${n}-viewer`,$="select",p="move",g="scale",m="rotate",b="transform",f="none",v="n-resize",w="e-resize",y="s-resize",C="w-resize",S="ne-resize",k="nw-resize",x="se-resize",A="sw-resize",E="action",T=i?"touchend touchcancel":"mouseup",z=i?"touchmove":"mousemove",M=i?"touchstart":"mousedown",D=s?"pointerdown":M,P=s?"pointermove":z,O=s?"pointerup pointercancel":T,I="error",N="keydown",R="load",X="resize",Y="wheel",W="action",L="actionend",j="actionmove",q="actionstart",H="change",B="transform";function U(t){return"string"==typeof t}const K=Number.isNaN||e.isNaN;function F(t){return"number"==typeof t&&!K(t)}function Z(t){return F(t)&&t>0&&t<1/0}function G(t){return void 0===t}function J(t){return"object"==typeof t&&null!==t}const{hasOwnProperty:Q}=Object.prototype;function V(t){if(!J(t))return!1;try{const{constructor:e}=t,{prototype:i}=e;return e&&i&&Q.call(i,"isPrototypeOf")}catch(t){return!1}}function _(t){return"function"==typeof t}function tt(t){return"object"==typeof t&&null!==t&&1===t.nodeType}const et=/([a-z\d])([A-Z])/g;function it(t){return String(t).replace(et,"$1-$2").toLowerCase()}const st=/-[A-z\d]/g;function nt(t){return t.replace(st,(t=>t.slice(1).toUpperCase()))}const at=/\s\s*/;function ot(t,e,i,s){e.trim().split(at).forEach((e=>{t.removeEventListener(e,i,s)}))}function rt(t,e,i,s){e.trim().split(at).forEach((e=>{t.addEventListener(e,i,s)}))}function ht(t,e,i,s){rt(t,e,i,Object.assign(Object.assign({},s),{once:!0}))}const ct={bubbles:!0,cancelable:!0,composed:!0};function lt(t,e,i,s){return t.dispatchEvent(new CustomEvent(e,Object.assign(Object.assign(Object.assign({},ct),{detail:i}),s)))}const dt=Promise.resolve();function ut(t,e){return e?dt.then(t?e.bind(t):e):dt}function $t(t){const{documentElement:i}=t.ownerDocument,s=t.getBoundingClientRect();return{left:s.left+(e.pageXOffset-i.clientLeft),top:s.top+(e.pageYOffset-i.clientTop)}}const pt=/deg|g?rad|turn$/i;function gt(t){const e=parseFloat(t)||0;if(0!==e){const[i="rad"]=String(t).match(pt)||[];switch(i.toLowerCase()){case"deg":return e/360*(2*Math.PI);case"grad":return e/400*(2*Math.PI);case"turn":return e*(2*Math.PI)}}return e}const mt="contain";function bt(t,e=mt){const{aspectRatio:i}=t;let{width:s,height:n}=t;const a=Z(s),o=Z(n);if(a&&o){const t=n*i;e===mt&&t>s||"cover"===e&&t{const e=nt(t);let i=this[e];G(i)||this.$propertyChangedCallback(e,void 0,i),Object.defineProperty(this,e,{enumerable:!0,configurable:!0,get:()=>i,set(t){const s=i;i=t,this.$propertyChangedCallback(e,s,t)}})}));const t=this.attachShadow({mode:this.shadowRootMode||wt});if(this.shadowRoot||yt.set(this,t),Ct.set(this,this.$addStyles(this.$sharedStyle)),this.$style&&this.$addStyles(this.$style),this.$template){const e=document.createElement("template");e.innerHTML=this.$template,t.appendChild(e.content)}if(this.slottable){const e=document.createElement("slot");t.appendChild(e)}}disconnectedCallback(){Ct.has(this)&&Ct.delete(this),yt.has(this)&&yt.delete(this)}$getTagNameOf(t){var e;return null!==(e=St.get(t))&&void 0!==e?e:t}$setStyles(t){return Object.keys(t).forEach((e=>{let i=t[e];F(i)&&(i=0!==i&&vt.test(e)?`${i}px`:String(i)),this.style[e]=i})),this}$getShadowRoot(){return this.shadowRoot||yt.get(this)}$addStyles(t){let e;const i=this.$getShadowRoot();return kt?(e=new CSSStyleSheet,e.replaceSync(t),i.adoptedStyleSheets=i.adoptedStyleSheets.concat(e)):(e=document.createElement("style"),e.textContent=t,i.appendChild(e)),e}$emit(t,e,i){return lt(this,t,e,i)}$nextTick(t){return ut(this,t)}static $define(i,s){J(i)&&(s=i,i=""),i||(i=this.$name||this.name),i=it(i),t&&e.customElements&&!e.customElements.get(i)&&customElements.define(i,this,s)}}xt.$version="2.0.0";class At extends xt{constructor(){super(...arguments),this.$onPointerDown=null,this.$onPointerMove=null,this.$onPointerUp=null,this.$onWheel=null,this.$wheeling=!1,this.$pointers=new Map,this.$style=':host{display:block;min-height:100px;min-width:200px;overflow:hidden;position:relative;touch-action:none;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}:host([background]){background-color:#fff;background-image:repeating-linear-gradient(45deg,#ccc 25%,transparent 0,transparent 75%,#ccc 0,#ccc),repeating-linear-gradient(45deg,#ccc 25%,transparent 0,transparent 75%,#ccc 0,#ccc);background-image:repeating-conic-gradient(#ccc 0 25%,#fff 0 50%);background-position:0 0,.5rem .5rem;background-size:1rem 1rem}:host([disabled]){pointer-events:none}:host([disabled]):after{bottom:0;content:"";cursor:not-allowed;display:block;left:0;pointer-events:none;position:absolute;right:0;top:0}',this.$action=f,this.background=!1,this.disabled=!1,this.scaleStep=.1,this.themeColor="#39f"}static get observedAttributes(){return super.observedAttributes.concat(["background","disabled","scale-step"])}connectedCallback(){super.connectedCallback(),this.disabled||this.$bind()}disconnectedCallback(){this.disabled||this.$unbind(),super.disconnectedCallback()}$propertyChangedCallback(t,e,i){if(!Object.is(i,e)&&(super.$propertyChangedCallback(t,e,i),"disabled"===t))i?this.$unbind():this.$bind()}$bind(){this.$onPointerDown||(this.$onPointerDown=this.$handlePointerDown.bind(this),rt(this,D,this.$onPointerDown)),this.$onPointerMove||(this.$onPointerMove=this.$handlePointerMove.bind(this),rt(this.ownerDocument,P,this.$onPointerMove)),this.$onPointerUp||(this.$onPointerUp=this.$handlePointerUp.bind(this),rt(this.ownerDocument,O,this.$onPointerUp)),this.$onWheel||(this.$onWheel=this.$handleWheel.bind(this),rt(this,Y,this.$onWheel,{passive:!1,capture:!0}))}$unbind(){this.$onPointerDown&&(ot(this,D,this.$onPointerDown),this.$onPointerDown=null),this.$onPointerMove&&(ot(this.ownerDocument,P,this.$onPointerMove),this.$onPointerMove=null),this.$onPointerUp&&(ot(this.ownerDocument,O,this.$onPointerUp),this.$onPointerUp=null),this.$onWheel&&(ot(this,Y,this.$onWheel,{capture:!0}),this.$onWheel=null)}$handlePointerDown(t){const{buttons:e,button:i,type:s}=t;if(this.disabled||("pointerdown"===s&&"mouse"===t.pointerType||"mousedown"===s)&&(F(e)&&1!==e||F(i)&&0!==i||t.ctrlKey))return;const{$pointers:n}=this;let a="";if(t.changedTouches)Array.from(t.changedTouches).forEach((({identifier:t,pageX:e,pageY:i})=>{n.set(t,{startX:e,startY:i,endX:e,endY:i})}));else{const{pointerId:e=0,pageX:i,pageY:s}=t;n.set(e,{startX:i,startY:s,endX:i,endY:s})}n.size>1?a=b:tt(t.target)&&(a=t.target.action||t.target.getAttribute(E)||""),!1!==this.$emit(q,{action:a,relatedEvent:t})&&(t.preventDefault(),this.$action=a,this.style.willChange="transform")}$handlePointerMove(t){const{$action:e,$pointers:i}=this;if(this.disabled||e===f||0===i.size)return;if(!1===this.$emit(j,{action:e,relatedEvent:t}))return;if(t.preventDefault(),t.changedTouches)Array.from(t.changedTouches).forEach((({identifier:t,pageX:e,pageY:s})=>{const n=i.get(t);n&&Object.assign(n,{endX:e,endY:s})}));else{const{pointerId:e=0,pageX:s,pageY:n}=t,a=i.get(e);a&&Object.assign(a,{endX:s,endY:n})}const s={action:e,relatedEvent:t};if(e===b){const e=new Map(i);let n=0,a=0,o=0,r=0,h=t.pageX,c=t.pageY;i.forEach(((t,i)=>{e.delete(i),e.forEach((e=>{let i=e.startX-t.startX,s=e.startY-t.startY,l=e.endX-t.endX,d=e.endY-t.endY,u=0,$=0,p=0,g=0;if(0===i?s<0?p=2*Math.PI:s>0&&(p=Math.PI):i>0?p=Math.PI/2+Math.atan(s/i):i<0&&(p=1.5*Math.PI+Math.atan(s/i)),0===l?d<0?g=2*Math.PI:d>0&&(g=Math.PI):l>0?g=Math.PI/2+Math.atan(d/l):l<0&&(g=1.5*Math.PI+Math.atan(d/l)),g>0||p>0){const i=g-p,s=Math.abs(i);s>n&&(n=s,o=i,h=(t.startX+e.startX)/2,c=(t.startY+e.startY)/2)}if(i=Math.abs(i),s=Math.abs(s),l=Math.abs(l),d=Math.abs(d),i>0&&s>0?u=Math.sqrt(i*i+s*s):i>0?u=i:s>0&&(u=s),l>0&&d>0?$=Math.sqrt(l*l+d*d):l>0?$=l:d>0&&($=d),u>0&&$>0){const i=($-u)/u,s=Math.abs(i);s>a&&(a=s,r=i,h=(t.startX+e.startX)/2,c=(t.startY+e.startY)/2)}}))}));const l=n>0,d=a>0;l&&d?(s.rotate=o,s.scale=r,s.centerX=h,s.centerY=c):l?(s.action=m,s.rotate=o,s.centerX=h,s.centerY=c):d?(s.action=g,s.scale=r,s.centerX=h,s.centerY=c):s.action=f}else{const[t]=Array.from(i.values());Object.assign(s,t)}i.forEach((t=>{t.startX=t.endX,t.startY=t.endY})),s.action!==f&&this.$emit(W,s,{cancelable:!1})}$handlePointerUp(t){const{$action:e,$pointers:i}=this;if(!this.disabled&&e!==f&&!1!==this.$emit(L,{action:e,relatedEvent:t})){if(t.preventDefault(),t.changedTouches)Array.from(t.changedTouches).forEach((({identifier:t})=>{i.delete(t)}));else{const{pointerId:e=0}=t;i.delete(e)}0===i.size&&(this.style.willChange="",this.$action=f)}}$handleWheel(t){if(this.disabled)return;if(t.preventDefault(),this.$wheeling)return;this.$wheeling=!0,setTimeout((()=>{this.$wheeling=!1}),50);const e=(t.deltaY>0?-1:1)*this.scaleStep;this.$emit(W,{action:g,scale:e,relatedEvent:t},{cancelable:!1})}$setAction(t){return U(t)&&(this.$action=t),this}$toCanvas(t){return new Promise(((e,i)=>{if(!this.isConnected)return void i(new Error("The current element is not connected to the DOM."));const s=document.createElement("canvas");let n=this.offsetWidth,a=this.offsetHeight,o=1;V(t)&&(Z(t.width)||Z(t.height))&&(({width:n,height:a}=bt({aspectRatio:n/a,width:t.width,height:t.height})),o=n/this.offsetWidth),s.width=n,s.height=a;const r=this.querySelector(this.$getTagNameOf(c));r?r.$ready().then((i=>{const h=s.getContext("2d");if(h){const[e,c,l,d,u,$]=r.$getTransform();let p=u,g=$,m=i.naturalWidth,b=i.naturalHeight;1!==o&&(p*=o,g*=o,m*=o,b*=o);const f=m/2,v=b/2;h.fillStyle="transparent",h.fillRect(0,0,n,a),V(t)&&_(t.beforeDraw)&&t.beforeDraw.call(this,h,s),h.save(),h.translate(f,v),h.transform(e,c,l,d,p,g),h.translate(-f,-v),h.drawImage(i,0,0,m,b),h.restore()}e(s)})).catch(i):e(s)}))}}At.$name=a,At.$version="2.0.0";const Et=new WeakMap,Tt=["alt","crossorigin","decoding","importance","loading","referrerpolicy","sizes","src","srcset"];class zt extends xt{constructor(){super(...arguments),this.$matrix=[1,0,0,1,0,0],this.$onLoad=null,this.$onCanvasAction=null,this.$onCanvasActionEnd=null,this.$onCanvasActionStart=null,this.$actionStartTarget=null,this.$style=":host{display:inline-block}img{display:block;height:100%;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}",this.$image=new Image,this.initialCenterSize="contain",this.rotatable=!1,this.scalable=!1,this.skewable=!1,this.slottable=!1,this.translatable=!1}set $canvas(t){Et.set(this,t)}get $canvas(){return Et.get(this)}static get observedAttributes(){return super.observedAttributes.concat(Tt,["initial-center-size","rotatable","scalable","skewable","translatable"])}attributeChangedCallback(t,e,i){Object.is(i,e)||(super.attributeChangedCallback(t,e,i),Tt.includes(t)&&this.$image.setAttribute(t,i))}$propertyChangedCallback(t,e,i){if(!Object.is(i,e)&&(super.$propertyChangedCallback(t,e,i),"initialCenterSize"===t))this.$nextTick((()=>{this.$center(i)}))}connectedCallback(){super.connectedCallback();const{$image:t}=this,e=this.closest(this.$getTagNameOf(a));e&&(this.$canvas=e,this.$setStyles({display:"block",position:"absolute"}),this.$onCanvasActionStart=t=>{var e,i;this.$actionStartTarget=null===(i=null===(e=t.detail)||void 0===e?void 0:e.relatedEvent)||void 0===i?void 0:i.target},this.$onCanvasActionEnd=()=>{this.$actionStartTarget=null},this.$onCanvasAction=this.$handleAction.bind(this),rt(e,q,this.$onCanvasActionStart),rt(e,L,this.$onCanvasActionEnd),rt(e,W,this.$onCanvasAction)),this.$onLoad=this.$handleLoad.bind(this),rt(t,R,this.$onLoad),this.$getShadowRoot().appendChild(t)}disconnectedCallback(){const{$image:t,$canvas:e}=this;e&&(this.$onCanvasActionStart&&(ot(e,q,this.$onCanvasActionStart),this.$onCanvasActionStart=null),this.$onCanvasActionEnd&&(ot(e,L,this.$onCanvasActionEnd),this.$onCanvasActionEnd=null),this.$onCanvasAction&&(ot(e,W,this.$onCanvasAction),this.$onCanvasAction=null)),t&&this.$onLoad&&(ot(t,R,this.$onLoad),this.$onLoad=null),this.$getShadowRoot().removeChild(t),super.disconnectedCallback()}$handleLoad(){const{$image:t}=this;this.$setStyles({width:t.naturalWidth,height:t.naturalHeight}),this.$canvas&&this.$center(this.initialCenterSize)}$handleAction(t){if(this.hidden||!(this.rotatable||this.scalable||this.translatable))return;const{$canvas:e}=this,{detail:i}=t;if(i){const{relatedEvent:t}=i;let{action:s}=i;switch(s!==b||this.rotatable&&this.scalable||(s=this.rotatable?m:this.scalable?g:f),s){case p:if(this.translatable){let s=null;t&&(s=t.target.closest(this.$getTagNameOf(l))),s||(s=e.querySelector(this.$getTagNameOf(l))),s&&s.multiple&&!s.active&&(s=e.querySelector(`${this.$getTagNameOf(l)}[active]`)),s&&!s.hidden&&s.movable&&!s.dynamic&&this.$actionStartTarget&&s.contains(this.$actionStartTarget)||this.$move(i.endX-i.startX,i.endY-i.startY)}break;case m:if(this.rotatable)if(t){const{x:e,y:s}=this.getBoundingClientRect();this.$rotate(i.rotate,t.clientX-e,t.clientY-s)}else this.$rotate(i.rotate);break;case g:if(this.scalable)if(t){const e=t.target.closest(this.$getTagNameOf(l));if(!e||!e.zoomable||e.zoomable&&e.dynamic){const{x:e,y:s}=this.getBoundingClientRect();this.$zoom(i.scale,t.clientX-e,t.clientY-s)}}else this.$zoom(i.scale);break;case b:if(this.rotatable&&this.scalable){const{rotate:e}=i;let{scale:s}=i;s<0?s=1/(1-s):s+=1;const n=Math.cos(e),a=Math.sin(e),[o,r,h,c]=[n*s,a*s,-a*s,n*s];if(t){const e=this.getBoundingClientRect(),i=t.clientX-e.x,s=t.clientY-e.y,[n,a,l,d]=this.$matrix,u=i-e.width/2,$=s-e.height/2,p=(u*d-l*$)/(n*d-l*a),g=($*n-a*u)/(n*d-l*a);this.$transform(o,r,h,c,p*(1-o)+g*h,g*(1-c)+p*r)}else this.$transform(o,r,h,c,0,0)}}}}$ready(t){const{$image:e}=this,i=new Promise(((t,i)=>{const s=new Error("Failed to load the image source");if(e.complete)e.naturalWidth>0&&e.naturalHeight>0?t(e):i(s);else{const n=()=>{ot(e,I,a),t(e)},a=()=>{ot(e,R,n),i(s)};ht(e,R,n),ht(e,I,a)}}));return _(t)&&i.then((e=>(t(e),e))),i}$center(t){const{parentElement:e}=this;if(!e)return this;const i=e.getBoundingClientRect(),s=i.width,n=i.height,{x:a,y:o,width:r,height:h}=this.getBoundingClientRect(),c=a+r/2,l=o+h/2,d=i.x+s/2,u=i.y+n/2;if(this.$move(d-c,u-l),t&&(r!==s||h!==n)){const e=s/r,i=n/h;switch(t){case"cover":this.$scale(Math.max(e,i));break;case"contain":this.$scale(Math.min(e,i))}}return this}$move(t,e=t){if(this.translatable&&F(t)&&F(e)){const[i,s,n,a]=this.$matrix,o=(t*a-n*e)/(i*a-n*s),r=(e*i-s*t)/(i*a-n*s);this.$translate(o,r)}return this}$moveTo(t,e=t){if(this.translatable&&F(t)&&F(e)){const[i,s,n,a]=this.$matrix,o=(t*a-n*e)/(i*a-n*s),r=(e*i-s*t)/(i*a-n*s);this.$setTransform(i,s,n,a,o,r)}return this}$rotate(t,e,i){if(this.rotatable){const s=gt(t),n=Math.cos(s),a=Math.sin(s),[o,r,h,c]=[n,a,-a,n];if(F(e)&&F(i)){const[t,s,n,a]=this.$matrix,{width:l,height:d}=this.getBoundingClientRect(),u=e-l/2,$=i-d/2,p=(u*a-n*$)/(t*a-n*s),g=($*t-s*u)/(t*a-n*s);this.$transform(o,r,h,c,p*(1-o)-g*h,g*(1-c)-p*r)}else this.$transform(o,r,h,c,0,0)}return this}$zoom(t,e,i){if(!this.scalable||0===t)return this;if(t<0?t=1/(1-t):t+=1,F(e)&&F(i)){const[s,n,a,o]=this.$matrix,{width:r,height:h}=this.getBoundingClientRect(),c=e-r/2,l=i-h/2,d=(c*o-a*l)/(s*o-a*n),u=(l*s-n*c)/(s*o-a*n);this.$transform(t,0,0,t,d*(1-t),u*(1-t))}else this.$scale(t);return this}$scale(t,e=t){return this.scalable&&this.$transform(t,0,0,e,0,0),this}$skew(t,e=0){if(this.skewable){const i=gt(t),s=gt(e);this.$transform(1,Math.tan(s),Math.tan(i),1,0,0)}return this}$translate(t,e=t){return this.translatable&&F(t)&&F(e)&&this.$transform(1,0,0,1,t,e),this}$transform(t,e,i,s,n,a){return F(t)&&F(e)&&F(i)&&F(s)&&F(n)&&F(a)?this.$setTransform(ft(this.$matrix,[t,e,i,s,n,a])):this}$setTransform(t,e,i,s,n,a){if((this.rotatable||this.scalable||this.skewable||this.translatable)&&(Array.isArray(t)&&([t,e,i,s,n,a]=t),F(t)&&F(e)&&F(i)&&F(s)&&F(n)&&F(a))){const o=[...this.$matrix],r=[t,e,i,s,n,a];if(!1===this.$emit(B,{matrix:r,oldMatrix:o}))return this;this.$matrix=r,this.style.transform=`matrix(${r.join(", ")})`}return this}$getTransform(){return this.$matrix.slice()}$resetTransform(){return this.$setTransform([1,0,0,1,0,0])}}zt.$name=c,zt.$version="2.0.0";const Mt=new WeakMap;class Dt extends xt{constructor(){super(...arguments),this.$onCanvasChange=null,this.$onCanvasActionEnd=null,this.$onCanvasActionStart=null,this.$style=":host{display:block;height:0;left:0;outline:var(--theme-color) solid 1px;position:relative;top:0;width:0}:host([transparent]){outline-color:transparent}",this.x=0,this.y=0,this.width=0,this.height=0,this.slottable=!1,this.themeColor="rgba(0, 0, 0, 0.65)"}set $canvas(t){Mt.set(this,t)}get $canvas(){return Mt.get(this)}static get observedAttributes(){return super.observedAttributes.concat(["height","width","x","y"])}connectedCallback(){super.connectedCallback();const t=this.closest(this.$getTagNameOf(a));if(t){this.$canvas=t,this.style.position="absolute";const e=t.querySelector(this.$getTagNameOf(l));e&&(this.$onCanvasActionStart=t=>{e.hidden&&t.detail.action===$&&(this.hidden=!1)},this.$onCanvasActionEnd=t=>{e.hidden&&t.detail.action===$&&(this.hidden=!0)},this.$onCanvasChange=t=>{const{x:i,y:s,width:n,height:a}=t.detail;this.$change(i,s,n,a),(e.hidden||0===i&&0===s&&0===n&&0===a)&&(this.hidden=!0)},rt(t,q,this.$onCanvasActionStart),rt(t,L,this.$onCanvasActionEnd),rt(t,H,this.$onCanvasChange))}this.$render()}disconnectedCallback(){const{$canvas:t}=this;t&&(this.$onCanvasActionStart&&(ot(t,q,this.$onCanvasActionStart),this.$onCanvasActionStart=null),this.$onCanvasActionEnd&&(ot(t,L,this.$onCanvasActionEnd),this.$onCanvasActionEnd=null),this.$onCanvasChange&&(ot(t,H,this.$onCanvasChange),this.$onCanvasChange=null)),super.disconnectedCallback()}$change(t,e,i=this.width,s=this.height){return F(t)&&F(e)&&F(i)&&F(s)&&(t!==this.x||e!==this.y||i!==this.width||s!==this.height)?(this.hidden&&(this.hidden=!1),this.x=t,this.y=e,this.width=i,this.height=s,this.$render()):this}$reset(){return this.$change(0,0,0,0)}$render(){return this.$setStyles({transform:`translate(${this.x}px, ${this.y}px)`,width:this.width,height:this.height,outlineWidth:e.innerWidth})}}Dt.$name=d,Dt.$version="2.0.0";class Pt extends xt{constructor(){super(...arguments),this.$onCanvasCropEnd=null,this.$onCanvasCropStart=null,this.$style=':host{background-color:var(--theme-color);display:block}:host([action=move]),:host([action=select]){height:100%;left:0;position:absolute;top:0;width:100%}:host([action=move]){cursor:move}:host([action=select]){cursor:crosshair}:host([action$=-resize]){background-color:transparent;height:15px;position:absolute;width:15px}:host([action$=-resize]):after{background-color:var(--theme-color);content:"";display:block;height:5px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:5px}:host([action=n-resize]),:host([action=s-resize]){cursor:ns-resize;left:50%;transform:translateX(-50%);width:100%}:host([action=n-resize]){top:-8px}:host([action=s-resize]){bottom:-8px}:host([action=e-resize]),:host([action=w-resize]){cursor:ew-resize;height:100%;top:50%;transform:translateY(-50%)}:host([action=e-resize]){right:-8px}:host([action=w-resize]){left:-8px}:host([action=ne-resize]){cursor:nesw-resize;right:-8px;top:-8px}:host([action=nw-resize]){cursor:nwse-resize;left:-8px;top:-8px}:host([action=se-resize]){bottom:-8px;cursor:nwse-resize;right:-8px}:host([action=se-resize]):after{height:15px;width:15px}@media (pointer:coarse){:host([action=se-resize]):after{height:10px;width:10px}}@media (pointer:fine){:host([action=se-resize]):after{height:5px;width:5px}}:host([action=sw-resize]){bottom:-8px;cursor:nesw-resize;left:-8px}:host([plain]){background-color:transparent}',this.action=f,this.plain=!1,this.slottable=!1,this.themeColor="rgba(51, 153, 255, 0.5)"}static get observedAttributes(){return super.observedAttributes.concat(["action","plain"])}}Pt.$name=h,Pt.$version="2.0.0";const Ot=new WeakMap;class It extends xt{constructor(){super(...arguments),this.$onCanvasAction=null,this.$onCanvasActionStart=null,this.$onCanvasActionEnd=null,this.$onDocumentKeyDown=null,this.$action="",this.$actionStartTarget=null,this.$changing=!1,this.$style=':host{display:block;left:0;position:relative;right:0}:host([outlined]){outline:1px solid var(--theme-color)}:host([multiple]){outline:1px dashed hsla(0,0%,100%,.5)}:host([multiple]):after{bottom:0;content:"";cursor:pointer;display:block;left:0;position:absolute;right:0;top:0}:host([multiple][active]){outline-color:var(--theme-color);z-index:1}:host([multiple])>*{visibility:hidden}:host([multiple][active])>*{visibility:visible}:host([multiple][active]):after{display:none}',this.$initialSelection={x:0,y:0,width:0,height:0},this.x=0,this.y=0,this.width=0,this.height=0,this.aspectRatio=NaN,this.initialAspectRatio=NaN,this.initialCoverage=NaN,this.active=!1,this.linked=!1,this.dynamic=!1,this.movable=!1,this.resizable=!1,this.zoomable=!1,this.multiple=!1,this.keyboard=!1,this.outlined=!1,this.precise=!1}set $canvas(t){Ot.set(this,t)}get $canvas(){return Ot.get(this)}static get observedAttributes(){return super.observedAttributes.concat(["active","aspect-ratio","dynamic","height","initial-aspect-ratio","initial-coverage","keyboard","linked","movable","multiple","outlined","precise","resizable","width","x","y","zoomable"])}$propertyChangedCallback(t,e,i){if(!Object.is(i,e))switch(super.$propertyChangedCallback(t,e,i),t){case"x":case"y":case"width":case"height":this.$changing||this.$nextTick((()=>{this.$change(this.x,this.y,this.width,this.height,this.aspectRatio,!0)}));break;case"aspectRatio":case"initialAspectRatio":this.$nextTick((()=>{this.$initSelection()}));break;case"initialCoverage":this.$nextTick((()=>{Z(i)&&i<=1&&this.$initSelection(!0,!0)}));break;case"keyboard":this.$nextTick((()=>{this.$canvas&&(i?this.$onDocumentKeyDown||(this.$onDocumentKeyDown=this.$handleKeyDown.bind(this),rt(this.ownerDocument,N,this.$onDocumentKeyDown)):this.$onDocumentKeyDown&&(ot(this.ownerDocument,N,this.$onDocumentKeyDown),this.$onDocumentKeyDown=null))}));break;case"multiple":this.$nextTick((()=>{if(this.$canvas){const t=this.$getSelections();i?(t.forEach((t=>{t.active=!1})),this.active=!0,this.$emit(H,{x:this.x,y:this.y,width:this.width,height:this.height})):(this.active=!1,t.slice(1).forEach((t=>{this.$removeSelection(t)})))}}));break;case"precise":this.$nextTick((()=>{this.$change(this.x,this.y)}));break;case"linked":i&&(this.dynamic=!0)}}connectedCallback(){super.connectedCallback();const t=this.closest(this.$getTagNameOf(a));t?(this.$canvas=t,this.$setStyles({position:"absolute",transform:`translate(${this.x}px, ${this.y}px)`}),this.hidden||this.$render(),this.$initSelection(!0),this.$onCanvasActionStart=this.$handleActionStart.bind(this),this.$onCanvasActionEnd=this.$handleActionEnd.bind(this),this.$onCanvasAction=this.$handleAction.bind(this),rt(t,q,this.$onCanvasActionStart),rt(t,L,this.$onCanvasActionEnd),rt(t,W,this.$onCanvasAction)):this.$render()}disconnectedCallback(){const{$canvas:t}=this;t&&(this.$onCanvasActionStart&&(ot(t,q,this.$onCanvasActionStart),this.$onCanvasActionStart=null),this.$onCanvasActionEnd&&(ot(t,L,this.$onCanvasActionEnd),this.$onCanvasActionEnd=null),this.$onCanvasAction&&(ot(t,W,this.$onCanvasAction),this.$onCanvasAction=null)),super.disconnectedCallback()}$getSelections(){let t=[];return this.parentElement&&(t=Array.from(this.parentElement.querySelectorAll(this.$getTagNameOf(l)))),t}$initSelection(t=!1,e=!1){const{initialCoverage:i,parentElement:s}=this;if(Z(i)&&s){const n=this.aspectRatio||this.initialAspectRatio;let a=(e?0:this.width)||s.offsetWidth*i,o=(e?0:this.height)||s.offsetHeight*i;Z(n)&&({width:a,height:o}=bt({aspectRatio:n,width:a,height:o})),this.$change(this.x,this.y,a,o),t&&this.$center(),this.$initialSelection={x:this.x,y:this.y,width:this.width,height:this.height}}}$createSelection(){const t=this.cloneNode(!0);return this.hasAttribute("id")&&t.removeAttribute("id"),t.initialCoverage=NaN,this.active=!1,this.parentElement&&this.parentElement.insertBefore(t,this.nextSibling),t}$removeSelection(t=this){if(this.parentElement){const e=this.$getSelections();if(e.length>1){const i=e.indexOf(t),s=e[i+1]||e[i-1];s&&(t.active=!1,this.parentElement.removeChild(t),s.active=!0,s.$emit(H,{x:s.x,y:s.y,width:s.width,height:s.height}))}else this.$clear()}}$handleActionStart(t){var e,i;const s=null===(i=null===(e=t.detail)||void 0===e?void 0:e.relatedEvent)||void 0===i?void 0:i.target;this.$action="",this.$actionStartTarget=s,!this.hidden&&this.multiple&&!this.active&&s===this&&this.parentElement&&(this.$getSelections().forEach((t=>{t.active=!1})),this.active=!0,this.$emit(H,{x:this.x,y:this.y,width:this.width,height:this.height}))}$handleAction(t){const{currentTarget:e,detail:i}=t;if(!e||!i)return;const{relatedEvent:s}=i;let{action:n}=i;if(!n&&this.multiple&&(n=this.$action||(null==s?void 0:s.target.action),this.$action=n),!n||this.hidden&&n!==$||this.multiple&&!this.active&&n!==g)return;const a=i.endX-i.startX,o=i.endY-i.startY,{width:r,height:h}=this;let{aspectRatio:c}=this;switch(!Z(c)&&s.shiftKey&&(c=Z(r)&&Z(h)?r/h:1),n){case $:if(0!==a&&0!==o){const{$canvas:t}=this,s=$t(e);(this.multiple&&!this.hidden?this.$createSelection():this).$change(i.startX-s.left,i.startY-s.top,Math.abs(a),Math.abs(o),c),a<0?o<0?n=k:o>0&&(n=A):a>0&&(o<0?n=S:o>0&&(n=x)),t&&(t.$action=n)}break;case p:this.movable&&(this.dynamic||this.$actionStartTarget&&this.contains(this.$actionStartTarget))&&this.$move(a,o);break;case g:if(s&&this.zoomable&&(this.dynamic||this.contains(s.target))){const t=$t(e);this.$zoom(i.scale,s.pageX-t.left,s.pageY-t.top)}break;default:this.$resize(n,a,o,c)}}$handleActionEnd(){this.$action="",this.$actionStartTarget=null}$handleKeyDown(t){if(this.hidden||!this.keyboard||this.multiple&&!this.active||t.defaultPrevented)return;const{activeElement:e}=document;if(!e||!["INPUT","TEXTAREA"].includes(e.tagName)&&!["true","plaintext-only"].includes(e.contentEditable))switch(t.key){case"Backspace":t.metaKey&&(t.preventDefault(),this.$removeSelection());break;case"Delete":t.preventDefault(),this.$removeSelection();break;case"ArrowLeft":t.preventDefault(),this.$move(-1,0);break;case"ArrowRight":t.preventDefault(),this.$move(1,0);break;case"ArrowUp":t.preventDefault(),this.$move(0,-1);break;case"ArrowDown":t.preventDefault(),this.$move(0,1);break;case"+":t.preventDefault(),this.$zoom(.1);break;case"-":t.preventDefault(),this.$zoom(-.1)}}$center(){const{parentElement:t}=this;if(!t)return this;const e=(t.offsetWidth-this.width)/2,i=(t.offsetHeight-this.height)/2;return this.$change(e,i)}$move(t,e=t){return this.$moveTo(this.x+t,this.y+e)}$moveTo(t,e=t){return this.movable?this.$change(t,e):this}$resize(t,e=0,i=0,s=this.aspectRatio){if(!this.resizable)return this;const n=Z(s),{$canvas:a}=this;let{x:o,y:r,width:h,height:c}=this;switch(t){case v:r+=i,c-=i,c<0&&(t=y,c=-c,r-=c),n&&(o+=(e=i*s)/2,h-=e,h<0&&(h=-h,o-=h));break;case w:h+=e,h<0&&(t=C,h=-h,o-=h),n&&(r-=(i=e/s)/2,c+=i,c<0&&(c=-c,r-=c));break;case y:c+=i,c<0&&(t=v,c=-c,r-=c),n&&(o-=(e=i*s)/2,h+=e,h<0&&(h=-h,o-=h));break;case C:o+=e,h-=e,h<0&&(t=w,h=-h,o-=h),n&&(r+=(i=e/s)/2,c-=i,c<0&&(c=-c,r-=c));break;case S:n&&(i=-e/s),r+=i,c-=i,h+=e,h<0&&c<0?(t=A,h=-h,c=-c,o-=h,r-=c):h<0?(t=k,h=-h,o-=h):c<0&&(t=x,c=-c,r-=c);break;case k:n&&(i=e/s),o+=e,r+=i,h-=e,c-=i,h<0&&c<0?(t=x,h=-h,c=-c,o-=h,r-=c):h<0?(t=S,h=-h,o-=h):c<0&&(t=A,c=-c,r-=c);break;case x:n&&(i=e/s),h+=e,c+=i,h<0&&c<0?(t=k,h=-h,c=-c,o-=h,r-=c):h<0?(t=A,h=-h,o-=h):c<0&&(t=S,c=-c,r-=c);break;case A:n&&(i=-e/s),o+=e,h-=e,c+=i,h<0&&c<0?(t=S,h=-h,c=-c,o-=h,r-=c):h<0?(t=x,h=-h,o-=h):c<0&&(t=k,c=-c,r-=c)}return a&&a.$setAction(t),this.$change(o,r,h,c)}$zoom(t,e,i){if(!this.zoomable||0===t)return this;t<0?t=1/(1-t):t+=1;const{width:s,height:n}=this,a=s*t,o=n*t;let r=this.x,h=this.y;return F(e)&&F(i)?(r-=(a-s)*((e-this.x)/s),h-=(o-n)*((i-this.y)/n)):(r-=(a-s)/2,h-=(o-n)/2),this.$change(r,h,a,o)}$change(t,e,i=this.width,s=this.height,n=this.aspectRatio,a=!1){return this.$changing||!F(t)||!F(e)||!F(i)||!F(s)||i<0||s<0?this:(Z(n)&&({width:i,height:s}=bt({aspectRatio:n,width:i,height:s},"cover")),this.precise||(t=Math.round(t),e=Math.round(e),i=Math.round(i),s=Math.round(s)),t===this.x&&e===this.y&&i===this.width&&s===this.height&&Object.is(n,this.aspectRatio)&&!a?this:(this.hidden&&(this.hidden=!1),!1===this.$emit(H,{x:t,y:e,width:i,height:s})?this:(this.$changing=!0,this.x=t,this.y=e,this.width=i,this.height=s,this.$changing=!1,this.$render())))}$reset(){const{x:t,y:e,width:i,height:s}=this.$initialSelection;return this.$change(t,e,i,s)}$clear(){return this.$change(0,0,0,0,NaN,!0),this.hidden=!0,this}$render(){return this.$setStyles({transform:`translate(${this.x}px, ${this.y}px)`,width:this.width,height:this.height})}$toCanvas(t){return new Promise(((e,i)=>{if(!this.isConnected)return void i(new Error("The current element is not connected to the DOM."));const s=document.createElement("canvas");let{width:n,height:a}=this,o=1;if(V(t)&&(Z(t.width)||Z(t.height))&&(({width:n,height:a}=bt({aspectRatio:n/a,width:t.width,height:t.height})),o=n/this.width),s.width=n,s.height=a,!this.$canvas)return void e(s);const r=this.$canvas.querySelector(this.$getTagNameOf(c));r?r.$ready().then((i=>{const h=s.getContext("2d");if(h){const[e,c,l,d,u,$]=r.$getTransform(),p=-this.x,g=-this.y,m=(p*d-l*g)/(e*d-l*c),b=(g*e-c*p)/(e*d-l*c);let f=e*m+l*b+u,v=c*m+d*b+$,w=i.naturalWidth,y=i.naturalHeight;1!==o&&(f*=o,v*=o,w*=o,y*=o);const C=w/2,S=y/2;h.fillStyle="transparent",h.fillRect(0,0,n,a),V(t)&&_(t.beforeDraw)&&t.beforeDraw.call(this,h,s),h.save(),h.translate(C,S),h.transform(e,c,l,d,f,v),h.translate(-C,-S),h.drawImage(i,0,0,w,y),h.restore()}e(s)})).catch(i):e(s)}))}}It.$name=l,It.$version="2.0.0";class Nt extends xt{constructor(){super(...arguments),this.$style=":host{display:flex;flex-direction:column;position:relative;touch-action:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}:host([bordered]){border:1px dashed var(--theme-color)}:host([covered]){bottom:0;left:0;position:absolute;right:0;top:0}:host>span{display:flex;flex:1}:host>span+span{border-top:1px dashed var(--theme-color)}:host>span>span{flex:1}:host>span>span+span{border-left:1px dashed var(--theme-color)}",this.bordered=!1,this.columns=3,this.covered=!1,this.rows=3,this.slottable=!1,this.themeColor="rgba(238, 238, 238, 0.5)"}static get observedAttributes(){return super.observedAttributes.concat(["bordered","columns","covered","rows"])}$propertyChangedCallback(t,e,i){Object.is(i,e)||(super.$propertyChangedCallback(t,e,i),"rows"!==t&&"columns"!==t||this.$nextTick((()=>{this.$render()})))}connectedCallback(){super.connectedCallback(),this.$render()}$render(){const t=this.$getShadowRoot(),e=document.createDocumentFragment();for(let t=0;t{setTimeout((()=>{this.$render()}),50)})))}$handleSourceImageTransform(t){this.$render(void 0,t.detail.matrix)}$render(t,e){const{$canvas:i,$selection:s}=this;t||s.hidden||(t=s),(!t||0===t.x&&0===t.y&&0===t.width&&0===t.height)&&(t={x:0,y:0,width:i.offsetWidth,height:i.offsetHeight});const{x:n,y:a,width:o,height:r}=t,h={},{clientWidth:c,clientHeight:l}=this;let d=c,u=l,$=NaN;switch(this.resize){case"both":$=1,d=o,u=r,h.width=o,h.height=r;break;case"horizontal":$=r>0?l/r:0,d=o*$,h.width=d;break;case jt:$=o>0?c/o:0,u=r*$,h.height=u;break;default:c>0?$=o>0?c/o:0:l>0&&($=r>0?l/r:0)}this.$scale=$,this.$setStyles(h),this.$sourceImage&&this.$transformImageByOffset(null!=e?e:this.$sourceImage.$getTransform(),-n,-a)}$transformImageByOffset(t,e,i){const{$image:s,$scale:n,$sourceImage:a}=this;if(a&&s&&n>=0){const[a,o,r,h,c,l]=t,d=(e*h-r*i)/(a*h-r*o),u=(i*a-o*e)/(a*h-r*o),$=a*d+r*u+c,p=o*d+h*u+l;s.$ready((t=>{this.$setStyles.call(s,{width:t.naturalWidth*n,height:t.naturalHeight*n})})),s.$setTransform(a,o,r,h,$*n,p*n)}}}qt.$name=u,qt.$version="2.0.0";var Ht='';const Bt=/^img|canvas$/,Ut=/<(\/?(?:script|style)[^>]*)>/gi,Kt={template:Ht};At.$define(),Rt.$define(),Nt.$define(),Pt.$define(),zt.$define(),It.$define(),Dt.$define(),qt.$define();class Ft{constructor(t,e){if(this.options=Kt,U(t)&&(t=document.querySelector(t)),!tt(t)||!Bt.test(t.localName))throw new Error("The first argument is required and must be an or element.");this.element=t,e=Object.assign(Object.assign({},Kt),e),this.options=e;const{ownerDocument:i}=t;let{container:s}=e;if(s&&(U(s)&&(s=i.querySelector(s)),!tt(s)))throw new Error("The `container` option must be an element or a valid selector.");tt(s)||(s=t.parentElement?t.parentElement:i.body),this.container=s;const n=t.localName;let a="";"img"===n?({src:a}=t):"canvas"===n&&window.HTMLCanvasElement&&(a=t.toDataURL());const{template:o}=e;if(o&&U(o)){const e=document.createElement("template"),i=document.createDocumentFragment();e.innerHTML=o.replace(Ut,"<$1>"),i.appendChild(e.content),Array.from(i.querySelectorAll(c)).forEach((e=>{e.setAttribute("src",a),e.setAttribute("alt",t.alt||"The image to crop")})),t.parentElement?(t.style.display="none",s.insertBefore(i,t.nextSibling)):s.appendChild(i)}}getCropperCanvas(){return this.container.querySelector(a)}getCropperImage(){return this.container.querySelector(c)}getCropperSelection(){return this.container.querySelector(l)}getCropperSelections(){return this.container.querySelectorAll(l)}}Ft.version="2.0.0";export{p as ACTION_MOVE,f as ACTION_NONE,w as ACTION_RESIZE_EAST,v as ACTION_RESIZE_NORTH,S as ACTION_RESIZE_NORTHEAST,k as ACTION_RESIZE_NORTHWEST,y as ACTION_RESIZE_SOUTH,x as ACTION_RESIZE_SOUTHEAST,A as ACTION_RESIZE_SOUTHWEST,C as ACTION_RESIZE_WEST,m as ACTION_ROTATE,g as ACTION_SCALE,$ as ACTION_SELECT,b as ACTION_TRANSFORM,E as ATTRIBUTE_ACTION,a as CROPPER_CANVAS,o as CROPPER_CROSSHAIR,r as CROPPER_GIRD,h as CROPPER_HANDLE,c as CROPPER_IMAGE,l as CROPPER_SELECTION,d as CROPPER_SHADE,u as CROPPER_VIEWER,At as CropperCanvas,Rt as CropperCrosshair,xt as CropperElement,Nt as CropperGrid,Pt as CropperHandle,zt as CropperImage,It as CropperSelection,Dt as CropperShade,qt as CropperViewer,Ht as DEFAULT_TEMPLATE,W as EVENT_ACTION,L as EVENT_ACTION_END,j as EVENT_ACTION_MOVE,q as EVENT_ACTION_START,H as EVENT_CHANGE,I as EVENT_ERROR,N as EVENT_KEYDOWN,R as EVENT_LOAD,D as EVENT_POINTER_DOWN,P as EVENT_POINTER_MOVE,O as EVENT_POINTER_UP,X as EVENT_RESIZE,T as EVENT_TOUCH_END,z as EVENT_TOUCH_MOVE,M as EVENT_TOUCH_START,B as EVENT_TRANSFORM,Y as EVENT_WHEEL,s as HAS_POINTER_EVENT,t as IS_BROWSER,i as IS_TOUCH_DEVICE,n as NAMESPACE,e as WINDOW,Ft as default,lt as emit,bt as getAdjustedSizes,$t as getOffset,tt as isElement,_ as isFunction,K as isNaN,F as isNumber,J as isObject,V as isPlainObject,Z as isPositiveNumber,U as isString,G as isUndefined,ft as multiplyMatrices,ut as nextTick,ot as off,rt as on,ht as once,gt as toAngleInRadian,nt as toCamelCase,it as toKebabCase}; diff --git a/src/Cropper.Blazor/Client.V1/.config/dotnet-tools.json b/src/Cropper.Blazor/Client.V1/.config/dotnet-tools.json new file mode 100644 index 00000000..2c41bf42 --- /dev/null +++ b/src/Cropper.Blazor/Client.V1/.config/dotnet-tools.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "excubo.webcompiler": { + "version": "4.2.1", + "commands": ["webcompiler"] + } + } +} diff --git a/src/Cropper.Blazor/Client.V1/App.razor b/src/Cropper.Blazor/Client.V1/App.razor new file mode 100644 index 00000000..3a5ededa --- /dev/null +++ b/src/Cropper.Blazor/Client.V1/App.razor @@ -0,0 +1,13 @@ + + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/src/Cropper.Blazor/Client.V1/Components/AspectRatioSettings.razor b/src/Cropper.Blazor/Client.V1/Components/AspectRatioSettings.razor new file mode 100644 index 00000000..b63525b7 --- /dev/null +++ b/src/Cropper.Blazor/Client.V1/Components/AspectRatioSettings.razor @@ -0,0 +1,29 @@ + + + +
+ Aspect Ratio Settings + + This option work's only for FREE aspect ratio mode! + +
+
+
+ + + + + +
+ + Current aspect ratio: + + +
+
+
diff --git a/src/Cropper.Blazor/Client.V1/Components/AspectRatioSettings.razor.cs b/src/Cropper.Blazor/Client.V1/Components/AspectRatioSettings.razor.cs new file mode 100644 index 00000000..37e6050f --- /dev/null +++ b/src/Cropper.Blazor/Client.V1/Components/AspectRatioSettings.razor.cs @@ -0,0 +1,93 @@ +using System.ComponentModel.DataAnnotations; +using Cropper.Blazor.Components; +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; + InvokeAsync(ApplyAspectRatioRulesForCropperAsync); + } + } + + public decimal? MinAspectRatio + { + get => minAspectRatio; + set + { + minAspectRatio = value; + InvokeAsync(ApplyAspectRatioRulesForCropperAsync); + } + } + + [CascadingParameter(Name = "AspectRatio"), Required] + private decimal? AspectRatio { get; set; } + + [CascadingParameter(Name = "CropperComponent"), Required] + private CropperComponent CropperComponent { get; set; } = null!; + + [CascadingParameter(Name = "IsFreeAspectRatioEnabled"), Required] + private bool IsFreeAspectRatioEnabled + { + 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 CropperComponent!.GetContainerDataAsync(); + CropBoxData cropBoxData = await CropperComponent!.GetCropBoxDataAsync(); + + if (cropBoxData.Height != 0) + { + decimal aspectRatio = cropBoxData.Width / cropBoxData.Height; + + if (aspectRatio < minAspectRatio || aspectRatio > maxAspectRatio) + { + decimal? newCropBoxWidth = cropBoxData.Height * ((minAspectRatio + maxAspectRatio) / 2); + decimal? left = (containerData.Width - newCropBoxWidth) / 2; + + CropperComponent!.SetCropBoxData(new SetCropBoxDataOptions + { + Left = left, + Width = newCropBoxWidth, + }); + + cropBoxData = await CropperComponent!.GetCropBoxDataAsync(); + aspectRatio = cropBoxData.Width / cropBoxData.Height; + } + + SetUpAspectRatio(aspectRatio); + } + } + } + + public void SetUpAspectRatio(decimal? aspectRatio) + { + AspectRatio = aspectRatio; + StateHasChanged(); + } + } +} diff --git a/src/Cropper.Blazor/Client.V1/Components/CroppedDimensionsSettings.razor b/src/Cropper.Blazor/Client.V1/Components/CroppedDimensionsSettings.razor new file mode 100644 index 00000000..406fc01f --- /dev/null +++ b/src/Cropper.Blazor/Client.V1/Components/CroppedDimensionsSettings.razor @@ -0,0 +1,32 @@ + + + +
+ Dimensions Settings + + This setting may not work with a specific aspect ratio. + It is recommended to use a FREE aspect ratio for this + option or calculate the valid values yourself. + +
+
+
+ +
+ + + + +
+
+ + + + +
+
+
diff --git a/src/Cropper.Blazor/Client.V1/Components/CroppedDimensionsSettings.razor.cs b/src/Cropper.Blazor/Client.V1/Components/CroppedDimensionsSettings.razor.cs new file mode 100644 index 00000000..d0673efa --- /dev/null +++ b/src/Cropper.Blazor/Client.V1/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.V1/Components/CropperDataPreview.razor b/src/Cropper.Blazor/Client.V1/Components/CropperDataPreview.razor new file mode 100644 index 00000000..ff8a490d --- /dev/null +++ b/src/Cropper.Blazor/Client.V1/Components/CropperDataPreview.razor @@ -0,0 +1,12 @@ + + + + + + + diff --git a/src/Cropper.Blazor/Client.V1/Components/CropperDataPreview.razor.cs b/src/Cropper.Blazor/Client.V1/Components/CropperDataPreview.razor.cs new file mode 100644 index 00000000..5c0d1ece --- /dev/null +++ b/src/Cropper.Blazor/Client.V1/Components/CropperDataPreview.razor.cs @@ -0,0 +1,28 @@ +using Cropper.Blazor.Events.CropEvent; + +namespace Cropper.Blazor.Client.Components +{ + public partial class CropperDataPreview + { + private decimal? X; + private decimal? Y; + private decimal? Height; + private decimal? Width; + private decimal? Rotate; + private decimal? ScaleX; + private decimal? ScaleY; + + public void OnCropEvent(CropEvent cropEvent) + { + X = cropEvent.X; + Y = cropEvent.Y; + Width = cropEvent.Width; + Height = cropEvent.Height; + Rotate = cropEvent.Rotate; + ScaleX = cropEvent.ScaleX; + ScaleY = cropEvent.ScaleY; + + StateHasChanged(); + } + } +} diff --git a/src/Cropper.Blazor/Client.V1/Components/Docs/ApiLink.cs b/src/Cropper.Blazor/Client.V1/Components/Docs/ApiLink.cs new file mode 100644 index 00000000..c1c5961b --- /dev/null +++ b/src/Cropper.Blazor/Client.V1/Components/Docs/ApiLink.cs @@ -0,0 +1,112 @@ +using System.Reflection; +using Cropper.Blazor.Components; +using Cropper.Blazor.Exceptions; +using Cropper.Blazor.Models; +using Cropper.Blazor.Shared.Extensions; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace Cropper.Blazor.Client.Components.Docs +{ + public static class ApiLink + { + private static readonly Dictionary _specialCaseComponents = + new() + { + [typeof(CropperComponent)] = "cropper-component" + }; + + public static string GetComponentLinkFor(Type type) + { + return $"components/{GetComponentName(type)}"; + } + + private static string GetComponentName(Type type) + { + if (!_specialCaseComponents.TryGetValue(type, out string? component)) + { + component = new string(type.ToString().Replace("Cropper.Blazor", "").TakeWhile(c => c != '`').ToArray()) + .ToLowerInvariant(); + } + + return component; + } + + public static Type? GetTypeFromComponentLink(string component) + { + if (component.Contains('#') == true) + { + component = component[..component.IndexOf('#')]; + } + + if (string.IsNullOrEmpty(component)) + { + return null; + } + + Assembly assembly = typeof(CropperComponent).Assembly; + Type[] types = assembly.GetTypes(); + + foreach (Type x in types) + { + if (new string(x.Name.TakeWhile(c => c != '`').ToArray()).Equals($"{component}", StringComparison.InvariantCultureIgnoreCase)) + { + if (x.Name.Contains('`')) + { + return x; + } + else if (x.Name.Equals($"{component}", StringComparison.InvariantCultureIgnoreCase)) + { + return x; + } + } + } + + return null; + } + + public static string GetContextType(this Type type) + { + string value = string.Empty; + + if (type == typeof(Options)) + { + value = nameof(Options).CreateLink(); + } + else if (type == typeof(SetDataOptions)) + { + value = nameof(SetDataOptions).CreateLink(); + } + else if (type == typeof(CropperComponentType)) + { + value = nameof(CropperComponentType).CreateLink(); + } + else if (type == typeof(RenderFragment)) + { + value = nameof(RenderFragment).CreateLink(); + } + else if (type == typeof(IJSObjectReference)) + { + value = nameof(IJSObjectReference).CreateLink(); + } + else if (type == typeof(ImageReceiver)) + { + value = nameof(ImageReceiver).CreateLink(); + } + else if (type == typeof(CroppedCanvasReceiver)) + { + value = nameof(CroppedCanvasReceiver).CreateLink(); + } + else if (type == typeof(CroppedCanvas)) + { + value = nameof(CroppedCanvas).CreateLink(); + } + else if (type == typeof(ImageProcessingException)) + { + value = nameof(ImageProcessingException).CreateLink(); + } + + return value; + } + } +} diff --git a/src/Cropper.Blazor/Client.V1/Components/Docs/ApiMethod.cs b/src/Cropper.Blazor/Client.V1/Components/Docs/ApiMethod.cs new file mode 100644 index 00000000..a32d8b0d --- /dev/null +++ b/src/Cropper.Blazor/Client.V1/Components/Docs/ApiMethod.cs @@ -0,0 +1,15 @@ +using System.Reflection; + +namespace Cropper.Blazor.Client.Components.Docs +{ + public class ApiMethod + { + public string Signature { get; set; } + public string? WarningSignatureMessage { get; set; } + public ParameterInfo Return { get; set; } + public string Documentation { get; set; } + public MethodInfo MethodInfo { get; set; } + public ParameterInfo[] Parameters { get; set; } + public bool IsJsInvokable { get; set; } + } +} diff --git a/src/Cropper.Blazor/Client.V1/Components/Docs/ApiProperty.cs b/src/Cropper.Blazor/Client.V1/Components/Docs/ApiProperty.cs new file mode 100644 index 00000000..b40f5007 --- /dev/null +++ b/src/Cropper.Blazor/Client.V1/Components/Docs/ApiProperty.cs @@ -0,0 +1,14 @@ +using System.Reflection; + +namespace Cropper.Blazor.Client.Components.Docs +{ + public class ApiProperty + { + public string Name { get; set; } + public Type Type { get; set; } + public PropertyInfo? PropertyInfo { get; set; } + public string Description { get; set; } + public object Default { get; set; } + public bool IsTwoWay { get; set; } + } +} diff --git a/src/Cropper.Blazor/Client.V1/Components/Docs/CodeInline.razor b/src/Cropper.Blazor/Client.V1/Components/Docs/CodeInline.razor new file mode 100644 index 00000000..d9bbdd54 --- /dev/null +++ b/src/Cropper.Blazor/Client.V1/Components/Docs/CodeInline.razor @@ -0,0 +1,17 @@ + + @if (Tag) {<}@Code@ChildContent@if (Tag) {>} + + +@code +{ + [Parameter] public RenderFragment? ChildContent { get; set; } = null; + + [Parameter] public string? Code { get; set; } = null; + [Parameter] public string Style { get; set; } = string.Empty; + + [Parameter] public bool SecondaryColor { get; set; } + + [Parameter] public string? Class { get; set; } = null; + + [Parameter] public bool Tag { get; set; } +} diff --git a/src/Cropper.Blazor/Client.V1/Components/Docs/DocsApi.razor b/src/Cropper.Blazor/Client.V1/Components/Docs/DocsApi.razor new file mode 100644 index 00000000..15f793a2 --- /dev/null +++ b/src/Cropper.Blazor/Client.V1/Components/Docs/DocsApi.razor @@ -0,0 +1,335 @@ +@using System.Reflection; +@using Cropper.Blazor.Client.Components.Docs +@using System.Text.RegularExpressions +@using System.Web +@using Cropper.Blazor.Client.Extensions; +@using Cropper.Blazor.Client.Models; +@using Microsoft.Extensions.DependencyInjection +@using System.Globalization +@using MudBlazor; +@using Cropper.Blazor.Shared.Extensions; + + + + + @if (Type is not null) + { + @if (!IsContract && !IsHelper) + { + (string? Href, string? Desc) pageInfo = GetHrefPageWithDesc(); + + + + + + + } + else + { + + @if (Type.IsEnum) + { +
+ Enum @TypeNameHelper.GetTypeDisplay(Type, false, true) +
+ } + else if (Type.IsInterface) + { +
+ Interface @TypeNameHelper.GetTypeDisplay(Type, false, true) +
+ } + else + { +
+ Contract @TypeNameHelper.GetTypeDisplay(Type, false, true) +
+ } +
+ } + + Description + + + + @(new MarkupString(AnalyseMethodDocumentation(GetClassDescription(), "summary"))) + + // save as lists to speed up displaying the page + var properties = GetProperties()?.ToList(); + bool? isShowPropertiesDescription = properties?.All(p => !string.IsNullOrWhiteSpace(p.Description)); + var methods = GetMethods()?.ToList(); + var eventCallbacks = GetEventCallbacks()?.ToList(); + + @if (properties?.Count() > 0) + { + + + + + + Name + Type + Default + @if (isShowPropertiesDescription == true) + { + Description + } + + + @if (_propertiesGrouping == Grouping.Inheritance && (Type)context.Key != Type) + { + + @($"Inherited from {((Type)context.Key).GetTypeDisplayName()}") + + } + else if (_propertiesGrouping == Grouping.Categories) + { + + @context.Key + + } + + + + @context.Name + @if (_propertiesGrouping == Grouping.Inheritance && IsOverridden(context.PropertyInfo)) + { + + overridden + + } + + +
+ + @if (context.IsTwoWay) + { + + + + } +
+
+ + @{ + var def = context.Default.PresentDefaultValue(context.PropertyInfo); + } + @if (def.Contains(" + } + else + { + @def + } + + @if (isShowPropertiesDescription == true) + { + + +
+ @(new MarkupString(HttpUtility.HtmlDecode(AnalyseMethodDocumentation(context.Description, "summary")))) +
+
+
+ + +
+ @(new MarkupString(HttpUtility.HtmlDecode(AnalyseMethodDocumentation(context.Description, "summary")))) +
+
+
+ } +
+ +
+
+
+ } + + @if (eventCallbacks?.Count() > 0) + { + + + + + + Name + Type + Description + + + @context.Name + + +
@(new MarkupString(context.Type.GetFormattedReturnSignature()))
+
+
+ + @(new MarkupString(context.Type.GetFormattedReturnSignature())) + + + @HttpUtility.HtmlDecode(AnalyseMethodDocumentation(context.Description, "summary")) + +
+
+
+
+ } + + @if (methods?.Count() > 0) + { + + + + + + Name + Parameters + Return + Description + + + + + @if (!string.IsNullOrWhiteSpace(context.WarningSignatureMessage)) + { + + @if (context.IsJsInvokable) + { +
+ Internally invokable from JS +
+ } +
@context.WarningSignatureMessage
+ @context.Signature +
+ + } + else + { + + @if (context.IsJsInvokable) + { +
+ Internally invokable from JS +
+ } + @context.Signature +
+ } + +
+ + @if (context.Parameters != null) + { + foreach (var parameterInfo in context.Parameters) + { +
+
@(new MarkupString($"
{TypeNameHelper.GetTypeDisplay(parameterInfo.ParameterType, false, true, Cropper.Blazor.Shared.Extensions.MethodInfoExtensions.CreateLink)} {parameterInfo.Name}
{AnalyseMethodDocumentation(context.Documentation, "param", parameterInfo.Name)}"))
+
+ } + } +
+ + @{ + string methodReturn = AnalyseMethodDocumentation(context.Documentation, "returns"); + } + @if (!string.IsNullOrEmpty(methodReturn)) + { +
@(new MarkupString($"{methodReturn}"))
+ } + else + { +
@TypeNameHelper.GetTypeDisplay(context.Return.ParameterType, false, true)
+ } +
+ +
@(new MarkupString(HttpUtility.HtmlDecode(AnalyseMethodDocumentation(context.Documentation, "summary"))))
+
+
+ + + @context.Signature + @if (context.IsJsInvokable) + { + +
+ Internally invokable from JS +
+
+ } + @if (!string.IsNullOrWhiteSpace(context.WarningSignatureMessage)) + { + +
+ @context.WarningSignatureMessage +
+
+ } +
+ + @if (context.Parameters != null) + { + foreach (var parameterInfo in context.Parameters) + { +
+
@(new MarkupString($"
{TypeNameHelper.GetTypeDisplay(parameterInfo.ParameterType, false, true, Cropper.Blazor.Shared.Extensions.MethodInfoExtensions.CreateLink)} {parameterInfo.Name}
{AnalyseMethodDocumentation(context.Documentation, "param", parameterInfo.Name)}"))
+
+
+ } + } +
+ + @{ + string methodReturn = AnalyseMethodDocumentation(context.Documentation, "returns"); + } + @if (!string.IsNullOrEmpty(methodReturn)) + { +
@(new MarkupString($"{methodReturn}"))
+ } + else + { +
@TypeNameHelper.GetTypeDisplay(context.Return.ParameterType, false, true)
+ } +
+ @(new MarkupString(HttpUtility.HtmlDecode(AnalyseMethodDocumentation(context.Documentation, "summary")))) +
+
+ + + + + + + + +
+
+
+ } + } + else + { + + The requested contract or component does not exist. + + } +
+
diff --git a/src/Cropper.Blazor/Client.V1/Components/Docs/DocsApi.razor.cs b/src/Cropper.Blazor/Client.V1/Components/Docs/DocsApi.razor.cs new file mode 100644 index 00000000..8e9bf294 --- /dev/null +++ b/src/Cropper.Blazor/Client.V1/Components/Docs/DocsApi.razor.cs @@ -0,0 +1,367 @@ +using System.Reflection; +using Cropper.Blazor.Client.Models; +using Cropper.Blazor.Components; +using Cropper.Blazor.Events; +using Cropper.Blazor.Exceptions; +using Cropper.Blazor.Models; +using Cropper.Blazor.Shared.Extensions; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using MudBlazor; + +namespace Cropper.Blazor.Client.Components.Docs +{ + public partial class DocsApi + { + [Parameter] public Type Type { get; set; } + [Parameter] public bool IsContract { get; set; } = false; + [Parameter] public bool IsHelper { get; set; } = false; + [Parameter] public bool? IsComponentContract { get; set; } = null; + [Inject] NavigationManager NavigationManager { get; set; } = null!; + + public DocsPage DocsPage { get; set; } + + // used for default value getting + private object CompInstance; + private readonly List _hiddenMethods = + [ + "ToString", + "GetType", + "GetHashCode", + "Equals", + "SetParametersAsync", + "ReferenceEquals" + ]; + + protected override async Task OnParametersSetAsync() + { + CompInstance = !Type.IsAssignableTo(typeof(IComponent)) ? null : Activator.CreateInstance(Type); + + await base.OnParametersSetAsync(); + } + + private (string? Href, string? Desc) GetHrefPageWithDesc() + { + if (Type == typeof(CropperComponent)) + { + return ("examples/cropperusage", ""); + } + else if (Type == typeof(CroppedCanvasReceiver)) + { + return ("examples/cropping#crop-a-polygon-image-in-background", "See 'Crop in Background' example."); + } + else if (Type == typeof(ImageReceiver)) + { + return ("examples/cropping#crop-a-round-image-in-background", "See 'Crop a polygon image in Background' or 'Crop a round image in Background' examples."); + } + + return (null, null); + } + + private IEnumerable GetEventCallbacks() + { + if (Type == null) + { + yield break; + } + + string saveTypename = DocStrings.GetSaveTypename(Type); + + if (IsContract) + { + yield break; + } + else + { + IEnumerable? propertyInfos = IsComponentContract == true + ? Type.GetPropertyInfos() + : Type.GetPropertyInfosWithAttribute(); + foreach (var info in propertyInfos.OrderBy(x => x.Name)) + { + if (IsEventCallback(info)) + { + yield return new ApiProperty + { + Name = info.Name, + PropertyInfo = info, + Default = string.Empty, + Description = DocStrings.GetMemberDescription(saveTypename, info, IsContract, IsComponentContract), + IsTwoWay = CheckIsTwoWayEventCallback(info), + Type = info.PropertyType, + }; + } + } + } + } + + private string GetClassDescription() + { + if (Type.IsClass) + { + string saveTypename = DocStrings.GetSaveTypename(Type); + + return DocStrings.GetClassDescription(saveTypename); + } + else if (Type.IsInterface) + { + string saveTypename = DocStrings.GetSaveTypename(Type); + + return DocStrings.GetInterfaceDescription(saveTypename); + } + else if (Type.IsEnum) + { + return DocStrings.GetEnumDescription(Type.Name); + } + + return string.Empty; + } + + private IEnumerable GetMethods() + { + if (Type == null) + { + yield break; + } + + string saveTypename = DocStrings.GetSaveTypename(Type); + + if (IsContract) + { + yield break; + } + else + { + foreach (var info in Type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Static).OrderBy(x => x.Name)) + { + if (!_hiddenMethods.Any(x => x.Contains(info.Name)) && !info.Name.StartsWith("get_") && !info.Name.StartsWith("set_")) + { + bool hasNoJsInvokableAttribute = info.GetCustomAttributes(typeof(JSInvokableAttribute), true).Length == 0; + + Attribute? attribute = info + .GetCustomAttribute(typeof(ObsoleteAttribute), true); + string? warningSignatureMessage = null; + + if (attribute != null) + { + ObsoleteAttribute obsoleteAttr = (ObsoleteAttribute)attribute; + + warningSignatureMessage = obsoleteAttr.Message; + } + + yield return new ApiMethod() + { + MethodInfo = info, + IsJsInvokable = !hasNoJsInvokableAttribute, + WarningSignatureMessage = warningSignatureMessage, + Return = info.ReturnParameter, + Signature = info.GetSignature(), + Parameters = info.GetParameters(), + Documentation = DocStrings.GetMemberDescription(saveTypename, info, IsContract, IsComponentContract) + }; + } + } + } + } + + private static bool IsEventCallback(PropertyInfo? propertyInfo) + { + return (propertyInfo!.PropertyType.Name.Contains("EventCallback") && (propertyInfo!.PropertyType.FullName ?? "").Contains(typeof(EventCallback).Namespace)) + || (propertyInfo!.PropertyType.Name.Contains("Action") && (propertyInfo!.PropertyType.FullName ?? "").Contains(typeof(Action).Namespace)) + || (propertyInfo!.PropertyType.Name.Contains("Func") && (propertyInfo!.PropertyType.FullName ?? "").Contains(typeof(Func<>).Namespace)); + } + + private IEnumerable GetProperties() + { + if (Type == null) + { + yield break; + } + + string saveTypename = DocStrings.GetSaveTypename(Type); + IEnumerable types = null!; + + if (IsContract || IsComponentContract == true) + { + types = Type + .GetPropertyInfos(); + } + else + { + types = Type + .GetPropertyInfosWithAttribute(); + } + + if (Type.IsEnum) + { + foreach (var info in Enum.GetValues(Type)) + { + string? enumDisplayStatus = Convert.ChangeType(info, Type).ToString(); + yield return ToApiProperty(Type, enumDisplayStatus, ((int)info).ToString()); + } + } + else + { + + foreach (var info in types.OrderBy(x => x.Name)) + { + if (!IsEventCallback(info)) + { + yield return ToApiProperty(info, saveTypename); + } + } + } + } + + private ApiProperty ToApiProperty(PropertyInfo info, string saveTypename) + { + object defaultValue = GetDefaultValue(info); + + return new ApiProperty + { + Name = info.Name, + PropertyInfo = info, + Default = defaultValue, + IsTwoWay = CheckIsTwoWayProperty(info), + Description = DocStrings.GetMemberDescription(saveTypename, info, IsContract, IsComponentContract), + Type = info.PropertyType + }; + } + + private static ApiProperty ToApiProperty(Type type, string? enumDisplayStatus, string value) + { + return new ApiProperty + { + Name = enumDisplayStatus, + PropertyInfo = null, + Default = value, + Description = DocStrings.GetEnumValueDescription(type.Name, enumDisplayStatus), + Type = type + }; + } + + private static string AnalyseMethodDocumentation(string documentation, string occurrence, string parameter = "") + { + try + { + // Define local variable + string doublequotes = @""""; + + // Define the start tag and the end tag + string endTag = $""; + string startTag = $"<{occurrence}{(parameter == string.Empty ? "" : " name=" + doublequotes + parameter + doublequotes)}>"; + + // Check if the documentation is valid and contains the start tag + if (documentation != null && documentation.Contains(startTag)) + { + // Remove the beginning of the documentation until the start tag + documentation = documentation.Substring(documentation.IndexOf(startTag), documentation.Length - documentation.IndexOf(startTag)); + + // Check if the documentation contains the end tag + if (documentation.Contains(endTag)) + { + // Return the extracted information + // If the information is not for summary, ' : ' is only added if there is a non-empty information to be returned + return ((occurrence != "summary" && documentation.Substring(startTag.Length, documentation.IndexOf(endTag) - startTag.Length).Trim() != "" ? "" : "") + + documentation.Substring(startTag.Length, documentation.IndexOf(endTag) - startTag.Length).Trim()) + .Replace(">", ">") + .Replace("<", "<"); + } + } + } + catch + { + // ignored + } + + return string.Empty; + } + + private static bool CheckIsTwoWayEventCallback(PropertyInfo propertyInfo) => propertyInfo.Name.EndsWith("Changed"); + + private bool CheckIsTwoWayProperty(PropertyInfo propertyInfo) + { + PropertyInfo? eventCallbackInfo = Type.GetProperty(propertyInfo.Name + "Changed"); + + return eventCallbackInfo != null && + eventCallbackInfo.PropertyType.Name.Contains("EventCallback") && + eventCallbackInfo.GetCustomAttribute() != null && + eventCallbackInfo.GetCustomAttribute() == null; + } + + private async Task OnPageChanged(int newPage) + { + await DocsPage.ContentNavigation.ScrollToSection(new Uri(NavigationManager.BaseUri + "/api#methods")); + } + + private object GetDefaultValue(PropertyInfo info) + { + if (CompInstance == null) + { + var constructors = Type.GetConstructors(); + + if (!constructors.Any()) + { + return info.GetValue(Activator.CreateInstance(Type), null); + } + + ParameterInfo[] parameters = constructors.First().GetParameters(); + + if (!parameters.Any()) + { + if (Type == typeof(JSEventData<>)) + { + return new JSEventData(); + } + else + { + return info.GetValue(Activator.CreateInstance(Type), null); + } + } + + if (Type == typeof(CroppedCanvas)) + { + return info.GetValue(new CroppedCanvas(default)); + } + else if (Type == typeof(CroppedCanvasReceiver)) + { + return info.GetValue(new CroppedCanvasReceiver(default, default)); + } + else if (Type == typeof(ImageProcessingException)) + { + return info.GetValue(new ImageProcessingException(default)); + } + else + { + throw new InvalidOperationException("Unsupported type"); + } + } + + return info.GetValue(CompInstance); + } + + #region Grouping properties + + private enum Grouping { Categories, Inheritance, None } + + private readonly Grouping _propertiesGrouping = Grouping.None; + + private TableGroupDefinition PropertiesGroupDefinition => _propertiesGrouping switch + { + Grouping.Categories => new() { Selector = (p) => p.PropertyInfo.GetCustomAttribute()?.Name ?? "Misc" }, + Grouping.Inheritance => new() { Selector = (p) => BaseDefinitionClass(p.PropertyInfo) }, + _ => null + }; + + // -- Grouping properties by inheritance ------------------------------------------------------------------------------------------ + + private static Type BaseDefinitionClass(MethodInfo m) => m.GetBaseDefinition().DeclaringType; + + private static Type BaseDefinitionClass(PropertyInfo p) => BaseDefinitionClass(p.GetMethod ?? p.SetMethod); // used for grouping properties + + private static bool IsOverridden(MethodInfo m) => m.GetBaseDefinition().DeclaringType != m.DeclaringType; + + private static bool IsOverridden(PropertyInfo p) => IsOverridden(p.GetMethod ?? p.SetMethod); // used for the "overridden" chip + + #endregion + } +} diff --git a/src/Cropper.Blazor/Client.V1/Components/Docs/DocsPage.razor b/src/Cropper.Blazor/Client.V1/Components/Docs/DocsPage.razor new file mode 100644 index 00000000..d6a2350e --- /dev/null +++ b/src/Cropper.Blazor/Client.V1/Components/Docs/DocsPage.razor @@ -0,0 +1,38 @@ + + +
+ + @ChildContent + +
+
+ @if (DisplayFooter) + { + +