Skip to content

Commit b0245db

Browse files
feat(TreeView): scripts to implement drag and drop function (#6380)
* feat:DraggableTree * fix:修正格式错误 * 添加TreeView拖拽功能测试 * Enhance TreeView drag-and-drop and add comprehensive tests Improved TreeView drag-and-drop logic to handle moving nodes without parents and updated node removal accordingly. Added and expanded unit tests to cover moving nodes as last child, first child, and as siblings below, ensuring correct parent/child relationships and node ordering. Also added clarifying comments in TreeViewRow for drag-and-drop preview UI. * Add draggable TreeView demo with drop restrictions Introduced a new demo block showcasing draggable TreeView nodes, including logic to restrict certain drag-and-drop operations. Added localized strings for the new demo in both English and Chinese. Updated the backend to provide a sample data set and drop event handler enforcing the restrictions. * Refactor TreeView drag-and-drop and improve tests Refactored TreeView and TreeViewRow to require OnItemDrop and remove null checks, simplifying drag-and-drop logic. Updated tests to cover cases where OnItemDrop is not set and to verify drag-and-drop visual feedback. * refactor: 代码格式化 * refactor: 代码规范化 * refactor: 提高可读性 * refactor: 代码格式化 * refactor: 代码重构调整位置 * refactor: 更改参数名称为 AllowDrag * revert: 重构 Row 对拖动支持 * refactor: 移除预留占位节点 * revert: 撤销更改 * revert: 撤销更改 * revert: 撤销 TreeDropType 类型 * doc: 更新文档 * test: 撤销单元测试更改 * refactor: 移除命名空间 * revert: 撤销 AllowDrag 参数 * revert: 撤销 AllowDrag 参数 * style: 调整样式 * style: 整理样式 * feat: JS 实现客户端拖动动画样式 * style: 精简样式 * feat: 增加 TriggerDragEnd 逻辑 * feat: JavaScript 实现拖动逻辑 * feat: 实现 OnDragItemEndAsync 逻辑 * doc: 更新示例 * feat: 增加重置客户端 DOM 逻辑 * doc: 更新示例 * doc: 撤销示例注释 * doc: 增加注释 * test: 更新单元测试 * fix: 增加逻辑保护 * doc: 更新文档 * chore: bump version 9.8.1-beta03 --------- Co-Authored-By: Symin <[email protected]> Co-authored-by: Argo Zhang <[email protected]>
1 parent 6943615 commit b0245db

File tree

11 files changed

+401
-22
lines changed

11 files changed

+401
-22
lines changed

src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@
5252
<ConsoleLogger @ref="Logger2"></ConsoleLogger>
5353
</DemoBlock>
5454

55+
<DemoBlock Title="@Localizer["TreeViewDraggableTitle"]"
56+
Introduction="@Localizer["TreeViewDraggableIntro"]"
57+
Name="TreeDraggable">
58+
<section ignore>@((MarkupString)Localizer["TreeViewDraggableDescription"].Value)</section>
59+
<TreeView Items="@DraggableItems" AllowDrag="true" OnDragItemEndAsync="OnDragItemEndAsync">
60+
</TreeView>
61+
</DemoBlock>
62+
5563
<DemoBlock Title="@Localizer["TreeViewTreeDisableTitle"]"
5664
Introduction="@Localizer["TreeViewTreeDisableIntro"]"
5765
Name="TreeDisable">

src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// See the LICENSE file in the project root for more information.
44
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone
55

6+
using DocumentFormat.OpenXml.Spreadsheet;
7+
68
namespace BootstrapBlazor.Server.Components.Samples;
79

810
/// <summary>
@@ -33,6 +35,8 @@ public sealed partial class TreeViews
3335

3436
private bool AutoCheckParent { get; set; }
3537

38+
private List<TreeViewItem<TreeFoo>> DraggableItems { get; set; } = [];
39+
3640
private List<TreeViewItem<TreeFoo>> DisabledItems { get; } = GetDisabledItems();
3741

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

7882
private string? _selectedValue;
7983

84+
/// <summary>
85+
/// <inheritdoc/>
86+
/// </summary>
87+
protected override void OnInitialized()
88+
{
89+
base.OnInitialized();
90+
91+
var items = GetDraggableItems();
92+
DraggableItems = TreeFoo.CascadingTree(items);
93+
DraggableItems[0].IsExpand = true;
94+
if (DraggableItems.Count > 1)
95+
{
96+
DraggableItems[1].IsExpand = true;
97+
}
98+
if (DraggableItems.Count > 2)
99+
{
100+
DraggableItems[2].IsExpand = true;
101+
}
102+
}
103+
80104
private Task OnTreeItemClick(TreeViewItem<TreeFoo> item)
81105
{
82106
Logger1.Log($"TreeItem: {item.Text} clicked");
83107
return Task.CompletedTask;
84108
}
85109

110+
private Task OnDragItemEndAsync(TreeViewDragContext<TreeFoo> context)
111+
{
112+
// 本例是使用静态数据模拟数据库操作的,实战中应该是更新节点的父级 Id 可能还需要更改排序字段等信息,然后重构 TreeView 数据源即可
113+
// 根据 context 处理原始数据
114+
var items = GetDraggableItems();
115+
var source = items.Find(i => i.Id == context.Source.Value.Id);
116+
if (source != null)
117+
{
118+
var target = items.Find(i => i.Id == context.Target.Value.Id);
119+
if (target != null)
120+
{
121+
source.ParentId = context.IsChildren ? target.Id : target.ParentId;
122+
}
123+
}
124+
DraggableItems = TreeFoo.CascadingTree(items);
125+
DraggableItems[0].IsExpand = true;
126+
if (DraggableItems.Count > 1)
127+
{
128+
DraggableItems[1].IsExpand = true;
129+
}
130+
if (DraggableItems.Count > 2)
131+
{
132+
DraggableItems[2].IsExpand = true;
133+
}
134+
135+
StateHasChanged();
136+
return Task.CompletedTask;
137+
}
138+
86139
private Task OnTreeItemKeyboardClick(TreeViewItem<TreeFoo> item)
87140
{
88141
_selectedValue = item.Value.Text;
@@ -122,6 +175,28 @@ private Task OnTreeItemChecked(List<TreeViewItem<TreeFoo>> items)
122175
return Task.CompletedTask;
123176
}
124177

178+
private static List<TreeFoo>? _dragItems = null;
179+
private static List<TreeFoo> GetDraggableItems()
180+
{
181+
_dragItems ??=
182+
[
183+
new() { Text = "Item A", Id = "1", Icon = "fa-solid fa-font-awesome" },
184+
new() { Text = "Item D", Id = "4", ParentId = "1", Icon = "fa-solid fa-font-awesome" },
185+
new() { Text = "Item E", Id = "5", ParentId = "1", Icon = "fa-solid fa-font-awesome" },
186+
187+
new() { Text = "Item B (Drop inside blocked)", Id = "2", Icon = "fa-solid fa-font-awesome" },
188+
new() { Text = "Item F", Id = "6", ParentId = "2", Icon = "fa-solid fa-font-awesome" },
189+
new() { Text = "Item G (Can not move out)", Id = "9", ParentId = "2", Icon = "fa-solid fa-font-awesome" },
190+
191+
new() { Text = "Item C", Id = "3", Icon = "fa-solid fa-font-awesome" },
192+
new() { Text = "Item H", Id = "7", ParentId = "3", Icon = "fa-solid fa-font-awesome" },
193+
new() { Text = "Item I", Id = "8", ParentId = "3", Icon = "fa-solid fa-font-awesome" },
194+
195+
196+
];
197+
return _dragItems;
198+
}
199+
125200
private static List<TreeViewItem<TreeFoo>> GetDisabledItems()
126201
{
127202
var ret = TreeFoo.GetTreeItems();

src/BootstrapBlazor.Server/Locales/en-US.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,9 @@
676676
"TreeViewCheckboxCheckBoxDisplayText2": "Automatically select parent node",
677677
"TreeViewCheckboxButtonText": "Refresh",
678678
"TreeViewCheckboxAddButtonText": "Add",
679+
"TreeViewDraggableTitle": "Draggable nodes",
680+
"TreeViewDraggableIntro": "Allows nodes to be dragged and dropped in the tree control",
681+
"TreeViewDraggableDescription": "By setting the <code>AllowDrag</code> property, you can drag and drop nodes in the tree control. Use the <code>OnDragItemEndAsync</code> callback delegate to handle the drop event.",
679682
"TreeViewTreeDisableTitle": "Disabled state",
680683
"TreeViewTreeDisableIntro": "Some nodes of the Tree can be set to disabled state",
681684
"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",

src/BootstrapBlazor.Server/Locales/zh-CN.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,9 @@
676676
"TreeViewCheckboxCheckBoxDisplayText2": "自动选中父节点",
677677
"TreeViewCheckboxButtonText": "刷新",
678678
"TreeViewCheckboxAddButtonText": "追加节点",
679+
"TreeViewDraggableTitle": "可拖拽节点",
680+
"TreeViewDraggableIntro": "使树中的节点可以进行跨层级拖拽操作",
681+
"TreeViewDraggableDescription": "通过设置 <code>AllowDrag</code> 属性开启节点拖拽功能,使用 <code>OnDragItemEndAsync</code> 回调委托方法响应拖拽节点放置事件",
679682
"TreeViewTreeDisableTitle": "禁用状态",
680683
"TreeViewTreeDisableIntro": "可将 Tree 的某些节点设置为禁用状态",
681684
"TreeViewTreeDisableDescription": "通过设置数据源 <code>TreeViewItem</code> 对象的 <code>Disabled</code> 属性,来控制此节点是否可以进行勾选动作,设置为 <code>false</code> 时不影响节点展开/收缩功能",

src/BootstrapBlazor/BootstrapBlazor.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Razor">
22

33
<PropertyGroup>
4-
<Version>9.8.1-beta02</Version>
4+
<Version>9.8.1-beta03</Version>
55
</PropertyGroup>
66

77
<ItemGroup>

src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -256,20 +256,6 @@ public partial class TreeView<TItem> : IModelEqualityComparer<TItem>
256256
[Parameter]
257257
public Func<TItem, string?, Task<bool>>? OnUpdateCallbackAsync { get; set; }
258258

259-
[NotNull]
260-
private string? NotSetOnTreeExpandErrorMessage { get; set; }
261-
262-
[Inject]
263-
[NotNull]
264-
private IStringLocalizer<TreeView<TItem>>? Localizer { get; set; }
265-
266-
[Inject]
267-
[NotNull]
268-
private IIconTheme? IconTheme { get; set; }
269-
270-
[NotNull]
271-
private TreeNodeCache<TreeViewItem<TItem>, TItem>? _treeNodeStateCache = null;
272-
273259
/// <summary>
274260
/// Gets or sets whether to automatically update child nodes when the node state changes. Default is false.
275261
/// </summary>
@@ -282,12 +268,36 @@ public partial class TreeView<TItem> : IModelEqualityComparer<TItem>
282268
[Parameter]
283269
public bool AutoCheckParent { get; set; }
284270

285-
private string? _searchText;
271+
/// <summary>
272+
/// Gets or sets a value indicating whether drag-and-drop operations are allowed. Default is false
273+
/// </summary>
274+
[Parameter]
275+
public bool AllowDrag { get; set; }
276+
277+
/// <summary>
278+
/// 获得/设置 拖动标签页结束回调方法
279+
/// </summary>
280+
[Parameter]
281+
public Func<TreeViewDragContext<TItem>, Task>? OnDragItemEndAsync { get; set; }
282+
283+
[Inject]
284+
[NotNull]
285+
private IStringLocalizer<TreeView<TItem>>? Localizer { get; set; }
286+
287+
[Inject]
288+
[NotNull]
289+
private IIconTheme? IconTheme { get; set; }
286290

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

289-
private bool _shouldRender = true;
293+
[NotNull]
294+
private string? NotSetOnTreeExpandErrorMessage { get; set; }
290295

296+
[NotNull]
297+
private TreeNodeCache<TreeViewItem<TItem>, TItem>? _treeNodeStateCache = null;
298+
299+
private string? _searchText;
300+
private bool _shouldRender = true;
291301
private bool _init;
292302

293303
/// <summary>
@@ -368,6 +378,11 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
368378
_keyboardArrowUpDownTrigger = false;
369379
await InvokeVoidAsync("scroll", Id, ScrollIntoViewOptions);
370380
}
381+
382+
if(!firstRender && AllowDrag)
383+
{
384+
await InvokeVoidAsync("resetTreeViewRow", Id);
385+
}
371386
}
372387

373388
/// <summary>
@@ -380,7 +395,13 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
380395
/// <inheritdoc/>
381396
/// </summary>
382397
/// <returns></returns>
383-
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, new { Invoke = Interop, Method = nameof(TriggerKeyDown) });
398+
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, new
399+
{
400+
Invoke = Interop,
401+
Method = nameof(TriggerKeyDown),
402+
AllowDrag,
403+
TriggerDragEnd = nameof(TriggerDragEnd)
404+
});
384405

385406
private bool _keyboardArrowUpDownTrigger;
386407

@@ -408,6 +429,30 @@ public async ValueTask TriggerKeyDown(string key)
408429
}
409430
}
410431

432+
/// <summary>
433+
/// Triggers the end of a drag-and-drop operation within the tree view.
434+
/// </summary>
435+
/// <remarks>This method is invoked via JavaScript interop to signal the completion of a drag-and-drop
436+
/// action. If a handler is assigned to <see cref="OnDragItemEndAsync"/>, it will be invoked with the drag
437+
/// context.</remarks>
438+
/// <param name="originIndex">The zero-based index of the item being dragged from its original position.</param>
439+
/// <param name="currentIndex">The zero-based index of the item's current position after the drag operation.</param>
440+
/// <param name="isChildren">A value indicating whether the drag operation involves child items.</param>
441+
/// <returns></returns>
442+
[JSInvokable]
443+
public async ValueTask TriggerDragEnd(int originIndex, int currentIndex, bool isChildren)
444+
{
445+
if (OnDragItemEndAsync != null)
446+
{
447+
var context = new TreeViewDragContext<TItem>(
448+
source: Rows[originIndex],
449+
target: Rows[currentIndex],
450+
children: isChildren
451+
);
452+
await OnDragItemEndAsync(context);
453+
}
454+
}
455+
411456
/// <summary>
412457
/// Client-side method to query the state of the specified row checkbox, called by JavaScript
413458
/// </summary>

0 commit comments

Comments
 (0)