@snap-engine/snapsort
v0.4.2
Published
Core TypeScript logic for drag-and-drop system
Readme
@snap-engine/snapsort
Core TypeScript logic for SnapEngine drag-and-drop interactions.
A single Container/Item class pair; each container picks its drag
behavior with config.mode: "euclidean" (default), "progressive",
"insertion", or "swap".
Install
npm install @snap-engine/snapsort @snap-engine/coreIncludes
ContainerItemDragSession- Event types:
ItemInsertEvent,ItemRemoveEvent,ItemMoveEvent,ItemSwapEvent,GhostCreateEvent,GhostInsertEvent,GhostRemoveEvent,DragStartEvent,DragEndEvent,DropTargetChangeEvent,CanDropEvent,DragLocation ContainerCallbacks,ContainerConfig,SortMode,SortStrategy
Usage
import { Container, Item } from "@snap-engine/snapsort";
const container = new Container(engine, parent, { mode: "insertion" });DOM ownership contract
SnapSort core is designed to work identically across Vanilla JS, Svelte, React, and any future framework adapter. That only works if core and the framework never fight over the same DOM nodes. The contract:
- The framework owns the DOM tree. Core never inserts, removes, or
reparents an element the framework rendered. All structural intent flows
through
ContainerCallbacks(onItemMove,onItemInsert,onItemRemove,onGhostInsert,onGhostRemove), which carry everything an adapter needs (index,beforeElement,ghostRect, metadata). The default callback implementations inmutation.tsare the "vanilla JS adapter" — legitimate defaults when no framework owns the DOM, not a parallel mutation path. - Core writes only non-structural properties on the dragged element —
transform,position/z-indexstyles. Every framework tolerates that; none diffs inline styles it didn't set. - Framework mutations commit synchronously. Core runs structural
callbacks inside
flushMutation(mutation). React and Svelte adapters provide this transaction automatically, committing state and DOM before core performs its final geometry read and writes the inverse FLIP transform. - After
flushMutation, the framework's DOM is the truth. Core resolves elements lazily from item identity (findItemByKey), revalidates them withisConnected, and verifies placement. It never silently repairs adapter output, since that would mask an integration bug.
awaitMutation remains deprecated for compatibility. Promise-returning
mutation waits are not paint-atomic and are not supported by the FLIP path.
A concrete consequence: the dragged element stays in its original DOM
parent for the entire drag. It never gets reparented into whichever
container is currently hovered — it's moved only by transform, computed
against a coordinate parent frozen at drag start. This is what lets the React
adapter stay thin (no DOM snapshot/restore bookkeeping): React's reconciler
never observes a node outside the parent it rendered it under.
Accepted trade-offs
- Clipping/stacking: an
overflow: hiddenancestor can clip the dragged element, and a FLIP-transformed ancestor becomes a stacking context, so the dragged item (z-index: 1000) can paint under a later sibling while that ancestor is mid-animation. Avoidoverflow: hiddenon draggable surfaces if this matters for your layout. - Original parent unmounted mid-drag: if the framework unmounts the
dragged item's parent while a drag is in flight, the dragged element goes
with it (the framework owns that subtree).
dragEndstill commits state viaonItemMove/onItemInsert, so application state recovers even though the visual drag ends abruptly. - Inner scroll containers: core does not track scroll offsets. Dragging within a container that itself scrolls mid-drag is unhandled today (this predates the ownership contract and is a separate, open limitation).
