Skip to content

Commit 3c8f5a1

Browse files
committed
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.
1 parent fea289f commit 3c8f5a1

File tree

4 files changed

+135
-35
lines changed

4 files changed

+135
-35
lines changed

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,16 @@ private async Task OnItemDrop(TreeDropEventArgs<TItem> e)
773773
}
774774

775775
// 如果允许改变源节点则更新拖拽项的父对象以及排序
776-
_draggingItem.Parent?.Items.Remove(_draggingItem);
776+
if (_draggingItem.Parent is not null)
777+
{
778+
_draggingItem.Parent.Items.Remove(_draggingItem);
779+
}
780+
else
781+
{
782+
// 没有父对象,则从顶层节点集合中移除
783+
Items.Remove(_draggingItem);
784+
}
785+
777786
_draggingItem.IsExpand = e.ExpandAfterDrop;
778787

779788
switch (e.DropType)

src/BootstrapBlazor/Components/TreeView/TreeViewRow.razor

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,20 @@
6262
}
6363
}
6464

65+
@* 鼠标拖到节点中央,表示放置到当前节点内部作为最后一个子节点,显示边框预览 *@
6566
@if (_previewChildLast)
6667
{
6768
<div class="tree-preview-child-last"></div>
6869
}
70+
@* 展开状态下,拖到当前节点靠下的位置,插入到内部作为第一个子节点,显示线条预览 *@
6971
@if (_previewChildFirst)
7072
{
7173
<div class="tree-preview-child-first">
7274
<div class="tree-preview-circle"></div>
7375
<div class="tree-preview-line"></div>
7476
</div>
7577
}
78+
@* 折叠状态或者没有子节点,拖到当前节点靠下的位置,作为同级节点,显示线条预览 *@
7679
@if (_previewBelow)
7780
{
7881
<div class="tree-preview-below">

src/BootstrapBlazor/Enums/TreeDropType.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public enum TreeDropType
1919
/// </summary>
2020
AsLastChild,
2121
/// <summary>
22-
/// 作为同级节点
22+
/// 作为下方同级节点
2323
/// </summary>
2424
AsSiblingBelow,
2525
}

test/UnitTest/Components/TreeViewTest.cs

Lines changed: 121 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,13 +1223,15 @@ public async Task ShowToolbar_Ok()
12231223
Assert.Contains("test-toolbar-template", cut.Markup);
12241224
}
12251225

1226+
#region DraggableTest
1227+
12261228
[Fact]
12271229
public async Task Draggable_Basic_MoveAsLastChild()
12281230
{
12291231
// Arrange
12301232
var items = new List<TreeFoo>
12311233
{
1232-
new() { Text = "Root", Id = "1" },
1234+
new() { Text = "Root1", Id = "1" },
12331235
new() { Text = "Child1", Id = "2", ParentId = "1" },
12341236
new() { Text = "Child2", Id = "3", ParentId = "1" },
12351237
new() { Text = "Root2", Id = "4" },
@@ -1245,37 +1247,39 @@ public async Task Draggable_Basic_MoveAsLastChild()
12451247

12461248
var rows = cut.FindComponents<TreeViewRow<TreeFoo>>();
12471249
var dragSource = rows[1]; // Child1
1248-
var dragTarget = rows[3]; // Root2
1250+
var dropTarget = rows[3]; // Root2
12491251

1250-
var dragSourceElement = dragSource.Find(".tree-node");
1251-
var dragTargetDropZone = dragTarget.Find(".tree-drop-child-inside");
1252-
// 1. 触发 dragstart
1253-
await dragSourceElement.TriggerEventAsync("ondragstart", new DragEventArgs());
1254-
// 2. 触发 dragenter 到目标 drop zone
1255-
await dragTargetDropZone.TriggerEventAsync("ondragenter", new DragEventArgs());
1256-
// 3. 触发 drop 到目标 drop zone
1257-
await dragTargetDropZone.TriggerEventAsync("ondrop", new DragEventArgs());
1258-
// 成功移动,原对象销毁,不触发dragend
1252+
var sourceElement = dragSource.Find(".tree-node");
1253+
// 拖动到节点中间的区域
1254+
var targetDropZone = dropTarget.Find(".tree-drop-child-inside");
1255+
1256+
await sourceElement.TriggerEventAsync("ondragstart", new DragEventArgs());
1257+
await targetDropZone.TriggerEventAsync("ondragenter", new DragEventArgs());
1258+
await targetDropZone.TriggerEventAsync("ondrop", new DragEventArgs());
12591259

12601260
// Assert
1261-
Assert.Equal("Root2", nodes[1].Text);
1262-
Assert.Equal(2, nodes[1].Items.Count);
1263-
Assert.Equal("Child1", nodes[1].Items[1].Text);
1264-
Assert.Equal(nodes[1], nodes[1].Items[1].Parent);
1261+
Assert.Single(nodes[0].Items); // Root1 只剩下 Child2
1262+
Assert.Equal(2, nodes[1].Items.Count); // Root2 有 Child3 和 Child1
1263+
Assert.Equal("Child1", nodes[1].Items[1].Text); // Child1 成为 Root2 的最后一个子节点
1264+
Assert.Equal(nodes[1], nodes[1].Items[1].Parent); // Child1 的父节点是 Root2
12651265
}
12661266

12671267
[Fact]
1268-
public async Task Draggable_Basic_MoveAsSiblingBelow()
1268+
public async Task Draggable_Basic_MoveAsFirstChild()
12691269
{
12701270
// Arrange
12711271
var items = new List<TreeFoo>
12721272
{
1273-
new() { Text = "Root", Id = "1" },
1273+
new() { Text = "Root1", Id = "1" },
12741274
new() { Text = "Child1", Id = "2", ParentId = "1" },
1275-
new() { Text = "Child2", Id = "3", ParentId = "1" }
1275+
new() { Text = "Child2", Id = "3", ParentId = "1" },
1276+
new() { Text = "Root2", Id = "4" },
1277+
new() { Text = "Child3", Id = "5", ParentId = "4" }
12761278
};
12771279
var nodes = TreeFoo.CascadingTree(items);
12781280
nodes[0].IsExpand = true;
1281+
// 展开情况才能作为第一个子节点进行拖入
1282+
nodes[1].IsExpand = true;
12791283
var cut = Context.RenderComponent<TreeView<TreeFoo>>(pb =>
12801284
{
12811285
pb.Add(a => a.Items, nodes);
@@ -1284,24 +1288,106 @@ public async Task Draggable_Basic_MoveAsSiblingBelow()
12841288

12851289
var rows = cut.FindComponents<TreeViewRow<TreeFoo>>();
12861290
var dragSource = rows[1]; // Child1
1287-
var dragTarget = rows[2]; // Child2
1291+
var dragTarget = rows[3]; // Root2
12881292

1289-
// 获取可拖拽的 DOM 元素(DynamicElement)
1290-
var dragSourceElement = dragSource.Find(".tree-node");
1291-
var dragTargetDropZone = dragTarget.Find(".tree-drop-child-below");
1292-
// 1. 触发 dragstart
1293-
await dragSourceElement.TriggerEventAsync("ondragstart", new DragEventArgs());
1294-
// 2. 触发 dragenter 到目标 drop zone
1295-
await dragTargetDropZone.TriggerEventAsync("ondragenter", new DragEventArgs());
1296-
// 3. 触发 drop 到目标 drop zone
1297-
await dragTargetDropZone.TriggerEventAsync("ondrop", new DragEventArgs());
1298-
// 成功移动,原对象销毁,不触发dragend
1293+
var sourceElement = dragSource.Find(".tree-node");
1294+
// 拖动到节点靠下的区域
1295+
var targetDropZone = dragTarget.Find(".tree-drop-child-below");
1296+
1297+
await sourceElement.TriggerEventAsync("ondragstart", new DragEventArgs());
1298+
await targetDropZone.TriggerEventAsync("ondragenter", new DragEventArgs());
1299+
await targetDropZone.TriggerEventAsync("ondrop", new DragEventArgs());
12991300

13001301
// Assert
1301-
var parent = nodes[0];
1302-
Assert.Equal(2, parent.Items.Count);
1303-
Assert.Equal("Child2", parent.Items[0].Text);
1304-
Assert.Equal("Child1", parent.Items[1].Text);
1302+
Assert.Single(nodes[0].Items); // Root1 只剩下 Child2
1303+
Assert.Equal(2, nodes[1].Items.Count); // Root2 有 Child1 和 Child3
1304+
Assert.Equal("Child1", nodes[1].Items[0].Text); // Child1 成为 Root2 的第一个子节点
1305+
Assert.Equal(nodes[1], nodes[1].Items[0].Parent); // Child1 的父节点是 Root2
1306+
}
1307+
1308+
[Fact]
1309+
public async Task Draggable_Basic_MoveAsSiblingBelow()
1310+
{
1311+
// Arrange
1312+
var items = new List<TreeFoo>
1313+
{
1314+
new() { Text = "Root1", Id = "1" },
1315+
new() { Text = "Child1", Id = "2", ParentId = "1" },
1316+
new() { Text = "Child2", Id = "3", ParentId = "1" },
1317+
new() { Text = "Root2", Id = "4" },
1318+
new() { Text = "Child3", Id = "5", ParentId = "4" },
1319+
new() { Text = "Child4", Id = "6", ParentId = "4" },
1320+
new() { Text = "Root3", Id = "7" }
1321+
};
1322+
var nodes = TreeFoo.CascadingTree(items);
1323+
nodes[0].IsExpand = true;
1324+
nodes[1].IsExpand = true;
1325+
var cut = Context.RenderComponent<TreeView<TreeFoo>>(pb =>
1326+
{
1327+
pb.Add(a => a.Items, nodes);
1328+
pb.Add(a => a.ItemDraggable, true);
1329+
});
1330+
1331+
var rowsA = cut.FindComponents<TreeViewRow<TreeFoo>>();
1332+
var source1 = rowsA[1]; // Child1
1333+
var target1 = rowsA[4]; // Child3
1334+
1335+
var drag1 = source1.Find(".tree-node");
1336+
// 拖动到 Child3 的下方,非最后一个兄弟节点
1337+
var drop1 = target1.Find(".tree-drop-child-below");
1338+
1339+
await drag1.TriggerEventAsync("ondragstart", new DragEventArgs());
1340+
await drop1.TriggerEventAsync("ondragenter", new DragEventArgs());
1341+
await drop1.TriggerEventAsync("ondrop", new DragEventArgs());
1342+
1343+
// Assert
1344+
Assert.Single(nodes[0].Items); // Root1 只剩下 Child2
1345+
Assert.Equal(3, nodes[1].Items.Count); // Root2 有 Child3, Child1 和 Child4
1346+
Assert.Equal("Child3", nodes[1].Items[0].Text);
1347+
Assert.Equal("Child1", nodes[1].Items[1].Text); // Child1 成为 Child3 的下一个兄弟节点
1348+
1349+
// 渲染变更后的内容
1350+
cut.Render();
1351+
1352+
var rowsB = cut.FindComponents<TreeViewRow<TreeFoo>>();
1353+
var source2 = rowsB[1]; // Child2
1354+
var target2 = rowsB[5]; // Child4
1355+
1356+
var drag2 = source2.Find(".tree-node");
1357+
// 拖动到 Child4 的下方,成为最后一个兄弟节点
1358+
var drop2 = target2.Find(".tree-drop-child-below");
1359+
1360+
await drag2.TriggerEventAsync("ondragstart", new DragEventArgs());
1361+
await drop2.TriggerEventAsync("ondragenter", new DragEventArgs());
1362+
await drop2.TriggerEventAsync("ondrop", new DragEventArgs());
1363+
1364+
// Assert
1365+
Assert.Empty(nodes[0].Items); // Root1 没有对象
1366+
Assert.Equal(4, nodes[1].Items.Count); // Root2 有 Child3, Child1, Child4 和 Child2
1367+
Assert.Equal("Child3", nodes[1].Items[0].Text);
1368+
Assert.Equal("Child1", nodes[1].Items[1].Text); // Child1 仍然是 Child3 的下一个兄弟节点
1369+
Assert.Equal("Child4", nodes[1].Items[2].Text); // Child4 仍然是 Child1 的下一个兄弟节点
1370+
Assert.Equal("Child2", nodes[1].Items[3].Text); // Child2 成为最后一个兄弟节点
1371+
1372+
// 渲染变更后的内容
1373+
cut.Render();
1374+
1375+
var rowsC = cut.FindComponents<TreeViewRow<TreeFoo>>();
1376+
var source3 = rowsC[0]; // Root1
1377+
var target3 = rowsC[6]; // Root3
1378+
1379+
var drag3 = source3.Find(".tree-node");
1380+
// 拖动到 Root 的下方,成为根节点,且为最后一个兄弟节点
1381+
var drop3 = target3.Find(".tree-drop-child-below");
1382+
1383+
await drag3.TriggerEventAsync("ondragstart", new DragEventArgs());
1384+
await drop3.TriggerEventAsync("ondragenter", new DragEventArgs());
1385+
await drop3.TriggerEventAsync("ondrop", new DragEventArgs());
1386+
1387+
// Assert
1388+
Assert.Equal(3, nodes.Count);
1389+
Assert.Equal("Root2", nodes[0].Text);
1390+
Assert.Equal("Root1", nodes[2].Text); // Root1 成为最后一个根节点
13051391
}
13061392

13071393
[Fact]
@@ -1375,6 +1461,8 @@ public async Task Draggable_DragClassState()
13751461
Assert.DoesNotContain("tree-drop-pass", row.Markup);
13761462
}
13771463

1464+
#endregion
1465+
13781466
class MockTree<TItem> : TreeView<TItem> where TItem : class
13791467
{
13801468
public bool TestComparerItem(TItem? a, TItem? b) => base.Equals(a, b);

0 commit comments

Comments
 (0)