diff --git a/src/BootstrapBlazor.Server/Components/Samples/ContextMenus.razor b/src/BootstrapBlazor.Server/Components/Samples/ContextMenus.razor index ad70bd22cb9..5642f1eddea 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/ContextMenus.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/ContextMenus.razor @@ -53,3 +53,31 @@ + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
diff --git a/src/BootstrapBlazor.Server/Components/Samples/ContextMenus.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/ContextMenus.razor.cs index 4333decc722..ca63d8acc29 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/ContextMenus.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/ContextMenus.razor.cs @@ -7,6 +7,10 @@ public partial class ContextMenus { private List> TreeItems { get; set; } = TreeFoo.GetTreeItems(); + private ConsoleLogger _callbackLogger = default!; + + private ConsoleLogger _disabledLogger = default!; + private static Task OnCopy(ContextMenuItem item, object value) { return Task.CompletedTask; @@ -31,4 +35,24 @@ protected override void OnInitialized() Foo = Foo.Generate(LocalizerFoo); Items = Foo.GenerateFoo(LocalizerFoo); } + + private Task OnBeforeShowCallback(object? item) + { + if (item is TreeFoo foo) + { + _callbackLogger.Log($"{foo.Text} trigger"); + } + return Task.CompletedTask; + } + + private bool OnDisabledCallback(ContextMenuItem item, object? context) + { + var ret = false; + if (context is Foo foo) + { + ret = foo.Id == 1; + _disabledLogger.Log($"{foo.Name} trigger {item.Text} Disabled: {ret}"); + } + return ret; + } } diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index dbe7513a698..ce2fc59d1c4 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -6042,7 +6042,11 @@ "ContextMenuTableTitle": "Table", "ContextMenuTableIntro": "Right click on the Table to pop up a context menu", "ContextMenuTreeTitle": "Tree", - "ContextMenuTreeIntro": "Right click on the Tree to pop up a context menu" + "ContextMenuTreeIntro": "Right click on the Tree to pop up a context menu", + "ContextMenuCallbackTitle": "ContextMenu Callback", + "ContextMenuCallbackIntro": "By setting the ContextMenu component parameter OnBeforeShowCallback, you can get the callback event before the right-click menu pops up, which can be used for data preparation", + "ContextMenuDisabledTitle": "OnDisabledCallback", + "ContextMenuDisabledIntro": "By setting the ContextMenuItem component parameter OnDisabledCallback callback method, you can set whether the current right-click option is disabled." }, "BootstrapBlazor.Server.Components.Samples.DockViews.Index": { "DockViewTitle": "DockView", diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index 8a82345a873..6b41e59614a 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -6042,7 +6042,11 @@ "ContextMenuTableTitle": "Table 组件", "ContextMenuTableIntro": "点击 Table 组件行数据右键,弹出上下文关联菜单", "ContextMenuTreeTitle": "Tree 组件", - "ContextMenuTreeIntro": "点击 Tree 组件行数据右键,弹出上下文关联菜单" + "ContextMenuTreeIntro": "点击 Tree 组件行数据右键,弹出上下文关联菜单", + "ContextMenuCallbackTitle": "ContextMenu 回调", + "ContextMenuCallbackIntro": "通过设置 ContextMenu 组件参数 OnBeforeShowCallback 获得右键菜单弹出前回调事件,可用于数据准备工作", + "ContextMenuDisabledTitle": "禁止回调方法", + "ContextMenuDisabledIntro": "通过设置 ContextMenuItem 组件参数 OnDisabledCallback 回调方法可用于设置当前右键选项是否禁用逻辑" }, "BootstrapBlazor.Server.Components.Samples.DockViews.Index": { "DockViewTitle": "DockView 可停靠视图", diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index cd098e01e96..137af407982 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@ - 8.8.4-beta02 + 8.8.4-beta03 diff --git a/src/BootstrapBlazor/Components/ContextMenu/ContextMenu.razor b/src/BootstrapBlazor/Components/ContextMenu/ContextMenu.razor index eaadf1deab7..d140b3ead61 100644 --- a/src/BootstrapBlazor/Components/ContextMenu/ContextMenu.razor +++ b/src/BootstrapBlazor/Components/ContextMenu/ContextMenu.razor @@ -6,4 +6,15 @@ @ChildContent + + @foreach (var item in _contextMenuItems) + { + var disabled = GetItemTriggerClick(item); + + + @item.Text + + } + diff --git a/src/BootstrapBlazor/Components/ContextMenu/ContextMenu.razor.cs b/src/BootstrapBlazor/Components/ContextMenu/ContextMenu.razor.cs index 00a5e1d7114..d53b39ea1ba 100644 --- a/src/BootstrapBlazor/Components/ContextMenu/ContextMenu.razor.cs +++ b/src/BootstrapBlazor/Components/ContextMenu/ContextMenu.razor.cs @@ -11,6 +11,18 @@ namespace BootstrapBlazor.Components; /// public partial class ContextMenu { + /// + /// 获得/设置 是否显示阴影 默认 true + /// + [Parameter] + public bool ShowShadow { get; set; } = true; + + /// + /// 获得/设置 弹出前回调方法 默认 null + /// + [Parameter] + public Func? OnBeforeShowCallback { get; set; } + /// /// 获得/设置 子组件 /// @@ -26,15 +38,23 @@ public partial class ContextMenu .AddClassFromAttributes(AdditionalAttributes) .Build(); - private object? ContextItem { get; set; } + private string ZoneId => ContextMenuZone.Id; - /// - /// 获得/设置 是否显示阴影 默认 true - /// - [Parameter] - public bool ShowShadow { get; set; } = true; + private List _contextMenuItems = []; - private string ZoneId => ContextMenuZone.Id; + private static string? GetItemClassString(bool disabled) => CssBuilder.Default("dropdown-item") + .AddClass("disabled", disabled) + .Build(); + + private bool GetItemTriggerClick(ContextMenuItem item) => item.OnDisabledCallback?.Invoke(item, _contextItem) ?? item.Disabled; + + private static string? GetItemIconString(ContextMenuItem item) => CssBuilder.Default("cm-icon") + .AddClass(item.Icon, !string.IsNullOrEmpty(item.Icon)) + .Build(); + + private MouseEventArgs? _mouseEventArgs; + + private object? _contextItem; /// /// @@ -46,6 +66,22 @@ protected override void OnInitialized() ContextMenuZone.RegisterContextMenu(this); } + /// + /// + /// + /// + /// + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (_mouseEventArgs != null) + { + await InvokeVoidAsync("show", Id, _mouseEventArgs); + _mouseEventArgs = null; + } + } + /// /// 弹出 ContextMenu /// @@ -54,13 +90,32 @@ protected override void OnInitialized() /// internal async Task Show(MouseEventArgs args, object? contextItem) { - ContextItem = contextItem; - await InvokeVoidAsync("show", Id, args); + _contextItem = contextItem; + _mouseEventArgs = args; + if (OnBeforeShowCallback != null) + { + await OnBeforeShowCallback(contextItem); + } + StateHasChanged(); + } + + private async Task OnClickItem(ContextMenuItem item) + { + if (item.OnClick != null) + { + await item.OnClick(item, _contextItem); + } } /// - /// 获取 ContextItem 值 + /// 增加 ContextMenuItem 方法 /// - /// - internal object? GetContextItem() => ContextItem; + /// + internal void AddItem(ContextMenuItem item) => _contextMenuItems.Add(item); + + /// + /// 移除 ContextMenuItem 方法 + /// + /// + internal void RemoveItem(ContextMenuItem item) => _contextMenuItems.Remove(item); } diff --git a/src/BootstrapBlazor/Components/ContextMenu/ContextMenuItem.cs b/src/BootstrapBlazor/Components/ContextMenu/ContextMenuItem.cs new file mode 100644 index 00000000000..01cb6302dea --- /dev/null +++ b/src/BootstrapBlazor/Components/ContextMenu/ContextMenuItem.cs @@ -0,0 +1,82 @@ +// 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; + +/// +/// ContextMenuItem 类 +/// +public class ContextMenuItem : ComponentBase, IDisposable +{ + /// + /// 获得/设置 显示文本 + /// + [Parameter] + public string? Text { get; set; } + + /// + /// 获得/设置 图标 + /// + [Parameter] + public string? Icon { get; set; } + + /// + /// 获得/设置 是否被禁用 默认 false 优先级低于 + /// + [Parameter] + public bool Disabled { get; set; } + + /// + /// 获得/设置 是否被禁用回调方法 默认 null 优先级高于 + /// + [Parameter] + public Func? OnDisabledCallback { get; set; } + + /// + /// 获得/设置 点击回调方法 默认 null + /// + [Parameter] + public Func? OnClick { get; set; } + + [CascadingParameter] + [NotNull] + private ContextMenu? ContextMenu { get; set; } + + /// + /// + /// + protected override void OnInitialized() + { + base.OnInitialized(); + + ContextMenu.AddItem(this); + } + + private bool disposedValue; + + /// + /// 释放资源方法 + /// + /// + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + ContextMenu.RemoveItem(this); + } + disposedValue = true; + } + } + + /// + /// + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/BootstrapBlazor/Components/ContextMenu/ContextMenuItem.razor b/src/BootstrapBlazor/Components/ContextMenu/ContextMenuItem.razor deleted file mode 100644 index 19b16041749..00000000000 --- a/src/BootstrapBlazor/Components/ContextMenu/ContextMenuItem.razor +++ /dev/null @@ -1,7 +0,0 @@ -@namespace BootstrapBlazor.Components -@inherits BootstrapComponentBase - -
- - @Text -
diff --git a/src/BootstrapBlazor/Components/ContextMenu/ContextMenuItem.razor.cs b/src/BootstrapBlazor/Components/ContextMenu/ContextMenuItem.razor.cs deleted file mode 100644 index 0ebf5bcec22..00000000000 --- a/src/BootstrapBlazor/Components/ContextMenu/ContextMenuItem.razor.cs +++ /dev/null @@ -1,55 +0,0 @@ -// 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; - -/// -/// ContextMenuItem 类 -/// -public partial class ContextMenuItem -{ - /// - /// 获得/设置 显示文本 - /// - [Parameter] - public string? Text { get; set; } - - /// - /// 获得/设置 图标 - /// - [Parameter] - public string? Icon { get; set; } - - /// - /// 获得/设置 是否被禁用 默认 false - /// - [Parameter] - public bool Disabled { get; set; } - - /// - /// 获得/设置 点击回调方法 默认 null - /// - [Parameter] - public Func? OnClick { get; set; } - - [CascadingParameter] - [NotNull] - private ContextMenu? ContextMenu { get; set; } - - private string? ClassString => CssBuilder.Default("dropdown-item") - .AddClassFromAttributes(AdditionalAttributes) - .Build(); - - private string? IconString => CssBuilder.Default("cm-icon") - .AddClass(Icon, !string.IsNullOrEmpty(Icon)) - .Build(); - - private async Task OnClickItem() - { - if (!Disabled && OnClick != null) - { - await OnClick(this, ContextMenu.GetContextItem()); - } - } -} diff --git a/test/UnitTest/Components/ContextMenuTest.cs b/test/UnitTest/Components/ContextMenuTest.cs index 6cc8e929ec1..1e86bbf8d3c 100644 --- a/test/UnitTest/Components/ContextMenuTest.cs +++ b/test/UnitTest/Components/ContextMenuTest.cs @@ -50,29 +50,60 @@ public async Task ContextMenu_Ok() }); }); - await cut.InvokeAsync(async () => - { - var row = cut.Find(".context-trigger"); - row.ContextMenu(0, 10, 10, 10, 10, 2, 2); + var row = cut.Find(".context-trigger"); + row.ContextMenu(0, 10, 10, 10, 10, 2, 2); - var menu = cut.FindComponent(); - menu.Contains("shadow"); - var pi = typeof(ContextMenu).GetProperty("ContextItem", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - Assert.NotNull(pi); + var menu = cut.FindComponent(); + menu.Contains("shadow"); + var pi = typeof(ContextMenu).GetField("_contextItem", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + Assert.NotNull(pi); - var v = pi.GetValue(menu.Instance); - Assert.NotNull(v); + var v = pi.GetValue(menu.Instance); + Assert.NotNull(v); - var item = menu.Find(".dropdown-item"); - item.Click(); - Assert.False(clicked); + var item = menu.Find(".dropdown-item"); + Assert.DoesNotContain("blazor:onclick", item.InnerHtml); - // 测试 Touch 事件 - TriggerTouchStart(row); + var contextItem = cut.FindComponent(); + contextItem.SetParametersAndRender(pb => + { + pb.Add(a => a.Disabled, false); + pb.Add(a => a.OnDisabledCallback, (item, v) => + { + return true; + }); + }); + item = menu.Find(".dropdown-item"); + Assert.DoesNotContain("blazor:onclick", item.InnerHtml); - await Task.Delay(500); - row.TouchEnd(); + // trigger OnBeforeShowCallback + bool menuCallback = false; + contextItem.SetParametersAndRender(pb => + { + pb.Add(a => a.Disabled, false); + pb.Add(a => a.OnDisabledCallback, (item, v) => + { + menuCallback = true; + return false; + }); + }); + menu.SetParametersAndRender(pb => + { + pb.Add(a => a.OnBeforeShowCallback, v => + { + return Task.CompletedTask; + }); }); + item = menu.Find(".dropdown-item"); + item.Click(); + Assert.True(menuCallback); + + // 测试 Touch 事件 + TriggerTouchStart(row); + + await Task.Delay(500); + row.TouchEnd(); + Assert.True(clicked); } [Theory] @@ -114,29 +145,26 @@ public async Task ContextMenu_Table(TableRenderMode renderMode) }); }); - await cut.InvokeAsync(async () => - { - var row = renderMode == TableRenderMode.CardView ? cut.Find(".table-row") : cut.Find("tbody tr"); - row.ContextMenu(0, 10, 10, 10, 10, 2, 2); + var row = renderMode == TableRenderMode.CardView ? cut.Find(".table-row") : cut.Find("tbody tr"); + row.ContextMenu(0, 10, 10, 10, 10, 2, 2); - var menu = cut.FindComponent(); - var pi = typeof(ContextMenu).GetProperty("ContextItem", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - Assert.NotNull(pi); + var menu = cut.FindComponent(); + var pi = typeof(ContextMenu).GetField("_contextItem", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + Assert.NotNull(pi); - var v = pi.GetValue(menu.Instance); - Assert.NotNull(v); + var v = pi.GetValue(menu.Instance); + Assert.NotNull(v); - var item = menu.Find(".dropdown-item"); - item.Click(); - Assert.True(clicked); + var item = menu.Find(".dropdown-item"); + item.Click(); + Assert.True(clicked); - TriggerTouchStart(row); - TriggerTouchStart(row); + TriggerTouchStart(row); + TriggerTouchStart(row); - var options = Context.Services.GetRequiredService>(); - await Task.Delay(100 + options.Value.ContextMenuOptions.OnTouchDelay); - row.TouchEnd(); - }); + var options = Context.Services.GetRequiredService>(); + await Task.Delay(100 + options.Value.ContextMenuOptions.OnTouchDelay); + row.TouchEnd(); } [Fact] @@ -180,29 +208,26 @@ public async Task ContextMenu_TreeView() }); }); - await cut.InvokeAsync(async () => - { - var row = cut.Find(".tree-content"); - row.ContextMenu(0, 10, 10, 10, 10, 2, 2); + var row = cut.Find(".tree-content"); + row.ContextMenu(0, 10, 10, 10, 10, 2, 2); - var menu = cut.FindComponent(); - var pi = typeof(ContextMenu).GetProperty("ContextItem", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - Assert.NotNull(pi); + var menu = cut.FindComponent(); + var pi = typeof(ContextMenu).GetField("_contextItem", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + Assert.NotNull(pi); - var v = pi.GetValue(menu.Instance); - Assert.NotNull(v); + var v = pi.GetValue(menu.Instance); + Assert.NotNull(v); - var item = menu.Find(".dropdown-item"); - item.Click(); - Assert.True(clicked); + var item = menu.Find(".dropdown-item"); + item.Click(); + Assert.True(clicked); - TriggerTouchStart(row); - TriggerTouchStart(row); + TriggerTouchStart(row); + TriggerTouchStart(row); - var options = Context.Services.GetRequiredService>(); - await Task.Delay(100 + options.Value.ContextMenuOptions.OnTouchDelay); - row.TouchEnd(); - }); + var options = Context.Services.GetRequiredService>(); + await Task.Delay(100 + options.Value.ContextMenuOptions.OnTouchDelay); + row.TouchEnd(); } private void TriggerTouchStart(IElement row)