diff --git a/BlazorReorder.sln b/BlazorReorder.sln index e620a90..1b60571 100644 --- a/BlazorReorder.sln +++ b/BlazorReorder.sln @@ -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 diff --git a/BlazorReorderExample/Pages/BetweenLists.razor b/BlazorReorderExample/Pages/BetweenLists.razor new file mode 100644 index 0000000..3596788 --- /dev/null +++ b/BlazorReorderExample/Pages/BetweenLists.razor @@ -0,0 +1,62 @@ +@page "/BetweenLists" + +Blazor Reorder Example + +

Drag between lists

+ +
+
+
+ +
+
@context.title
+

@context.details Go

+
+
+
+
+
+
+ +
+
@context.title
+

@context.details Go

+
+
+
+
+
+
+ +
+
@context.title
+

@context.details Go

+
+
+
+
+
+ +@code { + + public List 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 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 list3 = new() + { + new ListItem("Oculus", "https://oculus.com", "Meta"), + }; +} diff --git a/BlazorReorderExample/Pages/Styled.razor b/BlazorReorderExample/Pages/Styled.razor index 376718b..86d3d70 100644 --- a/BlazorReorderExample/Pages/Styled.razor +++ b/BlazorReorderExample/Pages/Styled.razor @@ -2,7 +2,7 @@ Blazor Reorder Example -

Hello, sorted world!

+

Custom styles

Welcome to your new reorderer list. diff --git a/BlazorReorderExample/Pages/Styled.razor.css b/BlazorReorderExample/Pages/Styled.razor.css index ed31007..628b56c 100644 --- a/BlazorReorderExample/Pages/Styled.razor.css +++ b/BlazorReorderExample/Pages/Styled.razor.css @@ -12,6 +12,7 @@ } ::deep .dragging { + background: #ffd800; box-shadow: 0 4px 20px #ffd800AA; opacity: 0.8; position: absolute; diff --git a/BlazorReorderExample/Program.cs b/BlazorReorderExample/Program.cs index 4c8572b..bff5390 100644 --- a/BlazorReorderExample/Program.cs +++ b/BlazorReorderExample/Program.cs @@ -7,7 +7,7 @@ builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); -builder.Services.AddTransient>(); +builder.Services.AddScoped>(); await builder.Build().RunAsync(); diff --git a/BlazorReorderExample/Shared/NavMenu.razor b/BlazorReorderExample/Shared/NavMenu.razor index 842181f..f3c4b0c 100644 --- a/BlazorReorderExample/Shared/NavMenu.razor +++ b/BlazorReorderExample/Shared/NavMenu.razor @@ -19,6 +19,11 @@ Styled + diff --git a/BlazorReorderList/Reorder.razor b/BlazorReorderList/Reorder.razor index fd668f0..8c1edb3 100644 --- a/BlazorReorderList/Reorder.razor +++ b/BlazorReorderList/Reorder.razor @@ -1,27 +1,30 @@ @typeparam TItem +@using Microsoft.AspNetCore.Components.Routing @using Microsoft.JSInterop -@inject ReorderJsInterop js +@inject ReorderService rs +@inject NavigationManager nav +@implements IDisposable -@if (debug) +@if (Debug) {

@log

@log2

} -@if(ghost != null) +@if(rs.isDragging && elemPosition.x != -1000) {
- @ChildContent(ghost) + @ChildContent(rs.selected)
} @if (itemElem != null) { -
+
@foreach (var item in Items.Select((v, i) => (v, i))) { -
@ChildContent(item.v)
@@ -33,30 +36,40 @@ { [Parameter, EditorRequired] public RenderFragment ChildContent { get; set; } = null!; [Parameter, EditorRequired] public List 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>? 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 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); - shouldRender = true; - Log("mouseup" + ghost); + Log("mouseup"); } - StateHasChanged(); + shouldRender = true; } + 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; + rs.originItems.RemoveAt(rs.elemIndex); + Items.Insert(newElemIndex, rs.selected); + rs.elemIndex = newElemIndex; + if (rs.originItems != Items) rs.originItems = Items; + break; } - - Log($"Sobre {item2}"); - break; } } + shouldRender = true; StateHasChanged(); } } @@ -155,7 +160,8 @@ // checks if mouse x/y is on top of an item async Task 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; } } - } diff --git a/BlazorReorderList/Reorder.razor.css b/BlazorReorderList/Reorder.razor.css index 1085ee7..d7a962c 100644 --- a/BlazorReorderList/Reorder.razor.css +++ b/BlazorReorderList/Reorder.razor.css @@ -1,3 +1,7 @@ +.sortable { + min-height: 10px; +} + .item { cursor: move; user-select: none; @@ -12,10 +16,11 @@ } .dragging { - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); - opacity: 0.8; - position: absolute; - z-index: 999; + background: #e0ffff; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + opacity: 0.8; + position: absolute; + z-index: 999; } .dragging:hover { diff --git a/BlazorReorderList/ReorderJsInterop.cs b/BlazorReorderList/ReorderService.cs similarity index 58% rename from BlazorReorderList/ReorderJsInterop.cs rename to BlazorReorderList/ReorderService.cs index 5745535..c6d7ae0 100644 --- a/BlazorReorderList/ReorderJsInterop.cs +++ b/BlazorReorderList/ReorderService.cs @@ -4,14 +4,36 @@ using Microsoft.JSInterop; namespace BlazorReorderList; -public class ReorderJsInterop : IAsyncDisposable +public class ReorderService : IAsyncDisposable { private readonly Lazy> moduleTask; + public List? 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( - "import", "./_content/BlazorReorderList/ReorderJsInterop.js").AsTask()); + "import", "./_content/BlazorReorderList/ReorderJsInterop.js").AsTask()); + } + + public void Set(List 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); + selected = default(TItem); + elemClickPosition = new point(0, 0); } public async ValueTask initEvents(DotNetObjectReference> dotNetInstance) @@ -20,6 +42,12 @@ public class ReorderJsInterop : IAsyncDisposable await module.InvokeVoidAsync("initEvents", dotNetInstance); } + public async ValueTask removeEvents(DotNetObjectReference> dotNetInstance) + { + var module = await moduleTask.Value; + await module.InvokeVoidAsync("removeEvents", dotNetInstance); + } + public async ValueTask getWidth(ElementReference el) { var module = await moduleTask.Value; diff --git a/BlazorReorderList/wwwroot/ReorderJsInterop.js b/BlazorReorderList/wwwroot/ReorderJsInterop.js index 7a50de4..99fb633 100644 --- a/BlazorReorderList/wwwroot/ReorderJsInterop.js +++ b/BlazorReorderList/wwwroot/ReorderJsInterop.js @@ -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(); }