diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3063d04e..c32f6cd1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,4 +8,9 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" + interval: "daily" + + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 682b622a..37bb5585 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -13,18 +13,14 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup .NET 8 - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - name: Setup .NET 7 + - name: Setup .NET 9 uses: actions/setup-dotnet@v4 with: - dotnet-version: 7.0.x - - name: Setup .NET 6 + dotnet-version: 9.0.x + - name: Setup .NET 8 uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/BlazorAppTest/BlazorAppTest.csproj b/BlazorAppTest/BlazorAppTest.csproj index a038fc4a..05f954d1 100644 --- a/BlazorAppTest/BlazorAppTest.csproj +++ b/BlazorAppTest/BlazorAppTest.csproj @@ -2,8 +2,8 @@ - - net8.0 + + net9.0 enable diff --git a/BlazorAppTest/Pages/HxGridTest.razor b/BlazorAppTest/Pages/HxGridTest.razor index de7b8cd3..f47de314 100644 --- a/BlazorAppTest/Pages/HxGridTest.razor +++ b/BlazorAppTest/Pages/HxGridTest.razor @@ -44,7 +44,7 @@

Clicked context menu item: @clickedItem?.DisplayName

Server paging, server sorting

- + diff --git a/BlazorAppTest/Pages/HxInputDate_Issue859_OnBlur_Test.razor b/BlazorAppTest/Pages/HxInputDate_Issue859_OnBlur_Test.razor new file mode 100644 index 00000000..6e908e5c --- /dev/null +++ b/BlazorAppTest/Pages/HxInputDate_Issue859_OnBlur_Test.razor @@ -0,0 +1,16 @@ +@page "/hxinputdate_issue859_onblur_test" + +

HxInputDate_Issue859_OnBlur_Test

+ + + + +@code { + public DateTime _value { get; set; } + + private Task Test() + { + Console.WriteLine("Test"); + return Task.CompletedTask; + } +} diff --git a/BlazorAppTest/Pages/InputsTest.razor b/BlazorAppTest/Pages/InputsTest.razor index 85bfcb67..070f6ae2 100644 --- a/BlazorAppTest/Pages/InputsTest.razor +++ b/BlazorAppTest/Pages/InputsTest.razor @@ -90,7 +90,7 @@ @bind-Value="@model.CultureInfoMultiSelectNames" /> - + Submit diff --git a/Directory.Build.props b/Directory.Build.props index c066bcef..0a2ef4f6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,12 +8,14 @@ HAVIT Blazor Library HAVIT Blazor Library true + true enable latest - + true + - 4.6.15 - 1.5.6 + 4.7.2-pre02 + 1.6.0
diff --git a/Directory.Packages.props b/Directory.Packages.props index 2fed9aed..7d922f1d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,50 +1,54 @@ - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - all - runtime; build; native; contentfiles; analyzers - - + + true + true + 8.0.11 + 9.0.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + all + runtime; build; native; contentfiles; analyzers + + \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap.EntifyFrameworkCore.Tests/Grids/GridDataProviderRequestExtensionsTests.cs b/Havit.Blazor.Components.Web.Bootstrap.EntifyFrameworkCore.Tests/Grids/GridDataProviderRequestExtensionsTests.cs new file mode 100644 index 00000000..61a4fe9a --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap.EntifyFrameworkCore.Tests/Grids/GridDataProviderRequestExtensionsTests.cs @@ -0,0 +1,64 @@ +using Microsoft.EntityFrameworkCore.Query.Internal; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Havit.Blazor.Components.Web.Bootstrap.Tests.Grids; + +[TestClass] +public class GridDataProviderRequestExtensionsTests +{ + [TestMethod] + public void GridDataProviderRequestExtensions_ApplyGridDataProviderRequest_EntityFrameworkCore_CanBeCompiledForSqlServer() + { + // Arrange + var gridDataProviderRequest = new GridDataProviderRequest + { + Sorting = new List> + { + new SortingItem(null, item => item.A, Collections.SortDirection.Ascending), + new SortingItem(null, item => item.B, Collections.SortDirection.Descending), + }, + StartIndex = 1, // zero based (skipping first item) + Count = 3 + }; + + var dbContext = new TestDbContext(); + var query = dbContext.Items.ApplyGridDataProviderRequest(gridDataProviderRequest); + +#pragma warning disable EF1001 // Internal EF Core API usage. + + // Act + + // We just want to check if EF Core is able to compile the query. + // We don't want to execute the query against the database (otherwise we need code migrations, database cleanup, etc.) + _ = dbContext.GetService().CompileQuery>(query.Expression, false); + +#pragma warning restore EF1001 // Internal EF Core API usage. + + // Assert + // No exception "(IComparable)...) could not be translated. Either rewrite the query in a form that can be translated, ..." was thrown. + } + + private class TestDbContext : DbContext // nested class + { + public DbSet Items { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + + // fake connection string (we don't need a real database connection for this test) + optionsBuilder.UseSqlServer("Data Source=FAKE"); + } + } + + private class Item // nested class + { + public int Id { get; set; } + + public string A { get; set; } + public int B { get; set; } + } +} diff --git a/Havit.Blazor.Components.Web.Bootstrap.EntifyFrameworkCore.Tests/Havit.Blazor.Components.Web.Bootstrap.EntifyFrameworkCore.Tests.csproj b/Havit.Blazor.Components.Web.Bootstrap.EntifyFrameworkCore.Tests/Havit.Blazor.Components.Web.Bootstrap.EntifyFrameworkCore.Tests.csproj new file mode 100644 index 00000000..f8eb3f93 --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap.EntifyFrameworkCore.Tests/Havit.Blazor.Components.Web.Bootstrap.EntifyFrameworkCore.Tests.csproj @@ -0,0 +1,21 @@ + + + + net9.0;net8.0 + false + enable + true + Exe + + + + + + + + + + + + + diff --git a/Havit.Blazor.Components.Web.Bootstrap.Smart/Havit.Blazor.Components.Web.Bootstrap.Smart.csproj b/Havit.Blazor.Components.Web.Bootstrap.Smart/Havit.Blazor.Components.Web.Bootstrap.Smart.csproj index c0e7f8d6..3b3a2665 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Smart/Havit.Blazor.Components.Web.Bootstrap.Smart.csproj +++ b/Havit.Blazor.Components.Web.Bootstrap.Smart/Havit.Blazor.Components.Web.Bootstrap.Smart.csproj @@ -1,12 +1,16 @@  - net8.0 + net9.0;net8.0 enable 1591;1701;1702;SA1134;BL0007 true + + + + @@ -42,5 +46,18 @@ + + + + <_CssToAttach Include="css\lib.css" /> + <_CssToAttachWithIntermediatePath Include="@(_CssToAttach)"> + $(IntermediateOutputPath)scopedcss\%(Filename).rz.scp.css + + + + + <_ScopedCssCandidateFile Include="@(_CssToAttachWithIntermediatePath->'%(IntermediatePath)')" RelativePath="@(_CssToAttachWithIntermediatePath->'%(Filename).rz.scp.css')" OriginalItemSpec="@(_CssToAttachWithIntermediatePath->'%(Filename)')" /> + + diff --git a/Havit.Blazor.Components.Web.Bootstrap.Smart/css/Ignored.razor b/Havit.Blazor.Components.Web.Bootstrap.Smart/css/Ignored.razor new file mode 100644 index 00000000..4fbecede --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap.Smart/css/Ignored.razor @@ -0,0 +1,5 @@ +@* + This file is not used at runtime. + It exists only so that the build system will activate the logic that bundles scoped CSS files. + It can be removed if there is any other component with CSS isolation in the project. +*@ \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap.Smart/css/Ignored.razor.css b/Havit.Blazor.Components.Web.Bootstrap.Smart/css/Ignored.razor.css new file mode 100644 index 00000000..c72d5f60 --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap.Smart/css/Ignored.razor.css @@ -0,0 +1 @@ +/* Ignored */ \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap.Smart/wwwroot/smart.css b/Havit.Blazor.Components.Web.Bootstrap.Smart/css/lib.css similarity index 100% rename from Havit.Blazor.Components.Web.Bootstrap.Smart/wwwroot/smart.css rename to Havit.Blazor.Components.Web.Bootstrap.Smart/css/lib.css diff --git a/Havit.Blazor.Components.Web.Bootstrap.Tests/Forms/HxInputDateTests.cs b/Havit.Blazor.Components.Web.Bootstrap.Tests/Forms/HxInputDateTests.cs index 27d5d82b..4fb1d416 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Tests/Forms/HxInputDateTests.cs +++ b/Havit.Blazor.Components.Web.Bootstrap.Tests/Forms/HxInputDateTests.cs @@ -71,9 +71,7 @@ public void HxInputDate_NonNullable_EmptyInputShouldRaiseParsingError_Issue892() // Assert Assert.AreEqual(new DateTime(2020, 2, 10), myValue, "Model value should remain unchanged."); -#if NET8_0_OR_GREATER Assert.AreEqual("", cut.Find("input").GetAttribute("value"), "Input value should be empty."); -#endif Assert.IsNotNull(cut.Find($"div.{HxInputBase.InvalidCssClass}")); Assert.AreEqual("TestParsingErrorMessage", cut.Find("div.invalid-feedback").TextContent, "ParsingValidationError should be displayed."); } diff --git a/Havit.Blazor.Components.Web.Bootstrap.Tests/Forms/HxInputNumberTests.cs b/Havit.Blazor.Components.Web.Bootstrap.Tests/Forms/HxInputNumberTests.cs index a76ceee4..5195bb72 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Tests/Forms/HxInputNumberTests.cs +++ b/Havit.Blazor.Components.Web.Bootstrap.Tests/Forms/HxInputNumberTests.cs @@ -21,14 +21,7 @@ public class HxInputNumberTests : BunitTestBase [DataRow("cs-CZ", "15 81,549", 1581.55, "1581,55")] [DataRow("cs-CZ", "1.234", 1.23, "1,23")] // Replace . with , (if possible) [DataRow("en-US", "1.237", 1.24, "1.24")] // rounding -#if NET8_0_OR_GREATER [DataRow("en-US", "abc", null, "abc")] // invalid input -#else - // Blazor bug - missing SetUpdatesAttributeName - // https://github.com/havit/Havit.Blazor/issues/468 - // https://github.com/dotnet/aspnetcore/pull/46434 - [DataRow("en-US", "abc", null, null)] // invalid input -#endif [DataRow("en-US", "", null, null)] // empty input public void HxInputNumber_NullableDecimal_ValueConversions(string culture, string input, double? expectedValue, string expectedInputText) { @@ -75,9 +68,7 @@ public void HxInputNumber_NonNullable_EmptyInputShouldRaiseParsingError_Issue892 // Assert Assert.AreEqual(15, myValue, "Model value should remain unchanged."); -#if NET8_0_OR_GREATER Assert.AreEqual("", cut.Find("input").GetAttribute("value"), "Input value should be empty."); -#endif Assert.IsNotNull(cut.Find($"div.{HxInputBase.InvalidCssClass}")); Assert.AreEqual("TestParsingErrorMessage", cut.Find("div.invalid-feedback").TextContent, "ParsingValidationError should be displayed."); } diff --git a/Havit.Blazor.Components.Web.Bootstrap.Tests/Forms/HxInputTextTests.cs b/Havit.Blazor.Components.Web.Bootstrap.Tests/Forms/HxInputTextTests.cs index 7daf8cc9..858dfc1e 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Tests/Forms/HxInputTextTests.cs +++ b/Havit.Blazor.Components.Web.Bootstrap.Tests/Forms/HxInputTextTests.cs @@ -10,7 +10,6 @@ namespace Havit.Blazor.Components.Web.Bootstrap.Tests.Forms; [TestClass] public class HxInputTextTests : BunitTestBase { -#if NET8_0_OR_GREATER [TestMethod] public void HxInputText_BindingToArrayOfString_Issue874() { @@ -54,8 +53,6 @@ public void HxInputText_BindingToListOfString_Issue874() // Assert Assert.IsFalse(cut.Markup.Contains("maxlength")); } -#endif - [TestMethod] public void HxInputText_BindingToArrayOfModel_Issue874() diff --git a/Havit.Blazor.Components.Web.Bootstrap.Tests/Forms/SearchBox/HxSearchBoxTests.cs b/Havit.Blazor.Components.Web.Bootstrap.Tests/Forms/SearchBox/HxSearchBoxTests.cs new file mode 100644 index 00000000..2a432e23 --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap.Tests/Forms/SearchBox/HxSearchBoxTests.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Havit.Blazor.Components.Web.Bootstrap.Tests.Forms.SearchBox; + +[TestClass] +public class HxSearchBoxTests : BunitTestBase +{ + [TestMethod] + public void HxSearchBox_EnabledFalse_ShouldRenderDisabledAttribute_Issue941() + { + // https://github.com/havit/Havit.Blazor/issues/941 + + // Arrange + RenderFragment componentRenderer = (RenderTreeBuilder builder) => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Enabled", false); + builder.CloseComponent(); + }; + + // Act + var cut = Render(componentRenderer); + + // Assert + Assert.IsTrue(cut.Find("input").HasAttribute("disabled")); + } +} diff --git a/Havit.Blazor.Components.Web.Bootstrap.Tests/GlobalUsings.cs b/Havit.Blazor.Components.Web.Bootstrap.Tests/GlobalUsings.cs new file mode 100644 index 00000000..83dc38e2 --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Bunit; \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap.Tests/Grids/GridDataProviderRequestExtensionsTests.cs b/Havit.Blazor.Components.Web.Bootstrap.Tests/Grids/GridDataProviderRequestExtensionsTests.cs new file mode 100644 index 00000000..2891fa82 --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap.Tests/Grids/GridDataProviderRequestExtensionsTests.cs @@ -0,0 +1,48 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Havit.Blazor.Components.Web.Bootstrap.Tests.Grids; + +[TestClass] +public class GridDataProviderRequestExtensionsTests +{ + [TestMethod] + public void GridDataProviderRequestExtensions_ApplyGridDataProviderRequest_InMemory_AppliesSortingAndPaging() + { + // Arrange + var source = new List + { + new Item{ A = "2", B = 3 }, + new Item{ A = "2", B = 2 }, + new Item{ A = "2", B = 1 }, + new Item{ A = "1", B = 1 }, + new Item{ A = "1", B = 2 }, + new Item{ A = "1", B = 3 } + }.AsQueryable(); + + var gridDataProviderRequest = new GridDataProviderRequest + { + Sorting = new List> + { + new SortingItem(null, item => item.A, Collections.SortDirection.Ascending), + new SortingItem(null, item => item.B, Collections.SortDirection.Descending), + }, + StartIndex = 1, // zero based (skipping first item), to verify paging + Count = 3 // to verify paging + }; + + // Act + var result = source.ApplyGridDataProviderRequest(gridDataProviderRequest).ToList(); + + // Assert + Assert.AreEqual(3, result.Count); + Assert.AreEqual(new Item { A = "1", B = 2 }, result[0]); + Assert.AreEqual(new Item { A = "1", B = 1 }, result[1]); + Assert.AreEqual(new Item { A = "2", B = 3 }, result[2]); + } + + private record Item // record: for comparison purposes + { + public string A { get; set; } + public int B { get; set; } + } +} diff --git a/Havit.Blazor.Components.Web.Bootstrap.Tests/Grids/HxGrid_PreserveSelection_Tests.cs b/Havit.Blazor.Components.Web.Bootstrap.Tests/Grids/HxGrid_PreserveSelection_Tests.cs new file mode 100644 index 00000000..17e3d22d --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap.Tests/Grids/HxGrid_PreserveSelection_Tests.cs @@ -0,0 +1,121 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Havit.Blazor.Components.Web.Bootstrap.Tests.Grids; + +[TestClass] +public class HxGrid_PreserveSelection_Tests : BunitTestBase +{ + [TestMethod] + public void HxGrid_PreserveSelection_false_SelectedItem_ShouldResetWhenItemNoLongerVisible() + { + // Arrange + var items = Enumerable.Range(1, 100).Select(i => new { Id = i, Name = $"Item {i}" }).ToList(); + object selectedItem = items[7]; + + GridDataProviderDelegate dataProvider = (GridDataProviderRequest request) => Task.FromResult(request.ApplyTo(items)); + + var cut = RenderComponent>(parameters => parameters + .Add(p => p.DataProvider, dataProvider) + .Add(p => p.PageSize, 10) // selectedItem visible + .Bind(p => p.SelectedDataItem, selectedItem, newValue => selectedItem = newValue, () => selectedItem) + .Add(p => p.PreserveSelection, false)); + + // Act + cut.SetParametersAndRender(parameters => parameters + .Add(p => p.PageSize, 5)); // selectedItem no longer visible + + // Assert + Assert.IsNull(selectedItem); + } + + [TestMethod] + public void HxGrid_PreserveSelection_false_SelectedItems_ShouldRemoveInvisibleItemsFromSelection() + { + // Arrange + var items = Enumerable.Range(1, 100).Select(i => new { Id = i, Name = $"Item {i}" }).ToList(); + var selectedItems = items[3..7].ToHashSet(); + + GridDataProviderDelegate dataProvider = (GridDataProviderRequest request) => Task.FromResult(request.ApplyTo(items)); + + var cut = RenderComponent>(parameters => parameters + .Add(p => p.DataProvider, dataProvider) + .Add(p => p.PageSize, 10) // selectedItem visible + .Bind(p => p.SelectedDataItems, selectedItems, newValue => selectedItems = newValue, () => selectedItems) + .Add(p => p.PreserveSelection, false)); + + // Act + cut.SetParametersAndRender(parameters => parameters + .Add(p => p.PageSize, 5)); // someItems no longer visible + + // Assert + CollectionAssert.AreEquivalent(items[3..5], selectedItems.ToList()); + } + + [TestMethod] + public async Task HxGrid_PreserveSelection_false_SelectedItem_ShouldPreserveWhenItemVisible() + { + // Arrange + var items = Enumerable.Range(1, 100).Select(i => new { Id = i, Name = $"Item {i}" }).ToList(); + object selectedItem = items[7]; + + GridDataProviderDelegate dataProvider = (GridDataProviderRequest request) => Task.FromResult(request.ApplyTo(items)); + + var cut = RenderComponent>(parameters => parameters + .Add(p => p.DataProvider, dataProvider) + .Add(p => p.PageSize, 10) // selectedItem visible + .Bind(p => p.SelectedDataItem, selectedItem, newValue => selectedItem = newValue, () => selectedItem) + .Add(p => p.PreserveSelection, false)); + + // Act + await cut.InvokeAsync(async () => await cut.Instance.RefreshDataAsync()); + + // Assert + Assert.AreSame(items[7], selectedItem); + } + + [TestMethod] + public void HxGrid_PreserveSelection_true_SelectedItem_ShouldPreserveWhenItemNoLongerVisible() + { + // Arrange + var items = Enumerable.Range(1, 100).Select(i => new { Id = i, Name = $"Item {i}" }).ToList(); + object selectedItem = items[7]; + + GridDataProviderDelegate dataProvider = (GridDataProviderRequest request) => Task.FromResult(request.ApplyTo(items)); + + var cut = RenderComponent>(parameters => parameters + .Add(p => p.DataProvider, dataProvider) + .Add(p => p.PageSize, 10) // selectedItem visible + .Bind(p => p.SelectedDataItem, selectedItem, newValue => selectedItem = newValue, () => selectedItem) + .Add(p => p.PreserveSelection, true)); + + // Act + cut.SetParametersAndRender(parameters => parameters + .Add(p => p.PageSize, 5)); // selectedItem no longer visible + + // Assert + Assert.AreSame(items[7], selectedItem); + } + + [TestMethod] + public void HxGrid_PreserveSelection_true_SelectedItems_ShouldPreserveWhenItemsNoLongerVisible() + { + // Arrange + var items = Enumerable.Range(1, 100).Select(i => new { Id = i, Name = $"Item {i}" }).ToList(); + var selectedItems = items[3..7].ToHashSet(); + + GridDataProviderDelegate dataProvider = (GridDataProviderRequest request) => Task.FromResult(request.ApplyTo(items)); + + var cut = RenderComponent>(parameters => parameters + .Add(p => p.DataProvider, dataProvider) + .Add(p => p.PageSize, 10) // selectedItem visible + .Bind(p => p.SelectedDataItems, selectedItems, newValue => selectedItems = newValue, () => selectedItems) + .Add(p => p.PreserveSelection, true)); + + // Act + cut.SetParametersAndRender(parameters => parameters + .Add(p => p.PageSize, 5)); // someItems no longer visible + + // Assert + CollectionAssert.AreEquivalent(items[3..7], selectedItems.ToList()); + } +} diff --git a/Havit.Blazor.Components.Web.Bootstrap.Tests/Grids/HxGridTests.cs b/Havit.Blazor.Components.Web.Bootstrap.Tests/Grids/HxGrid_SortingIcons_Tests.cs similarity index 99% rename from Havit.Blazor.Components.Web.Bootstrap.Tests/Grids/HxGridTests.cs rename to Havit.Blazor.Components.Web.Bootstrap.Tests/Grids/HxGrid_SortingIcons_Tests.cs index c4b4d6bf..983bf1cd 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Tests/Grids/HxGridTests.cs +++ b/Havit.Blazor.Components.Web.Bootstrap.Tests/Grids/HxGrid_SortingIcons_Tests.cs @@ -4,7 +4,7 @@ namespace Havit.Blazor.Components.Web.Bootstrap.Tests.Grids; [TestClass] -public class HxGridTests +public class HxGrid_SortingIcons_Tests { #region Showing icon "on hover", direction should be the same which will be used when a user clicks. diff --git a/Havit.Blazor.Components.Web.Bootstrap.Tests/Havit.Blazor.Components.Web.Bootstrap.Tests.csproj b/Havit.Blazor.Components.Web.Bootstrap.Tests/Havit.Blazor.Components.Web.Bootstrap.Tests.csproj index 89f537f3..4ecaa5a1 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Tests/Havit.Blazor.Components.Web.Bootstrap.Tests.csproj +++ b/Havit.Blazor.Components.Web.Bootstrap.Tests/Havit.Blazor.Components.Web.Bootstrap.Tests.csproj @@ -1,7 +1,7 @@  - net8.0;net6.0 + net9.0;net8.0 false enable true diff --git a/Havit.Blazor.Components.Web.Bootstrap/Buttons/HxButton.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Buttons/HxButton.razor.cs index af568a44..25a2616b 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Buttons/HxButton.razor.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Buttons/HxButton.razor.cs @@ -195,11 +195,6 @@ static HxButton() /// [Parameter(CaptureUnmatchedValues = true)] public Dictionary AdditionalAttributes { get; set; } - /// - /// Localization service. - /// - [Inject] protected IStringLocalizerFactory StringLocalizerFactory { get; set; } - protected bool SpinnerEffective => Spinner ?? clickInProgress; protected bool DisabledEffective => !CascadeEnabledComponent.EnabledEffective(this) || (SingleClickProtection && clickInProgress && (OnClick.HasDelegate || OnValidClick.HasDelegate || OnInvalidClick.HasDelegate)); @@ -237,8 +232,6 @@ protected string GetTooltipWrapperCssClass() private async Task HandleClick(MouseEventArgs mouseEventArgs) { - Contract.Requires(!DisabledEffective, $"The {GetType().Name} component is in a disabled state."); - if (!clickInProgress || !SingleClickProtection) { clickInProgress = true; diff --git a/Havit.Blazor.Components.Web.Bootstrap/Buttons/HxButton.razor.css b/Havit.Blazor.Components.Web.Bootstrap/Buttons/HxButton.razor.css index e3472794..107b1c7b 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Buttons/HxButton.razor.css +++ b/Havit.Blazor.Components.Web.Bootstrap/Buttons/HxButton.razor.css @@ -1,4 +1,4 @@ -.btn:not(.navbar-toggler) { +.btn:not(.navbar-toggler):not(.placeholder) { display: inline-flex; align-items: center; justify-content: center; diff --git a/Havit.Blazor.Components.Web.Bootstrap/Calendar/HxCalendar.razor b/Havit.Blazor.Components.Web.Bootstrap/Calendar/HxCalendar.razor index 2f17afd8..8c204e50 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Calendar/HxCalendar.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Calendar/HxCalendar.razor @@ -48,7 +48,7 @@ @foreach (WeekData week in _renderData.Weeks) { - + @foreach (DayData day in week.Days) { diff --git a/Havit.Blazor.Components.Web.Bootstrap/Calendar/HxCalendar.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Calendar/HxCalendar.razor.cs index 88a49cc5..0d32d29c 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Calendar/HxCalendar.razor.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Calendar/HxCalendar.razor.cs @@ -1,5 +1,4 @@ using System.Globalization; -using Microsoft.Extensions.DependencyInjection; namespace Havit.Blazor.Components.Web.Bootstrap; @@ -210,7 +209,8 @@ private void UpdateRenderData() for (var week = 0; week < 6; week++) { - WeekData weekData = new WeekData(); + var weekData = new WeekData(); + weekData.Key = week; weekData.Days = new List(7); for (int day = 0; day < 7; day++) @@ -318,6 +318,7 @@ private class RenderData private class WeekData { + public int Key { get; set; } public List Days { get; set; } } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Collapse/HxCollapse.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Collapse/HxCollapse.razor.cs index cda92172..4e6c72ad 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Collapse/HxCollapse.razor.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Collapse/HxCollapse.razor.cs @@ -128,6 +128,11 @@ protected override void OnInitialized() /// public async Task ShowAsync() { + if (_isShown) + { + return; + } + if (_initialized) { await EnsureJsModuleAsync(); @@ -145,6 +150,11 @@ public async Task ShowAsync() /// public async Task HideAsync() { + if (!_isShown) + { + return; + } + await EnsureJsModuleAsync(); _hideInProgress = true; await _jsModule.InvokeVoidAsync("hide", _collapseHtmlElement); diff --git a/Havit.Blazor.Components.Web.Bootstrap/ContextMenus/HxContextMenu.razor.css b/Havit.Blazor.Components.Web.Bootstrap/ContextMenus/HxContextMenu.razor.css index 3906cf3f..8336c5f6 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/ContextMenus/HxContextMenu.razor.css +++ b/Havit.Blazor.Components.Web.Bootstrap/ContextMenus/HxContextMenu.razor.css @@ -1,8 +1,12 @@ .hx-context-menu-btn { + display: flex; + align-items: center; + justify-content: center; color: var(--hx-context-menu-button-color); border: var(--hx-context-menu-button-border); border-radius: var(--hx-context-menu-button-border-radius); padding: var(--hx-context-menu-button-padding); + font-size: var(--hx-context-menu-button-font-size); } .hx-context-menu-btn:hover { diff --git a/Havit.Blazor.Components.Web.Bootstrap/Dropdowns/HxDropdownToggleElement.cs b/Havit.Blazor.Components.Web.Bootstrap/Dropdowns/HxDropdownToggleElement.cs index 0ddac9b6..f0f9ebf3 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Dropdowns/HxDropdownToggleElement.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Dropdowns/HxDropdownToggleElement.cs @@ -145,9 +145,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) // TODO VSTHRD101 via RuntimeHelpers.CreateInferredBindSetter? builder.AddAttribute(11, "onchange", EventCallback.Factory.CreateBinder(this, async (string value) => await InvokeValueChangedAsync(value), Value)); #pragma warning restore VSTHRD101 // Avoid unsupported async delegates -#if NET8_0_OR_GREATER builder.SetUpdatesAttributeName("value"); -#endif } builder.AddMultipleAttributes(99, AdditionalAttributes); diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/HxAutosuggest.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/HxAutosuggest.cs index b0533db0..e3545d91 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/HxAutosuggest.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/HxAutosuggest.cs @@ -115,7 +115,7 @@ public class HxAutosuggest : HxInputBase, IInputWithSize, /// [Parameter] public Func> ItemFromValueResolver { get; set; } - protected override LabelValueRenderOrder RenderOrder => (LabelType == Bootstrap.LabelType.Floating) ? LabelValueRenderOrder.ValueOnly /* Label rendered by HxAutosuggestInternal */ : LabelValueRenderOrder.LabelValue; + protected override LabelValueRenderOrder RenderOrder => (LabelTypeEffective == Bootstrap.LabelType.Floating) ? LabelValueRenderOrder.ValueOnly /* Label rendered by HxAutosuggestInternal */ : LabelValueRenderOrder.LabelValue; /// /// The input-group at the beginning of the input. @@ -174,9 +174,7 @@ protected override void BuildRenderInput(RenderTreeBuilder builder) builder.AddAttribute(1023, nameof(HxAutosuggestInternal.InputGroupStartTemplate), InputGroupStartTemplate); builder.AddAttribute(1024, nameof(HxAutosuggestInternal.InputGroupEndText), InputGroupEndText); builder.AddAttribute(1025, nameof(HxAutosuggestInternal.InputGroupEndTemplate), InputGroupEndTemplate); -#if NET8_0_OR_GREATER builder.AddAttribute(1026, nameof(HxAutosuggestInternal.NameAttributeValue), NameAttributeValue); -#endif builder.AddMultipleAttributes(2000, AdditionalAttributes); diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInputInternal.razor b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInputInternal.razor index 6d2547b6..f5ff3043 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInputInternal.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInputInternal.razor @@ -6,7 +6,7 @@ type="text" class="@CssClass hx-autosuggest-input" disabled="@(!EnabledEffective)" - autocomplete="false" + autocomplete="off" data-bs-reference="parent" data-bs-offset="@($"{DropdownOffset.Skidding},{DropdownOffset.Distance}")" value="@Value" diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxCheckbox.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxCheckbox.cs index aba50774..419750f6 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxCheckbox.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxCheckbox.cs @@ -97,9 +97,7 @@ protected override void BuildRenderInput(RenderTreeBuilder builder) BuildRenderInput_AddCommonAttributes(builder, "checkbox"); builder.AddAttribute(1000, "checked", BindConverter.FormatValue(CurrentValue)); builder.AddAttribute(1001, "onchange", value: EventCallback.Factory.CreateBinder(this, value => CurrentValue = value, CurrentValue)); -#if NET8_0_OR_GREATER builder.SetUpdatesAttributeName("checked"); -#endif builder.AddEventStopPropagationAttribute(1002, "onclick", true); builder.AddElementReferenceCapture(1003, elementReference => InputElement = elementReference); builder.CloseElement(); // input @@ -133,6 +131,20 @@ protected override void RenderChipLabel(RenderTreeBuilder builder) protected override void RenderChipValue(RenderTreeBuilder builder) { - builder.AddContent(0, CurrentValue ? Localizer["ChipValueTrue"] : Localizer["ChipValueFalse"]); + // #886 [HxCheckbox] [HxSwitch] Use Text instead of Yes/No for chips + // If both Text and Label are set, use Text for the positive chip value. + // BTW: Negative value is currently never used as chip is rendered only if the value is not equal to default(TValue). + // This might need additional attention if we implement support for three-state checkboxes + // or allow setting neutral value other than default(TValue). + string positiveValue; + if (!String.IsNullOrWhiteSpace(Text) && !String.IsNullOrWhiteSpace(Label)) + { + positiveValue = Text; + } + else + { + positiveValue = Localizer["ChipValueTrue"]; + } + builder.AddContent(0, CurrentValue ? positiveValue : Localizer["ChipValueFalse"]); } } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputBase.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputBase.cs index 0080c88a..bc810029 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputBase.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputBase.cs @@ -313,12 +313,10 @@ private protected virtual void BuildRenderInput_AddCommonAttributes(RenderTreeBu { builder.AddMultipleAttributes(1, AdditionalAttributes); builder.AddAttribute(2, "id", InputId); -#if NET8_0_OR_GREATER if (!String.IsNullOrEmpty(NameAttributeValue)) { builder.AddAttribute(3, "name", NameAttributeValue); } -#endif builder.AddAttribute(4, "type", typeValue); builder.AddAttribute(5, "class", GetInputCssClassToRender()); builder.AddAttribute(6, "disabled", !EnabledEffective); diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputDateRange.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputDateRange.cs index b4198073..f408332a 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputDateRange.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputDateRange.cs @@ -65,6 +65,20 @@ static HxInputDateRange() protected InputSize InputSizeEffective => InputSize ?? GetSettings()?.InputSize ?? GetDefaults()?.InputSize ?? HxSetup.Defaults.InputSize; InputSize IInputWithSize.InputSizeEffective => InputSizeEffective; + /// + /// Placeholder for the start-date input. + /// If not set, localized default is used ("From" + localizations). + /// + [Parameter] public string FromPlaceholder { get; set; } + public string FromPlaceholderEffective => FromPlaceholder ?? GetSettings()?.FromPlaceholder ?? GetDefaults().FromPlaceholder; // null = use localizations + + /// + /// Placeholder for the end-date input. + /// If not set, localized default is used ("End" + localizations). + /// + [Parameter] public string ToPlaceholder { get; set; } + public string ToPlaceholderEffective => ToPlaceholder ?? GetSettings()?.ToPlaceholder ?? GetDefaults().ToPlaceholder; // null = use localizations + /// /// Gets or sets the error message used when displaying a "from" parsing error. /// Used with String.Format(...), {0} is replaced by the Label property, {1} is replaced by the name of the bounded property. @@ -152,6 +166,8 @@ protected virtual void BuildRenderInputCore(RenderTreeBuilder builder) builder.AddAttribute(203, nameof(HxInputDateRangeInternal.InputSizeEffective), InputSizeEffective); builder.AddAttribute(204, nameof(HxInputDateRangeInternal.CalendarIconEffective), CalendarIconEffective); builder.AddAttribute(205, nameof(HxInputDateRangeInternal.EnabledEffective), EnabledEffective); + builder.AddAttribute(206, nameof(HxInputDateRangeInternal.FromPlaceholderEffective), FromPlaceholderEffective); + builder.AddAttribute(206, nameof(HxInputDateRangeInternal.ToPlaceholderEffective), ToPlaceholderEffective); builder.AddAttribute(206, nameof(HxInputDateRangeInternal.FromParsingErrorMessageEffective), GetFromParsingErrorMessage()); builder.AddAttribute(207, nameof(HxInputDateRangeInternal.ToParsingErrorMessageEffective), GetToParsingErrorMessage()); builder.AddAttribute(208, nameof(HxInputDateRangeInternal.ValidationMessageModeEffective), ValidationMessageModeEffective); diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputNumber.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputNumber.cs index 0bf6bbc3..b86b5901 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputNumber.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputNumber.cs @@ -70,6 +70,13 @@ public class HxInputNumber : HxInputBaseWithInputGroups, IInputW [Parameter] public InputMode? InputMode { get; set; } protected InputMode? InputModeEffective => InputMode ?? GetSettings()?.InputMode ?? GetDefaults()?.InputMode; + /// + /// Allows switching between textual and numeric input types. + /// Only (default) and are supported. + /// + [Parameter] public InputType? Type { get; set; } = InputType.Text; + protected InputType TypeEffective => Type ?? GetSettings()?.Type ?? GetDefaults()?.Type ?? InputType.Text; + /// /// Placeholder for the input. /// @@ -160,11 +167,21 @@ public HxInputNumber() protected bool forceRenderValue = false; private int _valueSequenceOffset = 0; + protected override void OnParametersSet() + { + if ((TypeEffective != InputType.Text) && (TypeEffective != InputType.Number)) + { + throw new InvalidOperationException($"Only {nameof(InputType)}.{nameof(InputType.Text)} and {nameof(InputType)}.{nameof(InputType.Number)} are supported for {nameof(Type)} parameter."); + } + + base.OnParametersSet(); + } + /// protected override void BuildRenderInput(RenderTreeBuilder builder) { builder.OpenElement(0, "input"); - BuildRenderInput_AddCommonAttributes(builder, "text"); + BuildRenderInput_AddCommonAttributes(builder, TypeEffective.ToString().ToLowerInvariant()); var inputMode = InputModeEffective; if ((inputMode is null) && (DecimalsEffective == 0)) @@ -181,18 +198,14 @@ protected override void BuildRenderInput(RenderTreeBuilder builder) builder.AddAttribute(1002, "onfocus", "this.select();"); } builder.AddAttribute(1003, "onchange", EventCallback.Factory.CreateBinder(this, value => CurrentValueAsString = value, CurrentValueAsString)); -#if NET8_0_OR_GREATER builder.SetUpdatesAttributeName("value"); -#endif // Normalization of pasted value (applied only if the pasted value differs from the normalized value) // - If we cancel the original paste event, Blazor does not recognize the change, so we fire change event manually. // - This is kind of hack and causes the input to behave slightly differently when normalizing the value (the value is accepted immediately, not after the user leaves the input) // - If this turns out to be a problem, we will have to implement the normalization in the Blazor-handled @onpaste event builder.AddAttribute(1004, "onpaste", @"var clipboardValue = event.clipboardData.getData('text/plain'); var normalizedValue = clipboardValue.replace(/[^\d.,\-eE]/g, ''); if (+clipboardValue != +normalizedValue) { this.value = normalizedValue; this.dispatchEvent(new Event('change')); return false; }"); -#if NET8_0_OR_GREATER builder.SetUpdatesAttributeName("value"); -#endif builder.AddEventStopPropagationAttribute(1004, "onclick", true); @@ -219,17 +232,24 @@ protected override void BuildRenderInput(RenderTreeBuilder builder) /// protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage) { - CultureInfo culture = CultureInfo.CurrentCulture; + var culture = CultureInfo.CurrentCulture; + if (TypeEffective == InputType.Number) + { + culture = CultureInfo.InvariantCulture; + } string workingValue = value; bool success = true; result = default; - // replace . with , - if ((culture.NumberFormat.NumberDecimalSeparator == ",") // when decimal separator is , - && (culture.NumberFormat.NumberGroupSeparator != ".")) // and . is NOT used as group separator) + if (TypeEffective == InputType.Text) { - workingValue = workingValue.Replace(".", ","); + // replace . with , + if ((culture.NumberFormat.NumberDecimalSeparator == ",") // when decimal separator is , + && (culture.NumberFormat.NumberGroupSeparator != ".")) // and . is NOT used as group separator) + { + workingValue = workingValue.Replace(".", ","); + } } // limitation of the number of decimal places @@ -287,6 +307,12 @@ protected override bool TryParseValueFromString(string value, out TValue result, /// A string representation of the value. protected override string FormatValueAsString(TValue value) { + var culture = CultureInfo.CurrentCulture; + if (TypeEffective == InputType.Number) + { + culture = CultureInfo.InvariantCulture; + } + // integer types switch (value) { @@ -295,29 +321,29 @@ protected override string FormatValueAsString(TValue value) // mostly used first case int @int: - return @int.ToString(CultureInfo.CurrentCulture); + return @int.ToString(culture); case long @long: - return @long.ToString(CultureInfo.CurrentCulture); + return @long.ToString(culture); case short @short: - return @short.ToString(CultureInfo.CurrentCulture); + return @short.ToString(culture); case byte @byte: - return @byte.ToString(CultureInfo.CurrentCulture); + return @byte.ToString(culture); // signed/unsigned integer variants case sbyte @sbyte: - return @sbyte.ToString(CultureInfo.CurrentCulture); + return @sbyte.ToString(culture); case ushort @ushort: - return @ushort.ToString(CultureInfo.CurrentCulture); + return @ushort.ToString(culture); case uint @uint: - return @uint.ToString(CultureInfo.CurrentCulture); + return @uint.ToString(culture); case ulong @ulong: - return @ulong.ToString(CultureInfo.CurrentCulture); + return @ulong.ToString(culture); } // floating-point types @@ -328,13 +354,13 @@ protected override string FormatValueAsString(TValue value) switch (value) { case float @float: - return @float.ToString(format, CultureInfo.CurrentCulture); + return @float.ToString(format, culture); case double @double: - return @double.ToString(format, CultureInfo.CurrentCulture); + return @double.ToString(format, culture); case decimal @decimal: - return @decimal.ToString(format, CultureInfo.CurrentCulture); + return @decimal.ToString(format, culture); } throw new InvalidOperationException($"Unsupported type {value.GetType()}."); diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputNumber.nongeneric.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputNumber.nongeneric.cs index fb1878c1..495e642b 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputNumber.nongeneric.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputNumber.nongeneric.cs @@ -15,6 +15,7 @@ static HxInputNumber() { Defaults = new InputNumberSettings() { + Type = InputType.Text, SelectOnFocus = true }; } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputRange.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputRange.cs index 92ef4352..96db68a6 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputRange.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputRange.cs @@ -90,9 +90,7 @@ protected override void BuildRenderInput(RenderTreeBuilder builder) // TODO VSTHRD101 via RuntimeHelpers.CreateInferredBindSetter? builder.AddAttribute(5, BindEventEffective.ToEventName(), EventCallback.Factory.CreateBinder(this, async value => await HandleValueChanged(value), Value)); #pragma warning restore VSTHRD101 // Avoid unsupported async delegates -#if NET8_0_OR_GREATER builder.SetUpdatesAttributeName("value"); -#endif builder.AddAttribute(10, "min", Min); builder.AddAttribute(11, "max", Max); @@ -101,12 +99,10 @@ protected override void BuildRenderInput(RenderTreeBuilder builder) builder.AddAttribute(20, "disabled", !EnabledEffective); builder.AddAttribute(30, "id", InputId); -#if NET8_0_OR_GREATER if (!String.IsNullOrEmpty(NameAttributeValue)) { builder.AddAttribute(31, "name", NameAttributeValue); } -#endif // Capture ElementReference to the input to make focusing it programmatically possible. builder.AddElementReferenceCapture(40, value => InputElement = value); diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputText.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputText.cs index da8c7ab1..d2d0665b 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputText.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputText.cs @@ -30,9 +30,24 @@ static HxInputText() /// [Parameter] public InputType Type { get; set; } = InputType.Text; + protected override void OnParametersSet() + { + if ((Type != InputType.Text) + && (Type != InputType.Email) + && (Type != InputType.Tel) + && (Type != InputType.Search) + && (Type != InputType.Password) + && (Type != InputType.Url)) + { + throw new InvalidOperationException($"Unsupported {nameof(Type)} parameter value {Type}."); + } + + base.OnParametersSet(); + } + /// private protected override string GetElementName() => "input"; /// - private protected override string GetTypeAttributeValue() => Type.ToString().ToLower(); + private protected override string GetTypeAttributeValue() => Type.ToString().ToLowerInvariant(); } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputTextBase.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputTextBase.cs index 84a25115..656b9858 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputTextBase.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxInputTextBase.cs @@ -88,13 +88,11 @@ protected override void BuildRenderInput(RenderTreeBuilder builder) } builder.AddAttribute(1002, "value", CurrentValueAsString); builder.AddAttribute(1003, BindEvent.ToEventName(), EventCallback.Factory.CreateBinder(this, value => CurrentValueAsString = value, CurrentValueAsString)); -#if NET8_0_OR_GREATER builder.SetUpdatesAttributeName("value"); if (!String.IsNullOrEmpty(NameAttributeValue)) { builder.AddAttribute(1004, "name", NameAttributeValue); } -#endif if (InputModeEffective is not null) { diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxRadioButtonListBase.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxRadioButtonListBase.cs index d5146271..24debcfd 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxRadioButtonListBase.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxRadioButtonListBase.cs @@ -157,9 +157,7 @@ protected void BuildRenderInput_RenderRadioItem(RenderTreeBuilder builder, int i builder.AddAttribute(207, "disabled", !CascadeEnabledComponent.EnabledEffective(this)); int j = index; builder.AddAttribute(208, "onclick", EventCallback.Factory.Create(this, () => HandleInputClick(j))); -#if NET8_0_OR_GREATER builder.SetUpdatesAttributeName("checked"); -#endif builder.AddEventStopPropagationAttribute(209, "onclick", true); builder.AddMultipleAttributes(250, AdditionalAttributes); builder.CloseElement(); // input diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/InputDateRangeSettings.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/InputDateRangeSettings.cs index d74d8b09..77e73490 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/InputDateRangeSettings.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/InputDateRangeSettings.cs @@ -12,6 +12,16 @@ public record InputDateRangeSettings : InputSettings /// public InputSize? InputSize { get; set; } + /// + /// Placeholder for the start-date input. + /// + public string FromPlaceholder { get; set; } + + /// + /// Placeholder for the end-date input. + /// + public string ToPlaceholder { get; set; } + /// /// Optional icon to display within the input. /// diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/InputNumberSettings.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/InputNumberSettings.cs index 4faa7a52..8a6e66e4 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/InputNumberSettings.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/InputNumberSettings.cs @@ -20,6 +20,12 @@ public record InputNumberSettings : InputSettings /// public InputMode? InputMode { get; set; } + /// + /// Allows switching between textual and numeric input types. + /// Only (default) and are supported. + /// + public InputType? Type { get; set; } + /// /// Determines whether all the content within the input field is automatically selected when it receives focus. /// diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxInputDateInternal.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxInputDateInternal.razor.cs index 6f5d2c9e..b45d8352 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxInputDateInternal.razor.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxInputDateInternal.razor.cs @@ -75,68 +75,20 @@ public partial class HxInputDateInternal : InputBase, IAsyncDisp protected DateTime GetCalendarDisplayMonthEffective => DateHelper.GetDateTimeFromValue(CurrentValue) ?? CalendarDisplayMonth; -#if !NET8_0_OR_GREATER - private TValue _previousValue; - private bool _previousParsingAttemptFailed; - private ValidationMessageStore _validationMessageStore; -#endif - private HxDropdownToggleElement _hxDropdownToggleElement; private ElementReference _iconWrapperElement; private IJSObjectReference _jsModule; private bool _firstRenderCompleted; -#if !NET8_0_OR_GREATER - protected override void OnParametersSet() - { - base.OnParametersSet(); - - _validationMessageStore ??= new ValidationMessageStore(EditContext); - - // clear parsing error after new value is set - if (!EqualityComparer.Default.Equals(_previousValue, Value)) - { - ClearPreviousParsingMessage(); - _previousValue = Value; - } - } -#endif - protected override string FormatValueAsString(TValue value) => HxInputDate.FormatValue(value); private void HandleValueChanged(string newInputValue) { -#if NET8_0_OR_GREATER CurrentValueAsString = newInputValue; -#else - // HandleValueChanged is used instead of TryParseValueFromString - // When TryParseValueFromString is used (pre net8), invalid input is replaced by previous value. - bool parsingFailed; - _validationMessageStore.Clear(FieldIdentifier); - - if (DateHelper.TryParseDateFromString(newInputValue, TimeProviderEffective, out var date)) - { - parsingFailed = false; - CurrentValue = date; - } - else - { - parsingFailed = true; - _validationMessageStore.Add(FieldIdentifier, ParsingErrorMessageEffective); - } - - // We can skip the validation notification if we were previously valid and still are - if (parsingFailed || _previousParsingAttemptFailed) - { - EditContext.NotifyValidationStateChanged(); - _previousParsingAttemptFailed = parsingFailed; - } -#endif } protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage) { -#if NET8_0_OR_GREATER if (DateHelper.TryParseDateFromString(value, TimeProviderEffective, out var date)) { result = date; @@ -149,9 +101,6 @@ protected override bool TryParseValueFromString(string value, out TValue result, validationErrorMessage = ParsingErrorMessageEffective; return false; } -#else - throw new NotSupportedException(); -#endif } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -204,39 +153,12 @@ protected async Task HandleCustomDateClick(DateTime value) protected void SetCurrentDate(DateTime? date) { -#if NET8_0_OR_GREATER CurrentValueAsString = date?.ToShortDateString(); // we need to trigger the logic in CurrentValueAsString setter -#else - if (date == null) - { - CurrentValue = default; - } - else - { - CurrentValue = DateHelper.GetValueFromDateTimeOffset(new DateTimeOffset(DateTime.SpecifyKind(date.Value, DateTimeKind.Unspecified), TimeSpan.Zero)); - } - ClearPreviousParsingMessage(); -#endif } -#if !NET8_0_OR_GREATER - private void ClearPreviousParsingMessage() - { - if (_previousParsingAttemptFailed) - { - _previousParsingAttemptFailed = false; - EditContext.NotifyValidationStateChanged(); - } - } -#endif - private string GetNameAttributeValue() { -#if NET8_0_OR_GREATER return String.IsNullOrEmpty(NameAttributeValue) ? null : NameAttributeValue; -#else - return null; -#endif } /// @@ -249,10 +171,6 @@ public async ValueTask DisposeAsync() protected virtual async ValueTask DisposeAsyncCore() { -#if !NET8_0_OR_GREATER - _validationMessageStore?.Clear(); -#endif - try { if (_firstRenderCompleted) diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxInputDateRangeInternal.razor b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxInputDateRangeInternal.razor index 3b5ff880..aa470a52 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxInputDateRangeInternal.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxInputDateRangeInternal.razor @@ -20,7 +20,7 @@ Caret="false" Value="@(_fromPreviousParsingAttemptFailed ? _incomingFromValueBeforeParsing : FormatDate(Value.StartDate))" ValueChanged="HandleFromChanged" - placeholder="@StringLocalizerFactory.GetLocalizedValue("From", typeof(HxInputDateRange))" + placeholder="@(FromPlaceholderEffective ?? StringLocalizerFactory.GetLocalizedValue("From", typeof(HxInputDateRange)))" disabled="@(!EnabledEffective)" onfocus="this.select();" inputmode="none" @@ -77,7 +77,7 @@ "rounded-start-0")" Value="@(_toPreviousParsingAttemptFailed ? _incomingToValueBeforeParsing : FormatDate(Value.EndDate))" ValueChanged="HandleToChanged" - placeholder="@StringLocalizerFactory.GetLocalizedValue("To", typeof(HxInputDateRange))" + placeholder="@(ToPlaceholderEffective ?? StringLocalizerFactory.GetLocalizedValue("To", typeof(HxInputDateRange)))" disabled="@(!EnabledEffective)" onfocus="this.select()" inputmode="none" diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxInputDateRangeInternal.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxInputDateRangeInternal.razor.cs index 4dfcc4fb..11b96b96 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxInputDateRangeInternal.razor.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxInputDateRangeInternal.razor.cs @@ -20,6 +20,10 @@ public partial class HxInputDateRangeInternal : InputBase, IAsync [Parameter] public bool ShowPredefinedDateRangesEffective { get; set; } [Parameter] public IEnumerable PredefinedDateRangesEffective { get; set; } + [Parameter] public string FromPlaceholderEffective { get; set; } + + [Parameter] public string ToPlaceholderEffective { get; set; } + [Parameter] public string FromParsingErrorMessageEffective { get; set; } [Parameter] public string ToParsingErrorMessageEffective { get; set; } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor index 50f3c15e..416744ad 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor @@ -46,7 +46,7 @@ id="@($"{InputId}_filter")" type="text" class="@CssClassHelper.Combine("form-control", InputSizeEffective.AsFormControlCssClass())" - autocomplete="false" + autocomplete="off" value="@_filterText" @oninput="HandleFilterInputChanged" @onclick:stopPropagation diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor.cs index f81ae083..7d425eb7 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor.cs @@ -253,7 +253,11 @@ public Task HandleJsHidden() public async Task HandleJsShown() { _isShown = true; - await _filterInputReference.FocusAsync(); + + if (AllowFiltering) + { + await _filterInputReference.FocusAsync(); + } } public async ValueTask DisposeAsync() diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/SearchBox/HxSearchBox.razor b/Havit.Blazor.Components.Web.Bootstrap/Forms/SearchBox/HxSearchBox.razor index 470e48c6..c390d79c 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/SearchBox/HxSearchBox.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/SearchBox/HxSearchBox.razor @@ -46,7 +46,7 @@ @onfocus="HandleInputFocus" @onblur="HandleInputBlur" inputmode="search" - enabled="@Enabled" + disabled="@(!Enabled)" placeholder="@(LabelTypeEffective == Bootstrap.LabelType.Floating ? "placeholder" : Placeholder)" class="@CssClassHelper.Combine( "form-control", diff --git a/Havit.Blazor.Components.Web.Bootstrap/Grids/GridDataProviderRequestExtensions.cs b/Havit.Blazor.Components.Web.Bootstrap/Grids/GridDataProviderRequestExtensions.cs index d32554bc..37605f87 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Grids/GridDataProviderRequestExtensions.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Grids/GridDataProviderRequestExtensions.cs @@ -1,4 +1,6 @@ -using Havit.Collections; +using System.Linq.Expressions; +using Havit.Collections; +using Havit.Linq.Expressions; namespace Havit.Blazor.Components.Web.Bootstrap; @@ -11,6 +13,16 @@ public static class GridDataProviderRequestExtensions /// request. public static IQueryable ApplyGridDataProviderRequest(this IQueryable source, GridDataProviderRequest gridDataProviderRequest) { + // The expressions used in the sorting are of type IComparable, but we need to convert them to object to be able to use them + // in the Entity Framework Core OrderBy/ThenBy methods. + // Inspired by: https://github.com/dotnet/efcore/issues/30228 + static Expression> ReplaceConvertType(Expression> expression) => + Expression.Lambda>( + Expression.Convert( + expression.Body.RemoveConvert(), + typeof(object)), + expression.Parameters[0]); + gridDataProviderRequest.CancellationToken.ThrowIfCancellationRequested(); // Sorting @@ -19,14 +31,14 @@ public static IQueryable ApplyGridDataProviderRequest(this IQuerya Contract.Assert(gridDataProviderRequest.Sorting.All(item => item.SortKeySelector != null), $"All sorting items must have the {nameof(SortingItem.SortKeySelector)} property set."); IOrderedQueryable orderedDataProvider = (gridDataProviderRequest.Sorting[0].SortDirection == SortDirection.Ascending) - ? source.OrderBy(gridDataProviderRequest.Sorting[0].SortKeySelector) - : source.OrderByDescending(gridDataProviderRequest.Sorting[0].SortKeySelector); + ? source.OrderBy(ReplaceConvertType(gridDataProviderRequest.Sorting[0].SortKeySelector)) + : source.OrderByDescending(ReplaceConvertType(gridDataProviderRequest.Sorting[0].SortKeySelector)); for (int i = 1; i < gridDataProviderRequest.Sorting.Count; i++) { orderedDataProvider = (gridDataProviderRequest.Sorting[i].SortDirection == SortDirection.Ascending) - ? orderedDataProvider.ThenBy(gridDataProviderRequest.Sorting[i].SortKeySelector) - : orderedDataProvider.ThenByDescending(gridDataProviderRequest.Sorting[i].SortKeySelector); + ? orderedDataProvider.ThenBy(ReplaceConvertType(gridDataProviderRequest.Sorting[i].SortKeySelector)) + : orderedDataProvider.ThenByDescending(ReplaceConvertType(gridDataProviderRequest.Sorting[i].SortKeySelector)); } source = orderedDataProvider; } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Grids/GridSettings.cs b/Havit.Blazor.Components.Web.Bootstrap/Grids/GridSettings.cs index 5e211c40..67661882 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Grids/GridSettings.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Grids/GridSettings.cs @@ -21,7 +21,8 @@ public record GridSettings public IconBase SortDescendingIcon { get; set; } /// - /// Height of the item row used for infinite scroll calculations (). + /// Height of the item row (in pixels) used for infinite scroll calculations (). + /// The row height is not applied for other navigation modes, use CSS for that. /// public float? ItemRowHeight { get; set; } @@ -109,4 +110,15 @@ public record GridSettings /// Settings for the "Load more" navigation button ( or ). /// public ButtonSettings LoadMoreButtonSettings { get; set; } + + /// + /// Gets or sets a value indicating whether the current selection (either for single selection + /// or for multiple selection) should be preserved during data operations, such as paging, sorting, filtering, + /// or manual invocation of .
+ ///
+ /// + /// This setting ensures that the selection remains intact during operations that refresh or modify the displayed data in the grid. + /// Note that preserving the selection requires that the underlying data items can still be matched in the updated dataset (e.g., by item1.Equals(item2)). + /// + public bool? PreserveSelection { get; set; } } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor b/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor index dacfc86b..3c94a358 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor @@ -8,12 +8,13 @@ @* To get the components to the collections, we have to let them render with this component. Still we don't want them to render any output. *@ @if (MultiSelectionEnabled) { - bool allDataItemsSelected = (_paginationDataItemsToRender != null) && (SelectedDataItems != null) && (SelectedDataItems.Count > 0) && (_paginationDataItemsToRender.Count == SelectedDataItems.Count); + bool allDataItemsSelected = (_paginationDataItemsToRender != null) && (SelectedDataItems != null) && (SelectedDataItems.Count > 0) && (_paginationDataItemsToRender.All(SelectedDataItems.Contains)); } @@ -69,7 +70,8 @@ - + @{ GridHeaderCellContext gridHeaderCellContext = CreateGridHeaderCellContext(); } @@ -137,6 +139,7 @@ ItemRowCssClassEffective, ItemRowCssClassSelector?.Invoke(item), ((SelectionEnabled && (item != null) && item.Equals(SelectedDataItem)) ? "table-active" : null))" + @attributes="ItemRowAdditionalAttributesSelectorEffective(item)" @onclick="async () => await HandleSelectOrMultiSelectDataItemClick(item)" @onclick:stopPropagation> @@ -156,6 +159,7 @@ @foreach (IHxGridColumn column in columnsToRender) @@ -247,7 +251,8 @@ && shouldRenderFooter) { - + @for (int i = 0; i < footerTemplates.Length; i++) // do not use foreach, we need to use i as a key as footerTemplates can contain duplicates { diff --git a/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor.cs index b9de1fce..9053f6e1 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor.cs @@ -105,6 +105,19 @@ public partial class HxGrid : ComponentBase, IDisposable /// protected virtual Task InvokeSelectedDataItemsChangedAsync(HashSet selectedDataItems) => SelectedDataItemsChanged.InvokeAsync(selectedDataItems); + /// + /// Gets or sets a value indicating whether the current selection (either for single selection + /// or for multiple selection) should be preserved during data operations, such as paging, sorting, filtering, + /// or manual invocation of .
+ /// Default value is false (can be set by using HxGrid.Defaults). + ///
+ /// + /// This setting ensures that the selection remains intact during operations that refresh or modify the displayed data in the grid. + /// Note that preserving the selection requires that the underlying data items can still be matched in the updated dataset (e.g., by item1.Equals(item2)). + /// + [Parameter] public bool? PreserveSelection { get; set; } + protected bool PreserveSelectionEffective => PreserveSelection ?? GetSettings()?.PreserveSelection ?? GetDefaults().PreserveSelection ?? throw new InvalidOperationException(nameof(PreserveSelection) + " default for " + nameof(HxGrid) + " has to be set."); + /// /// The strategy for how data items are displayed and loaded into the grid. Supported modes include pagination, load more, and infinite scroll. /// @@ -197,8 +210,9 @@ public partial class HxGrid : ComponentBase, IDisposable protected string ItemRowCssClassEffective => ItemRowCssClass ?? GetSettings()?.ItemRowCssClass ?? GetDefaults().ItemRowCssClass; /// - /// Height of each item row, used primarily in calculations for infinite scrolling. + /// Height of each item row, used in calculations for infinite scrolling (). /// The default value (41px) corresponds to the typical row height in the Bootstrap 5 default theme. + /// The row height is not applied for other navigation modes, use CSS for that. /// [Parameter] public float? ItemRowHeight { get; set; } protected float ItemRowHeightEffective => ItemRowHeight ?? GetSettings()?.ItemRowHeight ?? GetDefaults().ItemRowHeight ?? throw new InvalidOperationException(nameof(ItemRowHeight) + " default for " + nameof(HxGrid) + " has to be set."); @@ -278,6 +292,62 @@ public partial class HxGrid : ComponentBase, IDisposable [Parameter] public IconBase SortDescendingIcon { get; set; } protected IconBase SortDescendingIconEffective => SortDescendingIcon ?? GetSettings()?.SortDescendingIcon ?? GetDefaults().SortDescendingIcon; + /// + /// Defines a function that returns additional attributes for a specific tr element based on the item it represents. + /// This allows for custom behavior or event handling on a per-row basis. + /// + /// + /// If both and are specified, + /// both dictionaries are combined into one. + /// Note that there is no prevention of duplicate keys, which may result in a . + /// + [Parameter] public Func> ItemRowAdditionalAttributesSelector { get; set; } + + /// + /// Provides a dictionary of additional attributes to apply to all body tr elements in the grid. + /// These attributes can be used to customize the appearance or behavior of rows. + /// + /// + /// If both and are specified, + /// both dictionaries are combined into one. + /// Note that there is no prevention of duplicate keys, which may result in a . + /// + [Parameter] public Dictionary ItemRowAdditionalAttributes { get; set; } + + /// + /// Provides a dictionary of additional attributes to apply to the header tr element of the grid. + /// This allows for custom styling or behavior of the header row. + /// + [Parameter] public Dictionary HeaderRowAdditionalAttributes { get; set; } + + /// + /// Provides a dictionary of additional attributes to apply to the footer tr element of the grid. + /// This allows for custom styling or behavior of the footer row. + /// + [Parameter] public Dictionary FooterRowAdditionalAttributes { get; set; } + + /// + /// Determines the effective additional attributes for a given data row, combining both the global and per-item attributes. + /// + /// The data item for the current row. + /// A dictionary of additional attributes to apply to the row. + /// Thrown when there are duplicate keys in the combined dictionaries. + private Dictionary ItemRowAdditionalAttributesSelectorEffective(TItem item) + { + if (ItemRowAdditionalAttributesSelector == null) + { + return ItemRowAdditionalAttributes; + } + else if (ItemRowAdditionalAttributes == null) + { + return ItemRowAdditionalAttributesSelector(item); + } + else + { + return ItemRowAdditionalAttributes.Concat(ItemRowAdditionalAttributesSelector(item)).ToDictionary(x => x.Key, x => x.Value); + } + } + /// /// Retrieves the default settings for the grid. This method can be overridden in derived classes /// to provide different default settings or to use a derived settings class. @@ -327,7 +397,10 @@ protected override async Task OnParametersSetAsync() Contract.Requires(DataProvider != null, $"Property {nameof(DataProvider)} on {GetType()} must have a value."); Contract.Requires(CurrentUserState != null, $"Property {nameof(CurrentUserState)} on {GetType()} must have a value."); - Contract.Requires(!MultiSelectionEnabled || (ContentNavigationModeEffective != GridContentNavigationMode.InfiniteScroll), $"Cannot use multi selection with infinite scroll on {GetType()}."); + if ((ContentNavigationModeEffective == GridContentNavigationMode.InfiniteScroll) && MultiSelectionEnabled) + { + Contract.Requires(PreserveSelectionEffective, $"{nameof(PreserveSelection)} must be enabled on {nameof(HxGrid)} when using {nameof(GridContentNavigationMode.InfiniteScroll)} with {nameof(MultiSelectionEnabled)}."); + } if (_previousUserState != CurrentUserState) { @@ -524,7 +597,7 @@ private async Task HandleSelectOrMultiSelectDataItemClick(TItem clickedDataItem) } else // MultiSelectionEnabled { - var selectedDataItems = SelectedDataItems?.ToHashSet() ?? new HashSet(); + var selectedDataItems = SelectedDataItems?.ToHashSet() ?? []; if (selectedDataItems.Add(clickedDataItem) // when the item was added || selectedDataItems.Remove(clickedDataItem)) // or removed... But because of || item removal is performed only when the item was not added! { @@ -738,18 +811,21 @@ private async ValueTask RefreshPaginationOrLoadMoreDataCoreAsync(bool forceReloa { _paginationDataItemsToRender = result.Data?.ToList(); - if (!EqualityComparer.Default.Equals(SelectedDataItem, default)) + if (!PreserveSelectionEffective) { - if ((_paginationDataItemsToRender == null) || !_paginationDataItemsToRender.Contains(SelectedDataItem)) + if (!EqualityComparer.Default.Equals(SelectedDataItem, default)) { - await SetSelectedDataItemWithEventCallback(default); + if ((_paginationDataItemsToRender == null) || !_paginationDataItemsToRender.Contains(SelectedDataItem)) + { + await SetSelectedDataItemWithEventCallback(default); + } } - } - if (SelectedDataItems?.Count > 0) - { - HashSet selectedDataItems = _paginationDataItemsToRender?.Intersect(SelectedDataItems).ToHashSet() ?? new HashSet(); - await SetSelectedDataItemsWithEventCallback(selectedDataItems); + if (SelectedDataItems?.Count > 0) + { + HashSet selectedDataItems = _paginationDataItemsToRender?.Intersect(SelectedDataItems).ToHashSet() ?? new HashSet(); + await SetSelectedDataItemsWithEventCallback(selectedDataItems); + } } } else @@ -858,9 +934,8 @@ private async Task HandleMultiSelectSelectDataItemClicked(TItem selectedDataItem private async Task HandleMultiSelectUnselectDataItemClicked(TItem selectedDataItem) { Contract.Requires(MultiSelectionEnabled); - Contract.Requires((ContentNavigationModeEffective == GridContentNavigationMode.Pagination) || (ContentNavigationModeEffective == GridContentNavigationMode.LoadMore) || (ContentNavigationModeEffective == GridContentNavigationMode.PaginationAndLoadMore)); - var selectedDataItems = SelectedDataItems?.ToHashSet() ?? new HashSet(); + var selectedDataItems = SelectedDataItems?.ToHashSet() ?? []; if (selectedDataItems.Remove(selectedDataItem)) { await SetSelectedDataItemsWithEventCallback(selectedDataItems); @@ -870,24 +945,48 @@ private async Task HandleMultiSelectUnselectDataItemClicked(TItem selectedDataIt private async Task HandleMultiSelectSelectAllClicked() { Contract.Requires(MultiSelectionEnabled, nameof(MultiSelectionEnabled)); - Contract.Requires((ContentNavigationModeEffective == GridContentNavigationMode.Pagination) || (ContentNavigationModeEffective == GridContentNavigationMode.LoadMore) || (ContentNavigationModeEffective == GridContentNavigationMode.PaginationAndLoadMore)); if (_paginationDataItemsToRender is null) { - await SetSelectedDataItemsWithEventCallback(new HashSet()); + await SetSelectedDataItemsWithEventCallback([]); } else { - await SetSelectedDataItemsWithEventCallback(new HashSet(_paginationDataItemsToRender)); + if (PreserveSelectionEffective) + { + var selectedDataItems = SelectedDataItems?.ToHashSet() ?? []; + int originalCount = selectedDataItems.Count; + selectedDataItems.UnionWith(_paginationDataItemsToRender); + if (selectedDataItems.Count != originalCount) + { + await SetSelectedDataItemsWithEventCallback(selectedDataItems); + } + } + else + { + await SetSelectedDataItemsWithEventCallback(new HashSet(_paginationDataItemsToRender)); + } } } private async Task HandleMultiSelectSelectNoneClicked() { Contract.Requires(MultiSelectionEnabled); - Contract.Requires((ContentNavigationModeEffective == GridContentNavigationMode.Pagination) || (ContentNavigationModeEffective == GridContentNavigationMode.LoadMore) || (ContentNavigationModeEffective == GridContentNavigationMode.PaginationAndLoadMore)); - await SetSelectedDataItemsWithEventCallback(new HashSet()); + if (PreserveSelectionEffective) + { + var selectedDataItems = SelectedDataItems?.ToHashSet() ?? []; + int originalCount = selectedDataItems.Count; + selectedDataItems.ExceptWith(_paginationDataItemsToRender); + if (selectedDataItems.Count != originalCount) + { + await SetSelectedDataItemsWithEventCallback(selectedDataItems); + } + } + else + { + await SetSelectedDataItemsWithEventCallback([]); + } } #endregion diff --git a/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor.nongeneric.cs b/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor.nongeneric.cs index 410eb228..3d2779ef 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor.nongeneric.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor.nongeneric.cs @@ -24,6 +24,7 @@ static HxGrid() ItemRowHeight = 41, // 41px = row-height of a regular table-row within the Bootstrap 5 default theme OverscanCount = 3, PageSize = 20, + PreserveSelection = false, ProgressIndicatorDelay = 300, // 300ms PlaceholdersRowCount = 5, ShowFooterWhenEmptyData = false, diff --git a/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGridColumnBase.cs b/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGridColumnBase.cs index e5f339c9..012a66fb 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGridColumnBase.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGridColumnBase.cs @@ -41,8 +41,7 @@ protected override void OnInitialized() GridCellTemplate IHxGridColumn.GetFooterCellTemplate(GridFooterCellContext context) => GetFooterCellTemplate(context); /// - SortingItem[] IHxGridColumn.GetSorting() => _sorting ??= GetSorting().ToArray(); - private SortingItem[] _sorting; + SortingItem[] IHxGridColumn.GetSorting() => GetSorting().ToArray(); /// int? IHxGridColumn.GetDefaultSortingOrder() => GetDefaultSortingOrder(); diff --git a/Havit.Blazor.Components.Web.Bootstrap/Grids/Internal/HxMultiSelectGridColumnInternal.cs b/Havit.Blazor.Components.Web.Bootstrap/Grids/Internal/HxMultiSelectGridColumnInternal.cs index 5810ade5..11952865 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Grids/Internal/HxMultiSelectGridColumnInternal.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Grids/Internal/HxMultiSelectGridColumnInternal.cs @@ -2,6 +2,7 @@ public class HxMultiSelectGridColumnInternal : HxGridColumnBase { + [Parameter, EditorRequired] public bool SelectDeselectAllHeaderVisible { get; set; } [Parameter] public HashSet SelectedDataItems { get; set; } [Parameter] public bool AllDataItemsSelected { get; set; } [Parameter] public EventCallback OnSelectAllClicked { get; set; } @@ -18,30 +19,35 @@ public class HxMultiSelectGridColumnInternal : HxGridColumnBase /// protected override GridCellTemplate GetHeaderCellTemplate(GridHeaderCellContext context) { - return new GridCellTemplate + if (SelectDeselectAllHeaderVisible) { - CssClass = "text-center", - Template = (RenderTreeBuilder builder) => + return new GridCellTemplate { - builder.OpenElement(100, "input"); - builder.AddAttribute(101, "type", "checkbox"); - builder.AddAttribute(102, "class", "form-check-input"); + CssClass = "text-center", + Template = (RenderTreeBuilder builder) => + { + builder.OpenElement(100, "input"); + builder.AddAttribute(101, "type", "checkbox"); + builder.AddAttribute(102, "class", "form-check-input"); - builder.AddAttribute(103, "checked", AllDataItemsSelected); - builder.AddAttribute(104, "onchange", EventCallback.Factory.Create(this, HandleSelectAllOrNoneClick)); -#if NET8_0_OR_GREATER - builder.SetUpdatesAttributeName("checked"); -#endif - builder.AddEventStopPropagationAttribute(105, "onclick", true); + builder.AddAttribute(103, "checked", AllDataItemsSelected); + builder.AddAttribute(104, "onchange", EventCallback.Factory.Create(this, HandleSelectAllOrNoneClick)); + builder.SetUpdatesAttributeName("checked"); + builder.AddEventStopPropagationAttribute(105, "onclick", true); - if ((context.TotalCount is null) || (context.TotalCount == 0)) - { - builder.AddAttribute(102, "disabled"); - } + if ((context.TotalCount is null) || (context.TotalCount == 0)) + { + builder.AddAttribute(102, "disabled"); + } - builder.CloseElement(); // input - } - }; + builder.CloseElement(); // input + } + }; + } + else + { + return GridCellTemplate.Empty; + } } /// @@ -59,9 +65,7 @@ protected override GridCellTemplate GetItemCellTemplate(TItem item) bool selected = SelectedDataItems?.Contains(item) ?? false; builder.AddAttribute(103, "checked", selected); builder.AddAttribute(104, "onchange", EventCallback.Factory.Create(this, HandleSelectDataItemClick(item, selected))); -#if NET8_0_OR_GREATER builder.SetUpdatesAttributeName("checked"); -#endif builder.AddEventStopPropagationAttribute(105, "onclick", true); builder.CloseElement(); // input diff --git a/Havit.Blazor.Components.Web.Bootstrap/Havit.Blazor.Components.Web.Bootstrap.csproj b/Havit.Blazor.Components.Web.Bootstrap/Havit.Blazor.Components.Web.Bootstrap.csproj index 996f40e7..5cbb7d34 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Havit.Blazor.Components.Web.Bootstrap.csproj +++ b/Havit.Blazor.Components.Web.Bootstrap/Havit.Blazor.Components.Web.Bootstrap.csproj @@ -1,7 +1,7 @@  - net8.0;net6.0 + net9.0;net8.0 enable @@ -51,4 +51,18 @@ + + + + <_CssToAttach Include="wwwroot\*.lib.css" /> + <_CssToAttachWithIntermediatePath Include="@(_CssToAttach)"> + $(IntermediateOutputPath)scopedcss\%(Filename).rz.scp.css + + + + + <_ScopedCssCandidateFile Include="@(_CssToAttachWithIntermediatePath->'%(IntermediatePath)')" RelativePath="@(_CssToAttachWithIntermediatePath->'%(Filename).rz.scp.css')" OriginalItemSpec="@(_CssToAttachWithIntermediatePath->'%(Filename)')" /> + + + diff --git a/Havit.Blazor.Components.Web.Bootstrap/HxSetup.cs b/Havit.Blazor.Components.Web.Bootstrap/HxSetup.cs index 387018b2..525f73fb 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/HxSetup.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/HxSetup.cs @@ -1,4 +1,6 @@ -namespace Havit.Blazor.Components.Web.Bootstrap; +using System.Diagnostics; + +namespace Havit.Blazor.Components.Web.Bootstrap; public static class HxSetup { @@ -7,6 +9,11 @@ public static class HxSetup /// public static GlobalSettings Defaults { get; } = new GlobalSettings(); + /// + /// Bootstrap version used by the library. + /// + public static string BootstrapVersion = "5.3.3"; + /// /// Renders the <script> tag that references the corresponding Bootstrap JavaScript bundle with Popper.
/// To be used in _Layout.cshtml as @Html.Raw(HxSetup.RenderBootstrapJavaScriptReference()). @@ -30,7 +37,7 @@ public static string RenderBootstrapCssReference(BootstrapFlavor bootstrapFlavor { return bootstrapFlavor switch { - BootstrapFlavor.HavitDefault => "", + BootstrapFlavor.HavitDefault => "", BootstrapFlavor.PlainBootstrap => "", _ => throw new ArgumentOutOfRangeException($"Unknown {nameof(BootstrapFlavor)} value {bootstrapFlavor}.") }; diff --git a/Havit.Blazor.Components.Web.Bootstrap/JSRuntimeExtensions.cs b/Havit.Blazor.Components.Web.Bootstrap/JSRuntimeExtensions.cs index 9a020a1c..133ad3e4 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/JSRuntimeExtensions.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/JSRuntimeExtensions.cs @@ -6,7 +6,11 @@ public static class JSRuntimeExtensions { internal static ValueTask ImportHavitBlazorBootstrapModuleAsync(this IJSRuntime jsRuntime, string moduleNameWithoutExtension) { - var path = "./_content/Havit.Blazor.Components.Web.Bootstrap/" + moduleNameWithoutExtension + ".js?v=" + HxSetup.VersionIdentifierHavitBlazorBootstrap; + var path = "./_content/Havit.Blazor.Components.Web.Bootstrap/" + moduleNameWithoutExtension + ".js"; +#if !NET9_0_OR_GREATER + // pre-NET9 does not support StaticAssets with ImportMap + path = path + "?v=" + HxSetup.VersionIdentifierHavitBlazorBootstrap; +#endif return jsRuntime.InvokeAsync("import", path); } } \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap/Layouts/HxListLayout.razor.css b/Havit.Blazor.Components.Web.Bootstrap/Layouts/HxListLayout.razor.css index 766e0029..d5b5df8f 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Layouts/HxListLayout.razor.css +++ b/Havit.Blazor.Components.Web.Bootstrap/Layouts/HxListLayout.razor.css @@ -25,7 +25,6 @@ } ::deep .hx-grid { - --hx-context-menu-button-border-radius: .25rem; margin-bottom: 0; font-size: var(--hx-list-layout-table-font-size); } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Modals/HxMessageBox.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Modals/HxMessageBox.razor.cs index 6074e9c6..9c8ebac9 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Modals/HxMessageBox.razor.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Modals/HxMessageBox.razor.cs @@ -39,10 +39,18 @@ static HxMessageBox() ///
protected virtual MessageBoxSettings GetDefaults() => Defaults; - ///// - ///// Header icon. - ///// - //[Parameter] public IconBase Icon { get; set; } + /// + /// Set of settings to be applied to the component instance (overrides , overridden by individual parameters). + /// + [Parameter] public MessageBoxSettings Settings { get; set; } + + /// + /// Returns an optional set of component settings. + /// + /// + /// Similar to , enables defining wider in component descendants (by returning a derived settings class). + /// + protected virtual MessageBoxSettings GetSettings() => Settings; /// /// Title text (Header). @@ -89,19 +97,19 @@ static HxMessageBox() /// Settings for the dialog primary button. /// [Parameter] public ButtonSettings PrimaryButtonSettings { get; set; } - protected ButtonSettings PrimaryButtonSettingsEffective => PrimaryButtonSettings ?? GetDefaults().PrimaryButtonSettings ?? throw new InvalidOperationException(nameof(PrimaryButtonSettings) + " default for " + nameof(HxMessageBox) + " has to be set."); + protected ButtonSettings PrimaryButtonSettingsEffective => PrimaryButtonSettings ?? GetSettings()?.PrimaryButtonSettings ?? GetDefaults().PrimaryButtonSettings ?? throw new InvalidOperationException(nameof(PrimaryButtonSettings) + " default for " + nameof(HxMessageBox) + " has to be set."); /// /// Settings for dialog secondary button(s). /// [Parameter] public ButtonSettings SecondaryButtonSettings { get; set; } - protected ButtonSettings SecondaryButtonSettingsEffective => SecondaryButtonSettings ?? GetDefaults().SecondaryButtonSettings ?? throw new InvalidOperationException(nameof(SecondaryButtonSettings) + " default for " + nameof(HxMessageBox) + " has to be set."); + protected ButtonSettings SecondaryButtonSettingsEffective => SecondaryButtonSettings ?? GetSettings()?.SecondaryButtonSettings ?? GetDefaults().SecondaryButtonSettings ?? throw new InvalidOperationException(nameof(SecondaryButtonSettings) + " default for " + nameof(HxMessageBox) + " has to be set."); /// /// Settings for the underlying component. /// [Parameter] public ModalSettings ModalSettings { get; set; } - protected ModalSettings ModalSettingsEffective => ModalSettings ?? GetDefaults().ModalSettings ?? throw new InvalidOperationException(nameof(ModalSettings) + " default for " + nameof(HxMessageBox) + " has to be set."); + protected ModalSettings ModalSettingsEffective => ModalSettings ?? GetSettings()?.ModalSettings ?? GetDefaults().ModalSettings ?? throw new InvalidOperationException(nameof(ModalSettings) + " default for " + nameof(HxMessageBox) + " has to be set."); /// /// Raised when the message box gets closed. Returns the button clicked. @@ -153,62 +161,62 @@ private List GetButtonsToRender() { case MessageBoxButtons.AbortRetryIgnore: // no primary button (if not explicitly requested) - buttons.Add(new() { Id = MessageBoxButtons.Abort, Text = MessageBoxLocalizer["Abort"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Abort) }); - buttons.Add(new() { Id = MessageBoxButtons.Retry, Text = MessageBoxLocalizer["Retry"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Retry) }); - buttons.Add(new() { Id = MessageBoxButtons.Ignore, Text = MessageBoxLocalizer["Ignore"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Ignore) }); + buttons.Add(new() { Id = MessageBoxButtons.Abort, Text = GetButtonTextEffective(MessageBoxButtons.Abort), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Abort) }); + buttons.Add(new() { Id = MessageBoxButtons.Retry, Text = GetButtonTextEffective(MessageBoxButtons.Retry), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Retry) }); + buttons.Add(new() { Id = MessageBoxButtons.Ignore, Text = GetButtonTextEffective(MessageBoxButtons.Ignore), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Ignore) }); break; case MessageBoxButtons.OkCancel: primaryButtonEffective ??= MessageBoxButtons.Ok; - buttons.Add(new() { Id = MessageBoxButtons.Cancel, Text = MessageBoxLocalizer["Cancel"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Cancel) }); - buttons.Add(new() { Id = MessageBoxButtons.Ok, Text = MessageBoxLocalizer["OK"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Ok) }); + buttons.Add(new() { Id = MessageBoxButtons.Cancel, Text = GetButtonTextEffective(MessageBoxButtons.Cancel), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Cancel) }); + buttons.Add(new() { Id = MessageBoxButtons.Ok, Text = GetButtonTextEffective(MessageBoxButtons.Ok), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Ok) }); break; case MessageBoxButtons.YesNo: primaryButtonEffective ??= MessageBoxButtons.Yes; - buttons.Add(new() { Id = MessageBoxButtons.No, Text = MessageBoxLocalizer["No"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.No) }); - buttons.Add(new() { Id = MessageBoxButtons.Yes, Text = MessageBoxLocalizer["Yes"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Yes) }); + buttons.Add(new() { Id = MessageBoxButtons.No, Text = GetButtonTextEffective(MessageBoxButtons.No), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.No) }); + buttons.Add(new() { Id = MessageBoxButtons.Yes, Text = GetButtonTextEffective(MessageBoxButtons.Yes), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Yes) }); break; case MessageBoxButtons.RetryCancel: primaryButtonEffective ??= MessageBoxButtons.Retry; - buttons.Add(new() { Id = MessageBoxButtons.Cancel, Text = MessageBoxLocalizer["Cancel"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Cancel) }); - buttons.Add(new() { Id = MessageBoxButtons.Retry, Text = MessageBoxLocalizer["Retry"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Retry) }); + buttons.Add(new() { Id = MessageBoxButtons.Cancel, Text = GetButtonTextEffective(MessageBoxButtons.Cancel), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Cancel) }); + buttons.Add(new() { Id = MessageBoxButtons.Retry, Text = GetButtonTextEffective(MessageBoxButtons.Retry), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Retry) }); break; case MessageBoxButtons.CustomCancel: primaryButtonEffective ??= MessageBoxButtons.Custom; - buttons.Add(new() { Id = MessageBoxButtons.Cancel, Text = MessageBoxLocalizer["Cancel"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Cancel) }); - buttons.Add(new() { Id = MessageBoxButtons.Custom, Text = CustomButtonText, IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Custom) }); + buttons.Add(new() { Id = MessageBoxButtons.Cancel, Text = GetButtonTextEffective(MessageBoxButtons.Cancel), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Cancel) }); + buttons.Add(new() { Id = MessageBoxButtons.Custom, Text = GetButtonTextEffective(MessageBoxButtons.Custom), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Custom) }); break; default: if (Buttons.HasFlag(MessageBoxButtons.Abort)) { - buttons.Add(new() { Id = MessageBoxButtons.Abort, Text = MessageBoxLocalizer["Abort"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Abort) }); + buttons.Add(new() { Id = MessageBoxButtons.Abort, Text = GetButtonTextEffective(MessageBoxButtons.Abort), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Abort) }); } if (Buttons.HasFlag(MessageBoxButtons.Retry)) { - buttons.Add(new() { Id = MessageBoxButtons.Retry, Text = MessageBoxLocalizer["Retry"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Retry) }); + buttons.Add(new() { Id = MessageBoxButtons.Retry, Text = GetButtonTextEffective(MessageBoxButtons.Retry), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Retry) }); } if (Buttons.HasFlag(MessageBoxButtons.Ignore)) { - buttons.Add(new() { Id = MessageBoxButtons.Ignore, Text = MessageBoxLocalizer["Ignore"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Ignore) }); + buttons.Add(new() { Id = MessageBoxButtons.Ignore, Text = GetButtonTextEffective(MessageBoxButtons.Ignore), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Ignore) }); } if (Buttons.HasFlag(MessageBoxButtons.Cancel)) { - buttons.Add(new() { Id = MessageBoxButtons.Cancel, Text = MessageBoxLocalizer["Cancel"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Cancel) }); + buttons.Add(new() { Id = MessageBoxButtons.Cancel, Text = GetButtonTextEffective(MessageBoxButtons.Cancel), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Cancel) }); } if (Buttons.HasFlag(MessageBoxButtons.Yes)) { - buttons.Add(new() { Id = MessageBoxButtons.Yes, Text = MessageBoxLocalizer["Yes"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Yes) }); + buttons.Add(new() { Id = MessageBoxButtons.Yes, Text = GetButtonTextEffective(MessageBoxButtons.Yes), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Yes) }); } if (Buttons.HasFlag(MessageBoxButtons.No)) { - buttons.Add(new() { Id = MessageBoxButtons.No, Text = MessageBoxLocalizer["No"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.No) }); + buttons.Add(new() { Id = MessageBoxButtons.No, Text = GetButtonTextEffective(MessageBoxButtons.No), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.No) }); } if (Buttons.HasFlag(MessageBoxButtons.Custom)) { - buttons.Add(new() { Id = MessageBoxButtons.Custom, Text = CustomButtonText, IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Custom) }); + buttons.Add(new() { Id = MessageBoxButtons.Custom, Text = GetButtonTextEffective(MessageBoxButtons.Custom), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Custom) }); } if (Buttons.HasFlag(MessageBoxButtons.Ok)) { - buttons.Add(new() { Id = MessageBoxButtons.Ok, Text = MessageBoxLocalizer["OK"], IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Ok) }); + buttons.Add(new() { Id = MessageBoxButtons.Ok, Text = GetButtonTextEffective(MessageBoxButtons.Ok), IsPrimary = primaryButtonEffective?.HasFlag(MessageBoxButtons.Ok) }); } break; } @@ -227,6 +235,22 @@ private List GetButtonsToRender() return buttons; } + private string GetButtonTextEffective(MessageBoxButtons button) + { + return button switch + { + MessageBoxButtons.Ok => GetSettings()?.OkButtonText ?? GetDefaults()?.OkButtonText ?? MessageBoxLocalizer["OK"], + MessageBoxButtons.Cancel => GetSettings()?.CancelButtonText ?? GetDefaults()?.CancelButtonText ?? MessageBoxLocalizer["Cancel"], + MessageBoxButtons.Retry => GetSettings()?.RetryButtonText ?? GetDefaults()?.RetryButtonText ?? MessageBoxLocalizer["Retry"], + MessageBoxButtons.Ignore => GetSettings()?.IgnoreButtonText ?? GetDefaults()?.IgnoreButtonText ?? MessageBoxLocalizer["Ignore"], + MessageBoxButtons.Abort => GetSettings()?.AbortButtonText ?? GetDefaults()?.AbortButtonText ?? MessageBoxLocalizer["Abort"], + MessageBoxButtons.Yes => GetSettings()?.YesButtonText ?? GetDefaults()?.YesButtonText ?? MessageBoxLocalizer["Yes"], + MessageBoxButtons.No => GetSettings()?.NoButtonText ?? GetDefaults()?.NoButtonText ?? MessageBoxLocalizer["No"], + MessageBoxButtons.Custom => CustomButtonText, + _ => throw new InvalidOperationException("Unsupported button type."), + }; + } + private class ButtonDefinition { public MessageBoxButtons Id { get; set; } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Modals/HxMessageBoxHost.razor b/Havit.Blazor.Components.Web.Bootstrap/Modals/HxMessageBoxHost.razor index 666c553e..c9ec04fd 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Modals/HxMessageBoxHost.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Modals/HxMessageBoxHost.razor @@ -2,13 +2,13 @@ - \ No newline at end of file + Settings="_request.Settings" + @attributes="_request.AdditionalAttributes" /> \ No newline at end of file diff --git a/Havit.Blazor.Components.Web/Dialogs/HxMessageBoxService.cs b/Havit.Blazor.Components.Web.Bootstrap/Modals/HxMessageBoxService.cs similarity index 83% rename from Havit.Blazor.Components.Web/Dialogs/HxMessageBoxService.cs rename to Havit.Blazor.Components.Web.Bootstrap/Modals/HxMessageBoxService.cs index 3805991e..c9aa66fa 100644 --- a/Havit.Blazor.Components.Web/Dialogs/HxMessageBoxService.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Modals/HxMessageBoxService.cs @@ -1,4 +1,4 @@ -namespace Havit.Blazor.Components.Web; +namespace Havit.Blazor.Components.Web.Bootstrap; public class HxMessageBoxService : IHxMessageBoxService { diff --git a/Havit.Blazor.Components.Web/Dialogs/IHxMessageBoxService.cs b/Havit.Blazor.Components.Web.Bootstrap/Modals/IHxMessageBoxService.cs similarity index 77% rename from Havit.Blazor.Components.Web/Dialogs/IHxMessageBoxService.cs rename to Havit.Blazor.Components.Web.Bootstrap/Modals/IHxMessageBoxService.cs index b275f112..65a7d67d 100644 --- a/Havit.Blazor.Components.Web/Dialogs/IHxMessageBoxService.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Modals/IHxMessageBoxService.cs @@ -1,4 +1,4 @@ -namespace Havit.Blazor.Components.Web; +namespace Havit.Blazor.Components.Web.Bootstrap; public interface IHxMessageBoxService { diff --git a/Havit.Blazor.Components.Web/Dialogs/MessageBoxButtons.cs b/Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxButtons.cs similarity index 90% rename from Havit.Blazor.Components.Web/Dialogs/MessageBoxButtons.cs rename to Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxButtons.cs index 4856e24d..ae8ca607 100644 --- a/Havit.Blazor.Components.Web/Dialogs/MessageBoxButtons.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxButtons.cs @@ -1,4 +1,4 @@ -namespace Havit.Blazor.Components.Web; +namespace Havit.Blazor.Components.Web.Bootstrap; [Flags] public enum MessageBoxButtons diff --git a/Havit.Blazor.Components.Web/Dialogs/MessageBoxRequest.cs b/Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxRequest.cs similarity index 80% rename from Havit.Blazor.Components.Web/Dialogs/MessageBoxRequest.cs rename to Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxRequest.cs index 7108d3f2..62c805c3 100644 --- a/Havit.Blazor.Components.Web/Dialogs/MessageBoxRequest.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxRequest.cs @@ -1,5 +1,8 @@ -namespace Havit.Blazor.Components.Web; +namespace Havit.Blazor.Components.Web.Bootstrap; +/// +/// Represents a request to display a message box with various customizable options. +/// public struct MessageBoxRequest { /// @@ -42,6 +45,11 @@ public struct MessageBoxRequest /// public string CustomButtonText { get; set; } + /// + /// Settings for the message box. + /// + public MessageBoxSettings Settings { get; set; } + /// /// Additional attributes to be splatted onto an underlying UI component (Bootstrap: HxMessageBox -> HxModal). /// diff --git a/Havit.Blazor.Components.Web/Dialogs/MessageBoxServiceCollectionExtensions.cs b/Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxServiceCollectionExtensions.cs similarity index 91% rename from Havit.Blazor.Components.Web/Dialogs/MessageBoxServiceCollectionExtensions.cs rename to Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxServiceCollectionExtensions.cs index d06a0864..3bc92dd3 100644 --- a/Havit.Blazor.Components.Web/Dialogs/MessageBoxServiceCollectionExtensions.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxServiceCollectionExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection; -namespace Havit.Blazor.Components.Web; +namespace Havit.Blazor.Components.Web.Bootstrap; /// /// Extension methods for installation of support. diff --git a/Havit.Blazor.Components.Web/Dialogs/MessageBoxServiceExtensions.cs b/Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxServiceExtensions.cs similarity index 94% rename from Havit.Blazor.Components.Web/Dialogs/MessageBoxServiceExtensions.cs rename to Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxServiceExtensions.cs index cd600b58..edf9cd0b 100644 --- a/Havit.Blazor.Components.Web/Dialogs/MessageBoxServiceExtensions.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxServiceExtensions.cs @@ -1,4 +1,4 @@ -namespace Havit.Blazor.Components.Web; +namespace Havit.Blazor.Components.Web.Bootstrap; public static class MessageBoxServiceExtensions { diff --git a/Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxSettings.cs b/Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxSettings.cs index f4197b67..a64869e7 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxSettings.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Modals/MessageBoxSettings.cs @@ -1,4 +1,6 @@ -namespace Havit.Blazor.Components.Web.Bootstrap; +using Microsoft.Extensions.Localization; + +namespace Havit.Blazor.Components.Web.Bootstrap; /// /// Settings for the and derived components. @@ -19,4 +21,39 @@ public record MessageBoxSettings /// Settings for the underlying component. /// public ModalSettings ModalSettings { get; set; } + + /// + /// Text for the OK button. + /// + public string OkButtonText { get; set; } + + /// + /// Text for the Cancel button. + /// + public string CancelButtonText { get; set; } + + /// + /// Text for the Abort button. + /// + public string AbortButtonText { get; set; } + + /// + /// Text for the Yes button. + /// + public string YesButtonText { get; set; } + + /// + /// Text for the No button. + /// + public string NoButtonText { get; set; } + + /// + /// Text for the Retry button. + /// + public string RetryButtonText { get; set; } + + /// + /// Text for the Ignore button. + /// + public string IgnoreButtonText { get; set; } } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Navigation/HxNavLink.razor b/Havit.Blazor.Components.Web.Bootstrap/Navigation/HxNavLink.razor index dfbe9f5c..65452d97 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Navigation/HxNavLink.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Navigation/HxNavLink.razor @@ -11,7 +11,6 @@ tabindex="@(CascadeEnabledComponent.EnabledEffective(this) ? null : "-1")" aria-disabled="@(CascadeEnabledComponent.EnabledEffective(this) ? null : "true")" role="@(OnClick.HasDelegate ? "button" : null)" - @attributes="this.AdditionalAttributes"> - @Text - @ChildContent - \ No newline at end of file + @attributes="this.AdditionalAttributes" + Text="@Text" + ChildContent="ChildContent" /> \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap/Navigation/HxSidebar.razor b/Havit.Blazor.Components.Web.Bootstrap/Navigation/HxSidebar.razor index 78d0d15b..b8e07958 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Navigation/HxSidebar.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Navigation/HxSidebar.razor @@ -1,6 +1,6 @@ @namespace Havit.Blazor.Components.Web.Bootstrap -
+
[Parameter] public bool? OnClickPreventDefault { get; set; } + [Parameter] public string Text { get; set; } + protected override void BuildRenderTree(RenderTreeBuilder builder) { builder.OpenElement(0, "a"); @@ -39,7 +41,8 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddEventStopPropagationAttribute(5, "onclick", this.OnClickStopPropagation ?? true); } - builder.AddContent(6, ChildContent); + builder.AddContent(6, Text); + builder.AddContent(7, ChildContent); builder.CloseElement(); } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Navigation/Internal/HxSidebarItemNavLinkContentInternal.razor b/Havit.Blazor.Components.Web.Bootstrap/Navigation/Internal/HxSidebarItemNavLinkContentInternal.razor new file mode 100644 index 00000000..16639d17 --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap/Navigation/Internal/HxSidebarItemNavLinkContentInternal.razor @@ -0,0 +1,19 @@ +@namespace Havit.Blazor.Components.Web.Bootstrap.Internal + + \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap/Navigation/Internal/HxSidebarItemNavLinkContentInternal.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Navigation/Internal/HxSidebarItemNavLinkContentInternal.razor.cs new file mode 100644 index 00000000..1990ad0a --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap/Navigation/Internal/HxSidebarItemNavLinkContentInternal.razor.cs @@ -0,0 +1,14 @@ +namespace Havit.Blazor.Components.Web.Bootstrap.Internal; + +/// +/// Inner content of the . +/// +public partial class HxSidebarItemNavLinkContentInternal +{ + [Parameter] public string Text { get; set; } + [Parameter] public bool Expandable { get; set; } + [Parameter] public bool? Expanded { get; set; } + [Parameter] public IconBase Icon { get; set; } + [Parameter] public string InnerCssClass { get; set; } + [Parameter] public RenderFragment ContentTemplate { get; set; } +} \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap/Navigation/Internal/HxSidebarItemNavLinkContentInternal.razor.css b/Havit.Blazor.Components.Web.Bootstrap/Navigation/Internal/HxSidebarItemNavLinkContentInternal.razor.css new file mode 100644 index 00000000..8cb44511 --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap/Navigation/Internal/HxSidebarItemNavLinkContentInternal.razor.css @@ -0,0 +1,36 @@ +.hx-sidebar-item-navlink-content { + display: flex; + flex-direction: row; + align-items: center; + align-self: stretch; + gap: .75rem; +} + +.hx-sidebar-subitems.show .hx-sidebar-item-navlink-content-inner { + display: inline-flex; +} + +.hx-sidebar-item-navlink-content-inner { + display: flex; + align-items: center; + flex-grow: 1; +} + +::deep .expand-icon { + font-size: .75rem; + color: var(--hx-sidebar-item-icon-color); + transition: transform .35s ease; + transform-origin: .5em 50%; +} + +::deep .hx-sidebar-item-icon { + color: var(--hx-sidebar-item-icon-color); +} + +a.nav-link:hover ::deep .hx-sidebar-item-icon { + color: var(--hx-sidebar-item-hover-icon-color); +} + +a.nav-link.active ::deep .hx-sidebar-item-icon { + color: var(--hx-sidebar-item-active-icon-color); +} \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap/Navigation/SidebarMobileBreakpoint.cs b/Havit.Blazor.Components.Web.Bootstrap/Navigation/SidebarResponsiveBreakpoint.cs similarity index 100% rename from Havit.Blazor.Components.Web.Bootstrap/Navigation/SidebarMobileBreakpoint.cs rename to Havit.Blazor.Components.Web.Bootstrap/Navigation/SidebarResponsiveBreakpoint.cs diff --git a/Havit.Blazor.Components.Web.Bootstrap/Navigation/SidebarResponsiveBreakpointExtensions.cs b/Havit.Blazor.Components.Web.Bootstrap/Navigation/SidebarResponsiveBreakpointExtensions.cs new file mode 100644 index 00000000..a07f0bd5 --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap/Navigation/SidebarResponsiveBreakpointExtensions.cs @@ -0,0 +1,18 @@ +namespace Havit.Blazor.Components.Web.Bootstrap; + +public static class SidebarResponsiveBreakpointExtensions +{ + public static string GetCssClass(this SidebarResponsiveBreakpoint breakpoint, string cssClassPattern) + { + return breakpoint switch + { + SidebarResponsiveBreakpoint.None => cssClassPattern.Replace("-??-", "-"), // !!! Simplified for the use case of this component. + SidebarResponsiveBreakpoint.Small => cssClassPattern.Replace("??", "sm"), + SidebarResponsiveBreakpoint.Medium => cssClassPattern.Replace("??", "md"), + SidebarResponsiveBreakpoint.Large => cssClassPattern.Replace("??", "lg"), + SidebarResponsiveBreakpoint.ExtraLarge => cssClassPattern.Replace("??", "xl"), + SidebarResponsiveBreakpoint.Xxl => cssClassPattern.Replace("??", "xxl"), + _ => throw new InvalidOperationException($"Unknown {nameof(SidebarResponsiveBreakpoint)} value {breakpoint}.") + }; + } +} diff --git a/Havit.Blazor.Components.Web.Bootstrap/Tags/Internal/HxInputTagsInputInternal.razor b/Havit.Blazor.Components.Web.Bootstrap/Tags/Internal/HxInputTagsInputInternal.razor index 1179b08d..9e97a119 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Tags/Internal/HxInputTagsInputInternal.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Tags/Internal/HxInputTagsInputInternal.razor @@ -5,7 +5,7 @@ type="text" class="@CssClass" disabled="@(!EnabledEffective)" - autocomplete="false" + autocomplete="off" data-bs-reference="parent" data-bs-offset="@($"{Offset.X},{Offset.Y}")" value="@Value" diff --git a/Havit.Blazor.Components.Web.Bootstrap/Toasts/HxToast.cs b/Havit.Blazor.Components.Web.Bootstrap/Toasts/HxToast.cs index dd1ba4b5..42ebe170 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Toasts/HxToast.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Toasts/HxToast.cs @@ -89,7 +89,16 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddAttribute(101, "role", "alert"); builder.AddAttribute(102, "aria-live", "assertive"); builder.AddAttribute(103, "aria-atomic", "true"); - builder.AddAttribute(104, "class", CssClassHelper.Combine("toast", Color?.ToBackgroundColorCss(), HasContrastColor() ? "text-white" : "text-dark", CssClass)); + + // Known-issue: Pre-rendering flickering, see https://github.com/dotnet/aspnetcore/issues/42561 + // Initialization during first (static) rendering and detecting the second rendering by using PersistentComponentState won't help, because the HTMLElement is replaced in DOM and diasappears. + var ssrInit = true; // TODO .NET9 - disable ssrInit for pre-rendering (keep for pure static SSR) + builder.AddAttribute(104, "class", CssClassHelper.Combine( + "hx-toast toast", + ssrInit ? "hx-toast-init" : null, + Color?.ToBackgroundColorCss(), + HasContrastColor() ? "text-white" : "text-dark", + CssClass)); if (AutohideDelay != null) { @@ -210,7 +219,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { return; } - await _jsModule.InvokeVoidAsync("show", _toastElement, _dotnetObjectReference); + await _jsModule.InvokeVoidAsync("init", _toastElement, _dotnetObjectReference); } } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Tooltips/Internal/HxTooltipInternalBase.cs b/Havit.Blazor.Components.Web.Bootstrap/Tooltips/Internal/HxTooltipInternalBase.cs index 38f1ad79..e6f4b233 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Tooltips/Internal/HxTooltipInternalBase.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Tooltips/Internal/HxTooltipInternalBase.cs @@ -43,7 +43,7 @@ public abstract class HxTooltipInternalBase : ComponentBase, IAsyncDisposable protected string ContainerEffective => Container ?? GetSettings()?.Container ?? GetDefaults().Container; /// - /// Enable or disable the sanitization. If activated HTML content will be sanitized. See the sanitizer section in Bootstrap JavaScript documentation. + /// Enable or disable the sanitization. If activated, HTML content will be sanitized. See the sanitizer section in Bootstrap JavaScript documentation. /// Default is true. /// [Parameter] public bool Sanitize { get; set; } = true; diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/Havit.Blazor.Components.Web.Bootstrap.lib.module.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/Havit.Blazor.Components.Web.Bootstrap.lib.module.js new file mode 100644 index 00000000..5e540397 --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/Havit.Blazor.Components.Web.Bootstrap.lib.module.js @@ -0,0 +1,22 @@ +import HxToast from './HxToast.js'; + +export function afterWebStarted(blazor) { + console.debug('Havit.Blazor.Components.Web.Bootstrap.lib.module.js: afterWebStarted'); + + blazor.addEventListener('enhancedload', onEnhancedLoad); + + activateToasts(); // onEnhancedLoad is not called when enhanced navigation is not used +} + +function onEnhancedLoad() { + console.debug('Havit.Blazor.Components.Web.Bootstrap.lib.module.js: onEnhancedLoad'); + + activateToasts(); // idempotent +} + +function activateToasts() { + // idempotent implementation required + for (const element of document.querySelectorAll('.hx-toast-init')) { + HxToast.init(element); + } +} \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxAnchorFragmentNavigation.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxAnchorFragmentNavigation.js index 249f3ac1..2107f971 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxAnchorFragmentNavigation.js +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxAnchorFragmentNavigation.js @@ -1,7 +1,7 @@ export function scrollToAnchor(anchor) { - var selector = anchor || document.location.hash; + const selector = anchor || document.location.hash; if (selector && (selector.length > 1)) { - var element = null; + let element = null; try { element = document.querySelector(selector); } diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxAutosuggest.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxAutosuggest.js index 35637acc..f13d8478 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxAutosuggest.js +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxAutosuggest.js @@ -1,5 +1,5 @@ export function initialize(inputId, hxAutosuggestDotnetObjectReference, keysToPreventDefault) { - let inputElement = document.getElementById(inputId); + const inputElement = document.getElementById(inputId); if (!inputElement) { return; } @@ -16,7 +16,7 @@ } function handleKeyDown(event) { - let key = event.key; + const key = event.key; event.target.hxAutosuggestDotnetObjectReference.invokeMethodAsync("HxAutosuggestInternal_HandleInputKeyDown", key); @@ -47,7 +47,7 @@ export function open(inputElement, hxAutosuggestDotnetObjectReference) { inputElement.hxAutosuggestDotnetObjectReference = hxAutosuggestDotnetObjectReference; inputElement.addEventListener('hidden.bs.dropdown', handleDropdownHidden); - var d = new bootstrap.Dropdown(inputElement); + const d = new bootstrap.Dropdown(inputElement); if (d && (inputElement.clickIsComing === false)) { // clickIsComing logic fixes #572 - Initial suggestions disappear when the DataProvider response is quick // If click is coming, we do not want to show the dropdown as it will be toggled by the later click event (if we open it here, onfocus, click will hide it) @@ -62,7 +62,7 @@ export function destroy(inputElement) { inputElement.removeAttribute("data-bs-toggle", "dropdown"); - var d = bootstrap.Dropdown.getInstance(inputElement); + const d = bootstrap.Dropdown.getInstance(inputElement); if (d) { d.hide(); @@ -93,17 +93,17 @@ function handleDropdownHidden(event) { // But we need the item click event to fire first. // Therefore we delay jsinterop for a while. window.setTimeout(function (element) { - element.hxAutosuggestDotnetObjectReference.invokeMethodAsync('HxAutosuggestInternal_HandleDropdownHidden'); + element.hxAutosuggestDotnetObjectReference?.invokeMethodAsync('HxAutosuggestInternal_HandleDropdownHidden'); }, 1, event.target); - var d = bootstrap.Dropdown.getInstance(event.target); + const d = bootstrap.Dropdown.getInstance(event.target); if (d) { d.dispose(); } }; export function dispose(inputId) { - let inputElement = document.getElementById(inputId); + const inputElement = document.getElementById(inputId); if (inputElement) { inputElement.removeEventListener('keydown', handleKeyDown); diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxCarousel.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxCarousel.js index 469ca4f9..3dc8f5b5 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxCarousel.js +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxCarousel.js @@ -2,42 +2,42 @@ if (!element) { return; } - var carousel = new bootstrap.Carousel(element, options); + const carousel = new bootstrap.Carousel(element, options); element.hxCarouselDotnetObjectReference = hxCarouselDotnetObjectReference; element.addEventListener('slide.bs.carousel', handleSlide); element.addEventListener('slid.bs.carousel', handleSlid); } export function slideTo(element, index) { - var c = bootstrap.Carousel.getInstance(element); + const c = bootstrap.Carousel.getInstance(element); if (c) { c.to(index); } } export function previous(element) { - var c = bootstrap.Carousel.getInstance(element); + const c = bootstrap.Carousel.getInstance(element); if (c) { c.prev(); } } export function next(element) { - var c = bootstrap.Carousel.getInstance(element); + const c = bootstrap.Carousel.getInstance(element); if (c) { c.next(); } } export function cycle(element) { - var c = bootstrap.Carousel.getInstance(element); + const c = bootstrap.Carousel.getInstance(element); if (c) { c.cycle(); } } export function pause(element) { - var c = bootstrap.Carousel.getInstance(element); + const c = bootstrap.Carousel.getInstance(element); if (c) { c.pause(); } @@ -60,7 +60,7 @@ export function dispose(element) { element.removeEventListener('slid.bs.carousel', handleSlid); element.hxCarouselDotnetObjectReference = null; - var c = bootstrap.Carousel.getInstance(element); + const c = bootstrap.Carousel.getInstance(element); if (c) { c.dispose(); } diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxCollapse.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxCollapse.js index f0549aae..957e5665 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxCollapse.js +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxCollapse.js @@ -9,20 +9,20 @@ element.addEventListener('hide.bs.collapse', handleCollapseHide); element.addEventListener('hidden.bs.collapse', handleCollapseHidden); - var c = new bootstrap.Collapse(element, { + const c = new bootstrap.Collapse(element, { toggle: shouldToggle, }); } export function show(element) { - var c = bootstrap.Collapse.getInstance(element); + const c = bootstrap.Collapse.getInstance(element); if (c) { c.show(); } }; export function hide(element) { - var c = bootstrap.Collapse.getInstance(element); + const c = bootstrap.Collapse.getInstance(element); if (c) { c.hide(); } @@ -52,7 +52,7 @@ export function dispose(element) { element.removeEventListener('hidden.bs.collapse', handleCollapseHidden); element.hxCollapseDotnetObjectReference = null; - var c = bootstrap.Collapse.getInstance(element); + const c = bootstrap.Collapse.getInstance(element); if (c) { c.dispose(); } diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxInputDate.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxInputDate.js index 94128748..2fb2de2b 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxInputDate.js +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxInputDate.js @@ -12,7 +12,7 @@ } function handleIconClick(event) { - var triggerElement = event.currentTarget.triggerElement; + const triggerElement = event.currentTarget.triggerElement; triggerElement.click(); event.stopPropagation(); } diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxInputDateRange.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxInputDateRange.js index c857b3b5..7762b068 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxInputDateRange.js +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxInputDateRange.js @@ -12,7 +12,7 @@ export function addOpenAndCloseEventListeners(triggerElement, iconWrapperElement } function handleIconClick(event) { - var triggerElement = event.currentTarget.triggerElement; + const triggerElement = event.currentTarget.triggerElement; triggerElement.click(); event.stopPropagation(); } diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxInputTags.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxInputTags.js index c9b1d8c7..7b9f499b 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxInputTags.js +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxInputTags.js @@ -1,5 +1,5 @@ export function initialize(inputId, hxInputTagsDotnetObjectReference, keysToPrevendDefault) { - let inputElement = document.getElementById(inputId); + const inputElement = document.getElementById(inputId); if (!inputElement) { return; } @@ -12,7 +12,7 @@ } function handleKeyDown(event) { - let key = event.key; + const key = event.key; event.target.hxInputTagsDotnetObjectReference.invokeMethodAsync("HxInputTagsInternal_HandleInputKeyDown", key); @@ -27,7 +27,7 @@ function handleInputBlur(event) { // We need to recognize, whether the blur event is fired because of the dropdown item click or because of the user clicked somewhere else. // We will use relatedTarget property of the event to recognize the click on the dropdown item. // If relatedTarget is within the dropdown, we will ignore the blur event. - var isWithinDropdown = false; + let isWithinDropdown = false; if (event.relatedTarget) { isWithinDropdown = event.target.parentElement.contains(event.relatedTarget); } @@ -44,7 +44,7 @@ export function open(inputElement, hxInputTagsDotnetObjectReference, delayShow) inputElement.hxInputTagsDotnetObjectReference = hxInputTagsDotnetObjectReference; inputElement.addEventListener('hidden.bs.dropdown', handleDropdownHidden) - var dd = new bootstrap.Dropdown(inputElement); + const dd = new bootstrap.Dropdown(inputElement); if (!dd) { return; } @@ -76,7 +76,7 @@ export function destroy(inputElement) { inputElement.removeAttribute("data-bs-toggle", "dropdown"); - var dropdown = bootstrap.Dropdown.getInstance(inputElement); + const dropdown = bootstrap.Dropdown.getInstance(inputElement); if (dropdown) { dropdown.hide(); @@ -96,12 +96,12 @@ function handleDropdownHidden(event) { // But we need the item click event to fire first. // Therefore we delay jsinterop for a while. window.setTimeout(function (element) { - element.hxInputTagsDotnetObjectReference.invokeMethodAsync('HxInputTagsInternal_HandleDropdownHidden'); + element.hxInputTagsDotnetObjectReference?.invokeMethodAsync('HxInputTagsInternal_HandleDropdownHidden'); }, 1, event.target); } export function dispose(inputId) { - let inputElement = document.getElementById(inputId); + const inputElement = document.getElementById(inputId); inputElement.removeEventListener('keydown', handleKeyDown); inputElement.removeEventListener('blur', handleInputBlur); diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxModal.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxModal.js index b98072e1..3adabd97 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxModal.js +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxModal.js @@ -10,7 +10,7 @@ element.addEventListener('hidden.bs.modal', handleModalHidden); element.addEventListener('shown.bs.modal', handleModalShown); - var modal = new bootstrap.Modal(element, { + const modal = new bootstrap.Modal(element, { keyboard: closeOnEscape }); if (modal) { @@ -23,7 +23,7 @@ export function hide(element) { return; } element.hxModalHiding = true; - let modal = bootstrap.Modal.getInstance(element); + const modal = bootstrap.Modal.getInstance(element); if (modal) { modal.hide(); } @@ -34,7 +34,7 @@ function handleModalShown(event) { }; async function handleModalHide(event) { - let modalInstance = bootstrap.Modal.getInstance(event.target); + const modalInstance = bootstrap.Modal.getInstance(event.target); if (modalInstance.hidePreventionDisabled || event.target.hxModalDisposing) { modalInstance.hidePreventionDisabled = false; @@ -43,7 +43,7 @@ async function handleModalHide(event) { event.preventDefault(); - let cancel = await event.target.hxModalDotnetObjectReference.invokeMethodAsync('HxModal_HandleModalHide'); + const cancel = await event.target.hxModalDotnetObjectReference.invokeMethodAsync('HxModal_HandleModalHide'); if (!cancel) { modalInstance.hidePreventionDisabled = true; event.target.hxModalHiding = true; @@ -80,7 +80,7 @@ export function dispose(element) { element.removeEventListener('shown.bs.modal', handleModalShown); element.hxModalDotnetObjectReference = null; - var modal = bootstrap.Modal.getInstance(element); + const modal = bootstrap.Modal.getInstance(element); if (modal) { modal.dispose(); } diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxMultiSelect.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxMultiSelect.js index 6d8346ed..b6d08eda 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxMultiSelect.js +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxMultiSelect.js @@ -7,18 +7,18 @@ element.addEventListener('shown.bs.dropdown', handleDropdownShown); element.addEventListener('hidden.bs.dropdown', handleDropdownHidden); - var d = new bootstrap.Dropdown(element); + const d = new bootstrap.Dropdown(element); } export function show(element) { - var d = bootstrap.Dropdown.getInstance(element); + const d = bootstrap.Dropdown.getInstance(element); if (d) { d.show(); } }; export function hide(element) { - var d = bootstrap.Dropdown.getInstance(element); + const d = bootstrap.Dropdown.getInstance(element); if (d) { d.hide(); } @@ -41,7 +41,7 @@ export function dispose(element) { element.removeEventListener('hidden.bs.dropdown', handleDropdownHidden); element.hxMultiSelectDotnetObjectReference = null; - var d = bootstrap.Dropdown.getInstance(element); + const d = bootstrap.Dropdown.getInstance(element); if (d) { d.dispose(); } diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxOffcanvas.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxOffcanvas.js index b207bb9a..23df4ab8 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxOffcanvas.js +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxOffcanvas.js @@ -1,6 +1,6 @@ export function show(element, hxOffcanvasDotnetObjectReference, closeOnEscape, scrollingEnabled, subscribeToHideEvent) { if (window.offcanvasElement) { - let previousOffcanvas = bootstrap.Offcanvas.getInstance(window.offcanvasElement); + const previousOffcanvas = bootstrap.Offcanvas.getInstance(window.offcanvasElement); if (previousOffcanvas) { // Although opening a new offcanvas should close the previous one, // we do not set previousOffcanvas.hidePreventionDisabled = true and force the hide() here (when handling the OnHiding event) @@ -22,7 +22,7 @@ element.addEventListener('shown.bs.offcanvas', handleOffcanvasShown); window.offcanvasElement = element; - let offcanvas = new bootstrap.Offcanvas(element, { + const offcanvas = new bootstrap.Offcanvas(element, { keyboard: closeOnEscape, scroll: scrollingEnabled }); @@ -36,7 +36,7 @@ export function hide(element) { return; } element.hxOffcanvasHiding = true; - let o = bootstrap.Offcanvas.getInstance(element); + const o = bootstrap.Offcanvas.getInstance(element); if (o) { o.hide(); } @@ -47,7 +47,7 @@ function handleOffcanvasShown(event) { } async function handleOffcanvasHide(event) { - let o = bootstrap.Offcanvas.getInstance(event.target); + const o = bootstrap.Offcanvas.getInstance(event.target); if (o.hidePreventionDisabled || event.target.hxOffcanvasDisposing) { o.hidePreventionDisabled = false; @@ -56,7 +56,7 @@ async function handleOffcanvasHide(event) { event.preventDefault(); - let cancel = await event.target.hxOffcanvasDotnetObjectReference.invokeMethodAsync('HxOffcanvas_HandleOffcanvasHide'); + const cancel = await event.target.hxOffcanvasDotnetObjectReference.invokeMethodAsync('HxOffcanvas_HandleOffcanvasHide'); if (!cancel) { o.hidePreventionDisabled = true; event.target.hxOffcanvasHiding = true; @@ -106,7 +106,7 @@ export function dispose(element, opened) { element.removeEventListener('shown.bs.offcanvas', handleOffcanvasShown); element.hxOffcanvasDotnetObjectReference = null; - let o = bootstrap.Offcanvas.getInstance(element); + const o = bootstrap.Offcanvas.getInstance(element); if (o) { o.dispose(); } diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxPopover.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxPopover.js index 8161cce2..2ef8cef2 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxPopover.js +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxPopover.js @@ -10,21 +10,21 @@ export function initialize(element, hxDotnetObjectReference, options) { } export function show(element) { - var i = bootstrap.Popover.getInstance(element); + const i = bootstrap.Popover.getInstance(element); if (i) { i.show(); } } export function hide(element) { - var i = bootstrap.Popover.getInstance(element); + const i = bootstrap.Popover.getInstance(element); if (i) { i.hide(); } } export function enable(element) { - var i = bootstrap.Popover.getInstance(element); + const i = bootstrap.Popover.getInstance(element); if (i) { i.enable(); console.warn("enabled"); @@ -32,7 +32,7 @@ export function enable(element) { } export function disable(element) { - var i = bootstrap.Popover.getInstance(element); + const i = bootstrap.Popover.getInstance(element); if (i) { i.disable(); console.warn("disabled"); @@ -40,7 +40,7 @@ export function disable(element) { } export function setContent(element, newContent) { - var i = bootstrap.Popover.getInstance(element); + const i = bootstrap.Popover.getInstance(element); if (i) { i.setContent(newContent); } @@ -61,7 +61,7 @@ export function dispose(element) { element.removeEventListener('shown.bs.popover', handleShown); element.removeEventListener('hidden.bs.popover', handleHidden); element.hxDotnetObjectReference = null; - var popover = bootstrap.Popover.getInstance(element); + const popover = bootstrap.Popover.getInstance(element); if (popover) { popover.dispose(); } diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxScrollspy.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxScrollspy.js index d4c60425..8acedb46 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxScrollspy.js +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxScrollspy.js @@ -1,11 +1,11 @@ export function initialize(element, targetId) { - var scrollspy = new bootstrap.ScrollSpy(element, { + const scrollspy = new bootstrap.ScrollSpy(element, { target: '#' + targetId }); } export function refresh(element) { - var scrollspy = bootstrap.ScrollSpy.getInstance(element); + const scrollspy = bootstrap.ScrollSpy.getInstance(element); if (element.scrollTop > 0) { // scrollspy calculates the offsets properly only if the container is scrolled to @@ -15,6 +15,6 @@ export function refresh(element) { } export function dispose(element) { - var scrollspy = bootstrap.ScrollSpy.getInstance(element); + const scrollspy = bootstrap.ScrollSpy.getInstance(element); scrollspy.dispose(); } \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxSearchBox.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxSearchBox.js index 16dca791..2c53338d 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxSearchBox.js +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxSearchBox.js @@ -1,5 +1,5 @@ export function initialize(inputId, hxSearchBoxDotnetObjectReference, keysToPreventDefault) { - let inputElement = document.getElementById(inputId); + const inputElement = document.getElementById(inputId); if (!inputElement) { return; } @@ -15,7 +15,7 @@ } function handleKeyDown(event) { - let key = event.key; + const key = event.key; event.target.hxSearchBoxDotnetObjectReference.invokeMethodAsync("HxSearchBox_HandleInputKeyDown", key); @@ -48,7 +48,7 @@ export function scrollToFocusedItem() { } export function dispose(inputId) { - let inputElement = document.getElementById(inputId); + const inputElement = document.getElementById(inputId); if (!inputElement) { return; } diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxToast.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxToast.js index f587521e..1b3cf5f0 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxToast.js +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxToast.js @@ -1,15 +1,34 @@ -export function show(element, hxToastDotnetObjectReference) { +// TODO Add MutationObserver to cleanup the toast when it is removed from the DOM (especially when using SSR) - waiting for https://github.com/dotnet/AspNetCore.Docs/issues/33842 + +// !! When updating this file, update also import in Havit.Blazor.Components.Web.Bootstrap.lib.module.js +export function init(element, hxToastDotnetObjectReference) { if (!element) { return; } - element.hxToastDotnetObjectReference = hxToastDotnetObjectReference; - element.addEventListener('hidden.bs.toast', handleToastHidden); + if (hxToastDotnetObjectReference) { + // interactive render mode only + element.hxToastDotnetObjectReference = hxToastDotnetObjectReference; + element.addEventListener('hidden.bs.toast', handleToastHidden); + } - var toast = new bootstrap.Toast(element); - if (toast) { + // the instance can be already created and shown by module activation (lib.module.js > onEnhancedLoad > activateToasts) + // which helps to initialize toasts on static SSR (incl. prerendering!) + // we do not expect the single toast element to be shown multiple times + let toast = bootstrap.Toast.getInstance(element); + if (!toast) { + toast = new bootstrap.Toast(element); toast.show(); } + else if (toast._element.classList.contains('hx-toast-init')) { + // for SSR enahanced forms, when merging DOM changes, Blazor sometimes reuses the original element + // (currently not being present in DOM but returned to DOM within patching process) + // in this case, the Bootstrap Toast instance might already exist, but the element is not shown + // The .hx-toast-init class indicates that the element is not shown yet. + toast.show(); + } + + element.classList.remove('hx-toast-init'); } function handleToastHidden(event) { @@ -24,8 +43,15 @@ export function dispose(element) { element.removeEventListener('hidden.bs.toast', handleToastHidden); element.hxToastDotnetObjectReference = null; - var t = bootstrap.Toast.getInstance(element); + const t = bootstrap.Toast.getInstance(element); if (t) { t.dispose(); } -} \ No newline at end of file +} + +const HxToast = { + init, + dispose +}; + +export default HxToast; \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxTooltip.js b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxTooltip.js index 4fe7178a..0f965b48 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxTooltip.js +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/HxTooltip.js @@ -10,35 +10,35 @@ export function initialize(element, hxDotnetObjectReference, options) { } export function show(element) { - var i = bootstrap.Tooltip.getInstance(element); + const i = bootstrap.Tooltip.getInstance(element); if (i) { i.show(); } } export function hide(element) { - var i = bootstrap.Tooltip.getInstance(element); + const i = bootstrap.Tooltip.getInstance(element); if (i) { i.hide(); } } export function enable(element) { - var i = bootstrap.Tooltip.getInstance(element); + const i = bootstrap.Tooltip.getInstance(element); if (i) { i.enable(); } } export function disable(element) { - var i = bootstrap.Tooltip.getInstance(element); + const i = bootstrap.Tooltip.getInstance(element); if (i) { i.disable(); } } export function setContent(element, newContent) { - var i = bootstrap.Tooltip.getInstance(element); + const i = bootstrap.Tooltip.getInstance(element); if (i) { i.setContent(newContent); } @@ -59,7 +59,7 @@ export function dispose(element) { element.removeEventListener('shown.bs.tooltip', handleShown); element.removeEventListener('hidden.bs.tooltip', handleHidden); element.hxDotnetObjectReference = null; - var tooltip = bootstrap.Tooltip.getInstance(element); + const tooltip = bootstrap.Tooltip.getInstance(element); if (tooltip) { tooltip.dispose(); } diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/bootstrap-icons.css b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/bootstrap-icons.lib.css similarity index 100% rename from Havit.Blazor.Components.Web.Bootstrap/wwwroot/bootstrap-icons.css rename to Havit.Blazor.Components.Web.Bootstrap/wwwroot/bootstrap-icons.lib.css diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/defaults.css b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/defaults.css index a5a6ba62..f7e8ca49 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/defaults.css +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/defaults.css @@ -1,217 +1,5 @@ -@import url("bootstrap-icons.css"); -:root { - /* HxAutosuggest */ - --hx-autosuggest-input-close-icon-opacity: .25; - --hx-autosuggest-item-highlighted-background-color: var(--bs-tertiary-bg); - --hx-autosuggest-dropdown-menu-height: 300px; - --hx-autosuggest-dropdown-menu-width: 100%; - --hx-autosuggest-input-search-icon-color: unset; - --hx-autosuggest-input-clear-icon-color: unset; - - /* HxButton */ - --hx-button-gap: .25rem; - - /* HxSidebar */ - --hx-sidebar-background-color: transparent; - --hx-sidebar-collapsed-width: 72px; - --hx-sidebar-width: 250px; - --hx-sidebar-toggler-background: var(--bs-gray-500); - --hx-sidebar-item-font-size: 1rem; - --hx-sidebar-item-padding: .75rem; - --hx-sidebar-item-color: var(--bs-body-color); - --hx-sidebar-item-icon-color: var(--bs-primary); - --hx-sidebar-item-border-radius: var(--bs-border-radius-lg); - --hx-sidebar-item-margin: 0 0 .25rem 0; - --hx-sidebar-item-hover-color: var(--bs-primary); - --hx-sidebar-item-hover-background-color: var(--bs-primary-rgb); - --hx-sidebar-item-hover-background-opacity: .1; - --hx-sidebar-item-hover-icon-color: var(--bs-primary); - --hx-sidebar-item-active-color: var(--bs-primary); - --hx-sidebar-item-active-background-color: var(--bs-primary-rgb); - --hx-sidebar-item-active-background-opacity: .1; - --hx-sidebar-item-active-icon-color: var(--bs-primary); - --hx-sidebar-item-active-font-weight: inherit; - --hx-sidebar-parent-item-active-color: var(--hx-sidebar-item-color); - --hx-sidebar-parent-item-active-background-opacity: 0; - --hx-sidebar-parent-item-active-background-color: unset; - --hx-sidebar-parent-item-active-font-weight: 600; - --hx-sidebar-parent-item-active-icon-color: var(--hx-sidebar-item-hover-icon-color); - --hx-sidebar-subitem-font-size: .875rem; - --hx-sidebar-subitem-padding: .5rem; - --hx-sidebar-subitem-margin: 0 0 .25rem 2rem; - --hx-sidebar-header-padding: 1rem; - --hx-sidebar-body-padding: 0 1rem; - --hx-sidebar-brand-logo-width: 2.5rem; - --hx-sidebar-brand-logo-height: 2.5rem; - --hx-sidebar-brand-shortname-width: 2.5rem; - --hx-sidebar-brand-shortname-height: 2.5rem; - --hx-sidebar-brand-shortname-background-color: var(--bs-primary); - --hx-sidebar-brand-shortname-border-radius: .625rem; - --hx-sidebar-brand-shortname-color: var(--bs-white); - --hx-sidebar-brand-shortname-font-weight: 600; - --hx-sidebar-brand-name-color: var(--bs-body-color); - --hx-sidebar-brand-name-font-weight: 600; - --hx-sidebar-footer-padding: 1rem; - --hx-sidebar-footer-item-padding: .75rem; - --hx-sidebar-footer-item-margin: 0; - --hx-sidebar-footer-item-font-size: 1rem; - --hx-sidebar-footer-item-color: unset; - --hx-sidebar-footer-item-radius: unset; - --hx-sidebar-footer-item-hover-background-color: unset; - --hx-sidebar-footer-item-hover-background-opacity: unset; - --hx-sidebar-footer-item-hover-color: unset; - - /* HxProgressOverlay */ - --hx-progress-overlay-color: var(--bs-white); - --hx-progress-overlay-opacity: .65; - - /* HxEditForm */ - --hx-form-spacing: 1.25rem; - - /* HxChipList */ - --hx-chip-list-chip-margin: 0.75rem 0.25rem .375rem 0; - --hx-chip-list-chip-remove-btn-margin: 0 0 0 .25rem; - --hx-chip-list-chip-remove-btn-opacity: .75; - --hx-chip-list-chip-reset-btn-padding: .35em .65em; - --hx-chip-list-chip-label-font-weight: 400; - --hx-chip-list-chip-label-margin: .25rem; - --hx-chip-list-chip-label-opacity: .75; - --hx-chip-list-gap: .25rem; - - /* HxContextMenu */ - --hx-context-menu-button-color: unset; - --hx-context-menu-button-border: unset; - --hx-context-menu-button-border-radius: var(--bs-border-radius-sm); - --hx-context-menu-button-padding: 0 .25rem; - --hx-context-menu-button-hover-background: var(--bs-secondary-bg); - --hx-context-menu-item-icon-margin: 0 .5rem 0 0; - - /* HxDropdown */ - --hx-dropdown-menu-item-icon-margin: 0 .5rem 0 0; - - /* HxGrid */ - --hx-grid-button-hover-background: var(--bs-secondary-bg); - --hx-grid-button-border-radius: var(--bs-border-radius); - --hx-grid-sorted-icon-color: var(--bs-primary); - --hx-grid-progress-indicator-color: var(--bs-primary); - - /* HxInputFileDropZone */ - --hx-input-file-drop-zone-border-width: 1px; - --hx-input-file-drop-zone-box-shadow: none; - --hx-input-file-drop-zone-hover-box-shadow: 0 0 0 .25rem rgba(var(--bs-primary-rgb), .25); - --hx-input-file-drop-zone-border-color: var(--bs-border-color); - --hx-input-file-drop-zone-disabled-color: var(--bs-secondary-color); - --hx-input-file-drop-zone-disabled-background-color: var(--bs-secondary-bg); - --hx-input-file-drop-zone-background-color: transparent; - --hx-input-file-drop-zone-hover-background-color: rgba(var(--bs-primary-rgb), .1); - --hx-input-file-drop-zone-hover-border-color: var(--bs-primary); - --hx-input-file-drop-zone-border-radius: var(--bs-border-radius-lg); - --hx-input-file-drop-zone-margin: 0; - --hx-input-file-drop-zone-padding: 3rem; - - /* HxCalendar */ - --hx-calendar-day-hover-background: var(--bs-tertiary-bg); - --hx-calendar-day-hover-border: none; - --hx-calendar-day-selected-background: var(--bs-primary); - --hx-calendar-day-selected-color: var(--bs-white); - --hx-calendar-day-selected-border: none; - --hx-calendar-day-out-color: var(--bs-tertiary-color); - --hx-calendar-day-in-color: unset; - --hx-calendar-day-disabled-opacity: .5; - --hx-calendar-day-disabled-text-decoration: line-through; - --hx-calendar-day-names-color: unset; - --hx-calendar-day-names-font-weight: 700; - --hx-calendar-navigation-button-hover-background: var(--bs-tertiary-bg); - --hx-calendar-navigation-button-focus-box-shadow: 0 0 0 0.25rem rgb(0 157 224 / 25%); - --hx-calendar-navigation-button-text-color: var(--bs-tertiary-color); - --hx-calendar-day-today-border: none; - --hx-calendar-day-today-background: var(--bs-primary-rgb); - --hx-calendar-day-today-background-opacity: .1; - --hx-calendar-day-today-color: var(--bs-primary); - --hx-calendar-day-border-radius: var(--bs-border-radius-sm); - --hx-calendar-day-padding: .375rem .5rem; - --hx-calendar-day-width: 2.25rem; - --hx-calendar-day-height: 2.25rem; - --hx-calendar-day-spacing: .125rem; - --hx-calendar-font-size: .875rem; - - /* Offcanvas */ - --hx-offcanvas-close-icon-font-size: 2rem; - --hx-offcanvas-footer-padding-y: 1rem; - --hx-offcanvas-footer-padding-x: 1rem; - --hx-offcanvas-horizontal-width-sm: 400px; - --hx-offcanvas-horizontal-width-lg: 600px; - - /* ListLayout */ - --hx-list-layout-table-font-size: .875rem; - - /* HxMultiSelect */ - --hx-multi-select-background-color: var(--bs-body-bg); - --hx-multi-select-dropdown-menu-height: 300px; - --hx-multi-select-filter-input-icon-opacity: .25; - --hx-multi-select-dropdown-menu-width: 100%; - - /* TagInput */ - --hx-input-tags-tag-gap: .25rem; - --hx-input-tags-input-width: 3em; - --hx-input-tags-input-placeholder-color: var(--bs-secondary-color); - --hx-input-tags-naked-font-size-lg: 1.25em; - --hx-input-tags-naked-font-size-sm: .875em; - --hx-input-tags-control-focused-box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25); - --hx-input-tags-control-focused-border-color: rgba(var(--bs-primary-rgb), .3); - --hx-input-tags-add-button-text-margin: 0 0 0 .25rem; - --hx-input-tags-add-button-disabled-opacity: .65; - --hx-input-tags-remove-button-margin: 0 0 0 .25rem; - --hx-input-tags-dropdown-item-highlighted-background-color: var(--bs-tertiary-bg); - - /* TreeView */ - --hx-tree-view-item-border-radius: var(--bs-border-radius-sm); - --hx-tree-view-item-border-width: 0; - --hx-tree-view-item-border-style: unset; - --hx-tree-view-item-border-color: unset; - --hx-tree-view-item-color: var(--bs-bg-color); - --hx-tree-view-item-hover-color: var(--bs-primary); - --hx-tree-view-item-selected-color: var(--bs-primary); - --hx-tree-view-item-background: transparent; - --hx-tree-view-item-hover-background: var(--bs-primary-rgb); - --hx-tree-view-item-hover-background-opacity: .1; - --hx-tree-view-item-selected-background: var(--bs-primary-rgb); - --hx-tree-view-item-spacer-width: 1rem; - --hx-tree-view-item-font-size: .75rem; - --hx-tree-view-item-padding: .25rem .5rem; - --hx-tree-view-item-margin: 0 0 .125rem 0; - --hx-tree-view-item-gap: .25rem; - --hx-tree-view-expander-container-width: 1rem; - - /* HxProgressIndicator */ - --hx-progress-indicator-background: var(--bs-body-bg); - --hx-progress-indicator-box-shadow: var(--bs-box-shadow); - --hx-progress-indicator-border-color: var(--bs-border-color); - --hx-progress-indicator-spinner-color: var(--bs-primary); - - /* HxToastContainer */ - --hx-toast-container-margin: .5rem; - - /* HxSearchBox */ - --hx-search-box-item-icon-margin: 0 .5rem 0 0; - --hx-search-box-item-icon-font-size: inherit; - --hx-search-box-item-title-font-size: inherit; - --hx-search-box-item-title-color: inherit; - --hx-search-box-item-subtitle-color: var(--bs-secondary); - --hx-search-box-item-subtitle-font-size: .75rem; - --hx-search-box-item-highlighted-background-color: var(--bs-tertiary-bg); - --hx-search-box-dropdown-menu-height: 300px; - --hx-search-box-dropdown-menu-width: 100%; - --hx-search-box-input-search-icon-color: unset; - --hx-search-box-input-clear-icon-color: unset; -} - -form { - display: flex; - flex-direction: column; - gap: var(--hx-form-spacing); -} - -form > .hx-button { - align-self: start; -} \ No newline at end of file +/* + This file exists solely for backward compatibility to prevent a 404 error + for clients referencing this file based on legacy installation instructions. + ACTION REQUIRED: Remove the reference to defaults.css from your project. +*/ diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/defaults.lib.css b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/defaults.lib.css new file mode 100644 index 00000000..2e4199b1 --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/defaults.lib.css @@ -0,0 +1,222 @@ +:root, +[data-bs-theme=dark] { + /* HxAutosuggest */ + --hx-autosuggest-input-close-icon-opacity: .25; + --hx-autosuggest-item-highlighted-background-color: var(--bs-tertiary-bg); + --hx-autosuggest-dropdown-menu-height: 300px; + --hx-autosuggest-dropdown-menu-width: 100%; + --hx-autosuggest-input-search-icon-color: unset; + --hx-autosuggest-input-clear-icon-color: unset; + + /* HxButton */ + --hx-button-gap: .25rem; + + /* HxSidebar */ + --hx-sidebar-background-color: var(--bs-body-bg); + --hx-sidebar-collapsed-width: 72px; + --hx-sidebar-width: 300px; + --hx-sidebar-max-height: 100vh; + --hx-sidebar-toggler-background: var(--bs-gray-500); + --hx-sidebar-item-font-size: 1rem; + --hx-sidebar-item-padding: .75rem; + --hx-sidebar-item-color: var(--bs-body-color); + --hx-sidebar-item-icon-color: var(--bs-primary); + --hx-sidebar-item-border-radius: var(--bs-border-radius); + --hx-sidebar-item-margin: 0; + --hx-sidebar-item-hover-color: var(--bs-primary); + --hx-sidebar-item-hover-background-color: var(--bs-primary-rgb); + --hx-sidebar-item-hover-background-opacity: .1; + --hx-sidebar-item-hover-icon-color: var(--bs-primary); + --hx-sidebar-item-active-color: var(--bs-primary); + --hx-sidebar-item-active-background-color: var(--bs-primary-rgb); + --hx-sidebar-item-active-background-opacity: .1; + --hx-sidebar-item-active-icon-color: var(--bs-primary); + --hx-sidebar-item-active-font-weight: inherit; + --hx-sidebar-parent-item-active-color: var(--hx-sidebar-item-color); + --hx-sidebar-parent-item-active-background-opacity: 0; + --hx-sidebar-parent-item-active-background-color: unset; + --hx-sidebar-parent-item-active-font-weight: 600; + --hx-sidebar-parent-item-active-icon-color: var(--hx-sidebar-item-hover-icon-color); + --hx-sidebar-subitem-font-size: .875rem; + --hx-sidebar-subitem-padding: .5rem; + --hx-sidebar-subitem-margin: 0 0 0 2rem; + --hx-sidebar-header-padding: 1rem; + --hx-sidebar-body-padding: 0 1rem 1rem 1rem; + --hx-sidebar-body-nav-gap: .25rem; + --hx-sidebar-brand-logo-width: 2.5rem; + --hx-sidebar-brand-logo-height: 2.5rem; + --hx-sidebar-brand-shortname-width: 2.5rem; + --hx-sidebar-brand-shortname-height: 2.5rem; + --hx-sidebar-brand-shortname-background-color: var(--bs-primary); + --hx-sidebar-brand-shortname-border-radius: .625rem; + --hx-sidebar-brand-shortname-color: var(--bs-white); + --hx-sidebar-brand-shortname-font-weight: 600; + --hx-sidebar-brand-name-color: var(--bs-body-color); + --hx-sidebar-brand-name-font-weight: 600; + --hx-sidebar-brand-gap: .5rem; + --hx-sidebar-footer-padding: 0 1rem 1rem 1rem; + --hx-sidebar-footer-item-padding: .75rem; + --hx-sidebar-footer-item-margin: 0; + --hx-sidebar-footer-item-font-size: 1rem; + --hx-sidebar-footer-item-content-gap: .75rem; + --hx-sidebar-footer-item-color: var(--bs-body-color); + --hx-sidebar-footer-item-radius: unset; + --hx-sidebar-footer-item-hover-background-color: unset; + --hx-sidebar-footer-item-hover-background-opacity: unset; + --hx-sidebar-footer-item-hover-color: var(--bs-body-color); + + /* HxProgressOverlay */ + --hx-progress-overlay-color: var(--bs-white); + --hx-progress-overlay-opacity: .65; + + /* HxEditForm */ + --hx-form-spacing: 1.25rem; + + /* HxChipList */ + --hx-chip-list-chip-margin: 0.75rem 0.25rem .375rem 0; + --hx-chip-list-chip-remove-btn-margin: 0 0 0 .25rem; + --hx-chip-list-chip-remove-btn-opacity: .75; + --hx-chip-list-chip-reset-btn-padding: .35em .65em; + --hx-chip-list-chip-label-font-weight: 400; + --hx-chip-list-chip-label-margin: .25rem; + --hx-chip-list-chip-label-opacity: .75; + --hx-chip-list-gap: .25rem; + + /* HxContextMenu */ + --hx-context-menu-button-color: unset; + --hx-context-menu-button-border: unset; + --hx-context-menu-button-border-radius: var(--bs-border-radius-sm); + --hx-context-menu-button-padding: 0 .25rem; + --hx-context-menu-button-hover-background: var(--bs-secondary-bg); + --hx-context-menu-button-font-size: inherit; + --hx-context-menu-item-icon-margin: 0 .5rem 0 0; + + /* HxDropdown */ + --hx-dropdown-menu-item-icon-margin: 0 .5rem 0 0; + + /* HxGrid */ + --hx-grid-button-hover-background: var(--bs-secondary-bg); + --hx-grid-button-border-radius: var(--bs-border-radius); + --hx-grid-sorted-icon-color: var(--bs-primary); + --hx-grid-progress-indicator-color: var(--bs-primary); + + /* HxInputFileDropZone */ + --hx-input-file-drop-zone-border-width: 1px; + --hx-input-file-drop-zone-box-shadow: none; + --hx-input-file-drop-zone-hover-box-shadow: 0 0 0 .25rem rgba(var(--bs-primary-rgb), .25); + --hx-input-file-drop-zone-border-color: var(--bs-border-color); + --hx-input-file-drop-zone-disabled-color: var(--bs-secondary-color); + --hx-input-file-drop-zone-disabled-background-color: var(--bs-secondary-bg); + --hx-input-file-drop-zone-background-color: transparent; + --hx-input-file-drop-zone-hover-background-color: rgba(var(--bs-primary-rgb), .1); + --hx-input-file-drop-zone-hover-border-color: var(--bs-primary); + --hx-input-file-drop-zone-border-radius: var(--bs-border-radius-lg); + --hx-input-file-drop-zone-margin: 0; + --hx-input-file-drop-zone-padding: 3rem; + + /* HxCalendar */ + --hx-calendar-day-hover-background: var(--bs-tertiary-bg); + --hx-calendar-day-hover-border: none; + --hx-calendar-day-selected-background: var(--bs-primary); + --hx-calendar-day-selected-color: var(--bs-white); + --hx-calendar-day-selected-border: none; + --hx-calendar-day-out-color: var(--bs-tertiary-color); + --hx-calendar-day-in-color: unset; + --hx-calendar-day-disabled-opacity: .5; + --hx-calendar-day-disabled-text-decoration: line-through; + --hx-calendar-day-names-color: unset; + --hx-calendar-day-names-font-weight: 700; + --hx-calendar-navigation-button-hover-background: var(--bs-tertiary-bg); + --hx-calendar-navigation-button-focus-box-shadow: 0 0 0 0.25rem rgb(0 157 224 / 25%); + --hx-calendar-navigation-button-text-color: var(--bs-tertiary-color); + --hx-calendar-day-today-border: none; + --hx-calendar-day-today-background: var(--bs-primary-rgb); + --hx-calendar-day-today-background-opacity: .1; + --hx-calendar-day-today-color: var(--bs-primary); + --hx-calendar-day-border-radius: var(--bs-border-radius-sm); + --hx-calendar-day-padding: .375rem .5rem; + --hx-calendar-day-width: 2.25rem; + --hx-calendar-day-height: 2.25rem; + --hx-calendar-day-spacing: .125rem; + --hx-calendar-font-size: .875rem; + + /* Offcanvas */ + --hx-offcanvas-close-icon-font-size: 2rem; + --hx-offcanvas-footer-padding-y: 1rem; + --hx-offcanvas-footer-padding-x: 1rem; + --hx-offcanvas-horizontal-width-sm: 400px; + --hx-offcanvas-horizontal-width-lg: 600px; + + /* ListLayout */ + --hx-list-layout-table-font-size: .875rem; + + /* HxMultiSelect */ + --hx-multi-select-background-color: var(--bs-body-bg); + --hx-multi-select-dropdown-menu-height: 300px; + --hx-multi-select-filter-input-icon-opacity: .25; + --hx-multi-select-dropdown-menu-width: 100%; + + /* TagInput */ + --hx-input-tags-tag-gap: .25rem; + --hx-input-tags-input-width: 3em; + --hx-input-tags-input-placeholder-color: var(--bs-secondary-color); + --hx-input-tags-naked-font-size-lg: 1.25em; + --hx-input-tags-naked-font-size-sm: .875em; + --hx-input-tags-control-focused-box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25); + --hx-input-tags-control-focused-border-color: rgba(var(--bs-primary-rgb), .3); + --hx-input-tags-add-button-text-margin: 0 0 0 .25rem; + --hx-input-tags-add-button-disabled-opacity: .65; + --hx-input-tags-remove-button-margin: 0 0 0 .25rem; + --hx-input-tags-dropdown-item-highlighted-background-color: var(--bs-tertiary-bg); + + /* TreeView */ + --hx-tree-view-item-border-radius: var(--bs-border-radius-sm); + --hx-tree-view-item-border-width: 0; + --hx-tree-view-item-border-style: unset; + --hx-tree-view-item-border-color: unset; + --hx-tree-view-item-color: var(--bs-bg-color); + --hx-tree-view-item-hover-color: var(--bs-primary); + --hx-tree-view-item-selected-color: var(--bs-primary); + --hx-tree-view-item-background: transparent; + --hx-tree-view-item-hover-background: var(--bs-primary-rgb); + --hx-tree-view-item-hover-background-opacity: .1; + --hx-tree-view-item-selected-background: var(--bs-primary-rgb); + --hx-tree-view-item-spacer-width: 1rem; + --hx-tree-view-item-font-size: .75rem; + --hx-tree-view-item-padding: .25rem .5rem; + --hx-tree-view-item-margin: 0 0 .125rem 0; + --hx-tree-view-item-gap: .25rem; + --hx-tree-view-expander-container-width: 1rem; + + /* HxProgressIndicator */ + --hx-progress-indicator-background: var(--bs-body-bg); + --hx-progress-indicator-box-shadow: var(--bs-box-shadow); + --hx-progress-indicator-border-color: var(--bs-border-color); + --hx-progress-indicator-spinner-color: var(--bs-primary); + + /* HxToastContainer */ + --hx-toast-container-margin: .5rem; + + /* HxSearchBox */ + --hx-search-box-item-icon-margin: 0 .5rem 0 0; + --hx-search-box-item-icon-font-size: inherit; + --hx-search-box-item-title-font-size: inherit; + --hx-search-box-item-title-color: inherit; + --hx-search-box-item-subtitle-color: var(--bs-secondary); + --hx-search-box-item-subtitle-font-size: .75rem; + --hx-search-box-item-highlighted-background-color: var(--bs-tertiary-bg); + --hx-search-box-dropdown-menu-height: 300px; + --hx-search-box-dropdown-menu-width: 100%; + --hx-search-box-input-search-icon-color: unset; + --hx-search-box-input-clear-icon-color: unset; +} + +form { + display: flex; + flex-direction: column; + gap: var(--hx-form-spacing); +} + +form > .hx-button { + align-self: start; +} \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Tests/Havit.Blazor.Components.Web.Tests.csproj b/Havit.Blazor.Components.Web.Tests/Havit.Blazor.Components.Web.Tests.csproj index d9fd8728..71fa51a5 100644 --- a/Havit.Blazor.Components.Web.Tests/Havit.Blazor.Components.Web.Tests.csproj +++ b/Havit.Blazor.Components.Web.Tests/Havit.Blazor.Components.Web.Tests.csproj @@ -1,7 +1,7 @@  - net8.0;net6.0 + net9.0;net8.0 enable false true diff --git a/Havit.Blazor.Components.Web/Forms/InputType.cs b/Havit.Blazor.Components.Web/Forms/InputType.cs index 0f8cf365..caccd021 100644 --- a/Havit.Blazor.Components.Web/Forms/InputType.cs +++ b/Havit.Blazor.Components.Web/Forms/InputType.cs @@ -4,13 +4,12 @@ /// Enum for HTML input types. ///
/// -/// As the enum is currently used only for the HxInputText component, only relevant types are included. -/// As all the values will be needed, they can be added later (add restrictions/validation to HxInputText then). +/// As the enum is currently used only for HxInputText and HxInputNumber components, only relevant types are included. /// public enum InputType { /// - /// The default value. A single-line text field. Line-breaks are automatically removed from the input value. + /// A single-line text field. Line-breaks are automatically removed from the input value. /// Text = 0, @@ -43,4 +42,9 @@ public enum InputType /// keyboard in supporting browsers and devices with dynamic keyboards. /// Url, + + /// + /// A control for entering a number. Displays a numeric keypad in some devices with dynamic keypads. + /// + Number, } diff --git a/Havit.Blazor.Components.Web/Havit.Blazor.Components.Web.csproj b/Havit.Blazor.Components.Web/Havit.Blazor.Components.Web.csproj index 8ecacd1f..96bc323b 100644 --- a/Havit.Blazor.Components.Web/Havit.Blazor.Components.Web.csproj +++ b/Havit.Blazor.Components.Web/Havit.Blazor.Components.Web.csproj @@ -1,7 +1,7 @@  - net8.0;net6.0 + net9.0;net8.0 enable @@ -29,7 +29,6 @@ - diff --git a/Havit.Blazor.Components.Web/JSRuntimeExtensions.cs b/Havit.Blazor.Components.Web/JSRuntimeExtensions.cs index 45f45602..fd0709e3 100644 --- a/Havit.Blazor.Components.Web/JSRuntimeExtensions.cs +++ b/Havit.Blazor.Components.Web/JSRuntimeExtensions.cs @@ -8,10 +8,13 @@ public static ValueTask ImportModuleAsync(this IJSRuntime js { Contract.Requires(!String.IsNullOrWhiteSpace(modulePath)); +#if !NET9_0_OR_GREATER + // pre-NET9 does not support StaticAssets with ImportMap if (assemblyForVersionInfo is not null) { modulePath = modulePath + "?v=" + GetAssemblyVersionIdentifierForUri(assemblyForVersionInfo); } +#endif return jsRuntime.InvokeAsync("import", modulePath); } @@ -19,7 +22,13 @@ internal static ValueTask ImportHavitBlazorWebModuleAsync(th { s_versionIdentifierHavitBlazorWeb ??= GetAssemblyVersionIdentifierForUri(typeof(HxDynamicElement).Assembly); - var path = "./_content/Havit.Blazor.Components.Web/" + moduleNameWithoutExtension + ".js?v=" + s_versionIdentifierHavitBlazorWeb; + var path = "./_content/Havit.Blazor.Components.Web/" + moduleNameWithoutExtension + ".js"; + +#if !NET9_0_OR_GREATER + // pre-NET9 does not support StaticAssets with ImportMap + path = path + "?v=" + s_versionIdentifierHavitBlazorWeb; +#endif + return jsRuntime.InvokeAsync("import", path); } private static string s_versionIdentifierHavitBlazorWeb; diff --git a/Havit.Blazor.Components.Web/wwwroot/HxInputFileCore.js b/Havit.Blazor.Components.Web/wwwroot/HxInputFileCore.js index 8b0f5546..bee4bac8 100644 --- a/Havit.Blazor.Components.Web/wwwroot/HxInputFileCore.js +++ b/Havit.Blazor.Components.Web/wwwroot/HxInputFileCore.js @@ -1,22 +1,22 @@ export function upload(inputElementId, hxInputFileDotnetObjectReference, uploadEndpointUrl, accessToken, maxFileSize, maxParallelUploads, uploadHttpMethod) { - var inputElement = document.getElementById(inputElementId); - var dotnetReference = hxInputFileDotnetObjectReference; - var files = inputElement.files; - var totalSize = 0; + const inputElement = document.getElementById(inputElementId); + const dotnetReference = hxInputFileDotnetObjectReference; + const files = inputElement.files; + let totalSize = 0; inputElement.requests = new Array(); inputElement.cancelled = false; - var nextFile = maxParallelUploads; + let nextFile = maxParallelUploads; - var completedUploads = 0; + let completedUploads = 0; if (files.length === 0) { dotnetReference.invokeMethodAsync('HxInputFileCore_HandleUploadCompleted', 0, 0); return; } - for (var i = 0; i < Math.min(files.length, maxParallelUploads); i++) { + for (let i = 0; i < Math.min(files.length, maxParallelUploads); i++) { (function (curr) { uploadFile(curr); }(i)); @@ -28,10 +28,10 @@ return; } - var file = files[index]; + const file = files[index]; if (maxFileSize && (file.size > maxFileSize)) { - let msg = `[${index}]${file.name} client pre-check: File size ${file.size} bytes exceeds MaxFileSize limit ${maxFileSize} bytes.`; + const msg = `[${index}]${file.name} client pre-check: File size ${file.size} bytes exceeds MaxFileSize limit ${maxFileSize} bytes.`; console.warn(msg); dotnetReference.invokeMethodAsync('HxInputFileCore_HandleFileUploaded', index, file.name, file.size, file.type, file.lastModified, 413, msg); @@ -48,10 +48,10 @@ totalSize = totalSize + file.size; } - var data = new FormData(); + const data = new FormData(); data.append('file', file, file.name); - var request = new XMLHttpRequest(); + const request = new XMLHttpRequest(); inputElement.requests.push(request); request.open(uploadHttpMethod, uploadEndpointUrl, true); @@ -83,13 +83,13 @@ } export function getFiles(inputElementId) { - var inputElement = document.getElementById(inputElementId); + const inputElement = document.getElementById(inputElementId); inputElement.hxInputFileNextFileIndex = 0; return Array.from(inputElement.files).map(e => { return { index: inputElement.hxInputFileNextFileIndex++, name: e.name, lastModified: e.lastModified, size: e.size, type: e.type }; }); } export function reset(inputElementId) { - var inputElement = document.getElementById(inputElementId); + const inputElement = document.getElementById(inputElementId); if (!inputElement) { return; } @@ -107,6 +107,6 @@ export function reset(inputElementId) { } export function dispose(inputElementId) { - var inputElement = document.getElementById(inputElementId); + const inputElement = document.getElementById(inputElementId); inputElement.hxInputFileDotnetObjectReference = null; } \ No newline at end of file diff --git a/Havit.Blazor.Documentation.Server/.config/dotnet-tools.json b/Havit.Blazor.Documentation.Server/.config/dotnet-tools.json new file mode 100644 index 00000000..4f487990 --- /dev/null +++ b/Havit.Blazor.Documentation.Server/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "9.0.0", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/Havit.Blazor.Documentation.Server/App.razor b/Havit.Blazor.Documentation.Server/App.razor new file mode 100644 index 00000000..f4641e1e --- /dev/null +++ b/Havit.Blazor.Documentation.Server/App.razor @@ -0,0 +1,66 @@ +@using Havit.Blazor.Documentation.Shared.Components.DocColorMode +@inject IDocColorModeProvider DocColorModeProvider + + + + + + + + + + + @* CSS Isolation (includes *.lib.css) *@ + @* Prism: Syntax Highlighting *@ + @* Prism: Syntax Highlighting *@ + + + + + + + + + + +
+ + + +
+ An unhandled error has occurred. + Reload + 🗙 +
+ +
+ + + + + @((MarkupString)HxSetup.RenderBootstrapJavaScriptReference()) + + @* Prism: Syntax Highlighting *@ + + + + + diff --git a/Havit.Blazor.Documentation.Server/DocColorModeServerResolver.cs b/Havit.Blazor.Documentation.Server/DocColorModeServerResolver.cs deleted file mode 100644 index 25e37ca5..00000000 --- a/Havit.Blazor.Documentation.Server/DocColorModeServerResolver.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Havit.Blazor.Documentation.Shared.Components.DocColorMode; - -namespace Havit.Blazor.Documentation.Server; - -public class DocColorModeServerResolver : IDocColorModeResolver -{ - private readonly IHttpContextAccessor _httpContextAccessor; - - public DocColorModeServerResolver(IHttpContextAccessor httpContextAccessor) - { - _httpContextAccessor = httpContextAccessor; - } - - public ColorMode GetColorMode() - { - var cookie = _httpContextAccessor.HttpContext?.Request?.Cookies["ColorMode"]; - if (cookie == null) - { - return ColorMode.Auto; - } - if (Enum.TryParse(cookie, ignoreCase: true, out var mode)) - { - return mode; - } - return ColorMode.Auto; - } -} diff --git a/Havit.Blazor.Documentation.Server/Error.razor b/Havit.Blazor.Documentation.Server/Error.razor new file mode 100644 index 00000000..26e0542c --- /dev/null +++ b/Havit.Blazor.Documentation.Server/Error.razor @@ -0,0 +1,25 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (!string.IsNullOrEmpty(_requestId)) +{ +

+ Request ID: @_requestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

\ No newline at end of file diff --git a/Havit.Blazor.Documentation.Server/Error.razor.cs b/Havit.Blazor.Documentation.Server/Error.razor.cs new file mode 100644 index 00000000..8db31083 --- /dev/null +++ b/Havit.Blazor.Documentation.Server/Error.razor.cs @@ -0,0 +1,15 @@ +using System.Diagnostics; + +namespace Havit.Blazor.Documentation.Server; + +public partial class Error +{ + [CascadingParameter] private HttpContext HttpContext { get; set; } + + private string _requestId; + + protected override void OnInitialized() + { + _requestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; + } +} \ No newline at end of file diff --git a/Havit.Blazor.Documentation.Server/GlobalUsings.cs b/Havit.Blazor.Documentation.Server/GlobalUsings.cs index 9fc46609..14d6a434 100644 --- a/Havit.Blazor.Documentation.Server/GlobalUsings.cs +++ b/Havit.Blazor.Documentation.Server/GlobalUsings.cs @@ -1,2 +1,4 @@ global using Havit.Blazor.Components.Web; -global using Havit.Blazor.Components.Web.Bootstrap; \ No newline at end of file +global using Havit.Blazor.Components.Web.Bootstrap; +global using Havit.Diagnostics.Contracts; +global using Microsoft.AspNetCore.Components; diff --git a/Havit.Blazor.Documentation.Server/Havit.Blazor.Documentation.Server.csproj b/Havit.Blazor.Documentation.Server/Havit.Blazor.Documentation.Server.csproj index 43bf6594..32a94869 100644 --- a/Havit.Blazor.Documentation.Server/Havit.Blazor.Documentation.Server.csproj +++ b/Havit.Blazor.Documentation.Server/Havit.Blazor.Documentation.Server.csproj @@ -1,8 +1,10 @@  - net8.0 + net9.0 enable + Havit.Blazor.Documentation.Server + Havit.Blazor.Documentation.Server diff --git a/Havit.Blazor.Documentation.Server/Pages/_Host.cshtml b/Havit.Blazor.Documentation.Server/Pages/_Host.cshtml deleted file mode 100644 index 8f128c8f..00000000 --- a/Havit.Blazor.Documentation.Server/Pages/_Host.cshtml +++ /dev/null @@ -1,9 +0,0 @@ -@page "/" -@namespace Havit.Blazor.Documentation.Server.Pages -@using Havit.Blazor.Documentation -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@{ - Layout = "_Layout"; -} - - diff --git a/Havit.Blazor.Documentation.Server/Pages/_Layout.cshtml b/Havit.Blazor.Documentation.Server/Pages/_Layout.cshtml deleted file mode 100644 index c84bd87d..00000000 --- a/Havit.Blazor.Documentation.Server/Pages/_Layout.cshtml +++ /dev/null @@ -1,76 +0,0 @@ -@namespace Havit.Blazor.Documentation.Server.Pages -@using System.Diagnostics; -@using System.Reflection; -@using System.Web; -@using Havit.Blazor.Documentation -@using Havit.Blazor.Documentation.Shared.Components.DocColorMode; -@using Microsoft.AspNetCore.Components.Web -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@inject IDocColorModeResolver DocColorModeResolver - - - - - - - - - - - @Html.Raw(HxSetup.RenderBootstrapCssReference()) - - @* CSS Isolation *@ - @* Prism: Syntax Highlighting *@ - @* Prism: Syntax Highlighting *@ - @* TODO: Integrate into Blazor CSS bundle *@ - - - - - - - - -
- - @RenderBody() - - - -
- An unhandled error has occurred. - Reload - 🗙 -
- -
- - - @Html.Raw(HxSetup.RenderBootstrapJavaScriptReference()) - - @* Prism: Syntax Highlighting *@ - - - - - - - - diff --git a/Havit.Blazor.Documentation.Server/Program.cs b/Havit.Blazor.Documentation.Server/Program.cs index c5c9154a..8d086b98 100644 --- a/Havit.Blazor.Documentation.Server/Program.cs +++ b/Havit.Blazor.Documentation.Server/Program.cs @@ -1,27 +1,115 @@ +using System.Globalization; +using Havit.Blazor.Documentation.DemoData; +using Havit.Blazor.Documentation.Server.Services; +using Havit.Blazor.Documentation.Services; +using Havit.Blazor.Documentation.Shared.Components.DocColorMode; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using SmartComponents.Inference.OpenAI; +using SmartComponents.LocalEmbeddings; + namespace Havit.Blazor.Documentation.Server; public class Program { public static void Main(string[] args) { - CreateHostBuilder(args).Build().Run(); - } + var builder = WebApplication.CreateBuilder(args); - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }) - .ConfigureAppConfiguration((hostContext, config) => - { - config - .AddJsonFile("appsettings.json", optional: false) - .AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", optional: true) #if DEBUG - .AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.local.json", optional: true) + builder.Configuration.AddJsonFile($"appsettings.Development.local.json", optional: true); #endif - .AddEnvironmentVariables(); + + // enforce en-US culture + var cultureInfo = new CultureInfo("en-US"); + CultureInfo.DefaultThreadCurrentCulture = cultureInfo; + CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; + + // Add services to the container. + builder.Services.AddRazorComponents() + .AddInteractiveWebAssemblyComponents(); + builder.Services.AddControllers(); + + builder.Services.TryAddSingleton(); + builder.Services.AddSingleton(); + + builder.Services.AddHxServices(); + builder.Services.AddHxMessenger(); + builder.Services.AddHxMessageBoxHost(); + + builder.Services.AddSmartComponents() + .WithInferenceBackend(); + builder.Services.AddSingleton(); + + builder.Services.AddTransient(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + builder.Services.AddScoped(); + builder.Services.AddCascadingValue(services => + { + var docColorModeStateProvider = services.GetRequiredService(); + return new DocColorModeCascadingValueSource(docColorModeStateProvider); + }); + + builder.Services.AddTransient(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseWebAssemblyDebugging(); + } + else + { + app.UseExceptionHandler("/Error"); + + // old domain redirect + app.Use(async (context, next) => + { + + if (context.Request.Host.Host.Contains("havit.blazor.cz")) + { + var uriBuilder = new UriBuilder(UriHelper.GetDisplayUrl(context.Request)); + uriBuilder.Host = "havit.blazor.eu"; + context.Response.Redirect(uriBuilder.Uri.ToString(), permanent: true); + + return; + } + + await next(); }); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + // app.UseHsts(); + } + + app.UseHttpsRedirection(); + + app.UseAntiforgery(); + + // SmartComboBox + var embedder = app.Services.GetRequiredService(); + var expenseCategories = embedder.EmbedRange( + ["Groceries", "Utilities", "Rent", "Mortgage", "Car Payment", "Car Insurance", "Health Insurance", "Life Insurance", "Home Insurance", "Gas", "Public Transportation", "Dining Out", "Entertainment", "Travel", "Clothing", "Electronics", "Home Improvement", "Gifts", "Charity", "Education", "Childcare", "Pet Care", "Other"]); + var issueLabels = embedder.EmbedRange( + ["Bug", "Docs", "Enhancement", "Question", "UI (Android)", "UI (iOS)", "UI (Windows)", "UI (Mac)", "Performance", "Security", "Authentication", "Accessibility"]); + app.MapSmartComboBox("/api/SmartComboBox/expense-category", + request => embedder.FindClosest(request.Query, expenseCategories)); + + app.MapSmartComboBox("/api/SmartComboBox/issue-label", + request => embedder.FindClosest(request.Query, issueLabels)); + + + app.MapStaticAssets(); + app.MapControllers(); + app.MapRazorComponents() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(Havit.Blazor.Documentation._Imports).Assembly); + + app.Run(); + } } diff --git a/Havit.Blazor.Documentation.Server/Properties/PublishProfiles/TfsPublish.pubxml b/Havit.Blazor.Documentation.Server/Properties/PublishProfiles/TfsPublish.pubxml index 98747404..eda11281 100644 --- a/Havit.Blazor.Documentation.Server/Properties/PublishProfiles/TfsPublish.pubxml +++ b/Havit.Blazor.Documentation.Server/Properties/PublishProfiles/TfsPublish.pubxml @@ -14,9 +14,9 @@ by editing this MSBuild file. In order to learn more about this please visit htt obj\Release\TfsPublish\DocumentationWeb.zip true havit.blazor.eu - false - net8.0 + true c4cc1c76-bcc9-403a-917d-144868f1215e win-x86 + net9.0
\ No newline at end of file diff --git a/Havit.Blazor.Documentation.Server/Properties/launchSettings.json b/Havit.Blazor.Documentation.Server/Properties/launchSettings.json index 7750b755..7f7e6dbd 100644 --- a/Havit.Blazor.Documentation.Server/Properties/launchSettings.json +++ b/Havit.Blazor.Documentation.Server/Properties/launchSettings.json @@ -8,20 +8,22 @@ } }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Havit.Blazor.Documentation.Server": { - "commandName": "Project", - "launchBrowser": true, - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Havit.Blazor.Documentation.Server": { + "commandName": "Project", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } } } diff --git a/Havit.Blazor.Documentation.Server/Services/ServerHttpContextProxy.cs b/Havit.Blazor.Documentation.Server/Services/ServerHttpContextProxy.cs new file mode 100644 index 00000000..3a2a5c63 --- /dev/null +++ b/Havit.Blazor.Documentation.Server/Services/ServerHttpContextProxy.cs @@ -0,0 +1,17 @@ +using Havit.Blazor.Documentation.Services; + +namespace Havit.Blazor.Documentation.Server.Services; + +public class ServerHttpContextProxy( + IHttpContextAccessor httpContextAccessor) + : IHttpContextProxy +{ + private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; + + public string GetCookieValue(string key) + { + return _httpContextAccessor.HttpContext.Request.Cookies[key]; + } + + public bool IsSupported() => true; +} diff --git a/Havit.Blazor.Documentation.Server/Startup.cs b/Havit.Blazor.Documentation.Server/Startup.cs deleted file mode 100644 index 2a1105b5..00000000 --- a/Havit.Blazor.Documentation.Server/Startup.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Globalization; -using Havit.Blazor.Documentation.DemoData; -using Havit.Blazor.Documentation.Services; -using Havit.Blazor.Documentation.Shared.Components.DocColorMode; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.DependencyInjection.Extensions; -using SmartComponents.Inference.OpenAI; -using SmartComponents.LocalEmbeddings; - -namespace Havit.Blazor.Documentation.Server; - -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - services.AddRazorPages(); - services.TryAddSingleton(); - - services.AddHxServices(); - services.AddHxMessenger(); - services.AddHxMessageBoxHost(); - - services.AddSmartComponents() - .WithInferenceBackend(); - services.AddSingleton(); - - services.AddTransient(); - services.AddSingleton(); - services.AddSingleton(); - services.AddTransient(); - - services.AddTransient(); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - var cultureInfo = new CultureInfo("en-US"); - CultureInfo.DefaultThreadCurrentCulture = cultureInfo; - CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - app.UseWebAssemblyDebugging(); - } - else - { - app.UseExceptionHandler("/Error"); - - // old domain redirect - app.Use(async (context, next) => - { - - if (context.Request.Host.Host.Contains("havit.blazor.cz")) - { - var uriBuilder = new UriBuilder(UriHelper.GetDisplayUrl(context.Request)); - uriBuilder.Host = "havit.blazor.eu"; - context.Response.Redirect(uriBuilder.Uri.ToString(), permanent: true); - - return; - } - - await next(); - }); - } - - app.UseBlazorFrameworkFiles(); - app.UseStaticFiles(); - - app.UseRouting(); - - // SmartComboBox - var embedder = app.ApplicationServices.GetRequiredService(); - var expenseCategories = embedder.EmbedRange( - ["Groceries", "Utilities", "Rent", "Mortgage", "Car Payment", "Car Insurance", "Health Insurance", "Life Insurance", "Home Insurance", "Gas", "Public Transportation", "Dining Out", "Entertainment", "Travel", "Clothing", "Electronics", "Home Improvement", "Gifts", "Charity", "Education", "Childcare", "Pet Care", "Other"]); - var issueLabels = embedder.EmbedRange( - ["Bug", "Docs", "Enhancement", "Question", "UI (Android)", "UI (iOS)", "UI (Windows)", "UI (Mac)", "Performance", "Security", "Authentication", "Accessibility"]); - - app.UseEndpoints(endpoints => - { - endpoints.MapSmartComboBox("/api/SmartComboBox/expense-category", - request => embedder.FindClosest(request.Query, expenseCategories)); - - endpoints.MapSmartComboBox("/api/SmartComboBox/issue-label", - request => embedder.FindClosest(request.Query, issueLabels)); - - endpoints.MapRazorPages(); - endpoints.MapControllers(); - endpoints.MapFallbackToPage("/_Host"); - }); - } -} \ No newline at end of file diff --git a/Havit.Blazor.Documentation.Server/_Imports.razor b/Havit.Blazor.Documentation.Server/_Imports.razor new file mode 100644 index 00000000..b45655fe --- /dev/null +++ b/Havit.Blazor.Documentation.Server/_Imports.razor @@ -0,0 +1,23 @@ +@using System.ComponentModel.DataAnnotations; +@using System.Globalization +@using System.Net.Http +@using System.Net.Http.Json +@using System.Text.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop + +@using Havit.Blazor.Documentation +@using Havit.Blazor.Documentation.Shared +@using Havit.Blazor.Documentation.Shared.Components +@using Havit.Blazor.Documentation.Pages.Components.Common + +@using Havit.Blazor.Components.Web +@using Havit.Blazor.Components.Web.Bootstrap; +@using Havit.Blazor.Components.Web.Bootstrap.Smart; + +@using Havit.Blazor.Documentation.GenericTypePlaceholders; +@using Havit.Blazor.Documentation.DemoData; diff --git a/Havit.Blazor.Documentation.Tests/Havit.Blazor.Documentation.Tests.csproj b/Havit.Blazor.Documentation.Tests/Havit.Blazor.Documentation.Tests.csproj index caed48a8..10d72beb 100644 --- a/Havit.Blazor.Documentation.Tests/Havit.Blazor.Documentation.Tests.csproj +++ b/Havit.Blazor.Documentation.Tests/Havit.Blazor.Documentation.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable false true diff --git a/Havit.Blazor.Documentation/DemoData/DataFragmentResult.cs b/Havit.Blazor.Documentation/DemoData/DataFragmentResult.cs new file mode 100644 index 00000000..5309c88b --- /dev/null +++ b/Havit.Blazor.Documentation/DemoData/DataFragmentResult.cs @@ -0,0 +1,8 @@ +namespace Havit.Blazor.Documentation.DemoData; + +public record DataFragmentResult +{ + public required List Data { get; init; } + + public required int TotalCount { get; init; } +} diff --git a/Havit.Blazor.Documentation/DemoData/DemoDataService.EmployeesData.cs b/Havit.Blazor.Documentation/DemoData/DemoDataService.EmployeesData.cs new file mode 100644 index 00000000..28913116 --- /dev/null +++ b/Havit.Blazor.Documentation/DemoData/DemoDataService.EmployeesData.cs @@ -0,0 +1,531 @@ +namespace Havit.Blazor.Documentation.DemoData; + +public partial class DemoDataService +{ + private List GenerateEmployees() + { + return new() + { + new EmployeeDto() + { + Id = 1, + Name = "John Smith", + Email = "john.smith@company.demo", + Phone = "+420 123 456 789", + Salary = 20000M, + Position = "Software Engineer", + Location = "Prague", + }, + new EmployeeDto() + { + Id = 2, + Name = "Mary Johnson", + Email = "mary.johnson@company.demo", + Phone = "+420 234 567 890", + Salary = 25000M, + Position = "Product Manager", + Location = "San Francisco", + }, + new EmployeeDto() + { + Id = 3, + Name = "David Lee", + Email = "david.lee@company.demo", + Phone = "+420 345 678 901", + Salary = 18000M, + Position = "Sales Representative", + Location = "New York", + }, + new EmployeeDto() + { + Id = 4, + Name = "Jasmine Kim", + Email = "jasmine.kim@company.demo", + Phone = "+420 456 789 012", + Salary = 22000M, + Position = "Data Analyst", + Location = "Seoul", + }, + new EmployeeDto() + { + Id = 5, + Name = "Alexandra Brown", + Email = "alexandra.brown@company.demo", + Phone = "+420 567 890 123", + Salary = 28000M, + Position = "Marketing Manager", + Location = "London", + }, + new EmployeeDto() + { + Id = 6, + Name = "Robert Garcia", + Email = "robert.garcia@company.demo", + Phone = "+420 789 012 345", + Salary = 23000M, + Position = "Software Engineer", + Location = "Barcelona", + }, + new EmployeeDto() + { + Id = 7, + Name = "Olivia Smith", + Email = "olivia.smith@company.demo", + Phone = "+420 890 123 456", + Salary = 26000M, + Position = "Product Manager", + Location = "Sydney", + }, + new EmployeeDto() + { + Id = 8, + Name = "Mason Johnson", + Email = "mason.johnson@company.demo", + Phone = "+420 012 345 678", + Salary = 20000M, + Position = "Sales Representative", + Location = "Houston", + }, + new EmployeeDto() + { + Id = 9, + Name = "Ava Lee", + Email = "ava.lee@company.demo", + Phone = "+420 123 456 789", + Salary = 24000M, + Position = "Data Analyst", + Location = "Tokyo", + }, + new EmployeeDto() + { + Id = 10, + Name = "Jacob Kim", + Email = "jacob.kim@company.demo", + Phone = "+420 234 567 890", + Salary = 27000M, + Position = "Marketing Manager", + Location = "Paris", + }, + new EmployeeDto() + { + Id = 11, + Name = "Samuel Adams", + Email = "samuel.adams@company.demo", + Phone = "+420 789 012 345", + Salary = 23000M, + Position = "Software Developer", + Location = "Boston", + }, + new EmployeeDto() + { + Id = 12, + Name = "Emily Park", + Email = "emily.park@company.demo", + Phone = "+420 890 123 456", + Salary = 26000M, + Position = "Marketing Coordinator", + Location = "Vancouver", + }, + new EmployeeDto() + { + Id = 13, + Name = "Nathan Williams", + Email = "nathan.williams@company.demo", + Phone = "+420 012 345 678", + Salary = 21000M, + Position = "Sales Manager", + Location = "Sydney", + }, + new EmployeeDto() + { + Id = 14, + Name = "Abby Kim", + Email = "abby.kim@company.demo", + Phone = "+420 123 456 789", + Salary = 24000M, + Position = "Data Scientist", + Location = "Los Angeles", + }, + new EmployeeDto() + { + Id = 15, + Name = "Daniel Choi", + Email = "daniel.choi@company.demo", + Phone = "+420 234 567 890", + Salary = 27000M, + Position = "Software Engineer", + Location = "Seoul", + }, + new EmployeeDto() + { + Id = 16, + Name = "Hannah Garcia", + Email = "hannah.garcia@company.demo", + Phone = "+420 123 456 789", + Salary = 23000M, + Position = "Software Developer", + Location = "Miami", + }, + new EmployeeDto() + { + Id = 17, + Name = "William Chen", + Email = "william.chen@company.demo", + Phone = "+420 234 567 890", + Salary = 26000M, + Position = "Business Analyst", + Location = "Singapore", + }, + new EmployeeDto() + { + Id = 18, + Name = "Ethan Davis", + Email = "ethan.davis@company.demo", + Phone = "+420 345 678 901", + Salary = 21000M, + Position = "Sales Associate", + Location = "Houston", + }, + new EmployeeDto() + { + Id = 19, + Name = "Isabella Kim", + Email = "isabella.kim@company.demo", + Phone = "+420 456 789 012", + Salary = 24000M, + Position = "Marketing Coordinator", + Location = "Melbourne", + }, + new EmployeeDto() + { + Id = 20, + Name = "Jackson Brown", + Email = "jackson.brown@company.demo", + Phone = "+420 567 890 123", + Salary = 27000M, + Position = "Project Manager", + Location = "Toronto", + }, + new EmployeeDto() + { + Id = 21, + Name = "Ella Davis", + Email = "ella.davis@company.demo", + Phone = "+420 123 456 789", + Salary = 23000M, + Position = "Software Developer", + Location = "Los Angeles", + }, + new EmployeeDto() + { + Id = 22, + Name = "Ryan Nguyen", + Email = "ryan.nguyen@company.demo", + Phone = "+420 234 567 890", + Salary = 26000M, + Position = "Business Analyst", + Location = "Ho Chi Minh City", + }, + new EmployeeDto() + { + Id = 23, + Name = "Sophie Hernandez", + Email = "sophie.hernandez@company.demo", + Phone = "+420 345 678 901", + Salary = 21000M, + Position = "Sales Associate", + Location = "Buenos Aires", + }, + new EmployeeDto() + { + Id = 24, + Name = "Alexander Kim", + Email = "alexander.kim@company.demo", + Phone = "+420 456 789 012", + Salary = 24000M, + Position = "Marketing Coordinator", + Location = "Vancouver", + }, + new EmployeeDto() + { + Id = 25, + Name = "Benjamin Brown", + Email = "benjamin.brown@company.demo", + Phone = "+420 567 890 123", + Salary = 27000M, + Position = "Project Manager", + Location = "Berlin", + }, + new EmployeeDto() + { + Id = 26, + Name = "Robert Jackson", + Email = "robert.jackson@company.demo", + Phone = "+420 678 901 234", + Salary = 30000M, + Position = "Senior Software Engineer", + Location = "Chicago", + }, + new EmployeeDto() + { + Id = 27, + Name = "Elizabeth Martinez", + Email = "elizabeth.martinez@company.demo", + Phone = "+420 789 012 345", + Salary = 32000M, + Position = "Senior Product Manager", + Location = "Toronto", + }, + new EmployeeDto() + { + Id = 28, + Name = "William Davis", + Email = "william.davis@company.demo", + Phone = "+420 890 123 456", + Salary = 27000M, + Position = "Sales Manager", + Location = "Sydney", + }, + new EmployeeDto() + { + Id = 29, + Name = "Sophia Lee", + Email = "sophia.lee@company.demo", + Phone = "+420 901 234 567", + Salary = 29000M, + Position = "Senior Data Analyst", + Location = "Tokyo", + }, + new EmployeeDto() + { + Id = 30, + Name = "Gabriel Garcia", + Email = "gabriel.garcia@company.demo", + Phone = "+420 012 345 678", + Salary = 34000M, + Position = "Senior Marketing Manager", + Location = "Rio de Janeiro", + }, + new EmployeeDto() + { + Id = 31, + Name = "Jane Smith", + Email = "jane.smith@company.demo", + Phone = "+420 123 456 789", + Salary = 20000M, + Position = "Software Engineer", + Location = "Prague" + }, + new EmployeeDto() + { + Id = 32, + Name = "Jane Doe", + Email = "jane.doe@company.demo", + Phone = "+420 987 654 321", + Salary = 25000M, + Position = "Product Manager", + Location = "Prague" + }, + new EmployeeDto() + { + Id = 33, + Name = "Bob Johnson", + Email = "bob.johnson@company.demo", + Phone = "+420 555 555 555", + Salary = 18000M, + Position = "Sales Representative", + Location = "Brno" + }, + new EmployeeDto() + { + Id = 34, + Name = "Sarah Lee", + Email = "sarah.lee@company.demo", + Phone = "+420 111 222 333", + Salary = 22000M, + Position = "Marketing Coordinator", + Location = "Ostrava" + }, + new EmployeeDto() + { + Id = 35, + Name = "David Kim", + Email = "david.kim@company.demo", + Phone = "+420 999 888 777", + Salary = 30000M, + Position = "Senior Software Engineer", + Location = "Prague" + }, + new EmployeeDto() + { + Id = 36, + Name = "Eva Novak", + Email = "eva.novak@company.demo", + Phone = "+420 777 777 777", + Salary = 22000M, + Position = "HR Manager", + Location = "Brno" + }, + new EmployeeDto() + { + Id = 37, + Name = "Adam Smith", + Email = "adam.smith@company.demo", + Phone = "+420 333 333 333", + Salary = 24000M, + Position = "Marketing Manager", + Location = "Ostrava" + }, + new EmployeeDto() + { + Id = 38, + Name = "Linda Lee", + Email = "linda.lee@company.demo", + Phone = "+420 222 222 222", + Salary = 28000M, + Position = "Senior Product Manager", + Location = "Prague" + }, + new EmployeeDto() + { + Id = 39, + Name = "Peter Brown", + Email = "peter.brown@company.demo", + Phone = "+420 111 111 111", + Salary = 19000M, + Position = "Sales Manager", + Location = "Brno" + }, + new EmployeeDto() + { + Id = 40, + Name = "Nina Black", + Email = "nina.black@company.demo", + Phone = "+420 999 999 999", + Salary = 21000M, + Position = "Marketing Specialist", + Location = "Ostrava" + }, + new EmployeeDto() + { + Id = 41, + Name = "Mark Davis", + Email = "mark.davis@company.demo", + Phone = "+420 888 888 888", + Salary = 27000M, + Position = "Software Architect", + Location = "Prague" + }, + new EmployeeDto() + { + Id = 42, + Name = "Jack Green", + Email = "jack.green@company.demo", + Phone = "+420 666 666 666", + Salary = 22000M, + Position = "Software Developer", + Location = "Brno" + }, + new EmployeeDto() + { + Id = 43, + Name = "Emily Jones", + Email = "emily.jones@company.demo", + Phone = "+420 555 444 333", + Salary = 24000M, + Position = "Product Owner", + Location = "Ostrava" + }, + new EmployeeDto() + { + Id = 44, + Name = "Chris Wilson", + Email = "chris.wilson@company.demo", + Phone = "+420 777 888 999", + Salary = 18000M, + Position = "Sales Representative", + Location = "Prague" + }, + new EmployeeDto() + { + Id = 45, + Name = "Olivia Taylor", + Email = "olivia.taylor@company.demo", + Phone = "+420 111 222 333", + Salary = 20000M, + Position = "Marketing Specialist", + Location = "Brno" + }, + new EmployeeDto() + { + Id = 46, + Name = "Harry Brown", + Email = "harry.brown@company.demo", + Phone = "+420 444 555 666", + Salary = 26000M, + Position = "Senior Software Engineer", + Location = "Ostrava" + }, + new EmployeeDto() + { + Id = 47, + Name = "Sophia Wilson", + Email = "sophia.wilson@company.demo", + Phone = "+420 333 444 555", + Salary = 23000M, + Position = "Product Manager", + Location = "Prague" + }, + new EmployeeDto() + { + Id = 48, + Name = "Lucas Miller", + Email = "lucas.miller@company.demo", + Phone = "+420 777 666 555", + Salary = 21000M, + Position = "Software Engineer", + Location = "Brno" + }, + new EmployeeDto() + { + Id = 49, + Name = "Mia Davis", + Email = "mia.davis@company.demo", + Phone = "+420 888 777 666", + Salary = 22000M, + Position = "Marketing Coordinator", + Location = "Ostrava" + }, + new EmployeeDto() + { + Id = 50, + Name = "Alexander Wilson", + Email = "alexander.wilson@company.demo", + Phone = "+420 222 333 444", + Salary = 28000M, + Position = "Product Owner", + Location = "Prague" + }, + new EmployeeDto() + { + Id = 51, + Name = "Charlotte Harris", + Email = "charlotte.harris@company.demo", + Phone = "+420 111 555 999", + Salary = 19000M, + Position = "Sales Representative", + Location = "Brno" + }, + new EmployeeDto() + { + Id = 52, + Name = "Dominik Johnson", + Email = "dominik.johnson@company.demo", + Phone = "+420 222 555 999", + Salary = 45000M, + Position = "CEO", + Location = "Prague" + } + }; + } +} diff --git a/Havit.Blazor.Documentation/DemoData/DemoDataService.cs b/Havit.Blazor.Documentation/DemoData/DemoDataService.cs index 42142bf5..e536da8c 100644 --- a/Havit.Blazor.Documentation/DemoData/DemoDataService.cs +++ b/Havit.Blazor.Documentation/DemoData/DemoDataService.cs @@ -3,7 +3,7 @@ namespace Havit.Blazor.Documentation.DemoData; -public class DemoDataService : IDemoDataService +public partial class DemoDataService : IDemoDataService { private readonly ILogger _logger; @@ -12,531 +12,9 @@ public class DemoDataService : IDemoDataService public DemoDataService(ILogger logger) { _logger = logger; - - _employees = new() - { - new EmployeeDto() - { - Id = 1, - Name = "John Smith", - Email = "john.smith@company.demo", - Phone = "+420 123 456 789", - Salary = 20000M, - Position = "Software Engineer", - Location = "Prague", - }, - new EmployeeDto() - { - Id = 2, - Name = "Mary Johnson", - Email = "mary.johnson@company.demo", - Phone = "+420 234 567 890", - Salary = 25000M, - Position = "Product Manager", - Location = "San Francisco", - }, - new EmployeeDto() - { - Id = 3, - Name = "David Lee", - Email = "david.lee@company.demo", - Phone = "+420 345 678 901", - Salary = 18000M, - Position = "Sales Representative", - Location = "New York", - }, - new EmployeeDto() - { - Id = 4, - Name = "Jasmine Kim", - Email = "jasmine.kim@company.demo", - Phone = "+420 456 789 012", - Salary = 22000M, - Position = "Data Analyst", - Location = "Seoul", - }, - new EmployeeDto() - { - Id = 5, - Name = "Alexandra Brown", - Email = "alexandra.brown@company.demo", - Phone = "+420 567 890 123", - Salary = 28000M, - Position = "Marketing Manager", - Location = "London", - }, - new EmployeeDto() - { - Id = 6, - Name = "Robert Garcia", - Email = "robert.garcia@company.demo", - Phone = "+420 789 012 345", - Salary = 23000M, - Position = "Software Engineer", - Location = "Barcelona", - }, - new EmployeeDto() - { - Id = 7, - Name = "Olivia Smith", - Email = "olivia.smith@company.demo", - Phone = "+420 890 123 456", - Salary = 26000M, - Position = "Product Manager", - Location = "Sydney", - }, - new EmployeeDto() - { - Id = 8, - Name = "Mason Johnson", - Email = "mason.johnson@company.demo", - Phone = "+420 012 345 678", - Salary = 20000M, - Position = "Sales Representative", - Location = "Houston", - }, - new EmployeeDto() - { - Id = 9, - Name = "Ava Lee", - Email = "ava.lee@company.demo", - Phone = "+420 123 456 789", - Salary = 24000M, - Position = "Data Analyst", - Location = "Tokyo", - }, - new EmployeeDto() - { - Id = 10, - Name = "Jacob Kim", - Email = "jacob.kim@company.demo", - Phone = "+420 234 567 890", - Salary = 27000M, - Position = "Marketing Manager", - Location = "Paris", - }, - new EmployeeDto() - { - Id = 11, - Name = "Samuel Adams", - Email = "samuel.adams@company.demo", - Phone = "+420 789 012 345", - Salary = 23000M, - Position = "Software Developer", - Location = "Boston", - }, - new EmployeeDto() - { - Id = 12, - Name = "Emily Park", - Email = "emily.park@company.demo", - Phone = "+420 890 123 456", - Salary = 26000M, - Position = "Marketing Coordinator", - Location = "Vancouver", - }, - new EmployeeDto() - { - Id = 13, - Name = "Nathan Williams", - Email = "nathan.williams@company.demo", - Phone = "+420 012 345 678", - Salary = 21000M, - Position = "Sales Manager", - Location = "Sydney", - }, - new EmployeeDto() - { - Id = 14, - Name = "Abby Kim", - Email = "abby.kim@company.demo", - Phone = "+420 123 456 789", - Salary = 24000M, - Position = "Data Scientist", - Location = "Los Angeles", - }, - new EmployeeDto() - { - Id = 15, - Name = "Daniel Choi", - Email = "daniel.choi@company.demo", - Phone = "+420 234 567 890", - Salary = 27000M, - Position = "Software Engineer", - Location = "Seoul", - }, - new EmployeeDto() - { - Id = 16, - Name = "Hannah Garcia", - Email = "hannah.garcia@company.demo", - Phone = "+420 123 456 789", - Salary = 23000M, - Position = "Software Developer", - Location = "Miami", - }, - new EmployeeDto() - { - Id = 17, - Name = "William Chen", - Email = "william.chen@company.demo", - Phone = "+420 234 567 890", - Salary = 26000M, - Position = "Business Analyst", - Location = "Singapore", - }, - new EmployeeDto() - { - Id = 18, - Name = "Ethan Davis", - Email = "ethan.davis@company.demo", - Phone = "+420 345 678 901", - Salary = 21000M, - Position = "Sales Associate", - Location = "Houston", - }, - new EmployeeDto() - { - Id = 19, - Name = "Isabella Kim", - Email = "isabella.kim@company.demo", - Phone = "+420 456 789 012", - Salary = 24000M, - Position = "Marketing Coordinator", - Location = "Melbourne", - }, - new EmployeeDto() - { - Id = 20, - Name = "Jackson Brown", - Email = "jackson.brown@company.demo", - Phone = "+420 567 890 123", - Salary = 27000M, - Position = "Project Manager", - Location = "Toronto", - }, - new EmployeeDto() - { - Id = 21, - Name = "Ella Davis", - Email = "ella.davis@company.demo", - Phone = "+420 123 456 789", - Salary = 23000M, - Position = "Software Developer", - Location = "Los Angeles", - }, - new EmployeeDto() - { - Id = 22, - Name = "Ryan Nguyen", - Email = "ryan.nguyen@company.demo", - Phone = "+420 234 567 890", - Salary = 26000M, - Position = "Business Analyst", - Location = "Ho Chi Minh City", - }, - new EmployeeDto() - { - Id = 23, - Name = "Sophie Hernandez", - Email = "sophie.hernandez@company.demo", - Phone = "+420 345 678 901", - Salary = 21000M, - Position = "Sales Associate", - Location = "Buenos Aires", - }, - new EmployeeDto() - { - Id = 24, - Name = "Alexander Kim", - Email = "alexander.kim@company.demo", - Phone = "+420 456 789 012", - Salary = 24000M, - Position = "Marketing Coordinator", - Location = "Vancouver", - }, - new EmployeeDto() - { - Id = 25, - Name = "Benjamin Brown", - Email = "benjamin.brown@company.demo", - Phone = "+420 567 890 123", - Salary = 27000M, - Position = "Project Manager", - Location = "Berlin", - }, - new EmployeeDto() - { - Id = 26, - Name = "Robert Jackson", - Email = "robert.jackson@company.demo", - Phone = "+420 678 901 234", - Salary = 30000M, - Position = "Senior Software Engineer", - Location = "Chicago", - }, - new EmployeeDto() - { - Id = 27, - Name = "Elizabeth Martinez", - Email = "elizabeth.martinez@company.demo", - Phone = "+420 789 012 345", - Salary = 32000M, - Position = "Senior Product Manager", - Location = "Toronto", - }, - new EmployeeDto() - { - Id = 28, - Name = "William Davis", - Email = "william.davis@company.demo", - Phone = "+420 890 123 456", - Salary = 27000M, - Position = "Sales Manager", - Location = "Sydney", - }, - new EmployeeDto() - { - Id = 29, - Name = "Sophia Lee", - Email = "sophia.lee@company.demo", - Phone = "+420 901 234 567", - Salary = 29000M, - Position = "Senior Data Analyst", - Location = "Tokyo", - }, - new EmployeeDto() - { - Id = 30, - Name = "Gabriel Garcia", - Email = "gabriel.garcia@company.demo", - Phone = "+420 012 345 678", - Salary = 34000M, - Position = "Senior Marketing Manager", - Location = "Rio de Janeiro", - }, - new EmployeeDto() - { - Id = 31, - Name = "Jane Smith", - Email = "jane.smith@company.demo", - Phone = "+420 123 456 789", - Salary = 20000M, - Position = "Software Engineer", - Location = "Prague" - }, - new EmployeeDto() - { - Id = 32, - Name = "Jane Doe", - Email = "jane.doe@company.demo", - Phone = "+420 987 654 321", - Salary = 25000M, - Position = "Product Manager", - Location = "Prague" - }, - new EmployeeDto() - { - Id = 33, - Name = "Bob Johnson", - Email = "bob.johnson@company.demo", - Phone = "+420 555 555 555", - Salary = 18000M, - Position = "Sales Representative", - Location = "Brno" - }, - new EmployeeDto() - { - Id = 34, - Name = "Sarah Lee", - Email = "sarah.lee@company.demo", - Phone = "+420 111 222 333", - Salary = 22000M, - Position = "Marketing Coordinator", - Location = "Ostrava" - }, - new EmployeeDto() - { - Id = 35, - Name = "David Kim", - Email = "david.kim@company.demo", - Phone = "+420 999 888 777", - Salary = 30000M, - Position = "Senior Software Engineer", - Location = "Prague" - }, - new EmployeeDto() - { - Id = 36, - Name = "Eva Novak", - Email = "eva.novak@company.demo", - Phone = "+420 777 777 777", - Salary = 22000M, - Position = "HR Manager", - Location = "Brno" - }, - new EmployeeDto() - { - Id = 37, - Name = "Adam Smith", - Email = "adam.smith@company.demo", - Phone = "+420 333 333 333", - Salary = 24000M, - Position = "Marketing Manager", - Location = "Ostrava" - }, - new EmployeeDto() - { - Id = 38, - Name = "Linda Lee", - Email = "linda.lee@company.demo", - Phone = "+420 222 222 222", - Salary = 28000M, - Position = "Senior Product Manager", - Location = "Prague" - }, - new EmployeeDto() - { - Id = 39, - Name = "Peter Brown", - Email = "peter.brown@company.demo", - Phone = "+420 111 111 111", - Salary = 19000M, - Position = "Sales Manager", - Location = "Brno" - }, - new EmployeeDto() - { - Id = 40, - Name = "Nina Black", - Email = "nina.black@company.demo", - Phone = "+420 999 999 999", - Salary = 21000M, - Position = "Marketing Specialist", - Location = "Ostrava" - }, - new EmployeeDto() - { - Id = 41, - Name = "Mark Davis", - Email = "mark.davis@company.demo", - Phone = "+420 888 888 888", - Salary = 27000M, - Position = "Software Architect", - Location = "Prague" - }, - new EmployeeDto() - { - Id = 42, - Name = "Jack Green", - Email = "jack.green@company.demo", - Phone = "+420 666 666 666", - Salary = 22000M, - Position = "Software Developer", - Location = "Brno" - }, - new EmployeeDto() - { - Id = 43, - Name = "Emily Jones", - Email = "emily.jones@company.demo", - Phone = "+420 555 444 333", - Salary = 24000M, - Position = "Product Owner", - Location = "Ostrava" - }, - new EmployeeDto() - { - Id = 44, - Name = "Chris Wilson", - Email = "chris.wilson@company.demo", - Phone = "+420 777 888 999", - Salary = 18000M, - Position = "Sales Representative", - Location = "Prague" - }, - new EmployeeDto() - { - Id = 45, - Name = "Olivia Taylor", - Email = "olivia.taylor@company.demo", - Phone = "+420 111 222 333", - Salary = 20000M, - Position = "Marketing Specialist", - Location = "Brno" - }, - new EmployeeDto() - { - Id = 46, - Name = "Harry Brown", - Email = "harry.brown@company.demo", - Phone = "+420 444 555 666", - Salary = 26000M, - Position = "Senior Software Engineer", - Location = "Ostrava" - }, - new EmployeeDto() - { - Id = 47, - Name = "Sophia Wilson", - Email = "sophia.wilson@company.demo", - Phone = "+420 333 444 555", - Salary = 23000M, - Position = "Product Manager", - Location = "Prague" - }, - new EmployeeDto() - { - Id = 48, - Name = "Lucas Miller", - Email = "lucas.miller@company.demo", - Phone = "+420 777 666 555", - Salary = 21000M, - Position = "Software Engineer", - Location = "Brno" - }, - new EmployeeDto() - { - Id = 49, - Name = "Mia Davis", - Email = "mia.davis@company.demo", - Phone = "+420 888 777 666", - Salary = 22000M, - Position = "Marketing Coordinator", - Location = "Ostrava" - }, - new EmployeeDto() - { - Id = 50, - Name = "Alexander Wilson", - Email = "alexander.wilson@company.demo", - Phone = "+420 222 333 444", - Salary = 28000M, - Position = "Product Owner", - Location = "Prague" - }, - new EmployeeDto() - { - Id = 51, - Name = "Charlotte Harris", - Email = "charlotte.harris@company.demo", - Phone = "+420 111 555 999", - Salary = 19000M, - Position = "Sales Representative", - Location = "Brno" - }, - new EmployeeDto() - { - Id = 52, - Name = "Dominik Johnson", - Email = "dominik.johnson@company.demo", - Phone = "+420 222 555 999", - Salary = 45000M, - Position = "CEO", - Location = "Prague" - } - }; + _employees = GenerateEmployees(); } + public IEnumerable GetAllEmployees() { _logger.LogInformation("DemoDataService.GetAllEmployees() called."); @@ -558,22 +36,31 @@ public IQueryable GetEmployeesAsQueryable() return _employees.AsQueryable(); } - public async Task> GetEmployeesDataFragmentAsync(int startIndex, int? count, CancellationToken cancellationToken = default) + public async Task> GetEmployeesDataFragmentAsync(int startIndex, int? count, CancellationToken cancellationToken = default) { _logger.LogInformation($"DemoDataService.GetEmployeesDataFragmentAsync(startIndex: {startIndex}, count: {count}) called."); await Task.Delay(80, cancellationToken); // simulate server call - return _employees.Skip(startIndex).Take(count ?? Int32.MaxValue).ToList(); + return new() + { + Data = _employees.Skip(startIndex).Take(count ?? Int32.MaxValue).ToList(), + TotalCount = _employees.Count + }; } - public async Task> GetEmployeesDataFragmentAsync(EmployeesFilterDto filter, int startIndex, int? count, CancellationToken cancellationToken = default) + public async Task> GetEmployeesDataFragmentAsync(EmployeesFilterDto filter, int startIndex, int? count, CancellationToken cancellationToken = default) { _logger.LogInformation($"DemoDataService.GetEmployeesDataFragmentAsync(startIndex: {startIndex}, count: {count}) called."); await Task.Delay(80, cancellationToken); // simulate server call - return GetFilteredEmployees(filter).Skip(startIndex).Take(count ?? Int32.MaxValue).ToList(); + var data = GetFilteredEmployees(filter).Skip(startIndex).Take(count ?? Int32.MaxValue).ToList(); + return new() + { + Data = data, + TotalCount = data.Count + }; } private IEnumerable GetFilteredEmployees(EmployeesFilterDto filter) diff --git a/Havit.Blazor.Documentation/DemoData/EmployeeDto.cs b/Havit.Blazor.Documentation/DemoData/EmployeeDto.cs index 3d6e90ed..d572e3b5 100644 --- a/Havit.Blazor.Documentation/DemoData/EmployeeDto.cs +++ b/Havit.Blazor.Documentation/DemoData/EmployeeDto.cs @@ -1,6 +1,6 @@ namespace Havit.Blazor.Documentation.DemoData; -public class EmployeeDto +public record EmployeeDto { public int Id { get; internal set; } public string Name { get; internal set; } diff --git a/Havit.Blazor.Documentation/DemoData/IDemoDataService.cs b/Havit.Blazor.Documentation/DemoData/IDemoDataService.cs index 58582919..d6f99dbf 100644 --- a/Havit.Blazor.Documentation/DemoData/IDemoDataService.cs +++ b/Havit.Blazor.Documentation/DemoData/IDemoDataService.cs @@ -6,8 +6,8 @@ public interface IDemoDataService Task> GetAllEmployeesAsync(CancellationToken cancellationToken = default); IQueryable GetEmployeesAsQueryable(); - Task> GetEmployeesDataFragmentAsync(int startIndex, int? count, CancellationToken cancellationToken = default); - Task> GetEmployeesDataFragmentAsync(EmployeesFilterDto filter, int startIndex, int? count, CancellationToken cancellationToken = default); + Task> GetEmployeesDataFragmentAsync(int startIndex, int? count, CancellationToken cancellationToken = default); + Task> GetEmployeesDataFragmentAsync(EmployeesFilterDto filter, int startIndex, int? count, CancellationToken cancellationToken = default); Task GetEmployeesCountAsync(CancellationToken cancellationToken = default); Task GetEmployeesCountAsync(EmployeesFilterDto filter, CancellationToken cancellationToken = default); Task> FindEmployeesByNameAsync(string query, int? limitCount = null, CancellationToken cancellationToken = default); diff --git a/Havit.Blazor.Documentation/Havit.Blazor.Documentation.csproj b/Havit.Blazor.Documentation/Havit.Blazor.Documentation.csproj index 3f8826e5..a3d59d9b 100644 --- a/Havit.Blazor.Documentation/Havit.Blazor.Documentation.csproj +++ b/Havit.Blazor.Documentation/Havit.Blazor.Documentation.csproj @@ -1,8 +1,13 @@  - net8.0 + net9.0 + Havit.Blazor.Documentation + Havit.Blazor.Documentation enable + true + Default + true $(NoWarn);1701;1702;SA1134;VSTHRD003;VSTHRD200 @@ -11,9 +16,6 @@ - - - @@ -39,22 +41,11 @@ - - Never - - - true - - - true - - - @@ -62,8 +53,4 @@ - - - true - diff --git a/Havit.Blazor.Documentation/NavigationRoutes.cs b/Havit.Blazor.Documentation/NavigationRoutes.cs new file mode 100644 index 00000000..9cc4d644 --- /dev/null +++ b/Havit.Blazor.Documentation/NavigationRoutes.cs @@ -0,0 +1,13 @@ +namespace Havit.Blazor.Documentation; + +public static class NavigationRoutes +{ + public static class Premium + { + public const string GatewayPage = "/premium/access-content"; + public static string GetGatewayPage(string targetUrl) + { + return GatewayPage + "?url=" + Uri.EscapeDataString(targetUrl); + } + } +} diff --git a/Havit.Blazor.Documentation/Pages/Components/HxContextMenuDoc/HxContextMenu_Documentation.razor b/Havit.Blazor.Documentation/Pages/Components/HxContextMenuDoc/HxContextMenu_Documentation.razor index 4a81e508..1f93bc95 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxContextMenuDoc/HxContextMenu_Documentation.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxContextMenuDoc/HxContextMenu_Documentation.razor @@ -25,6 +25,9 @@ Padding of the context menu button. + + Font size of the context menu button icon. + Margin of the item icon. diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo.razor index 0d0011c3..1bbad0d7 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo.razor @@ -14,10 +14,11 @@ private async Task> GetGridData(GridDataProviderRequest request) { // you usually pass the data-request to your API/DataLayer + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(request.CancellationToken) + Data = response.Data, + TotalCount = response.TotalCount }; } } \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_ApplyTo_Async.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_ApplyTo_Async.razor new file mode 100644 index 00000000..5042a009 --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_ApplyTo_Async.razor @@ -0,0 +1,31 @@ +@inject IDemoDataService DemoDataService + + + + + + + + + + + +@code { + private IEnumerable employees; + private TaskCompletionSource dataLoadingTaskCompletionSource; + + protected override async Task OnInitializedAsync() + { + dataLoadingTaskCompletionSource = new TaskCompletionSource(); + + employees = await DemoDataService.GetAllEmployeesAsync(); + + dataLoadingTaskCompletionSource.SetResult(); + } + + private async Task> GetGridData(GridDataProviderRequest request) + { + await dataLoadingTaskCompletionSource.Task; + return request.ApplyTo(employees); + } +} \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_ContextMenu.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_ContextMenu.razor index 9fdad22c..074f77b4 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_ContextMenu.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_ContextMenu.razor @@ -20,10 +20,11 @@ private async Task> GetGridData(GridDataProviderRequest request) { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(request.CancellationToken) + Data = response.Data, + TotalCount = response.TotalCount }; } diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_CustomPagination.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_CustomPagination.razor index 494bddbc..289f9770 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_CustomPagination.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_CustomPagination.razor @@ -38,10 +38,11 @@ private async Task> GetGridData(GridDataProviderRequest request) { // you usually pass the data-request to your API/DataLayer + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(request.CancellationToken) + Data = response.Data, + TotalCount = response.TotalCount }; } } diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_HeaderFiltering.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_HeaderFiltering.razor index d1457964..58819848 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_HeaderFiltering.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_HeaderFiltering.razor @@ -39,10 +39,11 @@ private async Task> GetGridData(GridDataProviderRequest request) { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(filterModel, request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(filterModel, request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(filterModel, request.CancellationToken) + Data = response.Data, + TotalCount = response.TotalCount }; } } \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Hover.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Hover.razor index 75a0df70..00b9ead0 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Hover.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Hover.razor @@ -13,10 +13,11 @@ @code { private async Task> GetGridData(GridDataProviderRequest request) { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(request.CancellationToken) + Data = response.Data, + TotalCount = response.TotalCount }; } } \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_InfiniteScroll.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_InfiniteScroll.razor index 6a137a57..f43e61eb 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_InfiniteScroll.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_InfiniteScroll.razor @@ -7,8 +7,7 @@ } - -

@_debugOutput

- - - @code { - string _debugOutput; - HxGrid _gridComponent; - private async Task> GetGridData(GridDataProviderRequest request) { - _debugOutput = $"Requesting data: StartIndex={request.StartIndex}, Count={request.Count}"; - StateHasChanged(); - - await Task.Delay(1600); // simulate server delay in demo (do not put this in your code) - + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(request.CancellationToken) + Data = response.Data, + TotalCount = response.TotalCount }; } } \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_InlineEditing.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_InlineEditing.razor index 3187cf9e..22fe7e73 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_InlineEditing.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_InlineEditing.razor @@ -75,10 +75,11 @@ private async Task> GetGridData(GridDataProviderRequest request) { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(request.CancellationToken) + Data = response.Data, + TotalCount = response.TotalCount }; } diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_LoadMore.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_LoadMore.razor index 734389e3..336719f2 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_LoadMore.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_LoadMore.razor @@ -13,10 +13,11 @@ @code { private async Task> GetGridData(GridDataProviderRequest request) { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() - { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(request.CancellationToken) - }; + { + Data = response.Data, + TotalCount = response.TotalCount + }; } } \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Multiselect.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Multiselect.razor index 4d061c89..d0edc749 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Multiselect.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Multiselect.razor @@ -3,6 +3,7 @@ @@ -14,20 +15,24 @@ - + +

Selected employees: @(String.Join(", ", selectedEmployees.Select(e => e.Name)))

+ @code { private HashSet selectedEmployees = new(); + private bool preserveSelection = false; private async Task> GetGridData(GridDataProviderRequest request) { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(request.CancellationToken) + Data = response.Data, + TotalCount = response.TotalCount }; } } \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_RefreshData.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_RefreshData.razor index 1eaed1ef..690ea551 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_RefreshData.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_RefreshData.razor @@ -10,23 +10,24 @@ - + @code { private HxGrid gridComponent; private async Task> GetGridData(GridDataProviderRequest request) { - await Task.Delay(1000); // simulate 1s server delay in demo (do not put this in your code) + await Task.Delay(1000); // simulate slow 1s server response in demo (do not put this in your code) + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(request.CancellationToken) + Data = response.Data, + TotalCount = response.TotalCount }; } - private async Task HandleButtonClick() + private async Task HandleRefreshButtonClick() { await gridComponent.RefreshDataAsync(); } diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_StatePersisting.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_StatePersisting.razor index 5a4f6196..520e574d 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_StatePersisting.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_StatePersisting.razor @@ -23,10 +23,11 @@ private async Task> GetGridData(GridDataProviderRequest request) { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(request.CancellationToken) + Data = response.Data, + TotalCount = response.TotalCount }; } diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Striped.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Striped.razor index 769a22ae..c3b0e8f2 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Striped.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Striped.razor @@ -13,10 +13,11 @@ @code { private async Task> GetGridData(GridDataProviderRequest request) { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(request.CancellationToken) + Data = response.Data, + TotalCount = response.TotalCount }; } } \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Documentation.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Documentation.razor index f961f032..8ac28d5b 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Documentation.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Documentation.razor @@ -29,10 +29,19 @@

+ +

+ When loading data asynchronously, always ensure that the DataProvider waits for the data to be available; + otherwise, the skeleton UI (placeholders) will not display correctly.
+ If you are preloading data and using the request.ApplyTo(data) extension method, + you can leverage TaskCompletionSource to handle waiting until the data is loaded. +

+

- When utilizing IQueryable as your data source provider, commonly seen in Blazor Server applications, + When utilizing IQueryable as your data source provider, + commonly seen in Blazor Server applications with Entity Framework Core, you can employ the data.ApplyGridDataProviderRequest(request) method.

@@ -74,7 +83,7 @@

Enable continuous scrolling in HxGrid by setting ContentNavigationMode="GridContentNavigationMode.InfiniteScroll". This feature leverages the capabilities, requirements, and limitations of Blazor's - Virtualize component. + Virtualize component.

It's important to specify the ItemRowHeight for effective virtualization. By default, it is 41 pixels, aligning with the standard table row height in Bootstrap. @@ -113,14 +122,22 @@ Enable multi-row selection for users by setting @nameof(HxGrid.MultiSelectionEnabled)="true". The selected items can be accessed via the @nameof(HxGrid.SelectedDataItems) parameter, which is bindable.

- + +

Note that @nameof(HxGrid.SelectedDataItems) only includes visible items. - Items are removed from the selection when they become unrendered (for example, after paging, sorting, etc.). - Additionally, @nameof(HxGrid.MultiSelectionEnabled) is not compatible - with @nameof(GridContentNavigationMode.InfiniteScroll).
- This design decision might change in the future. + By default, items are removed from the selection when they become unrendered (for example, after paging, sorting, etc.). + However, this behavior can be modified by setting the @nameof(HxGrid.PreserveSelection)="true" parameter, + which ensures that selected items are preserved across data operations such as paging, sorting, or manual invocation of RefreshDataAsync. +

+

+ The "select/deselect all" checkbox operates only on visible records and adds/removes them from the selection accordingly. + Non-visible items (e.g., from other pages) are not affected by this operation. +

+ + When using @nameof(GridContentNavigationMode.InfiniteScroll), @nameof(HxGrid.PreserveSelection)="true" is required for multi-row selection to work. + Attempting to use @nameof(HxGrid.MultiSelectionEnabled)="true" without enabling PreserveSelection will result in an exception. Additionally, the "select/deselect all" checkbox is intentionally hidden in this mode, + as the grid does not have access to all data to reliably perform this operation. For more details, see ticket #950. - diff --git a/Havit.Blazor.Documentation/Pages/Components/HxInputDateDoc/HxInputDate_Documentation.razor b/Havit.Blazor.Documentation/Pages/Components/HxInputDateDoc/HxInputDate_Documentation.razor index 3e602818..afb90f14 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxInputDateDoc/HxInputDate_Documentation.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxInputDateDoc/HxInputDate_Documentation.razor @@ -28,7 +28,7 @@

- See Globalization and localization in official ASP.NET Core Blazor documentation for more details on how to set culture in you application. + See Globalization and localization in official ASP.NET Core Blazor documentation for more details on how to set culture in you application.

diff --git a/Havit.Blazor.Documentation/Pages/Components/HxInputFileDoc/HxInputFile_Documentation.razor b/Havit.Blazor.Documentation/Pages/Components/HxInputFileDoc/HxInputFile_Documentation.razor index 41719af9..d16c877b 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxInputFileDoc/HxInputFile_Documentation.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxInputFileDoc/HxInputFile_Documentation.razor @@ -5,11 +5,11 @@

- See File Uploads topics in ASP.NET Core documentation + See File Uploads topics in ASP.NET Core documentation for information on how to set up a server-side endpoint (controller action) to accept uploaded files.

- We recommend using the streaming approach, + We recommend using the streaming approach, which is also demonstrated in our demos. You can refer to the FileUploadControllerDemo.cs file for a sample controller action. diff --git a/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Demo.razor b/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Demo.razor index 8d36fcfc..c31069af 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Demo.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Demo.razor @@ -1,4 +1,4 @@ - +

Entered number: @enteredNumber
diff --git a/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Demo_InputMode.razor b/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Demo_InputMode.razor index efa99150..d10f1caf 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Demo_InputMode.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Demo_InputMode.razor @@ -1,6 +1,6 @@ - - - + + + @code { private float enteredNumber; diff --git a/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Demo_InputType.razor b/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Demo_InputType.razor new file mode 100644 index 00000000..43c45a85 --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Demo_InputType.razor @@ -0,0 +1,6 @@ + + + +@code { + private float enteredNumber; +} diff --git a/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Demo_SelectOnFocus.razor b/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Demo_SelectOnFocus.razor index 70b8cd33..20fda987 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Demo_SelectOnFocus.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Demo_SelectOnFocus.razor @@ -1,5 +1,5 @@ - - + + @code { private float enteredNumber = 123.4F; diff --git a/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Documentation.razor b/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Documentation.razor index c5ca25dd..4a2b3ebe 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Documentation.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxInputNumberDoc/HxInputNumber_Documentation.razor @@ -18,6 +18,28 @@
+ +

+ Type parameter allows you to change the <input type="..."> of the underlying HTML element. +

+ +

+ InputType.Text is the default value and is used when Type is not set. +

    +
  • Respects your application culture (CultureInfo.CurrentCulture)
  • +
  • Does not prevent non-numeric input (unless combined with additional JavaScript)
  • +
  • Allows for enhanced UX tricks (eg., automatic replacement of comma/decimal separators)
  • +
+

+

+ InputType.Number is a browser-provided input type that restricts input to numeric values. +

    +
  • Does not respect your application culture, uses user's browser locale.
  • +
  • Prevents non-numeric input.
  • +
  • Uses only browser-provided UX.
  • +
+

+

The SelectOnFocus parameter allows you to determine whether all the content within the input field is automatically selected when it receives focus.

diff --git a/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_Basic.razor b/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_Basic.razor index 5dc35f45..41ec2ce0 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_Basic.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_Basic.razor @@ -46,10 +46,11 @@ private async Task> GetGridData(GridDataProviderRequest request) { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(filterModel, request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(filterModel, request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(filterModel, request.CancellationToken) + Data = response.Data, + TotalCount = response.TotalCount }; } diff --git a/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_InfiniteScrollSticky.razor b/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_InfiniteScrollSticky.razor index e5c5b672..e55b071f 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_InfiniteScrollSticky.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_InfiniteScrollSticky.razor @@ -30,10 +30,11 @@ @code { private async Task> GetGridData(GridDataProviderRequest request) { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(request.CancellationToken) + Data = response.Data, + TotalCount = response.TotalCount }; } } \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_NamedViews.razor b/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_NamedViews.razor index 4cc1a73c..2a25d7c3 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_NamedViews.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_NamedViews.razor @@ -54,10 +54,11 @@ private async Task> GetGridData(GridDataProviderRequest request) { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(filterModel, request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(filterModel, request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(filterModel, request.CancellationToken) + Data = response.Data, + TotalCount = response.TotalCount }; } } diff --git a/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_SearchTemplate.razor b/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_SearchTemplate.razor index 624069f8..1ce14872 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_SearchTemplate.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_SearchTemplate.razor @@ -46,10 +46,11 @@ private async Task> GetGridData(GridDataProviderRequest request) { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(filterModel, request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(filterModel, request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync(filterModel, request.CancellationToken) + Data = response.Data, + TotalCount = response.TotalCount }; } diff --git a/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_TitleTemplate.razor b/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_TitleTemplate.razor index bd7577e3..c311baad 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_TitleTemplate.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Demo_TitleTemplate.razor @@ -23,10 +23,11 @@ @code { private async Task> GetGridData(GridDataProviderRequest request) { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); return new GridDataProviderResult() { - Data = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken), - TotalCount = await DemoDataService.GetEmployeesCountAsync() + Data = response.Data, + TotalCount = response.TotalCount }; } } diff --git a/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Documentation.razor b/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Documentation.razor index d97c76a2..1704bac4 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Documentation.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxListLayoutDoc/HxListLayout_Documentation.razor @@ -11,7 +11,7 @@

Enable continuous scrolling in HxGrid by setting ContentNavigationMode="GridContentNavigationMode.InfiniteScroll". This feature leverages the capabilities, requirements, and limitations of Blazor's - Virtualize component. + Virtualize component. For optimal virtualization, specify ItemRowHeight; the default is 41 pixels, aligning with Bootstrap's standard table row height. Additionally, defining the container element's height is essential for virtualization functionality.

diff --git a/Havit.Blazor.Documentation/Pages/Components/HxMessageBoxDoc/HxMessageBoxService_Demo_CustomButtonTexts.razor b/Havit.Blazor.Documentation/Pages/Components/HxMessageBoxDoc/HxMessageBoxService_Demo_CustomButtonTexts.razor new file mode 100644 index 00000000..49f62484 --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/Components/HxMessageBoxDoc/HxMessageBoxService_Demo_CustomButtonTexts.razor @@ -0,0 +1,26 @@ + + +@code +{ + [Inject] protected IHxMessageBoxService MessageBox { get; set; } + + private async Task OpenMessageBox() + { + var selectedButton = await MessageBox.ShowAsync(new MessageBoxRequest() + { + Text = "Do you want to join?", + Title = "Subscribe now!", + Buttons = MessageBoxButtons.YesNo, + Settings = new MessageBoxSettings() + { + YesButtonText = "Yeah!", + NoButtonText = "Nope", + PrimaryButtonSettings = new ButtonSettings() + { + Color = ThemeColor.Success, + Icon = BootstrapIcon.Check + } + } + }); + } +} diff --git a/Havit.Blazor.Documentation/Pages/Components/HxMessageBoxDoc/HxMessageBoxService_Demo_NoHeader.razor b/Havit.Blazor.Documentation/Pages/Components/HxMessageBoxDoc/HxMessageBoxService_Demo_NoHeader.razor index 315a2b7d..884f39d0 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxMessageBoxDoc/HxMessageBoxService_Demo_NoHeader.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxMessageBoxDoc/HxMessageBoxService_Demo_NoHeader.razor @@ -6,7 +6,7 @@ private async Task OpenMessageBox() { - _ = await MessageBox.ShowAsync(new MessageBoxRequest() + var selectedButton = await MessageBox.ShowAsync(new MessageBoxRequest() { Text = "This is the text", Title = null, diff --git a/Havit.Blazor.Documentation/Pages/Components/HxMessageBoxDoc/HxMessageBox_Documentation.razor b/Havit.Blazor.Documentation/Pages/Components/HxMessageBoxDoc/HxMessageBox_Documentation.razor index 98ed30f4..7864f3fe 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxMessageBoxDoc/HxMessageBox_Documentation.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxMessageBoxDoc/HxMessageBox_Documentation.razor @@ -3,32 +3,40 @@ - - @nameof(HxMessageBox) is an implementation component that you use directly only in rare specific cases.
- Usually, you use the message box by using the @nameof(IHxMessageBoxService) injected service and its - ShowAsync method (or derived extension methods).
-
- - -

Inject the @nameof(IHxMessageBoxService) into your code and use one of its methods to raise the message box:

-
    -
  • Task<MessageBoxButtons> ShowAsync(MessageBoxRequest request) - original method
  • -
  • Task<MessageBoxButtons> ShowAsync(string title, string text, MessageBoxButtons buttons = MessageBoxButtons.Ok, MessageBoxButtons? primaryButton = null, string customButtonText = null) - extension method
  • -
  • Task<bool> ConfirmAsync(string title, string text) - extension method
  • -
  • ...or you can add your own extension method
  • -
- - - -

You should place the @nameof(HxMessageBoxHost) component in MainLayout.razor (or App.razor) to make it work!

- - -

Register the required services using services.AddHxMessageBoxHost() from Startup.cs (Blazor Server) or Program.cs (Blazor WebAssembly).

- - - -

If you clear the Title, HeaderTemplate, and set ShowCloseButton="false", the header won't show up.

- + + @nameof(HxMessageBox) is an implementation component that you use directly only in rare specific cases.
+ Usually, you use the message box by using the @nameof(IHxMessageBoxService) injected service and its + ShowAsync method (or derived extension methods).
+
+ + +

Inject the @nameof(IHxMessageBoxService) into your code and use one of its methods to raise the message box:

+ + + + +

You should place the @nameof(HxMessageBoxHost) component in MainLayout.razor (or App.razor) to make it work!

+ + +

Register the required services using services.AddHxMessageBoxHost() from Startup.cs (Blazor Server) or Program.cs (Blazor WebAssembly).

+ + + +

+ Customize the message box appearance and behavior by using the MessageBoxRequest + and its Settings property. This allows you to set custom button texts, + define a unique header, and more to suit your specific requirements. +

+ + + +

If you clear the Title, HeaderTemplate, and set ShowCloseButton="false", the header won't show up.

+
\ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo.razor b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo.razor index 9db0caab..41ec8499 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo.razor @@ -1,23 +1,25 @@ - - - - +
+ + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - \ No newline at end of file + + + + +
\ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_Colors.razor b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_Colors.razor new file mode 100644 index 00000000..e4a072c7 --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_Colors.razor @@ -0,0 +1,36 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_Dark.razor b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_Dark.razor new file mode 100644 index 00000000..0f57cd3f --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_Dark.razor @@ -0,0 +1,25 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_ItemCustomContent.razor b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_ItemCustomContent.razor index 48f6a882..b9594512 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_ItemCustomContent.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_ItemCustomContent.razor @@ -1,25 +1,30 @@ - - - - - Notifications 8 - - - - - Received payments 3 - - - - - Pending payments 5 - - - - - - - - - - \ No newline at end of file +
+ + + + + + + + Notifications 8 + + + + + Received payments 3 + + + + + Pending payments 5 + + + + + + + + + + +
\ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_Logo.razor b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_Logo.razor index f7325f18..caaaf89b 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_Logo.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_Logo.razor @@ -1,23 +1,24 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file +
+ + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_MultipleItemsExpansion.razor b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_MultipleItemsExpansion.razor index d4329c74..bbb9ea3f 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_MultipleItemsExpansion.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_MultipleItemsExpansion.razor @@ -1,20 +1,22 @@ - - - - - - - - - - - - - - - - - - - - +
+ + + + + + + + + + + + + + + + + + + + +
diff --git a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_ProgramaticallyExpandCollapse.razor b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_ProgramaticallyExpandCollapse.razor index b23f0192..e264c8ce 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_ProgramaticallyExpandCollapse.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_ProgramaticallyExpandCollapse.razor @@ -1,26 +1,28 @@ - - - - +
+ + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - + + + + +
Toggle sidebar diff --git a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_TogglerTemplate.razor b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_TogglerTemplate.razor index 9ec95fa2..b2a26dc8 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_TogglerTemplate.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Demo_TogglerTemplate.razor @@ -1,43 +1,44 @@ - - -
- @if (context.SidebarCollapsed) - { - - OPEN - - - } - else - { - - - CLOSE - - } -
-
+
+ + +
+ @if (context.SidebarCollapsed) + { + + OPEN + + + } + else + { + + + CLOSE + + } +
+
- - - + + + - - - - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + + + +
diff --git a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Documentation.razor b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Documentation.razor index 21cd3d6f..b97ad4d6 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Documentation.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxSidebarDoc/HxSidebar_Documentation.razor @@ -8,6 +8,14 @@ + +

Similarly to Bootstrap Navbar, the Sidebar color can be inverted with data-bs-theme="dark" attribute on parent div.

+ + + +

Similarly to Bootstrap Navbar, you can use utility classes to create any color variation eg. bg-body-secondary. Colors of the items and active/hover states need to be fine-tuned via CSS vars.

+ +

Use your own custom logo in the sidebar header by setting the LogoTemplate parameter (RenderFragment) on the HxSidebarBrand component.

@@ -50,9 +58,12 @@ Width of collapsed sidebar. - + Width of the sidebar. + + Max height of the sidebar. + Toggler bar/arrow background. @@ -68,7 +79,7 @@ Color of the items icons. - + Border radius of the items. @@ -113,7 +124,7 @@ Font weight of the parent of active item. - + Margin of the items. @@ -122,16 +133,19 @@ Padding of the subitems. - + Margin of the subitems. Padding of the sidebar header. - + Padding of the sidebar body. - + + Gap of the sidebar body nav element. + + Padding of the sidebar footer. @@ -164,6 +178,12 @@ Font weight of the brand name. + + Gap between the brand logo and the brand name. + + + Font weight of the brand name. + Padding of the items in the footer. diff --git a/Havit.Blazor.Documentation/Pages/Components/HxTabPanelDoc/HxTabPanel_Documentation.razor b/Havit.Blazor.Documentation/Pages/Components/HxTabPanelDoc/HxTabPanel_Documentation.razor index 8926816d..bf3cb11f 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxTabPanelDoc/HxTabPanel_Documentation.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxTabPanelDoc/HxTabPanel_Documentation.razor @@ -2,24 +2,30 @@ @attribute [Route("/components/" + nameof(HxTab))] - + + + Wrapping HxTab components with <AuthorizeView> or similar structures is not supported. + HxTabPanel expects HxTab components to be its direct children. + To dynamically control tab visibility, set the HxTab.Visible parameter + or use conditional rendering with an @@if condition.
+
- + - -

You can choose any variant of nav using the @nameof(HxTabPanel.NavVariant) parameter.

- - - - - - + +

You can choose any variant of nav using the @nameof(HxTabPanel.NavVariant) parameter.

+ + + + + + - -

When Variant is set to TabPanelVariant.Card, the tab navigation is placed in the card header and tab contents go into the card body.

- + +

When Variant is set to TabPanelVariant.Card, the tab navigation is placed in the card header and tab contents go into the card body.

+

diff --git a/Havit.Blazor.Documentation/Pages/Components/HxToastDoc/HxToast_Demo.razor b/Havit.Blazor.Documentation/Pages/Components/HxToastDoc/HxToast_Demo.razor new file mode 100644 index 00000000..58fc0c36 --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/Components/HxToastDoc/HxToast_Demo.razor @@ -0,0 +1,42 @@ + + @if (isVisible1) + { + + } + + @if (isVisible2) + { + + + + Hi! All messages have been deleted and cannot be restored. + + + } + + @if (isVisible3) + { + + + This message should disappear after 5 sec. + + + } + + + + Always visible. No close button here. + + + + + + + + + +@code { + private bool isVisible1 = true; + private bool isVisible2 = true; + private bool isVisible3 = true; +} diff --git a/Havit.Blazor.Documentation/Pages/Components/HxToastDoc/HxToast_Documentation.razor b/Havit.Blazor.Documentation/Pages/Components/HxToastDoc/HxToast_Documentation.razor index fd9ca393..6dda6f72 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxToastDoc/HxToast_Documentation.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxToastDoc/HxToast_Documentation.razor @@ -2,15 +2,28 @@ @attribute [Route("/components/" + nameof(HxToastContainer))] - - -

See @nameof(HxMessenger) documentation.

- - - - Margin of the container. - - + + +

+ HxToast is usually not used directly, but as a part of HxMessenger component. + See @nameof(HxMessenger) documentation. +

+ + +

+ You can use HxToast directly to show a toast. Usually you will use it in combination with HxToastContainer. +

+ + + +

HxToast supports static server rendering.

+
+
+ + + Margin of the container. + +
diff --git a/Havit.Blazor.Documentation/Pages/Concepts/DarkColorMode.razor b/Havit.Blazor.Documentation/Pages/Concepts/DarkColorMode.razor index cd69283f..33bebe2b 100644 --- a/Havit.Blazor.Documentation/Pages/Concepts/DarkColorMode.razor +++ b/Havit.Blazor.Documentation/Pages/Concepts/DarkColorMode.razor @@ -1,5 +1,8 @@ @page "/concepts/dark-color-mode-theme" + + +

Dark color mode (theme)

diff --git a/Havit.Blazor.Documentation/Pages/Concepts/Debouncer_Documentation.razor b/Havit.Blazor.Documentation/Pages/Concepts/Debouncer_Documentation.razor index cb6a42cc..b2a4ec0c 100644 --- a/Havit.Blazor.Documentation/Pages/Concepts/Debouncer_Documentation.razor +++ b/Havit.Blazor.Documentation/Pages/Concepts/Debouncer_Documentation.razor @@ -1,6 +1,8 @@ @page "/concepts/Debouncer" -Debouncer + + +

Debouncer helps you to debounce asynchronous actions. You can use it in your callbacks to prevent multiple calls of the same action in a short period of time.

diff --git a/Havit.Blazor.Documentation/Pages/Concepts/SettingsAndDefaults_Documentation.razor b/Havit.Blazor.Documentation/Pages/Concepts/SettingsAndDefaults_Documentation.razor index 9973278c..3427d444 100644 --- a/Havit.Blazor.Documentation/Pages/Concepts/SettingsAndDefaults_Documentation.razor +++ b/Havit.Blazor.Documentation/Pages/Concepts/SettingsAndDefaults_Documentation.razor @@ -1,5 +1,7 @@ @page "/concepts/defaults-and-settings" + +

Defaults and Settings

Although most components support the presented functionalities, some components were constructed without settings or defaults because they wouldn't add sufficient value. diff --git a/Havit.Blazor.Documentation/Pages/GettingStarted/GettingStarted.razor b/Havit.Blazor.Documentation/Pages/GettingStarted/GettingStarted.razor new file mode 100644 index 00000000..b663b146 --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/GettingStarted/GettingStarted.razor @@ -0,0 +1,421 @@ +@page "/getting-started" + +Getting started | HAVIT Blazor Bootstrap - Free components for ASP.NET Core Blazor + + + + + + + +

Havit.Blazor components have the following requirements:

+
    +
  • .NET 8.0 or later (our NuGet package provides builds for both net8.0 and net9.0 to utilize the latest features).
  • +
  • Most components require interactive rendering mode for full functionality (some support for static SSR is available, with limited functionality where applicable).
  • +
+ + + + +

+ You can either add Havit.Blazor components to an existing project + or create a new project using one of the following GitHub repository templates: +

+ + +

+ For a quick start, you can use the Simple Blazor Web App Template: +

    +
  • provides a basic setup - a .NET 9 Blazor Web App with Auto interactive render mode,
  • +
  • has the MainLayout adjusted to use HxSidebar,
  • +
  • comes with Havit.Blazor preinstalled (including HxMessenger and HxMessageBox support),
  • +
  • features sample pages updated to utilize our components.
  • +
+

+ + +

+ Try our Enterprise Project Template, which includes features like EF Core, gRPC code-first, and more: +

    +
  • layered architecture (model, data layer with repositories, services, etc.),
  • +
  • Entity Framework Core for data access,
  • +
  • gRPC code-first communication between server and client,
  • +
  • ...and much more.
  • +
+

+ + + + +

Select your setup to get customized instructions.

+ +

Framework version

+ + + + + + + + + + +

Blazor app model

+ + + + + + + + + + + + + + @if (_setup.ProjectSetup == ProjectSetup.BlazorWebApp) + { +

Interactive render mode

+ + + + + + + + + + + + + + + + } + +

Did you opt for sample pages when creating the project?

+ + + + + + + + + +
+ + + + +@if (HasClientProject()) +{ +

+ Add Havit.Blazor.Components.Web.Bootstrap NuGet package + to your {YourBlazorApp}.Client project. + You can use the NuGet Package Manager + or execute the following command: +

+} +else +{ +

+ Add Havit.Blazor.Components.Web.Bootstrap NuGet package + to your Blazor project. + You can use the NuGet Package Manager + or execute the following command: +

+} +dotnet add package Havit.Blazor.Components.Web.Bootstrap + + + + +

+ Our library only requires the presence of any Bootstrap CSS bundle (version @HxSetup.BootstrapVersion) in your project.
+ All additional CSS rules needed for our components are automatically included in the bundle of scoped CSS. + Ensure that the link to {YourBlazorApp}.styles.css remains in your @GetHtmlHostFile() + and has NOT been removed. +

+ +

Choose a Bootstrap CSS bundle

+ + + + + + + + + + + + + + + +
+ +@if ((_setup.BootstrapTheme == BootstrapTheme.HavitBlazor) || (_setup.BootstrapTheme == BootstrapTheme.PlainCdn)) +{ +

+ Add the following line to the <head> section of your @GetHtmlHostFile() file: +

+ if (_setup.BootstrapTheme == BootstrapTheme.HavitBlazor) + { + if (_setup.ProjectSetup == ProjectSetup.WasmStandalone) + { + @HxSetup.RenderBootstrapCssReference() + } + else if (HasStaticFileAssets()) + { + @("""""") + } + else + { + @("""@((MarkupString)HxSetup.RenderBootstrapCssReference())""") + } +

+ This snippet adds a <link> to our customized Bootstrap CSS build, incorporating the Havit.Blazor theme. + It includes various subtle adjustments, such as colors, borders, and other styling tweaks to enhance the appearance of your app. + This is the same theme used throughout our documentation to showcase components. +

+ } + else if (_setup.BootstrapTheme == BootstrapTheme.PlainCdn) + { + if (_setup.ProjectSetup == ProjectSetup.WasmStandalone) + { + @HxSetup.RenderBootstrapCssReference(BootstrapFlavor.PlainBootstrap) + } + else + { + @("""@((MarkupString)HxSetup.RenderBootstrapCssReference(BootstrapFlavor.PlainBootstrap))""") + } +

+ This will add a <link> to the Bootstrap CSS that always matches the version required by Havit.Blazor, + so you won’t need to maintain the link manually in the future. +

+ } +} +else if (_setup.BootstrapTheme == BootstrapTheme.Custom) +{ +

+ You can use your own custom Bootstrap CSS build or use any pre-built Bootsrap theme. + Just add a <link> to the Bootstrap CSS of your choice + to the <head> section of your @GetHtmlHostFile() file. +

+ @* TODO Offer creation of customized Bootstrap build here. *@ +} + +@if ((_setup.SamplePagesCreated) && (_setup.BootstrapTheme != BootstrapTheme.PlainProject)) +{ +
Clean sample CSS
+

+ When creating your Blazor project, you selected the Include sample pages option, + which automatically added local Bootstrap CSS files to your project. + Delete the @GetSampleFilesBootstrapFolder() folder as it is no longer needed. +

+

+ The Include sample pages option also inserted some custom CSS rules + in your app.css file that override default Bootstrap styles. + For example, it changes primary button color or sets a different font, + which could disrupt the intended Bootstrap styling. While you may choose + to keep these changes, we recommend removing them as they conflict + with Bootstrap’s design principles and may lead to inconsistencies + (e.g., the primary button color may no longer match other Bootstrap elements). +

+

+ To resolve this, we suggest either: +

    +
  • removing these custom rules from app.css, or
  • +
  • starting with an empty app.css file.
  • +
+

+} + +@if (_setup.BootstrapTheme == BootstrapTheme.PlainProject) +{ + if (_setup.SamplePagesCreated) + { + if (_setup.TargetFramework == TargetFramework.Net9) + { +

+ When creating your Blazor project, you selected the Include sample pages option, + which automatically added Bootstrap CSS (version 5.3.3) to your project. + This means you likely won’t need to make any additional adjustments to use Bootstrap, + unless a new Bootstrap version is released and adopted by Havit.Blazor. +

+

+ However, the Include sample pages option also inserts some custom CSS rules + in your app.css file that override default Bootstrap styles. + For example, it changes primary button color or set a different font, + which could disrupt the intended Bootstrap styling. While you may choose + to keep these changes, we recommend removing them as they conflict + with Bootstrap’s design principles and may lead to inconsistencies + (e.g., the primary button color may no longer match other Bootstrap elements). +

+

+ To resolve this, we suggest either: +

    +
  • removing these custom rules from app.css, or
  • +
  • starting with an empty app.css file.
  • +
+

+

+ Consider switching to the . + This approach saves you from maintaining the Bootstrap CSS files manually + (you will need to update whenever a new version of Bootstrap is released + and adopted by Havit.Blazor). +

+ } + else + { +

+ When creating your Blazor project, you selected the Include sample pages option, + which automatically added Bootstrap CSS to your project. Unfortunately, the version + included in the .NET 8 template is outdated and cannot be used with Havit.Blazor. +

+

+ To resolve this, we suggest either: +

    +
  • + switching to the , or +
  • +
  • + starting with , or +
  • +
  • + updating the current Bootstrap CSS files to latest version (5.3.3) + and cleaning the sample CSS rules from app.css + file (or start with an empty one). +
  • +
+

+ } + } + else + { + @* Empty project without sample files + Plain Bootstrap from project *@ +

+ We recommend using the . + This approach saves you from maintaining the Bootstrap CSS files manually + (you will need to update whenever a new version of Bootstrap is released and adopted by Havit.Blazor). +

+

+ If you insist on embedding the Bootstrap CSS files into your project (for example if your users are unable to reach the CDN), + you can download the latest compiled version from + the official Bootstrap website. +

+ @if (HasClientProject()) + { +

+ After downloading, extract the contents and copy the bootstrap.min.css file + to the /wwwroot folder of your Blazor server project (the one without the .Client suffix). +

+ } + else + { +

+ After downloading, extract the contents and copy the bootstrap.min.css file + to the /wwwroot folder of your Blazor project. +

+ } +

+ Add the following line to the <head> section of your @GetHtmlHostFile() file: +

+ @if (HasStaticFileAssets()) + { + @("""""") + } + else + { + @("""""") + } + } +} + + + +

+ Our library only requires the inclusion of the appropriate Bootstrap JavaScript bundle (version @HxSetup.BootstrapVersion) in your project.
+ Any additional JavaScript needed for our components (small JS modules supporting the integration of individual components with Blazor) is automatically loaded as needed. +

+

+ Add the following line at the end of the <body> section of your @GetHtmlHostFile() file. +

+@if (_setup.ProjectSetup == ProjectSetup.WasmStandalone) +{ + @HxSetup.RenderBootstrapJavaScriptReference() +

+ This adds Bootstrap JavaScript with Popper to the project. +

+} +else +{ + @("""@((MarkupString)@HxSetup.RenderBootstrapJavaScriptReference())""") +

+ This snippet adds a <script> tag referencing Bootstrap JavaScript with Popper + that always matches the version required by Havit.Blazor, + so you won’t need to maintain the link manually in the future. +

+} + + + + +@if (HasClientProject()) +{ +

Add the following code to both {YourBlazorProject}/Components/_Imports.razor and {YourBlazorProject}.Client/_Imports.razor files:

+} +else +{ +

Add the following code to your _Imports.razor file:

+ +} +@("""@using Havit.Blazor.Components.Web""" + Environment.NewLine + """@using Havit.Blazor.Components.Web.Bootstrap""") +

+ This code imports the namespaces of the Havit.Blazor library so you can use the components in your Razor files + without having to add @@using directive to each file or specify the full namespace each time. +

+ + + + +@if (HasClientProject()) +{ +

Add the following code to service registrations in both {YourBlazorProject}/Program.cs and {YourBlazorProject}.Client/Program.cs files:

+} +else +{ +

Add the following code to service registrations in {YourBlazorProject}/Program.cs file:

+} +@("""builder.Services.AddHxServices();""") +

You will need to add the following using directive to the top of the file:

+@("""using Havit.Blazor.Components.Web;""") + + + + + +

+ [OPTIONAL] Some components require a specific project setup to function correctly. + This typically involves registering a service and adding a host component to a MainLayout.razor component. +

+

For detailed instructions, please refer to the documentation of the respective components:

+ + + + + +

You are now all set to utilize the full range of components in your Razor files. These components are prefixed with Hx. Rely on IntelliSense to guide you through their usage.

+ + + + + This entire documentation is created using the Havit.Blazor library and operates as a Blazor Web App project with WebAssembly interactivity and server prerendering. + You can view the source code of this documentation on GitHub. + diff --git a/Havit.Blazor.Documentation/Pages/GettingStarted/GettingStarted.razor.cs b/Havit.Blazor.Documentation/Pages/GettingStarted/GettingStarted.razor.cs new file mode 100644 index 00000000..a54317f4 --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/GettingStarted/GettingStarted.razor.cs @@ -0,0 +1,120 @@ +namespace Havit.Blazor.Documentation.Pages.GettingStarted; + +public partial class GettingStarted( + NavigationManager navigationManager) +{ + [SupplyParameterFromQuery(Name = nameof(SetupModel.TargetFramework))] public string TargetFrameworkQuery { get; set; } + [SupplyParameterFromQuery(Name = nameof(SetupModel.ProjectSetup))] public string ProjectSetupQuery { get; set; } + [SupplyParameterFromQuery(Name = nameof(SetupModel.BwaRenderMode))] public string BwaRenderModeQuery { get; set; } + [SupplyParameterFromQuery(Name = nameof(SetupModel.BootstrapTheme))] public string BootstrapThemeQuery { get; set; } + [SupplyParameterFromQuery(Name = nameof(SetupModel.SamplePagesCreated))] public string SamplePagesCreatedQuery { get; set; } + + private readonly NavigationManager _navigationManager = navigationManager; + + private SetupModel _setup = new SetupModel(); + + protected override void OnParametersSet() + { + if (Enum.TryParse(TargetFrameworkQuery, true, out var targetFramework)) + { + _setup.TargetFramework = targetFramework; + } + if (Enum.TryParse(ProjectSetupQuery, true, out var projectSetup)) + { + _setup.ProjectSetup = projectSetup; + } + if (Enum.TryParse(BwaRenderModeQuery, true, out var bwaRenderMode)) + { + _setup.BwaRenderMode = bwaRenderMode; + } + if (Enum.TryParse(BootstrapThemeQuery, true, out var bootstrapTheme)) + { + _setup.BootstrapTheme = bootstrapTheme; + } + if (bool.TryParse(SamplePagesCreatedQuery, out var samplePagesCreated)) + { + _setup.SamplePagesCreated = samplePagesCreated; + } + } + + private void ChangeSetup(SetupModel newSetup) + { + if (newSetup != _setup) + { + _setup = newSetup; + + // DO NOT CALL UpdateUri() unless the issue gets resolved - it causes page scrolling to the top + // https://github.com/dotnet/aspnetcore/issues/40190 - Blazor NavigationManager.NavigateTo always scrolls page to the top + // UpdateUri(); + } + } + + private void UpdateUri() + { + _navigationManager.NavigateTo(GetSetupUri(_setup), replace: true); + } + + private string GetSetupUri(SetupModel setup) + { + return _navigationManager.GetUriWithQueryParameters(new Dictionary + { + { nameof(SetupModel.TargetFramework), setup.TargetFramework.ToString() }, + { nameof(SetupModel.ProjectSetup), setup.ProjectSetup.ToString() }, + { nameof(SetupModel.BwaRenderMode), setup.BwaRenderMode.ToString() }, + { nameof(SetupModel.BootstrapTheme), setup.BootstrapTheme.ToString() }, + { nameof(SetupModel.SamplePagesCreated), setup.SamplePagesCreated.ToString() } + }); + } + + private bool HasClientProject() + { + if ((_setup.ProjectSetup == ProjectSetup.BlazorWebApp) + && ((_setup.BwaRenderMode == BwaRenderMode.Auto) || (_setup.BwaRenderMode == BwaRenderMode.Wasm))) + { + return true; + } + return false; + } + + private bool HasStaticFileAssets() + { + if ((_setup.TargetFramework == TargetFramework.Net9) && (_setup.ProjectSetup != ProjectSetup.WasmStandalone)) + { + return true; + } + return false; + } + + private string GetHtmlHostFile() + { + if (_setup.ProjectSetup == ProjectSetup.WasmStandalone) + { + return "wwwroot/index.html"; + } + return "App.razor"; + } + + private string GetSampleFilesBootstrapFolder() + { + if (_setup.TargetFramework == TargetFramework.Net9) + { + return "wwwroot/lib/bootstrap"; + } + return "wwwroot/bootstrap"; + + } + + private record SetupModel + { + public TargetFramework TargetFramework { get; set; } = TargetFramework.Net9; + public ProjectSetup ProjectSetup { get; set; } = ProjectSetup.BlazorWebApp; + public BwaRenderMode BwaRenderMode { get; set; } = BwaRenderMode.Auto; + public BootstrapTheme BootstrapTheme { get; set; } = BootstrapTheme.HavitBlazor; + public bool SamplePagesCreated { get; set; } = false; + } + + private enum TargetFramework { Net8, Net9 } + private enum ProjectSetup { BlazorWebApp, Server, WasmStandalone } + private enum BwaRenderMode { Auto, Server, Wasm, None } + private enum BootstrapTheme { HavitBlazor, PlainCdn, PlainProject, Custom } +} diff --git a/Havit.Blazor.Documentation/Pages/GettingStarted_Demo.razor b/Havit.Blazor.Documentation/Pages/GettingStarted/GettingStarted_Demo.razor similarity index 100% rename from Havit.Blazor.Documentation/Pages/GettingStarted_Demo.razor rename to Havit.Blazor.Documentation/Pages/GettingStarted/GettingStarted_Demo.razor diff --git a/Havit.Blazor.Documentation/Pages/GettingStarted_CSS.CodeSnippet.html b/Havit.Blazor.Documentation/Pages/GettingStarted_CSS.CodeSnippet.html deleted file mode 100644 index a15675cd..00000000 --- a/Havit.Blazor.Documentation/Pages/GettingStarted_CSS.CodeSnippet.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/GettingStarted_CustomCSS.CodeSnippet.html b/Havit.Blazor.Documentation/Pages/GettingStarted_CustomCSS.CodeSnippet.html deleted file mode 100644 index 6fe4f4d2..00000000 --- a/Havit.Blazor.Documentation/Pages/GettingStarted_CustomCSS.CodeSnippet.html +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/GettingStarted_HxSetupCshtmlPage.CodeSnippet.html b/Havit.Blazor.Documentation/Pages/GettingStarted_HxSetupCshtmlPage.CodeSnippet.html deleted file mode 100644 index fdbc8384..00000000 --- a/Havit.Blazor.Documentation/Pages/GettingStarted_HxSetupCshtmlPage.CodeSnippet.html +++ /dev/null @@ -1,20 +0,0 @@ - - ... - - - @Html.Raw(HxSetup.RenderBootstrapCssReference()) - - - - - - - - ... - - - ... - - - @Html.Raw(HxSetup.RenderBootstrapJavaScriptReference()) - \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/GettingStarted_HxSetupRazorPage.CodeSnippet.html b/Havit.Blazor.Documentation/Pages/GettingStarted_HxSetupRazorPage.CodeSnippet.html deleted file mode 100644 index 4a2905a4..00000000 --- a/Havit.Blazor.Documentation/Pages/GettingStarted_HxSetupRazorPage.CodeSnippet.html +++ /dev/null @@ -1,20 +0,0 @@ - - ... - - - @((MarkupString)HxSetup.RenderBootstrapCssReference()) - - - - - - - - ... - - - ... - - - @((MarkupString)HxSetup.RenderBootstrapJavaScriptReference()) - \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/GettingStarted_JavaScript.CodeSnippet.html b/Havit.Blazor.Documentation/Pages/GettingStarted_JavaScript.CodeSnippet.html deleted file mode 100644 index 915d6306..00000000 --- a/Havit.Blazor.Documentation/Pages/GettingStarted_JavaScript.CodeSnippet.html +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/GettingStarted_Namespaces.CodeSnippet.razor b/Havit.Blazor.Documentation/Pages/GettingStarted_Namespaces.CodeSnippet.razor deleted file mode 100644 index 2de79717..00000000 --- a/Havit.Blazor.Documentation/Pages/GettingStarted_Namespaces.CodeSnippet.razor +++ /dev/null @@ -1,2 +0,0 @@ -@using Havit.Blazor.Components.Web -@using Havit.Blazor.Components.Web.Bootstrap \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/HeroCard.razor b/Havit.Blazor.Documentation/Pages/HeroCard.razor new file mode 100644 index 00000000..61645b68 --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/HeroCard.razor @@ -0,0 +1,11 @@ + + + +
+ +
+ @Title +
+

@Description

+
+
\ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/HeroCard.razor.cs b/Havit.Blazor.Documentation/Pages/HeroCard.razor.cs new file mode 100644 index 00000000..3cbdbb29 --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/HeroCard.razor.cs @@ -0,0 +1,12 @@ +namespace Havit.Blazor.Documentation.Pages; + +public partial class HeroCard +{ + [Parameter] public string Title { get; set; } + + [Parameter] public string Description { get; set; } + + [Parameter] public BootstrapIcon Icon { get; set; } + + [Parameter] public ThemeColor Color { get; set; } +} \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Index.razor b/Havit.Blazor.Documentation/Pages/Index.razor index bb3f72ee..4e4c9c7c 100644 --- a/Havit.Blazor.Documentation/Pages/Index.razor +++ b/Havit.Blazor.Documentation/Pages/Index.razor @@ -1,101 +1,84 @@ @page "/" -@page "/intro" - -

HAVIT Blazor Bootstrap

-

Free Bootstrap 5.3 components for ASP.NET Blazor.

- -

- GitHub Repository - nuget version - GitHub Release Notes - nuget downloads - GitHub - GitHub - Build Status -
- #StandWithUkraine: Russian warship, go f#ck yourself -

- - - - -

Havit.Blazor components have the following requirements:

-
    -
  • .NET 6.0 or newer (net8.0 and net6.0 multitargeting)
  • -
  • Most components require interactive rendering mode to work fully (limited functionality for static SSR where applicable)
  • -
- - - - - Try our enterprise project template which includes layered architecture, EF Core, gRPC code-first, ... - - - -

To incorporate the Havit.Blazor.Components.Web.Bootstrap package into your project, you can use the NuGet Package Manager or execute the following command:

-dotnet add package Havit.Blazor.Components.Web.Bootstrap -

This package should be added to the project where the components will be utilized, typically in the user interface layer. For instance, in Visual Studio Blazor templates, this would be YourApp.Client.

- - -

To ensure proper styling and functionality, add references to CSS and JavaScript in your project.

- -

Insert the following line into the <head> section of your HTML file. The specific file to modify depends on your project's configuration. This could be App.razor, index.html, or _Host.cshtml/_Layout.cshtml:

- - - - If you're utilizing a standard Blazor template, it's important to clean up your CSS files. Specifically, you should remove any unnecessary code from site.css and completely delete the bootstrap.min.css reference from either App.razor, index.html or _Host.cshtml/_Layout.cshtml. - - - -

If you prefer to utilize our custom Bootstrap theme, which is used in this documentation and our demos, substitute the first link with the following:

- -

Similarly, you can reference your custom Bootstrap build or any other Bootstrap theme in the same manner.

- - -

In the same HTML file, add the following line at the end of the <body> section. This includes the Bootstrap JavaScript Bundle with Popper:

- - @HxSetup.RenderBootstrapJavaScriptReference() - - - -

- If your Blazor app is hosted using an ASP.NET Core, take advantage of our HxSetup helper methods instead. These methods automatically emit the <link /> - and <script /> tags and handle versioning for you. -

-

For Blazor Web App (.NET 8 and above), put the following in App.razor.

- -

For Razor Page (.NET 7 and below), put the following into _Host.cshtml or _Layout.cshtml.

- - - -

Add the following code to your _Imports.razor file:

- - - -

Add the following line of code to your services registration, typically found in the Program.cs file of your Blazor client project:

-builder.Services.AddHxServices(); -

- For projects that originated from earlier Blazor templates, these service registrations may be found in the Startup.cs file, - specifically within the ConfigureServices() method. Note that in this case, you will not use the builder; - instead, register the services directly to the services collection. -

- - -

- [OPTIONAL] Some components require a specific project setup to function correctly. - This typically involves registering a service and adding a host component to App.razor or a MainLayout.razor component. -

-

For detailed instructions, please refer to the documentation of the respective components:

- - - -

You are now all set to utilize the full range of components in your Razor files. These components are prefixed with Hx. Rely on IntelliSense to guide you through their usage.

- - - - This entire documentation is created using the Havit.Blazor library and operates as a Blazor WebAssembly ASP.NET Core Hosted project - with server-side prerendering. You can view the source code of this documentation on GitHub. - +@layout HomeLayout + +HAVIT Blazor | Free Bootstrap 5 components for Blazor + + +
+
+
+

HAVIT Blazor

+

Free open-source ASP.NET Blazor components based on Bootstrap 5.

+
+ GitHub Repository + nuget version + @* GitHub Release Notes *@ + nuget downloads + GitHub + Build Status + GitHub +
+ #StandWithUkraine: Russian warship, go f#ck yourself +
+ +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
diff --git a/Havit.Blazor.Documentation/Pages/Index.razor.css b/Havit.Blazor.Documentation/Pages/Index.razor.css new file mode 100644 index 00000000..ee495e3b --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/Index.razor.css @@ -0,0 +1,3 @@ +.hero { + background-image: radial-gradient(56.1514% 56.1514% at 49.972% 38.959%, rgba(var(--bs-primary-rgb), 0.25) 0%, var(--bs-body-bg) 100%); +} \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Migrations/Migrating.razor b/Havit.Blazor.Documentation/Pages/Migrations/Migrating.razor deleted file mode 100644 index 111f8b92..00000000 --- a/Havit.Blazor.Documentation/Pages/Migrations/Migrating.razor +++ /dev/null @@ -1,78 +0,0 @@ -@page "/migrating" -@page "/migrating-to-v3" - -

Migrating to v4

-

Migrating your projects to version 4 is pretty easy.

- - -

Update the Havit.Blazor.Components.Web.Bootstrap package through NuGet Package Manager or with the following command:

-dotnet add package Havit.Blazor.Components.Web.Bootstrap - - - -

If you are using Bootstrap CSS from CDN, update the following line in your HTML head section. It's either index.html or _Host.cshtml/_Layout.cshtml depending on whether you're running WebAssembly or Server:

- -@@HxSetup.RenderBootstrapCssReference(BootstrapFlavor.PlainBootstrap) -or -@HxSetup.RenderBootstrapCssReference(BootstrapFlavor.PlainBootstrap) - -

- If you are referencing our Bootstrap CSS build _content/Havit.Blazor.Components.Web.Bootstrap/bootstrap.css, it is updated automatically. - If you are referencing your custom Bootstrap build/theme, upgrade it to Bootstrap 5.3. -

- - -

At the end of the HTML <body> section of either index.html or _Host.cshtml/_Layout.cshtml, update this line referencing Bootstrap JavaScript Bundle (with Popper) from CDN:

- -@@HxSetup.RenderBootstrapJavaScriptReference() -or -@HxSetup.RenderBootstrapJavaScriptReference() - - - - - -

- We replaced HxInputCheckbox with the new HxCheckbox and HxInputSwitch with the new HxSwitch.
- The original Label parameter is now Text (the Label parameter of new components has a different purpose, see HxCheckbox documentation). -

- - - -

- As Bootstrap 5.3 deprecated the original color schemes, the ColorScheme parameter was removed.
- Use the new parameter ColorMode="ColorMode.Dark" if needed (Light is the default). -

-

- UPDATE v4.0.1: As Bootstrap 5.3 now expects you to use text and background color CSS utilities to customize the navbar with the CssClass parameter, - we changed the default value of the Color parameter to ThemeColor.None to not interfere with your CSS classes. You can still use Color="ThemeColor.Light" to get the original navbar default color. -

- - -

- The ContextMenu parameter was removed from HxGrid as it was replaced by HxContextMenuGridColumn a while ago and was already marked as [Obsolete].
-

- - - - -

Check your CSS customizations and adjust them according to the new Bootstrap 5.3 and Havit.Blazor setup. Focus on these areas where there were some changes that might affect your visual:

- - - -

- Bootstrap 5.3 introduced the dark color scheme and we have added support for it in all our components.
- Use the data-bs-theme="dark" attribute on the <html> element to enable it.
- Please refer to the Bootstrap Dark mode documentation for more details. -

- - - -

- If you encounter any issues while migrating your project to v3, please feel free to use the GitHub Discussions to ask your questions.
- If you find anything that appears to be a bug, please report it on our GitHub Issues. -

\ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Migrations/Migrating_HxCheckbox_HxSwitch.CodeSnippet.razor b/Havit.Blazor.Documentation/Pages/Migrations/Migrating_HxCheckbox_HxSwitch.CodeSnippet.razor deleted file mode 100644 index 677b3d10..00000000 --- a/Havit.Blazor.Documentation/Pages/Migrations/Migrating_HxCheckbox_HxSwitch.CodeSnippet.razor +++ /dev/null @@ -1,5 +0,0 @@ - @* v2 *@ - @* v3 *@ - - @* v2 *@ - @* v3 *@ \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Migrations/Migrating_HxGrid_ContextMenu.CodeSnippet.razor b/Havit.Blazor.Documentation/Pages/Migrations/Migrating_HxGrid_ContextMenu.CodeSnippet.razor deleted file mode 100644 index bffb32d6..00000000 --- a/Havit.Blazor.Documentation/Pages/Migrations/Migrating_HxGrid_ContextMenu.CodeSnippet.razor +++ /dev/null @@ -1,24 +0,0 @@ -@* Original *@ - - - ... - - - - - - - -@* New structure *@ - - - ... - - - - - - - ... - - \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Premium/GatewayToPremium.razor b/Havit.Blazor.Documentation/Pages/Premium/GatewayToPremium.razor new file mode 100644 index 00000000..008e65d1 --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/Premium/GatewayToPremium.razor @@ -0,0 +1,31 @@ +@page "/premium/access-content" +@layout HomeLayout + +Access Premium Content | HAVIT Blazor Bootstrap - Free components for ASP.NET Core Blazor + + +
+ + +

HAVIT Blazor Premium

+

+ You are accessing content for Premium subscribers.
+
+ To access the content, you must be logged in to a GitHub account with an active Premium sponsorship.
+ Without this, the content will not be accessible, and you may see a 404 error indicating it doesn't exist.
+
+ How to get Premium? +

+ + +
+
+
+ + diff --git a/Havit.Blazor.Documentation/Pages/Premium/GatewayToPremium.razor.cs b/Havit.Blazor.Documentation/Pages/Premium/GatewayToPremium.razor.cs new file mode 100644 index 00000000..beae5a23 --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/Premium/GatewayToPremium.razor.cs @@ -0,0 +1,76 @@ +using Havit.Blazor.Documentation.Services; +using Microsoft.JSInterop; + +namespace Havit.Blazor.Documentation.Pages.Premium; + +public partial class GatewayToPremium( + IHttpContextProxy httpContextProxy, + NavigationManager navigationManager, + IJSRuntime jSRuntime) : IAsyncDisposable +{ + [SupplyParameterFromQuery] public string Url { get; set; } + + private const string SkipGatewayPageCookieEnabledValue = "1"; + private readonly NavigationManager _navigationManager = navigationManager; + private readonly IJSRuntime _jSRuntime = jSRuntime; + private readonly IHttpContextProxy _httpContextProxy = httpContextProxy; + + private IJSObjectReference _jsModule; + private bool _skipGatewayPage = true; + + protected override async Task OnInitializedAsync() + { + await RedirectToPremiumContentIfCookieIsSet(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await RedirectToPremiumContentIfCookieIsSet(); + } + } + + private async Task RedirectToPremiumContentIfCookieIsSet() + { + if (!RendererInfo.IsInteractive) + { + if (_httpContextProxy.GetCookieValue("SkipGatewayPage") == SkipGatewayPageCookieEnabledValue) + { + _navigationManager.NavigateTo(Url); + } + } + else + { + await EnsureJsModuleAsync(); + string skipGatewayPage = await _jsModule.InvokeAsync("getSkipGatewayPage"); + if (skipGatewayPage == SkipGatewayPageCookieEnabledValue) + { + _navigationManager.NavigateTo(Url); + } + } + } + + private async Task ContinueToPremiumContent() + { + if (_skipGatewayPage) + { + await EnsureJsModuleAsync(); + await _jsModule.InvokeVoidAsync("setSkipGatewayPage", SkipGatewayPageCookieEnabledValue); + } + _navigationManager.NavigateTo(Url); + } + + private async Task EnsureJsModuleAsync() + { + _jsModule ??= await _jSRuntime.ImportModuleAsync($"./Pages/Premium/{nameof(GatewayToPremium)}.razor.js"); + } + + public async ValueTask DisposeAsync() + { + if (_jsModule != null) + { + await _jsModule.DisposeAsync(); + } + } +} diff --git a/Havit.Blazor.Documentation/Pages/Premium/GatewayToPremium.razor.js b/Havit.Blazor.Documentation/Pages/Premium/GatewayToPremium.razor.js new file mode 100644 index 00000000..74161088 --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/Premium/GatewayToPremium.razor.js @@ -0,0 +1,21 @@ +export function setSkipGatewayPage(skipGatewayPage) { + const date = new Date(); + date.setTime(date.getTime() + (24 * 60 * 60 * 1000)); // 24 hours + document.cookie = "SkipGatewayPage=" + skipGatewayPage + "; expires = " + date.toGMTString() + "; path = /"; +} + +export function getSkipGatewayPage() { + const name = "SkipGatewayPage="; + const decodedCookie = decodeURIComponent(document.cookie); + const ca = decodedCookie.split(';'); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1); + } + if (c.indexOf(name) === 0) { + return c.substring(name.length, c.length); + } + } + return ""; +} diff --git a/Havit.Blazor.Documentation/Pages/Premium/GetPremium.razor b/Havit.Blazor.Documentation/Pages/Premium/GetPremium.razor new file mode 100644 index 00000000..32d4d8a3 --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/Premium/GetPremium.razor @@ -0,0 +1,159 @@ +@page "/premium" +@layout HomeLayout + +Get Premium | HAVIT Blazor Bootstrap - Free components for ASP.NET Core Blazor + + +
+

Upgrade to Premium

+
+ Enjoy access to a carefully selected collection of prebuilt UI blocks, complete with Blazor, C# and CSS source code, + priority support, and enterprise project source code for your inspiration. +
+
+ +
+
+
+
+ + +
Free forever
+

$0 Free for everyone

+
+ + + + +
+ + Get free content + +
+
+
+
+ + +
Premium
+

$19 per user/month

+
+ @* Everything from Free, plus *@ + + + + +
+ + Get Premium + +
+
+
+
+ + +
Custom
+

Contact sales

+
+ Tailor-made support for your team + + + + +
+ + Contact us + +
+
+
+
+
+ +
+
+

Frequently
asked
questions.

+
+
+ + + Can I use Free plan for commercial development? + + Yes, our commitment is to keep the core component library free forever for everyone. You can use it in commercial projects without any restrictions. + + + + How does the priority support work? + + We actively monitor GitHub issues and discussions, + and any tickets from Premium sponsors/subscribers are flagged for faster attention. + When you raise an issue or start a discussion with a linked GitHub account associated + with your Premium subscription, we prioritize our responses and aim to provide more dedicated support, + ensuring your feedback and questions are addressed promptly. + + + + What happens if I cancel the subscription? + + You can cancel your subscription at any time. If you cancel your subscription, you will lose access to the Premium content. + This means your access to the Havit.Blazor.Premium repository, you’ll lose access to all premium features, updates, and support, as well as any new content added to the repository. + You’ll still be able to use any code you’ve integrated into your own projects, as allowed under our license. However, you must delete all copies of the repository itself + (including any forks or local clones). + + + + Can I purchase Premium through a GitHub organization account? + + Yes, you can purchase Premium using a GitHub organization account. + However, after completing the sponsorship, + please email blazor@havit.eu + with the personal (individual) GitHub account + to which the Premium benefits should be activated. + + + + Can I purchase Premium for multiple users in my organization? + + Yes, Premium can be purchased for multiple users in your organization. + Simply multiply the number of users by the subscription cost + and after completing the sponsorship, email blazor@havit.eu + with a list of the personal (individual) GitHub accounts + to which the benefits should be activated. + + + + What if I need an invoice? + + If you need an invoice for your Premium subscription, + please email blazor@havit.eu. + We’ll be happy to assist you with the necessary documentation. + + + + Is there any other purchase option than GitHub Sponsorship? + + Currently, we've chosen GitHub Sponsorship as the simplest option. + If GitHub Sponsorship isn’t suitable for you, please email + us at blazor@havit.eu, + and we can arrange an alternative solution, such as an invoice and wire transfer. + + + +
+
+
+ + \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Premium/GetPremium.razor.css b/Havit.Blazor.Documentation/Pages/Premium/GetPremium.razor.css new file mode 100644 index 00000000..1c0b3345 --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/Premium/GetPremium.razor.css @@ -0,0 +1,3 @@ +.hero { + background-image: radial-gradient(56.1514% 56.1514% at 49.972% 38.959%, rgba(var(--bs-primary-rgb), 0.3) 0%, var(--bs-body-bg) 100%); +} \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Premium/PlanItem.razor b/Havit.Blazor.Documentation/Pages/Premium/PlanItem.razor new file mode 100644 index 00000000..5aba58fc --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/Premium/PlanItem.razor @@ -0,0 +1,9 @@ +
+ + @Text +
+ +@code +{ + [Parameter] public string Text { get; set; } +} \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Premium/PremiumWelcome.razor b/Havit.Blazor.Documentation/Pages/Premium/PremiumWelcome.razor new file mode 100644 index 00000000..ab6c7dbb --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/Premium/PremiumWelcome.razor @@ -0,0 +1,114 @@ +@page "/premium/welcome" +@layout HomeLayout +@inject NavigationManager NavigationManager + +Welcome to Premium | HAVIT Blazor Bootstrap - Free components for ASP.NET Core Blazor + + +
+
+
+

Welcome to HAVIT Blazor Premium

+
+ We're excited to welcome you to the HAVIT Blazor Premium community! As a Premium subscriber, + you have exclusive access to additional resources and priority support, + designed to help you get the most out of the HAVIT Blazor library. +
+
+ +
+
+
+
+

Access to Premium repository

+

+ The Havit.Blazor.Premium repository + on GitHub includes exclusive UI Blocks and samples specifically for Premium subscribers. + To access this repository, please ensure you are logged into your GitHub account with an active Premium subscription. + Once authenticated, you’ll gain full access to the latest premium content and examples. +

+
+
+ +
+
+

Priority support

+

+ Priority support is one of the key benefits of your Premium subscription. We provide prioritized assistance through: +

+
    +
  • + GitHub issues: Submit any bug reports or feature + requests directly in the Havit.Blazor GitHub repository. + Reports from Premium subscribers are flagged and receive priority attention from our team. +
  • +
  • + GitHub discussions: For general questions + and usage advice, feel free to post in the Discussions section of our GitHub repository. + Our team will prioritize flagged questions from Premium subscribers to ensure your queries are addressed promptly. +
  • +
+
+
+ +
+
+

Access to showcase project

+

+ As a Premium subscriber, you also gain exclusive access to the GoranG3 repository, + a closed-source commercial information system that we allow our Premium subscribers to access as a showcase project.
+ This repository provides you with real-world examples of how to effectively use HAVIT Blazor components in a professional setting. + You can explore the source code to draw inspiration and observe best practices in implementing Blazor + components within a complex, production-grade environment. +

+

+ Access to the showcase project will be configured within 24 hours of your subscription activation. You’ll receive an invitation email once your access is ready. +

+
+
+ +
+
+

GitHub organization account sponsors

+

+ If your sponsorship is made from a GitHub organization account, please provide the personal GitHub account to receive Premium benefits. + Send an email to blazor@havit.eu with the following details: +

+
    +
  • The name of your organization’s GitHub account used for the sponsorship.
  • +
  • The personal GitHub account name to assign Premium benefits to.
  • +
+

+ This ensures we can correctly link your Premium benefits. Thank you for your support! +

+
+
+
+
+ +
+

+ Enjoy your Premium subscription, and feel free to contact us if you have any questions or need assistance! +

+
+
+
+ +@if (NavigationManager.Uri.Contains("utm_")) +{ + +} \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Pages/Premium/PremiumWelcome.razor.css b/Havit.Blazor.Documentation/Pages/Premium/PremiumWelcome.razor.css new file mode 100644 index 00000000..c89e9dc9 --- /dev/null +++ b/Havit.Blazor.Documentation/Pages/Premium/PremiumWelcome.razor.css @@ -0,0 +1,3 @@ +.hero { + background-image: radial-gradient(56.1514% 56.1514% at 49.972% 38.959%, rgba(var(--bs-primary-rgb), 0.25) 0%, var(--bs-body-bg) 100%); +} diff --git a/Havit.Blazor.Documentation/Program.cs b/Havit.Blazor.Documentation/Program.cs index 1e2cf8e7..6473a03d 100644 --- a/Havit.Blazor.Documentation/Program.cs +++ b/Havit.Blazor.Documentation/Program.cs @@ -22,8 +22,15 @@ public static async Task Main(string[] args) builder.Services.AddTransient(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + builder.Services.AddScoped(); + builder.Services.AddCascadingValue(services => + { + var docColorModeStateProvider = services.GetRequiredService(); + return new DocColorModeCascadingValueSource(docColorModeStateProvider); + }); builder.Services.AddTransient(); diff --git a/Havit.Blazor.Documentation/App.razor b/Havit.Blazor.Documentation/Routes.razor similarity index 100% rename from Havit.Blazor.Documentation/App.razor rename to Havit.Blazor.Documentation/Routes.razor diff --git a/Havit.Blazor.Documentation/Services/DocPageNavigationItemsHolder.cs b/Havit.Blazor.Documentation/Services/DocPageNavigationItemsHolder.cs deleted file mode 100644 index 163b12dc..00000000 --- a/Havit.Blazor.Documentation/Services/DocPageNavigationItemsHolder.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Havit.Blazor.Documentation.Shared.Components; - -namespace Havit.Blazor.Documentation.Services; - -public class DocPageNavigationItemsHolder : IDocPageNavigationItemsHolder -{ - private Dictionary> _items = new(); - - public void RegisterNew(IDocPageNavigationItem item, string url) - { - string page = GetPageFromUrl(url); - EnsureKey(page); - - if (!_items[page].Any(st => st.Id == item.Id)) - { - _items[page].Add(item); - } - } - - public ICollection RetrieveAll(string url) - { - string page = GetPageFromUrl(url); - EnsureKey(page); - return _items[page]; - } - - private void EnsureKey(string page) - { - if (!_items.ContainsKey(page)) - { - _items.Add(page, new List()); - } - } - - private string GetPageFromUrl(string url) - { - return url?.Split('#')[0]; - } - - public void Clear() - { - _items.Clear(); - } -} diff --git a/Havit.Blazor.Documentation/Services/DocPageNavigationItemsTracker.cs b/Havit.Blazor.Documentation/Services/DocPageNavigationItemsTracker.cs new file mode 100644 index 00000000..7d22c846 --- /dev/null +++ b/Havit.Blazor.Documentation/Services/DocPageNavigationItemsTracker.cs @@ -0,0 +1,35 @@ +using Havit.Blazor.Documentation.Shared.Components; + +namespace Havit.Blazor.Documentation.Services; + +public class DocPageNavigationItemsTracker : IDocPageNavigationItemsTracker +{ + private readonly Dictionary> _itemsByPage = new(); + + public void RegisterNavigationItem(string url, DocPageNavigationItem item) + { + Contract.Requires(item != null); + + var pageKey = UrlHelper.RemoveFragmentFromUrl(url); + + if (!_itemsByPage.ContainsKey(pageKey)) + { + _itemsByPage.Add(pageKey, [item]); + } + else if (!_itemsByPage[pageKey].Exists(st => st.Id == item.Id)) + { + _itemsByPage[pageKey].Add(item); + } + } + + public List GetPageNavigationItems(string url) + { + string pageKey = UrlHelper.RemoveFragmentFromUrl(url); + + if (_itemsByPage.TryGetValue(pageKey, out var items)) + { + return items; + } + return []; + } +} diff --git a/Havit.Blazor.Documentation/Services/IDocPageNavigationItemsHolder.cs b/Havit.Blazor.Documentation/Services/IDocPageNavigationItemsHolder.cs deleted file mode 100644 index 385cbc9d..00000000 --- a/Havit.Blazor.Documentation/Services/IDocPageNavigationItemsHolder.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Havit.Blazor.Documentation.Shared.Components; - -namespace Havit.Blazor.Documentation.Services; - -public interface IDocPageNavigationItemsHolder -{ - void RegisterNew(IDocPageNavigationItem item, string url); - - ICollection RetrieveAll(string url); - - void Clear(); -} diff --git a/Havit.Blazor.Documentation/Services/IDocPageNavigationItemsTracker.cs b/Havit.Blazor.Documentation/Services/IDocPageNavigationItemsTracker.cs new file mode 100644 index 00000000..492128ea --- /dev/null +++ b/Havit.Blazor.Documentation/Services/IDocPageNavigationItemsTracker.cs @@ -0,0 +1,10 @@ +using Havit.Blazor.Documentation.Shared.Components; + +namespace Havit.Blazor.Documentation.Services; + +public interface IDocPageNavigationItemsTracker +{ + void RegisterNavigationItem(string url, DocPageNavigationItem item); + + List GetPageNavigationItems(string url); +} diff --git a/Havit.Blazor.Documentation/Services/IHttpContextProxy.cs b/Havit.Blazor.Documentation/Services/IHttpContextProxy.cs new file mode 100644 index 00000000..564c9f90 --- /dev/null +++ b/Havit.Blazor.Documentation/Services/IHttpContextProxy.cs @@ -0,0 +1,11 @@ +namespace Havit.Blazor.Documentation.Services; + +/// +/// Provides methods to interact with the server-side HTTP context (during prerendering). +/// Avoids direct dependency on HttpContext in Client project (WASM). +/// +public interface IHttpContextProxy +{ + bool IsSupported(); + string GetCookieValue(string key); +} diff --git a/Havit.Blazor.Documentation/Services/UrlHelper.cs b/Havit.Blazor.Documentation/Services/UrlHelper.cs new file mode 100644 index 00000000..12b163c3 --- /dev/null +++ b/Havit.Blazor.Documentation/Services/UrlHelper.cs @@ -0,0 +1,15 @@ +namespace Havit.Blazor.Documentation.Services; + +public static class UrlHelper +{ + public static string RemoveFragmentFromUrl(string url) + { + if (url == null) + { + return null; + } + + int hashIndex = url.IndexOf('#'); + return (hashIndex == -1) ? url : url.Substring(0, hashIndex); + } +} diff --git a/Havit.Blazor.Documentation/Services/WebAssemblyHttpContextProxy.cs b/Havit.Blazor.Documentation/Services/WebAssemblyHttpContextProxy.cs new file mode 100644 index 00000000..890d6c98 --- /dev/null +++ b/Havit.Blazor.Documentation/Services/WebAssemblyHttpContextProxy.cs @@ -0,0 +1,7 @@ +namespace Havit.Blazor.Documentation.Services; + +public class WebAssemblyHttpContextProxy : IHttpContextProxy +{ + public bool IsSupported() => false; + public string GetCookieValue(string key) => throw new NotSupportedException(); +} diff --git a/Havit.Blazor.Documentation/Shared/Components/ComponentApiDoc.razor b/Havit.Blazor.Documentation/Shared/Components/ComponentApiDoc.razor index fdd8d385..c3e73e9f 100644 --- a/Havit.Blazor.Documentation/Shared/Components/ComponentApiDoc.razor +++ b/Havit.Blazor.Documentation/Shared/Components/ComponentApiDoc.razor @@ -1,11 +1,11 @@ @using Havit.Blazor.Documentation.Services -@{ - var plainTypeName = ApiRenderer.RemoveSpecialCharacters(Type.Name); -} - + + + + @if (!String.IsNullOrWhiteSpace(_model.Class?.Comments?.Summary)) { -

@((MarkupString)_model.Class.Comments.Summary)

+

@((MarkupString)_model.Class.Comments.Summary)

} @@ -15,226 +15,226 @@ @if (HasApi) { - + } @if (IsDelegate) { -
@((MarkupString)_model.DelegateSignature)
+
@((MarkupString)_model.DelegateSignature)
} @if (IsEnum) { - -
-
@footerTemplates[i].Template
- - - - - - - - - @foreach (var enumMember in _model.EnumMembers) - { - - - - - - } - -
NameValueDescription
@enumMember.Name@enumMember.Value@((MarkupString)enumMember.Summary)
- + +
+ + + + + + + + + + @foreach (var enumMember in _model.EnumMembers) + { + + + + + + } + +
NameValueDescription
@enumMember.Name@enumMember.Value@((MarkupString)enumMember.Summary)
+
} @if (HasParameters) { - - -
- - - - - - - - - - @foreach (var property in _model.Parameters.OrderByDescending(p => p.EditorRequired).ThenBy(p => p.PropertyInfo.Name)) - { - - - - - - } - -
NameTypeDescription
- @if (property.IsStatic) - { - static - } - - @property.PropertyInfo.Name - @if (property.EditorRequired) - { - REQUIRED - } - @((MarkupString)ApiRenderer.FormatType(property.PropertyInfo.PropertyType))@((MarkupString)property.Comments.Summary)
-
+ + +
+ + + + + + + + + + @foreach (var property in _model.Parameters.OrderByDescending(p => p.EditorRequired).ThenBy(p => p.PropertyInfo.Name)) + { + + + + + + } + +
NameTypeDescription
+ @if (property.IsStatic) + { + static + } + + @property.PropertyInfo.Name + @if (property.EditorRequired) + { + REQUIRED + } + @((MarkupString)ApiRenderer.FormatType(property.PropertyInfo.PropertyType))@((MarkupString)property.Comments.Summary)
+
} @if (HasProperties) { - -
- - - - - - - - - - @foreach (var property in _model.Properties.OrderBy(p => p.PropertyInfo.Name)) - { - - - - - - } - -
NameTypeDescription
@property.PropertyInfo.Name@((MarkupString)ApiRenderer.FormatType(property.PropertyInfo.PropertyType))@((MarkupString)property.Comments.Summary)
-
+ +
+ + + + + + + + + + @foreach (var property in _model.Properties.OrderBy(p => p.PropertyInfo.Name)) + { + + + + + + } + +
NameTypeDescription
@property.PropertyInfo.Name@((MarkupString)ApiRenderer.FormatType(property.PropertyInfo.PropertyType))@((MarkupString)property.Comments.Summary)
+
} @if (HasEvents) { - - -
- - - - - - - - - - @foreach (var currentEvent in _model.Events.OrderBy(e => e.PropertyInfo.Name)) - { - - - - - - } - -
NameTypeDescription
@currentEvent.PropertyInfo.Name @((MarkupString)ApiRenderer.FormatType(currentEvent.PropertyInfo.PropertyType))@((MarkupString)currentEvent.Comments.Summary)
-
+ + +
+ + + + + + + + + + @foreach (var currentEvent in _model.Events.OrderBy(e => e.PropertyInfo.Name)) + { + + + + + + } + +
NameTypeDescription
@currentEvent.PropertyInfo.Name @((MarkupString)ApiRenderer.FormatType(currentEvent.PropertyInfo.PropertyType))@((MarkupString)currentEvent.Comments.Summary)
+
} @if (HasMethods) { - -
- - - - - - - - - - @foreach (var method in _model.Methods.OrderBy(m => m.MethodInfo.Name)) - { - - - - - - } - -
MethodReturnsDescription
@method.MethodInfo.Name@((MarkupString)@method.GetParameters())@((MarkupString)ApiRenderer.FormatMethodReturnType(method.MethodInfo.ReturnType, _model))@((MarkupString)method.Comments.Summary)
-
+ +
+ + + + + + + + + + @foreach (var method in _model.Methods.OrderBy(m => m.MethodInfo.Name)) + { + + + + + + } + +
MethodReturnsDescription
@method.MethodInfo.Name@((MarkupString)@method.GetParameters())@((MarkupString)ApiRenderer.FormatMethodReturnType(method.MethodInfo.ReturnType, _model))@((MarkupString)method.Comments.Summary)
+
} @if (HasStaticProperties) { - - -
- - - - - - - - - - @foreach (var property in _model.StaticProperties.OrderBy(p => p.PropertyInfo.Name)) - { - - - - - - } - -
PropertyTypeDescription
@property.PropertyInfo.Name@((MarkupString)ApiRenderer.FormatType(property.PropertyInfo.PropertyType))@((MarkupString)property.Comments.Summary)
-
+ + +
+ + + + + + + + + + @foreach (var property in _model.StaticProperties.OrderBy(p => p.PropertyInfo.Name)) + { + + + + + + } + +
PropertyTypeDescription
@property.PropertyInfo.Name@((MarkupString)ApiRenderer.FormatType(property.PropertyInfo.PropertyType))@((MarkupString)property.Comments.Summary)
+
} @if (HasStaticMethods) { - - -
- - - - - - - - - - @foreach (var method in _model.StaticMethods.OrderBy(m => m.MethodInfo.Name)) - { - - - - - - } - -
MethodTypeDescription
@method.MethodInfo.Name @((MarkupString)@method.GetParameters())@((MarkupString)ApiRenderer.FormatType(method.MethodInfo.ReturnType))@((MarkupString)method.Comments.Summary)
-
+ + +
+ + + + + + + + + + @foreach (var method in _model.StaticMethods.OrderBy(m => m.MethodInfo.Name)) + { + + + + + + } + +
MethodTypeDescription
@method.MethodInfo.Name @((MarkupString)@method.GetParameters())@((MarkupString)ApiRenderer.FormatType(method.MethodInfo.ReturnType))@((MarkupString)method.Comments.Summary)
+
} @if (HasCssVariables) { - -
- - - - - - - - - - @CssVariables - -
NameDescriptionDefault
-
+ +
+ + + + + + + + + + @CssVariables + +
NameDescriptionDefault
+
} diff --git a/Havit.Blazor.Documentation/Shared/Components/ComponentApiDoc.razor.cs b/Havit.Blazor.Documentation/Shared/Components/ComponentApiDoc.razor.cs index da408c7e..ec72de62 100644 --- a/Havit.Blazor.Documentation/Shared/Components/ComponentApiDoc.razor.cs +++ b/Havit.Blazor.Documentation/Shared/Components/ComponentApiDoc.razor.cs @@ -17,8 +17,7 @@ public partial class ComponentApiDoc [Parameter] public Type Type { get; set; } [Inject] protected IComponentApiDocModelBuilder ComponentApiDocModelBuilder { get; set; } - - private ComponentApiDocModel _model; + [Inject] protected NavigationManager NavigationManager { get; set; } private bool HasApi => _model.HasValues || CssVariables is not null; private bool IsDelegate => _model.IsDelegate; @@ -31,8 +30,20 @@ public partial class ComponentApiDoc private bool HasStaticMethods => !_model.IsEnum && _model.StaticMethods.Any(); private bool HasCssVariables => CssVariables is not null; + private ComponentApiDocModel _model; + private string _plainTypeName; + + protected override void OnParametersSet() { _model = ComponentApiDocModelBuilder.BuildModel(Type); + _plainTypeName = ApiRenderer.RemoveSpecialCharacters(Type.Name); + + } + + private string GetRelativeCanonicalUrl(string plainTypeName) + { + bool isComponent = NavigationManager.ToBaseRelativePath(NavigationManager.Uri).Contains("components"); + return $"{(isComponent ? "components" : "types")}/{plainTypeName}"; } } diff --git a/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeCascadingValueSource.cs b/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeCascadingValueSource.cs new file mode 100644 index 00000000..547c94a5 --- /dev/null +++ b/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeCascadingValueSource.cs @@ -0,0 +1,29 @@ +namespace Havit.Blazor.Documentation.Shared.Components.DocColorMode; + +// Reference implementation in AuthenticationStateCascadingValueSource +// https://github.com/dotnet/aspnetcore/blob/79d06db8a4be29165e24eb841054a337161bd09a/src/Components/Authorization/src/CascadingAuthenticationStateServiceCollectionExtensions.cs#L29-L56 +public class DocColorModeCascadingValueSource : CascadingValueSource, IDisposable +{ + private readonly IDocColorModeProvider _docColorModeStateProvider; + + public DocColorModeCascadingValueSource(IDocColorModeProvider docColorModeStateProvider) + : base(docColorModeStateProvider.GetColorMode, isFixed: false) + { + _docColorModeStateProvider = docColorModeStateProvider; + _docColorModeStateProvider.ColorModeChanged += HandleColorModeChanged; + } + + private void HandleColorModeChanged(ColorMode colorMode) + { + // It's OK to discard the task because this only represents the duration of the dispatch to sync context. + // It handles any exceptions internally by dispatching them to the renderer within the context of whichever + // component threw when receiving the update. This is the same as how a CascadingValue doesn't get notified + // about exceptions that happen inside the recipients of value notifications. + _ = NotifyChangedAsync(colorMode); + } + + public void Dispose() + { + _docColorModeStateProvider.ColorModeChanged -= HandleColorModeChanged; + } +} diff --git a/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeClientResolver.cs b/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeClientResolver.cs deleted file mode 100644 index 7794db69..00000000 --- a/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeClientResolver.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Havit.Blazor.Documentation.Shared.Components.DocColorMode; - -public class DocColorModeClientResolver : IDocColorModeResolver -{ - public ColorMode GetColorMode() - { - return ColorMode.Auto; // client always resolves to auto, cookie used for server prerendering - } -} diff --git a/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeProvider.cs b/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeProvider.cs new file mode 100644 index 00000000..bc705104 --- /dev/null +++ b/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeProvider.cs @@ -0,0 +1,78 @@ +using Havit.Blazor.Documentation.Services; + +namespace Havit.Blazor.Documentation.Shared.Components.DocColorMode; + +public class DocColorModeProvider : IDisposable, IDocColorModeProvider +{ + private readonly IHttpContextProxy _httpContextProxy; + private readonly PersistentComponentState _persistentComponentState; + private PersistingComponentStateSubscription _persistingComponentStateSubscription; + private ColorMode? _colorMode; + + public DocColorModeProvider( + IHttpContextProxy httpContextProxy, + PersistentComponentState persistentComponentState) + { + _httpContextProxy = httpContextProxy; + _persistentComponentState = persistentComponentState; + _persistingComponentStateSubscription = _persistentComponentState.RegisterOnPersisting(PersistMode); + } + + public event ColorModeChangedHandler ColorModeChanged; + + /// + /// Raises the event. + /// + /// A that supplies the updated . + public void SetColorMode(ColorMode colorMode) + { + _colorMode = colorMode; + ColorModeChanged?.Invoke(colorMode); + } + + public ColorMode GetColorMode() + { + if (_colorMode == null) + { + ResolveInitialColorMode(); + } + return _colorMode.Value; + } + + private void ResolveInitialColorMode() + { + // prerendering + if (_httpContextProxy.IsSupported() + && _httpContextProxy.GetCookieValue("ColorMode") is string cookie + && Enum.TryParse(cookie, ignoreCase: true, out var mode)) + { + _colorMode = mode; + } + else if (_persistentComponentState.TryTakeFromJson("ColorMode", out var restored)) + { + _colorMode = restored; + } + else + { + _colorMode = ColorMode.Auto; + } + } + + private Task PersistMode() + { + _persistentComponentState.PersistAsJson("ColorMode", GetColorMode()); + return Task.CompletedTask; + } + + void IDisposable.Dispose() + { + _persistingComponentStateSubscription.Dispose(); + } +} + +/// +/// A handler for the event. +/// +/// A that supplies the updated . +public delegate void ColorModeChangedHandler(ColorMode colorMode); + diff --git a/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeSwitcher.razor.cs b/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeSwitcher.razor.cs index 6f9ea4fd..df6442bc 100644 --- a/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeSwitcher.razor.cs +++ b/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeSwitcher.razor.cs @@ -12,38 +12,20 @@ namespace Havit.Blazor.Documentation.Shared.Components.DocColorMode; /// The client-side component uses JS to switch the color mode and save the new value to the cookie. /// The auto mode is resolved by color-mode-auto.js startup script (see _Layout.cshtml). /// -public partial class DocColorModeSwitcher : IDisposable +public partial class DocColorModeSwitcher( + IDocColorModeProvider docColorModeProvider, + IJSRuntime jsRuntime) { - [Inject] protected IDocColorModeResolver DocColorModeResolver { get; set; } - [Inject] protected PersistentComponentState PersistentComponentState { get; set; } - [Inject] protected IJSRuntime JSRuntime { get; set; } + [CascadingParameter] protected ColorMode ColorMode { get; set; } - private PersistingComponentStateSubscription _persistingSubscription; - private IJSObjectReference _jsModule; - private ColorMode _mode = ColorMode.Auto; - - protected override void OnInitialized() - { - _persistingSubscription = PersistentComponentState.RegisterOnPersisting(PersistMode); + private readonly IDocColorModeProvider _docColorModeProvider = docColorModeProvider; + private readonly IJSRuntime _jsRuntime = jsRuntime; - if (PersistentComponentState.TryTakeFromJson("ColorMode", out var restored)) - { - _mode = restored; - } - else - { - _mode = DocColorModeResolver.GetColorMode(); - } - } - private Task PersistMode() - { - PersistentComponentState.PersistAsJson("ColorMode", _mode); - return Task.CompletedTask; - } + private IJSObjectReference _jsModule; private async Task HandleClick() { - _mode = _mode switch + ColorMode = ColorMode switch { ColorMode.Auto => ColorMode.Dark, ColorMode.Dark => ColorMode.Light, @@ -52,28 +34,30 @@ private async Task HandleClick() }; await EnsureJsModule(); - await _jsModule.InvokeVoidAsync("setColorMode", _mode.ToString("g").ToLowerInvariant()); + await _jsModule.InvokeVoidAsync("setColorMode", ColorMode.ToString("g").ToLowerInvariant()); + + _docColorModeProvider.SetColorMode(ColorMode); } private async Task EnsureJsModule() { - _jsModule ??= await JSRuntime.InvokeAsync("import", "./Shared/Components/DocColorMode/DocColorModeSwitcher.razor.js"); + _jsModule ??= await _jsRuntime.InvokeAsync("import", "./Shared/Components/DocColorMode/DocColorModeSwitcher.razor.js"); } private IconBase GetIcon() { - return _mode switch + return ColorMode switch { ColorMode.Auto => BootstrapIcon.CircleHalf, ColorMode.Light => BootstrapIcon.Sun, ColorMode.Dark => BootstrapIcon.Moon, - _ => throw new InvalidOperationException($"Unknown color mode '{_mode}'.") + _ => throw new InvalidOperationException($"Unknown color mode '{ColorMode}'.") }; } private string GetTooltip() { - return _mode switch + return ColorMode switch { ColorMode.Auto => "Auto color mode (theme). Click to switch to Dark.", ColorMode.Dark => "Dark color mode (theme). Click to switch to Light.", @@ -81,9 +65,4 @@ private string GetTooltip() _ => "Click to switch color mode (theme) to Auto." // fallback }; } - - void IDisposable.Dispose() - { - _persistingSubscription.Dispose(); - } } diff --git a/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeSwitcher.razor.js b/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeSwitcher.razor.js index 107d9a2a..87856c3a 100644 --- a/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeSwitcher.razor.js +++ b/Havit.Blazor.Documentation/Shared/Components/DocColorMode/DocColorModeSwitcher.razor.js @@ -5,7 +5,7 @@ export function setColorMode(colorMode) { document.documentElement.setAttribute('data-bs-theme', colorMode) } - var date = new Date(); + const date = new Date(); date.setTime(date.getTime() + (60 * 24 * 60 * 60 * 1000)); // 60 days document.cookie = "ColorMode=" + colorMode + "; expires=" + date.toGMTString() + "; path=/"; } \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Shared/Components/DocColorMode/IDocColorModeProvider.cs b/Havit.Blazor.Documentation/Shared/Components/DocColorMode/IDocColorModeProvider.cs new file mode 100644 index 00000000..546a97ee --- /dev/null +++ b/Havit.Blazor.Documentation/Shared/Components/DocColorMode/IDocColorModeProvider.cs @@ -0,0 +1,9 @@ +namespace Havit.Blazor.Documentation.Shared.Components.DocColorMode; + +public interface IDocColorModeProvider +{ + event ColorModeChangedHandler ColorModeChanged; + + ColorMode GetColorMode(); + void SetColorMode(ColorMode colorMode); +} \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Shared/Components/DocColorMode/IDocColorModeResolver.cs b/Havit.Blazor.Documentation/Shared/Components/DocColorMode/IDocColorModeResolver.cs deleted file mode 100644 index ce2ecacf..00000000 --- a/Havit.Blazor.Documentation/Shared/Components/DocColorMode/IDocColorModeResolver.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Havit.Blazor.Documentation.Shared.Components.DocColorMode; - -public interface IDocColorModeResolver -{ - ColorMode GetColorMode(); -} diff --git a/Havit.Blazor.Documentation/Shared/Components/DocHeadContent.razor b/Havit.Blazor.Documentation/Shared/Components/DocHeadContent.razor new file mode 100644 index 00000000..10cd3915 --- /dev/null +++ b/Havit.Blazor.Documentation/Shared/Components/DocHeadContent.razor @@ -0,0 +1,7 @@ +@if (_shouldRender) +{ + + + @ChildContent + +} diff --git a/Havit.Blazor.Documentation/Shared/Components/DocHeadContent.razor.cs b/Havit.Blazor.Documentation/Shared/Components/DocHeadContent.razor.cs new file mode 100644 index 00000000..1e0b9e91 --- /dev/null +++ b/Havit.Blazor.Documentation/Shared/Components/DocHeadContent.razor.cs @@ -0,0 +1,31 @@ +namespace Havit.Blazor.Documentation.Shared.Components; + +/// +/// A component that renders with metadata (such as canonical URL) for the current page. +/// Can be used several times on a page, but only the first occurence is used. +/// +public partial class DocHeadContent +{ + private const string BaseUrl = "https://havit.blazor.eu"; + + [Parameter] public string CanonicalRelativeUrl { get; set; } + + [Parameter] public RenderFragment ChildContent { get; set; } + + [CascadingParameter] protected DocHeadContentTracker DocHeadContentTracker { get; set; } + + private bool _shouldRender = false; + private string _canonicalAbsoluteUrl; + + protected override void OnParametersSet() + { + if (CanonicalRelativeUrl != null) + { + _shouldRender = DocHeadContentTracker.TryRegisterCanonicalUrlForCurrentPage(CanonicalRelativeUrl); + if (_shouldRender) + { + _canonicalAbsoluteUrl = DocHeadContentTracker.GetAbsoluteCanonicalUrl(); + } + } + } +} diff --git a/Havit.Blazor.Documentation/Shared/Components/DocHeadContentTracker.cs b/Havit.Blazor.Documentation/Shared/Components/DocHeadContentTracker.cs new file mode 100644 index 00000000..82264457 --- /dev/null +++ b/Havit.Blazor.Documentation/Shared/Components/DocHeadContentTracker.cs @@ -0,0 +1,65 @@ +namespace Havit.Blazor.Documentation.Shared.Components; + +/// +/// Tracks usages of component during page rendering +/// and returns the canonical URL of the first registration. +/// +public class DocHeadContentTracker(NavigationManager navigationManager) +{ + private readonly NavigationManager _navigationManager = navigationManager; + + private string _activePageUri; + private string _canonicalUrl; + + /// + /// Registers a canonical URL for the current page and returns true + /// if the registration was successful (first registration for the current page rendered). + /// + public bool TryRegisterCanonicalUrlForCurrentPage(string canonicalUrl) + { + Contract.Requires(canonicalUrl != null); + + ResetIfCurrentPageUrlChanged(); + + if (_canonicalUrl is null) + { + _canonicalUrl = canonicalUrl; + return true; + } + + return false; + } + + private void ResetIfCurrentPageUrlChanged() + { + string currentUri = _navigationManager.Uri; + if (_activePageUri != currentUri) + { + _canonicalUrl = null; + _activePageUri = currentUri; + } + } + + private string GetRegisteredCanonicalUrl() + { + if (_navigationManager.Uri == _activePageUri) + { + // if there was an explicit registration for this page, return the canonical URL + return _canonicalUrl; + } + return null; + } + + public string GetAbsoluteCanonicalUrl() + { + string result = null; + var relativeUrl = GetRegisteredCanonicalUrl(); + if (relativeUrl != null) + { + result = "https://havit.blazor.eu/" + relativeUrl.TrimStart('/'); + result = result.TrimEnd('/'); + } + + return result; + } +} diff --git a/Havit.Blazor.Documentation/Shared/Components/DocHeading.razor b/Havit.Blazor.Documentation/Shared/Components/DocHeading.razor index 027822f5..d6e693b5 100644 --- a/Havit.Blazor.Documentation/Shared/Components/DocHeading.razor +++ b/Havit.Blazor.Documentation/Shared/Components/DocHeading.razor @@ -1,3 +1,3 @@ @namespace Havit.Blazor.Documentation.Shared.Components -@Title@ChildContent # +@Title # diff --git a/Havit.Blazor.Documentation/Shared/Components/DocHeading.razor.cs b/Havit.Blazor.Documentation/Shared/Components/DocHeading.razor.cs index 1e3b3db2..2f8d3d98 100644 --- a/Havit.Blazor.Documentation/Shared/Components/DocHeading.razor.cs +++ b/Havit.Blazor.Documentation/Shared/Components/DocHeading.razor.cs @@ -1,24 +1,13 @@ -using System.Text.RegularExpressions; -using Havit.Blazor.Documentation.Services; +using Havit.Blazor.Documentation.Services; namespace Havit.Blazor.Documentation.Shared.Components; -public partial class DocHeading : IDocPageNavigationItem +public partial class DocHeading( + IDocPageNavigationItemsTracker docPageNavigationItemsTracker, + NavigationManager navigationManager) { - /// - /// Which heading tags are to be used for which levels. - /// - protected static readonly Dictionary LevelHeadingTags = new() - { - { 1, "h1" }, - { 2, "h2" }, - { 3, "h3" }, - { 4, "h4" }, - { 5, "h5" }, - { 6, "h6" } - }; - - [Inject] public NavigationManager NavigationManager { get; set; } + private readonly IDocPageNavigationItemsTracker _docPageNavigationItemsTracker = docPageNavigationItemsTracker; + private readonly NavigationManager _navigationManager = navigationManager; /// /// Id of the section. @@ -28,56 +17,30 @@ public partial class DocHeading : IDocPageNavigationItem /// /// Title of the section. If not set, Title is extracted from the Href. /// - [Parameter] public string Title { get; set; } + [Parameter, EditorRequired] public string Title { get; set; } /// /// Determines the heading tag to be used. Level should be used for sections and higher integers for subsections. /// [Parameter] public int Level { get; set; } = 2; - [Parameter] public RenderFragment ChildContent { get; set; } - - [Parameter(CaptureUnmatchedValues = true)] public IDictionary AdditionalAttributes { get; set; } - - /// - /// Tag for the section title ( will be used if you won't set the parameter). - /// - [Parameter] public string HeadingTag { get; set; } - - [Inject] public IDocPageNavigationItemsHolder DocPageNavigationItemsHolder { get; set; } - - protected string IdEffective => Id ?? GetIdFromTitle(); - string IDocPageNavigationItem.Id => IdEffective; - - protected string HeadingTagEffective => HeadingTag ?? (LevelHeadingTags.ContainsKey(Level) ? LevelHeadingTags[Level] : LevelHeadingTags.Values.LastOrDefault()); + private string _idEffective; + private string _headingTag; + private string _hrefEffective; protected override void OnParametersSet() { - AdditionalAttributes ??= new Dictionary(); - AdditionalAttributes["id"] = IdEffective; + _idEffective = Id ?? Title.NormalizeForUrl(); + _headingTag = "h" + Math.Min(Level, 6); - DocPageNavigationItemsHolder?.RegisterNew(this, NavigationManager.Uri); - } + string currentUri = _navigationManager.Uri; + _hrefEffective = UrlHelper.RemoveFragmentFromUrl(currentUri) + "#" + _idEffective; - private string GetIdFromTitle() - { - if (String.IsNullOrWhiteSpace(Title)) + _docPageNavigationItemsTracker.RegisterNavigationItem(currentUri, new DocPageNavigationItem() { - return null; - } - return Regex.Replace(Title.ToLower(), @"[^A-Za-z]+", "-").Trim('-'); - } - - public string GetItemUrl(string currentUrl) - { - string uri = currentUrl.Split('?')[0]; - uri = uri.Split('#')[0]; - - return $"{uri}#{IdEffective}"; - } - - private string GetItemUrl() - { - return GetItemUrl(NavigationManager.Uri); + Id = _idEffective, + Level = Level, + Title = Title + }); } } diff --git a/Havit.Blazor.Documentation/Shared/Components/DocPageNavigationItem.cs b/Havit.Blazor.Documentation/Shared/Components/DocPageNavigationItem.cs index 3c03bdd6..12993548 100644 --- a/Havit.Blazor.Documentation/Shared/Components/DocPageNavigationItem.cs +++ b/Havit.Blazor.Documentation/Shared/Components/DocPageNavigationItem.cs @@ -1,14 +1,12 @@ namespace Havit.Blazor.Documentation.Shared.Components; -public class DocPageNavigationItem : IDocPageNavigationItem +public class DocPageNavigationItem { public string Id { get; set; } public int Level { get; set; } public string Title { get; init; } - public RenderFragment ChildContent { get; set; } - public string GetItemUrl(string currentUrl) { return $"{currentUrl}#{Id}"; diff --git a/Havit.Blazor.Documentation/Shared/Components/IDocPageNavigationItem.cs b/Havit.Blazor.Documentation/Shared/Components/IDocPageNavigationItem.cs deleted file mode 100644 index 06973d93..00000000 --- a/Havit.Blazor.Documentation/Shared/Components/IDocPageNavigationItem.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Havit.Blazor.Documentation.Shared.Components; - -public interface IDocPageNavigationItem -{ - string Id { get; } - int Level { get; } - string Title { get; } - RenderFragment ChildContent { get; } - - string GetItemUrl(string currentUrl); -} diff --git a/Havit.Blazor.Documentation/Shared/Components/OnThisPageNavigation.razor b/Havit.Blazor.Documentation/Shared/Components/OnThisPageNavigation.razor index ba1f1f2b..2cf03827 100644 --- a/Havit.Blazor.Documentation/Shared/Components/OnThisPageNavigation.razor +++ b/Havit.Blazor.Documentation/Shared/Components/OnThisPageNavigation.razor @@ -1,5 +1,5 @@ 
- @if ((Items is not null) && Items.Any()) + @if ((_items is not null) && _items.Any()) { @ChildContent
    diff --git a/Havit.Blazor.Documentation/Shared/Components/OnThisPageNavigation.razor.cs b/Havit.Blazor.Documentation/Shared/Components/OnThisPageNavigation.razor.cs index 5b764425..66fc8238 100644 --- a/Havit.Blazor.Documentation/Shared/Components/OnThisPageNavigation.razor.cs +++ b/Havit.Blazor.Documentation/Shared/Components/OnThisPageNavigation.razor.cs @@ -3,67 +3,71 @@ namespace Havit.Blazor.Documentation.Shared.Components; -public partial class OnThisPageNavigation : IDisposable +public partial class OnThisPageNavigation( + IDocPageNavigationItemsTracker docPageNavigationItemsHolder, + NavigationManager navigationManager) : IDisposable { - [Inject] public IDocPageNavigationItemsHolder DocPageNavigationItemsHolder { get; set; } - [Inject] public NavigationManager NavigationManager { get; set; } - [Parameter] public string CssClass { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } - private IEnumerable Items { get; set; } + private readonly IDocPageNavigationItemsTracker _docPageNavigationItemsHolder = docPageNavigationItemsHolder; + private readonly NavigationManager _navigationManager = navigationManager; + + private List _items; protected override void OnInitialized() { - NavigationManager.LocationChanged += LoadItems; + _navigationManager.LocationChanged += LoadItems; } protected override void OnAfterRender(bool firstRender) { if (firstRender) { - Items = DocPageNavigationItemsHolder.RetrieveAll(NavigationManager.Uri); + _items = _docPageNavigationItemsHolder.GetPageNavigationItems(_navigationManager.Uri); StateHasChanged(); } } private void LoadItems(object sender, LocationChangedEventArgs eventArgs) { - Items = DocPageNavigationItemsHolder.RetrieveAll(eventArgs.Location); + _items = _docPageNavigationItemsHolder.GetPageNavigationItems(eventArgs.Location); StateHasChanged(); } private RenderFragment GenerateNavigationTree() => builder => { - if (!Items.Any()) + if (!_items.Any()) { return; } - var items = Items.ToList(); - var topLevel = items.Min(i => i.Level); + List itemsToRender; + int topLevel; // if there is only single top-level item, we don't need to render the top-level list - if (items.Count(i => i.Level == topLevel) == 1) + if (_items.Count(i => i.Level == 1) == 1) { - items = items.Where(i => i.Level != topLevel).ToList(); - topLevel = items.Min(i => i.Level); + itemsToRender = _items.Where(i => i.Level != 1).ToList(); + topLevel = 2; + } + else + { + itemsToRender = _items; + topLevel = 1; } var currentLevel = topLevel; - int sequence = 1; - for (int i = 0; i < items.Count; i++) + foreach (var item in itemsToRender) { - IDocPageNavigationItem item = items[i]; - // Handle level adjustments - nested lists. int levelDifference = Math.Abs(item.Level - currentLevel); if (item.Level > currentLevel) { for (int j = 0; j < levelDifference; j++) { - builder.OpenElement(sequence++, "ul"); + builder.OpenElement(71, "ul"); } } else if (item.Level < currentLevel) @@ -76,13 +80,12 @@ private RenderFragment GenerateNavigationTree() => builder => currentLevel = item.Level; // Render the list item and a link to the heading. - builder.OpenElement(sequence++, "li"); + builder.OpenElement(84, "li"); - builder.OpenElement(sequence++, "a"); - builder.AddAttribute(sequence++, "href", item.GetItemUrl(NavigationManager.Uri)); // TODO direct usage of HxAnchorFragmentNavigation.ScrollToAnchorAsync() ? - builder.AddAttribute(sequence++, "class", "text-secondary mb-1 text-truncate"); - builder.AddContent(sequence++, item.Title); - builder.AddContent(sequence++, item.ChildContent); + builder.OpenElement(86, "a"); + builder.AddAttribute(87, "href", item.GetItemUrl(_navigationManager.Uri)); // TODO direct usage of HxAnchorFragmentNavigation.ScrollToAnchorAsync() ? + builder.AddAttribute(88, "class", "text-secondary mb-1 text-truncate"); + builder.AddContent(89, item.Title); builder.CloseElement(); builder.CloseElement(); @@ -96,6 +99,6 @@ private RenderFragment GenerateNavigationTree() => builder => public void Dispose() { - NavigationManager.LocationChanged -= LoadItems; + _navigationManager.LocationChanged -= LoadItems; } } diff --git a/Havit.Blazor.Documentation/Shared/Components/OnThisPageNavigation.razor.css b/Havit.Blazor.Documentation/Shared/Components/OnThisPageNavigation.razor.css index 5286b279..cb279be4 100644 --- a/Havit.Blazor.Documentation/Shared/Components/OnThisPageNavigation.razor.css +++ b/Havit.Blazor.Documentation/Shared/Components/OnThisPageNavigation.razor.css @@ -1,5 +1,6 @@ .on-this-page-navigation { height: fit-content; + top: 3rem; } ul { @@ -16,9 +17,9 @@ ul, list-style-type: none; } - ul li, ::deep ul li { - margin-bottom: .25rem; - } +ul li, ::deep ul li { + margin-bottom: .25rem; +} ::deep a { text-decoration: none; diff --git a/Havit.Blazor.Documentation/Shared/Components/Search/Search.razor b/Havit.Blazor.Documentation/Shared/Components/Search/Search.razor index 3f2498ed..d3218dca 100644 --- a/Havit.Blazor.Documentation/Shared/Components/Search/Search.razor +++ b/Havit.Blazor.Documentation/Shared/Components/Search/Search.razor @@ -6,8 +6,8 @@ TValue="SearchItem" @bind-Value="@SelectedResult" MinimumLength="1" - Delay="1" - CssClass="sidebar-search mb-3 pt-1" + Delay="1" + CssClass="sidebar-search" DataProvider="ProvideSuggestions" @ref="_autosuggest"> @searchItem.Title diff --git a/Havit.Blazor.Documentation/Shared/Components/Search/Search.razor.cs b/Havit.Blazor.Documentation/Shared/Components/Search/Search.razor.cs index 1801f0d9..11d80256 100644 --- a/Havit.Blazor.Documentation/Shared/Components/Search/Search.razor.cs +++ b/Havit.Blazor.Documentation/Shared/Components/Search/Search.razor.cs @@ -25,7 +25,8 @@ private SearchItem SelectedResult private readonly List _searchItems = new() { - new("/migrating-to-v3", "Migrating to v3", "upgrade release notes update 5.2 5.1"), + new("/premium", "Premium", "support subscription sponsorship price pricing license licensing SLA priority enterprise showcase Goran blocks elements"), + // Components and other pages @@ -54,8 +55,8 @@ private SearchItem SelectedResult new("/components/HxCollapseToggleElement", "HxCollapseToggleElement", ""), new("/components/HxContextMenu", "HxContextMenu", "dropdown popup"), new("/components/HxDialogBase", "HxDialogBase", "custom dialog modal messagebox"), - new("/components/HxDropdown", "HxDropdown", "collapse tooltip popover popup popper"), - new("/components/HxDropdownButtonGroup", "HxDropdownButtonGroup", "collapse tooltip popover popup popper"), + new("/components/HxDropdown", "HxDropdown", "collapse tooltip popover popup popper HxDropdownToggleElement HxDropdownMenu HxDropdownContent HxDropdownHeader HxDropdownItemNavLink HxDropdownItem HxDropdownItemText HxDropdownDivider"), + new("/components/HxDropdownButtonGroup", "HxDropdownButtonGroup", "collapse tooltip popover popup popper HxDropdownToggleButton"), new("/components/HxDynamicElement", "HxDynamicElement", "dynamiccomponent html"), new("/components/HxFilterForm", "HxFilterForm", "HxListLayout"), new("/components/HxFormState", "HxFormState", "enabled disabled"), @@ -228,14 +229,10 @@ private SearchItem SelectedResult private HxAutosuggest _autosuggest; - private bool _wasFocused = false; - protected override async Task OnAfterRenderAsync(bool firstRender) { - if (firstRender && !_wasFocused) + if (firstRender && (_autosuggest is not null)) { - _wasFocused = true; - await Task.Delay(1); await _autosuggest.FocusAsync(); } } @@ -257,7 +254,7 @@ private IEnumerable GetSearchItems() .OrderBy(si => si.Level) .ThenByDescending(si => si.GetRelevance(_userInput)) .ThenBy(si => si.Title) - .Take(5); + .Take(8); } public void NavigateToSelectedPage(SearchItem searchItem) diff --git a/Havit.Blazor.Documentation/Shared/EmptyLayout.razor b/Havit.Blazor.Documentation/Shared/EmptyLayout.razor new file mode 100644 index 00000000..6c662d60 --- /dev/null +++ b/Havit.Blazor.Documentation/Shared/EmptyLayout.razor @@ -0,0 +1,5 @@ +@inherits LayoutComponentBase + + + @Body + diff --git a/Havit.Blazor.Documentation/Shared/EmptyLayout.razor.cs b/Havit.Blazor.Documentation/Shared/EmptyLayout.razor.cs new file mode 100644 index 00000000..5334fb43 --- /dev/null +++ b/Havit.Blazor.Documentation/Shared/EmptyLayout.razor.cs @@ -0,0 +1,15 @@ +using Havit.Blazor.Documentation.Shared.Components; + +namespace Havit.Blazor.Documentation.Shared; + +public partial class EmptyLayout +{ + [Inject] protected NavigationManager NavigationManager { get; set; } + + private DocHeadContentTracker _docHeadContentTracker; + + protected override void OnInitialized() + { + _docHeadContentTracker = new DocHeadContentTracker(NavigationManager); + } +} diff --git a/Havit.Blazor.Documentation/Shared/HomeLayout.razor b/Havit.Blazor.Documentation/Shared/HomeLayout.razor new file mode 100644 index 00000000..350df6cc --- /dev/null +++ b/Havit.Blazor.Documentation/Shared/HomeLayout.razor @@ -0,0 +1,8 @@ +@inherits LayoutComponentBase + + +
    + + @Body + +
    \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Shared/HomeLayout.razor.cs b/Havit.Blazor.Documentation/Shared/HomeLayout.razor.cs new file mode 100644 index 00000000..e988a5d1 --- /dev/null +++ b/Havit.Blazor.Documentation/Shared/HomeLayout.razor.cs @@ -0,0 +1,15 @@ +using Havit.Blazor.Documentation.Shared.Components; + +namespace Havit.Blazor.Documentation.Shared; + +public partial class HomeLayout +{ + [Inject] protected NavigationManager NavigationManager { get; set; } + + private DocHeadContentTracker _docHeadContentTracker; + + protected override void OnInitialized() + { + _docHeadContentTracker = new DocHeadContentTracker(NavigationManager); + } +} diff --git a/Havit.Blazor.Documentation/Shared/MainLayout.razor b/Havit.Blazor.Documentation/Shared/MainLayout.razor index 142e421c..f183dc14 100644 --- a/Havit.Blazor.Documentation/Shared/MainLayout.razor +++ b/Havit.Blazor.Documentation/Shared/MainLayout.razor @@ -5,17 +5,17 @@ -
    + +
    -
    - @Body +
    + + @Body +
    On this page
    - -@* Workaround for https://github.com/dotnet/aspnetcore/issues/54533 *@ - \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Shared/MainLayout.razor.cs b/Havit.Blazor.Documentation/Shared/MainLayout.razor.cs index af4b50c6..c4e49475 100644 --- a/Havit.Blazor.Documentation/Shared/MainLayout.razor.cs +++ b/Havit.Blazor.Documentation/Shared/MainLayout.razor.cs @@ -11,6 +11,13 @@ public partial class MainLayout private string _title; + private DocHeadContentTracker _docHeadContentTracker; + + protected override void OnInitialized() + { + _docHeadContentTracker = new DocHeadContentTracker(NavigationManager); + } + protected override void OnParametersSet() { var path = new Uri(NavigationManager.Uri).AbsolutePath.TrimEnd('/'); diff --git a/Havit.Blazor.Documentation/Shared/MainLayout.razor.css b/Havit.Blazor.Documentation/Shared/MainLayout.razor.css index 23214a87..a897410f 100644 --- a/Havit.Blazor.Documentation/Shared/MainLayout.razor.css +++ b/Havit.Blazor.Documentation/Shared/MainLayout.razor.css @@ -5,7 +5,18 @@ flex-basis: 250px; } -.container-floating { +.container-fluid { max-width: 1600px; margin: 0 auto; -} \ No newline at end of file +} + +/* Offset headings by Navbar height on desktop */ +@media screen and (min-width: 992px) { + .doc-content ::deep h1, + .doc-content ::deep h2, + .doc-content ::deep h3, + .doc-content ::deep h4, + .doc-content ::deep h5 { + padding-top: 56px; margin-top: -56px; + } +} diff --git a/Havit.Blazor.Documentation/Shared/Navbar.razor b/Havit.Blazor.Documentation/Shared/Navbar.razor new file mode 100644 index 00000000..a0e82eae --- /dev/null +++ b/Havit.Blazor.Documentation/Shared/Navbar.razor @@ -0,0 +1,43 @@ +
    + + + HAVIT Blazor + HAVIT Blazor + + + + + + Introduction + + + Documentation + + + Blocks + + @* + + Showcase + + *@ + + Premium + + +
    + + + + + + +
    +
    +
    +
    + +@code +{ + [Parameter] public string CssClass { get; set; } +} \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Shared/Navbar.razor.css b/Havit.Blazor.Documentation/Shared/Navbar.razor.css new file mode 100644 index 00000000..22d20ad9 --- /dev/null +++ b/Havit.Blazor.Documentation/Shared/Navbar.razor.css @@ -0,0 +1,17 @@ +::deep .nav-link.active { + font-weight: 600; + --bs-link-opacity: 1; +} + +::deep .container-fluid { + max-width: 1600px; + margin: 0 auto; +} + +.nav-container { + --bs-bg-opacity: .8; + background-color: rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity)) !important; + backdrop-filter: saturate(120%) blur(20px); + -webkit-backdrop-filter: saturate(120%) blur(20px); + z-index: 1030; +} \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Shared/Sidebar.razor b/Havit.Blazor.Documentation/Shared/Sidebar.razor index 5ed65113..cd98df8a 100644 --- a/Havit.Blazor.Documentation/Shared/Sidebar.razor +++ b/Havit.Blazor.Documentation/Shared/Sidebar.razor @@ -1,18 +1,25 @@ - + - - - - - - - +
    + + + + + +
    + + +
    +
    - - - - + + + +
    Blocks 🔥
    + Premium +
    +
    @@ -105,7 +112,7 @@ - + @@ -199,18 +206,9 @@ )}")" Text="@(nameof(HxValidationMessage))" /> - - - - - @if (_selectedTheme?.Name == "Bootstrap 5") - { - @((MarkupString)HxSetup.RenderBootstrapCssReference(BootstrapFlavor.PlainBootstrap)) - } - else if (_selectedTheme is not null) // null = HAVIT Bootstrap build (default) - { - - } - +@code +{ + private bool _isDesktopCollapsed; +} \ No newline at end of file diff --git a/Havit.Blazor.Documentation/Shared/Sidebar.razor.cs b/Havit.Blazor.Documentation/Shared/Sidebar.razor.cs deleted file mode 100644 index 2e2d4b86..00000000 --- a/Havit.Blazor.Documentation/Shared/Sidebar.razor.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Text.Json; - -namespace Havit.Blazor.Documentation.Shared; - -public partial class Sidebar -{ - private static readonly HttpClient s_client = new HttpClient(); - - private List _themes = new(); - private Theme _selectedTheme; - - protected override async Task OnInitializedAsync() - { - try - { - var result = await s_client.GetStreamAsync("https://bootswatch.com/api/5.json"); - var themesHolder = await JsonSerializer.DeserializeAsync(result, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); - _themes = themesHolder.Themes; - _themes.ForEach(t => t.Name += " (bootswatch.com)"); - } - catch - { - Console.WriteLine("Unable to fetch themes from Bootswatch API."); - _themes = new(); - } - - _themes = _themes.Prepend(new() { Name = "Bootstrap 5", CssCdn = "FULL_LINK_HARDCODED_IN_RAZOR" }).ToList(); - } -} - -public class ThemeHolder -{ - public List Themes { get; set; } -} - -public class Theme -{ - public string Name { get; set; } - public string CssCdn { get; set; } -} diff --git a/Havit.Blazor.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml b/Havit.Blazor.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml index ac38f5b0..f720632d 100644 --- a/Havit.Blazor.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml +++ b/Havit.Blazor.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml @@ -477,11 +477,6 @@ Additional attributes to be splatted onto an underlying <button> element. - - - Localization service. - - Gets the basic CSS class(es) which get rendered to every single button.
    @@ -2583,6 +2578,11 @@ Prevents the default action for the onclick event. Default is null, which means true when is set.
    + + + Inner content of the . + + Offset between dropdown input and dropdown menu @@ -2721,7 +2721,7 @@ - Enable or disable the sanitization. If activated HTML content will be sanitized. See the sanitizer section in Bootstrap JavaScript documentation. + Enable or disable the sanitization. If activated, HTML content will be sanitized. See the sanitizer section in Bootstrap JavaScript documentation. Default is true. @@ -4161,6 +4161,18 @@ Size of the input. + + + Placeholder for the start-date input. + If not set, localized default is used ("From" + localizations). + + + + + Placeholder for the end-date input. + If not set, localized default is used ("End" + localizations). + + Gets or sets the error message used when displaying a "from" parsing error. @@ -4277,6 +4289,12 @@ Feel free to set the InputMode on your own as needed. + + + Allows switching between textual and numeric input types. + Only (default) and are supported. + + Placeholder for the input. @@ -5173,6 +5191,16 @@ Input size. + + + Placeholder for the start-date input. + + + + + Placeholder for the end-date input. + + Optional icon to display within the input. @@ -5288,6 +5316,12 @@ A hint to browsers regarding the type of virtual keyboard configuration to use when editing. + + + Allows switching between textual and numeric input types. + Only (default) and are supported. + + Determines whether all the content within the input field is automatically selected when it receives focus. @@ -6089,7 +6123,8 @@ - Height of the item row used for infinite scroll calculations (). + Height of the item row (in pixels) used for infinite scroll calculations (). + The row height is not applied for other navigation modes, use CSS for that. @@ -6177,6 +6212,17 @@ Settings for the "Load more" navigation button ( or ). + + + Gets or sets a value indicating whether the current selection (either for single selection + or for multiple selection) should be preserved during data operations, such as paging, sorting, filtering, + or manual invocation of .
    +
    + + This setting ensures that the selection remains intact during operations that refresh or modify the displayed data in the grid. + Note that preserving the selection requires that the underlying data items can still be matched in the updated dataset (e.g., by item1.Equals(item2)). + +
    User state of the . @@ -6385,6 +6431,18 @@ Triggers the event. This method can be overridden in derived components to implement custom logic before or after the event is triggered. + + + Gets or sets a value indicating whether the current selection (either for single selection + or for multiple selection) should be preserved during data operations, such as paging, sorting, filtering, + or manual invocation of .
    + Default value is false (can be set by using HxGrid.Defaults). +
    + + This setting ensures that the selection remains intact during operations that refresh or modify the displayed data in the grid. + Note that preserving the selection requires that the underlying data items can still be matched in the updated dataset (e.g., by item1.Equals(item2)). + +
    The strategy for how data items are displayed and loaded into the grid. Supported modes include pagination, load more, and infinite scroll. @@ -6468,8 +6526,9 @@ - Height of each item row, used primarily in calculations for infinite scrolling. + Height of each item row, used in calculations for infinite scrolling (). The default value (41px) corresponds to the typical row height in the Bootstrap 5 default theme. + The row height is not applied for other navigation modes, use CSS for that. @@ -6539,6 +6598,48 @@ Icon to indicate the descending sort direction in the column header. This icon is shown when a column is sorted in descending order. + + + Defines a function that returns additional attributes for a specific tr element based on the item it represents. + This allows for custom behavior or event handling on a per-row basis. + + + If both and are specified, + both dictionaries are combined into one. + Note that there is no prevention of duplicate keys, which may result in a . + + + + + Provides a dictionary of additional attributes to apply to all body tr elements in the grid. + These attributes can be used to customize the appearance or behavior of rows. + + + If both and are specified, + both dictionaries are combined into one. + Note that there is no prevention of duplicate keys, which may result in a . + + + + + Provides a dictionary of additional attributes to apply to the header tr element of the grid. + This allows for custom styling or behavior of the header row. + + + + + Provides a dictionary of additional attributes to apply to the footer tr element of the grid. + This allows for custom styling or behavior of the footer row. + + + + + Determines the effective additional attributes for a given data row, combining both the global and per-item attributes. + + The data item for the current row. + A dictionary of additional attributes to apply to the row. + Thrown when there are duplicate keys in the combined dictionaries. + Retrieves the default settings for the grid. This method can be overridden in derived classes @@ -7079,6 +7180,11 @@ Global settings for the Havit Blazor Components library. + + + Bootstrap version used by the library. + + Renders the <script> tag that references the corresponding Bootstrap JavaScript bundle with Popper.
    @@ -7599,7 +7705,7 @@ Component for displaying message boxes.
    - Usually used via and .
    + Usually used via and .
    Full documentation and demos: https://havit.blazor.eu/components/HxMessageBox
    @@ -7614,6 +7720,19 @@ Enables overriding defaults in descendants (use a separate set of defaults).
    + + + Set of settings to be applied to the component instance (overrides , overridden by individual parameters). + + + + + Returns an optional set of component settings. + + + Similar to , enables defining wider in component descendants (by returning a derived settings class). + + Title text (Header). @@ -7642,7 +7761,7 @@ - Buttons to show. The default is . + Buttons to show. The default is . @@ -7652,7 +7771,7 @@ - Text for . + Text for . @@ -7675,7 +7794,7 @@ Raised when the message box gets closed. Returns the button clicked. - + Triggers the event. Allows interception of the event in derived components. @@ -7693,7 +7812,7 @@ - Displays message boxes initiated through . + Displays message boxes initiated through . To be placed in the root application component / main layout. @@ -7897,6 +8016,71 @@ Formats a value for use in the data-bs-backdrop attribute.
    + + + Represents a request to display a message box with various customizable options. + + + + + Title in the header. + + + + + Template for the header. + + + + + Content (body) text. + + + + + Body (content) template. + + + + + Indicates whether to show the close button. + + + + + Buttons to show. + + + + + Primary button (if you want to override the default). + + + + + Text for the button. + + + + + Settings for the message box. + + + + + Additional attributes to be splatted onto an underlying UI component (Bootstrap: HxMessageBox -> HxModal). + + + + + Extension methods for installation of support. + + + + + Adds support to be able to display message boxes using HxMessageBoxHost. + + Settings for the and derived components. @@ -7917,6 +8101,41 @@ Settings for the underlying component. + + + Text for the OK button. + + + + + Text for the Cancel button. + + + + + Text for the Abort button. + + + + + Text for the Yes button. + + + + + Text for the No button. + + + + + Text for the Retry button. + + + + + Text for the Ignore button. + + Options for controlling the behavior of the . @@ -8551,8 +8770,12 @@ - Allows you to disable the item with false. - The default value is true. + Any additional CSS class to add. + + + + + Any additional CSS class to add to inner text. diff --git a/Havit.Blazor.Documentation/XmlDoc/Havit.Blazor.Components.Web.xml b/Havit.Blazor.Documentation/XmlDoc/Havit.Blazor.Components.Web.xml index c315bda1..9882c7a1 100644 --- a/Havit.Blazor.Documentation/XmlDoc/Havit.Blazor.Components.Web.xml +++ b/Havit.Blazor.Documentation/XmlDoc/Havit.Blazor.Components.Web.xml @@ -66,61 +66,6 @@ The delay in milliseconds for debouncing. The asynchronous action to be executed. The gets canceled if the method is called again. - - - Title in the header. - - - - - Template for the header. - - - - - Content (body) text. - - - - - Body (content) template. - - - - - Indicates whether to show the close button. - - - - - Buttons to show. - - - - - Primary button (if you want to override the default). - - - - - Text for the button. - - - - - Additional attributes to be splatted onto an underlying UI component (Bootstrap: HxMessageBox -> HxModal). - - - - - Extension methods for installation of support. - - - - - Adds support to be able to display message boxes using HxMessageBoxHost. - - Arguments for event. @@ -521,13 +466,12 @@ Enum for HTML input types. - As the enum is currently used only for the HxInputText component, only relevant types are included. - As all the values will be needed, they can be added later (add restrictions/validation to HxInputText then). + As the enum is currently used only for HxInputText and HxInputNumber components, only relevant types are included. - The default value. A single-line text field. Line-breaks are automatically removed from the input value. + A single-line text field. Line-breaks are automatically removed from the input value. @@ -559,6 +503,11 @@ keyboard in supporting browsers and devices with dynamic keyboards. + + + A control for entering a number. Displays a numeric keypad in some devices with dynamic keypads. + + Returns a model clone. diff --git a/Havit.Blazor.Documentation/_Imports.razor b/Havit.Blazor.Documentation/_Imports.razor index 72a56713..b45655fe 100644 --- a/Havit.Blazor.Documentation/_Imports.razor +++ b/Havit.Blazor.Documentation/_Imports.razor @@ -6,8 +6,10 @@ @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode @using Microsoft.AspNetCore.Components.WebAssembly.Http @using Microsoft.JSInterop + @using Havit.Blazor.Documentation @using Havit.Blazor.Documentation.Shared @using Havit.Blazor.Documentation.Shared.Components diff --git a/Havit.Blazor.Documentation/wwwroot/css/site.css b/Havit.Blazor.Documentation/wwwroot/css/site.css index 360605bf..cf42b9c4 100644 --- a/Havit.Blazor.Documentation/wwwroot/css/site.css +++ b/Havit.Blazor.Documentation/wwwroot/css/site.css @@ -1,7 +1,11 @@ -:root { +.doc-sidebar { --hx-sidebar-width: 320px; + --hx-sidebar-collapsed-width: 56px; + --hx-sidebar-max-height: calc(100vh - 56px); --hx-sidebar-brand-logo-width: 40px; --hx-sidebar-brand-logo-height: 30px; + --hx-sidebar-header-padding: .5rem 0; + --hx-sidebar-body-padding: 0 1rem 0 0; } #blazor-error-ui { @@ -62,19 +66,20 @@ pre[class*="language-"] { margin-bottom: .25rem } - .doc-content > ul li > p ~ ul, - .doc-content > ol li > p ~ ul { - margin-top: -.5rem; - margin-bottom: 1rem - } +.doc-content > ul li > p ~ ul, +.doc-content > ol li > p ~ ul { + margin-top: -.5rem; + margin-bottom: 1rem +} -.sidebar .hx-sidebar-header { +.doc-sidebar .hx-sidebar-header { flex-wrap: wrap; } -.collapsed.sidebar .sidebar-search, -.collapsed.sidebar .hx-sidebar-footer .hx-form-group { - display: none; +@media screen and (min-width: 992px) { + .doc-sidebar { + top: 56px; + } } .calendar-demo { diff --git a/Havit.Blazor.Documentation/wwwroot/js/app.js b/Havit.Blazor.Documentation/wwwroot/js/app.js deleted file mode 100644 index 4620552f..00000000 --- a/Havit.Blazor.Documentation/wwwroot/js/app.js +++ /dev/null @@ -1,3 +0,0 @@ -window.highlightCode = function() { - Prism.highlightAll(); -}; diff --git a/Havit.Blazor.Documentation/wwwroot/js/color-mode-auto.js b/Havit.Blazor.Documentation/wwwroot/js/color-mode-auto.js deleted file mode 100644 index 372e1486..00000000 --- a/Havit.Blazor.Documentation/wwwroot/js/color-mode-auto.js +++ /dev/null @@ -1,3 +0,0 @@ -if ((document.documentElement.getAttribute('data-bs-theme') == 'auto') && window.matchMedia('(prefers-color-scheme: dark)').matches) { - document.documentElement.setAttribute('data-bs-theme', 'dark'); -} \ No newline at end of file diff --git a/Havit.Blazor.GoogleTagManager/Havit.Blazor.GoogleTagManager.csproj b/Havit.Blazor.GoogleTagManager/Havit.Blazor.GoogleTagManager.csproj index c700263c..def36bb7 100644 --- a/Havit.Blazor.GoogleTagManager/Havit.Blazor.GoogleTagManager.csproj +++ b/Havit.Blazor.GoogleTagManager/Havit.Blazor.GoogleTagManager.csproj @@ -1,7 +1,7 @@  - net8.0;net6.0 + net9.0;net8.0 enable 1591;1701;1702;SA1134 true @@ -14,7 +14,7 @@ - 1.2.1 + 1.3.1 HAVIT Blazor Library - Google Tag Manager support (incl. optional automatic page-views tracking) MIT https://github.com/havit/Havit.Blazor diff --git a/Havit.Blazor.GoogleTagManager/HxGoogleTagManager.cs b/Havit.Blazor.GoogleTagManager/HxGoogleTagManager.cs index 5fc67b74..2da02509 100644 --- a/Havit.Blazor.GoogleTagManager/HxGoogleTagManager.cs +++ b/Havit.Blazor.GoogleTagManager/HxGoogleTagManager.cs @@ -32,14 +32,14 @@ public HxGoogleTagManager( /// public async Task InitializeAsync() { + _jsModule ??= await _jsRuntime.InvokeAsync("import", "./_content/Havit.Blazor.GoogleTagManager/" + nameof(HxGoogleTagManager) + ".js"); + if (_isInitialized) { return; } _isInitialized = true; - _jsModule ??= await _jsRuntime.InvokeAsync("import", "./_content/Havit.Blazor.GoogleTagManager/" + nameof(HxGoogleTagManager) + ".js"); - await _jsModule.InvokeVoidAsync("initialize", _gtmOptions.GtmId); } diff --git a/Havit.Blazor.GoogleTagManager/wwwroot/HxGoogleTagManager.js b/Havit.Blazor.GoogleTagManager/wwwroot/HxGoogleTagManager.js index a883a723..06f169af 100644 --- a/Havit.Blazor.GoogleTagManager/wwwroot/HxGoogleTagManager.js +++ b/Havit.Blazor.GoogleTagManager/wwwroot/HxGoogleTagManager.js @@ -5,7 +5,7 @@ "gtm.start": new Date().getTime(), event: "gtm.js", }); - var f = d.getElementsByTagName("head")[0], + const f = d.getElementsByTagName("head")[0], j = d.createElement(s), dl = l !== "dataLayer" ? "&l=" + l : ""; j.async = true; diff --git a/Havit.Blazor.Grpc.Client.ServerSideRendering/Havit.Blazor.Grpc.Client.ServerSideRendering.csproj b/Havit.Blazor.Grpc.Client.ServerSideRendering/Havit.Blazor.Grpc.Client.ServerSideRendering.csproj index 4e1a2318..e1633c70 100644 --- a/Havit.Blazor.Grpc.Client.ServerSideRendering/Havit.Blazor.Grpc.Client.ServerSideRendering.csproj +++ b/Havit.Blazor.Grpc.Client.ServerSideRendering/Havit.Blazor.Grpc.Client.ServerSideRendering.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0;net8.0 diff --git a/Havit.Blazor.Grpc.Client.Tests/GrpcClientServiceCollectionExtensionsTests.cs b/Havit.Blazor.Grpc.Client.Tests/GrpcClientServiceCollectionExtensionsTests.cs index d0f0b42b..74ffbfa9 100644 --- a/Havit.Blazor.Grpc.Client.Tests/GrpcClientServiceCollectionExtensionsTests.cs +++ b/Havit.Blazor.Grpc.Client.Tests/GrpcClientServiceCollectionExtensionsTests.cs @@ -19,20 +19,4 @@ public void GrpcClientServiceCollectionExtensions_AddGrpcClientsByApiContractAtt // assert Assert.IsNotNull(services.FirstOrDefault(sd => sd.ServiceType == typeof(ITestFacade))); } - -#if NET6_0 - [TestMethod] - public void GrpcClientServiceCollectionExtensions_AddGrpcClientsByApiContractAttributes_RegistersFuncFactoryForServiceWithAttribute() - { - // arrange - var services = new ServiceCollection(); - - - // act - services.AddGrpcClientsByApiContractAttributes(typeof(Dto).Assembly); - - // assert - Assert.IsNotNull(services.FirstOrDefault(sd => sd.ServiceType == typeof(Func))); - } -#endif } diff --git a/Havit.Blazor.Grpc.Client.Tests/Havit.Blazor.Grpc.Client.Tests.csproj b/Havit.Blazor.Grpc.Client.Tests/Havit.Blazor.Grpc.Client.Tests.csproj index 8389ce88..6f18ca55 100644 --- a/Havit.Blazor.Grpc.Client.Tests/Havit.Blazor.Grpc.Client.Tests.csproj +++ b/Havit.Blazor.Grpc.Client.Tests/Havit.Blazor.Grpc.Client.Tests.csproj @@ -1,7 +1,7 @@  - net8.0;net6.0 + net9.0;net8.0 enable false true diff --git a/Havit.Blazor.Grpc.Client.WebAssembly/Havit.Blazor.Grpc.Client.WebAssembly.csproj b/Havit.Blazor.Grpc.Client.WebAssembly/Havit.Blazor.Grpc.Client.WebAssembly.csproj index e1320411..23e5ded6 100644 --- a/Havit.Blazor.Grpc.Client.WebAssembly/Havit.Blazor.Grpc.Client.WebAssembly.csproj +++ b/Havit.Blazor.Grpc.Client.WebAssembly/Havit.Blazor.Grpc.Client.WebAssembly.csproj @@ -1,7 +1,7 @@  - net8.0;net6.0 + net9.0;net8.0 enable diff --git a/Havit.Blazor.Grpc.Client/GrpcClientServiceCollectionExtensions.cs b/Havit.Blazor.Grpc.Client/GrpcClientServiceCollectionExtensions.cs index 8927035c..a6f5a1ff 100644 --- a/Havit.Blazor.Grpc.Client/GrpcClientServiceCollectionExtensions.cs +++ b/Havit.Blazor.Grpc.Client/GrpcClientServiceCollectionExtensions.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.InteropServices; using Grpc.Net.Client.Web; using Grpc.Net.ClientFactory; using Havit.Blazor.Grpc.Client.HttpHeaders; @@ -7,6 +6,7 @@ using Havit.Blazor.Grpc.Client.ServerExceptions; using Havit.Blazor.Grpc.Core; using Havit.ComponentModel; +using Havit.Diagnostics.Contracts; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -25,16 +25,30 @@ public static class GrpcClientServiceCollectionExtensions /// Adds the necessary infrastructure for gRPC clients. /// /// The to add the services to. - /// The assembly to scan for data contracts. + /// Assembly to scan for data contracts. public static void AddGrpcClientInfrastructure( this IServiceCollection services, Assembly assemblyToScanForDataContracts) + { + AddGrpcClientInfrastructure(services, [assemblyToScanForDataContracts]); + } + + /// + /// Adds the necessary infrastructure for gRPC clients. + /// + /// The to add the services to. + /// Assemblies to scan for data contracts. + public static void AddGrpcClientInfrastructure( + this IServiceCollection services, + Assembly[] assembliesToScanForDataContracts) { services.AddTransient(); services.AddSingleton(); services.AddScoped(); services.AddTransient(provider => new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler())); - services.AddSingleton(ClientFactory.Create(BinderConfiguration.Create(marshallerFactories: new[] { ProtoBufMarshallerFactory.Create(RuntimeTypeModel.Create().RegisterApplicationContracts(assemblyToScanForDataContracts)) }, binder: new ProtoBufServiceBinder()))); + services.AddSingleton(ClientFactory.Create(BinderConfiguration.Create( + marshallerFactories: CreateMarshallerFactories(assembliesToScanForDataContracts), + binder: new ProtoBufServiceBinder()))); services.TryAddScoped(); } @@ -57,7 +71,31 @@ public static void AddGrpcClientsByApiContractAttributes( Action configureGrpClientAll = null, Action configureGrpcClientFactory = null) { - var interfacesAndAttributes = (from type in assemblyToScan.GetTypes() + AddGrpcClientsByApiContractAttributes(services, [assemblyToScan], configureGrpcClientWithAuthorization, configureGrpClientAll, configureGrpcClientFactory); + } + + /// + /// Adds gRPC clients based on API contract attributes. + /// + /// The to add the services to. + /// The assembly to scan for API contract attributes. + /// An optional action to configure gRPC clients with authorization. + /// An optional action to configure all gRPC clients. + /// + /// An optional action to configure the gRPC client factory. + /// If Not provided, options.Address (backend URL) will be configured from NavigationManager.BaseUri. + /// + public static void AddGrpcClientsByApiContractAttributes( + this IServiceCollection services, + Assembly[] assembliesToScan, + Action configureGrpcClientWithAuthorization = null, + Action configureGrpClientAll = null, + Action configureGrpcClientFactory = null) + { + Contract.Requires(assembliesToScan is not null); + + var interfacesAndAttributes = (from assembly in assembliesToScan + from type in assembly.GetTypes() from apiContractAttribute in type.GetCustomAttributes(typeof(ApiContractAttribute), false).Cast() select new { Interface = type, Attribute = apiContractAttribute }).ToArray(); @@ -77,6 +115,11 @@ from apiContractAttribute in type.GetCustomAttributes(typeof(ApiContractAttribut } } + private static List CreateMarshallerFactories(Assembly[] assembliesToScanForDataContracts) => + assembliesToScanForDataContracts + .Select(assembly => ProtoBufMarshallerFactory.Create(RuntimeTypeModel.Create().RegisterApplicationContracts(assembly))) + .ToList(); + private static void AddGrpcClientCore( this IServiceCollection services, Action configureGrpcClientWithAuthorization = null, @@ -108,10 +151,5 @@ private static void AddGrpcClientCore( configureGrpClientAll?.Invoke(grpcClient); configureGrpcClientWithAuthorization?.Invoke(grpcClient); - -#if NET6_0 - // NET6 failing GC workaround https://github.com/dotnet/runtime/issues/62054 - services.AddSingleton>(sp => () => sp.GetRequiredService()); -#endif } } diff --git a/Havit.Blazor.Grpc.Client/Havit.Blazor.Grpc.Client.csproj b/Havit.Blazor.Grpc.Client/Havit.Blazor.Grpc.Client.csproj index 88dbba8c..d602d0f4 100644 --- a/Havit.Blazor.Grpc.Client/Havit.Blazor.Grpc.Client.csproj +++ b/Havit.Blazor.Grpc.Client/Havit.Blazor.Grpc.Client.csproj @@ -1,7 +1,7 @@  - net8.0;net6.0 + net9.0;net8.0 enable diff --git a/Havit.Blazor.Grpc.Core.Tests/Havit.Blazor.Grpc.Core.Tests.csproj b/Havit.Blazor.Grpc.Core.Tests/Havit.Blazor.Grpc.Core.Tests.csproj index f6b755c2..f84c7af5 100644 --- a/Havit.Blazor.Grpc.Core.Tests/Havit.Blazor.Grpc.Core.Tests.csproj +++ b/Havit.Blazor.Grpc.Core.Tests/Havit.Blazor.Grpc.Core.Tests.csproj @@ -1,7 +1,7 @@  - net8.0;net6.0 + net9.0;net8.0 enable false true diff --git a/Havit.Blazor.Grpc.Core/Havit.Blazor.Grpc.Core.csproj b/Havit.Blazor.Grpc.Core/Havit.Blazor.Grpc.Core.csproj index 30fe39e0..3784f760 100644 --- a/Havit.Blazor.Grpc.Core/Havit.Blazor.Grpc.Core.csproj +++ b/Havit.Blazor.Grpc.Core/Havit.Blazor.Grpc.Core.csproj @@ -1,7 +1,7 @@  - net8.0;net6.0 + net9.0;net8.0 enable diff --git a/Havit.Blazor.Grpc.Server/EndpointRouteBuilderGrpcExtensions.cs b/Havit.Blazor.Grpc.Server/EndpointRouteBuilderGrpcExtensions.cs index edf5656f..b90d9127 100644 --- a/Havit.Blazor.Grpc.Server/EndpointRouteBuilderGrpcExtensions.cs +++ b/Havit.Blazor.Grpc.Server/EndpointRouteBuilderGrpcExtensions.cs @@ -20,11 +20,28 @@ public static void MapGrpcServicesByApiContractAttributes( Assembly assemblyToScan, Action configureEndpointWithAuthorization = null, Action configureEndpointAll = null) + { + MapGrpcServicesByApiContractAttributes(builder, [assemblyToScan], configureEndpointWithAuthorization, configureEndpointAll); + } + + /// + /// Maps gRPC services with as route endpoints. + /// + /// Endpoint route builder. + /// Assemblies to scan for interfaces with . + /// Optional configuration for endpoints with authorization (all except [ApiContract(RequireAuthorization = false)]). Usually you want to setup endpoint.RequireAuthorization(...) here."/> + /// Optional configuration for all endpoints. + public static void MapGrpcServicesByApiContractAttributes( + this IEndpointRouteBuilder builder, + Assembly[] assembliesToScan, + Action configureEndpointWithAuthorization = null, + Action configureEndpointAll = null) { Contract.Requires(builder is not null, nameof(builder)); - Contract.Requires(assemblyToScan is not null, nameof(assemblyToScan)); + Contract.Requires(assembliesToScan is not null, nameof(assembliesToScan)); - var interfacesAndAttributes = (from type in assemblyToScan.GetTypes() + var interfacesAndAttributes = (from assembly in assembliesToScan + from type in assembly.GetTypes() from apiContractAttribute in type.GetCustomAttributes(typeof(ApiContractAttribute), false).Cast() select new { Interface = type, Attribute = apiContractAttribute }).ToArray(); diff --git a/Havit.Blazor.Grpc.Server/GrpcServerServiceCollectionExtensions.cs b/Havit.Blazor.Grpc.Server/GrpcServerServiceCollectionExtensions.cs index a4c5bebf..ca487a66 100644 --- a/Havit.Blazor.Grpc.Server/GrpcServerServiceCollectionExtensions.cs +++ b/Havit.Blazor.Grpc.Server/GrpcServerServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using Havit.Blazor.Grpc.Core; using Havit.Blazor.Grpc.Server.GlobalizationLocalization; using Havit.Blazor.Grpc.Server.ServerExceptions; +using Havit.Diagnostics.Contracts; using Microsoft.Extensions.DependencyInjection; using ProtoBuf.Grpc.Configuration; using ProtoBuf.Grpc.Server; @@ -12,14 +13,38 @@ namespace Havit.Blazor.Grpc.Server; public static class GrpcServerServiceCollectionExtensions { + /// + /// Adds the necessary infrastructure for gRPC servers. + /// + /// The to add the services to. + /// Assembly to scan for data contracts + /// gRPC Service options public static void AddGrpcServerInfrastructure( this IServiceCollection services, Assembly assemblyToScanForDataContracts, Action configureOptions = null) { + AddGrpcServerInfrastructure(services, [assemblyToScanForDataContracts], configureOptions); + } + + /// + /// Adds the necessary infrastructure for gRPC servers. + /// + /// The to add the services to. + /// Assemblies to scan for data contracts + /// gRPC Service options + public static void AddGrpcServerInfrastructure( + this IServiceCollection services, + Assembly[] assembliesToScanForDataContracts, + Action configureOptions = null) + { + Contract.Requires(assembliesToScanForDataContracts is not null); + services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(BinderConfiguration.Create(marshallerFactories: new[] { ProtoBufMarshallerFactory.Create(RuntimeTypeModel.Create().RegisterApplicationContracts(assemblyToScanForDataContracts)) }, binder: new ServiceBinderWithServiceResolutionFromServiceCollection(services))); + services.AddSingleton(BinderConfiguration.Create( + marshallerFactories: CreateMarshallerFactories(assembliesToScanForDataContracts), + binder: new ServiceBinderWithServiceResolutionFromServiceCollection(services))); services.AddCodeFirstGrpc(options => { @@ -30,4 +55,13 @@ public static void AddGrpcServerInfrastructure( configureOptions?.Invoke(options); }); } + + /// + /// Creates marshaller factories for the specified assemblies. + /// Each assembly has its own marshaller factory. + /// + private static List CreateMarshallerFactories(Assembly[] assembliesToScanForDataContracts) => + assembliesToScanForDataContracts + .Select(assembly => ProtoBufMarshallerFactory.Create(RuntimeTypeModel.Create().RegisterApplicationContracts(assembly))) + .ToList(); } diff --git a/Havit.Blazor.Grpc.Server/Havit.Blazor.Grpc.Server.csproj b/Havit.Blazor.Grpc.Server/Havit.Blazor.Grpc.Server.csproj index 0ea3e342..083bccf7 100644 --- a/Havit.Blazor.Grpc.Server/Havit.Blazor.Grpc.Server.csproj +++ b/Havit.Blazor.Grpc.Server/Havit.Blazor.Grpc.Server.csproj @@ -1,7 +1,7 @@  - net8.0;net6.0 + net9.0;net8.0 enable diff --git a/Havit.Blazor.Grpc.TestContracts/Havit.Blazor.Grpc.TestContracts.csproj b/Havit.Blazor.Grpc.TestContracts/Havit.Blazor.Grpc.TestContracts.csproj index 5ce2e0b7..f726e1b6 100644 --- a/Havit.Blazor.Grpc.TestContracts/Havit.Blazor.Grpc.TestContracts.csproj +++ b/Havit.Blazor.Grpc.TestContracts/Havit.Blazor.Grpc.TestContracts.csproj @@ -1,7 +1,7 @@  - net8.0;net6.0 + net9.0;net8.0 enable diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/DependencyInjectionExtensions.cs b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/DependencyInjectionExtensions.cs new file mode 100644 index 00000000..cb3a6244 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/DependencyInjectionExtensions.cs @@ -0,0 +1,14 @@ +using Havit.Blazor.Components.Web; + +namespace Havit.Blazor.TestApp.Client; + +public static class DependencyInjectionExtensions +{ + public static IServiceCollection AddClientServices(this IServiceCollection services) + { + services.AddHxServices(); + services.AddTransient(); + + return services; + } +} diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/GlobalUsings.cs b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/GlobalUsings.cs new file mode 100644 index 00000000..d0bfcfa3 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/GlobalUsings.cs @@ -0,0 +1,6 @@ +global using Microsoft.AspNetCore.Components; + +global using Havit.Blazor.Components.Web; +global using Havit.Blazor.Components.Web.Bootstrap; + +global using Havit.Blazor.Documentation.DemoData; \ No newline at end of file diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/Havit.Blazor.TestApp.Client.csproj b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/Havit.Blazor.TestApp.Client.csproj new file mode 100644 index 00000000..350f66c2 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/Havit.Blazor.TestApp.Client.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + true + Default + + + + $(NoWarn);1701;1702;SA1134;VSTHRD003;VSTHRD200 + + + + + + + + + + + + + DemoData\%(RecursiveDir)%(FileName)%(Extension) + + + + diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxCollapseTests/HxCollapse_MultipleShowShouldRender_Issue910_Test.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxCollapseTests/HxCollapse_MultipleShowShouldRender_Issue910_Test.razor new file mode 100644 index 00000000..201d9a59 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxCollapseTests/HxCollapse_MultipleShowShouldRender_Issue910_Test.razor @@ -0,0 +1,18 @@ +@page "/HxCollapse_ShouldRender_Test" +@rendermode @(new InteractiveServerRenderMode(prerender: false)) + +

    +
    + #910 - [HxCollapse] When you call ShowAsync() while the collapse is already shown, ShouldRender gets disabled +

    + + + + @_counter + + + +@code { + private HxCollapse _collapseComponent; + private int _counter = 0; +} diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxGridTests/HxGrid_DragDropRows_Test.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxGridTests/HxGrid_DragDropRows_Test.razor new file mode 100644 index 00000000..7617ad12 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxGridTests/HxGrid_DragDropRows_Test.razor @@ -0,0 +1,95 @@ +@page "/HxGrid_DragDropRows" +@rendermode InteractiveServer + +

    HxGrid_DragDropRows

    + + + + + + + + + + + + + +@code { + HxGrid grid; + private record class Person(string Name, string Initials) + { + public int Position { get; set; } + }; + private List people; + + protected override void OnInitialized() + { + people = new List + { + new Person("Starr Ringo", "RS") { Position = 1 }, + new Person("Lennon John", "JL") { Position = 2 }, + new Person("McCartney Paul", "PMC") { Position = 3 }, + new Person("Harrison George", "GH") { Position = 4 } + }; + } + + Person clickedEmployee; + Dictionary HeaderRowAdditionalAttributes = new() { { "data-row-type", "header" } }; + Dictionary ItemRowAdditionalAttributes = new() { { "draggable", "true" }, { "ondragover", "event.preventDefault();" } }; + + Dictionary EmployeeRowAttributes(Person item) + { + return new() { + {"ondragstart", EventCallback.Factory.Create(this, (e) => HandleDragStart(item, e))}, + {"ondrop", EventCallback.Factory.Create(this, () => HandleDrop(item))}, + // {"ondragenter", EventCallback.Factory.Create(this,(e) => HandleDragEnter(item, e)) }, + // {"ondragleave", EventCallback.Factory.Create(this,HandleDragLeave) }, + // {"ondragend", EventCallback.Factory.Create(this,HandleDragEnd) }, + }; + } + + private void SetEmpoloyee(Person employee) + { + clickedEmployee = employee; + } + + private Task> GetGridData(GridDataProviderRequest request) + { + return Task.FromResult(new GridDataProviderResult() + { + Data = people.OrderBy(x => x.Position).ToList(), + TotalCount = people?.Count + }); + } + + private Person draggedItem; + + + private void HandleDragStart(Person item, DragEventArgs e) + { + draggedItem = item; + } + + private async Task HandleDrop(Person item) + { + if (draggedItem == null) return; + + + var draggedItemPosition = draggedItem.Position; + + if (draggedItemPosition == item?.Position) return; + draggedItem.Position = item.Position; + item.Position = draggedItemPosition; + + + await grid.RefreshDataAsync(); + } +} \ No newline at end of file diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxGridTests/HxGrid_InfiniteScroll_MultiSelectionEnabled_Test.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxGridTests/HxGrid_InfiniteScroll_MultiSelectionEnabled_Test.razor new file mode 100644 index 00000000..eae7ad5a --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxGridTests/HxGrid_InfiniteScroll_MultiSelectionEnabled_Test.razor @@ -0,0 +1,45 @@ +@page "/HxGrid_InfiniteScroll_MultiSelectionEnabled" +@rendermode InteractiveServer +@inject IDemoDataService DemoDataService + + + + + + + + + + + + + +

    + Selected employees: @(String.Join(", ", selectedEmployees.Select(e => e.Name))) +

    + + +@code { + private HashSet selectedEmployees = new(); + + private async Task> GetGridData(GridDataProviderRequest request) + { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); + return new GridDataProviderResult() + { + Data = response.Data, + TotalCount = response.TotalCount + }; + } +} \ No newline at end of file diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxGridTests/HxGrid_RowAdditionalAttributes_Test.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxGridTests/HxGrid_RowAdditionalAttributes_Test.razor new file mode 100644 index 00000000..7c4b33e0 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxGridTests/HxGrid_RowAdditionalAttributes_Test.razor @@ -0,0 +1,44 @@ +@page "/HxGrid_RowAdditionalAttributes" +@rendermode InteractiveServer +@inject IDemoDataService DemoDataService + +

    HxGrid_RowAdditionalAttributes

    + +Selectet phone: @clickedEmployee?.Phone + + + + + + + + + + +@code { + EmployeeDto clickedEmployee; + Dictionary headerRowAdditionalAttributes = new() { { "data-row-type", "header" } }; + Dictionary footerRowAdditionalAttributes = new() { { "data-row-type", "footer" } }; + Dictionary itemRowAdditionalAttributes = new() { { "data-row-type", "row" }, { "data-other", "dummy" } }; + + private Dictionary EmployeeRowAttributes(EmployeeDto e) + { + return new() { + { "data-name", e?.Name }, + { "onmouseup", EventCallback.Factory.Create(this,x => clickedEmployee = e) } }; + } + + private async Task> GetGridData(GridDataProviderRequest request) + { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); + return new GridDataProviderResult() + { + Data = response.Data, + TotalCount = response.TotalCount + }; + } +} \ No newline at end of file diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_InteractiveServer_NoPrerendering_Test.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_InteractiveServer_NoPrerendering_Test.razor new file mode 100644 index 00000000..5762f55c --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_InteractiveServer_NoPrerendering_Test.razor @@ -0,0 +1,19 @@ +@page "/HxToast_InteractiveServer_NoPrerendering" +@rendermode @(new InteractiveServerRenderMode(prerender: false)) + +

    HxToast_InteractiveServer_NoPrerendering

    + + + + + @if (_show) + { + + } + + + + +@code { + bool _show = false; +} \ No newline at end of file diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_InteractiveServer_Test.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_InteractiveServer_Test.razor new file mode 100644 index 00000000..c2abd057 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_InteractiveServer_Test.razor @@ -0,0 +1,23 @@ +@page "/HxToast_InteractiveServer" +@rendermode InteractiveServer + +

    HxToast_InteractiveServer

    + + + @* + With pre-rendering, the HxToast gets shown twice as Blazor replaces the DOM completely + https://github.com/dotnet/aspnetcore/issues/42561 + *@ + + + @if (_show) + { + + } + + + + +@code { + bool _show = false; +} \ No newline at end of file diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_InteractiveWebAssembly_NoPrerendering_Test.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_InteractiveWebAssembly_NoPrerendering_Test.razor new file mode 100644 index 00000000..23f699bb --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_InteractiveWebAssembly_NoPrerendering_Test.razor @@ -0,0 +1,19 @@ +@page "/HxToast_InteractiveWebAssembly_NoPrerendering" +@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false)) + +

    HxToast_InteractiveWebAssembly_NoPrerendering

    + + + + + @if (_show) + { + + } + + + + +@code { + bool _show = false; +} \ No newline at end of file diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_InteractiveWebAssembly_Test.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_InteractiveWebAssembly_Test.razor new file mode 100644 index 00000000..6959a59e --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_InteractiveWebAssembly_Test.razor @@ -0,0 +1,23 @@ +@page "/HxToast_InteractiveWebAssembly" +@rendermode InteractiveWebAssembly + +

    HxToast_InteractiveWebAssembly

    + + + @* + With pre-rendering, the HxToast gets shown twice as Blazor replaces the DOM completely + https://github.com/dotnet/aspnetcore/issues/42561 + *@ + + + @if (_show) + { + + } + + + + +@code { + bool _show = false; +} \ No newline at end of file diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_StaticSSR_NoEnhanceNav_Test.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_StaticSSR_NoEnhanceNav_Test.razor new file mode 100644 index 00000000..74631ace --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_StaticSSR_NoEnhanceNav_Test.razor @@ -0,0 +1,7 @@ +@page "/HxToast_StaticSSR_NoEnhanceNav" + +

    HxToast_StaticSSR

    + + + + \ No newline at end of file diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_StaticSSR_Test.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_StaticSSR_Test.razor new file mode 100644 index 00000000..5d223fae --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxToastTests/HxToast_StaticSSR_Test.razor @@ -0,0 +1,35 @@ +@page "/HxToast_StaticSSR" + +

    HxToast_StaticSSR

    + + + + + + + @if (_submitted && !FormModel.IsValid) + { + + } + + + + + +@code { + [SupplyParameterFromForm] public Model FormModel { get; set; } = new Model(); + + private void HandleSubmit() + { + _submitted = true; + } + + private bool _submitted; + + public class Model + { + public string Text { get; set; } + + public bool IsValid => !String.IsNullOrWhiteSpace(Text); + } +} \ No newline at end of file diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxTooltipTests/HxTooltip_SettingEmptyTextShouldNotFail_Test.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxTooltipTests/HxTooltip_SettingEmptyTextShouldNotFail_Test.razor new file mode 100644 index 00000000..add375d6 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxTooltipTests/HxTooltip_SettingEmptyTextShouldNotFail_Test.razor @@ -0,0 +1,21 @@ +@page "/HxTooltip_SettingEmptyTextShouldNotFail" +@rendermode InteractiveWebAssembly + + + +@{ + string tooltip = null; //""; + if (isChecked) + { + tooltip = "Checked"; + } + +
    + Hover over me to see the tooltip. +
    +
    +} + +@code { + bool isChecked = false; +} diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/Index.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/Index.razor new file mode 100644 index 00000000..72b32212 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/Index.razor @@ -0,0 +1,11 @@ +@page "/" + +

    Havit.Blazor.Tests

    + + diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/Index.razor.cs b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/Index.razor.cs new file mode 100644 index 00000000..4a2b5a25 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/Index.razor.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Components; +using System.Reflection; + +namespace Havit.Blazor.TestApp.Client; + +public partial class Index +{ + private IEnumerable GetTestPages() + { + return GetRoutesToRender(typeof(_Imports).Assembly); + } + + public static List GetRoutesToRender(Assembly assembly) + { + // Get all the components whose base class is ComponentBase + var components = assembly + .ExportedTypes + .Where(t => t.IsSubclassOf(typeof(ComponentBase))) + .Where(t => t.Name.EndsWith("Test")); + + var routes = components + .SelectMany(component => GetRoutesFromComponent(component)) + .Where(config => config is not null) + .ToList(); + + return routes; + } + + private static IEnumerable GetRoutesFromComponent(Type component) + { + var attributes = component.GetCustomAttributes(inherit: true); + + var routeAttributes = attributes.OfType(); + + if (routeAttributes is null) + { + yield return null; + } + + foreach (var routeAttribute in routeAttributes) + { + if (String.IsNullOrWhiteSpace(routeAttribute.Template)) + { + continue; + } + + if (routeAttribute.Template.Contains('{')) + { + continue; + } + + yield return routeAttribute.Template; + } + } +} diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/Program.cs b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/Program.cs new file mode 100644 index 00000000..865f386c --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/Program.cs @@ -0,0 +1,8 @@ +using Havit.Blazor.TestApp.Client; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); + +builder.Services.AddClientServices(); + +await builder.Build().RunAsync(); diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/_Imports.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/_Imports.razor new file mode 100644 index 00000000..ab08a367 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/_Imports.razor @@ -0,0 +1,13 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop + +@using Havit.Blazor.Components.Web +@using Havit.Blazor.Components.Web.Bootstrap + +@using Havit.Blazor.TestApp.Client diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/App.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/App.razor new file mode 100644 index 00000000..c25f7bbe --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/App.razor @@ -0,0 +1,25 @@ +@using Havit.Blazor.Components.Web.Bootstrap + + + + + + + + + + + + + + + + + + + + + @((MarkupString)HxSetup.RenderBootstrapJavaScriptReference()) + + + diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/Layout/MainLayout.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/Layout/MainLayout.razor new file mode 100644 index 00000000..0fd1b20e --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/Layout/MainLayout.razor @@ -0,0 +1,9 @@ +@inherits LayoutComponentBase + +@Body + +
    + An unhandled error has occurred. + Reload + 🗙 +
    diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/Layout/MainLayout.razor.css b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/Layout/MainLayout.razor.css new file mode 100644 index 00000000..df8c10ff --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/Layout/MainLayout.razor.css @@ -0,0 +1,18 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/Pages/Error.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/Pages/Error.razor new file mode 100644 index 00000000..71d3a5db --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

    Error.

    +

    An error occurred while processing your request.

    + +@if (ShowRequestId) +{ +

    + Request ID: @RequestId +

    +} + +

    Development Mode

    +

    + Swapping to Development environment will display more detailed information about the error that occurred. +

    +

    + The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

    + +@code{ + [CascadingParameter] + private HttpContext HttpContext { get; set; } + + private string RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/Routes.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/Routes.razor new file mode 100644 index 00000000..d39c7e89 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/Routes.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/_Imports.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/_Imports.razor new file mode 100644 index 00000000..e270ce23 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Components/_Imports.razor @@ -0,0 +1,11 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Havit.Blazor.TestApp +@using Havit.Blazor.TestApp.Client +@using Havit.Blazor.TestApp.Components diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Havit.Blazor.TestApp.csproj b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Havit.Blazor.TestApp.csproj new file mode 100644 index 00000000..260011e0 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Havit.Blazor.TestApp.csproj @@ -0,0 +1,12 @@ + + + + net9.0 + + + + + + + + diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Program.cs b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Program.cs new file mode 100644 index 00000000..0b33ec2a --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Program.cs @@ -0,0 +1,37 @@ +using Havit.Blazor.TestApp.Components; +using Havit.Blazor.TestApp.Client; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents() + .AddInteractiveWebAssemblyComponents(); + +builder.Services.AddClientServices(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseWebAssemblyDebugging(); +} +else +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); + +app.MapStaticAssets(); +app.UseAntiforgery(); + +app.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(Havit.Blazor.TestApp.Client._Imports).Assembly); + +app.Run(); diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Properties/launchSettings.json b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Properties/launchSettings.json new file mode 100644 index 00000000..cb7b4294 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:26999", + "sslPort": 44368 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5277", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7180;http://localhost:5277", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp/appsettings.Development.json b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp/appsettings.json b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp/wwwroot/app.css b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/wwwroot/app.css new file mode 100644 index 00000000..e398853b --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp/wwwroot/app.css @@ -0,0 +1,29 @@ +h1:focus { + outline: none; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.darker-border-checkbox.form-check-input { + border-color: #929292; +} diff --git a/Havit.Blazor.sln b/Havit.Blazor.sln index 68a81bf0..d9eb6000 100644 --- a/Havit.Blazor.sln +++ b/Havit.Blazor.sln @@ -1,5 +1,5 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# 17 +# Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Havit.Blazor.Components.Web", "Havit.Blazor.Components.Web\Havit.Blazor.Components.Web.csproj", "{DF1C423F-ACA1-446E-A9F1-099DFDF70D44}" @@ -22,6 +22,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props exclusion.dic = exclusion.dic + global.json = global.json LICENSE = LICENSE logo.png = logo.png nuget.config = nuget.config @@ -71,6 +72,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{45BC .github\FUNDING.yml = .github\FUNDING.yml EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Havit.Blazor.Components.Web.Bootstrap.EntifyFrameworkCore.Tests", "Havit.Blazor.Components.Web.Bootstrap.EntifyFrameworkCore.Tests\Havit.Blazor.Components.Web.Bootstrap.EntifyFrameworkCore.Tests.csproj", "{AD166DF7-CAFA-4471-B6F2-CA188BC96E5B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Havit.Blazor.TestApp", "Havit.Blazor.TestApp\Havit.Blazor.TestApp\Havit.Blazor.TestApp.csproj", "{9EF7E599-FF7D-4574-BF95-F439365F2B69}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Havit.Blazor.TestApp.Client", "Havit.Blazor.TestApp\Havit.Blazor.TestApp.Client\Havit.Blazor.TestApp.Client.csproj", "{38D87399-13C1-4C86-9343-712EAE6095F0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -161,6 +168,18 @@ Global {F6A546C8-60C3-47AC-A58B-66E3513DCC7A}.Debug|Any CPU.Build.0 = Debug|Any CPU {F6A546C8-60C3-47AC-A58B-66E3513DCC7A}.Release|Any CPU.ActiveCfg = Release|Any CPU {F6A546C8-60C3-47AC-A58B-66E3513DCC7A}.Release|Any CPU.Build.0 = Release|Any CPU + {AD166DF7-CAFA-4471-B6F2-CA188BC96E5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD166DF7-CAFA-4471-B6F2-CA188BC96E5B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD166DF7-CAFA-4471-B6F2-CA188BC96E5B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD166DF7-CAFA-4471-B6F2-CA188BC96E5B}.Release|Any CPU.Build.0 = Release|Any CPU + {9EF7E599-FF7D-4574-BF95-F439365F2B69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9EF7E599-FF7D-4574-BF95-F439365F2B69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9EF7E599-FF7D-4574-BF95-F439365F2B69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9EF7E599-FF7D-4574-BF95-F439365F2B69}.Release|Any CPU.Build.0 = Release|Any CPU + {38D87399-13C1-4C86-9343-712EAE6095F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38D87399-13C1-4C86-9343-712EAE6095F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38D87399-13C1-4C86-9343-712EAE6095F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38D87399-13C1-4C86-9343-712EAE6095F0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Havit.Blazor.slnLaunch b/Havit.Blazor.slnLaunch index 17cec1ef..7f8a2dda 100644 --- a/Havit.Blazor.slnLaunch +++ b/Havit.Blazor.slnLaunch @@ -1,37 +1,62 @@ [ - { - "Name": "Doc", - "Projects": [ - { - "Path": "Havit.Blazor.Documentation.Server\\Havit.Blazor.Documentation.Server.csproj", - "Action": "Start", - "DebugTarget": "IIS Express" - } - ] - }, - { - "Name": "Doc \u002B BlazorAppTest", - "Projects": [ - { - "Path": "BlazorAppTest\\BlazorAppTest.csproj", - "Action": "Start", - "DebugTarget": "IIS Express" - }, - { - "Path": "Havit.Blazor.Documentation.Server\\Havit.Blazor.Documentation.Server.csproj", - "Action": "Start", - "DebugTarget": "IIS Express" - } - ] - }, - { - "Name": "BlazorAppTest", - "Projects": [ - { - "Path": "BlazorAppTest\\BlazorAppTest.csproj", - "Action": "Start", - "DebugTarget": "IIS Express" - } - ] - } + { + "Name": "Doc", + "Projects": [ + { + "Path": "Havit.Blazor.Documentation.Server\\Havit.Blazor.Documentation.Server.csproj", + "Action": "Start", + "DebugTarget": "IIS Express" + } + ] + }, + { + "Name": "Doc + BlazorAppTest", + "Projects": [ + { + "Path": "BlazorAppTest\\BlazorAppTest.csproj", + "Action": "Start", + "DebugTarget": "IIS Express" + }, + { + "Path": "Havit.Blazor.Documentation.Server\\Havit.Blazor.Documentation.Server.csproj", + "Action": "Start", + "DebugTarget": "IIS Express" + } + ] + }, + { + "Name": "BlazorAppTest", + "Projects": [ + { + "Path": "BlazorAppTest\\BlazorAppTest.csproj", + "Action": "Start", + "DebugTarget": "IIS Express" + } + ] + }, + { + "Name": "TestApp", + "Projects": [ + { + "Path": "Havit.Blazor.TestApp\\Havit.Blazor.TestApp\\Havit.Blazor.TestApp.csproj", + "Action": "Start", + "DebugTarget": "IIS Express" + } + ] + }, + { + "Name": "Doc + TestApp", + "Projects": [ + { + "Path": "Havit.Blazor.Documentation.Server\\Havit.Blazor.Documentation.Server.csproj", + "Action": "Start", + "DebugTarget": "IIS Express" + }, + { + "Path": "Havit.Blazor.TestApp\\Havit.Blazor.TestApp\\Havit.Blazor.TestApp.csproj", + "Action": "Start", + "DebugTarget": "IIS Express" + } + ] + } ] \ No newline at end of file diff --git a/Havit.Extensions.Localization/Havit.Extensions.Localization.csproj b/Havit.Extensions.Localization/Havit.Extensions.Localization.csproj index 1619eb23..7fe274e9 100644 --- a/Havit.Extensions.Localization/Havit.Extensions.Localization.csproj +++ b/Havit.Extensions.Localization/Havit.Extensions.Localization.csproj @@ -12,7 +12,7 @@ - 1.0.9 + 1.0.10 HAVIT .NET Framework Extensions - Localization MIT https://github.com/havit/Havit.Blazor diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/Havit.SourceGenerators.StrongApiStringLocalizers.csproj b/Havit.SourceGenerators.StrongApiStringLocalizers/Havit.SourceGenerators.StrongApiStringLocalizers.csproj index a2b63649..bf749b85 100644 --- a/Havit.SourceGenerators.StrongApiStringLocalizers/Havit.SourceGenerators.StrongApiStringLocalizers.csproj +++ b/Havit.SourceGenerators.StrongApiStringLocalizers/Havit.SourceGenerators.StrongApiStringLocalizers.csproj @@ -12,7 +12,7 @@ - 1.0.10 + 1.0.11 HAVIT Source Generators for generating string localizers strong API. MIT https://github.com/havit/Havit.Blazor diff --git a/README.md b/README.md index e8194ca4..9f4a1c01 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,11 @@ [![#StandWithUkraine](https://img.shields.io/badge/%23StandWithUkraine-Russian%20warship%2C%20go%20f%23ck%20yourself-blue)](https://www.peopleinneed.net/what-we-do/humanitarian-aid-and-development/ukraine) * Free Bootstrap 5.3 components for ASP.NET Blazor -* .NET 6+ with Blazor WebAssembly or Blazor Server (other hosting models not tested yet, .NET 7 fully supported) * [Enterprise project template](https://github.com/havit/NewProjectTemplate-Blazor) (optional) - layered architecture, EF Core, gRPC code-first, ... If you enjoy using [HAVIT Blazor](https://havit.blazor.eu/), you can [become a sponsor](https://github.com/sponsors/havit). Your sponsorship will allow us to devote time to features and issues requested by the community (i.e. not required by our own projects) ❤️. - -# See [>>Interactive Documentation & Demos<<](https://havit.blazor.eu) - -## 🔥[Migration to v4](https://havit.blazor.eu/migrating)🔥 + See 👉👉 [Interactive Documentation & Demos](https://havit.blazor.eu) 👈👈 # Components diff --git a/global.json b/global.json new file mode 100644 index 00000000..1a66c048 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "9.0.100", + "rollForward": "latestPatch" + } +} \ No newline at end of file