From f9f13536c9c07a4a6ede02e8464771772c9e2949 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 6 Aug 2024 15:53:15 +0800 Subject: [PATCH] refactor(IntersectionObserver): add AutoUnobserverWhenIntersection parameter (#3990) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 更新 OnIntersecting 回调方法参数 * doc: 更新示例 * doc: 增加本地化 * test: 更新单元测试 * doc: 更新示例 * doc: 更新示例组件 * refactor: 更正脚本方法名称 * refactor: 更新逻辑 * refactor: 更新参数 * doc: 更新示例 * doc: 更新示例增加视频状态文字 * doc: 更新本地化文本内容 * test: 更新单元测试 * doc: 格式化文档 * doc: 更新示例 * doc: 增加示例 * test: 更新单元测试 * test: 更新单元测试 * test: 更新单元测试 --- .../Components/Components/VideoDemo.razor | 4 ++ .../Components/Components/VideoDemo.razor.cs | 23 +++++++ .../Components/Components/VideoDemo.razor.js | 9 +++ .../Samples/IntersectionObservers.razor | 40 +++++++++-- .../Samples/IntersectionObservers.razor.cs | 68 ++++++++++++++++--- .../Samples/IntersectionObservers.razor.css | 14 +++- src/BootstrapBlazor.Server/Locales/en-US.json | 13 +++- src/BootstrapBlazor.Server/Locales/zh-CN.json | 11 ++- .../IntersectionObserver.razor.cs | 20 ++++-- .../IntersectionObserver.razor.js | 23 +++++-- .../IntersectionObserverEntry.cs | 32 +++++++++ .../Components/IntersectionObserverTest.cs | 18 +++-- 12 files changed, 238 insertions(+), 37 deletions(-) create mode 100644 src/BootstrapBlazor.Server/Components/Components/VideoDemo.razor create mode 100644 src/BootstrapBlazor.Server/Components/Components/VideoDemo.razor.cs create mode 100644 src/BootstrapBlazor.Server/Components/Components/VideoDemo.razor.js create mode 100644 src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserverEntry.cs diff --git a/src/BootstrapBlazor.Server/Components/Components/VideoDemo.razor b/src/BootstrapBlazor.Server/Components/Components/VideoDemo.razor new file mode 100644 index 000000000..3112040dd --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Components/VideoDemo.razor @@ -0,0 +1,4 @@ +@inherits WebSiteModuleComponentBase +@attribute [JSModuleAutoLoader("Components/VideoDemo.razor.js", AutoInvokeInit = false, AutoInvokeDispose = false)] + + diff --git a/src/BootstrapBlazor.Server/Components/Components/VideoDemo.razor.cs b/src/BootstrapBlazor.Server/Components/Components/VideoDemo.razor.cs new file mode 100644 index 000000000..7ae552e9c --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Components/VideoDemo.razor.cs @@ -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; + +/// +/// VideoDemo 组件 +/// +public partial class VideoDemo +{ + /// + /// 开始播放方法 + /// + /// + public Task Play() => InvokeVoidAsync("play", Id); + + /// + /// 暂停方法 + /// + /// + public Task Pause() => InvokeVoidAsync("pause", Id); +} diff --git a/src/BootstrapBlazor.Server/Components/Components/VideoDemo.razor.js b/src/BootstrapBlazor.Server/Components/Components/VideoDemo.razor.js new file mode 100644 index 000000000..575253f37 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Components/VideoDemo.razor.js @@ -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(); +} diff --git a/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor b/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor index a1cd603ea..b1afcde8d 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor @@ -15,7 +15,7 @@ {
- +
} @@ -33,13 +33,13 @@
  • @((MarkupString)Localizer["IntersectionObserverLoadDesc3"].Value)
  • - +
    @foreach (var image in _items) {
    - +
    }
    @@ -52,4 +52,36 @@ - + +

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

    +

    @_videoStateString

    + +
    +
    + + + +
    +
    +
    +
    + + +

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

    +

    @_thresholdValueString

    + +
    +
    + +
    +
    +
    +
    +
    +
    + + diff --git a/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor.cs index 23343df8f..ebb195ae6 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor.cs @@ -13,6 +13,8 @@ public partial class IntersectionObservers private List _items = default!; + private VideoDemo _video = default!; + /// /// /// @@ -24,20 +26,58 @@ public partial class IntersectionObservers _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); - StateHasChanged(); + if (entry.IsIntersecting) + { + _images[entry.Index] = GetImageUrl(entry.Index); + StateHasChanged(); + } + return Task.CompletedTask; } - private async Task OnLoadMoreAsync(int index) + private async Task OnLoadMoreAsync(IntersectionObserverEntry entry) { - await Task.Delay(1000); - _items.AddRange(Enumerable.Range(_items.Count + 1, 20).Select(i => $"https://picsum.photos/160/160?random={i}")); + if (entry.IsIntersecting) + { + 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(); } + 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 AttributeItem[] GetAttributes() => @@ -68,17 +108,25 @@ public partial class IntersectionObservers }, new() { - Name = "AutoUnobserve", - Description = Localizer["AttributeAutoUnobserve"], + Name = nameof(IntersectionObserver.AutoUnobserveWhenIntersection), + Description = Localizer["AttributeAutoUnobserveWhenIntersection"], Type = "bool", ValueList = "true|false", DefaultValue = "true" }, new() { - Name = "OnIntersectingAsync", + Name = nameof(IntersectionObserver.AutoUnobserveWhenNotIntersection), + Description = Localizer["AttributeAutoUnobserveWhenNotIntersection"], + Type = "bool", + ValueList = "true|false", + DefaultValue = "false" + }, + new() + { + Name = "OnIntersecting", Description = Localizer["AttributeOnIntersectingAsync"], - Type = "Func", + Type = "Func", ValueList = " — ", DefaultValue = " — " }, diff --git a/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor.css b/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor.css index d8be87942..b73075d45 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor.css +++ b/src/BootstrapBlazor.Server/Components/Samples/IntersectionObservers.razor.css @@ -4,14 +4,14 @@ border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius); padding: .25rem; - height: 500px; + height: 400px; } .bb-list-load { border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius); padding: .25rem; - height: 500px; + height: 400px; } .bb-list-demo { @@ -40,3 +40,13 @@ opacity: 0.3; padding: .5rem 0; } + +.bb-video-demo { + height: 260px; +} + +.bb-video { + height: 600px; + text-align: center; + margin-top: 160px; +} diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index 26fdc1315..b8263ef20 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -6600,11 +6600,20 @@ "IntersectionObserverLoadDesc1": "By setting Threshold=\"1.0\" 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 AutoUnobserve=\"false\" to prevent automatic removal of the check once it becomes visible", "IntersectionObserverLoadDesc3": "Simulate network delay of 1s by setting the OnLoadMoreAsync method", + "IntersectionObserverVisibleTitle": "Threshold", + "IntersectionObserverVisibleIntro": "By setting Threshold specifying a ratio of intersection area to total bounding box area for the observed target", + "IntersectionObserverVisibleDesc": "In this example, Threshold=\"1\" 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 Threshold, the intersection ratio between the observed element and the root element is obtained through the callback parameter IntersectionObserveEntry attribute IntersectionRatio", + "IntersectionObserverThresholdDesc": "Drag the scroll bar of the div below to observe data changes", "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", "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", - "AttributeChildContent": "Child componenet" + "AttributeChildContent": "Child component" } } diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index 2f866e68f..44b564a26 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -6600,10 +6600,19 @@ "IntersectionObserverLoadDesc1": "通过设置 Threshold=\"1.0\" 约束正在加载指示符完全可见时,执行加载更多操作", "IntersectionObserverLoadDesc2": "通过设置 AutoUnobserve=\"false\" 保证始终检测正在加载指示符可见性,防止可见后自动移除检测", "IntersectionObserverLoadDesc3": "通过设置 OnLoadMoreAsync 方法模拟网络延时 1s", + "IntersectionObserverVisibleTitle": "触发阈值", + "IntersectionObserverVisibleIntro": "通过设置 Threshold 调整触发条件", + "IntersectionObserverVisibleDesc": "本例中设置 Threshold=\"1\" 即触发条件为视频完全可见时播放,否则自动暂停", + "IntersectionObserverVisiblePauseLabel": "视频已暂停", + "IntersectionObserverVisiblePlayLabel": "视频已开始播放", + "IntersectionObserverThresholdTitle": "阈值变化观察", + "IntersectionObserverThresholdIntro": "通过设置 Threshold 后通过回调参数 IntersectionObserveEntry 属性 IntersectionRatio 获得观察元素与根元素交叉率", + "IntersectionObserverThresholdDesc": "拖动下方 div 滚动条,可观察数据变化", "AttributeUseElementViewport": "是否使用当前元素作为视窗", "AttributeRootMargin": "根元素边距", "AttributeThreshold": "可见性阈值", - "AttributeAutoUnobserve": "是否自动取消观察", + "AttributeAutoUnobserveWhenIntersection": "元素可见时是否自动取消观察", + "AttributeAutoUnobserveWhenNotIntersection": "元素不可见时是否自动取消观察", "AttributeOnIntersectingAsync": "可见回调方法", "AttributeChildContent": "子组件" } diff --git a/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.cs b/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.cs index f4b6a507a..e1a2e0a3d 100644 --- a/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.cs +++ b/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.cs @@ -29,16 +29,22 @@ public partial class IntersectionObserver public string? Threshold { get; set; } /// - /// 获得/设置 是否自动取消观察 默认 true 可见后自动取消观察提高性能 + /// 获得/设置 可见后是否自动取消观察 默认 true 可见后自动取消观察提高性能 /// [Parameter] - public bool AutoUnobserve { get; set; } = true; + public bool AutoUnobserveWhenIntersection { get; set; } = true; + + /// + /// 获得/设置 不可见后是否自动取消观察 默认 false 不可见后自动取消观察提高性能 + /// + [Parameter] + public bool AutoUnobserveWhenNotIntersection { get; set; } /// /// 获得/设置 已经交叉回调方法 /// [Parameter] - public Func? OnIntersecting { get; set; } + public Func? OnIntersecting { get; set; } /// /// 获得/设置 子组件 @@ -54,19 +60,19 @@ public partial class IntersectionObserver /// /// /// - 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) }); /// /// 交叉检测回调方法 由 JavaScript 调用 /// - /// + /// 实例 /// [JSInvokable] - public async Task TriggerIntersecting(int index) + public async Task TriggerIntersecting(IntersectionObserverEntry entry) { if (OnIntersecting != null) { - await OnIntersecting(index); + await OnIntersecting(entry); } } } diff --git a/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.js b/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.js index b5d69fc12..ddb8ff6b9 100644 --- a/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.js +++ b/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserver.razor.js @@ -11,18 +11,27 @@ export function init(id, invoke, options) { if (options.useElementViewport === false) { 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 observer = new IntersectionObserver(entries => { entries.forEach(entry => { - if (entry.isIntersecting) { - if (autoUnobserve) { - observer.unobserve(entry.target); - } - const index = items.indexOf(entry.target); - invoke.invokeMethodAsync(callback, index); + if(entry.isIntersecting && autoUnobserveWhenIntersection) { + observer.unobserve(entry.target); } + 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); diff --git a/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserverEntry.cs b/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserverEntry.cs new file mode 100644 index 000000000..bae6f26cb --- /dev/null +++ b/src/BootstrapBlazor/Components/IntersectionObserver/IntersectionObserverEntry.cs @@ -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; + +/// +/// 交叉检测项实例类 +/// + +public class IntersectionObserverEntry +{ + /// + /// 获得/设置 检测项与根元素交叉比率 0 - 1 之间 + /// + public float IntersectionRatio { get; set; } + + /// + /// 获得/设置 是否正在交叉 + /// + public bool IsIntersecting { get; set; } + + /// + /// 获得/设置 当前项索引 + /// + public int Index { get; set; } + + /// + /// 获得/设置 与文档创建时间差 + /// + public double Time { get; set; } +} diff --git a/test/UnitTest/Components/IntersectionObserverTest.cs b/test/UnitTest/Components/IntersectionObserverTest.cs index 851640ef8..e4de8451e 100644 --- a/test/UnitTest/Components/IntersectionObserverTest.cs +++ b/test/UnitTest/Components/IntersectionObserverTest.cs @@ -17,7 +17,8 @@ public class IntersectionObserverTest : BootstrapBlazorTestBase pb.Add(a => a.UseElementViewport, false); pb.Add(a => a.RootMargin, "10px 20px 30px 40px"); 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 => { builder.OpenComponent(0); @@ -41,14 +42,23 @@ public class IntersectionObserverTest : BootstrapBlazorTestBase builder.AddAttribute(1, "ChildContent", new RenderFragment(builder => builder.AddContent(0, "observer-item"))); 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; }); }); - 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); } }