Drag between lists (#1)
* * first approach with some issues * modified service name * added better handle of listeners * addded new page * * added css for empty list * added posiblity to drag to an empty list * corrected bug when blazor reload elements between a query to DOM * corrected bug handling elementreferences * corrected bug ondrop ghost still appears * * corrected styles * better example with three lists Co-authored-by: Santiago Cattaneo <santiago@rd-its.com>
This commit is contained in:
parent
006c3eb044
commit
55f8ab5329
10 changed files with 208 additions and 77 deletions
|
@ -5,7 +5,7 @@ VisualStudioVersion = 17.2.32314.265
|
|||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorReorderExample", "BlazorReorderExample\BlazorReorderExample.csproj", "{43EF61CE-4621-48DB-8648-AA686391D140}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorReorderList", "BlazorReorderList\BlazorReorderList.csproj", "{3DCD1165-CC1D-4015-A6D4-CE97E8A4A72B}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorReorderList", "BlazorReorderList\BlazorReorderList.csproj", "{3DCD1165-CC1D-4015-A6D4-CE97E8A4A72B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
|
62
BlazorReorderExample/Pages/BetweenLists.razor
Normal file
62
BlazorReorderExample/Pages/BetweenLists.razor
Normal file
|
@ -0,0 +1,62 @@
|
|||
@page "/BetweenLists"
|
||||
|
||||
<PageTitle>Blazor Reorder Example</PageTitle>
|
||||
|
||||
<h1>Drag between lists</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<div class="card">
|
||||
<Reorder Items="list1" TItem="ListItem">
|
||||
<div class="mb-2 mx-2">
|
||||
<h5>@context.title</h5>
|
||||
<p>@context.details <a href="@context.url">Go</a></p>
|
||||
</div>
|
||||
</Reorder>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="card">
|
||||
<Reorder Items="list2" TItem="ListItem">
|
||||
<div class="mb-2 mx-2">
|
||||
<h5>@context.title</h5>
|
||||
<p>@context.details <a href="@context.url">Go</a></p>
|
||||
</div>
|
||||
</Reorder>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="card">
|
||||
<Reorder Items="list3" TItem="ListItem">
|
||||
<div class="mb-2 mx-2">
|
||||
<h5>@context.title</h5>
|
||||
<p>@context.details <a href="@context.url">Go</a></p>
|
||||
</div>
|
||||
</Reorder>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
public List<ListItem> list1 = new()
|
||||
{
|
||||
new ListItem("Google", "https://google.com", "Again looking for a bug ..."),
|
||||
new ListItem("StackOverflow", "https://stackoverflow.com", "Could be this the solution?"),
|
||||
new ListItem("GitHub", "https://github.com", "Let's get awesome code"),
|
||||
new ListItem("Twitter", "https://twitter.com", "I want to rest"),
|
||||
new ListItem("Another", "https://another.com", "The solution must be somewhere!!!")
|
||||
};
|
||||
|
||||
public List<ListItem> list2 = new()
|
||||
{
|
||||
new ListItem("Facebook", "https://facebook.com", "Meta"),
|
||||
new ListItem("Instagram", "https://instagram.com", "Meta"),
|
||||
new ListItem("Whatsapp", "https://web.whatsapp.com", "Meta"),
|
||||
};
|
||||
|
||||
public List<ListItem> list3 = new()
|
||||
{
|
||||
new ListItem("Oculus", "https://oculus.com", "Meta"),
|
||||
};
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<PageTitle>Blazor Reorder Example</PageTitle>
|
||||
|
||||
<h1>Hello, sorted world!</h1>
|
||||
<h1>Custom styles</h1>
|
||||
|
||||
Welcome to your new reorderer list.
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
}
|
||||
|
||||
::deep .dragging {
|
||||
background: #ffd800;
|
||||
box-shadow: 0 4px 20px #ffd800AA;
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
|
|
|
@ -7,7 +7,7 @@ builder.RootComponents.Add<App>("#app");
|
|||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
builder.Services.AddTransient<BlazorReorderList.ReorderJsInterop<ListItem>>();
|
||||
builder.Services.AddScoped<BlazorReorderList.ReorderService<ListItem>>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
|
|
|
@ -19,6 +19,11 @@
|
|||
<span class="oi oi-plus" aria-hidden="true"></span> Styled
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="BetweenLists">
|
||||
<span class="oi oi-plus" aria-hidden="true"></span> BetweenLists
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,27 +1,30 @@
|
|||
@typeparam TItem
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.JSInterop
|
||||
@inject ReorderJsInterop<TItem> js
|
||||
@inject ReorderService<TItem> rs
|
||||
@inject NavigationManager nav
|
||||
@implements IDisposable
|
||||
|
||||
@if (debug)
|
||||
@if (Debug)
|
||||
{
|
||||
<p>@log</p>
|
||||
<p>@log2</p>
|
||||
}
|
||||
|
||||
@if(ghost != null)
|
||||
@if(rs.isDragging && elemPosition.x != -1000)
|
||||
{
|
||||
<div class="dragging item"
|
||||
style="width: @(elemWidth)px; left: @(elemPosition.x)px; top: @(elemPosition.y)px; transform: translateX(@(ghostTrans.x)px) translateY(@(ghostTrans.y)px )">
|
||||
@ChildContent(ghost)
|
||||
@ChildContent(rs.selected)
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (itemElem != null)
|
||||
{
|
||||
<div @ref="container" class="sortable">
|
||||
<div class="sortable" @ref="sortable">
|
||||
@foreach (var item in Items.Select((v, i) => (v, i)))
|
||||
{
|
||||
<div @ref="itemElem[item.i]" class="item @(item.v.Equals(ghost) ? "active" : "")"
|
||||
<div @ref="itemElem[item.i]" class="item @(item.v.Equals(rs.selected) ? "active" : "")"
|
||||
@onmousedown="async (e) => await onPress(e, item.v, item.i)">
|
||||
@ChildContent(item.v)
|
||||
</div>
|
||||
|
@ -33,30 +36,40 @@
|
|||
{
|
||||
[Parameter, EditorRequired] public RenderFragment<TItem> ChildContent { get; set; } = null!;
|
||||
[Parameter, EditorRequired] public List<TItem> Items { get; set; } = null!;
|
||||
[Parameter] public bool withShadow { get; set; } = true;
|
||||
[Parameter] public bool debug { get; set; } = false;
|
||||
[Parameter] public bool Copy { get; set; } = false;
|
||||
[Parameter] public bool WithShadow { get; set; } = true;
|
||||
[Parameter] public bool Debug { get; set; } = false;
|
||||
|
||||
private bool shouldRender = true; // cancel re-rendering
|
||||
private DotNetObjectReference<Reorder<TItem>>? dotNetHelper; //js-interop 2-ways
|
||||
ElementReference container;
|
||||
ElementReference[]? itemElem;
|
||||
ElementReference ghostElem;
|
||||
TItem? ghost;
|
||||
point elemPosition = new point(0, 0);
|
||||
point elemClickPosition = new point(0, 0);
|
||||
Dictionary<int, ElementReference> itemElem = new();
|
||||
ElementReference? sortable;
|
||||
point elemPosition = new point(-1000, 0);
|
||||
point ghostTrans = new point(0, 0);
|
||||
point clickPosition = new point(0, 0);
|
||||
int elemWidth = 0;
|
||||
int elemIndex = -1;
|
||||
int newElemIndex = -1;
|
||||
|
||||
string log = "", log2 = "";
|
||||
|
||||
protected override void OnParametersSet()
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (Items == null) return;
|
||||
nav.LocationChanged += HandleLocationChanged;
|
||||
}
|
||||
|
||||
var count = Items.Count;
|
||||
itemElem = new ElementReference[count];
|
||||
public void Dispose()
|
||||
{
|
||||
nav.LocationChanged -= HandleLocationChanged;
|
||||
}
|
||||
|
||||
public void HandleLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
if (dotNetHelper == null) return;
|
||||
base.InvokeAsync(async () =>
|
||||
{
|
||||
await rs.removeEvents(dotNetHelper);
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
|
@ -64,7 +77,7 @@
|
|||
if (firstRender)
|
||||
{
|
||||
dotNetHelper = DotNetObjectReference.Create(this);
|
||||
await js.initEvents(dotNetHelper);
|
||||
await rs.initEvents(dotNetHelper);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,77 +85,69 @@
|
|||
{
|
||||
if (itemElem == null) return;
|
||||
shouldRender = false; // Because the method triggers re-render, the click propagation is canceled, if you have a link/or any click event inside it's going to stop working
|
||||
ghost = item;
|
||||
ghostElem = itemElem[index];
|
||||
elemIndex = index;
|
||||
elemWidth = await js.getWidth(ghostElem);
|
||||
elemPosition = await js.getPosition(ghostElem);
|
||||
elemClickPosition = await js.getPoint(m);
|
||||
var ghostElem = itemElem[index];
|
||||
elemWidth = await rs.getWidth(ghostElem);
|
||||
elemPosition = await rs.getPosition(ghostElem);
|
||||
clickPosition = await rs.getPoint(m);
|
||||
rs.Set(Items, item, index, clickPosition);
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public void onRelease(MouseEventArgs m)
|
||||
{
|
||||
if (Items == null) return;
|
||||
if (ghost != null)
|
||||
elemPosition = new point(-1000, 0);
|
||||
if (rs.isDragging && rs.originItems == Items)
|
||||
{
|
||||
ghost = default(TItem);
|
||||
ghostElem = default;
|
||||
ghostTrans = new point(0, 0);
|
||||
elemPosition = new point(0, 0);
|
||||
elemClickPosition = new point(0, 0);
|
||||
elemWidth = 0;
|
||||
rs.Reset();
|
||||
|
||||
// flip
|
||||
if (!withShadow)
|
||||
if (!WithShadow)
|
||||
{
|
||||
var item = Items[elemIndex];
|
||||
var item = Items[rs.elemIndex];
|
||||
shouldRender = false;
|
||||
Items.RemoveAt(elemIndex);
|
||||
Items.RemoveAt(rs.elemIndex);
|
||||
Items.Insert(newElemIndex, item);
|
||||
Log("mouseup");
|
||||
}
|
||||
shouldRender = true;
|
||||
Log("mouseup" + ghost);
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public async Task onMove(point pos)
|
||||
{
|
||||
if (Items == null || itemElem == null) return;
|
||||
if (ghost != null)
|
||||
if (itemElem == null) return;
|
||||
if (rs.isDragging)
|
||||
{
|
||||
ghostTrans = new point(pos.x - elemClickPosition.x, pos.y - elemClickPosition.y);
|
||||
Log($"onMove ({ghostTrans.x}, {ghostTrans.y}) {ghost}");
|
||||
shouldRender = false;
|
||||
ghostTrans = new point(pos.x - rs.elemClickPosition.x, pos.y - rs.elemClickPosition.y);
|
||||
Log($"onMove ({elemPosition.x}, {elemPosition.y})");
|
||||
|
||||
// check if current drag item is over another item and swap places
|
||||
for(var b = 0; b < itemElem.Length; ++b)
|
||||
for (var b = 0; b < itemElem.Count; ++b)
|
||||
{
|
||||
if (b == elemIndex) continue;
|
||||
var subItem = itemElem[b];
|
||||
if (rs.originItems == Items && b == rs.elemIndex) continue;
|
||||
|
||||
if (await isOnTop(subItem, pos))
|
||||
// If is on top of an element or if top top of a empty list -> confirm drag
|
||||
if (await isOnTop(subItem, pos) ||
|
||||
Items.Count == 0 && await isOnTop((ElementReference) sortable, pos))
|
||||
{
|
||||
// reorder
|
||||
newElemIndex = b;
|
||||
var item = Items[elemIndex];
|
||||
var item2 = Items[newElemIndex];
|
||||
// flip
|
||||
if (withShadow)
|
||||
if (WithShadow)
|
||||
{
|
||||
shouldRender = false;
|
||||
Items.RemoveAt(elemIndex);
|
||||
Items.Insert(newElemIndex, item);
|
||||
elemIndex = newElemIndex;
|
||||
shouldRender = true;
|
||||
}
|
||||
|
||||
Log($"Sobre {item2}");
|
||||
rs.originItems.RemoveAt(rs.elemIndex);
|
||||
Items.Insert(newElemIndex, rs.selected);
|
||||
rs.elemIndex = newElemIndex;
|
||||
if (rs.originItems != Items) rs.originItems = Items;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shouldRender = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
@ -155,7 +160,8 @@
|
|||
// checks if mouse x/y is on top of an item
|
||||
async Task<bool> isOnTop(ElementReference item, point pos)
|
||||
{
|
||||
var box = await js.getClientRect(item);
|
||||
var box = await rs.getClientRect(item);
|
||||
if (box.width < 0) return false;
|
||||
Log($"\npos x: {pos.x}, y: {pos.y}\n item: left:{box.left}, width:{box.width}\nitem: top:{box.top}, height:{box.height}", true);
|
||||
var isx = (pos.x > box.left && pos.x < (box.left + box.width));
|
||||
var isy = (pos.y > box.top && pos.y < (box.top + box.height));
|
||||
|
@ -166,11 +172,10 @@
|
|||
|
||||
void Log(string info, bool sublog = false)
|
||||
{
|
||||
if (debug)
|
||||
if (Debug)
|
||||
{
|
||||
if (sublog) log2 = info;
|
||||
else log = info;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.sortable {
|
||||
min-height: 10px;
|
||||
}
|
||||
|
||||
.item {
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
|
@ -12,6 +16,7 @@
|
|||
}
|
||||
|
||||
.dragging {
|
||||
background: #e0ffff;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
|
|
|
@ -4,22 +4,50 @@ using Microsoft.JSInterop;
|
|||
|
||||
namespace BlazorReorderList;
|
||||
|
||||
public class ReorderJsInterop<TItem> : IAsyncDisposable
|
||||
public class ReorderService<TItem> : IAsyncDisposable
|
||||
{
|
||||
private readonly Lazy<Task<IJSObjectReference>> moduleTask;
|
||||
public List<TItem>? originItems;
|
||||
public int elemIndex = -1;
|
||||
public TItem selected = default(TItem);
|
||||
public point elemClickPosition = new point(0, 0);
|
||||
public bool isDragging = false;
|
||||
|
||||
public ReorderJsInterop(IJSRuntime jsRuntime)
|
||||
public ReorderService(IJSRuntime jsRuntime)
|
||||
{
|
||||
moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>(
|
||||
"import", "./_content/BlazorReorderList/ReorderJsInterop.js").AsTask());
|
||||
}
|
||||
|
||||
public void Set(List<TItem> list, TItem item, int index, point clickPoint)
|
||||
{
|
||||
isDragging = true;
|
||||
originItems = list;
|
||||
selected = item;
|
||||
elemIndex = index;
|
||||
elemClickPosition = clickPoint;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
isDragging = false;
|
||||
originItems = default(List<TItem>);
|
||||
selected = default(TItem);
|
||||
elemClickPosition = new point(0, 0);
|
||||
}
|
||||
|
||||
public async ValueTask initEvents(DotNetObjectReference<Reorder<TItem>> dotNetInstance)
|
||||
{
|
||||
var module = await moduleTask.Value;
|
||||
await module.InvokeVoidAsync("initEvents", dotNetInstance);
|
||||
}
|
||||
|
||||
public async ValueTask removeEvents(DotNetObjectReference<Reorder<TItem>> dotNetInstance)
|
||||
{
|
||||
var module = await moduleTask.Value;
|
||||
await module.InvokeVoidAsync("removeEvents", dotNetInstance);
|
||||
}
|
||||
|
||||
public async ValueTask<int> getWidth(ElementReference el)
|
||||
{
|
||||
var module = await moduleTask.Value;
|
|
@ -2,6 +2,7 @@
|
|||
var _w = window,
|
||||
_b = document.body,
|
||||
_d = document.documentElement;
|
||||
var dotNetInstance = [];
|
||||
|
||||
// get position of mouse/touch in relation to viewport
|
||||
export function getPoint(e) {
|
||||
|
@ -13,11 +14,33 @@ export function getPoint(e) {
|
|||
return { x: pointX, y: pointY };
|
||||
}
|
||||
|
||||
export function initEvents(dotNetInstance) {
|
||||
window.addEventListener("mousemove", (e) => dotNetInstance.invokeMethodAsync("onMove", getPoint(e)), true);
|
||||
window.addEventListener("touchmove", (e) => dotNetInstance.invokeMethodAsync("onMove", getPoint(e)), true);
|
||||
window.addEventListener("mouseup", (e) => dotNetInstance.invokeMethodAsync("onRelease", e), true);
|
||||
window.addEventListener("touchend", (e) => dotNetInstance.invokeMethodAsync("onRelease", e), true);
|
||||
// init events for each list (if the event exists it's ignored)
|
||||
export function initEvents(dotNet) {
|
||||
dotNetInstance.push(dotNet);
|
||||
window.addEventListener("mousemove", onMove);
|
||||
window.addEventListener("touchmove", onMove);
|
||||
window.addEventListener("mouseup", onRelease);
|
||||
window.addEventListener("touchend", onRelease);
|
||||
}
|
||||
|
||||
// only remove from the collection
|
||||
export function removeEvents(dotNet) {
|
||||
dotNetInstance = dotNetInstance.filter(x => x._id !== dotNet._id);
|
||||
}
|
||||
|
||||
// only invoke events form the collection
|
||||
function onMove(e) {
|
||||
var point = getPoint(e);
|
||||
for (var i = 0; i < dotNetInstance.length; i++) {
|
||||
dotNetInstance[i].invokeMethodAsync("onMove", point);
|
||||
}
|
||||
}
|
||||
|
||||
// only invoke events form the collection
|
||||
export function onRelease(e) {
|
||||
for (var i = 0; i < dotNetInstance.length; i++) {
|
||||
dotNetInstance[i].invokeMethodAsync("onRelease", e);
|
||||
}
|
||||
}
|
||||
|
||||
export function getWidth(e) {
|
||||
|
@ -27,5 +50,7 @@ export function getPosition(e) {
|
|||
return { x: e.offsetLeft, y: e.offsetTop };
|
||||
}
|
||||
export function getClientRect(e) {
|
||||
// blazor reset elements id in the middle of the query, so this invalidate the query and prevents errors
|
||||
if (e === null || e === undefined) return { width: -1 };
|
||||
return e.getBoundingClientRect();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue