!2393 feat(#I4SNXQ): add DragDrop component

* Merge branch 'main' into dev-drop
* refactor: 更新拖拽组件菜单
* refactor: 格式化文档
* feat: 增加 IDispose 接口调用
* refactor: 格式代码
* 修复警告,添加文档
* 修复最后一个Item不执行OnDragEnd的问题
* Merge branch 'main' into dev-drop
* !2366 创建DragDrop组件
* 同步版本
* 解决一些bug
* 第一个版本
* 增加Card相关文档
This commit is contained in:
j4587698 2022-02-07 13:47:09 +00:00 committed by Argo
parent 2ac51a7520
commit 13ee6a2207
12 changed files with 868 additions and 5 deletions

View File

@ -2543,6 +2543,7 @@
"Client": "WebClientService",
"LayoutComponents": "LAYOUTS",
"Divider": "Divider",
"Dragdrop": "DragDrop",
"Layout": "Layout",
"Footer": "Footer",
"Row": "Row",

View File

@ -2548,6 +2548,7 @@
"LayoutComponents": "布局组件",
"Client": "客户信息服务 Client",
"Divider": "分割线 Divider",
"Dragdrop": "拖拽组件 DragDrop",
"Layout": "布局组件 Layout",
"FullScreen": "全屏组件 FullScreen",
"Footer": "页脚组件 Footer",

View File

@ -0,0 +1,86 @@
@page "/dragdrops"
<h3>DragDrop 拖拽</h3>
<h4>用于拖拽使用</h4>
<DemoBlock Title="基本用法" Introduction="简单拖拽" Name="Normal">
<Row ItemsPerRow="ItemsPerRow.Two">
<Dropzone TItem="string" Items="@StrList1">
<Card IsShadow="true">
<CardBody>
@context
</CardBody>
</Card>
</Dropzone>
<Dropzone TItem="string" Items="@StrList2">
<Card IsShadow="true">
<CardBody>
@context
</CardBody>
</Card>
</Dropzone>
</Row>
</DemoBlock>
<DemoBlock Title="复制到新容器" Introduction="使用<code>CopyItem</code>复制一份新的到新位置" Name="Normal">
<Row ItemsPerRow="ItemsPerRow.Two">
<Dropzone TItem="string" Items="@StrList1">
<Card IsShadow="true">
<CardBody>
@context
</CardBody>
</Card>
</Dropzone>
<Dropzone TItem="string" Items="@StrList2" CopyItem="s => new string(s)">
<Card IsShadow="true">
<CardBody>
@context
</CardBody>
</Card>
</Dropzone>
</Row>
</DemoBlock>
<DemoBlock Title="限制拖入的内容" Introduction="使用<code>Accepts</code>只允许左侧拖入10并且使用<code>AllowsDrag</code>限制2不能被拖动使用<code>MaxItems</code>限制右侧最多拥有6个节点" Name="Normal">
<Row ItemsPerRow="ItemsPerRow.Two">
<Dropzone TItem="string" Items="@StrList1" Accepts="@((s, s1) => s == "10")" AllowsDrag="@(s => s != "2")">
<Card IsShadow="true">
<CardBody>
@context
</CardBody>
</Card>
</Dropzone>
<Dropzone TItem="string" Items="@StrList2" MaxItems="6">
<Card IsShadow="true">
<CardBody>
@context
</CardBody>
</Card>
</Dropzone>
</Row>
</DemoBlock>
<DemoBlock Title="各种事件" Introduction="当拖入数量超限时<code>OnItemDropRejectedByMaxItemLimit</code>,当拖拽被禁止时<code>OnItemDropRejected</code>返回底下的Item<code>OnReplacedItemDrop</code>,返回放下的Item<code>OnItemDrop</code>" Name="Normal">
<Row ItemsPerRow="ItemsPerRow.Two">
<Dropzone TItem="string" Items="@StrList1" Accepts="@((s, s1) => s == "10")" OnItemDropRejected="@OnItemDropRejected" OnItemDrop="@OnItemDrop" OnReplacedItemDrop="@OnReplacedItemDrop">
<Card IsShadow="true">
<CardBody>
@context
</CardBody>
</Card>
</Dropzone>
<Dropzone TItem="string" Items="@StrList2" MaxItems="6" OnItemDropRejectedByMaxItemLimit="@OnItemDropRejectedByMaxItemLimit" OnItemDrop="@OnItemDrop" OnReplacedItemDrop="@OnReplacedItemDrop">
<Card IsShadow="true">
<CardBody>
@context
</CardBody>
</Card>
</Dropzone>
</Row>
<BlockLogger @ref="Trace" class="mt-3" />
</DemoBlock>
<AttributeTable Items="@GetAttributes()" />
<MethodTable Items="GetMethods()"></MethodTable>

View File

@ -0,0 +1,153 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using BootstrapBlazor.Components;
using BootstrapBlazor.Shared.Common;
using BootstrapBlazor.Shared.Components;
namespace BootstrapBlazor.Shared.Samples;
/// <summary>
///
/// </summary>
public partial class DragDrops
{
[NotNull]
private List<string>? StrList1 { get; set; }
[NotNull]
private List<string>? StrList2 { get; set; }
[NotNull]
private BlockLogger? Trace { get; set; }
/// <summary>
///
/// </summary>
protected override void OnInitialized()
{
base.OnInitialized();
StrList1 = new List<string>()
{
"1",
"2",
"3",
"4",
"5"
};
StrList2 = new List<string>()
{
"6",
"7",
"8",
"9",
"10"
};
}
private void OnReplacedItemDrop(string item)
{
Trace?.Log($"新元素放在{item}下");
}
private void OnItemDrop(string item)
{
Trace?.Log($"{item}被放下");
}
private void OnItemDropRejected(string item)
{
Trace?.Log($"{item}被拒绝");
}
private void OnItemDropRejectedByMaxItemLimit(string item)
{
Trace?.Log($"{item}由于超过最大数量限制被禁止");
}
/// <summary>
///
/// </summary>
/// <returns></returns>
private static IEnumerable<AttributeItem> GetAttributes() => new AttributeItem[]
{
new()
{
Name = "MaxItems",
Description = "最大数量,null为不限制",
Type = "int?",
ValueList = " — ",
DefaultValue = "null"
},
new()
{
Name = "ChildContent",
Description = "内容组件",
Type = "RenderFragment<TItem>?",
ValueList = " — ",
DefaultValue = " — "
},
};
private static IEnumerable<MethodItem> GetMethods() => new MethodItem[]
{
new()
{
Name = nameof(Dropzone<MethodItem>.Accepts),
Description = "是否运行拖放",
Parameters = "Func<TItem?, TItem?, bool>",
ReturnValue = "bool "
},
new()
{
Name = nameof(Dropzone<MethodItem>.AllowsDrag),
Description = "节点是否允许被拖拽",
Parameters = "TItem",
ReturnValue = "bool"
},
new()
{
Name = nameof(Dropzone<MethodItem>.CopyItem),
Description = "复制一个新的 Item 到目标位置",
Parameters = "TItem, TItem",
ReturnValue = "TItem"
},
new()
{
Name = nameof(Dropzone<MethodItem>.ItemWrapperClass),
Description = "针对 Item 添加特殊的 css class",
Parameters = "TItem",
ReturnValue = "string"
},
new()
{
Name = nameof(Dropzone<MethodItem>.OnItemDrop),
Description = "Item 释放时的事件",
Parameters = " — ",
ReturnValue = " — "
},
new()
{
Name = nameof(Dropzone<MethodItem>.OnItemDropRejected),
Description = "Item 释放被拒绝时的事件",
Parameters = " — ",
ReturnValue = " — "
},
new()
{
Name = nameof(Dropzone<MethodItem>.OnReplacedItemDrop),
Description = "当 Item 在另一个 Item 上,不是空白处被释放时的事件",
Parameters = " — ",
ReturnValue = " — "
},
new()
{
Name = nameof(Dropzone<MethodItem>.OnItemDropRejectedByMaxItemLimit),
Description = "Item 因为 Dropzone 内最大数量超限被拒绝时的事件",
Parameters = " — ",
ReturnValue = " — "
}
};
}

View File

@ -523,7 +523,7 @@ public sealed partial class NavMenu
{
Text = Localizer["Transition"],
Url = "transitions"
},
}
};
AddBadge(item);
}
@ -885,6 +885,12 @@ public sealed partial class NavMenu
{
item.Items = new List<DemoMenuItem>
{
new()
{
IsNew = true,
Text = Localizer["Dragdrop"],
Url = "dragdrops"
},
new()
{
Text = Localizer["Divider"],

View File

@ -14,6 +14,7 @@
"datetimepickers": "DateTimePickers",
"datetimeranges": "DateTimeRanges",
"doughnut": "Charts/Doughnut",
"dragdrops": "DragDrops",
"dropdownlists": "DropdownLists",
"dropdownwidgets": "DropdownWidgets",
"fullscreens": "FullScreens",

View File

@ -0,0 +1,92 @@
/*add this to avoid flickering*/
.bb-dd-inprogess > * {
pointer-events: none;
}
/*dropzone style style*/
.bb-dd-dropzone {
min-height: 50px;
}
/*drag drop styles*/
.bb-dd-spacing {
height: 10px;
}
.bb-dd-spacing-dragged-over {
padding: 25px;
}
.bb-dd-dragged-over {
background-color: lightgray;
opacity: 0.6;
animation: blinker 1s linear infinite;
}
.bb-dd-dragged-over > div {
background-color: lightgray;
opacity: 0.6;
animation: blinker 1s linear infinite;
}
.bb-dd-dragged-over-denied {
background-color: red;
opacity: 0.6;
animation: blinker 1s linear infinite;
}
.bb-dd-in-transit {
opacity: 0;
}
.bb-dd-in-transit > div {
opacity: 0;
}
@keyframes blinker {
50% {
opacity: 0;
}
}
.blink_me {
animation: blinker 1s linear infinite;
}
/*for flex demo*/
.bb-flex .bb-dd-spacing {
width: 20px;
height: auto;
}
.bb-flex .bb-dd-dragged-over {
background-color: lightgray;
opacity: 0.6;
animation: blinker 1s linear infinite;
}
.bb-flex .bb-dd-dragged-over > div {
background-color: lightgray;
opacity: 0.9;
animation: blinker 1s linear infinite;
}
.bb-flex .bb-dd-in-transit {
background-color: orangered;
}
.bb-flex .bb-dd-in-transit > div {
background-color: orangered;
}
.bb-dd-noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Old versions of Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome, Edge, Opera and Firefox */
}

View File

@ -0,0 +1,61 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
namespace BootstrapBlazor.Components;
/// <summary>
/// 拖拽服务
/// </summary>
/// <typeparam name="T"></typeparam>
internal class DragDropService<T>
{
/// <summary>
/// 活动的Item
/// </summary>
public T? ActiveItem { get; set; }
/// <summary>
/// 悬停的项目
/// </summary>
public T? DragTargetItem { get; set; }
/// <summary>
/// 被拖拽的Items
/// </summary>
public IList<T?>? Items { get; set; }
/// <summary>
/// 活动的Id
/// </summary>
public int? ActiveSpacerId { get; set; }
/// <summary>
/// 之前的位置
/// </summary>
public int? OldIndex { get; set; }
/// <summary>
/// 通知刷新
/// </summary>
public EventHandler? StateHasChanged { get; set; }
public void Reset()
{
if (OldIndex is >= 0 && Items != null && ActiveItem != null)
{
Items.Insert(OldIndex.Value, ActiveItem);
}
Commit();
}
public void Commit()
{
ActiveItem = default;
ActiveSpacerId = null;
Items = null;
DragTargetItem = default;
StateHasChanged?.Invoke(this, EventArgs.Empty);
}
}

View File

@ -0,0 +1,41 @@
@typeparam TItem
@inherits BootstrapComponentBase
@namespace BootstrapBlazor.Components
<div @attributes="@AdditionalAttributes" class="@ClassName"
@ondragover:preventDefault @ondragover="() => {}"
@ondragenter:preventDefault @ondragenter="() => {}"
@ondrop:preventDefault @ondrop="@OnDrop"
ondragstart="event.dataTransfer.setData('text', event.target.id);"
@ondrop:stopPropagation
@ondragenter:stopPropagation
@ondragend:stopPropagation
@ondragover:stopPropagation
@ondragleave:stopPropagation
@ondragstart:stopPropagation>
<div @ondrop="() => OnDropItemOnSpacing(0)" @ondrop:stopPropagation @ondragenter="() => DragDropService.ActiveSpacerId = 0"
@ondragleave="() => DragDropService.ActiveSpacerId = null" class="@GetClassesForSpacing(0)"></div>
@foreach (var item in Items)
{
<div draggable="@IsItemDragable(item)"
@ondragstart="() => OnDragStart(item)"
@ondragend="@OnDragEnd"
@ondragenter="() => OnDragEnter(item)"
@ondragleave="@OnDragLeave"
class="@GetItemClass(item) @ItemClass"
style="@(item == null ? "display:none" : "")">
@if (item != null)
{
@ChildContent?.Invoke(item)
}
</div>
@if (MaxItems is null or > 1)
{
<div @ondrop="()=>OnDropItemOnSpacing(Items.IndexOf(item)+1)" @ondrop:stopPropagation @ondragenter="()=>DragDropService.ActiveSpacerId = Items.IndexOf(item)+1" @ondragleave="()=>DragDropService.ActiveSpacerId = null" class="@ItemClass @GetClassesForSpacing(Items.IndexOf(item)+1)"></div>
}
}
</div>

View File

@ -0,0 +1,420 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using Microsoft.AspNetCore.Components;
using System.Text;
namespace BootstrapBlazor.Components;
/// <summary>
/// 拖拽容器
/// </summary>
/// <typeparam name="TItem"></typeparam>
public partial class Dropzone<TItem> : IDisposable
{
/// <summary>
/// 获取/设置 拖拽列表
/// </summary>
[Parameter]
[NotNull]
public IList<TItem?>? Items { get; set; }
/// <summary>
/// 获取/设置 最大数量Null为不限制
/// </summary>
[Parameter]
public int? MaxItems { get; set; }
/// <summary>
/// 子组件
/// </summary>
[Parameter]
public RenderFragment<TItem>? ChildContent { get; set; }
/// <summary>
/// 每个Item的特殊class
/// </summary>
[Parameter]
public Func<TItem, string>? ItemWrapperClass { get; set; }
/// <summary>
/// 复制内容
/// </summary>
[Parameter]
public Func<TItem, TItem>? CopyItem { get; set; }
/// <summary>
/// 是否允许拖拽释放
/// </summary>
[Parameter]
public Func<TItem?, TItem?, bool>? Accepts { get; set; }
/// <summary>
/// 当拖拽因为数量超限被禁止时调用
/// </summary>
[Parameter]
public EventCallback<TItem> OnItemDropRejectedByMaxItemLimit { get; set; }
/// <summary>
/// 当拖拽被禁止时调用
/// </summary>
[Parameter]
public EventCallback<TItem> OnItemDropRejected { get; set; }
/// <summary>
/// 返回被替换的Item
/// </summary>
[Parameter]
public EventCallback<TItem> OnReplacedItemDrop { get; set; }
/// <summary>
/// 返回放下的Item
/// </summary>
[Parameter]
public EventCallback<TItem> OnItemDrop { get; set; }
/// <summary>
/// 当前节点是否允许被拖拽
/// </summary>
[Parameter]
public Func<TItem, bool>? AllowsDrag { get; set; }
/// <summary>
/// service
/// </summary>
[Inject]
[NotNull]
private DragDropService<TItem>? DragDropService { get; set; }
/// <summary>
/// 获得 拖拽容器样式集合
/// </summary>
/// <returns></returns>
protected string? ClassName => CssBuilder.Default("bb-dd-dropzone").Build();
private string? ItemClass =>
CssBuilder.Default("").AddClass("bb-dd-inprogess", DragDropService.ActiveItem != null).Build();
private string GetItemClass(TItem? item)
{
if (item == null)
{
return "";
}
var builder = new StringBuilder();
builder.Append("bb-dd-draggable");
if (ItemWrapperClass != null)
{
builder.Append($" {ItemWrapperClass(item)}");
}
var activeItem = DragDropService.ActiveItem;
if (activeItem == null)
{
return builder.ToString();
}
if (item.Equals(activeItem))
{
builder.Append(" no-pointer-events");
}
if (!item.Equals(activeItem) && item.Equals(DragDropService.DragTargetItem))
{
builder.Append(IsItemAccepted(DragDropService.DragTargetItem)
? " bb-dd-dragged-over"
: " bb-dd-dragged-over-denied");
}
if (AllowsDrag != null && !AllowsDrag(item))
{
builder.Append(" bb-dd-noselect");
}
return builder.ToString();
}
private string GetClassesForSpacing(int spacerId)
{
var builder = new StringBuilder();
builder.Append("bb-dd-spacing");
if (DragDropService.ActiveItem == null)
{
return builder.ToString();
}
//if active space id and item is from another dropzone -> always create insert space
if (DragDropService.ActiveSpacerId == spacerId && Items.IndexOf(DragDropService.ActiveItem) == -1)
{
builder.Append(" bb-dd-spacing-dragged-over");
} // else -> check if active space id and that it is an item that needs space
else if (DragDropService.ActiveSpacerId == spacerId && (spacerId != Items.IndexOf(DragDropService.ActiveItem)) && (spacerId != Items.IndexOf(DragDropService.ActiveItem) + 1))
{
builder.Append(" bb-dd-spacing-dragged-over");
}
return builder.ToString();
}
private string IsItemDragable(TItem? item)
{
if (item == null)
{
return "false";
}
if (AllowsDrag == null)
{
return "true";
}
if (item == null)
{
return "false";
}
return AllowsDrag(item).ToString();
}
private bool IsDropAllowed()
{
if (!IsValidItem())
{
return false;
}
var activeItem = DragDropService.ActiveItem;
if (IsMaxItemLimitReached())
{
OnItemDropRejectedByMaxItemLimit.InvokeAsync(activeItem);
return false;
}
if (!IsItemAccepted(activeItem))
{
OnItemDropRejected.InvokeAsync(activeItem);
return false;
}
return true;
}
private bool IsItemAccepted(TItem? dragTargetItem)
{
if (Accepts == null)
{
return true;
}
return Accepts(DragDropService.ActiveItem, dragTargetItem);
}
private bool IsMaxItemLimitReached()
{
var activeItem = DragDropService.ActiveItem;
if (activeItem == null)
{
return false;
}
return (!Items.Contains(activeItem) && MaxItems.HasValue && MaxItems == Items.Count);
}
private bool IsValidItem()
{
return DragDropService.ActiveItem != null;
}
private void OnDropItemOnSpacing(int newIndex)
{
if (!IsDropAllowed())
{
DragDropService.Reset();
return;
}
var activeItem = DragDropService.ActiveItem;
if (activeItem == null)
{
DragDropService.Reset();
return;
}
// 当从其他地方拖过来并且当前以达到最大限制,放弃
if (IsMaxItemLimitReached())
{
DragDropService.Reset();
return;
}
bool sameDropZone = Equals(DragDropService.Items, Items);
if (CopyItem == null || sameDropZone)
{
Items.Insert(newIndex, activeItem);
DragDropService.Commit();
}
else
{
// for the same zone - do not call CopyItem
Items.Insert(newIndex, CopyItem(activeItem));
DragDropService.Reset();
}
//Operation is finished
OnItemDrop.InvokeAsync(activeItem);
}
private void OnDragStart(TItem? item)
{
DragDropService.OldIndex = Items.IndexOf(item);
DragDropService.ActiveItem = item;
DragDropService.Items = Items;
Items.Remove(item);
if (DragDropService.OldIndex >= Items.Count)
{
Items.Add(default);
}
}
private void OnDragEnd()
{
if (DragDropService.Items != null && DragDropService.OldIndex != null && DragDropService.ActiveItem != null)
{
DragDropService.Items.Insert(DragDropService.OldIndex.Value, DragDropService.ActiveItem);
StateHasChanged();
}
Items.Remove(default);
}
private void OnDragEnter(TItem? item)
{
if (item == null)
{
return;
}
var activeItem = DragDropService.ActiveItem;
if (activeItem == null)
{
return;
}
if (item.Equals(activeItem))
{
return;
}
if (!IsValidItem())
{
return;
}
if (IsMaxItemLimitReached())
{
return;
}
if (!IsItemAccepted(item))
{
return;
}
DragDropService.DragTargetItem = item;
StateHasChanged();
}
private void OnDragLeave()
{
DragDropService.DragTargetItem = default;
StateHasChanged();
}
private void OnDrop()
{
if (!IsDropAllowed())
{
DragDropService.Reset();
return;
}
var activeItem = DragDropService.ActiveItem;
if (activeItem == null)
{
return;
}
// 如果没有释放在Item上则添加到最后
if (DragDropService.DragTargetItem == null)
{
// 当从其他位置拖拽过来的时候
if (!Equals(DragDropService.Items, Items) && CopyItem != null)
{
Items.Insert(Items.Count, CopyItem(activeItem));
DragDropService.Reset();
}
else
{
Items.Insert(Items.Count, activeItem);
DragDropService.Commit();
}
}
else
{
OnReplacedItemDrop.InvokeAsync(DragDropService.DragTargetItem);
if (!Equals(DragDropService.Items, Items) && CopyItem != null)
{
Swap(DragDropService.DragTargetItem, CopyItem(activeItem));
DragDropService.Reset();
}
else
{
Swap(DragDropService.DragTargetItem, activeItem);
DragDropService.Commit();
}
}
StateHasChanged();
OnItemDrop.InvokeAsync(activeItem);
}
private void Swap(TItem draggedOverItem, TItem activeItem)
{
var indexDraggedOverItem = Items.IndexOf(draggedOverItem);
//insert into new zone
Items.Insert(indexDraggedOverItem + 1, activeItem);
}
/// <summary>
/// OnInitialized 方法
/// </summary>
protected override void OnInitialized()
{
base.OnInitialized();
DragDropService.StateHasChanged += ForceRender;
}
private void ForceRender(object? sender, EventArgs e)
{
StateHasChanged();
}
/// <summary>
/// Dispose 方法
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
DragDropService.StateHasChanged -= ForceRender;
}
}
/// <summary>
/// Dispose 方法
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}

View File

@ -48,6 +48,7 @@ public static class BootstrapBlazorServiceCollectionExtensions
services.TryAddScoped<DownloadService>();
services.TryAddScoped<WebClientService>();
services.TryAddScoped<AjaxService>();
services.TryAddScoped(typeof(DragDropService<>));
services.TryAddSingleton<IConfigureOptions<BootstrapBlazorOptions>, ConfigureOptions<BootstrapBlazorOptions>>();
services.ConfigureBootstrapBlazorOption(configureOptions);
@ -58,7 +59,7 @@ public static class BootstrapBlazorServiceCollectionExtensions
}
/// <summary>
///
///
/// </summary>
/// <param name="services"></param>
/// <param name="locatorAction"></param>
@ -70,7 +71,7 @@ public static class BootstrapBlazorServiceCollectionExtensions
}
/// <summary>
///
///
/// </summary>
/// <param name="services"></param>
/// <param name="options"></param>
@ -93,7 +94,7 @@ public static class BootstrapBlazorServiceCollectionExtensions
}
/// <summary>
///
///
/// </summary>
/// <param name="services"></param>
/// <param name="localizationAction"></param>

File diff suppressed because one or more lines are too long