snap-dnd 0.1.3 → 0.2.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 CHANGED
@@ -351,6 +351,111 @@ interface DropEvent {
351
351
  }
352
352
  ```
353
353
 
354
+ ## Security Considerations
355
+
356
+ Snap is designed with security in mind for enterprise and banking applications.
357
+
358
+ ### Built-in Security Features
359
+
360
+ 1. **Prototype Pollution Protection**
361
+ - All option merging uses `safeMerge()` which blocks `__proto__`, `constructor`, and `prototype` keys
362
+
363
+ 2. **Selector Validation**
364
+ - User-provided CSS selectors are validated before use
365
+ - Blocks potentially dangerous patterns (javascript:, expression(), etc.)
366
+ - Falls back to safe defaults if validation fails
367
+
368
+ 3. **Data Attribute Sanitization**
369
+ - Only allowed data attributes are extracted (`data-drag-id`, `data-drag-type`, etc.)
370
+ - Values are sanitized to prevent XSS via HTML tags
371
+
372
+ 4. **Banking-Grade File Validation** (FileDrop plugin)
373
+ ```javascript
374
+ const snap = new Snap(container).use(new FileDrop({
375
+ secureValidation: true, // Enable strict validation
376
+ validateMagicBytes: true, // Detect extension spoofing
377
+ maxSize: 10 * 1024 * 1024, // 10MB limit
378
+ minSize: 1, // Reject empty files
379
+ maxFiles: 10, // Limit batch uploads
380
+ accept: ['.pdf', '.docx'], // Whitelist extensions
381
+ onValidationError: (errors) => {
382
+ // Handle rejected files
383
+ errors.forEach(({ file, error }) => {
384
+ console.warn(`${file.name}: ${error}`);
385
+ });
386
+ }
387
+ }));
388
+ ```
389
+
390
+ ### Content Security Policy (CSP)
391
+
392
+ Snap requires the following CSP directives:
393
+
394
+ ```
395
+ style-src 'unsafe-inline'; /* Required for ghost/placeholder positioning */
396
+ ```
397
+
398
+ For strict CSP environments without `unsafe-inline`:
399
+ - Provide your own ghost/placeholder elements via CSS classes
400
+ - Override the default ghost creation using callbacks
401
+
402
+ ### Security Best Practices
403
+
404
+ 1. **Validate Drop Data**
405
+ ```javascript
406
+ onDrop: (e) => {
407
+ // Always validate data from drag operations
408
+ const itemId = e.data.getData('id');
409
+ if (!isValidId(itemId)) return;
410
+
411
+ // Verify user permissions server-side
412
+ await api.moveItem(itemId, e.dropZone.id);
413
+ }
414
+ ```
415
+
416
+ 2. **Sanitize File Uploads**
417
+ ```javascript
418
+ // Always validate files server-side even with client validation
419
+ onFileDrop: async (e) => {
420
+ const formData = new FormData();
421
+ for (const file of e.files) {
422
+ formData.append('files', file);
423
+ }
424
+
425
+ // Server should re-validate file types, scan for malware
426
+ const response = await fetch('/upload', {
427
+ method: 'POST',
428
+ body: formData,
429
+ credentials: 'same-origin'
430
+ });
431
+ }
432
+ ```
433
+
434
+ 3. **CSRF Protection**
435
+ - Include CSRF tokens in any server requests triggered by drop events
436
+ - Use `credentials: 'same-origin'` for fetch requests
437
+
438
+ 4. **Disable in Sensitive Contexts**
439
+ ```javascript
440
+ // Disable drag when viewing sensitive data
441
+ if (isViewingSecureDocument) {
442
+ snap.disable();
443
+ }
444
+ ```
445
+
446
+ ### Security Utilities
447
+
448
+ Snap exports security utilities for custom use:
449
+
450
+ ```javascript
451
+ import {
452
+ safeMerge, // Prototype pollution-safe object merge
453
+ sanitizeSelector, // Validate CSS selectors
454
+ validateFiles, // Banking-grade file validation
455
+ sanitizeDataValue, // Strip HTML from strings
456
+ } from 'snap-dnd';
457
+ ```
458
+
354
459
  ## Browser Support
355
460
 
356
461
  - Chrome 88+
@@ -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
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Snap - Main entry point for the drag and drop library
3
+ * Coordinates all subsystems and provides the public API
4
+ */
5
+ import type { SnapOptions, SnapInstance, ItemOptions, DropZoneOptions, Plugin, Behavior } from '../types/index.js';
6
+ export declare class Snap implements SnapInstance {
7
+ private _container;
8
+ private _options;
9
+ private _state;
10
+ private _engine;
11
+ private _dropZoneManager;
12
+ private _imperativeDraggables;
13
+ private _imperativeDropZones;
14
+ private _plugins;
15
+ private _behaviors;
16
+ private _observer;
17
+ private _refreshScheduled;
18
+ private _scrollHandler;
19
+ private _resizeHandler;
20
+ constructor(container: HTMLElement | ShadowRoot, options?: SnapOptions);
21
+ /**
22
+ * Current options
23
+ */
24
+ get options(): SnapOptions;
25
+ /**
26
+ * Enable drag and drop
27
+ */
28
+ enable(): void;
29
+ /**
30
+ * Disable drag and drop
31
+ */
32
+ disable(): void;
33
+ /**
34
+ * Cleanup and destroy instance
35
+ */
36
+ destroy(): void;
37
+ /**
38
+ * Re-scan for declarative elements (call after DOM changes)
39
+ */
40
+ refresh(): void;
41
+ /**
42
+ * Register a draggable element imperatively
43
+ */
44
+ addDraggable(element: HTMLElement, options?: ItemOptions): void;
45
+ /**
46
+ * Unregister a draggable element
47
+ */
48
+ removeDraggable(element: HTMLElement): void;
49
+ /**
50
+ * Register a drop zone imperatively
51
+ */
52
+ addDropZone(element: HTMLElement, options?: DropZoneOptions): void;
53
+ /**
54
+ * Unregister a drop zone
55
+ */
56
+ removeDropZone(element: HTMLElement): void;
57
+ /**
58
+ * Check if currently dragging
59
+ */
60
+ isDragging(): boolean;
61
+ /**
62
+ * Get the element currently being dragged
63
+ */
64
+ getActiveElement(): HTMLElement | null;
65
+ /**
66
+ * Register a plugin
67
+ */
68
+ use(plugin: Plugin): this;
69
+ /**
70
+ * Add a behavior
71
+ */
72
+ addBehavior(behavior: Behavior): this;
73
+ /**
74
+ * Update options dynamically
75
+ */
76
+ setOptions(options: Partial<SnapOptions>): void;
77
+ private _getDropZones;
78
+ private _getItemData;
79
+ private _getItemAxis;
80
+ private _scanDeclarativeElements;
81
+ private _setupStateListeners;
82
+ private _setupAutoRefresh;
83
+ private _setupScrollResize;
84
+ }
85
+ export default Snap;
@@ -0,0 +1,4 @@
1
+ export { Snap, default } from './Snap.js';
2
+ export { DragState } from './DragState.js';
3
+ export { DragEngine, type DragEngineOptions } from './DragEngine.js';
4
+ export { DropZone, DropZoneManager } from './DropZone.js';
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Snap - A zero-dependency, memory-optimized drag and drop library
3
+ *
4
+ * @example Basic usage with data attributes
5
+ * ```html
6
+ * <div id="container">
7
+ * <div data-draggable>Drag me</div>
8
+ * <div data-droppable>Drop here</div>
9
+ * </div>
10
+ *
11
+ * <script>
12
+ * const snap = new Snap(document.getElementById('container'));
13
+ * </script>
14
+ * ```
15
+ *
16
+ * @example Imperative usage
17
+ * ```javascript
18
+ * const snap = new Snap(container, {
19
+ * onDrop: (e) => console.log('Dropped!', e.element, e.dropZone)
20
+ * });
21
+ *
22
+ * snap.addDraggable(myElement, { data: { id: 1 } });
23
+ * snap.addDropZone(myZone, { accepts: ['item'] });
24
+ * ```
25
+ *
26
+ * @example With Lit Element
27
+ * ```javascript
28
+ * class MyComponent extends LitElement {
29
+ * snap;
30
+ *
31
+ * firstUpdated() {
32
+ * this.snap = new Snap(this.shadowRoot, {
33
+ * autoRefresh: true,
34
+ * onDrop: this.handleDrop.bind(this)
35
+ * });
36
+ * }
37
+ *
38
+ * disconnectedCallback() {
39
+ * super.disconnectedCallback();
40
+ * this.snap?.destroy();
41
+ * }
42
+ * }
43
+ * ```
44
+ */
45
+ export { Snap, default } from './core/Snap.js';
46
+ export { DragState } from './core/DragState.js';
47
+ export { DragEngine } from './core/DragEngine.js';
48
+ export { DropZone, DropZoneManager } from './core/DropZone.js';
49
+ export { Sortable } from './plugins/Sortable.js';
50
+ export { Kanban } from './plugins/Kanban.js';
51
+ export { FileDrop, createFileDropZone } from './plugins/FileDrop.js';
52
+ export { AutoScroll } from './behaviors/AutoScroll.js';
53
+ export { SnapGrid } from './behaviors/SnapGrid.js';
54
+ export { ConstraintAxis } from './behaviors/ConstraintAxis.js';
55
+ export { EventEmitter } from './utils/EventEmitter.js';
56
+ export { ObjectPool, pointPool, rectPool } from './utils/ObjectPool.js';
57
+ export { BoundsCache, boundsCache } from './utils/BoundsCache.js';
58
+ export { RAFThrottle, rafThrottle } from './utils/RAFThrottle.js';
59
+ export { SnapDataTransfer } from './utils/DataTransfer.js';
60
+ export { PointerSensor } from './sensors/PointerSensor.js';
61
+ export { FileSensor } from './sensors/FileSensor.js';
62
+ export type { Point, Rect, Axis, Unsubscribe, DragSession, DragPhase, DataTransfer, DragStartEvent, DragMoveEvent, DragEndEvent, DropEvent, DropZoneEnterEvent, DropZoneLeaveEvent, FileDropEvent, SnapOptions, SnapInstance, ItemOptions, DropZoneOptions, AutoScrollOptions, GridOptions, SortableOptions, KanbanOptions, FileDropOptions, Plugin, Behavior, Sensor, } from './types/index.js';
@@ -0,0 +1,33 @@
1
+ /**
2
+ * FileDrop plugin - enables file drop zones for external files
3
+ */
4
+ import type { Plugin, SnapInstance, FileDropOptions, FileDropEvent } from '../types/index.js';
5
+ export declare class FileDrop implements Plugin {
6
+ name: string;
7
+ private _snap;
8
+ private _options;
9
+ private _sensor;
10
+ private _onFileDrop;
11
+ constructor(options?: FileDropOptions);
12
+ init(snap: SnapInstance): void;
13
+ destroy(): void;
14
+ /**
15
+ * Set callback for file drop events
16
+ */
17
+ onFileDrop(callback: (event: FileDropEvent) => void): this;
18
+ /**
19
+ * Update file drop options
20
+ */
21
+ setOptions(options: Partial<FileDropOptions>): void;
22
+ private _getContainer;
23
+ private _setupEventHandlers;
24
+ }
25
+ /**
26
+ * Simple file drop zone creation helper
27
+ * For basic use cases without full Snap instance
28
+ */
29
+ export declare function createFileDropZone(element: HTMLElement, options: FileDropOptions & {
30
+ onDrop: (files: File[]) => void;
31
+ onDragEnter?: () => void;
32
+ onDragLeave?: () => void;
33
+ }): () => void;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Kanban plugin - enables moving items between multiple containers
3
+ */
4
+ import type { Plugin, SnapInstance, KanbanOptions } from '../types/index.js';
5
+ export declare class Kanban implements Plugin {
6
+ name: string;
7
+ private _snap;
8
+ private _options;
9
+ private _sourceContainer;
10
+ private _targetContainer;
11
+ private _placeholder;
12
+ private _originalIndex;
13
+ private _currentIndex;
14
+ constructor(options?: KanbanOptions);
15
+ init(snap: SnapInstance): void;
16
+ destroy(): void;
17
+ private _onDragStart;
18
+ private _onDragMove;
19
+ private _onDropZoneEnter;
20
+ private _onDropZoneLeave;
21
+ private _onDragEnd;
22
+ private _createPlaceholder;
23
+ private _movePlaceholder;
24
+ private _calculateInsertionIndex;
25
+ private _getItemCount;
26
+ private _animateItems;
27
+ private _cleanup;
28
+ /**
29
+ * Get source container during drag
30
+ */
31
+ getSourceContainer(): HTMLElement | null;
32
+ /**
33
+ * Get target container during drag
34
+ */
35
+ getTargetContainer(): HTMLElement | null;
36
+ /**
37
+ * Get current insertion index
38
+ */
39
+ getCurrentIndex(): number;
40
+ }