refactor(IntersectionObserver): add AutoUnobserverWhenIntersection parameter (#3990)

* refactor: 更新 OnIntersecting 回调方法参数

* doc: 更新示例

* doc: 增加本地化

* test: 更新单元测试

* doc: 更新示例

* doc: 更新示例组件

* refactor: 更正脚本方法名称

* refactor: 更新逻辑

* refactor: 更新参数

* doc: 更新示例

* doc: 更新示例增加视频状态文字

* doc: 更新本地化文本内容

* test: 更新单元测试

* doc: 格式化文档

* doc: 更新示例

* doc: 增加示例

* test: 更新单元测试

* test: 更新单元测试

* test: 更新单元测试
This commit is contained in:
Argo Zhang 2024-08-06 15:53:15 +08:00 committed by GitHub
parent 7126ee9a33
commit f9f13536c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 238 additions and 37 deletions

View File

@ -0,0 +1,4 @@
@inherits WebSiteModuleComponentBase
@attribute [JSModuleAutoLoader("Components/VideoDemo.razor.js", AutoInvokeInit = false, AutoInvokeDispose = false)]
<video id="@Id" controls preload="auto" autoplay width="350" src="https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4"></video>

View File

@ -0,0 +1,23 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
namespace BootstrapBlazor.Server.Components.Components;
/// <summary>
/// VideoDemo 组件
/// </summary>
public partial class VideoDemo
{
/// <summary>
/// 开始播放方法
/// </summary>
/// <returns></returns>
public Task Play() => InvokeVoidAsync("play", Id);
/// <summary>
/// 暂停方法
/// </summary>
/// <returns></returns>
public Task Pause() => InvokeVoidAsync("pause", Id);
}

View File

@ -0,0 +1,9 @@
export function play(id) {
const video = document.getElementById(id);
video.play();
}
export function pause(id) {
const video = document.getElementById(id);
video.pause();
}

View File

@ -15,7 +15,7 @@
{ {
<IntersectionObserverItem> <IntersectionObserverItem>
<div class="bb-list-item"> <div class="bb-list-item">
<img src="@image"/> <img src="@image" />
</div> </div>
</IntersectionObserverItem> </IntersectionObserverItem>
} }
@ -33,13 +33,13 @@
<li>@((MarkupString)Localizer["IntersectionObserverLoadDesc3"].Value)</li> <li>@((MarkupString)Localizer["IntersectionObserverLoadDesc3"].Value)</li>
</ul> </ul>
</section> </section>
<IntersectionObserver OnIntersecting="OnLoadMoreAsync" Threshold="1" AutoUnobserve="false"> <IntersectionObserver OnIntersecting="OnLoadMoreAsync" Threshold="1" AutoUnobserveWhenIntersection="false">
<div class="bb-list-load scroll"> <div class="bb-list-load scroll">
<div class="bb-list-demo"> <div class="bb-list-demo">
@foreach (var image in _items) @foreach (var image in _items)
{ {
<div class="bb-list-item"> <div class="bb-list-item">
<img src="@image"/> <img src="@image" />
</div> </div>
} }
</div> </div>
@ -52,4 +52,36 @@
</IntersectionObserver> </IntersectionObserver>
</DemoBlock> </DemoBlock>
<AttributeTable Items="@GetAttributes()"/> <DemoBlock Title="@Localizer["IntersectionObserverVisibleTitle"]"
Introduction="@Localizer["IntersectionObserverVisibleIntro"]"
Name="Visible">
<section ignore><p>@((MarkupString)Localizer["IntersectionObserverVisibleDesc"].Value)</p></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">
<IntersectionObserverItem>
<VideoDemo @ref="_video"></VideoDemo>
</IntersectionObserverItem>
</div>
</div>
</IntersectionObserver>
</DemoBlock>
<DemoBlock Title="@Localizer["IntersectionObserverThresholdTitle"]"
Introduction="@Localizer["IntersectionObserverThresholdIntro"]"
Name="Threshold">
<section ignore><p>@((MarkupString)Localizer["IntersectionObserverThresholdDesc"].Value)</p></section>
<p class="text-center">@_thresholdValueString</p>
<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;">
<IntersectionObserverItem>
<div class="bb-list-item bg-info"></div>
</IntersectionObserverItem>
</div>
</div>
</IntersectionObserver>
</DemoBlock>
<AttributeTable Items="@GetAttributes()" />

View File

@ -13,6 +13,8 @@ public partial class IntersectionObservers
private List<string> _items = default!; private List<string> _items = default!;
private VideoDemo _video = default!;
/// <summary> /// <summary>
/// <inheritdoc/> /// <inheritdoc/>
/// </summary> /// </summary>
@ -24,20 +26,58 @@ public partial class IntersectionObservers
_items = Enumerable.Range(1, 20).Select(i => $"https://picsum.photos/160/160?random={i}").ToList(); _items = Enumerable.Range(1, 20).Select(i => $"https://picsum.photos/160/160?random={i}").ToList();
} }
private Task OnIntersectingAsync(int index) private Task OnIntersectingAsync(IntersectionObserverEntry entry)
{ {
_images[index] = GetImageUrl(index); if (entry.IsIntersecting)
StateHasChanged(); {
_images[entry.Index] = GetImageUrl(entry.Index);
StateHasChanged();
}
return Task.CompletedTask; return Task.CompletedTask;
} }
private async Task OnLoadMoreAsync(int index) private async Task OnLoadMoreAsync(IntersectionObserverEntry entry)
{ {
await Task.Delay(1000); if (entry.IsIntersecting)
_items.AddRange(Enumerable.Range(_items.Count + 1, 20).Select(i => $"https://picsum.photos/160/160?random={i}")); {
await Task.Delay(1000);
_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";
private async Task OnVisibleChanged(IntersectionObserverEntry entry)
{
if (entry.IsIntersecting)
{
_videoStateString = Localizer["IntersectionObserverVisiblePlayLabel"];
_textColorString = "text-success";
await _video.Play();
}
else
{
_videoStateString = Localizer["IntersectionObserverVisiblePauseLabel"];
_textColorString = "text-danger";
await _video.Pause();
}
StateHasChanged(); StateHasChanged();
} }
private string? _thresholdValueString;
private Task OnThresholdChanged(IntersectionObserverEntry entry)
{
_thresholdValueString = entry.IntersectionRatio.ToString("P");
StateHasChanged();
return Task.CompletedTask;
}
private static string GetImageUrl(int index) => $"https://picsum.photos/160/160?random={index}"; private static string GetImageUrl(int index) => $"https://picsum.photos/160/160?random={index}";
private AttributeItem[] GetAttributes() => private AttributeItem[] GetAttributes() =>
@ -68,17 +108,25 @@ public partial class IntersectionObservers
}, },
new() new()
{ {
Name = "AutoUnobserve", Name = nameof(IntersectionObserver.AutoUnobserveWhenIntersection),
Description = Localizer["AttributeAutoUnobserve"], Description = Localizer["AttributeAutoUnobserveWhenIntersection"],
Type = "bool", Type = "bool",
ValueList = "true|false", ValueList = "true|false",
DefaultValue = "true" DefaultValue = "true"
}, },
new() new()
{ {
Name = "OnIntersectingAsync", Name = nameof(IntersectionObserver.AutoUnobserveWhenNotIntersection),
Description = Localizer["AttributeAutoUnobserveWhenNotIntersection"],
Type = "bool",
ValueList = "true|false",
DefaultValue = "false"
},
new()
{
Name = "OnIntersecting",
Description = Localizer["AttributeOnIntersectingAsync"], Description = Localizer["AttributeOnIntersectingAsync"],
Type = "Func<int, Task>", Type = "Func<IntersectionObserverEntry, Task>",
ValueList = " — ", ValueList = " — ",
DefaultValue = " — " DefaultValue = " — "
}, },

View File

@ -4,14 +4,14 @@
border: var(--bs-border-width) solid var(--bs-border-color); border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius); border-radius: var(--bs-border-radius);
padding: .25rem; padding: .25rem;
height: 500px; height: 400px;
} }
.bb-list-load { .bb-list-load {
border: var(--bs-border-width) solid var(--bs-border-color); border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius); border-radius: var(--bs-border-radius);
padding: .25rem; padding: .25rem;
height: 500px; height: 400px;
} }
.bb-list-demo { .bb-list-demo {
@ -40,3 +40,13 @@
opacity: 0.3; opacity: 0.3;
padding: .5rem 0; padding: .5rem 0;
} }
.bb-video-demo {
height: 260px;
}
.bb-video {
height: 600px;
text-align: center;
margin-top: 160px;
}

View File

@ -6600,11 +6600,20 @@
"IntersectionObserverLoadDesc1": "By setting <code>Threshold=\"1.0\"</code> to constrain the loading indicator to be fully visible, perform the load more action", "IntersectionObserverLoadDesc1": "By setting <code>Threshold=\"1.0\"</code> to constrain the loading indicator to be fully visible, perform the load more action",
"IntersectionObserverLoadDesc2": "Ensure that the loading indicator visibility is always checked by setting <code>AutoUnobserve=\"false\"</code> to prevent automatic removal of the check once it becomes visible", "IntersectionObserverLoadDesc2": "Ensure that the loading indicator visibility is always checked by setting <code>AutoUnobserve=\"false\"</code> to prevent automatic removal of the check once it becomes visible",
"IntersectionObserverLoadDesc3": "Simulate network delay of 1s by setting the <code>OnLoadMoreAsync</code> method", "IntersectionObserverLoadDesc3": "Simulate network delay of 1s by setting the <code>OnLoadMoreAsync</code> method",
"IntersectionObserverVisibleTitle": "Threshold",
"IntersectionObserverVisibleIntro": "By setting <code>Threshold</code> specifying a ratio of intersection area to total bounding box area for the observed target",
"IntersectionObserverVisibleDesc": "In this example, <code>Threshold=\"1\"</code> is set, which means that the trigger condition is to play when the video is fully visible, otherwise it will automatically pause",
"IntersectionObserverVisiblePauseLabel": "Video Paused",
"IntersectionObserverVisiblePlayLabel": "Video Playing",
"IntersectionObserverThresholdTitle": "IntersectionRatio",
"IntersectionObserverThresholdIntro": "After setting <code>Threshold</code>, the intersection ratio between the observed element and the root element is obtained through the callback parameter <code>IntersectionObserveEntry</code> attribute <code>IntersectionRatio</code>",
"IntersectionObserverThresholdDesc": "Drag the scroll bar of the <code>div</code> below to observe data changes",
"AttributeUseElementViewport": "The element that is used as the viewport for checking visibility of the target", "AttributeUseElementViewport": "The element that is used as the viewport for checking visibility of the target",
"AttributeRootMargin": "Margin around the root. Can have values similar to the CSS margin property", "AttributeRootMargin": "Margin around the root. Can have values similar to the CSS margin property",
"AttributeThreshold": "Either a single number or an array of numbers which indicate at what percentage of the target's visibility the observer's callback should be executed", "AttributeThreshold": "Either a single number or an array of numbers which indicate at what percentage of the target's visibility the observer's callback should be executed",
"AttributeAutoUnobserve": "Whether to automatically cancel the observation", "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", "AttributeOnIntersectingAsync": "The callback when intersecting",
"AttributeChildContent": "Child componenet" "AttributeChildContent": "Child component"
} }
} }

View File

@ -6600,10 +6600,19 @@
"IntersectionObserverLoadDesc1": "通过设置 <code>Threshold=\"1.0\"</code> 约束正在加载指示符完全可见时,执行加载更多操作", "IntersectionObserverLoadDesc1": "通过设置 <code>Threshold=\"1.0\"</code> 约束正在加载指示符完全可见时,执行加载更多操作",
"IntersectionObserverLoadDesc2": "通过设置 <code>AutoUnobserve=\"false\"</code> 保证始终检测正在加载指示符可见性,防止可见后自动移除检测", "IntersectionObserverLoadDesc2": "通过设置 <code>AutoUnobserve=\"false\"</code> 保证始终检测正在加载指示符可见性,防止可见后自动移除检测",
"IntersectionObserverLoadDesc3": "通过设置 <code>OnLoadMoreAsync</code> 方法模拟网络延时 1s", "IntersectionObserverLoadDesc3": "通过设置 <code>OnLoadMoreAsync</code> 方法模拟网络延时 1s",
"IntersectionObserverVisibleTitle": "触发阈值",
"IntersectionObserverVisibleIntro": "通过设置 <code>Threshold</code> 调整触发条件",
"IntersectionObserverVisibleDesc": "本例中设置 <code>Threshold=\"1\"</code> 即触发条件为视频完全可见时播放,否则自动暂停",
"IntersectionObserverVisiblePauseLabel": "视频已暂停",
"IntersectionObserverVisiblePlayLabel": "视频已开始播放",
"IntersectionObserverThresholdTitle": "阈值变化观察",
"IntersectionObserverThresholdIntro": "通过设置 <code>Threshold</code> 后通过回调参数 <code>IntersectionObserveEntry</code> 属性 <code>IntersectionRatio</code> 获得观察元素与根元素交叉率",
"IntersectionObserverThresholdDesc": "拖动下方 <code>div</code> 滚动条,可观察数据变化",
"AttributeUseElementViewport": "是否使用当前元素作为视窗", "AttributeUseElementViewport": "是否使用当前元素作为视窗",
"AttributeRootMargin": "根元素边距", "AttributeRootMargin": "根元素边距",
"AttributeThreshold": "可见性阈值", "AttributeThreshold": "可见性阈值",
"AttributeAutoUnobserve": "是否自动取消观察", "AttributeAutoUnobserveWhenIntersection": "元素可见时是否自动取消观察",
"AttributeAutoUnobserveWhenNotIntersection": "元素不可见时是否自动取消观察",
"AttributeOnIntersectingAsync": "可见回调方法", "AttributeOnIntersectingAsync": "可见回调方法",
"AttributeChildContent": "子组件" "AttributeChildContent": "子组件"
} }

View File

@ -29,16 +29,22 @@ public partial class IntersectionObserver
public string? Threshold { get; set; } public string? Threshold { get; set; }
/// <summary> /// <summary>
/// 获得/设置 是否自动取消观察 默认 true 可见后自动取消观察提高性能 /// 获得/设置 可见后是否自动取消观察 默认 true 可见后自动取消观察提高性能
/// </summary> /// </summary>
[Parameter] [Parameter]
public bool AutoUnobserve { get; set; } = true; public bool AutoUnobserveWhenIntersection { get; set; } = true;
/// <summary>
/// 获得/设置 不可见后是否自动取消观察 默认 false 不可见后自动取消观察提高性能
/// </summary>
[Parameter]
public bool AutoUnobserveWhenNotIntersection { get; set; }
/// <summary> /// <summary>
/// 获得/设置 已经交叉回调方法 /// 获得/设置 已经交叉回调方法
/// </summary> /// </summary>
[Parameter] [Parameter]
public Func<int, Task>? OnIntersecting { get; set; } public Func<IntersectionObserverEntry, Task>? OnIntersecting { get; set; }
/// <summary> /// <summary>
/// 获得/设置 子组件 /// 获得/设置 子组件
@ -54,19 +60,19 @@ public partial class IntersectionObserver
/// <inheritdoc/> /// <inheritdoc/>
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new { UseElementViewport, RootMargin, Threshold, AutoUnobserve, Callback = nameof(TriggerIntersecting) }); protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new { UseElementViewport, RootMargin, Threshold, AutoUnobserveWhenIntersection, AutoUnobserveWhenNotIntersection, Callback = nameof(TriggerIntersecting) });
/// <summary> /// <summary>
/// 交叉检测回调方法 由 JavaScript 调用 /// 交叉检测回调方法 由 JavaScript 调用
/// </summary> /// </summary>
/// <param name="index"></param> /// <param name="entry"><see cref="IntersectionObserverEntry"/> 实例</param>
/// <returns></returns> /// <returns></returns>
[JSInvokable] [JSInvokable]
public async Task TriggerIntersecting(int index) public async Task TriggerIntersecting(IntersectionObserverEntry entry)
{ {
if (OnIntersecting != null) if (OnIntersecting != null)
{ {
await OnIntersecting(index); await OnIntersecting(entry);
} }
} }
} }

View File

@ -11,18 +11,27 @@ export function init(id, invoke, options) {
if (options.useElementViewport === false) { if (options.useElementViewport === false) {
options.root = el; options.root = el;
} }
const { root, rootMargin, threshold, autoUnobserve, callback } = options; if (options.threshold && options.threshold.indexOf(' ') > 0) {
options.threshold = options.threshold.split(' ');
}
const { root, rootMargin, threshold, autoUnobserveWhenIntersection, autoUnobserveWhenNotIntersection, callback } = options;
const option = { root, rootMargin: rootMargin ?? '0px 0px 0px 0px', threshold: threshold ?? 0 }; const option = { root, rootMargin: rootMargin ?? '0px 0px 0px 0px', threshold: threshold ?? 0 };
const observer = new IntersectionObserver(entries => { const observer = new IntersectionObserver(entries => {
entries.forEach(entry => { entries.forEach(entry => {
if (entry.isIntersecting) { if(entry.isIntersecting && autoUnobserveWhenIntersection) {
if (autoUnobserve) { observer.unobserve(entry.target);
observer.unobserve(entry.target);
}
const index = items.indexOf(entry.target);
invoke.invokeMethodAsync(callback, index);
} }
else if(!entry.isIntersecting && autoUnobserveWhenNotIntersection) {
observer.unobserve(entry.target);
}
const index = items.indexOf(entry.target);
invoke.invokeMethodAsync(callback, {
isIntersecting: entry.isIntersecting,
index,
time: entry.time,
intersectionRatio: entry.intersectionRatio
});
}); });
}, option); }, option);

View File

@ -0,0 +1,32 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
namespace BootstrapBlazor.Components;
/// <summary>
/// 交叉检测项实例类
/// </summary>
public class IntersectionObserverEntry
{
/// <summary>
/// 获得/设置 检测项与根元素交叉比率 0 - 1 之间
/// </summary>
public float IntersectionRatio { get; set; }
/// <summary>
/// 获得/设置 是否正在交叉
/// </summary>
public bool IsIntersecting { get; set; }
/// <summary>
/// 获得/设置 当前项索引
/// </summary>
public int Index { get; set; }
/// <summary>
/// 获得/设置 与文档创建时间差
/// </summary>
public double Time { get; set; }
}

View File

@ -17,7 +17,8 @@ public class IntersectionObserverTest : BootstrapBlazorTestBase
pb.Add(a => a.UseElementViewport, false); pb.Add(a => a.UseElementViewport, false);
pb.Add(a => a.RootMargin, "10px 20px 30px 40px"); pb.Add(a => a.RootMargin, "10px 20px 30px 40px");
pb.Add(a => a.Threshold, "0.5"); pb.Add(a => a.Threshold, "0.5");
pb.Add(a => a.AutoUnobserve, true); pb.Add(a => a.AutoUnobserveWhenIntersection, false);
pb.Add(a => a.AutoUnobserveWhenNotIntersection, false);
pb.Add(a => a.ChildContent, builder => pb.Add(a => a.ChildContent, builder =>
{ {
builder.OpenComponent<IntersectionObserverItem>(0); builder.OpenComponent<IntersectionObserverItem>(0);
@ -41,14 +42,23 @@ public class IntersectionObserverTest : BootstrapBlazorTestBase
builder.AddAttribute(1, "ChildContent", new RenderFragment(builder => builder.AddContent(0, "observer-item"))); builder.AddAttribute(1, "ChildContent", new RenderFragment(builder => builder.AddContent(0, "observer-item")));
builder.CloseComponent(); builder.CloseComponent();
}); });
pb.Add(a => a.OnIntersecting, index => pb.Add(a => a.OnIntersecting, entry =>
{ {
count = index; if (entry.IsIntersecting && entry.Time == 100 && entry.IntersectionRatio == 0.5f)
{
count = entry.Index;
}
return Task.CompletedTask; return Task.CompletedTask;
}); });
}); });
await cut.InvokeAsync(() => cut.Instance.TriggerIntersecting(10)); await cut.InvokeAsync(() => cut.Instance.TriggerIntersecting(new IntersectionObserverEntry()
{
IsIntersecting = true,
Index = 10,
Time = 100.00,
IntersectionRatio = 0.5f
}));
Assert.Equal(10, count); Assert.Equal(10, count);
} }
} }