feat(TreeView): add MaxSelectedCount OnMaxSelectedCountExceed parameter (#4007)

* feat: 增加 OnBeforeStateChanged 回调方法

* feat: 增加 MaxSelectedCount 参数

* refactor: 通过 OnBeforeStateChanged 实现逻辑

* refactor: 增加 OnMaxSelectedCountExceed 回调执行逻辑

* refactor: 增加客户端阻止逻辑

* refactor: 增加阻止继承标签

* doc: 更新文档

* refactor: 更新注释

* refactor: 精简代码

* test: 增加单元测试

* refactor: 重构代码

* test: 更新单元测试

* test: 更新单元测试

* doc: 更新文档

* doc: 增加多选最大值示例
This commit is contained in:
Argo Zhang 2024-08-09 17:14:38 +08:00 committed by GitHub
parent fbfa97c528
commit debdabce33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 173 additions and 42 deletions

View File

@ -94,7 +94,8 @@ public sealed partial class Checkboxs
ValueList = " — ", ValueList = " — ",
DefaultValue = " — " DefaultValue = " — "
}, },
new(){ new()
{
Name = "IsDisabled", Name = "IsDisabled",
Description = Localizer["Att4"], Description = Localizer["Att4"],
Type = "boolean", Type = "boolean",
@ -102,14 +103,13 @@ public sealed partial class Checkboxs
DefaultValue = "false" DefaultValue = "false"
}, },
new() new()
{ {
Name = "State", Name = "State",
Description = Localizer["Att5"], Description = Localizer["Att5"],
Type = "CheckboxState", Type = "CheckboxState",
ValueList = "Mixed / Checked / UnChecked", ValueList = "Mixed / Checked / UnChecked",
DefaultValue = "UnChecked" DefaultValue = "UnChecked"
}, }
]; ];
/// <summary> /// <summary>

View File

@ -182,6 +182,14 @@
<TreeView TItem="TreeFoo" Items="@SearchItems" ShowSearch="true" OnSearchAsync="@OnSearchAsync" IsFixedSearch="true" style="height: 180px;"></TreeView> <TreeView TItem="TreeFoo" Items="@SearchItems" ShowSearch="true" OnSearchAsync="@OnSearchAsync" IsFixedSearch="true" style="height: 180px;"></TreeView>
</DemoBlock> </DemoBlock>
<DemoBlock Title="@Localizer["TreeViewMaxSelectedCountTitle"]"
Introduction="@Localizer["TreeViewMaxSelectedCountIntro"]"
Name="MaxSelectedCount">
<section ignore>@((MarkupString)Localizer["TreeViewMaxSelectedCountDesc"].Value)</section>
<TreeView TItem="TreeFoo" Items="@SearchItems" ShowCheckbox="true" AutoCheckChildren="true" AutoCheckParent="true"
MaxSelectedCount="2" OnMaxSelectedCountExceed="OnMaxSelectedCountExceed"></TreeView>
</DemoBlock>
<AttributeTable Items="@GetAttributes()"></AttributeTable> <AttributeTable Items="@GetAttributes()"></AttributeTable>
<AttributeTable Items="@GetTreeItemAttributes()" Title="@Localizer["TreeViewsAttribute"]"></AttributeTable> <AttributeTable Items="@GetTreeItemAttributes()" Title="@Localizer["TreeViewsAttribute"]"></AttributeTable>

View File

@ -81,6 +81,14 @@ public sealed partial class TreeViews
new("False", "Keep") new("False", "Keep")
]; ];
[Inject, NotNull]
private ToastService? ToastService { get; set; }
private Task OnMaxSelectedCountExceed()
{
return ToastService.Information(Localizer["OnMaxSelectedCountExceedTitle"], Localizer["OnMaxSelectedCountExceedContent", 2]);
}
private Task OnTreeItemChecked(List<TreeViewItem<TreeFoo>> items) private Task OnTreeItemChecked(List<TreeViewItem<TreeFoo>> items)
{ {
Logger2.Log($"当前共选中{items.Count}项"); Logger2.Log($"当前共选中{items.Count}项");
@ -119,7 +127,7 @@ public sealed partial class TreeViews
{ {
var ret = TreeFoo.GetCheckedTreeItems(); var ret = TreeFoo.GetCheckedTreeItems();
ret[0].IsExpand = true; ret[0].IsExpand = true;
ret[0].Items= TreeFoo.GetCheckedTreeItems(); ret[0].Items = TreeFoo.GetCheckedTreeItems();
return ret; return ret;
} }
@ -235,7 +243,7 @@ public sealed partial class TreeViews
builder.AddAttribute(2, nameof(Button.Text), "Click"); builder.AddAttribute(2, nameof(Button.Text), "Click");
builder.AddAttribute(3, nameof(Button.OnClick), EventCallback.Factory.Create<MouseEventArgs>(this, e => builder.AddAttribute(3, nameof(Button.OnClick), EventCallback.Factory.Create<MouseEventArgs>(this, e =>
{ {
ToastService.Warning("自定义 TreeItem", "测试 TreeItem 按钮点击事件"); ToastService.Warning("自定义 TreeViewItem", "测试 TreeViewItem 按钮点击事件");
})); }));
builder.CloseComponent(); builder.CloseComponent();
} }
@ -251,9 +259,9 @@ public sealed partial class TreeViews
{ {
Name = "Items", Name = "Items",
Description = "menu data set", Description = "menu data set",
Type = "IEnumerable<TreeItem>", Type = "IEnumerable<TreeViewItem>",
ValueList = " — ", ValueList = " — ",
DefaultValue = "new List<TreeItem>(20)" DefaultValue = "new List<TreeViewItem>(20)"
}, },
new() new()
{ {
@ -291,7 +299,7 @@ public sealed partial class TreeViews
{ {
Name = nameof(TreeView<string>.OnTreeItemClick), Name = nameof(TreeView<string>.OnTreeItemClick),
Description = "Callback delegate when tree control node is clicked", Description = "Callback delegate when tree control node is clicked",
Type = "Func<TreeItem, Task>", Type = "Func<TreeViewItem, Task>",
ValueList = " — ", ValueList = " — ",
DefaultValue = " — " DefaultValue = " — "
}, },
@ -299,7 +307,7 @@ public sealed partial class TreeViews
{ {
Name = nameof(TreeView<string>.OnTreeItemChecked), Name = nameof(TreeView<string>.OnTreeItemChecked),
Description = "Callback delegate when tree control node is selected", Description = "Callback delegate when tree control node is selected",
Type = "Func<TreeItem, Task>", Type = "Func<TreeViewItem, Task>",
ValueList = " — ", ValueList = " — ",
DefaultValue = " — " DefaultValue = " — "
}, },
@ -307,7 +315,7 @@ public sealed partial class TreeViews
{ {
Name = nameof(TreeView<string>.OnExpandNodeAsync), Name = nameof(TreeView<string>.OnExpandNodeAsync),
Description = "Tree control node expand callback delegate", Description = "Tree control node expand callback delegate",
Type = "Func<TreeItem, Task>", Type = "Func<TreeViewItem, Task>",
ValueList = " — ", ValueList = " — ",
DefaultValue = " — " DefaultValue = " — "
}, },
@ -326,6 +334,22 @@ public sealed partial class TreeViews
Type = "bool", Type = "bool",
ValueList = "true|false", ValueList = "true|false",
DefaultValue = "false" DefaultValue = "false"
},
new()
{
Name = nameof(TreeView<string>.MaxSelectedCount),
Description = "The maximum count of selected node",
Type = "int",
ValueList = " — ",
DefaultValue = "0"
},
new()
{
Name = nameof(TreeView<string>.OnMaxSelectedCountExceed),
Description = "Select the callback method when the maximum number of nodes is reached",
Type = "Func<Task>",
ValueList = " — ",
DefaultValue = " — "
} }
]; ];
@ -335,7 +359,7 @@ public sealed partial class TreeViews
{ {
Name = nameof(TreeViewItem<TreeFoo>.Items), Name = nameof(TreeViewItem<TreeFoo>.Items),
Description = "Child node data source", Description = "Child node data source",
Type = "List<TreeItem<TItem>>", Type = "List<TreeViewItem<TItem>>",
ValueList = " — ", ValueList = " — ",
DefaultValue = "new ()" DefaultValue = "new ()"
}, },

View File

@ -630,15 +630,15 @@
"TreeViewsTitle": "Tree tree control", "TreeViewsTitle": "Tree tree control",
"TreeViewsDescription": "Present information in a clear hierarchy that can be expanded or collapsed", "TreeViewsDescription": "Present information in a clear hierarchy that can be expanded or collapsed",
"TreeViewsTips1": "If the component is a generic component, you need to use <code>TItem</code> to specify the bound data model. In this example, the model is <code>TreeFoo</code> and needs to be set", "TreeViewsTips1": "If the component is a generic component, you need to use <code>TItem</code> to specify the bound data model. In this example, the model is <code>TreeFoo</code> and needs to be set",
"TreeViewsTips2": "Set <code>TreeItem</code> its <code>IsExpand</code> parameter to control whether the current child node is expanded", "TreeViewsTips2": "Set <code>TreeViewItem</code> its <code>IsExpand</code> parameter to control whether the current child node is expanded",
"TreeViewsTips3": "When clicking on the small arrow to expand the child item, obtain the child item data collection through the <code>OnExpandNodeAsync</code> callback delegate method", "TreeViewsTips3": "When clicking on the small arrow to expand the child item, obtain the child item data collection through the <code>OnExpandNodeAsync</code> callback delegate method",
"TreeViewsTips4": "Keep the node state fallback mechanism, <code>ModelEqualityComparer</code> <code>CustomKeyAttribute</code> <code>IEqualityComparer&lt;TItem&gt;</code> <code>Equals</code> overloaded method", "TreeViewsTips4": "Keep the node state fallback mechanism, <code>ModelEqualityComparer</code> <code>CustomKeyAttribute</code> <code>IEqualityComparer&lt;TItem&gt;</code> <code>Equals</code> overloaded method",
"TreeViewsTips5": "The component will remain in the <code>expanded</code> <code>collapsed</code> <code>selected</code> state", "TreeViewsTips5": "The component will remain in the <code>expanded</code> <code>collapsed</code> <code>selected</code> state",
"TreeViewsTips6": "Set whether the node is <b>expanded</b> state through <code>TreeItem&lt;TItem&gt;.IsExpand</code>", "TreeViewsTips6": "Set whether the node is <b>expanded</b> state through <code>TreeViewItem&lt;TItem&gt;.IsExpand</code>",
"TreeViewsTips7": "Set whether the node is <b>selected</b> state through <code>TreeItem&lt;TItem&gt;.IsActive</code>", "TreeViewsTips7": "Set whether the node is <b>selected</b> state through <code>TreeViewItem&lt;TItem&gt;.IsActive</code>",
"TreeViewsTips8": "Through <code>TreeItem&lt;TItem&gt;.Checked</code>, set whether the node is in <b>checked/single selection</b> state", "TreeViewsTips8": "Through <code>TreeViewItem&lt;TItem&gt;.Checked</code>, set whether the node is in <b>checked/single selection</b> state",
"TreeViewsTips9": "Step 1: Set the <code>TItem</code> generic model", "TreeViewsTips9": "Step 1: Set the <code>TItem</code> generic model",
"TreeViewsTips10": "Step 2: Set <code>Items</code> to get the component data source <b>Note</b> The data source type is <code>IEnumerable&lt;TreeItem&lt;TItem&gt;&gt;</code>", "TreeViewsTips10": "Step 2: Set <code>Items</code> to get the component data source <b>Note</b> The data source type is <code>IEnumerable&lt;TreeViewItem&lt;TItem&gt;&gt;</code>",
"TreeViewsTips11": "Step 3: Set the <code>OnExpandNodeAsync</code> callback delegate to expand the response node to get the child data source collection", "TreeViewsTips11": "Step 3: Set the <code>OnExpandNodeAsync</code> callback delegate to expand the response node to get the child data source collection",
"TreeViewsTips12": "Step 4: Set <code>ModelEqualityComparer</code> to provide a component identification model comparison delegate method, <b>Note</b> This setting is optional. Perform downgrade search through the fallback mechanism explained above", "TreeViewsTips12": "Step 4: Set <code>ModelEqualityComparer</code> to provide a component identification model comparison delegate method, <b>Note</b> This setting is optional. Perform downgrade search through the fallback mechanism explained above",
"TreeViewNormalTitle": "Basic usage", "TreeViewNormalTitle": "Basic usage",
@ -654,19 +654,19 @@
"TreeViewCheckboxButtonText": "refresh", "TreeViewCheckboxButtonText": "refresh",
"TreeViewTreeDisableTitle": "Disabled state", "TreeViewTreeDisableTitle": "Disabled state",
"TreeViewTreeDisableIntro": "Some nodes of the Tree can be set to disabled state", "TreeViewTreeDisableIntro": "Some nodes of the Tree can be set to disabled state",
"TreeViewTreeDisableDescription": "By setting the <code>Disabled</code> property of the data source <code>TreeItem</code> object, you can control whether this node can be checked or not. When set to <code>false</code>, it will not affect the node expansion. /shrink function", "TreeViewTreeDisableDescription": "By setting the <code>Disabled</code> property of the data source <code>TreeViewItem</code> object, you can control whether this node can be checked or not. When set to <code>false</code>, it will not affect the node expansion. /shrink function",
"TreeViewAccordionModelTitle": "Accordion mode", "TreeViewAccordionModelTitle": "Accordion mode",
"TreeViewAccordionModelIntro": "For nodes of the same level, only one can be expanded at a time", "TreeViewAccordionModelIntro": "For nodes of the same level, only one can be expanded at a time",
"TreeViewAccordionModelDescription": "Enable the accordion effect by setting the <code>IsAccordion</code> property of the <code>Tree</code> component", "TreeViewAccordionModelDescription": "Enable the accordion effect by setting the <code>IsAccordion</code> property of the <code>Tree</code> component",
"TreeViewDefaultExpandTitle": "Expanded by default and selected by default", "TreeViewDefaultExpandTitle": "Expanded by default and selected by default",
"TreeViewDefaultExpandIntro": "Some nodes of <code>Tree</code> can be set to be expanded or selected by default", "TreeViewDefaultExpandIntro": "Some nodes of <code>Tree</code> can be set to be expanded or selected by default",
"TreeViewDefaultExpandDescription": "By setting the <code>IsExpand</code> property of the <code>TreeItem</code> object, you can control whether this node is in the expanded state by default. In this example, <b>navigation 2</b> is in the expanded state by default, and the rest Node defaults to contracted state", "TreeViewDefaultExpandDescription": "By setting the <code>IsExpand</code> property of the <code>TreeViewItem</code> object, you can control whether this node is in the expanded state by default. In this example, <b>navigation 2</b> is in the expanded state by default, and the rest Node defaults to contracted state",
"TreeViewTreeDisplayIconTitle": "Show icon", "TreeViewTreeDisplayIconTitle": "Show icon",
"TreeViewTreeDisplayIconIntro": "Control whether the component displays the icon by setting <code>ShowIcon</code>", "TreeViewTreeDisplayIconIntro": "Control whether the component displays the icon by setting <code>ShowIcon</code>",
"TreeViewTreeDisplayIconDescription": "By setting the <code>ShowIcon</code> property of the <code>TreeItem</code> object, you can control whether this node displays the icon or not", "TreeViewTreeDisplayIconDescription": "By setting the <code>ShowIcon</code> property of the <code>TreeViewItem</code> object, you can control whether this node displays the icon or not",
"TreeViewTreeClickExpandTitle": "Click on the node to expand and contract", "TreeViewTreeClickExpandTitle": "Click on the node to expand and contract",
"TreeViewTreeClickExpandIntro": "By setting <code>ClickToggleNode</code> to control whether to expand and contract when the node is clicked", "TreeViewTreeClickExpandIntro": "By setting <code>ClickToggleNode</code> to control whether to expand and contract when the node is clicked",
"TreeViewTreeClickExpandDescription": "By setting the <code>ClickToggleNode</code> property of the <code>TreeItem</code> object, you can control whether this node can be expanded and contracted by clicking", "TreeViewTreeClickExpandDescription": "By setting the <code>ClickToggleNode</code> property of the <code>TreeViewItem</code> object, you can control whether this node can be expanded and contracted by clicking",
"TreeViewTreeValidationFormTitle": "The Tree component is built into the validation form", "TreeViewTreeValidationFormTitle": "The Tree component is built into the validation form",
"TreeViewTreeValidationFormIntro": "<code>Tree</code> can be enabled inside the component <code>Checkbox</code> will be displayed when it is built into the validation form <code>DisplayName</code> This function needs to be disabled in the tree component", "TreeViewTreeValidationFormIntro": "<code>Tree</code> can be enabled inside the component <code>Checkbox</code> will be displayed when it is built into the validation form <code>DisplayName</code> This function needs to be disabled in the tree component",
"TreeViewTreeValidationFormDescription": "Show <code>Checkbox</code> built into validation component <code>ValidateForm</code> by setting <code>ShowCheckbox</code> property does not show <code>DisplayName</code>", "TreeViewTreeValidationFormDescription": "Show <code>Checkbox</code> built into validation component <code>ValidateForm</code> by setting <code>ShowCheckbox</code> property does not show <code>DisplayName</code>",
@ -674,9 +674,9 @@
"TreeViewTreeLazyLoadingIntro": "Dynamically add child nodes when expanding a node", "TreeViewTreeLazyLoadingIntro": "Dynamically add child nodes when expanding a node",
"TreeViewTreeLazyLoadingDescription": "By setting the node <code>HasChildNode</code> to control whether to display the small arrow picture of the node. Add nodes through Tree's <code>OnExpandNodeAsync</code> delegate", "TreeViewTreeLazyLoadingDescription": "By setting the node <code>HasChildNode</code> to control whether to display the small arrow picture of the node. Add nodes through Tree's <code>OnExpandNodeAsync</code> delegate",
"TreeViewTreeCustomNodeTitle": "Custom node", "TreeViewTreeCustomNodeTitle": "Custom node",
"TreeViewTreeCustomNodeIntro": "Implement your own node template by setting <code>TreeItem</code> <code>Template</code>", "TreeViewTreeCustomNodeIntro": "Implement your own node template by setting <code>TreeViewItem</code> <code>Template</code>",
"TreeViewTreeNodeColorTitle": "Node color", "TreeViewTreeNodeColorTitle": "Node color",
"TreeViewTreeNodeColorIntro": "Implement your own node style by setting <code>TreeItem</code> <code>CssClass</code>", "TreeViewTreeNodeColorIntro": "Implement your own node style by setting <code>TreeViewItem</code> <code>CssClass</code>",
"TreeViewCheckedItemsTitle": "Get all selected nodes", "TreeViewCheckedItemsTitle": "Get all selected nodes",
"TreeViewCheckedItemsIntro": "Get all nodes by setting the <code>OnTreeItemChecked</code> callback delegate", "TreeViewCheckedItemsIntro": "Get all nodes by setting the <code>OnTreeItemChecked</code> callback delegate",
"TreeViewCustomCheckedItemsTitle": "Customize the selected node", "TreeViewCustomCheckedItemsTitle": "Customize the selected node",
@ -692,9 +692,14 @@
"TreeViewSetActiveTitle": "Set Active Node", "TreeViewSetActiveTitle": "Set Active Node",
"TreeViewSetActiveIntro": "Set the currently active node by calling the <code>SetActiveItem</code> method", "TreeViewSetActiveIntro": "Set the currently active node by calling the <code>SetActiveItem</code> method",
"TreeViewSetActiveDisplayText": "Current Active Node", "TreeViewSetActiveDisplayText": "Current Active Node",
"TreeViewsAttribute": "TreeItem property", "TreeViewsAttribute": "TreeViewItem property",
"TreeViewsDisableWholeTreeView": "Whole TreeView disable", "TreeViewsDisableWholeTreeView": "Whole TreeView disable",
"TreeViewsWhetherToExpandWhenDisable": "Whether to expand when the control node is disabled" "TreeViewsWhetherToExpandWhenDisable": "Whether to expand when the control node is disabled",
"OnMaxSelectedCountExceedTitle": "Maximum Info",
"OnMaxSelectedCountExceedContent": "You can select at most {0} items",
"TreeViewMaxSelectedCountTitle": "MaxSelectedCount",
"TreeViewMaxSelectedCountIntro": "Control the maximum number of selectable items by setting the <code>MaxSelectedCount</code> property, and handle the logic through the <code>OnMaxSelectedCountExceed</code> callback",
"TreeViewMaxSelectedCountDesc": "When more than 2 nodes are selected, a <code>Toast</code> prompt bar will pop up"
}, },
"BootstrapBlazor.Server.Components.Samples.Trees": { "BootstrapBlazor.Server.Components.Samples.Trees": {
"TreeIntro": "<p>ObsoleteThe <a href=\"treeviews\" alt=\"treeview\">TreeView</a> provides more functions", "TreeIntro": "<p>ObsoleteThe <a href=\"treeviews\" alt=\"treeview\">TreeView</a> provides more functions",

View File

@ -630,15 +630,15 @@
"TreeViewsTitle": "Tree 树形控件", "TreeViewsTitle": "Tree 树形控件",
"TreeViewsDescription": "用清晰的层级结构展示信息,可展开或折叠", "TreeViewsDescription": "用清晰的层级结构展示信息,可展开或折叠",
"TreeViewsTips1": "组件为泛型组件需要使用 <code>TItem</code> 指定绑定的数据模型,本例中模型为 <code>TreeFoo</code> 需要设置", "TreeViewsTips1": "组件为泛型组件需要使用 <code>TItem</code> 指定绑定的数据模型,本例中模型为 <code>TreeFoo</code> 需要设置",
"TreeViewsTips2": "设置 <code>TreeItem</code> 其 <code>IsExpand</code> 参数控制当前子节点是否展开", "TreeViewsTips2": "设置 <code>TreeViewItem</code> 其 <code>IsExpand</code> 参数控制当前子节点是否展开",
"TreeViewsTips3": "点击子项展开小箭头时,通过 <code>OnExpandNodeAsync</code> 回调委托方法获取子项数据集合", "TreeViewsTips3": "点击子项展开小箭头时,通过 <code>OnExpandNodeAsync</code> 回调委托方法获取子项数据集合",
"TreeViewsTips4": "保持节点状态回落机制,<code>ModelEqualityComparer</code> <code>CustomKeyAttribute</code> <code>IEqualityComparer&lt;TItem&gt;</code> <code>Equals</code> 重载方法", "TreeViewsTips4": "保持节点状态回落机制,<code>ModelEqualityComparer</code> <code>CustomKeyAttribute</code> <code>IEqualityComparer&lt;TItem&gt;</code> <code>Equals</code> 重载方法",
"TreeViewsTips5": "组件将会保持 <code>展开</code> <code>收缩</code> <code>选中</code> 状态", "TreeViewsTips5": "组件将会保持 <code>展开</code> <code>收缩</code> <code>选中</code> 状态",
"TreeViewsTips6": "通过 <code>TreeItem&lt;TItem&gt;.IsExpand</code> 设置节点是否 <b>展开</b> 状态", "TreeViewsTips6": "通过 <code>TreeViewItem&lt;TItem&gt;.IsExpand</code> 设置节点是否 <b>展开</b> 状态",
"TreeViewsTips7": "通过 <code>TreeItem&lt;TItem&gt;.IsActive</code> 设置节点是否 <b>选中</b> 状态", "TreeViewsTips7": "通过 <code>TreeViewItem&lt;TItem&gt;.IsActive</code> 设置节点是否 <b>选中</b> 状态",
"TreeViewsTips8": "通过 <code>TreeItem&lt;TItem&gt;.Checked</code> 设置节点是否 <b>复选/单选</b> 状态", "TreeViewsTips8": "通过 <code>TreeViewItem&lt;TItem&gt;.Checked</code> 设置节点是否 <b>复选/单选</b> 状态",
"TreeViewsTips9": "第一步:设置 <code>TItem</code> 泛型模型", "TreeViewsTips9": "第一步:设置 <code>TItem</code> 泛型模型",
"TreeViewsTips10": "第二步:设置 <code>Items</code> 获得组件数据源 <b>注意</b> 数据源类型为 <code>IEnumerable&lt;TreeItem&lt;TItem&gt;&gt;</code>", "TreeViewsTips10": "第二步:设置 <code>Items</code> 获得组件数据源 <b>注意</b> 数据源类型为 <code>IEnumerable&lt;TreeViewItem&lt;TItem&gt;&gt;</code>",
"TreeViewsTips11": "第三步:设置 <code>OnExpandNodeAsync</code> 回调委托响应节点展开获取子项数据源集合", "TreeViewsTips11": "第三步:设置 <code>OnExpandNodeAsync</code> 回调委托响应节点展开获取子项数据源集合",
"TreeViewsTips12": "第四步:设置 <code>ModelEqualityComparer</code> 提供组件识别模型比较委托方法,<b>注意</b> 本设置为可选项 通过上面讲解的回落机制进行降级搜索", "TreeViewsTips12": "第四步:设置 <code>ModelEqualityComparer</code> 提供组件识别模型比较委托方法,<b>注意</b> 本设置为可选项 通过上面讲解的回落机制进行降级搜索",
"TreeViewNormalTitle": "基础用法", "TreeViewNormalTitle": "基础用法",
@ -654,19 +654,19 @@
"TreeViewCheckboxButtonText": "刷新", "TreeViewCheckboxButtonText": "刷新",
"TreeViewTreeDisableTitle": "禁用状态", "TreeViewTreeDisableTitle": "禁用状态",
"TreeViewTreeDisableIntro": "可将 Tree 的某些节点设置为禁用状态", "TreeViewTreeDisableIntro": "可将 Tree 的某些节点设置为禁用状态",
"TreeViewTreeDisableDescription": "通过设置数据源 <code>TreeItem</code> 对象的 <code>Disabled</code> 属性,来控制此节点是否可以进行勾选动作,设置为 <code>false</code> 时不影响节点展开/收缩功能", "TreeViewTreeDisableDescription": "通过设置数据源 <code>TreeViewItem</code> 对象的 <code>Disabled</code> 属性,来控制此节点是否可以进行勾选动作,设置为 <code>false</code> 时不影响节点展开/收缩功能",
"TreeViewAccordionModelTitle": "手风琴模式", "TreeViewAccordionModelTitle": "手风琴模式",
"TreeViewAccordionModelIntro": "对于同一级的节点,每次只能展开一个", "TreeViewAccordionModelIntro": "对于同一级的节点,每次只能展开一个",
"TreeViewAccordionModelDescription": "通过设置 <code>Tree</code> 组件的 <code>IsAccordion</code> 属性开启手风琴效果", "TreeViewAccordionModelDescription": "通过设置 <code>Tree</code> 组件的 <code>IsAccordion</code> 属性开启手风琴效果",
"TreeViewDefaultExpandTitle": "默认展开和默认选中", "TreeViewDefaultExpandTitle": "默认展开和默认选中",
"TreeViewDefaultExpandIntro": "可将 <code>Tree</code> 的某些节点设置为默认展开或默认选中", "TreeViewDefaultExpandIntro": "可将 <code>Tree</code> 的某些节点设置为默认展开或默认选中",
"TreeViewDefaultExpandDescription": "通过设置 <code>TreeItem</code> 对象的 <code>IsExpand</code> 属性,来控制此节点是否默认为展开状态,本例中 <b>导航二</b> 默认为展开状态,其余节点默认为收缩状态", "TreeViewDefaultExpandDescription": "通过设置 <code>TreeViewItem</code> 对象的 <code>IsExpand</code> 属性,来控制此节点是否默认为展开状态,本例中 <b>导航二</b> 默认为展开状态,其余节点默认为收缩状态",
"TreeViewTreeDisplayIconTitle": "显示图标", "TreeViewTreeDisplayIconTitle": "显示图标",
"TreeViewTreeDisplayIconIntro": "通过设置 <code>ShowIcon</code> 来控制组件是否显示图标", "TreeViewTreeDisplayIconIntro": "通过设置 <code>ShowIcon</code> 来控制组件是否显示图标",
"TreeViewTreeDisplayIconDescription": "通过设置 <code>TreeItem</code> 对象的 <code>ShowIcon</code> 属性,来控制此节点是否显示图标", "TreeViewTreeDisplayIconDescription": "通过设置 <code>TreeViewItem</code> 对象的 <code>ShowIcon</code> 属性,来控制此节点是否显示图标",
"TreeViewTreeClickExpandTitle": "点击节点展开收缩功能", "TreeViewTreeClickExpandTitle": "点击节点展开收缩功能",
"TreeViewTreeClickExpandIntro": "通过设置 <code>ClickToggleNode</code> 来控制点击节点时是否进行展开收缩操作", "TreeViewTreeClickExpandIntro": "通过设置 <code>ClickToggleNode</code> 来控制点击节点时是否进行展开收缩操作",
"TreeViewTreeClickExpandDescription": "通过设置 <code>TreeItem</code> 对象的 <code>ClickToggleNode</code> 属性,来控制此节点是否通过点击来实现展开收缩操作", "TreeViewTreeClickExpandDescription": "通过设置 <code>TreeViewItem</code> 对象的 <code>ClickToggleNode</code> 属性,来控制此节点是否通过点击来实现展开收缩操作",
"TreeViewTreeValidationFormTitle": "Tree 组件内置到验证表单中", "TreeViewTreeValidationFormTitle": "Tree 组件内置到验证表单中",
"TreeViewTreeValidationFormIntro": "<code>Tree</code> 组件内部可开启 <code>Checkbox</code> 内置到验证表单时会显示 <code>DisplayName</code> 此功能在树状组件内需要禁止", "TreeViewTreeValidationFormIntro": "<code>Tree</code> 组件内部可开启 <code>Checkbox</code> 内置到验证表单时会显示 <code>DisplayName</code> 此功能在树状组件内需要禁止",
"TreeViewTreeValidationFormDescription": "通过设置 <code>ShowCheckbox</code> 属性显示 <code>Checkbox</code> 内置到验证组件 <code>ValidateForm</code> 中不显示 <code>DisplayName</code>", "TreeViewTreeValidationFormDescription": "通过设置 <code>ShowCheckbox</code> 属性显示 <code>Checkbox</code> 内置到验证组件 <code>ValidateForm</code> 中不显示 <code>DisplayName</code>",
@ -674,9 +674,9 @@
"TreeViewTreeLazyLoadingIntro": "展开节点时动态添加子节点", "TreeViewTreeLazyLoadingIntro": "展开节点时动态添加子节点",
"TreeViewTreeLazyLoadingDescription": "通过设置节点 <code>HasChildNode</code> 控制是否显示节点小箭头图片 。通过Tree的 <code>OnExpandNodeAsync</code> 委托添加节点", "TreeViewTreeLazyLoadingDescription": "通过设置节点 <code>HasChildNode</code> 控制是否显示节点小箭头图片 。通过Tree的 <code>OnExpandNodeAsync</code> 委托添加节点",
"TreeViewTreeCustomNodeTitle": "自定义节点", "TreeViewTreeCustomNodeTitle": "自定义节点",
"TreeViewTreeCustomNodeIntro": "通过设置 <code>TreeItem</code> <code>Template</code> 来实现自己的节点模板", "TreeViewTreeCustomNodeIntro": "通过设置 <code>TreeViewItem</code> <code>Template</code> 来实现自己的节点模板",
"TreeViewTreeNodeColorTitle": "节点颜色", "TreeViewTreeNodeColorTitle": "节点颜色",
"TreeViewTreeNodeColorIntro": "通过设置 <code>TreeItem</code> <code>CssClass</code> 来实现自己的节点样式", "TreeViewTreeNodeColorIntro": "通过设置 <code>TreeViewItem</code> <code>CssClass</code> 来实现自己的节点样式",
"TreeViewCheckedItemsTitle": "获取所有选中节点", "TreeViewCheckedItemsTitle": "获取所有选中节点",
"TreeViewCheckedItemsIntro": "通过设置 <code>OnTreeItemChecked</code> 回调委托获取所有节点", "TreeViewCheckedItemsIntro": "通过设置 <code>OnTreeItemChecked</code> 回调委托获取所有节点",
"TreeViewCustomCheckedItemsTitle": "自定义选中节点", "TreeViewCustomCheckedItemsTitle": "自定义选中节点",
@ -692,9 +692,14 @@
"TreeViewSetActiveTitle": "设置激活节点", "TreeViewSetActiveTitle": "设置激活节点",
"TreeViewSetActiveIntro": "通过调用 <code>SetActiveItem</code> 方法设置当前激活节点", "TreeViewSetActiveIntro": "通过调用 <code>SetActiveItem</code> 方法设置当前激活节点",
"TreeViewSetActiveDisplayText": "当前激活节点", "TreeViewSetActiveDisplayText": "当前激活节点",
"TreeViewsAttribute": "TreeItem 属性", "TreeViewsAttribute": "TreeViewItem 属性",
"TreeViewsDisableWholeTreeView": "是否禁用整个TreeView", "TreeViewsDisableWholeTreeView": "是否禁用整个 TreeView",
"TreeViewsWhetherToExpandWhenDisable": "禁用时候是否可以展开或折叠子节点" "TreeViewsWhetherToExpandWhenDisable": "禁用时候是否可以展开或折叠子节点",
"OnMaxSelectedCountExceedTitle": "可选最大数量提示",
"OnMaxSelectedCountExceedContent": "最多只能选择 {0} 项",
"TreeViewMaxSelectedCountTitle": "最大选择数量",
"TreeViewMaxSelectedCountIntro": "通过设置 <code>MaxSelectedCount</code> 属性控制最大可选数量,通过 <code>OnMaxSelectedCountExceed</code> 回调处理逻辑",
"TreeViewMaxSelectedCountDesc": "选中节点超过 2 个时,弹出 <code>Toast</code> 提示栏"
}, },
"BootstrapBlazor.Server.Components.Samples.Trees": { "BootstrapBlazor.Server.Components.Samples.Trees": {
"TreeIntro": "<p>本组件已弃用,请使用新组件 <a href=\"treeviews\" alt=\"treeview\">TreeView</a> 提供更多功能", "TreeIntro": "<p>本组件已弃用,请使用新组件 <a href=\"treeviews\" alt=\"treeview\">TreeView</a> 提供更多功能",

View File

@ -164,8 +164,6 @@ public partial class Checkbox<TValue> : ValidateBase<TValue>
/// <summary> /// <summary>
/// 触发 OnBeforeStateChanged 回调方法 由 JavaScript 调用 /// 触发 OnBeforeStateChanged 回调方法 由 JavaScript 调用
/// </summary> /// </summary>
/// <param name="v"></param>
/// <returns></returns>
[JSInvokable] [JSInvokable]
public async Task TriggerOnBeforeStateChanged() public async Task TriggerOnBeforeStateChanged()
{ {

View File

@ -63,6 +63,7 @@ else
{ {
<Checkbox Value="@item.CheckedState" IsDisabled="GetItemDisabledState(item)" SkipValidate="true" <Checkbox Value="@item.CheckedState" IsDisabled="GetItemDisabledState(item)" SkipValidate="true"
ShowLabel="false" ShowAfterLabel="false" @bind-State="@item.CheckedState" ShowLabel="false" ShowAfterLabel="false" @bind-State="@item.CheckedState"
OnBeforeStateChanged="_onBeforeStateChangedCallback"
OnStateChanged="(state, v) => OnCheckStateChanged(item, true)" StopPropagation="true" /> OnStateChanged="(state, v) => OnCheckStateChanged(item, true)" StopPropagation="true" />
} }
<DynamicElement class="@GetNodeClassString(item)" TriggerClick="TriggerNodeLabel(item)" OnClick="() => OnClick(item)"> <DynamicElement class="@GetNodeClassString(item)" TriggerClick="TriggerNodeLabel(item)" OnClick="() => OnClick(item)">

View File

@ -184,6 +184,18 @@ public partial class TreeView<TItem> : IModelEqualityComparer<TItem>
[Parameter] [Parameter]
public bool ShowCheckbox { get; set; } public bool ShowCheckbox { get; set; }
/// <summary>
/// 获得/设置 最多选中数量
/// </summary>
[Parameter]
public int MaxSelectedCount { get; set; }
/// <summary>
/// 获得/设置 超过最大选中数量时回调委托
/// </summary>
[Parameter]
public Func<Task>? OnMaxSelectedCountExceed { get; set; }
/// <summary> /// <summary>
/// 获得/设置 是否显示 Icon 图标 默认 false 不显示 /// 获得/设置 是否显示 Icon 图标 默认 false 不显示
/// </summary> /// </summary>
@ -271,6 +283,8 @@ public partial class TreeView<TItem> : IModelEqualityComparer<TItem>
private string? _searchText; private string? _searchText;
private Func<CheckboxState, Task<bool>>? _onBeforeStateChangedCallback;
/// <summary> /// <summary>
/// <inheritdoc/> /// <inheritdoc/>
/// </summary> /// </summary>
@ -332,6 +346,9 @@ public partial class TreeView<TItem> : IModelEqualityComparer<TItem>
ActiveItem ??= Items.FirstOrDefaultActiveItem(); ActiveItem ??= Items.FirstOrDefaultActiveItem();
ActiveItem?.SetParentExpand<TreeViewItem<TItem>, TItem>(true); ActiveItem?.SetParentExpand<TreeViewItem<TItem>, TItem>(true);
} }
_onBeforeStateChangedCallback = (ShowCheckbox && MaxSelectedCount > 0) ? new Func<CheckboxState, Task<bool>>(OnBeforeStateChanged)
: null;
} }
async Task CheckExpand(IEnumerable<TreeViewItem<TItem>> nodes) async Task CheckExpand(IEnumerable<TreeViewItem<TItem>> nodes)
@ -488,6 +505,22 @@ public partial class TreeView<TItem> : IModelEqualityComparer<TItem>
} }
} }
private async Task<bool> OnBeforeStateChanged(CheckboxState state)
{
var ret = true;
if (state == CheckboxState.Checked)
{
var items = GetCheckedItems().Where(i => i.HasChildren == false).ToList();
ret = items.Count < MaxSelectedCount;
}
if (!ret && OnMaxSelectedCountExceed != null)
{
await OnMaxSelectedCountExceed();
}
return ret;
}
/// <summary> /// <summary>
/// 节点 Checkbox 状态改变时触发此方法 /// 节点 Checkbox 状态改变时触发此方法
/// </summary> /// </summary>
@ -496,8 +529,6 @@ public partial class TreeView<TItem> : IModelEqualityComparer<TItem>
/// <returns></returns> /// <returns></returns>
private async Task OnCheckStateChanged(TreeViewItem<TItem> item, bool shouldRender = false) private async Task OnCheckStateChanged(TreeViewItem<TItem> item, bool shouldRender = false)
{ {
//item.CheckedState = ToggleCheckState(item.CheckedState);
if (AutoCheckChildren) if (AutoCheckChildren)
{ {
// 向下级联操作 // 向下级联操作

View File

@ -206,6 +206,65 @@ public class TreeViewTest : BootstrapBlazorTestBase
cut.WaitForAssertion(() => cut.Contains("fa-solid fa-font-awesome")); cut.WaitForAssertion(() => cut.Contains("fa-solid fa-font-awesome"));
} }
[Fact]
public async Task OnMaxSelectedCountExceed_Ok()
{
bool max = false;
var items = TreeFoo.CascadingTree(new List<TreeFoo>()
{
new() { Text = "navigation one", Id = "1010", Icon = "fa-solid fa-font-awesome" },
new() { Text = "Navigation two", Id = "1020", Icon = "fa-solid fa-font-awesome" },
new() { Text = "Navigation three", Id = "1030", Icon = "fa-solid fa-font-awesome" }
});
var cut = Context.RenderComponent<TreeView<TreeFoo>>(pb =>
{
pb.Add(a => a.ShowCheckbox, true);
pb.Add(a => a.MaxSelectedCount, 2);
pb.Add(a => a.Items, items);
pb.Add(a => a.OnMaxSelectedCountExceed, () =>
{
max = true;
return Task.CompletedTask;
});
});
var checkboxes = cut.FindComponents<Checkbox<CheckboxState>>();
Assert.Equal(3, checkboxes.Count);
await cut.InvokeAsync(async () =>
{
await checkboxes[0].Instance.TriggerOnBeforeStateChanged();
});
Assert.Equal(CheckboxState.Checked, checkboxes[0].Instance.State);
await cut.InvokeAsync(async () =>
{
await checkboxes[1].Instance.TriggerOnBeforeStateChanged();
});
Assert.Equal(CheckboxState.Checked, checkboxes[1].Instance.State);
// 选中第三个由于限制无法选中
await cut.InvokeAsync(async () =>
{
await checkboxes[2].Instance.TriggerOnBeforeStateChanged();
});
Assert.Equal(CheckboxState.Checked, checkboxes[0].Instance.State);
Assert.Equal(CheckboxState.Checked, checkboxes[1].Instance.State);
Assert.Equal(CheckboxState.UnChecked, checkboxes[2].Instance.State);
Assert.True(max);
// 取消选择第一个
max = false;
await cut.InvokeAsync(async () =>
{
await checkboxes[0].Instance.TriggerOnBeforeStateChanged();
});
Assert.Equal(CheckboxState.UnChecked, checkboxes[0].Instance.State);
Assert.Equal(CheckboxState.Checked, checkboxes[1].Instance.State);
Assert.Equal(CheckboxState.UnChecked, checkboxes[2].Instance.State);
Assert.False(max);
}
[Fact] [Fact]
public void Template_Ok() public void Template_Ok()
{ {