@oomfware/tug
v0.2.1
Published
yet another drag and drop library
Readme
@oomfware/tug
yet another drag and drop library.
pnpm add @oomfware/tugtug wraps the HTML Drag and Drop API, providing a simple, channel-based programming model with
full end-to-end type safety.
usage
basic setup
create an isolated channel to coordinate drag-and-drop operations.
the first type parameter is the data your draggables carry (source.data); the optional second is
the data your drop targets attach (self.data, and every event's location). both flow end-to-end.
import { createDnd } from '@oomfware/tug';
type Card = { id: string; column: string };
type Column = { columnId: string };
const card = createDnd<Card, Column>();
// make an element draggable
const cleanupDraggable = card.draggable({
element: cardEl,
getInitialData: () => ({ id: 'c1', column: 'todo' }),
});
// register a drop target
const cleanupDropTarget = card.dropTarget({
element: columnEl,
getData: () => ({ columnId: 'todo' }),
canDrop: ({ source }) => source.data.column !== 'done',
onDragEnter: ({ self }) => self.element.classList.add('is-over'),
onDragLeave: ({ self }) => self.element.classList.remove('is-over'),
onDrop: ({ source }) => moveCard(source.data.id),
});
// listen to drag activity across the entire channel
const cleanupMonitor = card.monitor({
onDragStart: ({ source }) => console.log('dragging', source.data.id),
// location.current.dropTargets is typed as Column data
onDrop: ({ location }) =>
console.log('dropped on', location.current.dropTargets[0]?.data.columnId),
});each draggable and drop target can also narrow to a variant of its channel type: a getInitialData
/ getData that returns a narrower shape surfaces that shape in its own handlers, while monitors
and location still see the channel-wide type.
drag lifecycle events
draggables, drop targets, and monitors can all hook into the drag lifecycle:
card.draggable({
element,
getInitialData() {
return { id: 'c1' };
},
onGenerateDragPreview({ nativeSetDragImage }) {
// customize or disable the native drag image here
},
onDragStart: ({ source }) => {},
onDrag: ({ source, input }) => {},
onDrop: ({ source, location, reason }) => {
// reason is 'dropped' or 'cancelled'
},
});drop target behavior
drop targets can dynamically shape behavior and attach data during a drag.
card.dropTarget({
element,
canDrop: ({ source }) => true, // filter drops dynamically
getData: ({ element, input }) => ({}), // attach data, recomputed on move
getDropEffect: () => 'move', // 'move' | 'copy' | 'link'
getIsSticky: () => false, // keep active briefly after pointer leaves
onDragEnter: ({ self, source }) => {},
onDragLeave: ({ self, source }) => {},
});addons
hitbox (closest edge detection)
calculate which edge of a drop target is closest to the pointer, useful for list/grid sorting.
import { attachClosestEdge, extractClosestEdge } from '@oomfware/tug/hitbox';
card.dropTarget({
element,
getData({ element, input }) {
return attachClosestEdge({}, { element, input, allowedEdges: ['top', 'bottom'] });
},
onDrag({ self }) {
const edge = extractClosestEdge(self.data); // 'top' | 'bottom' | null
showIndicator(edge);
},
});reorder (list reordering)
helpers to calculate index transitions and reorder list arrays.
import { getReorderDestinationIndex, reorder } from '@oomfware/tug/reorder';
const to = getReorderDestinationIndex({
axis: 'vertical',
closestEdgeOfTarget: extractClosestEdge(self.data),
indexOfTarget,
startIndex,
});
const next = reorder(items, startIndex, to);preview (custom drag previews)
render custom DOM elements as the native drag image, or disable the native preview entirely.
import {
centerUnderPointer,
disableNativeDragPreview,
pointerOutsideOfPreview,
preserveOffsetOnSource,
setCustomNativeDragPreview,
} from '@oomfware/tug/preview';
// disable native preview to render custom pointer-following element
card.draggable({
element,
onGenerateDragPreview({ nativeSetDragImage }) {
disableNativeDragPreview({ nativeSetDragImage });
},
});
// snapshot a custom element as the drag image
card.draggable({
element,
onGenerateDragPreview({ nativeSetDragImage }) {
setCustomNativeDragPreview({
nativeSetDragImage,
getOffset: pointerOutsideOfPreview({ x: '8px', y: '8px' }),
// options: centerUnderPointer, pointerOutsideOfPreview, preserveOffsetOnSource
render({ container }) {
container.append(createPreviewNode());
},
});
},
});