Skip to content

Ignore a fallback drag whose element was removed from the DOM - #2483

Open
Kebechet wants to merge 1 commit into
SortableJS:masterfrom
Kebechet:fix-removed-drag-element
Open

Kebechet wants to merge 1 commit into
SortableJS:masterfrom
Kebechet:fix-removed-drag-element

Conversation

@Kebechet

@Kebechet Kebechet commented Oct 28, 2026 •

Copy link
Copy Markdown

Addresses the error in #2307. It does not add the cancel API that issue also asks for.

The problem

Frameworks own the DOM of a list, so they can remove the element that is being dragged: a re-render after the data changed, an item deleted by another user, and so on. During a fallback drag (any touch drag, or forceFallback: true) Sortable then breaks in two ways:

  1. _emulateDragOver runs every 50 ms and reads dragEl.parentNode[expando]. With dragEl detached, it throws TypeError: Cannot read properties of null (reading 'Sortable...') on every tick until the pointer is released.
  2. nearestEmptyInsertDetectEvent still runs on touchmove/mousemove. When the pointer gets near an empty list, it calls _onDragOver, which appends the removed element to that list. On release, Sortable fires onAdd/onRemove for an element the app has already deleted, and the element stays in the DOM untracked by the framework.

We hit this in a Blazor app on Android. When an item is dropped into another list, the Blazor wrapper puts the DOM back the way it was and tells .NET, and .NET re-renders a moment later. A user who grabs the item again inside that window loses it mid-drag.

Reproduction

tests/empty-list.html with forceFallback: true, plus a timer that removes the chosen item:

new Sortable(document.getElementById('list1'), { group: 'shared', forceFallback: true });
new Sortable(document.getElementById('list2'), { group: 'shared', forceFallback: true });

setTimeout(() => document.querySelector('.sortable-chosen').remove(), 3000);

Start dragging an item from list1 and keep holding it. After 3 seconds the console starts logging the TypeError. Now move over the empty list2 and the removed item appears in it. Release, and onAdd fires for it.

The change

Both entry points now skip the drag once dragEl has no parent. Nothing is left to sort, and the drop that follows ends the drag the way it does today: index(dragEl) is -1, so no add/remove/update events fire and _nulling() resets the state. The native HTML5 drag path is unchanged.

Test

I added a test in a new Dragged element removed fixture on empty-list.html. It starts a touch-type pointer drag, removes the item, then moves over the empty list and releases. It asserts that there are no errors and that the item was not inserted back.

  • On master it fails with the TypeError from _emulateDragOver.
  • With only the _emulateDragOver guard it fails on the re-insertion.
  • With both guards it passes. All 19 tests pass with npm run build:umd && npm test (headless Chrome). The build output is not committed, as CONTRIBUTING asks.

When the app removes the dragged element while a fallback (touch or
forceFallback) drag is in progress, _emulateDragOver throws
"Cannot read properties of null (reading 'Sortable...')" every 50 ms
until the pointer is released, and nearestEmptyInsertDetectEvent puts
the removed element back into a nearby empty list, so the drop reports
an add/remove for an element the app no longer has.

Both now skip the drag once dragEl has no parent. The drop that follows
ends it as before.

SortableJS#2307

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant