Skip to content
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
8ae8825
feat:DraggableTree
syminomega May 30, 2025
0643cd4
Merge branch 'main' into main
syminomega May 30, 2025
ab77b46
fix:修正格式错误
syminomega May 30, 2025
de2be13
添加TreeView拖拽功能测试
syminomega May 31, 2025
0d6ba2e
Merge branch 'main' into main
syminomega May 31, 2025
525a4ec
Merge branch 'main' into main
syminomega Jun 3, 2025
b24e2d8
Merge branch 'pack'
ArgoZhang Jun 12, 2025
5cabbd0
Merge branch 'doc-blog'
ArgoZhang Jun 15, 2025
fea289f
Merge branch 'dotnetcore:main' into main
syminomega Jul 8, 2025
3c8f5a1
Enhance TreeView drag-and-drop and add comprehensive tests
syminomega Jul 8, 2025
7ea39da
Merge branch 'main' into main
syminomega Jul 8, 2025
906f2aa
Add draggable TreeView demo with drop restrictions
syminomega Jul 8, 2025
81235e9
Refactor TreeView drag-and-drop and improve tests
syminomega Jul 8, 2025
cd74a9f
refactor: 代码格式化
ArgoZhang Jul 9, 2025
10863c2
refactor: 代码规范化
ArgoZhang Jul 9, 2025
61bbb9a
refactor: 提高可读性
ArgoZhang Jul 9, 2025
3bb6ae5
refactor: 代码格式化
ArgoZhang Jul 9, 2025
f02fa27
Merge branch 'main' into feat-treeview-drag
syminomega Jul 9, 2025
5bce9a7
refactor: 代码重构调整位置
ArgoZhang Jul 9, 2025
1140b5b
refactor: 更改参数名称为 AllowDrag
ArgoZhang Jul 9, 2025
237eb21
revert: 重构 Row 对拖动支持
ArgoZhang Jul 9, 2025
5307bad
refactor: 移除预留占位节点
ArgoZhang Jul 9, 2025
7b72f64
revert: 撤销更改
ArgoZhang Jul 9, 2025
f1300b7
revert: 撤销更改
ArgoZhang Jul 9, 2025
68c2f09
revert: 撤销 TreeDropType 类型
ArgoZhang Jul 9, 2025
305590e
doc: 更新文档
ArgoZhang Jul 9, 2025
9a5d729
test: 撤销单元测试更改
ArgoZhang Jul 9, 2025
939fea8
refactor: 移除命名空间
ArgoZhang Jul 9, 2025
e29c3ac
revert: 撤销 AllowDrag 参数
ArgoZhang Jul 9, 2025
ee236ac
revert: 撤销 AllowDrag 参数
ArgoZhang Jul 9, 2025
b4f31cb
style: 调整样式
ArgoZhang Jul 10, 2025
5a6ae2f
style: 整理样式
ArgoZhang Jul 10, 2025
b4e0183
feat: JS 实现客户端拖动动画样式
ArgoZhang Jul 10, 2025
693c596
style: 精简样式
ArgoZhang Jul 10, 2025
c26e3d7
feat: 增加 TriggerDragEnd 逻辑
ArgoZhang Jul 10, 2025
f4969a8
feat: JavaScript 实现拖动逻辑
ArgoZhang Jul 10, 2025
b918dc0
feat: 实现 OnDragItemEndAsync 逻辑
ArgoZhang Jul 10, 2025
f0e8dce
doc: 更新示例
ArgoZhang Jul 10, 2025
6b544d5
feat: 增加重置客户端 DOM 逻辑
ArgoZhang Jul 10, 2025
7c98bc5
doc: 更新示例
ArgoZhang Jul 10, 2025
9659f27
doc: 撤销示例注释
ArgoZhang Jul 10, 2025
e81dbf0
doc: 增加注释
ArgoZhang Jul 10, 2025
d1c8dab
test: 更新单元测试
ArgoZhang Jul 10, 2025
0100b95
Merge branch 'main' into feat-treeview-drag
ArgoZhang Jul 10, 2025
6fe9e0d
fix: 增加逻辑保护
ArgoZhang Jul 10, 2025
9322d97
doc: 更新文档
ArgoZhang Jul 10, 2025
abc319d
chore: bump version 9.8.1-beta03
ArgoZhang Jul 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@
<ConsoleLogger @ref="Logger2"></ConsoleLogger>
</DemoBlock>

<DemoBlock Title="@Localizer["TreeViewDraggableTitle"]"
Introduction="@Localizer["TreeViewDraggableIntro"]"
Name="TreeDraggable">
<section ignore>@((MarkupString)Localizer["TreeViewDraggableDescription"].Value)</section>
<TreeView Items="@DraggableItems" AllowDrag="true" OnDragItemEndAsync="OnDragItemEndAsync">
</TreeView>
</DemoBlock>

<DemoBlock Title="@Localizer["TreeViewTreeDisableTitle"]"
Introduction="@Localizer["TreeViewTreeDisableIntro"]"
Name="TreeDisable">
Expand Down
75 changes: 75 additions & 0 deletions src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone

using DocumentFormat.OpenXml.Spreadsheet;

namespace BootstrapBlazor.Server.Components.Samples;

/// <summary>
Expand Down Expand Up @@ -33,6 +35,8 @@ public sealed partial class TreeViews

private bool AutoCheckParent { get; set; }

private List<TreeViewItem<TreeFoo>> DraggableItems { get; set; } = [];

private List<TreeViewItem<TreeFoo>> DisabledItems { get; } = GetDisabledItems();

private List<TreeViewItem<TreeFoo>>? AccordionItems { get; } = TreeFoo.GetAccordionItems();
Expand Down Expand Up @@ -77,12 +81,61 @@ public sealed partial class TreeViews

private string? _selectedValue;

/// <summary>
/// <inheritdoc/>
/// </summary>
protected override void OnInitialized()
{
base.OnInitialized();

var items = GetDraggableItems();
DraggableItems = TreeFoo.CascadingTree(items);
DraggableItems[0].IsExpand = true;
if (DraggableItems.Count > 1)
{
DraggableItems[1].IsExpand = true;
}
if (DraggableItems.Count > 2)
{
DraggableItems[2].IsExpand = true;
}
}

private Task OnTreeItemClick(TreeViewItem<TreeFoo> item)
{
Logger1.Log($"TreeItem: {item.Text} clicked");
return Task.CompletedTask;
}

private Task OnDragItemEndAsync(TreeViewDragContext<TreeFoo> context)
{
// 本例是使用静态数据模拟数据库操作的,实战中应该是更新节点的父级 Id 可能还需要更改排序字段等信息,然后重构 TreeView 数据源即可
// 根据 context 处理原始数据
var items = GetDraggableItems();
var source = items.Find(i => i.Id == context.Source.Value.Id);
if (source != null)
{
var target = items.Find(i => i.Id == context.Target.Value.Id);
if (target != null)
{
source.ParentId = context.IsChildren ? target.Id : target.ParentId;
}
}
DraggableItems = TreeFoo.CascadingTree(items);
DraggableItems[0].IsExpand = true;
if (DraggableItems.Count > 1)
{
DraggableItems[1].IsExpand = true;
}
if (DraggableItems.Count > 2)
{
DraggableItems[2].IsExpand = true;
}

StateHasChanged();
return Task.CompletedTask;
}

private Task OnTreeItemKeyboardClick(TreeViewItem<TreeFoo> item)
{
_selectedValue = item.Value.Text;
Expand Down Expand Up @@ -122,6 +175,28 @@ private Task OnTreeItemChecked(List<TreeViewItem<TreeFoo>> items)
return Task.CompletedTask;
}

private static List<TreeFoo>? _dragItems = null;
private static List<TreeFoo> GetDraggableItems()
{
_dragItems ??=
[
new() { Text = "Item A", Id = "1", Icon = "fa-solid fa-font-awesome" },
new() { Text = "Item D", Id = "4", ParentId = "1", Icon = "fa-solid fa-font-awesome" },
new() { Text = "Item E", Id = "5", ParentId = "1", Icon = "fa-solid fa-font-awesome" },

new() { Text = "Item B (Drop inside blocked)", Id = "2", Icon = "fa-solid fa-font-awesome" },
new() { Text = "Item F", Id = "6", ParentId = "2", Icon = "fa-solid fa-font-awesome" },
new() { Text = "Item G (Can not move out)", Id = "9", ParentId = "2", Icon = "fa-solid fa-font-awesome" },

new() { Text = "Item C", Id = "3", Icon = "fa-solid fa-font-awesome" },
new() { Text = "Item H", Id = "7", ParentId = "3", Icon = "fa-solid fa-font-awesome" },
new() { Text = "Item I", Id = "8", ParentId = "3", Icon = "fa-solid fa-font-awesome" },


];
return _dragItems;
}

private static List<TreeViewItem<TreeFoo>> GetDisabledItems()
{
var ret = TreeFoo.GetTreeItems();
Expand Down
3 changes: 3 additions & 0 deletions src/BootstrapBlazor.Server/Locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,9 @@
"TreeViewCheckboxCheckBoxDisplayText2": "Automatically select parent node",
"TreeViewCheckboxButtonText": "Refresh",
"TreeViewCheckboxAddButtonText": "Add",
"TreeViewDraggableTitle": "Draggable nodes",
"TreeViewDraggableIntro": "Allows nodes to be dragged and dropped in the tree control",
"TreeViewDraggableDescription": "By setting the <code>ItemDraggable</code> property, you can drag and drop nodes in the tree control. Use the <code>OnDrop</code> callback delegate to handle the drop event, block the drop event by returning <code>false</code>, or allow the drop event by returning <code>true</code>.",
"TreeViewTreeDisableTitle": "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>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",
Expand Down
3 changes: 3 additions & 0 deletions src/BootstrapBlazor.Server/Locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,9 @@
"TreeViewCheckboxCheckBoxDisplayText2": "自动选中父节点",
"TreeViewCheckboxButtonText": "刷新",
"TreeViewCheckboxAddButtonText": "追加节点",
"TreeViewDraggableTitle": "可拖拽节点",
"TreeViewDraggableIntro": "使树中的节点可以进行跨层级拖拽操作",
"TreeViewDraggableDescription": "通过设置 <code>ItemDraggable</code> 属性开启节点拖拽功能,使用 <code>OnDrop</code> 回调委托方法响应拖拽节点放置事件,通过返回 <code>false</code> 阻止拖拽节点放置",
"TreeViewTreeDisableTitle": "禁用状态",
"TreeViewTreeDisableIntro": "可将 Tree 的某些节点设置为禁用状态",
"TreeViewTreeDisableDescription": "通过设置数据源 <code>TreeViewItem</code> 对象的 <code>Disabled</code> 属性,来控制此节点是否可以进行勾选动作,设置为 <code>false</code> 时不影响节点展开/收缩功能",
Expand Down
79 changes: 62 additions & 17 deletions src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,20 +256,6 @@ public partial class TreeView<TItem> : IModelEqualityComparer<TItem>
[Parameter]
public Func<TItem, string?, Task<bool>>? OnUpdateCallbackAsync { get; set; }

[NotNull]
private string? NotSetOnTreeExpandErrorMessage { get; set; }

[Inject]
[NotNull]
private IStringLocalizer<TreeView<TItem>>? Localizer { get; set; }

[Inject]
[NotNull]
private IIconTheme? IconTheme { get; set; }

[NotNull]
private TreeNodeCache<TreeViewItem<TItem>, TItem>? _treeNodeStateCache = null;

/// <summary>
/// Gets or sets whether to automatically update child nodes when the node state changes. Default is false.
/// </summary>
Expand All @@ -282,12 +268,36 @@ public partial class TreeView<TItem> : IModelEqualityComparer<TItem>
[Parameter]
public bool AutoCheckParent { get; set; }

private string? _searchText;
/// <summary>
/// Gets or sets a value indicating whether drag-and-drop operations are allowed. Default is false
/// </summary>
[Parameter]
public bool AllowDrag { get; set; }

/// <summary>
/// 获得/设置 拖动标签页结束回调方法
/// </summary>
[Parameter]
public Func<TreeViewDragContext<TItem>, Task>? OnDragItemEndAsync { get; set; }

[Inject]
[NotNull]
private IStringLocalizer<TreeView<TItem>>? Localizer { get; set; }

[Inject]
[NotNull]
private IIconTheme? IconTheme { get; set; }

private string? EnableKeyboardString => EnableKeyboard ? "true" : null;

private bool _shouldRender = true;
[NotNull]
private string? NotSetOnTreeExpandErrorMessage { get; set; }

[NotNull]
private TreeNodeCache<TreeViewItem<TItem>, TItem>? _treeNodeStateCache = null;

private string? _searchText;
private bool _shouldRender = true;
private bool _init;

/// <summary>
Expand Down Expand Up @@ -368,6 +378,11 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
_keyboardArrowUpDownTrigger = false;
await InvokeVoidAsync("scroll", Id, ScrollIntoViewOptions);
}

if(!firstRender && AllowDrag)
{
await InvokeVoidAsync("resetTreeViewRow", Id);
}
}

/// <summary>
Expand All @@ -380,7 +395,13 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, new { Invoke = Interop, Method = nameof(TriggerKeyDown) });
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, new
{
Invoke = Interop,
Method = nameof(TriggerKeyDown),
AllowDrag,
TriggerDragEnd = nameof(TriggerDragEnd)
});

private bool _keyboardArrowUpDownTrigger;

Expand Down Expand Up @@ -408,6 +429,30 @@ public async ValueTask TriggerKeyDown(string key)
}
}

/// <summary>
/// Triggers the end of a drag-and-drop operation within the tree view.
/// </summary>
/// <remarks>This method is invoked via JavaScript interop to signal the completion of a drag-and-drop
/// action. If a handler is assigned to <see cref="OnDragItemEndAsync"/>, it will be invoked with the drag
/// context.</remarks>
/// <param name="originIndex">The zero-based index of the item being dragged from its original position.</param>
/// <param name="currentIndex">The zero-based index of the item's current position after the drag operation.</param>
/// <param name="isChildren">A value indicating whether the drag operation involves child items.</param>
/// <returns></returns>
[JSInvokable]
public async ValueTask TriggerDragEnd(int originIndex, int currentIndex, bool isChildren)
{
if (OnDragItemEndAsync != null)
{
var context = new TreeViewDragContext<TItem>(
source: Rows[originIndex],
target: Rows[currentIndex],
children: isChildren
);
await OnDragItemEndAsync(context);
}
}

/// <summary>
/// Client-side method to query the state of the specified row checkbox, called by JavaScript
/// </summary>
Expand Down
Loading
Loading