Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@
@inject IStringLocalizer<IntersectionObservers> Localizer
@inject IOptionsMonitor<WebsiteOptions> WebsiteOption

<HeadContent>
<style>
.bb-video video {
width: 256px;
}

@@media (min-width: 767.99px) {
.bb-video video {
width: 350px;
}
}
</style>
</HeadContent>

<h3>@Localizer["IntersectionObserverTitle"]</h3>

<h4>@Localizer["IntersectionObserverDescription"]</h4>
Expand Down Expand Up @@ -56,23 +70,10 @@
<DemoBlock Title="@Localizer["IntersectionObserverVisibleTitle"]"
Introduction="@Localizer["IntersectionObserverVisibleIntro"]"
Name="Visible">
<HeadContent>
<style>
.bb-video video {
width: 256px;
}

@@media (min-width: 767.99px) {
.bb-video video {
width: 350px;
}
}
</style>
</HeadContent>
<section ignore>
<p>@((MarkupString)Localizer["IntersectionObserverVisibleDesc"].Value)</p>
<div class="text-center @_textColorString">@_videoStateString</div>
</section>
<p class="text-center @_textColorString">@_videoStateString</p>
<IntersectionObserver OnIntersecting="OnVisibleChanged" Threshold="1" AutoUnobserveWhenIntersection="false">
<div class="bb-video-demo scroll">
<div class="bb-video">
Expand All @@ -87,8 +88,10 @@
<DemoBlock Title="@Localizer["IntersectionObserverThresholdTitle"]"
Introduction="@Localizer["IntersectionObserverThresholdIntro"]"
Name="Threshold">
<section ignore><p>@((MarkupString)Localizer["IntersectionObserverThresholdDesc"].Value)</p></section>
<p class="text-center">@_thresholdValueString</p>
<section ignore>
<p>@((MarkupString)Localizer["IntersectionObserverThresholdDesc"].Value)</p>
<div class="text-center">@_thresholdValueString</div>
</section>
<IntersectionObserver OnIntersecting="OnThresholdChanged" Threshold="0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1" AutoUnobserveWhenIntersection="false">
<div class="bb-list-load scroll" style="height: 200px;">
<div class="d-flex" style="height: 600px; justify-content: center; align-items: center;">
Expand All @@ -100,4 +103,24 @@
</IntersectionObserver>
</DemoBlock>

<DemoBlock Title="@Localizer["LoadMoreTitle"]"
Introduction="@Localizer["LoadMoreIntro"]"
Name="LoadMoreComponent">
<section ignore>
<div>@((MarkupString)Localizer["LoadMoreDesc"].Value)</div>
</section>
<div style="height: 400px; overflow: auto;">
<div class="bb-list-demo">
@foreach (var image in _items)
{
<div class="bb-list-item">
<img src="@image" />
</div>
}
</div>
<LoadMore OnLoadMoreAsync="OnLoadMoreItemAsync" CanLoading="_canLoading">
</LoadMore>
</div>
</DemoBlock>

<AttributeTable Items="@GetAttributes()" />
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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";

Expand Down
5 changes: 4 additions & 1 deletion src/BootstrapBlazor.Server/Locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>LoadMore</code> component parameter <code>IsLoading</code> to control the loading state, the <code>OnLoadMoreAsync</code> callback method loads more data",
"LoadMoreDesc": "In this example, the loading indicator is displayed by setting <code>CanLoading</code> to <code>true</code>, and the <b>No More Data</b> prompt text is displayed by setting it to <code>false</code> after loading is complete. The UI for loading more data can be customized through <code>LoadingTemplate</code>, the UI displayed when there is no more data can be customized through <code>NoMoreTemplate</code>, and the indicator text displayed when there are no more add-ons can be set through the <code>NoMoreText</code> parameter."
},
"BootstrapBlazor.Server.Components.Samples.SortableLists": {
"SortableListTitle": "SortableList",
Expand Down
5 changes: 4 additions & 1 deletion src/BootstrapBlazor.Server/Locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -6855,7 +6855,10 @@
"AttributeAutoUnobserveWhenIntersection": "元素可见时是否自动取消观察",
"AttributeAutoUnobserveWhenNotIntersection": "元素不可见时是否自动取消观察",
"AttributeOnIntersectingAsync": "可见回调方法",
"AttributeChildContent": "子组件"
"AttributeChildContent": "子组件",
"LoadMoreTitle": "LoadMore 组件",
"LoadMoreIntro": "通过设置 <code>LoadMore</code> 组件参数 <code>CanLoading</code> 控制加载状态,<code>OnLoadMoreAsync</code> 回调方法加载更多数据",
"LoadMoreDesc": "本例中通过设置 <code>CanLoading</code> 为 <code>true</code> 显示加载指示符,加载完成后设置为 <code>false</code> 显示 <b>没有更多数据</b> 提示文本,可以通过 <code>LoadingTemplate</code> 自定义加载更多的 UI,通过 <code>NoMoreTemplate</code> 自定义没有更多数据时显示的 UI,可以通过 <code>NoMoreText</code> 参数设置没有更多加载项时显示的指示文本"
},
"BootstrapBlazor.Server.Components.Samples.SortableLists": {
"SortableListTitle": "SortableList 拖拽组件",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone


namespace BootstrapBlazor.Components;

/// <summary>
Expand All @@ -12,7 +11,8 @@ namespace BootstrapBlazor.Components;
public partial class IntersectionObserver
{
/// <summary>
/// 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 使用浏览器视口作为根元素
/// <para>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</para>
/// </summary>
[Parameter]
public bool UseElementViewport { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
28 changes: 28 additions & 0 deletions src/BootstrapBlazor/Components/IntersectionObserver/LoadMore.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@namespace BootstrapBlazor.Components
@inherits BootstrapModuleComponentBase

<IntersectionObserver OnIntersecting="OnIntersecting" Threshold="@Threshold" AutoUnobserveWhenIntersection="false">
<IntersectionObserverItem>
<div class="bb-intersection-loading">
@if (CanLoading)
{
if (LoadingTemplate != null)
{
@LoadingTemplate
}
else
{
<Spinner></Spinner>
}
}
else if (NoMoreTemplate != null)
{
@NoMoreTemplate
}
else if (!string.IsNullOrEmpty(NoMoreText))
{
@NoMoreText
}
</div>
</IntersectionObserverItem>
</IntersectionObserver>
Original file line number Diff line number Diff line change
@@ -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([email protected]) Website: https://www.blazor.zone

using Microsoft.Extensions.Localization;

namespace BootstrapBlazor.Components;

/// <summary>
/// 加载更多组件
/// </summary>
public partial class LoadMore
{
/// <summary>
/// 获得/设置 触底元素触发 <see cref="OnLoadMoreAsync"/> 阈值 默认为 1
/// </summary>
[Parameter]
public string Threshold { get; set; } = "1";

/// <summary>
/// 获得/设置 触底回调方法 <see cref="CanLoading"/> 为 true 时才触发此回调方法
/// </summary>
[Parameter] public Func<Task>? OnLoadMoreAsync { get; set; }

/// <summary>
/// 获得/设置 是否可以加载更多数据 默认为 true
/// </summary>
[Parameter]
public bool CanLoading { get; set; } = true;

/// <summary>
/// 获得/设置 加载更多模板 默认 null
/// </summary>
[Parameter]
public RenderFragment? LoadingTemplate { get; set; }

/// <summary>
/// 获得/设置 没有更多数据提示信息 默认为 null 读取资源文件中的预设值
/// </summary>
[Parameter]
public string? NoMoreText { get; set; }

/// <summary>
/// 获得/设置 没有更多数据时显示的模板 默认为 null
/// </summary>
[Parameter]
public RenderFragment? NoMoreTemplate { get; set; }

[Inject, NotNull]
private IStringLocalizer<LoadMore>? Localizer { get; set; }

/// <summary>
/// <inheritdoc/>
/// </summary>
protected override void OnParametersSet()
{
base.OnParametersSet();

NoMoreText ??= Localizer[nameof(NoMoreText)];
}

private async Task OnIntersecting(IntersectionObserverEntry entry)
{
if (entry.IsIntersecting && CanLoading && OnLoadMoreAsync != null)
{
await OnLoadMoreAsync();
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
3 changes: 3 additions & 0 deletions src/BootstrapBlazor/Locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -395,5 +395,8 @@
"NetworkType": "NetworkType",
"Downlink": "Downlink",
"RTT": "RTT"
},
"BootstrapBlazor.Components.LoadMore": {
"NoMoreText": "No More Data"
}
}
3 changes: 3 additions & 0 deletions src/BootstrapBlazor/Locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -395,5 +395,8 @@
"NetworkType": "网络类型",
"Downlink": "下载速度",
"RTT": "响应时间"
},
"BootstrapBlazor.Components.LoadMore": {
"NoMoreText": "没有更多数据了"
}
}
1 change: 1 addition & 0 deletions src/BootstrapBlazor/wwwroot/scss/components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
5 changes: 5 additions & 0 deletions src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
55 changes: 55 additions & 0 deletions test/UnitTest/Components/IntersectionObserverTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<LoadMore>(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("<div class=\"bb-intersection-observer-item\"><div class=\"bb-intersection-loading\"><div class=\"spinner spinner-border\" role=\"status\"><span class=\"visually-hidden\">Loading...</span></div></div></div>");

// trigger intersecting
var observerItem = cut.FindComponent<IntersectionObserver>();
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<IntersectionObserver>();
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("没有更多数据模板");
}
}
Loading