Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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
9 changes: 9 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,15 @@
<ConsoleLogger @ref="Logger2"></ConsoleLogger>
</DemoBlock>

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

<DemoBlock Title="@Localizer["TreeViewTreeDisableTitle"]"
Introduction="@Localizer["TreeViewTreeDisableIntro"]"
Name="TreeDisable">
Expand Down
47 changes: 47 additions & 0 deletions src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public sealed partial class TreeViews

private bool AutoCheckParent { get; set; }

private List<TreeViewItem<TreeFoo>> DraggableItems { get; } = GetDraggableItems();

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

private List<TreeViewItem<TreeFoo>>? AccordionItems { get; } = TreeFoo.GetAccordionItems();
Expand Down Expand Up @@ -83,6 +85,26 @@ private Task OnTreeItemClick(TreeViewItem<TreeFoo> item)
return Task.CompletedTask;
}

private static Task<bool> OnDrop(TreeDropEventArgs<TreeFoo> arg)
{
// 如果拖拽到 Id=2 的节点下则不允许
if (arg.Target.Value.Id == "2" && arg.DropType is TreeDropType.AsFirstChild or TreeDropType.AsLastChild)
{
return Task.FromResult(false);
}
// 如果拖拽到 Id=2 的节点下的兄弟节点则不允许
if (arg.DropType is TreeDropType.AsSiblingBelow && arg.Target.Parent?.Value.Id == "2")
{
return Task.FromResult(false);
}
// 如果 Id=6 的节点则不允许拖出
if (arg.Source?.Value.Id == "6")
{
return Task.FromResult(false);
}
return Task.FromResult(true);
}

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

private static List<TreeViewItem<TreeFoo>> GetDraggableItems()
{
List<TreeFoo> items =
[
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 C", Id = "3", Icon = "fa-solid fa-font-awesome" },
new() { Text = "Item H", Id = "6", ParentId = "3", Icon = "fa-solid fa-font-awesome" },
new() { Text = "Item I", Id = "6", ParentId = "3", Icon = "fa-solid fa-font-awesome" },

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

];
var ret = TreeFoo.CascadingTree(items);
ret[0].IsExpand = true;
ret[1].IsExpand = true;
ret[2].IsExpand = true;
return ret;
}

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
3 changes: 2 additions & 1 deletion src/BootstrapBlazor/Components/TreeView/TreeView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,14 @@ else
RenderFragment RenderRow(TreeViewItem<TItem> item) =>
@<TreeViewRow @key="item" IsActive="GetActive(item)" Index="GetIndex(item)" Item="item"
NodeIcon="@NodeIcon" ExpandNodeIcon="@ExpandNodeIcon" LoadingIcon="@LoadingIcon"
MaxSelectedCount="MaxSelectedCount"
MaxSelectedCount="MaxSelectedCount" Draggable="ItemDraggable" PreviewDrop="_previewDrop"
ToolbarEditTitle="@ToolbarEditTitle" ToolbarEditLabelText="@ToolbarEditLabelText"
IsDisabled="IsDisabled" CanExpandWhenDisabled="CanExpandWhenDisabled"
ShowCheckbox="ShowCheckbox" ShowIcon="ShowIcon"
ShowToolbar="ShowToolbar" ShowToolbarCallback="ShowToolbarCallback"
OnToggleNodeAsync="OnToggleNodeAsync" OnClick="OnClick"
OnBeforeStateChangedCallback="OnBeforeStateChangedCallback"
OnCheckStateChanged="OnCheckStateChanged"
OnItemDragStart="OnItemDragStart" OnItemDragEnd="OnItemDragEnd" OnItemDrop="OnItemDrop"
OnUpdateCallbackAsync="OnUpdateCallbackAsync" ToolbarTemplate="ToolbarTemplate"></TreeViewRow>;
}
140 changes: 140 additions & 0 deletions src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,120 @@
StateHasChanged();
}

/// <summary>
/// Gets or sets whether to enable item dragging. Default is false.
/// </summary>
[Parameter]
public bool ItemDraggable { get; set; }

/// <summary>
/// Gets or sets the callback method to be invoked when an item is dropped.
/// Drop action can be cancelled by returning false.
/// </summary>
[Parameter]
public Func<TreeDropEventArgs<TItem>, Task<bool>> OnDrop { get; set; } = _ => Task.FromResult(true);

private bool _previewDrop;
private TreeViewItem<TItem>? _draggingItem;

private void OnItemDragStart(TreeViewItem<TItem> treeViewItem)
{
_previewDrop = true;
_draggingItem = treeViewItem;
StateHasChanged();
}

private void OnItemDragEnd()
{
_previewDrop = false;
_draggingItem = null;
StateHasChanged();
}

private async Task OnItemDrop(TreeDropEventArgs<TItem> e)
{
if (_draggingItem is not null)
{
e.Source = _draggingItem;
var allowChangeSource = await OnDrop.Invoke(e);
if (!allowChangeSource)
{
return;
}

// 如果允许改变源节点则更新拖拽项的父对象以及排序
if (_draggingItem.Parent is not null)
{
_draggingItem.Parent.Items.Remove(_draggingItem);
}
else
{
// 没有父对象,则从顶层节点集合中移除
Items.Remove(_draggingItem);
}

_draggingItem.IsExpand = e.ExpandAfterDrop;

switch (e.DropType)
{
case TreeDropType.AsFirstChild:
// 插入到目标的第一个子节点
e.Target.Items.Insert(0, _draggingItem);
_draggingItem.Parent = e.Target;
break;
case TreeDropType.AsLastChild:
// 插入到目标的最后一个子节点
e.Target.Items.Add(_draggingItem);
_draggingItem.Parent = e.Target;
break;
case TreeDropType.AsSiblingBelow:
// 作为目标的下一个兄弟节点
if (e.Target.Parent is not null)
{
var index = e.Target.Parent.Items.IndexOf(e.Target);
if (index >= 0 && index < e.Target.Parent.Items.Count - 1)
{
e.Target.Parent.Items.Insert(index + 1, _draggingItem);
}
else
{
e.Target.Parent.Items.Add(_draggingItem);
}

_draggingItem.Parent = e.Target.Parent;
}
// 如果目标没有父节点,则作为顶层节点处理
else
{
// 目标节点的Index
var index = Items.IndexOf(e.Target);
if (index >= 0 && index < Items.Count - 1)
{
Items.Insert(index + 1, _draggingItem);
}

Check warning on line 820 in src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs

View check run for this annotation

Codecov / codecov/patch

src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs#L818-L820

Added lines #L818 - L820 were not covered by tests
else
{
Items.Add(_draggingItem);
}

_draggingItem.Parent = null;
}

break;
}

_draggingItem = null;
_previewDrop = false;
_rows = GetTreeItems().ToFlat();

StateHasChanged();
}
else
{
throw new InvalidOperationException("拖拽的项为空");
}
}

/// <summary>
/// Gets all selected node collections
/// </summary>
Expand Down Expand Up @@ -763,3 +877,29 @@

private int GetIndex(TreeViewItem<TItem> item) => Rows.IndexOf(item);
}

/// <summary>
/// Represents the event arguments for the TreeView drop event.
/// </summary>
public class TreeDropEventArgs<TItem>
{
/// <summary>
/// Gets or sets the source item that is being dropped.
/// </summary>
public TreeViewItem<TItem>? Source { get; set; }

/// <summary>
/// Gets or sets the target item.
/// </summary>
public TreeViewItem<TItem> Target { get; set; } = null!;

/// <summary>
/// Gets or sets the drop type.
/// </summary>
public TreeDropType DropType { get; set; }

/// <summary>
/// Gets or sets whether to expand the source item's children when dropping.
/// </summary>
public bool ExpandAfterDrop { get; set; }
}
Loading
Loading