diff --git a/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor b/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor index 2b8ebe5e0f7..ae4fc22ece8 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor @@ -2,6 +2,20 @@ @inject IStringLocalizer Localizer @inject IOptionsMonitor WebsiteOption + + + +

@Localizer["IntersectionObserverTitle"]

@Localizer["IntersectionObserverDescription"]

@@ -56,23 +70,10 @@ - - -

@((MarkupString)Localizer["IntersectionObserverVisibleDesc"].Value)

+
@_videoStateString
-

@_videoStateString

@@ -87,8 +88,10 @@ -

@((MarkupString)Localizer["IntersectionObserverThresholdDesc"].Value)

-

@_thresholdValueString

+
+

@((MarkupString)Localizer["IntersectionObserverThresholdDesc"].Value)

+
@_thresholdValueString
+
@@ -100,4 +103,24 @@ + +
+
@((MarkupString)Localizer["LoadMoreDesc"].Value)
+
+
+
+ @foreach (var image in _items) + { +
+ +
+ } +
+ + +
+
+ diff --git a/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor.cs index 5ea07dcc1e0..6e278fa8677 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor.cs @@ -23,8 +23,8 @@ protected override void OnInitialized() { base.OnInitialized(); - _images = Enumerable.Range(1, 100).Select(i => $"{WebsiteOption.CurrentValue.AssetRootPath}images/default.jpeg").ToList(); - _items = Enumerable.Range(1, 20).Select(i => $"https://picsum.photos/160/160?random={i}").ToList(); + _images = [.. Enumerable.Range(1, 100).Select(i => $"{WebsiteOption.CurrentValue.AssetRootPath}images/default.jpeg")]; + _items = [.. Enumerable.Range(1, 20).Select(i => $"https://picsum.photos/160/160?random={i}")]; } private Task OnIntersectingAsync(IntersectionObserverEntry entry) @@ -43,12 +43,21 @@ private async Task OnLoadMoreAsync(IntersectionObserverEntry entry) if (entry.IsIntersecting) { await Task.Delay(1000); - _items.AddRange(Enumerable.Range(_items.Count + 1, 20) - .Select(i => $"https://picsum.photos/160/160?random={i}")); + _items.AddRange(Enumerable.Range(_items.Count + 1, 20).Select(i => $"https://picsum.photos/160/160?random={i}")); StateHasChanged(); } } + private bool _canLoading = true; + private async Task OnLoadMoreItemAsync() + { + await Task.Delay(500); + + _canLoading = _items.Count < 100; + _items.AddRange(Enumerable.Range(_items.Count + 1, 20).Select(i => $"https://picsum.photos/160/160?random={i}")); + StateHasChanged(); + } + private string? _videoStateString; private string? _textColorString = "text-muted"; diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index f47a6dacfcd..6a7d7173378 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -6855,7 +6855,10 @@ "AttributeAutoUnobserveWhenIntersection": "Whether to automatically cancel the observation when element visible", "AttributeAutoUnobserveWhenNotIntersection": "Whether to automatically cancel the observation when element invisible", "AttributeOnIntersectingAsync": "The callback when intersecting", - "AttributeChildContent": "Child component" + "AttributeChildContent": "Child component", + "LoadMoreTitle": "LoadMore Component", + "LoadMoreIntro": "By setting the LoadMore component parameter IsLoading to control the loading state, the OnLoadMoreAsync callback method loads more data", + "LoadMoreDesc": "In this example, the loading indicator is displayed by setting CanLoading to true, and the No More Data prompt text is displayed by setting it to false after loading is complete. The UI for loading more data can be customized through LoadingTemplate, the UI displayed when there is no more data can be customized through NoMoreTemplate, and the indicator text displayed when there are no more add-ons can be set through the NoMoreText parameter." }, "BootstrapBlazor.Server.Components.Samples.SortableLists": { "SortableListTitle": "SortableList", diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index 4518851fa07..bbfa34c8e17 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -6855,7 +6855,10 @@ "AttributeAutoUnobserveWhenIntersection": "元素可见时是否自动取消观察", "AttributeAutoUnobserveWhenNotIntersection": "元素不可见时是否自动取消观察", "AttributeOnIntersectingAsync": "可见回调方法", - "AttributeChildContent": "子组件" + "AttributeChildContent": "子组件", + "LoadMoreTitle": "LoadMore 组件", + "LoadMoreIntro": "通过设置 LoadMore 组件参数 CanLoading 控制加载状态,OnLoadMoreAsync 回调方法加载更多数据", + "LoadMoreDesc": "本例中通过设置 CanLoadingtrue 显示加载指示符,加载完成后设置为 false 显示 没有更多数据 提示文本,可以通过 LoadingTemplate 自定义加载更多的 UI,通过 NoMoreTemplate 自定义没有更多数据时显示的 UI,可以通过 NoMoreText 参数设置没有更多加载项时显示的指示文本" }, "BootstrapBlazor.Server.Components.Samples.SortableLists": { "SortableListTitle": "SortableList 拖拽组件", diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index d50fa55ed68..3e7808b3594 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@  - 9.8.2-beta01 + 9.8.2-beta02 diff --git a/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.cs b/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.cs index 316e8662912..11691d48843 100644 --- a/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.cs +++ b/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone - namespace BootstrapBlazor.Components; /// @@ -12,7 +11,8 @@ namespace BootstrapBlazor.Components; public partial class IntersectionObserver { /// - /// The element that is used as the viewport for checking visibility of the target. Must be the ancestor of the target. Defaults to the browser viewport if not specified or if null + /// 获得/设置 是否使用元素视口作为根元素 默认为 false 使用浏览器视口作为根元素 + /// The element that is used as the viewport for checking visibility of the target. Must be the ancestor of the target. Defaults to the browser viewport if value is false. Default value is false /// [Parameter] public bool UseElementViewport { get; set; } diff --git a/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.js b/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.js index ddb8ff6b977..f35dce9229f 100644 --- a/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.js +++ b/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.js @@ -8,7 +8,7 @@ export function init(id, invoke, options) { const items = [...el.querySelectorAll(".bb-intersection-observer-item")]; - if (options.useElementViewport === false) { + if (options.useElementViewport === true) { options.root = el; } if (options.threshold && options.threshold.indexOf(' ') > 0) { diff --git a/src/BootstrapBlazor/Components/IntersectionObserver/LoadMore.razor b/src/BootstrapBlazor/Components/IntersectionObserver/LoadMore.razor new file mode 100644 index 00000000000..13a44835e20 --- /dev/null +++ b/src/BootstrapBlazor/Components/IntersectionObserver/LoadMore.razor @@ -0,0 +1,28 @@ +@namespace BootstrapBlazor.Components +@inherits BootstrapModuleComponentBase + + + +
+ @if (CanLoading) + { + if (LoadingTemplate != null) + { + @LoadingTemplate + } + else + { + + } + } + else if (NoMoreTemplate != null) + { + @NoMoreTemplate + } + else if (!string.IsNullOrEmpty(NoMoreText)) + { + @NoMoreText + } +
+
+
diff --git a/src/BootstrapBlazor/Components/IntersectionObserver/LoadMore.razor.cs b/src/BootstrapBlazor/Components/IntersectionObserver/LoadMore.razor.cs new file mode 100644 index 00000000000..660299ba6d3 --- /dev/null +++ b/src/BootstrapBlazor/Components/IntersectionObserver/LoadMore.razor.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using Microsoft.Extensions.Localization; + +namespace BootstrapBlazor.Components; + +/// +/// 加载更多组件 +/// +public partial class LoadMore +{ + /// + /// 获得/设置 触底元素触发 阈值 默认为 1 + /// + [Parameter] + public string Threshold { get; set; } = "1"; + + /// + /// 获得/设置 触底回调方法 为 true 时才触发此回调方法 + /// + [Parameter] public Func? OnLoadMoreAsync { get; set; } + + /// + /// 获得/设置 是否可以加载更多数据 默认为 true + /// + [Parameter] + public bool CanLoading { get; set; } = true; + + /// + /// 获得/设置 加载更多模板 默认 null + /// + [Parameter] + public RenderFragment? LoadingTemplate { get; set; } + + /// + /// 获得/设置 没有更多数据提示信息 默认为 null 读取资源文件中的预设值 + /// + [Parameter] + public string? NoMoreText { get; set; } + + /// + /// 获得/设置 没有更多数据时显示的模板 默认为 null + /// + [Parameter] + public RenderFragment? NoMoreTemplate { get; set; } + + [Inject, NotNull] + private IStringLocalizer? Localizer { get; set; } + + /// + /// + /// + protected override void OnParametersSet() + { + base.OnParametersSet(); + + NoMoreText ??= Localizer[nameof(NoMoreText)]; + } + + private async Task OnIntersecting(IntersectionObserverEntry entry) + { + if (entry.IsIntersecting && CanLoading && OnLoadMoreAsync != null) + { + await OnLoadMoreAsync(); + } + } +} diff --git a/src/BootstrapBlazor/Components/IntersectionObserver/LoadMore.razor.scss b/src/BootstrapBlazor/Components/IntersectionObserver/LoadMore.razor.scss new file mode 100644 index 00000000000..33922c4e989 --- /dev/null +++ b/src/BootstrapBlazor/Components/IntersectionObserver/LoadMore.razor.scss @@ -0,0 +1,14 @@ +.bb-intersection-observer { + --bb-intersection-observer-loading-bg: #{$bb-intersection-observer-loading-bg}; + --bb-intersection-observer-loading-color: #{$bb-intersection-observer-loading-color}; + --bb-intersection-observer-loading-padding: #{$bb-intersection-observer-loading-padding}; + + .bb-intersection-loading { + display: flex; + justify-content: center; + align-items: center; + background-color: var(--bb-intersection-observer-loading-bg); + color: var(--bb-intersection-observer-loading-color); + padding: var(--bb-intersection-observer-loading-padding) + } +} diff --git a/src/BootstrapBlazor/Locales/en.json b/src/BootstrapBlazor/Locales/en.json index f68bd3852cb..ac0c904b922 100644 --- a/src/BootstrapBlazor/Locales/en.json +++ b/src/BootstrapBlazor/Locales/en.json @@ -395,5 +395,8 @@ "NetworkType": "NetworkType", "Downlink": "Downlink", "RTT": "RTT" + }, + "BootstrapBlazor.Components.LoadMore": { + "NoMoreText": "No More Data" } } diff --git a/src/BootstrapBlazor/Locales/zh.json b/src/BootstrapBlazor/Locales/zh.json index 88d10375a7a..4de8211c777 100644 --- a/src/BootstrapBlazor/Locales/zh.json +++ b/src/BootstrapBlazor/Locales/zh.json @@ -395,5 +395,8 @@ "NetworkType": "网络类型", "Downlink": "下载速度", "RTT": "响应时间" + }, + "BootstrapBlazor.Components.LoadMore": { + "NoMoreText": "没有更多数据了" } } diff --git a/src/BootstrapBlazor/wwwroot/scss/components.scss b/src/BootstrapBlazor/wwwroot/scss/components.scss index bcbd0818c41..39a3ee3c711 100644 --- a/src/BootstrapBlazor/wwwroot/scss/components.scss +++ b/src/BootstrapBlazor/wwwroot/scss/components.scss @@ -58,6 +58,7 @@ @import "../../Components/Input/BootstrapInputGroup.razor.scss"; @import "../../Components/Input/FloatingLabel.razor.scss"; @import "../../Components/Input/OtpInput.razor.scss"; +@import "../../Components/IntersectionObserver/LoadMore.razor.scss"; @import "../../Components/IpAddress/IpAddress.razor.scss"; @import "../../Components/Layout/Layout.razor.scss"; @import "../../Components/Layout/LayoutSplitBar.razor.scss"; diff --git a/src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss b/src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss index ecb160ce76c..ff0c8283b69 100644 --- a/src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss +++ b/src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss @@ -311,6 +311,11 @@ $bb-svg-icon-width: 12px; $bb-viewer-button-bg: #606266; $bb-viewer-border-radius: 50%; +// IntersectionObserver +$bb-intersection-observer-loading-bg: var(--bs-body-bg); +$bb-intersection-observer-loading-color: var(--bs-body-color); +$bb-intersection-observer-loading-padding: 0.5rem; + // Ip Address $bb-ip-cell-max-width: 30px; diff --git a/test/UnitTest/Components/IntersectionObserverTest.cs b/test/UnitTest/Components/IntersectionObserverTest.cs index 7374f2a8017..112cd1dd072 100644 --- a/test/UnitTest/Components/IntersectionObserverTest.cs +++ b/test/UnitTest/Components/IntersectionObserverTest.cs @@ -62,4 +62,59 @@ await cut.InvokeAsync(() => cut.Instance.TriggerIntersecting(new IntersectionObs })); Assert.Equal(10, count); } + + [Fact] + public async Task LoadMore_Ok() + { + var loading = false; + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.Threshold, "1"); + pb.Add(a => a.CanLoading, true); + pb.Add(a => a.OnLoadMoreAsync, () => + { + loading = true; + return Task.CompletedTask; + }); + }); + cut.Contains("
Loading...
"); + + // trigger intersecting + var observerItem = cut.FindComponent(); + await cut.InvokeAsync(() => observerItem.Instance.TriggerIntersecting(new IntersectionObserverEntry() + { + IsIntersecting = true, + Index = 10, + Time = 100.00, + IntersectionRatio = 0.5f + })); + Assert.True(loading); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.LoadingTemplate, new RenderFragment(builder => builder.AddContent(0, "loading template"))); + }); + cut.Contains("loading template"); + + loading = false; + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.CanLoading, false); + }); + observerItem = cut.FindComponent(); + await cut.InvokeAsync(() => observerItem.Instance.TriggerIntersecting(new IntersectionObserverEntry() + { + IsIntersecting = true, + Index = 10, + Time = 100.00, + IntersectionRatio = 0.5f + })); + Assert.False(loading); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.NoMoreTemplate, new RenderFragment(builder => builder.AddContent(0, "没有更多数据模板"))); + }); + cut.Contains("没有更多数据模板"); + } }