Skip to content

Commit 8ae8825

Browse files
committed
feat:DraggableTree
1 parent 7065ff2 commit 8ae8825

File tree

8 files changed

+463
-13
lines changed

8 files changed

+463
-13
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
Introduction="@Localizer["TreeViewNormalIntro"]"
2828
Name="Normal">
2929
<section ignore>@((MarkupString)Localizer["TreeViewNormalDescription"].Value)</section>
30-
<TreeView Items="@NormalItems" OnTreeItemClick="@OnTreeItemClick" ShowToolbar="true"></TreeView>
30+
<TreeView Items="@NormalItems" ItemDraggable="true" OnTreeItemClick="@OnTreeItemClick" ShowToolbar="true"
31+
OnDrop="OnDrop"></TreeView>
3132
<ConsoleLogger @ref="Logger1" />
3233
</DemoBlock>
3334

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ private Task OnTreeItemClick(TreeViewItem<TreeFoo> item)
8282
Logger1.Log($"TreeItem: {item.Text} clicked");
8383
return Task.CompletedTask;
8484
}
85+
private Task<bool> OnDrop(TreeDropEventArgs<TreeFoo> arg)
86+
{
87+
Logger1.Log("Move node from " + arg.Source?.Text + " to " + arg.Target.Text + " " + arg.DropType);
88+
return Task.FromResult(true);
89+
}
8590

8691
private Task OnTreeItemKeyboardClick(TreeViewItem<TreeFoo> item)
8792
{

src/BootstrapBlazor/Components/TreeView/TreeView.razor

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,16 @@ else
6666

6767
@code {
6868
RenderFragment RenderRow(TreeViewItem<TItem> item) =>
69-
@<TreeViewRow @key="item" IsActive="GetActive(item)" Index="GetIndex(item)" Item="item"
70-
NodeIcon="@NodeIcon" ExpandNodeIcon="@ExpandNodeIcon" LoadingIcon="@LoadingIcon"
71-
MaxSelectedCount="MaxSelectedCount"
72-
ToolbarEditTitle="@ToolbarEditTitle" ToolbarEditLabelText="@ToolbarEditLabelText"
73-
IsDisabled="IsDisabled" CanExpandWhenDisabled="CanExpandWhenDisabled"
74-
ShowCheckbox="ShowCheckbox" ShowIcon="ShowIcon"
75-
ShowToolbar="ShowToolbar" ShowToolbarCallback="ShowToolbarCallback"
76-
OnToggleNodeAsync="OnToggleNodeAsync" OnClick="OnClick"
77-
OnBeforeStateChangedCallback="OnBeforeStateChangedCallback"
78-
OnCheckStateChanged="OnCheckStateChanged"
79-
OnUpdateCallbackAsync="OnUpdateCallbackAsync" ToolbarTemplate="ToolbarTemplate"></TreeViewRow>;
69+
@<TreeViewRow @key="item" IsActive="GetActive(item)" Index="GetIndex(item)" Item="item"
70+
NodeIcon="@NodeIcon" ExpandNodeIcon="@ExpandNodeIcon" LoadingIcon="@LoadingIcon"
71+
MaxSelectedCount="MaxSelectedCount" Draggable="ItemDraggable" PreviewDrop="_previewDrop"
72+
ToolbarEditTitle="@ToolbarEditTitle" ToolbarEditLabelText="@ToolbarEditLabelText"
73+
IsDisabled="IsDisabled" CanExpandWhenDisabled="CanExpandWhenDisabled"
74+
ShowCheckbox="ShowCheckbox" ShowIcon="ShowIcon"
75+
ShowToolbar="ShowToolbar" ShowToolbarCallback="ShowToolbarCallback"
76+
OnToggleNodeAsync="OnToggleNodeAsync" OnClick="OnClick"
77+
OnBeforeStateChangedCallback="OnBeforeStateChangedCallback"
78+
OnCheckStateChanged="OnCheckStateChanged"
79+
OnItemDragStart="OnItemDragStart" OnItemDragEnd="OnItemDragEnd" OnItemDrop="OnItemDrop"
80+
OnUpdateCallbackAsync="OnUpdateCallbackAsync" ToolbarTemplate="ToolbarTemplate"></TreeViewRow>;
8081
}

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

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,120 @@ public void ClearCheckedItems()
727727
StateHasChanged();
728728
}
729729

730+
731+
#region Draggable
732+
733+
734+
/// <summary>
735+
/// Gets or sets whether to enable item dragging. Default is false.
736+
/// </summary>
737+
[Parameter]
738+
public bool ItemDraggable { get; set; }
739+
740+
/// <summary>
741+
/// Gets or sets the callback method to be invoked when an item is dropped.
742+
/// Drop action can be cancelled by returning false.
743+
/// </summary>
744+
[Parameter]
745+
public Func<TreeDropEventArgs<TItem>, Task<bool>> OnDrop { get; set; } = _ => Task.FromResult(true);
746+
747+
private bool _previewDrop;
748+
private TreeViewItem<TItem>? _draggingItem;
749+
750+
private void OnItemDragStart(TreeViewItem<TItem> treeViewItem)
751+
{
752+
_previewDrop = true;
753+
_draggingItem = treeViewItem;
754+
StateHasChanged();
755+
}
756+
757+
private void OnItemDragEnd()
758+
{
759+
_previewDrop = false;
760+
_draggingItem = null;
761+
StateHasChanged();
762+
}
763+
764+
private async Task OnItemDrop(TreeDropEventArgs<TItem> e)
765+
{
766+
if (_draggingItem is not null)
767+
{
768+
e.Source = _draggingItem;
769+
var allowChangeSource = await OnDrop.Invoke(e);
770+
if (!allowChangeSource)
771+
{
772+
return;
773+
}
774+
775+
// 如果允许改变源节点则更新拖拽项的父对象以及排序
776+
_draggingItem.Parent?.Items.Remove(_draggingItem);
777+
_draggingItem.IsExpand = e.ExpandAfterDrop;
778+
779+
switch (e.DropType)
780+
{
781+
case TreeDropType.AsFirstChild:
782+
// 插入到目标的第一个子节点
783+
e.Target.Items.Insert(0, _draggingItem);
784+
_draggingItem.Parent = e.Target;
785+
break;
786+
case TreeDropType.AsLastChild:
787+
// 插入到目标的最后一个子节点
788+
e.Target.Items.Add(_draggingItem);
789+
_draggingItem.Parent = e.Target;
790+
break;
791+
case TreeDropType.AsSiblingBelow:
792+
// 作为目标的下一个兄弟节点
793+
if (e.Target.Parent is not null)
794+
{
795+
var index = e.Target.Parent.Items.IndexOf(e.Target);
796+
if (index >= 0 && index < e.Target.Parent.Items.Count - 1)
797+
{
798+
e.Target.Parent.Items.Insert(index + 1, _draggingItem);
799+
}
800+
else
801+
{
802+
e.Target.Parent.Items.Add(_draggingItem);
803+
}
804+
805+
_draggingItem.Parent = e.Target.Parent;
806+
}
807+
// 如果目标没有父节点,则作为顶层节点处理
808+
else
809+
{
810+
// 目标节点的Index
811+
var index = Items.IndexOf(e.Target);
812+
if (index >= 0 && index < Items.Count - 1)
813+
{
814+
Items.Insert(index + 1, _draggingItem);
815+
}
816+
else
817+
{
818+
Items.Add(_draggingItem);
819+
}
820+
821+
_draggingItem.Parent = null;
822+
}
823+
824+
break;
825+
default:
826+
throw new ArgumentOutOfRangeException();
827+
}
828+
829+
830+
_draggingItem = null;
831+
_previewDrop = false;
832+
_rows = GetTreeItems().ToFlat();
833+
834+
StateHasChanged();
835+
}
836+
else
837+
{
838+
throw new InvalidOperationException("拖拽的项为空");
839+
}
840+
}
841+
842+
#endregion
843+
730844
/// <summary>
731845
/// Gets all selected node collections
732846
/// </summary>
@@ -763,3 +877,30 @@ private List<TreeViewItem<TItem>> Rows
763877

764878
private int GetIndex(TreeViewItem<TItem> item) => Rows.IndexOf(item);
765879
}
880+
881+
/// <summary>
882+
/// Represents the event arguments for the TreeView drop event.
883+
/// </summary>
884+
public class TreeDropEventArgs<TItem>
885+
{
886+
/// <summary>
887+
/// Gets or sets the source item that is being dropped.
888+
/// </summary>
889+
public TreeViewItem<TItem>? Source { get; set; }
890+
891+
/// <summary>
892+
/// Gets or sets the target item.
893+
/// </summary>
894+
public TreeViewItem<TItem> Target { get; set; } = null!;
895+
896+
/// <summary>
897+
/// Gets or sets the drop type.
898+
/// </summary>
899+
public TreeDropType DropType { get; set; }
900+
901+
/// <summary>
902+
/// Gets or sets whether to expand the source item's children when dropping.
903+
/// </summary>
904+
public bool ExpandAfterDrop { get; set; }
905+
906+
}

src/BootstrapBlazor/Components/TreeView/TreeView.razor.scss

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
--bb-tree-disabled-opacity: #{$bb-tree-disabled-opacity};
1515
--bb-tree-search-height: #{$bb-tree-search-height};
1616
--bb-tree-item-bg-radius: var(--bs-border-radius);
17+
--bb-tree-drop-preview-color: #{$bb-primary-color};
1718
position: relative;
1819
height: 100%;
1920
display: flex;
@@ -38,6 +39,7 @@
3839
flex-wrap: nowrap;
3940
align-items: center;
4041
cursor: pointer;
42+
user-select: none;
4143

4244
.tree-content-toolbar {
4345
display: none;
@@ -55,6 +57,30 @@
5557
flex-shrink: 0;
5658
align-items: center;
5759
border-radius: var(--bs-border-radius);
60+
position: relative;
61+
62+
.tree-drop-zone {
63+
position: absolute;
64+
top: 4px;
65+
left: 20px;
66+
right: 0;
67+
bottom: -12px;
68+
pointer-events: all;
69+
background-color: transparent;
70+
display: grid;
71+
grid-template-rows: 1fr 16px;
72+
73+
.tree-drop-child-inside {
74+
grid-row: 1 / 2;
75+
//background-color: rgba(236, 209, 62, 0.5);
76+
}
77+
78+
.tree-drop-child-below {
79+
grid-row: 2 / 3;
80+
//background-color: rgba(94, 236, 62, 0.5);
81+
}
82+
83+
}
5884
}
5985

6086
.node-icon {
@@ -121,6 +147,7 @@
121147
display: inline-flex;
122148
align-items: center;
123149
padding: var(--bb-tree-node-padding);
150+
position: relative;
124151
flex: 1;
125152

126153
.tree-icon {
@@ -133,6 +160,69 @@
133160
white-space: nowrap;
134161
}
135162

163+
.tree-preview-child-last {
164+
position: absolute;
165+
top: -2px;
166+
left: -2px;
167+
right: -2px;
168+
bottom: -2px;
169+
border: 2px solid var(--bb-tree-drop-preview-color);
170+
border-radius: 6px;
171+
pointer-events: none;
172+
z-index: 1;
173+
background: transparent;
174+
}
175+
176+
.tree-preview-child-first {
177+
position: absolute;
178+
bottom: -4px;
179+
height: 8px;
180+
left: 20px;
181+
right: 0;
182+
display: flex;
183+
flex-direction: row;
184+
align-items: center;
185+
pointer-events: none;
186+
187+
.tree-preview-circle {
188+
width: 8px;
189+
height: 8px;
190+
border-radius: 50%;
191+
background-color: var(--bb-tree-drop-preview-color);
192+
}
193+
194+
.tree-preview-line {
195+
flex: 1;
196+
height: 2px;
197+
background-color: var(--bb-tree-drop-preview-color);
198+
}
199+
}
200+
201+
.tree-preview-below {
202+
position: absolute;
203+
bottom: -4px;
204+
height: 8px;
205+
left: 0;
206+
right: 0;
207+
display: flex;
208+
flex-direction: row;
209+
align-items: center;
210+
pointer-events: none;
211+
212+
.tree-preview-circle {
213+
width: 8px;
214+
height: 8px;
215+
border-radius: 50%;
216+
background-color: var(--bb-tree-drop-preview-color);
217+
}
218+
219+
.tree-preview-line {
220+
flex: 1;
221+
height: 2px;
222+
background-color: var(--bb-tree-drop-preview-color);
223+
}
224+
}
225+
136226
&.disabled {
137227
opacity: var(--bb-tree-disabled-opacity);
138228
}
@@ -149,6 +239,10 @@
149239
display: none;
150240
}
151241
}
242+
243+
.tree-drop-pass {
244+
pointer-events: none;
245+
}
152246
}
153247

154248
.tree-view-edit-form {

src/BootstrapBlazor/Components/TreeView/TreeViewRow.razor

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,23 @@
99
<div class="tree-content-body">
1010
<DynamicElement TagName="i" class="@CaretClassString" TriggerClick="CanTriggerClickNode" OnClick="ToggleNodeAsync"></DynamicElement>
1111
<i class="@NodeLoadingClassString"></i>
12+
13+
@if (Draggable && !_draggingItem)
14+
{
15+
<div class="tree-drop-zone">
16+
<div class="tree-drop-child-inside"
17+
ondragover="event.preventDefault();"
18+
@ondragenter="DragEnterChildInside"
19+
@ondragleave="DragLeaveChildInside"
20+
@ondrop="DropChildInside"></div>
21+
<div class="tree-drop-child-below"
22+
ondragover="event.preventDefault();"
23+
@ondragenter="DragEnterChildBelow"
24+
@ondragleave="DragLeaveChildBelow"
25+
@ondrop="DropChildBelow"></div>
26+
</div>
27+
}
28+
1229
@if (ShowCheckbox)
1330
{
1431
<Checkbox Value="@Item" IsDisabled="ItemDisabledState"
@@ -17,7 +34,9 @@
1734
OnBeforeStateChanged="@(MaxSelectedCount > 0 ? state => TriggerBeforeStateChangedCallback(state) : null)">
1835
</Checkbox>
1936
}
20-
<DynamicElement class="@NodeClassString" TriggerClick="!ItemDisabledState" OnClick="ClickRow">
37+
<DynamicElement class="@NodeClassString" TriggerClick="!ItemDisabledState" OnClick="ClickRow"
38+
draggable="@(Draggable ? "true" : "false")"
39+
@ondragstart="DragStart" @ondragend="DragEnd">
2140
@if (ShowIcon)
2241
{
2342
<i class="@IconClassString"></i>
@@ -42,6 +61,25 @@
4261
OnUpdateCallbackAsync="OnUpdateCallbackAsync"></TreeViewToolbarEditButton>
4362
}
4463
}
64+
65+
@if (_previewChildLast)
66+
{
67+
<div class="tree-preview-child-last"></div>
68+
}
69+
@if (_previewChildFirst)
70+
{
71+
<div class="tree-preview-child-first">
72+
<div class="tree-preview-circle"></div>
73+
<div class="tree-preview-line"></div>
74+
</div>
75+
}
76+
@if (_previewBelow)
77+
{
78+
<div class="tree-preview-below">
79+
<div class="tree-preview-circle"></div>
80+
<div class="tree-preview-line"></div>
81+
</div>
82+
}
4583
</DynamicElement>
4684
</div>
4785
</div>

0 commit comments

Comments
 (0)