feat(Split): add OnResizeAsync parameter (#4177)

* feat: 增加 OnResizedAsync 回调方法

* feat: 增加 SplitterResizedEventArgs 类

* doc: 增加 OnResizeAsync 示例

* refactor: 增加 SplitsOnResizedAsync 文档

* doc: 增加折叠按钮文档

* refactor: 增加 IsCollapsible 参数动态支持

* test: 更新单元测试

* refactor: 重构方法名

* chore: bump version 8.8.5-beta06
This commit is contained in:
Argo Zhang 2024-08-29 13:33:57 +08:00 committed by GitHub
parent ca28669677
commit 09d53ff881
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 192 additions and 64 deletions

View File

@ -6,12 +6,22 @@
<DemoBlock Title="@Localizer["SplitsNormalTitle"]" <DemoBlock Title="@Localizer["SplitsNormalTitle"]"
Introduction="@Localizer["SplitsNormalIntro"]" Introduction="@Localizer["SplitsNormalIntro"]"
Name="Normal"> Name="Normal">
<section ignore class="d-flex align-items-center"> <section ignore class="row">
<Switch Value="_showBarHandle" OnValueChanged="OnShowBarHandle" class="w-auto"></Switch> <div class="col-12 col-sm-6">
<div class="ms-3">@_barHandleText</div> <div class="d-flex align-items-center">
<Switch Value="_showBarHandle" OnValueChanged="OnShowBarHandle" class="w-auto"></Switch>
<div class="ms-3">@_barHandleText</div>
</div>
</div>
<div class="col-12 col-sm-6">
<div class="d-flex align-items-center">
<Switch Value="_isCollapsible" OnValueChanged="OnCollapsible" class="w-auto"></Switch>
<div class="ms-3">@_collapsibleText</div>
</div>
</div>
</section> </section>
<div class="border split-demo split-demo-horizontal"> <div class="border split-demo split-demo-horizontal">
<Split ShowBarHandle="_showBarHandle"> <Split ShowBarHandle="_showBarHandle" OnResizedAsync="OnResizedAsync" IsCollapsible="_isCollapsible">
<FirstPaneTemplate> <FirstPaneTemplate>
<div class="d-flex justify-content-center align-items-center h-100">@Localizer["SplitsPanel1"]</div> <div class="d-flex justify-content-center align-items-center h-100">@Localizer["SplitsPanel1"]</div>
</FirstPaneTemplate> </FirstPaneTemplate>
@ -20,6 +30,7 @@
</SecondPaneTemplate> </SecondPaneTemplate>
</Split> </Split>
</div> </div>
<ConsoleLogger @ref="_logger"></ConsoleLogger>
</DemoBlock> </DemoBlock>
<DemoBlock Title="@Localizer["SplitsPercentTitle"]" <DemoBlock Title="@Localizer["SplitsPercentTitle"]"

View File

@ -11,8 +11,14 @@ public sealed partial class Splits
{ {
private bool _showBarHandle = true; private bool _showBarHandle = true;
private bool _isCollapsible = false;
private string? _barHandleText; private string? _barHandleText;
private string? _collapsibleText;
private ConsoleLogger _logger = default!;
/// <summary> /// <summary>
/// <inheritdoc/> /// <inheritdoc/>
/// </summary> /// </summary>
@ -20,6 +26,7 @@ public sealed partial class Splits
{ {
base.OnParametersSet(); base.OnParametersSet();
_barHandleText = _showBarHandle ? Localizer["SplitsBarHandleShow"] : Localizer["SplitsBarHandleHide"]; _barHandleText = _showBarHandle ? Localizer["SplitsBarHandleShow"] : Localizer["SplitsBarHandleHide"];
_collapsibleText = _showBarHandle ? Localizer["SplitsCollapsibleTrue"] : Localizer["SplitsCollapsibleFalse"];
} }
private Task OnShowBarHandle(bool v) private Task OnShowBarHandle(bool v)
@ -30,6 +37,20 @@ public sealed partial class Splits
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task OnCollapsible(bool v)
{
_isCollapsible = v;
_collapsibleText = _showBarHandle ? Localizer["SplitsCollapsibleTrue"] : Localizer["SplitsCollapsibleFalse"];
StateHasChanged();
return Task.CompletedTask;
}
private Task OnResizedAsync(SplitterResizedEventArgs args)
{
_logger.Log($"FirstPanelSize: {args.FirstPanelSize} IsCollapsed: {args.IsCollapsed} IsExpanded: {args.IsExpanded}");
return Task.CompletedTask;
}
/// <summary> /// <summary>
/// 获得属性方法 /// 获得属性方法
/// </summary> /// </summary>
@ -91,6 +112,14 @@ public sealed partial class Splits
Type = "bool", Type = "bool",
ValueList = "true|false", ValueList = "true|false",
DefaultValue = "true" DefaultValue = "true"
},
new()
{
Name = "OnResizedAsync",
Description = Localizer["SplitsOnResizedAsync"],
Type = "Func<SplitterResizedEventArgs, Task>",
ValueList = " — ",
DefaultValue = " — "
} }
]; ];
} }

View File

@ -1774,7 +1774,10 @@
"SplitsIsKeepOriginalSize": "Whether to keep the original size when restoring after collapsed", "SplitsIsKeepOriginalSize": "Whether to keep the original size when restoring after collapsed",
"SplitsShowBarHandle": "Whether to display the drag bar", "SplitsShowBarHandle": "Whether to display the drag bar",
"SplitsBarHandleShow": "Bar handle shown", "SplitsBarHandleShow": "Bar handle shown",
"SplitsBarHandleHide": "Bar handle hidden" "SplitsBarHandleHide": "Bar handle hidden",
"SplitsOnResizedAsync": "Callback method when the panel size changes",
"SplitsCollapsibleTrue": "Collapse button shown",
"SplitsCollapsibleFalse": "Collapse button hidden"
}, },
"BootstrapBlazor.Server.Components.Samples.Dropdowns": { "BootstrapBlazor.Server.Components.Samples.Dropdowns": {
"Title": "Dropdown", "Title": "Dropdown",

View File

@ -1774,7 +1774,10 @@
"SplitsIsKeepOriginalSize": "折叠后恢复时是否保持原始大小", "SplitsIsKeepOriginalSize": "折叠后恢复时是否保持原始大小",
"SplitsShowBarHandle": "是否显示拖动条", "SplitsShowBarHandle": "是否显示拖动条",
"SplitsBarHandleShow": "显示拖动栏", "SplitsBarHandleShow": "显示拖动栏",
"SplitsBarHandleHide": "隐藏拖动栏" "SplitsBarHandleHide": "隐藏拖动栏",
"SplitsOnResizedAsync": "面板尺寸改变时回调方法",
"SplitsCollapsibleTrue": "显示调整按钮",
"SplitsCollapsibleFalse": "隐藏调整按钮"
}, },
"BootstrapBlazor.Server.Components.Samples.Dropdowns": { "BootstrapBlazor.Server.Components.Samples.Dropdowns": {
"Title": "Dropdown 下拉菜单", "Title": "Dropdown 下拉菜单",

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor"> <Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup> <PropertyGroup>
<Version>8.8.5-beta05</Version> <Version>8.8.5-beta06</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">

View File

@ -9,21 +9,6 @@ namespace BootstrapBlazor.Components;
/// </summary> /// </summary>
public sealed partial class Split public sealed partial class Split
{ {
/// <summary>
/// 获得 组件样式
/// </summary>
private string? ClassString => CssBuilder.Default("split")
.AddClass("is-vertical", IsVertical)
.AddClassFromAttributes(AdditionalAttributes)
.Build();
/// <summary>
/// 获得 第一个窗格 Style
/// </summary>
private string? StyleString => CssBuilder.Default()
.AddClass($"flex-basis: {Basis.ConvertToPercentString()};")
.Build();
/// <summary> /// <summary>
/// 获取 是否开启折叠功能 默认 false /// 获取 是否开启折叠功能 默认 false
/// </summary> /// </summary>
@ -70,25 +55,69 @@ public sealed partial class Split
/// 获得/设置 窗格折叠时回调方法 参数 bool 值为 true 是表示已折叠 值为 false 表示第二个已折叠 /// 获得/设置 窗格折叠时回调方法 参数 bool 值为 true 是表示已折叠 值为 false 表示第二个已折叠
/// </summary> /// </summary>
[Parameter] [Parameter]
[Obsolete("已过期,请使用 Deprecated. Please use OnResizedAsync")]
[ExcludeFromCodeCoverage]
public Func<bool, Task>? OnCollapsedAsync { get; set; } public Func<bool, Task>? OnCollapsedAsync { get; set; }
/// <summary>
/// 获得/设置 窗格尺寸改变时回调方法 可参阅 <see cref="SplitterResizedEventArgs"/>
/// </summary>
[Parameter]
public Func<SplitterResizedEventArgs, Task>? OnResizedAsync { get; set; }
/// <summary>
/// 获得 组件样式
/// </summary>
private string? ClassString => CssBuilder.Default("split")
.AddClass("is-vertical", IsVertical)
.AddClassFromAttributes(AdditionalAttributes)
.Build();
/// <summary>
/// 获得 第一个窗格 Style
/// </summary>
private string? StyleString => CssBuilder.Default()
.AddClass($"flex-basis: {Basis.ConvertToPercentString()};")
.Build();
private bool _lastCollapsible;
/// <summary> /// <summary>
/// <inheritdoc/> /// <inheritdoc/>
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, nameof(TriggerOnCollapsed), new { IsKeepOriginalSize }); protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, nameof(TriggerOnResize), new { IsKeepOriginalSize });
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="firstRender"></param>
/// <returns></returns>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
_lastCollapsible = IsCollapsible;
}
else if (_lastCollapsible != IsCollapsible)
{
_lastCollapsible = IsCollapsible;
await InvokeVoidAsync("update", Id);
}
}
/// <summary> /// <summary>
/// 窗格折叠时回调方法 由 JavaScript 调用 /// 窗格折叠时回调方法 由 JavaScript 调用
/// </summary> /// </summary>
/// <param name="collapsed"></param>
/// <returns></returns> /// <returns></returns>
[JSInvokable] [JSInvokable]
public async Task TriggerOnCollapsed(bool collapsed) public async Task TriggerOnResize(string left)
{ {
if (OnCollapsedAsync != null) if (OnResizedAsync != null)
{ {
await OnCollapsedAsync(collapsed); await OnResizedAsync(new SplitterResizedEventArgs(left));
} }
} }
} }

View File

@ -19,9 +19,7 @@ export function init(id, invoke, method, option) {
const splitRight = el.children[1]; const splitRight = el.children[1];
const splitBar = el.children[2]; const splitBar = el.children[2];
const split = { el, option } const split = { el, invoke, method, option, splitLeft, splitBar };
split.splitLeft = splitLeft;
split.splitBar = splitBar;
Data.set(id, split) Data.set(id, split)
Drag.drag(splitBar, Drag.drag(splitBar,
e => { e => {
@ -55,38 +53,53 @@ export function init(id, invoke, method, option) {
}, },
() => { () => {
el.classList.remove('dragging'); el.classList.remove('dragging');
console.log('end');
delete option.restoreLeftBasis; delete option.restoreLeftBasis;
removeMask(splitLeft, splitRight); removeMask(splitLeft, splitRight);
invoke.invokeMethodAsync(method, splitLeft.style.flexBasis);
} }
); );
let start = 0; split.initCollapseButton = () => {
const step = ts => { let start = 0;
if (start === 0) { const step = ts => {
start = ts; if (start === 0) {
} start = ts;
if (ts - start > 300) { }
splitLeft.classList.remove('is-collapsed'); if (ts - start > 300) {
} splitLeft.classList.remove('is-collapsed');
requestAnimationFrame(step); }
}
[...splitBar.querySelectorAll('.split-bar-arrow')].forEach(element => {
EventHandler.on(element, 'mousedown', e => {
e.stopPropagation();
splitLeft.classList.add('is-collapsed');
const triggerLeft = element.classList.contains("split-bar-arrow-left");
invoke.invokeMethodAsync(method, triggerLeft);
setLeftBasis(split, triggerLeft);
start = 0;
requestAnimationFrame(step); requestAnimationFrame(step);
}
[...splitBar.querySelectorAll('.split-bar-arrow')].forEach(element => {
EventHandler.on(element, 'mousedown', e => {
e.stopPropagation();
splitLeft.classList.add('is-collapsed');
const triggerLeft = element.classList.contains("split-bar-arrow-left");
setLeftBasis(split, triggerLeft);
start = 0;
requestAnimationFrame(step);
});
}); });
}); };
split.initCollapseButton();
}
export function update(id) {
const split = Data.get(id)
if (split) {
const { splitBar, initCollapseButton } = split;
if (splitBar) {
disposeCollapseButton(splitBar);
initCollapseButton();
}
}
} }
const setLeftBasis = (split, triggerLeft) => { const setLeftBasis = (split, triggerLeft) => {
const { option, splitLeft } = split; const { option, splitLeft, invoke, method } = split;
let leftBasis = splitLeft.style.flexBasis; let leftBasis = splitLeft.style.flexBasis;
if (option.isKeepOriginalSize) { if (option.isKeepOriginalSize) {
if (option.restoreLeftBasis === void 0) { if (option.restoreLeftBasis === void 0) {
@ -112,6 +125,7 @@ const setLeftBasis = (split, triggerLeft) => {
} }
} }
splitLeft.style.setProperty('flex-basis', leftBasis); splitLeft.style.setProperty('flex-basis', leftBasis);
invoke.invokeMethodAsync(method, leftBasis);
} }
const showMask = (left, right) => { const showMask = (left, right) => {
@ -136,15 +150,21 @@ const deleteMask = el => {
} }
} }
const disposeCollapseButton = splitBar => {
[...splitBar.querySelectorAll('.split-bar-arrow')].forEach(element => {
EventHandler.off(element, 'mousedown');
});
}
export function dispose(id) { export function dispose(id) {
const split = Data.get(id) const split = Data.get(id)
Data.remove(id) Data.remove(id)
if (split) { if (split) {
const { el } = split; const { splitBar } = split;
if (el.splitBar) { if (splitBar) {
EventHandler.off(splitBar, 'click', '.split-bar-arrow'); disposeCollapseButton(splitBar);
Drag.dispose(el.splitBar); Drag.dispose(splitBar);
} }
} }
} }

View File

@ -0,0 +1,26 @@
// 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>
/// SplitterResizedEventArgs 类
/// </summary>
public class SplitterResizedEventArgs(string left)
{
/// <summary>
/// Gets the size of panel 1 (top/left) after a resize operation.
/// </summary>
public string FirstPanelSize => left;
/// <summary>
/// 获得 组件第一个面板是否折叠
/// </summary>
public bool IsCollapsed => left == "0%";
/// <summary>
/// 获得 组件第一个面板是否展开
/// </summary>
public bool IsExpanded => left == "100%";
}

View File

@ -67,24 +67,31 @@ public class SplitTest : BootstrapBlazorTestBase
} }
[Fact] [Fact]
public async Task OnCollapsedAsync_Ok() public async Task OnResizedAsync_Ok()
{ {
var state = false; SplitterResizedEventArgs? state = null;
var cut = Context.RenderComponent<Split>(pb => var cut = Context.RenderComponent<Split>(pb =>
{ {
pb.Add(b => b.FirstPaneTemplate, RenderSplitView("I am Pane1")); pb.Add(b => b.FirstPaneTemplate, RenderSplitView("I am Pane1"));
pb.Add(b => b.SecondPaneTemplate, RenderSplitView("I am Pane2")); pb.Add(b => b.SecondPaneTemplate, RenderSplitView("I am Pane2"));
pb.Add(b => b.IsCollapsible, true); pb.Add(b => b.IsCollapsible, true);
pb.Add(b => b.OnCollapsedAsync, async (collapsed) => pb.Add(b => b.OnResizedAsync, async args =>
{ {
state = collapsed; state = args;
await Task.CompletedTask; await Task.CompletedTask;
}); });
}); });
await cut.InvokeAsync(() => cut.Instance.TriggerOnCollapsed(true)); Assert.Null(state);
Assert.True(state); await cut.InvokeAsync(() => cut.Instance.TriggerOnResize("0%"));
await cut.InvokeAsync(() => cut.Instance.TriggerOnCollapsed(false)); Assert.NotNull(state);
Assert.False(state); Assert.Equal("0%", state.FirstPanelSize);
Assert.True(state.IsCollapsed);
Assert.False(state.IsExpanded);
await cut.InvokeAsync(() => cut.Instance.TriggerOnResize("100%"));
Assert.Equal("100%", state.FirstPanelSize);
Assert.True(state.IsExpanded);
Assert.False(state.IsCollapsed);
} }
static RenderFragment RenderSplitView(string name = "I am Pane1") => builder => static RenderFragment RenderSplitView(string name = "I am Pane1") => builder =>