snap-dnd 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,318 @@
1
+ # Snap
2
+
3
+ A zero-dependency, memory-optimized drag and drop library for vanilla JavaScript and Web Components.
4
+
5
+ ## Features
6
+
7
+ - **Zero dependencies** - Pure vanilla JavaScript
8
+ - **Tiny footprint** - Core ~5KB gzipped, Full ~9KB gzipped
9
+ - **Memory efficient** - Object pooling, event delegation, WeakMap caches
10
+ - **Web Component ready** - Works with Shadow DOM and Lit Elements
11
+ - **Touch support** - Mouse, touch, and pointer events
12
+ - **Beginner friendly** - Works with just data attributes
13
+ - **Highly customizable** - Plugin system, behaviors, comprehensive options
14
+
15
+ ## Bundle Sizes
16
+
17
+ | Import | Minified | Gzipped |
18
+ |--------|----------|---------|
19
+ | `snap-dnd/core` | 17.6 KB | **~5 KB** |
20
+ | `snap-dnd` (full) | 35.3 KB | ~9 KB |
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install snap-dnd
26
+ ```
27
+
28
+ ### Core Only (Minimal ~5KB)
29
+
30
+ If you don't need plugins (Sortable, Kanban, FileDrop), import the core:
31
+
32
+ ```javascript
33
+ import { Snap } from 'snap-dnd/core';
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ### Declarative (Data Attributes)
39
+
40
+ The simplest way to use Snap - just add data attributes:
41
+
42
+ ```html
43
+ <div id="container">
44
+ <div data-draggable>Drag me!</div>
45
+ <div data-draggable>Drag me too!</div>
46
+ <div data-droppable>Drop here</div>
47
+ </div>
48
+
49
+ <script type="module">
50
+ import { Snap } from 'snap-dnd';
51
+
52
+ const snap = new Snap(document.getElementById('container'), {
53
+ onDrop: (e) => console.log('Dropped!', e.element, 'into', e.dropZone)
54
+ });
55
+ </script>
56
+ ```
57
+
58
+ ### Imperative (JavaScript)
59
+
60
+ For more control, use the imperative API:
61
+
62
+ ```javascript
63
+ import { Snap } from 'snap-dnd';
64
+
65
+ const snap = new Snap(container, {
66
+ onDragStart: (e) => console.log('Started dragging', e.element),
67
+ onDragMove: (e) => console.log('Moving', e.position),
68
+ onDrop: (e) => console.log('Dropped', e.element, 'at index', e.insertionIndex),
69
+ });
70
+
71
+ // Add elements programmatically
72
+ snap.addDraggable(myElement, {
73
+ data: { id: 1, type: 'task' },
74
+ axis: 'y' // Only vertical movement
75
+ });
76
+
77
+ snap.addDropZone(myZone, {
78
+ accepts: ['task'],
79
+ onEnter: () => myZone.classList.add('highlight')
80
+ });
81
+
82
+ // Cleanup when done
83
+ snap.destroy();
84
+ ```
85
+
86
+ ## Data Attributes
87
+
88
+ | Attribute | Description |
89
+ |-----------|-------------|
90
+ | `data-draggable` | Makes element draggable |
91
+ | `data-droppable` | Makes element a drop zone |
92
+ | `data-drag-handle` | Only this element can initiate drag |
93
+ | `data-drag-axis="x\|y"` | Constrain to horizontal or vertical |
94
+ | `data-drag-id="..."` | Custom ID passed to callbacks |
95
+ | `data-drag-type="..."` | Type for drop zone filtering |
96
+ | `data-accepts="a,b,c"` | Types this drop zone accepts |
97
+ | `data-file-drop` | Enable file drop zone |
98
+
99
+ ## Options
100
+
101
+ ```typescript
102
+ const snap = new Snap(container, {
103
+ // Selectors
104
+ draggableSelector: '[data-draggable]',
105
+ dropZoneSelector: '[data-droppable]',
106
+ handleSelector: '[data-drag-handle]',
107
+
108
+ // Behavior
109
+ axis: 'both', // 'x', 'y', or 'both'
110
+ grid: { x: 20, y: 20 }, // Snap to grid
111
+ delay: 0, // ms before drag starts
112
+ distance: 0, // px before drag starts
113
+
114
+ // Auto-scroll when near edges
115
+ autoScroll: true, // or { threshold: 40, maxSpeed: 15 }
116
+
117
+ // Callbacks
118
+ onDragStart: (e) => {},
119
+ onDragMove: (e) => {},
120
+ onDragEnd: (e) => {},
121
+ onDrop: (e) => {},
122
+ onDropZoneEnter: (e) => {},
123
+ onDropZoneLeave: (e) => {},
124
+
125
+ // Advanced
126
+ autoRefresh: false, // Auto-detect DOM changes
127
+ ghostClass: 'my-ghost',
128
+ });
129
+ ```
130
+
131
+ ## With Lit Elements
132
+
133
+ Snap works seamlessly with Web Components:
134
+
135
+ ```javascript
136
+ import { LitElement, html } from 'lit';
137
+ import { Snap } from 'snap-dnd';
138
+
139
+ class TaskBoard extends LitElement {
140
+ snap;
141
+
142
+ firstUpdated() {
143
+ this.snap = new Snap(this.shadowRoot, {
144
+ autoRefresh: true, // Handle Lit re-renders
145
+ onDrop: this.handleDrop.bind(this)
146
+ });
147
+ }
148
+
149
+ disconnectedCallback() {
150
+ super.disconnectedCallback();
151
+ this.snap?.destroy();
152
+ }
153
+
154
+ handleDrop(e) {
155
+ // Update your state, Lit will re-render
156
+ this.tasks = reorder(this.tasks, e.insertionIndex);
157
+ }
158
+
159
+ render() {
160
+ return html`
161
+ <ul>
162
+ ${this.tasks.map(task => html`
163
+ <li data-draggable data-drag-id=${task.id}>${task.title}</li>
164
+ `)}
165
+ </ul>
166
+ `;
167
+ }
168
+ }
169
+ ```
170
+
171
+ ## Plugins
172
+
173
+ ### Sortable
174
+
175
+ Reorder items within a container:
176
+
177
+ ```javascript
178
+ import { Snap, Sortable } from 'snap-dnd';
179
+
180
+ const snap = new Snap(container).use(new Sortable({
181
+ animation: 150,
182
+ ghostClass: 'sortable-ghost',
183
+ placeholderClass: 'sortable-placeholder'
184
+ }));
185
+ ```
186
+
187
+ ### Kanban
188
+
189
+ Move items between multiple containers:
190
+
191
+ ```javascript
192
+ import { Snap, Kanban } from 'snap-dnd';
193
+
194
+ const snap = new Snap(board).use(new Kanban({
195
+ containers: '.column',
196
+ items: '.card',
197
+ animation: 150
198
+ }));
199
+ ```
200
+
201
+ ### FileDrop
202
+
203
+ Handle file drops from desktop:
204
+
205
+ ```javascript
206
+ import { Snap, FileDrop } from 'snap-dnd';
207
+
208
+ const snap = new Snap(container).use(
209
+ new FileDrop({
210
+ accept: ['image/*', '.pdf'],
211
+ multiple: true,
212
+ maxSize: 10 * 1024 * 1024 // 10MB
213
+ }).onFileDrop((e) => {
214
+ console.log('Files dropped:', e.files);
215
+ })
216
+ );
217
+ ```
218
+
219
+ Or use the standalone helper:
220
+
221
+ ```javascript
222
+ import { createFileDropZone } from 'snap-dnd';
223
+
224
+ const cleanup = createFileDropZone(element, {
225
+ accept: ['image/*'],
226
+ onDrop: (files) => uploadFiles(files)
227
+ });
228
+
229
+ // Later: cleanup();
230
+ ```
231
+
232
+ ## Behaviors
233
+
234
+ Add optional behaviors for extra functionality:
235
+
236
+ ```javascript
237
+ import { Snap, AutoScroll, SnapGrid } from 'snap-dnd';
238
+
239
+ const snap = new Snap(container)
240
+ .addBehavior(new AutoScroll({ threshold: 50, maxSpeed: 20 }))
241
+ .addBehavior(new SnapGrid({ x: 10, y: 10 }));
242
+ ```
243
+
244
+ ## CSS
245
+
246
+ Snap doesn't inject any CSS. Add your own styles:
247
+
248
+ ```css
249
+ /* Required for touch devices */
250
+ [data-draggable] {
251
+ touch-action: none;
252
+ user-select: none;
253
+ cursor: grab;
254
+ }
255
+
256
+ /* Optional visual feedback */
257
+ .snap-dragging { opacity: 0.5; }
258
+ .snap-drop-active { background: rgba(0,120,255,0.1); }
259
+ .snap-ghost { box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
260
+ ```
261
+
262
+ See `examples/snap.css` for a complete reference stylesheet.
263
+
264
+ ## API Reference
265
+
266
+ ### Snap Instance
267
+
268
+ ```typescript
269
+ interface Snap {
270
+ enable(): void;
271
+ disable(): void;
272
+ destroy(): void;
273
+ refresh(): void;
274
+
275
+ addDraggable(element: HTMLElement, options?: ItemOptions): void;
276
+ removeDraggable(element: HTMLElement): void;
277
+ addDropZone(element: HTMLElement, options?: DropZoneOptions): void;
278
+ removeDropZone(element: HTMLElement): void;
279
+
280
+ isDragging(): boolean;
281
+ getActiveElement(): HTMLElement | null;
282
+
283
+ use(plugin: Plugin): this;
284
+ addBehavior(behavior: Behavior): this;
285
+ setOptions(options: Partial<SnapOptions>): void;
286
+ }
287
+ ```
288
+
289
+ ### Event Objects
290
+
291
+ ```typescript
292
+ interface DragStartEvent {
293
+ element: HTMLElement;
294
+ position: { x: number; y: number };
295
+ data: DataTransfer;
296
+ cancel(): void;
297
+ }
298
+
299
+ interface DropEvent {
300
+ element: HTMLElement;
301
+ dropZone: HTMLElement;
302
+ position: { x: number; y: number };
303
+ data: DataTransfer;
304
+ insertionIndex?: number;
305
+ sourceContainer?: HTMLElement;
306
+ }
307
+ ```
308
+
309
+ ## Browser Support
310
+
311
+ - Chrome 88+
312
+ - Firefox 85+
313
+ - Safari 14+
314
+ - Edge 88+
315
+
316
+ ## License
317
+
318
+ MIT
@@ -0,0 +1,20 @@
1
+ /**
2
+ * AutoScroll behavior - scrolls containers when dragging near edges
3
+ */
4
+ import type { Behavior, DragSession, AutoScrollOptions } from '../types/index.js';
5
+ export declare class AutoScroll implements Behavior {
6
+ name: string;
7
+ private _options;
8
+ private _scrollableAncestors;
9
+ private _rafId;
10
+ private _active;
11
+ constructor(options?: AutoScrollOptions);
12
+ onDragStart(session: DragSession): void;
13
+ onDragMove(session: DragSession): void;
14
+ onDragEnd(): void;
15
+ destroy(): void;
16
+ private _performScroll;
17
+ private _calculateScrollSpeed;
18
+ private _getSpeed;
19
+ private _findScrollableAncestors;
20
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * ConstraintAxis behavior - constrain movement to specific axis
3
+ * Note: Basic axis constraint is handled in DragEngine via options.axis
4
+ * This behavior adds additional constraint features like bounds
5
+ */
6
+ import type { Behavior, DragSession, Axis, Point } from '../types/index.js';
7
+ export interface ConstraintOptions {
8
+ axis?: Axis;
9
+ /** Bounding rectangle to constrain within */
10
+ bounds?: {
11
+ minX?: number;
12
+ maxX?: number;
13
+ minY?: number;
14
+ maxY?: number;
15
+ };
16
+ /** Constrain to parent element bounds */
17
+ containWithinParent?: boolean;
18
+ }
19
+ export declare class ConstraintAxis implements Behavior {
20
+ name: string;
21
+ private _options;
22
+ private _bounds;
23
+ constructor(options?: ConstraintOptions);
24
+ onDragStart(session: DragSession): void;
25
+ onDragMove(_session: DragSession): void;
26
+ onDragEnd(): void;
27
+ destroy(): void;
28
+ /**
29
+ * Apply constraints to a point
30
+ */
31
+ constrain(point: Point, origin: Point): Point;
32
+ /**
33
+ * Get current bounds
34
+ */
35
+ getBounds(): typeof this._bounds;
36
+ private _calculateParentBounds;
37
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * SnapGrid behavior - snaps drag position to a grid
3
+ * Note: This is handled in DragEngine via options.grid
4
+ * This behavior provides additional grid-related features
5
+ */
6
+ import type { Behavior, DragSession, GridOptions } from '../types/index.js';
7
+ export interface SnapGridOptions extends GridOptions {
8
+ /** Only snap when within threshold of grid line */
9
+ threshold?: number;
10
+ /** Show visual grid guides */
11
+ showGuides?: boolean;
12
+ }
13
+ export declare class SnapGrid implements Behavior {
14
+ name: string;
15
+ private _options;
16
+ private _guideContainer;
17
+ constructor(options: SnapGridOptions);
18
+ onDragStart(session: DragSession): void;
19
+ onDragMove(_session: DragSession): void;
20
+ onDragEnd(): void;
21
+ destroy(): void;
22
+ /**
23
+ * Snap a point to the grid
24
+ */
25
+ snap(x: number, y: number): {
26
+ x: number;
27
+ y: number;
28
+ };
29
+ /**
30
+ * Snap with threshold - only snap when close to grid line
31
+ */
32
+ snapWithThreshold(x: number, y: number, threshold?: number): {
33
+ x: number;
34
+ y: number;
35
+ };
36
+ private _createGuides;
37
+ private _removeGuides;
38
+ }
@@ -0,0 +1,3 @@
1
+ export { AutoScroll } from './AutoScroll.js';
2
+ export { SnapGrid, type SnapGridOptions } from './SnapGrid.js';
3
+ export { ConstraintAxis, type ConstraintOptions } from './ConstraintAxis.js';
@@ -0,0 +1,60 @@
1
+ /**
2
+ * DragEngine orchestrates the drag operation
3
+ * Coordinates between sensors, state, and drop zones
4
+ */
5
+ import type { SnapOptions, Axis } from '../types/index.js';
6
+ import { DragState } from './DragState.js';
7
+ export interface DragEngineOptions {
8
+ container: HTMLElement | ShadowRoot;
9
+ state: DragState;
10
+ options: SnapOptions;
11
+ getDropZones: () => HTMLElement[];
12
+ getItemData: (element: HTMLElement) => Record<string, unknown> | undefined;
13
+ getItemAxis: (element: HTMLElement) => Axis | undefined;
14
+ }
15
+ export declare class DragEngine {
16
+ private _container;
17
+ private _state;
18
+ private _options;
19
+ private _getDropZones;
20
+ private _getItemData;
21
+ private _getItemAxis;
22
+ private _pointerSensor;
23
+ private _enabled;
24
+ private _ghost;
25
+ private _ghostOffset;
26
+ constructor(engineOptions: DragEngineOptions);
27
+ /**
28
+ * Enable drag engine
29
+ */
30
+ enable(): void;
31
+ /**
32
+ * Disable drag engine
33
+ */
34
+ disable(): void;
35
+ /**
36
+ * Check if enabled
37
+ */
38
+ get isEnabled(): boolean;
39
+ /**
40
+ * Update options
41
+ */
42
+ updateOptions(options: Partial<SnapOptions>): void;
43
+ private _setupListeners;
44
+ private _onPointerDown;
45
+ private _onPointerMove;
46
+ private _onPointerUp;
47
+ private _onPointerCancel;
48
+ private _applyAxisConstraint;
49
+ private _applyGridSnap;
50
+ private _updateDropZone;
51
+ private _createGhost;
52
+ private _updateGhost;
53
+ private _removeGhost;
54
+ private _cleanup;
55
+ private _extractDataAttributes;
56
+ /**
57
+ * Cleanup
58
+ */
59
+ destroy(): void;
60
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Centralized drag state management
3
+ * Single source of truth for the current drag session
4
+ */
5
+ import type { DragSession, Point, StateEvent, Unsubscribe } from '../types/index.js';
6
+ import { EventEmitter } from '../utils/EventEmitter.js';
7
+ interface StateEvents {
8
+ dragstart: DragSession;
9
+ dragmove: DragSession;
10
+ dragend: DragSession;
11
+ dropzoneenter: DragSession;
12
+ dropzoneleave: DragSession;
13
+ drop: DragSession;
14
+ }
15
+ export declare class DragState extends EventEmitter<StateEvents> {
16
+ private _session;
17
+ private _idCounter;
18
+ /**
19
+ * Get current drag session (null if not dragging)
20
+ */
21
+ get session(): DragSession | null;
22
+ /**
23
+ * Check if currently dragging
24
+ */
25
+ isDragging(): boolean;
26
+ /**
27
+ * Get the element being dragged
28
+ */
29
+ getActiveElement(): HTMLElement | null;
30
+ /**
31
+ * Get current drop zone
32
+ */
33
+ getCurrentDropZone(): HTMLElement | null;
34
+ /**
35
+ * Start a new drag session
36
+ */
37
+ startDrag(element: HTMLElement, origin: Point, initialData?: Record<string, unknown>): DragSession;
38
+ /**
39
+ * Update position during drag
40
+ */
41
+ updatePosition(point: Point): void;
42
+ /**
43
+ * Set or clear drop zone target
44
+ */
45
+ setDropTarget(zone: HTMLElement | null): void;
46
+ /**
47
+ * Complete the drag with a drop
48
+ */
49
+ endDrag(): DragSession | null;
50
+ /**
51
+ * Cancel the current drag
52
+ */
53
+ cancelDrag(): DragSession | null;
54
+ /**
55
+ * Subscribe to state changes
56
+ */
57
+ subscribe(event: StateEvent, callback: (session: DragSession) => void): Unsubscribe;
58
+ /**
59
+ * Reset state
60
+ */
61
+ reset(): void;
62
+ /**
63
+ * Cleanup
64
+ */
65
+ destroy(): void;
66
+ }
67
+ export {};
@@ -0,0 +1,97 @@
1
+ /**
2
+ * DropZone manages individual drop target areas
3
+ * Handles hit testing and insertion index calculation
4
+ */
5
+ import type { DataTransfer, DropZoneOptions } from '../types/index.js';
6
+ export declare class DropZone {
7
+ private _element;
8
+ private _options;
9
+ private _isActive;
10
+ constructor(element: HTMLElement, options?: DropZoneOptions);
11
+ /**
12
+ * The DOM element for this drop zone
13
+ */
14
+ get element(): HTMLElement;
15
+ /**
16
+ * Whether this zone is currently active (being hovered)
17
+ */
18
+ get isActive(): boolean;
19
+ /**
20
+ * Update options
21
+ */
22
+ setOptions(options: Partial<DropZoneOptions>): void;
23
+ /**
24
+ * Check if a point is inside this drop zone
25
+ */
26
+ containsPoint(x: number, y: number): boolean;
27
+ /**
28
+ * Check if this zone accepts the given data
29
+ */
30
+ accepts(data: DataTransfer): boolean;
31
+ /**
32
+ * Set active state and add/remove CSS class
33
+ */
34
+ setActive(active: boolean): void;
35
+ /**
36
+ * Calculate insertion index for sortable behavior
37
+ * Returns the index where an item should be inserted based on position
38
+ */
39
+ getInsertionIndex(x: number, y: number, itemSelector: string, excludeElement?: HTMLElement): number;
40
+ /**
41
+ * Get closest item to a point (for visual feedback)
42
+ */
43
+ getClosestItem(x: number, y: number, itemSelector: string, excludeElement?: HTMLElement): {
44
+ element: HTMLElement;
45
+ position: 'before' | 'after';
46
+ } | null;
47
+ /**
48
+ * Force update cached bounds
49
+ */
50
+ updateBounds(): void;
51
+ /**
52
+ * Cleanup
53
+ */
54
+ destroy(): void;
55
+ }
56
+ /**
57
+ * DropZoneManager handles multiple drop zones
58
+ */
59
+ export declare class DropZoneManager {
60
+ private _zones;
61
+ /**
62
+ * Register a drop zone
63
+ */
64
+ register(element: HTMLElement, options?: DropZoneOptions): DropZone;
65
+ /**
66
+ * Unregister a drop zone
67
+ */
68
+ unregister(element: HTMLElement): void;
69
+ /**
70
+ * Get drop zone for an element
71
+ */
72
+ get(element: HTMLElement): DropZone | undefined;
73
+ /**
74
+ * Get all drop zone elements
75
+ */
76
+ getElements(): HTMLElement[];
77
+ /**
78
+ * Get all drop zones
79
+ */
80
+ getAll(): DropZone[];
81
+ /**
82
+ * Find drop zone at point
83
+ */
84
+ findAtPoint(x: number, y: number): DropZone | null;
85
+ /**
86
+ * Clear all drop zones
87
+ */
88
+ clear(): void;
89
+ /**
90
+ * Update all bounds (e.g., after scroll/resize)
91
+ */
92
+ updateAllBounds(): void;
93
+ /**
94
+ * Cleanup
95
+ */
96
+ destroy(): void;
97
+ }