diff --git a/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor b/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor index 97270a8e75b..53beb0200ca 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor @@ -27,7 +27,7 @@ Introduction="@Localizer["TreeViewNormalIntro"]" Name="Normal">
@((MarkupString)Localizer["TreeViewNormalDescription"].Value)
- + @@ -203,6 +203,16 @@ + +
+

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

+
+ + +
+ diff --git a/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs index 45fdeba725e..9745d90689f 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs @@ -30,6 +30,8 @@ public sealed partial class TreeViews private List> Items { get; } = TreeFoo.GetTreeItems(); + private List> EditItems { get; } = TreeFoo.GetTreeItems(); + private bool AutoCheckChildren { get; set; } private bool AutoCheckParent { get; set; } @@ -218,6 +220,12 @@ private static async Task>> OnExpandVirtualNod return items; } + private Task OnUpdateCallbackAsync(TreeFoo foo, string? text) + { + foo.Text = text; + return Task.FromResult(true); + } + /// /// 获得属性方法 /// diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index 80eede68798..b9af6258e8f 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -711,7 +711,10 @@ "TreeViewEnableKeyboardArrowUpDownIntro": "Support keyboard up and down arrow operations by setting EnableKeyboardArrowUpDown=\"true\". ArrowLeft collapse the node, ArrowRight expand the node, ArrowUp move the node up, ArrowDown move the node down, Space select the node,", "TreeViewVirtualizeTitle": "Virtualize", "TreeViewVirtualizeIntro": "Enable virtual scrolling by setting IsVirtualize=\"true\" to support big data", - "TreeViewVirtualizeDescription": "The component uses Virtualize to implement virtual scrolling logic, which reduces the pressure on the browser. However, if there is a lot of tree structure data, such as Select All, all data must be marked, resulting in large data in the memory. This problem has not been solved yet. Currently, this component still puts a lot of pressure on the CPU due to large data." + "TreeViewVirtualizeDescription": "The component uses Virtualize to implement virtual scrolling logic, which reduces the pressure on the browser. However, if there is a lot of tree structure data, such as Select All, all data must be marked, resulting in large data in the memory. This problem has not been solved yet. Currently, this component still puts a lot of pressure on the CPU due to large data.", + "TreeViewShowToolbarTitle": "Show Toolbar", + "TreeViewShowToolbarIntro": "Show the toolbar by setting ShowToolbar=\"true\"", + "TreeViewShowToolbarDesc": "After it is turned on, when the mouse hovers over a node, a toolbar icon appears on the right, and the toolbar function can be customized; data is updated by setting the OnUpdateCallbackAsync callback method; the toolbar template is defined by setting ToolbarTemplate, and if not set, the internal default node name template is used" }, "BootstrapBlazor.Server.Components.Samples.SwitchButtons": { "SwitchButtonsTitle": "Switch Button state switch button", diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index 49ef713068a..9f0918a687f 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -711,7 +711,10 @@ "TreeViewEnableKeyboardArrowUpDownIntro": "通过设置 EnableKeyboardArrowUpDown=\"true\" 支持键盘上下箭头操作。左箭头 收起节点,右箭头 展开节点,上箭头 向上移动节点,下箭头 向下移动节点,空格 选中节点", "TreeViewVirtualizeTitle": "虚拟滚动", "TreeViewVirtualizeIntro": "通过设置 IsVirtualize=\"true\" 开启虚拟滚动,支持大数据", - "TreeViewVirtualizeDescription": "组件内部使用 Virtualize 来实现虚拟滚动逻辑,对浏览器压力会减少很多;但是如果树状结构数据比较多,比如 全选 等操作必须对所有数据进行标记,导致内存中确实有大数据存在,目前还没有解决这个问题,目前此组件由于大数据对 CPU 压力还是比较大的" + "TreeViewVirtualizeDescription": "组件内部使用 Virtualize 来实现虚拟滚动逻辑,对浏览器压力会减少很多;但是如果树状结构数据比较多,比如 全选 等操作必须对所有数据进行标记,导致内存中确实有大数据存在,目前还没有解决这个问题,目前此组件由于大数据对 CPU 压力还是比较大的", + "TreeViewShowToolbarTitle": "显示工具栏", + "TreeViewShowToolbarIntro": "通过设置 ShowToolbar=\"true\" 显示工具栏", + "TreeViewShowToolbarDesc": "开启后鼠标悬浮节点时,右侧出现工具栏图标,可自定义工具栏功能;通过设置 OnUpdateCallbackAsync 回调方法进行数据的更新;通过设置 ToolbarTemplate 定义工具栏模板,如未设置时使用内部默认更改节点名称模板" }, "BootstrapBlazor.Server.Components.Samples.SwitchButtons": { "SwitchButtonsTitle": "Switch Button 状态切换按钮", diff --git a/src/BootstrapBlazor/Components/TreeView/TreeView.razor b/src/BootstrapBlazor/Components/TreeView/TreeView.razor index de3fa3df839..a479ef8eac0 100644 --- a/src/BootstrapBlazor/Components/TreeView/TreeView.razor +++ b/src/BootstrapBlazor/Components/TreeView/TreeView.razor @@ -8,7 +8,7 @@ { if (_init) { - + } else if (ShowSkeleton) { @@ -48,9 +48,7 @@ else {
- - @RenderTreeRow(context) - + @RenderRow(context)
} @@ -59,7 +57,7 @@ else
@foreach (var item in Rows) { - @RenderTreeRow(item) + @RenderRow(item) }
} @@ -67,33 +65,16 @@ else } @code { - private RenderFragment> RenderTreeRow => item => - @
-
-
- - - @if (ShowCheckbox) - { - - } - - @if (ShowIcon) - { - - } - @if (item.Template == null) - { - @item.Text - } - else - { - @item.Template(item.Value) - } - -
-
; + RenderFragment RenderRow(TreeViewItem item) => + @; } diff --git a/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs b/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs index a1b1a7aacd2..1bea8c3904a 100644 --- a/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs +++ b/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs @@ -9,208 +9,165 @@ namespace BootstrapBlazor.Components; /// -/// Tree 组件 +/// Tree component /// [CascadingTypeParameter(nameof(TItem))] public partial class TreeView : IModelEqualityComparer { - /// - /// 获得 按钮样式集合 - /// private string? ClassString => CssBuilder.Default("tree-view") .AddClass("is-fixed-search", ShowSearch && IsFixedSearch) .AddClassFromAttributes(AdditionalAttributes) .Build(); - /// - /// 获得 Loading 样式集合 - /// private string? LoadingClassString => CssBuilder.Default("table-loading") .AddClassFromAttributes(AdditionalAttributes) .Build(); - /// - /// 获得/设置 TreeItem 图标 - /// - /// - /// - private static string? GetIconClassString(TreeViewItem item) => CssBuilder.Default("tree-icon") - .AddClass(item.Icon) - .AddClass(item.ExpandIcon, item.IsExpand && !string.IsNullOrEmpty(item.ExpandIcon)) - .Build(); + private TreeViewItem? _activeItem; /// - /// 获得/设置 TreeItem 小箭头样式 + /// Gets or sets whether to show the loading animation. Default is false. /// - /// - /// - private string? GetCaretClassString(TreeViewItem item) => CssBuilder.Default("node-icon") - .AddClass("visible", item.HasChildren || item.Items.Count > 0) - .AddClass(NodeIcon, !item.IsExpand) - .AddClass(ExpandNodeIcon, item.IsExpand) - .AddClass("disabled", IsDisabled || (!CanExpandWhenDisabled && item.IsDisabled)) - .Build(); - - private string? NodeLoadingClassString => CssBuilder.Default("node-icon node-loading") - .AddClass(LoadingIcon) - .Build(); - - private string? GetContentClassString(TreeViewItem item) => CssBuilder.Default("tree-content") - .AddClass("active", _activeItem == item) - .Build(); - - private string? GetNodeClassString(TreeViewItem item) => CssBuilder.Default("tree-node") - .AddClass("disabled", GetItemDisabledState(item)) - .Build(); - - private bool CanTriggerClickNode(TreeViewItem item) => !IsDisabled && (CanExpandWhenDisabled || !item.IsDisabled); - - private bool TriggerNodeLabel(TreeViewItem item) => !GetItemDisabledState(item); - - private bool GetItemDisabledState(TreeViewItem item) => item.IsDisabled || IsDisabled; + [Obsolete("Deprecated. Please remove it.")] + [ExcludeFromCodeCoverage] + public bool IsReset { get; set; } /// - /// 获得/设置 选中节点 默认 null + /// Gets or sets whether show the toolbar of tree view item. Default is false. /// - private TreeViewItem? _activeItem; + [Parameter] + public bool ShowToolbar { get; set; } /// - /// 获得/设置 是否显示正在加载动画 默认为 false + /// Gets or sts A callback method that determines whether to show the toolbar of the tree view item. /// - [Obsolete("已弃用 直接删除即可;Deprecated Please remove it")] - [ExcludeFromCodeCoverage] - public bool IsReset { get; set; } + [Parameter] + public Func, Task>? ShowToolbarCallback { get; set; } /// - /// 获得/设置 是否禁用整个组件 默认 false + /// Gets or sets whether the entire component is disabled. Default is false. /// [Parameter] public bool IsDisabled { get; set; } /// - /// 获得/设置 当节点被禁用时 是否可以进行折叠展开操作 默认 false + /// Gets or sets whether nodes can be expanded or collapsed when the component is disabled. Default is false. /// [Parameter] public bool CanExpandWhenDisabled { get; set; } /// - /// 获得/设置 是否为手风琴效果 默认为 false 虚拟滚动模式下不支持手风琴效果 + /// Gets or sets whether the tree view has accordion behavior. Default is false. Accordion behavior is not supported in virtual scrolling mode. /// [Parameter] public bool IsAccordion { get; set; } /// - /// 获得/设置 是否点击节点时展开或者收缩子项 默认 false + /// Gets or sets whether clicking a node expands or collapses its children. Default is false. /// [Parameter] public bool ClickToggleNode { get; set; } /// - /// 获得/设置 是否点击节点自动切换 Checkbox 状态 默认 false 时生效 + /// Gets or sets whether clicking a node toggles its checkbox state. Default is false. Effective when is true. /// [Parameter] public bool ClickToggleCheck { get; set; } /// - /// 获得/设置 是否显示加载骨架屏 默认 false 不显示 + /// Gets or sets whether to show the loading skeleton. Default is false. /// [Parameter] public bool ShowSkeleton { get; set; } /// - /// 获得/设置 是否显示搜索栏 默认 false 不显示 + /// Gets or sets whether to show the search bar. Default is false. /// [Parameter] public bool ShowSearch { get; set; } /// - /// 获得/设置 是否固定搜索栏 默认 false 不固定 + /// Gets or sets whether the search bar is fixed. Default is false. /// [Parameter] public bool IsFixedSearch { get; set; } /// - /// 获得/设置 是否显示重置搜索栏按钮 默认 true 显示 + /// Gets or sets whether to show the reset search button. Default is true. /// [Parameter] public bool ShowResetSearchButton { get; set; } = true; /// - /// 获得/设置 搜索栏模板 默认 null + /// Gets or sets the search bar template. Default is null. /// [Parameter] public RenderFragment? SearchTemplate { get; set; } /// - /// 获得/设置 搜索栏图标 默认 未设置 使用主题内置图标 + /// Gets or sets the search icon. Default is not set, using the built-in theme icon. /// [Parameter] public string? SearchIcon { get; set; } /// - /// 获得/设置 清除搜索栏图标 默认 未设置 使用主题内置图标 + /// Gets or sets the clear search icon. Default is not set, using the built-in theme icon. /// [Parameter] public string? ClearSearchIcon { get; set; } /// - /// 获得/设置 搜索回调方法 默认 null 未设置 + /// Gets or sets the search callback method. Default is null. /// - /// 通过设置 开启 + /// Enabled by setting to true. [Parameter] public Func>?>>? OnSearchAsync { get; set; } /// - /// 获得/设置 带层次数据集合 + /// Gets or sets the hierarchical data collection. /// [Parameter] [NotNull] public List>? Items { get; set; } - ///// - ///// 获得/设置 扁平化数据集合注意 参数一定要赋值,不然无法呈现层次结构 - ///// - //[Parameter] - //public List>? FlatItems { get; set; } - /// - /// 获得/设置 是否显示 CheckBox 默认 false 不显示 + /// Gets or sets whether to show checkboxes. Default is false. /// [Parameter] public bool ShowCheckbox { get; set; } /// - /// 获得/设置 最多选中数量 + /// Gets or sets the maximum number of selected items. /// [Parameter] public int MaxSelectedCount { get; set; } /// - /// 获得/设置 超过最大选中数量时回调委托 + /// Gets or sets the callback method when the maximum number of selected items is exceeded. /// [Parameter] public Func? OnMaxSelectedCountExceed { get; set; } /// - /// 获得/设置 是否显示 Icon 图标 默认 false 不显示 + /// Gets or sets whether to show icons. Default is false. /// [Parameter] public bool ShowIcon { get; set; } /// - /// 获得/设置 树形控件节点点击时回调委托 + /// Gets or sets the callback method when a tree item is clicked. /// [Parameter] public Func, Task>? OnTreeItemClick { get; set; } /// - /// 获得/设置 树形控件节点选中时回调委托 + /// Gets or sets the callback method when a tree item is checked. /// [Parameter] public Func>, Task>? OnTreeItemChecked { get; set; } /// - /// 获得/设置 点击节点获取子数据集合回调方法 + /// Gets or sets the callback method to get child data when a node is expanded. /// [Parameter] public Func, Task>>>? OnExpandNodeAsync { get; set; } @@ -222,62 +179,83 @@ public partial class TreeView : IModelEqualityComparer public Type CustomKeyAttribute { get; set; } = typeof(KeyAttribute); /// - /// 获得/设置 比较数据是否相同回调方法 默认为 null + /// /// - /// 提供此回调方法时忽略 属性 [Parameter] public Func? ModelEqualityComparer { get; set; } /// - /// 获得/设置 Tree Node 正在加载动画图标 + /// Gets or sets the loading icon for tree nodes. /// [Parameter] public string? LoadingIcon { get; set; } /// - /// 获得/设置 Tree Node 节点图标 + /// Gets or sets the icon for tree nodes. /// [Parameter] public string? NodeIcon { get; set; } /// - /// 获得/设置 Tree Node 展开节点图标 + /// Gets or sets the icon for expanded tree nodes. /// [Parameter] public string? ExpandNodeIcon { get; set; } /// - /// 获得/设置 是否开启键盘上下左右按键操作 默认 false - /// ArrowLeft 收起节点 - /// ArrowRight 展开节点 - /// ArrowUp 向上移动节点 - /// ArrowDown 向下移动节点 - /// Space 选中当前节点 + /// Gets or sets whether to enable keyboard navigation. Default is false. + /// ArrowLeft collapses the node. + /// ArrowRight expands the node. + /// ArrowUp moves to the previous node. + /// ArrowDown moves to the next node. + /// Space selects the current node. /// [Parameter] public bool EnableKeyboard { get; set; } /// - /// 获得/设置 是否键盘上下键操作当前选中节点与视窗关系配置 默认 null 使用 { behavior: "smooth", block: "nearest", inline: "start" } + /// Gets or sets the scroll into view options for keyboard navigation. Default is null, using { behavior: "smooth", block: "nearest", inline: "start" }. /// [Parameter] public ScrollIntoViewOptions? ScrollIntoViewOptions { get; set; } /// - /// 获得/设置 是否启用虚拟滚动 默认 false 不启用 + /// Gets or sets whether to enable virtual scrolling. Default is false. /// [Parameter] public bool IsVirtualize { get; set; } /// - /// 获得/设置 虚拟滚动行高 默认为 38 + /// Gets or sets the row height for virtual scrolling. Default is 38. /// - /// 需要设置 值为 Virtual 时生效 + /// Effective when is set to Virtual. [Parameter] public float RowHeight { get; set; } = 38f; - [CascadingParameter] - private ContextMenuZone? ContextMenuZone { get; set; } + /// + /// Gets or sets the toolbar content template. Default is null. + /// + [Parameter] + public RenderFragment? ToolbarTemplate { get; set; } + + /// + /// Gets or sets the title of the popup-window. Default is null. + /// + [Parameter] + public string? ToolbarEditTitle { get; set; } + + /// + /// Gets or sets the title of the popup-window. Default is null. + /// + [Parameter] + public string? ToolbarEditLabelText { get; set; } + + /// + /// Gets or sets the update the tree text value callback. Default is null. + /// If return true will update the tree text value, otherwise will not update. + /// + [Parameter] + public Func>? OnUpdateCallbackAsync { get; set; } [NotNull] private string? NotSetOnTreeExpandErrorMessage { get; set; } @@ -290,24 +268,17 @@ public partial class TreeView : IModelEqualityComparer [NotNull] private IIconTheme? IconTheme { get; set; } - [Inject] - [NotNull] - private IOptionsMonitor? Options { get; set; } - - /// - /// 节点状态缓存类实例 - /// [NotNull] - private TreeNodeCache, TItem>? TreeNodeStateCache { get; set; } + private TreeNodeCache, TItem>? _treeNodeStateCache = null; /// - /// 改变节点状态后自动更新子节点 默认 false + /// Gets or sets whether to automatically update child nodes when the node state changes. Default is false. /// [Parameter] public bool AutoCheckChildren { get; set; } /// - /// 改变节点状态后自动更新父节点 默认 false + /// Gets or sets whether to automatically update parent nodes when the node state changes. Default is false. /// [Parameter] public bool AutoCheckParent { get; set; } @@ -318,10 +289,6 @@ public partial class TreeView : IModelEqualityComparer private bool _shouldRender = true; - private static string? GetItemTextClassString(TreeViewItem item) => CssBuilder.Default("tree-node-text") - .AddClass(item.CssClass) - .Build(); - private bool _init; /// @@ -331,9 +298,10 @@ protected override void OnInitialized() { base.OnInitialized(); - // 初始化节点缓存 - TreeNodeStateCache ??= new(this); + _treeNodeStateCache = new(this); NotSetOnTreeExpandErrorMessage = Localizer[nameof(NotSetOnTreeExpandErrorMessage)]; + ToolbarEditTitle ??= Localizer[nameof(ToolbarEditTitle)]; + ToolbarEditLabelText ??= Localizer[nameof(ToolbarEditLabelText)]; } /// @@ -370,19 +338,16 @@ protected override async Task OnParametersSetAsync() if (ShowCheckbox && (AutoCheckParent || AutoCheckChildren)) { - // 开启 Checkbox 功能时初始化选中节点 - TreeNodeStateCache.IsChecked(Items); + _treeNodeStateCache.IsChecked(Items); } - // 从数据源中恢复当前 active 节点 if (_activeItem != null) { - _activeItem = TreeNodeStateCache.Find(Items, _activeItem.Value, out _); + _activeItem = _treeNodeStateCache.Find(Items, _activeItem.Value, out _); } if (_init == false) { - // 设置 ActiveItem 默认值 _activeItem ??= Items.FirstOrDefaultActiveItem(); _activeItem?.SetParentExpand, TItem>(true); _init = true; @@ -421,15 +386,15 @@ protected override async Task OnAfterRenderAsync(bool firstRender) private bool _keyboardArrowUpDownTrigger; /// - /// 客户端用户键盘操作处理方法 由 JavaScript 调用 + /// Client-side user keyboard operation handler method called by JavaScript /// /// /// [JSInvokable] public async ValueTask TriggerKeyDown(string key) { - // 通过 ActiveItem 找到兄弟节点 - // 如果兄弟节点没有时,找到父亲节点 + // Find sibling nodes through ActiveItem + // If there are no sibling nodes, find the parent node if (_activeItem != null) { if (key == "ArrowUp" || key == "ArrowDown") @@ -439,13 +404,13 @@ public async ValueTask TriggerKeyDown(string key) } else if (key == "ArrowLeft" || key == "ArrowRight") { - await OnToggleNodeAsync(_activeItem, true); + await OnToggleNodeAsync(_activeItem); } } } /// - /// 客户端查询指定行选择框状态方法 由 JavaScript 调用 + /// Client-side method to query the state of the specified row checkbox, called by JavaScript /// /// /// @@ -467,7 +432,7 @@ public Task> GetParentsState(List items) return Task.FromResult(result); } - private static bool IsExpand(TreeViewItem item) => item.IsExpand && item.Items.Count > 0; + private static bool IsExpand(TreeViewItem item) => item is { IsExpand: true, Items.Count: > 0 }; private List> GetItems(TreeViewItem item) => item.Parent?.Items ?? Items; @@ -540,7 +505,6 @@ private async Task OnBeforeStateChangedCallback(TreeViewItem item, { if (state == CheckboxState.Checked) { - // 展开节点 var items = GetCheckedItems().Where(i => i.HasChildren == false).ToList(); var count = items.Count + item.GetAllTreeSubItems().Count(); ret = count < MaxSelectedCount; @@ -556,10 +520,9 @@ private async Task OnBeforeStateChangedCallback(TreeViewItem item, async Task CheckExpand(IEnumerable> nodes) { - // 恢复当前节点状态 foreach (var node in nodes) { - await TreeNodeStateCache.CheckExpandAsync(node, GetChildrenRowAsync); + await _treeNodeStateCache.CheckExpandAsync(node, GetChildrenRowAsync); if (node.Items.Count > 0) { @@ -581,16 +544,12 @@ private async Task>> GetChildrenRowAsync(Tree return ret; } - /// - /// 选中节点时触发此方法 - /// - /// private async Task OnClick(TreeViewItem item) { _activeItem = item; - if (ClickToggleNode && CanTriggerClickNode(item)) + if (ClickToggleNode && item.CanTriggerClickNode(IsDisabled, CanExpandWhenDisabled)) { - await OnToggleNodeAsync(item, false); + await OnToggleNodeAsync(item); } if (OnTreeItemClick != null) @@ -637,7 +596,7 @@ private Task OnClickResetSearch() } /// - /// 设置选中节点 + /// Set the active node /// public void SetActiveItem(TreeViewItem? item) { @@ -647,30 +606,17 @@ public void SetActiveItem(TreeViewItem? item) } /// - /// 重新设置 数据源方法 + /// Set the data source method for /// public void SetItems(List> items) { - //FlatItems = null; Items = items; _rows = null; StateHasChanged(); } - ///// - ///// 重新设置 数据源方法 - ///// - ///// - //public void SetFlatItems(List> flatItems) - //{ - // Items = null; - // FlatItems = flatItems; - // _rows = null; - // StateHasChanged(); - //} - /// - /// 设置选中节点 + /// Set the active node /// public void SetActiveItem(TItem item) { @@ -685,42 +631,28 @@ public void SetActiveItem(TItem item) }; /// - /// 切换节点展开收缩状态方法 + /// Toggle node expand collapse state method /// /// - /// - private async Task OnToggleNodeAsync(TreeViewItem node, bool shouldRender) + private async Task OnToggleNodeAsync(TreeViewItem node) { // 手风琴效果逻辑 node.IsExpand = !node.IsExpand; - //// 如果节点设置有子节点但是当前没有时调用 GetChildrenRowAsync 方法 - //if (node.IsExpand && node.HasChildren && node.Items.Count == 0) - //{ - // var items = await GetChildrenRowAsync(node); - // if (items != null) - // { - // foreach (var item in items) - // { - // item.Parent = node; - // node.Items.Add(item); - // } - // } - //} if (IsAccordion && !IsVirtualize) { - await TreeNodeStateCache.ToggleNodeAsync(node, GetChildrenRowAsync); + await _treeNodeStateCache.ToggleNodeAsync(node, GetChildrenRowAsync); // 展开此节点关闭其他同级节点 if (node.IsExpand) { // 通过 item 找到父节点 - var nodes = TreeNodeStateCache.FindParentNode(Items, node)?.Items ?? Items; + var nodes = _treeNodeStateCache.FindParentNode(Items, node)?.Items ?? Items; foreach (var n in nodes.Where(n => n != node)) { // 收缩同级节点 n.IsExpand = false; - await TreeNodeStateCache.ToggleNodeAsync(n, GetChildrenRowAsync); + await _treeNodeStateCache.ToggleNodeAsync(n, GetChildrenRowAsync); } } _rows = null; @@ -728,7 +660,7 @@ private async Task OnToggleNodeAsync(TreeViewItem node, bool shouldRender else { // 重建缓存 并且更改节点展开状态 - await TreeNodeStateCache.ToggleNodeAsync(node, GetChildrenRowAsync); + await _treeNodeStateCache.ToggleNodeAsync(node, GetChildrenRowAsync); _rows = null; } @@ -736,41 +668,31 @@ private async Task OnToggleNodeAsync(TreeViewItem node, bool shouldRender { if (AutoCheckChildren) { - node.SetChildrenCheck(TreeNodeStateCache); + node.SetChildrenCheck(_treeNodeStateCache); } if (AutoCheckParent) { - node.SetParentCheck(TreeNodeStateCache); + node.SetParentCheck(_treeNodeStateCache); } if (!AutoCheckChildren && AutoCheckParent && node.Items.Count > 0) { - node.Items[0].SetParentCheck(TreeNodeStateCache); + node.Items[0].SetParentCheck(_treeNodeStateCache); } } - - if (shouldRender) - { - StateHasChanged(); - } + StateHasChanged(); } - /// - /// 节点 Checkbox 状态改变时触发此方法 - /// - /// - /// - /// private async Task OnCheckStateChanged(TreeViewItem item, CheckboxState state) { item.CheckedState = state; - TreeNodeStateCache.ToggleCheck(item); + _treeNodeStateCache.ToggleCheck(item); if (AutoCheckChildren) { // 向下级联操作 if (item.CheckedState != CheckboxState.Indeterminate) { - item.SetChildrenCheck(TreeNodeStateCache); + item.SetChildrenCheck(_treeNodeStateCache); _ = InvokeVoidAsync("setChildrenState", Id, Rows.IndexOf(item), item.CheckedState); } } @@ -778,36 +700,36 @@ private async Task OnCheckStateChanged(TreeViewItem item, CheckboxState s if (AutoCheckParent) { // 向上级联操作 - item.SetParentCheck(TreeNodeStateCache); + item.SetParentCheck(_treeNodeStateCache); _ = InvokeVoidAsync("setParentState", Id, Interop, nameof(GetParentsState), Rows.IndexOf(item)); } if (OnTreeItemChecked != null) { - await OnTreeItemChecked(GetCheckedItems().ToList()); + await OnTreeItemChecked([.. GetCheckedItems()]); } } /// - /// 清除 所有选中节点 + /// Clear all selected nodes /// public void ClearCheckedItems() { Items.ForEach(item => { item.CheckedState = CheckboxState.UnChecked; - TreeNodeStateCache.ToggleCheck(item); + _treeNodeStateCache.ToggleCheck(item); item.GetAllTreeSubItems().ToList().ForEach(s => { s.CheckedState = CheckboxState.UnChecked; - TreeNodeStateCache.ToggleCheck(s); + _treeNodeStateCache.ToggleCheck(s); }); }); StateHasChanged(); } /// - /// 获得 所有选中节点集合 + /// Gets all selected node collections /// /// public IEnumerable> GetCheckedItems() => Items.Aggregate(new List>(), (t, item) => @@ -818,90 +740,27 @@ public IEnumerable> GetCheckedItems() => Items.Aggregate(new }).Where(i => i.CheckedState == CheckboxState.Checked); /// - /// 比较数据是否相同 + /// Check if the data is the same /// /// /// /// public bool Equals(TItem? x, TItem? y) => this.Equals(x, y); - private async Task OnContextMenu(MouseEventArgs e, TreeViewItem item) - { - if (ContextMenuZone != null) - { - await ContextMenuZone.OnContextMenu(e, item.Value); - } - } - - private bool IsPreventDefault => ContextMenuZone != null; - - /// - /// 是否触摸 - /// - private bool TouchStart { get; set; } - - /// - /// 触摸定时器工作指示 - /// - private bool IsBusy { get; set; } - - private async Task OnTouchStart(TouchEventArgs e, TreeViewItem item) - { - if (!IsBusy && ContextMenuZone != null) - { - IsBusy = true; - TouchStart = true; - - // 延时保持 TouchStart 状态 - var delay = Options.CurrentValue.ContextMenuOptions.OnTouchDelay; - await Task.Delay(delay); - if (TouchStart) - { - var args = new MouseEventArgs() - { - ClientX = e.Touches[0].ClientX, - ClientY = e.Touches[0].ClientY, - ScreenX = e.Touches[0].ScreenX, - ScreenY = e.Touches[0].ScreenY, - }; - // 弹出关联菜单 - await OnContextMenu(args, item); - - //延时防止重复激活菜单功能 - await Task.Delay(delay); - } - IsBusy = false; - } - } - - private void OnTouchEnd() - { - TouchStart = false; - } - private List>? _rows = null; private List> Rows { get { - // 扁平化数据集合 - _rows ??= GetTreeItems().ToFlat(); + _rows ??= GetTreeItems().ToFlat(); return _rows; } } private List> GetTreeItems() => _searchItems ?? Items; - private static string? GetTreeRowStyle(TreeViewItem item) - { - var level = 0; - var parent = item.Parent; - while (parent != null) - { - level++; - parent = parent.Parent; - } - return $"--bb-tree-view-level: {level};"; - } + private bool GetActive(TreeViewItem item) => _activeItem == item; + + private int GetIndex(TreeViewItem item) => Rows.IndexOf(item); } diff --git a/src/BootstrapBlazor/Components/TreeView/TreeView.razor.scss b/src/BootstrapBlazor/Components/TreeView/TreeView.razor.scss index 0d0f664ff25..7774e60a701 100644 --- a/src/BootstrapBlazor/Components/TreeView/TreeView.razor.scss +++ b/src/BootstrapBlazor/Components/TreeView/TreeView.razor.scss @@ -39,6 +39,10 @@ align-items: center; cursor: pointer; + .tree-content-toolbar { + display: none; + } + .tree-content-header { flex-basis: calc(var(--bb-tree-padding-left) * var(--bb-tree-view-level, 0)); flex-shrink: 0; @@ -132,5 +136,33 @@ &.disabled { opacity: var(--bb-tree-disabled-opacity); } + + .tree-node-toolbar-edit { + position: absolute; + right: 0; + height: 100%; + display: flex; + align-items: center; + } + + &:not(:hover) .tree-node-toolbar-edit { + display: none; + } + } +} + +.tree-view-edit-form { + display: flex; + + > span { + display: flex; + align-items: center; + margin-inline-end: 0.25rem; + + & + * { + flex: 1; + width: 1%; + min-width: 0; + } } } diff --git a/src/BootstrapBlazor/Components/TreeView/TreeViewItem.cs b/src/BootstrapBlazor/Components/TreeView/TreeViewItem.cs index 0ba21d92675..e1599b73c8e 100644 --- a/src/BootstrapBlazor/Components/TreeView/TreeViewItem.cs +++ b/src/BootstrapBlazor/Components/TreeView/TreeViewItem.cs @@ -30,7 +30,7 @@ public class TreeViewItem : TreeNodeBase, ICheckableNode /// /// 获得/设置 子节点集合 /// - IEnumerable> IExpandableNode.Items { get => Items; set => Items = value.OfType>().ToList(); } + IEnumerable> IExpandableNode.Items { get => Items; set => Items = [.. value.OfType>()]; } /// /// 获得/设置 父级节点 diff --git a/src/BootstrapBlazor/Components/TreeView/TreeViewRow.razor b/src/BootstrapBlazor/Components/TreeView/TreeViewRow.razor new file mode 100644 index 00000000000..0ee10125677 --- /dev/null +++ b/src/BootstrapBlazor/Components/TreeView/TreeViewRow.razor @@ -0,0 +1,47 @@ +@namespace BootstrapBlazor.Components +@typeparam TItem +@inherits ComponentBase + +
+
+
+ + + @if (ShowCheckbox) + { + + + } + + @if (ShowIcon) + { + + } + @if (Item.Template == null) + { + @Item.Text + } + else + { + @Item.Template(Item.Value) + } + @if (_showToolbar) + { + @if (ToolbarTemplate != null) + { + @ToolbarTemplate(Item.Value) + } + else + { + + } + } + +
+
diff --git a/src/BootstrapBlazor/Components/TreeView/TreeViewRow.razor.cs b/src/BootstrapBlazor/Components/TreeView/TreeViewRow.razor.cs new file mode 100644 index 00000000000..39c5e453b13 --- /dev/null +++ b/src/BootstrapBlazor/Components/TreeView/TreeViewRow.razor.cs @@ -0,0 +1,294 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using Microsoft.AspNetCore.Components.Web; + +namespace BootstrapBlazor.Components; + +/// +/// TreeViewRow component +/// +public partial class TreeViewRow +{ + /// + /// Gets or sets whether the node is active. Default is false. + /// + [Parameter] + public bool IsActive { get; set; } + + /// + /// Gets or sets the node index. Default is 0. + /// + [Parameter] + public int Index { get; set; } + + /// + /// Gets or sets the tree node item. Default is null. + /// + [Parameter, NotNull] + public TreeViewItem? Item { get; set; } + + /// + /// Gets or sets the loading icon for tree nodes. + /// + [Parameter] + public string? LoadingIcon { get; set; } + + /// + /// Gets or sets the icon for tree nodes. + /// + [Parameter] + public string? NodeIcon { get; set; } + + /// + /// Gets or sets the icon for expanded tree nodes. + /// + [Parameter] + public string? ExpandNodeIcon { get; set; } + + /// + /// Gets or sets whether the entire component is disabled. Default is false. + /// + [Parameter] + public bool IsDisabled { get; set; } + + /// + /// Gets or sets whether to show checkboxes. Default is false. + /// + [Parameter] + public bool ShowCheckbox { get; set; } + + /// + /// Gets or sets whether nodes can be expanded or collapsed when the component is disabled. Default is false. + /// + [Parameter] + public bool CanExpandWhenDisabled { get; set; } + + /// + /// Get or sets the node click event callback. + /// + [Parameter] + public Func, Task>? OnToggleNodeAsync { get; set; } + + /// + /// Get or sets the node checkbox state change event callback. + /// + [Parameter] + public Func, CheckboxState, Task>? OnCheckStateChanged { get; set; } + + /// + /// Gets or sets the maximum number of selected items. + /// + [Parameter] + public int MaxSelectedCount { get; set; } + + /// + /// Gets or sets the callback that is invoked before the node state changes. + /// + [Parameter] + public Func, CheckboxState, Task>? OnBeforeStateChangedCallback { get; set; } + + /// + /// Gets or sets whether to show icons. Default is false. + /// + [Parameter] + public bool ShowIcon { get; set; } + + /// + /// Gets or sets the click event callback. Default is null. + /// + [Parameter] + public Func, Task>? OnClick { get; set; } + + /// + /// Gets or sets whether show the toolbar of tree view item. Default is false. + /// + [Parameter] + public bool ShowToolbar { get; set; } + + /// + /// A callback method that determines whether to show the toolbar of the tree view item. + /// + [Parameter] + public Func, Task>? ShowToolbarCallback { get; set; } + + /// + /// Gets or sets the title of the popup-window. Default is null. + /// + [Parameter] + public string? ToolbarEditTitle { get; set; } + + /// + /// Gets or sets the title of the popup-window. Default is null. + /// + [Parameter] + public string? ToolbarEditLabelText { get; set; } + + /// + /// Gets or sets the toolbar content template. Default is null. + /// + [Parameter] + public RenderFragment? ToolbarTemplate { get; set; } + + /// + /// Gets or sets the update the tree text value callback. Default is null. + /// If return true will update the tree text value, otherwise will not update. + /// + [Parameter] + public Func>? OnUpdateCallbackAsync { get; set; } + + [Inject] + [NotNull] + private IOptionsMonitor? Options { get; set; } + + [CascadingParameter] + private ContextMenuZone? ContextMenuZone { get; set; } + + private string? ContentClassString => CssBuilder.Default("tree-content") + .AddClass("active", IsActive) + .Build(); + + private string? CaretClassString => CssBuilder.Default("node-icon") + .AddClass("visible", Item.HasChildren || Item.Items.Count > 0) + .AddClass(NodeIcon, !Item.IsExpand) + .AddClass(ExpandNodeIcon, Item.IsExpand) + .AddClass("disabled", !CanTriggerClickNode) + .Build(); + + private string? NodeLoadingClassString => CssBuilder.Default("node-icon node-loading") + .AddClass(LoadingIcon) + .Build(); + + private string? NodeClassString => CssBuilder.Default("tree-node") + .AddClass("disabled", ItemDisabledState) + .Build(); + + private string? ItemTextClassString => CssBuilder.Default("tree-node-text") + .AddClass(Item.CssClass) + .Build(); + + private string? IconClassString => CssBuilder.Default("tree-icon") + .AddClass(Item.Icon) + .AddClass(Item.ExpandIcon, Item.IsExpand && !string.IsNullOrEmpty(Item.ExpandIcon)) + .Build(); + + private bool IsPreventDefault => ContextMenuZone != null; + + private bool _touchStart = false; + + private bool _isBusy = false; + + private bool _showToolbar = false; + + /// + /// + /// + /// + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); + + if (ShowToolbarCallback != null) + { + _showToolbar = await ShowToolbarCallback(Item); + } + else + { + _showToolbar = ShowToolbar; + } + } + + private async Task OnTouchStart(TouchEventArgs e) + { + if (!_isBusy && ContextMenuZone != null) + { + _isBusy = true; + _touchStart = true; + + // 延时保持 TouchStart 状态 + // keep the TouchStart state for a while + var delay = Options.CurrentValue.ContextMenuOptions.OnTouchDelay; + await Task.Delay(delay); + if (_touchStart) + { + var args = new MouseEventArgs() + { + ClientX = e.Touches[0].ClientX, + ClientY = e.Touches[0].ClientY, + ScreenX = e.Touches[0].ScreenX, + ScreenY = e.Touches[0].ScreenY, + }; + await OnContextMenu(args); + + // prevents the menu from being activated repeatedly + await Task.Delay(delay); + } + _isBusy = false; + } + } + + private void OnTouchEnd() + { + _touchStart = false; + } + + private async Task OnContextMenu(MouseEventArgs e) + { + if (ContextMenuZone != null) + { + await ContextMenuZone.OnContextMenu(e, Item.Value); + } + } + + private string? GetTreeRowStyle() + { + var level = 0; + var parent = Item.Parent; + while (parent != null) + { + level++; + parent = parent.Parent; + } + return $"--bb-tree-view-level: {level};"; + } + + private bool CanTriggerClickNode => Item.CanTriggerClickNode(IsDisabled, CanExpandWhenDisabled); + + private bool ItemDisabledState => Item.IsDisabled || IsDisabled; + + private async Task ToggleNodeAsync() + { + if (OnToggleNodeAsync != null) + { + await OnToggleNodeAsync(Item); + } + } + + private async Task CheckStateChanged(CheckboxState state) + { + if (OnCheckStateChanged != null) + { + await OnCheckStateChanged(Item, state); + } + } + + private async Task TriggerBeforeStateChangedCallback(CheckboxState state) + { + var ret = true; + if (OnBeforeStateChangedCallback != null) + { + ret = await OnBeforeStateChangedCallback(Item, state); + } + return ret; + } + + private async Task ClickRow() + { + if (OnClick != null) + { + await OnClick(Item); + } + } +} diff --git a/src/BootstrapBlazor/Components/TreeView/TreeViewToolbarEditButton.razor b/src/BootstrapBlazor/Components/TreeView/TreeViewToolbarEditButton.razor new file mode 100644 index 00000000000..8ab219092fb --- /dev/null +++ b/src/BootstrapBlazor/Components/TreeView/TreeViewToolbarEditButton.razor @@ -0,0 +1,14 @@ +@namespace BootstrapBlazor.Components +@typeparam TItem +@inherits ComponentBase + +
+ + +
+ @Text + +
+
+
+
diff --git a/src/BootstrapBlazor/Components/TreeView/TreeViewToolbarEditButton.razor.cs b/src/BootstrapBlazor/Components/TreeView/TreeViewToolbarEditButton.razor.cs new file mode 100644 index 00000000000..247c48e207e --- /dev/null +++ b/src/BootstrapBlazor/Components/TreeView/TreeViewToolbarEditButton.razor.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Components; + +/// +/// TreeViewToolbarEditButton component +/// +/// +public partial class TreeViewToolbarEditButton : ComponentBase +{ + /// + /// Gets or sets the tree view item. Default is null. + /// + [Parameter, NotNull] + public TreeViewItem? Item { get; set; } + + /// + /// Gets or sets the item changed event callback. + /// + [Parameter] + public EventCallback> ItemChanged { get; set; } + + /// + /// Gets or sets the update the tree text value callback. Default is null. + /// If return true will update the tree text value, otherwise will not update. + /// + [Parameter] + public Func>? OnUpdateCallbackAsync { get; set; } + + /// + /// Gets or sets the title of the popup-window. Default is null. + /// + [Parameter] + public string? Title { get; set; } + + /// + /// Gets or sets the title of the popup-window. Default is null. + /// + [Parameter] + public string? Text { get; set; } + + private string? _text; + + /// + /// + /// + protected override void OnParametersSet() + { + base.OnParametersSet(); + + _text = Item.Text; + } + + private async Task OnConfirm() + { + var ret = true; + if (OnUpdateCallbackAsync != null) + { + ret = await OnUpdateCallbackAsync(Item.Value, _text); + } + + if (ret) + { + Item.Text = _text; + if (ItemChanged.HasDelegate) + { + await ItemChanged.InvokeAsync(Item); + } + } + } +} diff --git a/src/BootstrapBlazor/Extensions/TreeViewExtensions.cs b/src/BootstrapBlazor/Extensions/TreeViewExtensions.cs index 3e7ee82ab0f..d232ff923fa 100644 --- a/src/BootstrapBlazor/Extensions/TreeViewExtensions.cs +++ b/src/BootstrapBlazor/Extensions/TreeViewExtensions.cs @@ -68,4 +68,6 @@ public static List> ToFlat(this IEnumerable(this TreeViewItem item, bool isDisabled, bool canExpandWhenDisabled) => !isDisabled && (canExpandWhenDisabled || !item.IsDisabled); } diff --git a/src/BootstrapBlazor/Locales/en.json b/src/BootstrapBlazor/Locales/en.json index 12d0d4c01a2..7166f59fed2 100644 --- a/src/BootstrapBlazor/Locales/en.json +++ b/src/BootstrapBlazor/Locales/en.json @@ -306,7 +306,9 @@ "NotSetOnTreeExpandErrorMessage": "not set OnExpandNodeAsync parameter" }, "BootstrapBlazor.Components.TreeView": { - "NotSetOnTreeExpandErrorMessage": "not set OnExpandNodeAsync parameter" + "NotSetOnTreeExpandErrorMessage": "not set OnExpandNodeAsync parameter", + "ToolbarEditTitle": "Edit Tree Node", + "ToolbarEditLabelText": "Rename" }, "BootstrapBlazor.Components.UploadBase": { "DeleteButtonText": "Delete", diff --git a/src/BootstrapBlazor/Locales/zh.json b/src/BootstrapBlazor/Locales/zh.json index be7cc4b2cb4..ecc46d43db4 100644 --- a/src/BootstrapBlazor/Locales/zh.json +++ b/src/BootstrapBlazor/Locales/zh.json @@ -306,7 +306,9 @@ "NotSetOnTreeExpandErrorMessage": "未设置 OnExpandNodeAsync 回调委托方法" }, "BootstrapBlazor.Components.TreeView": { - "NotSetOnTreeExpandErrorMessage": "未设置 OnExpandNodeAsync 回调委托方法" + "NotSetOnTreeExpandErrorMessage": "未设置 OnExpandNodeAsync 回调委托方法", + "ToolbarEditTitle": "节点名称编辑", + "ToolbarEditLabelText": "更改为" }, "BootstrapBlazor.Components.UploadBase": { "DeleteButtonText": "删除", diff --git a/test/UnitTest/Components/TreeViewTest.cs b/test/UnitTest/Components/TreeViewTest.cs index 63de44ec223..f48a97d9042 100644 --- a/test/UnitTest/Components/TreeViewTest.cs +++ b/test/UnitTest/Components/TreeViewTest.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone +using System.Threading.Tasks; + namespace UnitTest.Components; public class TreeViewTest : BootstrapBlazorTestBase @@ -1179,6 +1181,53 @@ public async Task ToggleExpand_Ok() cut.Contains("node-icon visible fa-solid fa-caret-right"); } + [Fact] + public async Task ShowToolbar_Ok() + { + List data = + [ + new() { Text = "1010", Id = "1010" }, + new() { Text = "1010-01", Id = "1010-01", ParentId = "1010" }, + ]; + + var items = TreeFoo.CascadingTree(data); + items[0].IsActive = true; + var count = 0; + var edit = false; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.ShowToolbar, true); + pb.Add(a => a.ShowToolbarCallback, foo => + { + count++; + return Task.FromResult(true); + }); + pb.Add(a => a.Items, items); + pb.Add(a => a.OnUpdateCallbackAsync, (foo, text) => + { + edit = true; + return Task.FromResult(true); + }); + }); + + // 节点未展开只回调一次 + Assert.Equal(1, count); + + // 点击确定按钮 + var button = cut.Find(".popover-body .btn-primary"); + await cut.InvokeAsync(() => button.Click()); + Assert.True(edit); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.ToolbarTemplate, foo => builder => + { + builder.AddContent(0, new MarkupString("
foo.Text
")); + }); + }); + Assert.Contains("test-toolbar-template", cut.Markup); + } + class MockTree : TreeView where TItem : class { public bool TestComparerItem(TItem? a, TItem? b) => base.Equals(a, b);