UNPKG

snapdrag

Version:

A simple, lightweight, and performant drag and drop library for React and vanilla JS

1,261 lines (962 loc) 40.9 kB
<p align="center"> <img width="200" src="https://raw.githubusercontent.com/zheksoon/snapdrag/readme-rewrite/assets/snapdrag-black.webp" alt="Snapdrag" /> </p> <p align="center"> <b>⚡️ Simple yet powerful drag-and-drop for React and Vanilla JS ⚡️</b> </p> <p align="center"> <img width="400" alt="Snapdrag in action" src="https://raw.githubusercontent.com/zheksoon/snapdrag/readme-rewrite/assets/drag-and-drop-kanban.avif" /> </p> ## What is Snapdrag? **Snapdrag** is an alternative vision of how drag-and-drop should be done in React - simple, intuitive, and performant. With just two hooks and an overlay component you can build rich drag-and-drop interactions - starting from simple squares, ending with scrollable and sortable multi-lists. Snapdrag is built on top of `snapdrag/core`, a universal building block that works with any framework or vanilla JavaScript. ## Key Features - 🚀 **Minimal, modern API:** just two hooks and one overlay component - 🎛️ **Full control:** granular event callbacks for every drag stage - 🔄 **Two-way data flow:** draggables and droppables exchange data seamlessly - 🗂️ **Multiple drop targets:** supports overlapping and nested zones - 🔌 **Plugins system:** easily extend functionality - 🛑 **No HTML5 DnD:** consistent, reliable behavior across browsers - ⚡️ **Built for performance and extensibility** ## TL;DR ```tsx import { useDraggable, useDroppable, Overlay } from "snapdrag"; import "./styles.css"; const App = () => { const { draggable } = useDraggable({ kind: "SQUARE", data: { color: "red" }, move: true, }); const { droppable } = useDroppable({ accepts: "SQUARE", onDrop({ data }) { alert(`Dropped ${data.color} square`); }, }); return ( <div className="app"> <div className="absolute left-100"> {draggable(<div className="square red">Drag me</div>)} </div> <div className="absolute left-300"> {droppable(<div className="square green">Drop on me</div>)} </div> <Overlay /> </div> ); }; ``` Result: <p align="center"> <img width="400" alt="TL;DR example" src="https://raw.githubusercontent.com/zheksoon/snapdrag/readme-rewrite/assets/tldr-drop.avif" /> </p> ## Table of Contents - [Installation](#installation) - [Basic Concepts](#basic-concepts) - [Quick Start Example](#quick-start-example) - [How Snapdrag Works](#how-snapdrag-works) - [Core Components](#core-components) - [useDraggable](#usedraggable) - [useDroppable](#usedroppable) - [Overlay](#overlay) - [Draggable Lifecycle](#draggable-lifecycle) - [Droppable Lifecycle](#droppable-lifecycle) - [Common Patterns](#common-patterns) - [Examples](#examples) - [Basic: Colored Squares](#basic-colored-squares) - [Intermediate: Simple List](#intermediate-simple-list) - [Advanced: List with Animations](#advanced-list-with-animations) - [Expert: Kanban Board](#expert-kanban-board) - [API Reference](#api-reference) - [useDraggable Configuration](#usedraggable-configuration) - [useDroppable Configuration](#usedroppable-configuration) - [Plugins](#plugins) - [Browser Compatibility](#browser-compatibility) - [License](#license) - [Author](#author) ## Installation ```bash # npm npm install --save snapdrag # yarn yarn add snapdrag ``` ## Basic Concepts Snapdrag is built around three core components: - **`useDraggable`** - A hook that makes any React element draggable - **`useDroppable`** - A hook that makes any React element a potential drop target - **`Overlay`** - A component that renders the dragged element during drag operations The fundamental relationship works like this: 1. Each draggable has a **`kind`** (like "CARD" or "ITEM") that identifies what type of element it is 2. Each droppable specifies what **`kind`** it **`accepts`** through its configuration 3. They exchange **`data`** during interactions, allowing for rich behaviors and communication When a draggable is over a compatible droppable, they can exchange information. This unlocks dynamic behaviors such as highlighting, sorting, or visually transforming elements based on the ongoing interaction. ## Quick Start Example Here is more comprehensive example that demonstrate the lifecycle of draggable and droppable items. Usually you need to use only subset of that, but we will show almost every callback for clarity. <p align="center"> <img width="400" alt="Simple drag-and-drop squares" src="https://raw.githubusercontent.com/zheksoon/snapdrag/readme-rewrite/assets/simple-squares.avif" /> </p> **DraggableSquare.tsx** ```tsx import { useState } from "react"; import { useDraggable } from "snapdrag"; export const DraggableSquare = ({ color }: { color: string }) => { const [text, setText] = useState("Drag me"); const { draggable, isDragging } = useDraggable({ kind: "SQUARE", data: { color }, move: true, // Callbacks are totally optional onDragStart({ data }) { // data is the own data of the draggable setText(`Dragging ${data.color}`); }, onDragMove({ dropTargets }) { // Check if there are any drop targets under the pointer if (dropTargets.length > 0) { // Update the text based on the first drop target color setText(`Over ${dropTargets[0].data.color}`); } else { setText("Dragging..."); } }, onDragEnd({ dropTargets }) { // Check if the draggable was dropped on a valid target if (dropTargets.length > 0) { setText(`Dropped on ${dropTargets[0].data.color}`); } else { setText("Drag me"); } }, }); const opacity = isDragging ? 0.5 : 1; return draggable( <div className="square draggable" style={{ backgroundColor: color, opacity }}> {text} </div> ); }; ``` **DroppableSquare.tsx** ```tsx import { useState } from "react"; import { useDroppable } from "snapdrag"; export const DroppableSquare = ({ color }: { color: string }) => { const [text, setText] = useState("Drop here"); const { droppable } = useDroppable({ accepts: "SQUARE", data: { color }, // Optional callbacks onDragIn({ data }) { // Some draggable is hovering over this droppable // data is the data of the draggable setText(`Hovered over ${data.color}`); }, onDragOut() { // The draggable is no longer hovering over this droppable setText("Drop here"); }, onDrop({ data }) { // Finally, the draggable is dropped on this droppable setText(`Dropped ${data.color}`); }, }); return droppable( <div className="square droppable" style={{ backgroundColor: color }}> {text} </div> ); }; ``` **App.tsx** ```tsx import { Overlay } from "snapdrag"; export default function App() { return ( <div className="app relative"> {/* Just two squares for simplicity */} <div className="absolute top-100 left-100"> <DraggableSquare color="red" /> </div> <div className="absolute top-100 left-300"> <DroppableSquare color="green" /> </div> {/* Render overlay to show the dragged component */} <Overlay /> </div> ); } ``` This example on [CodeSandbox](https://codesandbox.io/p/sandbox/snapdrag-simple-squares-8rw96s) ## How Snapdrag Works Under the hood, Snapdrag takes a different approach than traditional drag-and-drop libraries: 1. **Event Listening**: Snapdrag attaches a `pointerdown` event listener to draggable elements 2. **Tracking Movement**: Once triggered, it tracks `pointermove` events on the document until `pointerup` occurs 3. **Finding Targets**: On every move, it uses `document.elementsFromPoint()` to check what elements are under the cursor 4. **Target Handling**: It then determines which droppable elements are valid targets and manages the interaction 5. **Event Firing**: Appropriate callbacks are fired based on the current state of the drag operation Unlike HTML5 drag-and-drop which has limited customization options, Snapdrag gives you control over every aspect of the drag experience. You can change settings of draggable and droppable at any time during the drag operation, making Snapdrag extremely flexible. Want to dynamically change what a draggable can do based on its current position? No problem! ## Core Components ### `useDraggable` The `useDraggable` hook makes any React element draggable. It returns an object with two properties: - `draggable`: A function that wraps your component, making it draggable - `isDragging`: A boolean indicating if the element is currently being dragged Basic usage: ```tsx const DraggableItem = () => { const { draggable, isDragging } = useDraggable({ kind: "ITEM", // Required: identifies this draggable type data: { id: "123" }, // Optional: data to share during drag operations move: true, // Optional: move vs clone during dragging }); return draggable(<div className={isDragging ? "dragging" : ""}>Drag me!</div>); }; ``` **Important Note**: The wrapped component must accept a `ref` to the DOM node to be draggable. If you already have a ref, Snapdrag will handle it correctly: ```jsx const myRef = useRef(null); const { draggable } = useDraggable({ kind: "ITEM", }); // Both refs work correctly return draggable(<div ref={myRef} />); ``` You can even make an element both draggable and droppable: ```jsx const { draggable } = useDraggable({ kind: "ITEM" }); const { droppable } = useDroppable({ accepts: "ITEM" }); // Combine the wrappers (order doesn't matter) return draggable(droppable(<div>I'm both!</div>)); ``` ### `useDroppable` The `useDroppable` hook makes any React element a potential drop target. It returns: - `droppable`: A function that wraps your component, making it a drop target - `hovered`: Data about the draggable currently hovering over this element (or `null` if none) Basic usage: ```jsx const DropZone = () => { const { droppable, hovered } = useDroppable({ accepts: "ITEM", // Required: which draggable kinds to accept data: { zone: "main" }, // Optional: data to share with draggables onDrop({ data }) { // Optional: handle successful drops console.log("Dropped item:", data.id); }, }); // Change appearance when being hovered const isHovered = Boolean(hovered); return droppable(<div className={isHovered ? "drop-zone hovered" : "drop-zone"}>Drop here</div>); }; ``` ### `Overlay` The `Overlay` component renders the currently dragged element. It should be included once in your application: ```tsx import { Overlay } from "snapdrag"; function App() { return ( <div> {/* Your app content */} <YourDraggableComponents /> {/* Required: Shows the dragged element */} <Overlay className="your-class" style={someStyles} /> </div> ); } ``` You can add your own classes and styles to the overlay to make it fit your application. ## Draggable Lifecycle The draggable component goes through a lifecycle during drag interactions, with callbacks at each stage. ### `onDragStart` Called when the drag operation begins (after the user clicks and begins moving, and after `shouldDrag` if provided, returns `true`): ```jsx const { draggable } = useDraggable({ kind: "CARD", onDragStart({ data, event, dragStartEvent, element }) { console.log("Started dragging card:", data.id); // Setup any state needed during dragging }, }); ``` The callback receives an object with the following properties: - `data`: The draggable's data (from the `data` config option of `useDraggable`). - `event`: The `PointerEvent` that triggered the drag start (usually the first `pointermove` after `pointerdown` and `shouldDrag` validation). - `dragStartEvent`: The initial `PointerEvent` from `pointerdown` that initiated the drag attempt. - `element`: The DOM element that is being dragged (this is the element rendered in the `Overlay`). ### `onDragMove` Called on every pointer movement during dragging: ```jsx const { draggable } = useDraggable({ kind: "CARD", onDragMove({ dropTargets, top, left, data, event, dragStartEvent, element }) { // dropTargets contains info about all drop targets under the pointer if (dropTargets.length > 0) { console.log("Over drop zone:", dropTargets[0].data.zone); } // top and left are the screen coordinates of the draggable console.log(`Position: ${left}px, ${top}px`); }, }); ``` In addition to the properties from `onDragStart` (`data`, `dragStartEvent`, `element`), this callback receives: - `event`: The current `PointerEvent` from the `pointermove` handler. - `dropTargets`: An array of objects, each representing a droppable target currently under the pointer. Each object contains: - `data`: The `data` associated with the droppable (from its `useDroppable` configuration). - `element`: The DOM element of the droppable. - `top`: The calculated top screen coordinate of the draggable element in the overlay. - `left`: The calculated left screen coordinate of the draggable element in the overlay. **Note**: This callback is called frequently, so avoid expensive operations here. ### `onDragEnd` Called when the drag operation completes (on `pointerup`): ```jsx const { draggable } = useDraggable({ kind: "CARD", onDragEnd({ dropTargets, top, left, data, event, dragStartEvent, element }) { if (dropTargets.length > 0) { console.log("Dropped on:", dropTargets[0].data.zone); } else { console.log("Dropped outside of any drop zone"); // Handle "cancel" logic } }, }); ``` Receives the same properties as `onDragMove` (`data`, `event`, `dragStartEvent`, `element`, `dropTargets`, `top`, `left`). If the user dropped the element on valid drop targets, `dropTargets` will contain them; otherwise, it will be an empty array. The `top` and `left` coordinates represent the final position of the draggable in the overlay just before it's hidden. ## Droppable Lifecycle The droppable component also has lifecycle events during drag interactions. All droppable callbacks receive a `dropTargets` array, similar to the one in `useDraggable`'s `onDragMove` and `onDragEnd`, representing all droppables currently under the pointer. ### `onDragIn` Called when a draggable first enters this drop target: ```jsx const { droppable } = useDroppable({ accepts: "CARD", onDragIn({ kind, data, event, element, dropElement, dropTargets }) { console.log(`${kind} entered drop zone`); // Change appearance, update state, etc. }, }); ``` The callback receives an object with: - `kind`: The `kind` of the draggable that entered. - `data`: The `data` from the draggable. - `event`: The current `PointerEvent` from the `pointermove` handler. - `element`: The DOM element of the draggable. - `dropElement`: The DOM element of this droppable. - `dropTargets`: Array of all active drop targets under the pointer, including the current one. Each entry contains: - `data`: The `data` from the droppable (from its `useDroppable` configuration). - `element`: The DOM element of the droppable. This is called once when a draggable enters and can be used to trigger animations or state changes. ### `onDragMove` (Droppable) Called as a draggable moves _within_ the drop target: ```jsx const { droppable } = useDroppable({ accepts: "CARD", onDragMove({ kind, data, event, element, dropElement, dropTargets }) { // Calculate position within the drop zone const rect = dropElement.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; console.log(`Position in drop zone: ${x}px, ${y}px`); }, }); ``` Receives the same properties as `onDragIn`. Like the draggable version, this is called frequently, so keep operations light. This is perfect for creating dynamic visual cues like highlighting different sections of your drop zone based on cursor position. ### `onDragOut` Called when a draggable leaves the drop target: ```jsx const { droppable } = useDroppable({ accepts: "CARD", onDragOut({ kind, data, event, element, dropElement, dropTargets }) { console.log(`${kind} left drop zone`); // Revert animations, update state, etc. }, }); ``` Receives the same properties as `onDragIn`. This is typically used to undo changes made in `onDragIn`. Use it to clean up and reset any visual changes you made when the draggable entered. ### `onDrop` Called when a draggable is successfully dropped on this target: ```jsx const { droppable } = useDroppable({ accepts: "CARD", onDrop({ kind, data, event, element, dropElement, dropTargets }) { console.log(`${kind} was dropped with data:`, data); // Handle the dropped item }, }); ``` Receives the same properties as `onDragIn`. This is where you implement the main logic for what happens when a drop succeeds. Update your application state, save the new position, or trigger any other business logic related to the completed drag operation. ## Common Patterns ### Two-way Data Exchange Snapdrag makes it simple for draggables and droppables to talk to each other by exchanging data in both directions: ```jsx // Draggable component accessing droppable data const { draggable } = useDraggable({ kind: "CARD", data: { id: "card-1", color: "red" }, onDragMove({ dropTargets }) { if (dropTargets.length > 0) { // Read data from the drop zone underneath const dropZoneType = dropTargets[0].data.type; console.log(`Over ${dropZoneType} zone`); } }, }); // Droppable component accessing draggable data const { droppable, hovered } = useDroppable({ accepts: "CARD", data: { type: "inbox" }, onDragIn({ data }) { console.log(`Card ${data.id} entered inbox`); }, }); ``` This pattern is especially useful for adapting the UI based on the interaction context. ### Dynamic Colors Example Here's how to create a draggable that changes color based on the droppable it's over: ```jsx // In DraggableSquare.tsx import { useState } from "react"; import { useDraggable } from "snapdrag"; export const DraggableSquare = ({ color: initialColor }) => { const [color, setColor] = useState(initialColor); const { draggable, isDragging } = useDraggable({ kind: "SQUARE", data: { color }, move: true, onDragMove({ dropTargets }) { if (dropTargets.length) { setColor(dropTargets[0].data.color); } else { setColor(initialColor); } }, onDragEnd() { setColor(initialColor); // Reset on drop }, }); return draggable( <div className="square" style={{ backgroundColor: color, opacity: isDragging ? 0.9 : 1, }} > {isDragging ? "Dragging" : "Drag me"} </div> ); }; // In DroppableSquare.tsx import { useDroppable } from "snapdrag"; export const DroppableSquare = ({ color }) => { const [text, setText] = useState("Drop here"); const { droppable } = useDroppable({ accepts: "SQUARE", data: { color }, // Share this color with draggables onDrop({ data }) { setText(`Dropped ${data.color}`); }, }); return droppable( <div className="square" style={{ backgroundColor: color }}> {text} </div> ); }; ``` ### Dynamic Border Example This example shows how to create a visual indication of where an item will be dropped: ```jsx import { useState } from "react"; import { useDroppable } from "snapdrag"; export const DroppableSquare = ({ color }) => { const [text, setText] = useState("Drop here"); const [borderPosition, setBorderPosition] = useState(""); const { droppable } = useDroppable({ accepts: "SQUARE", onDragMove({ event, dropElement }) { // Calculate which quadrant of the square the pointer is in const { top, left, height } = dropElement.getBoundingClientRect(); const x = event.clientX - left; const y = event.clientY - top; // Set border on the appropriate side if (x / y < 1.0) { if (x / (height - y) < 1.0) { setBorderPosition("borderLeft"); } else { setBorderPosition("borderBottom"); } } else { if (x / (height - y) < 1.0) { setBorderPosition("borderTop"); } else { setBorderPosition("borderRight"); } } }, onDragOut() { setBorderPosition(""); // Remove border when draggable leaves }, onDrop({ data }) { setText(`Dropped ${data.color}`); setBorderPosition(""); // Remove border after drop }, }); // Add border to appropriate side const style = { backgroundColor: color, [borderPosition]: "10px solid red", }; return droppable( <div className="square" style={style}> {text} </div> ); }; ``` ### Multiple Drop Targets Snapdrag handles the case where multiple drop targets overlap: ```jsx const { draggable } = useDraggable({ kind: "ITEM", onDragMove({ dropTargets }) { // Sort by order to find the topmost const sorted = [...dropTargets].sort((a, b) => b.data.order - a.data.order); if (sorted.length) { console.log(`Topmost target: ${sorted[0].data.name}`); } }, }); // ... somewere in your code const { droppable } = useDroppable({ accepts: "ITEM", data: { order: 10 }, }); ``` You also can you DOM elements to get the topmost drop target: ```tsx const { draggable } = useDraggable({ kind: "ITEM", onDragMove({ dropTargets }) { // Sort by order to find the topmost const sorted = [...dropTargets].sort((a, b) => { // access drop target element instead of data const aIndex = a.element.getComputedStyle().zIndex || 0; const bIndex = b.element.getComputedStyle().zIndex || 0; return bIndex - aIndex; }); if (sorted.length) { console.log(`Topmost target: ${sorted[0].data.name}`); } }, }); ``` ### Drag Threshold For finer control, you can start dragging only after the pointer has moved a certain distance: ```jsx const { draggable } = useDraggable({ kind: "ITEM", shouldDrag({ event, dragStartEvent }) { // Calculate distance from start position const dx = event.clientX - dragStartEvent.clientX; const dy = event.clientY - dragStartEvent.clientY; const distance = Math.sqrt(dx * dx + dy * dy); // Only start dragging after moving 5px return distance > 5; }, }); ``` ### Touch Support Snapdrag supports touch events out of the box. It uses `PointerEvent` to handle both mouse and touch interactions seamlessly. You can use the same API for both types of events. To make your draggable elements touch-friendly, ensure they are touchable (e.g., using `touch-action: none` in CSS). The container can have `touch-action: pan-x` or `touch-action: pan-y` to allow scrolling while dragging. ## Examples Snapdrag includes several examples that demonstrate its capabilities, from simple to complex use cases. ### Basic: Colored Squares The simplest example shows dragging a colored square onto a drop target: <p align="center"> <img width="400" alt="Simple squares" src="https://raw.githubusercontent.com/zheksoon/snapdrag/readme-rewrite/assets/drag-and-drop-squares.avif" /> </p> This demonstrates the fundamentals of drag-and-drop with Snapdrag: - Defining a draggable with `kind` and `data` - Creating a drop target that `accepts` the draggable - Handling the `onDrop` event [Try it on CodeSandbox](https://codesandbox.io/p/sandbox/snapdrag-simple-squares-8rw96s) ### Intermediate: Simple List A sortable list where items can be reordered by dragging: <p align="center"> <img width="400" alt="Simple List" src="https://raw.githubusercontent.com/zheksoon/snapdrag/readme-rewrite/assets/drag-and-drop-simple-list.avif" /> </p> This example demonstrates: - Using data to identify list items - Visual feedback during dragging (blue insertion line) - Reordering items in a state array on drop [Try it on CodeSandbox](https://codesandbox.io/p/sandbox/snapdrag-simple-list-w4njk5) ### Advanced: List with Animations A more sophisticated list with smooth animations: <p align="center"> <img width="400" alt="Advanced list" src="https://raw.githubusercontent.com/zheksoon/snapdrag/readme-rewrite/assets/drag-and-drop-advanced-list.avif" /> </p> This example showcases: - CSS transitions for smooth animations - A special drop area for appending items to the end - Animated placeholders that create space for dropped items [Try it on CodeSandbox](https://codesandbox.io/p/sandbox/snapdrag-advanced-list-5p44wd) ### Expert: Kanban Board A full kanban board with multiple columns and draggable cards: <p align="center"> <img width="400" alt="Kanban Board" src="https://raw.githubusercontent.com/zheksoon/snapdrag/readme-rewrite/assets/drag-and-drop-kanban.avif" /> </p> This complex example demonstrates advanced features: - Multiple drop targets with different behaviors - Conditional acceptance of draggables - Smooth animations during drag operations - Two-way data exchange between components - Touch support with drag threshold - Item addition and removal All this is achieved in just about 200 lines of code (excluding state management and styling). [Try it on CodeSandbox](https://codesandbox.io/p/sandbox/snapdrag-kanban-board-jlj4wc) ## API Reference ### `useDraggable` Configuration The `useDraggable` hook accepts a configuration object with these options: | Option | Type | Description | | ------------- | ---------------------- | ------------------------------------------------------------------------------------- | | `kind` | `string` or `symbol` | **Required.** Identifies this draggable type | | `data` | `object` or `function` | Data to share with droppables. Can be a static object or a function that returns data | | `disabled` | `boolean` | When `true`, disables dragging functionality | | `move` | `boolean` | When `true`, moves the component instead of cloning it to the overlay | | `component` | `function` | Provides a custom component to show while dragging | | `placeholder` | `function` | Custom component to show in place of the dragged item | | `offset` | `object` or `function` | Controls positioning relative to cursor | **Event Callbacks:** | Callback | Description | | ------------- | ---------------------------------------------------------------------------- | | `shouldDrag` | Function determining if dragging should start. Must return `true` or `false` | | `onDragStart` | Called when drag begins | | `onDragMove` | Called on every pointer move while dragging | | `onDragEnd` | Called when dragging ends | #### Detailed Configuration Description ##### `kind` (Required) Defines the type of the draggable. It must be a unique string or symbol. ```jsx const { draggable } = useDraggable({ kind: "SQUARE", // Identify this as a "SQUARE" type }); ``` ##### `data` Data associated with the draggable. It can be a static object or a function that returns an object: ```jsx // Static object const { draggable } = useDraggable({ kind: "SQUARE", data: { color: "red", id: "square-1" }, }); // Function (calculated at drag start) const { draggable } = useDraggable({ kind: "SQUARE", data: ({ dragElement, dragStartEvent }) => ({ id: dragElement.id, position: { x: dragStartEvent.clientX, y: dragStartEvent.clientY }, }), }); ``` ##### `disabled` When `true`, temporarily disables dragging: ```jsx const { draggable } = useDraggable({ kind: "SQUARE", disabled: !canDrag, // Disable based on some condition }); ``` ##### `move` When `true`, the original component is moved during dragging instead of creating a clone: ```jsx const { draggable } = useDraggable({ kind: "SQUARE", move: true, // Move the actual component }); ``` Note: If `move` is `false` (default), the component is cloned to the overlay layer while the original stays in place. The original component won't receive prop updates during dragging. ##### `component` A function that returns a custom component to be shown during dragging: ```jsx const { draggable } = useDraggable({ kind: "SQUARE", component: ({ data, props }) => <Square color={data.color} style={{ opacity: 0.8 }} />, }); ``` ##### `placeholder` A function that returns a component to be shown in place of the dragged item: ```jsx const { draggable } = useDraggable({ kind: "SQUARE", placeholder: ({ data, props }) => <Square color="gray" style={{ opacity: 0.4 }} />, }); ``` When specified, the `move` option is ignored. ##### `offset` Controls the offset of the dragging component relative to the cursor: ```jsx // Static offset const { draggable } = useDraggable({ kind: "SQUARE", offset: { top: 10, left: 10 }, // 10px down and right from cursor }); // Dynamic offset const { draggable } = useDraggable({ kind: "SQUARE", offset: ({ element, event, data }) => { // Calculate based on event or element position return { top: 0, left: 0 }; }, }); ``` If not specified, the offset is calculated to maintain the element's initial position relative to the cursor. #### Callback Details ##### `shouldDrag` Function that determines if dragging should start. It's called on every pointer move until it returns `true` or the drag attempt ends: ```jsx const { draggable } = useDraggable({ kind: "SQUARE", shouldDrag: ({ event, dragStartEvent, element, data }) => { // Only drag if shifted 10px horizontally return Math.abs(event.clientX - dragStartEvent.clientX) > 10; }, }); ``` ##### `onDragStart` Called when dragging begins (after `shouldDrag` returns `true`): ```jsx const { draggable } = useDraggable({ kind: "SQUARE", onDragStart: ({ event, dragStartEvent, element, data }) => { console.log("Drag started at:", event.clientX, event.clientY); // Setup any initial state needed during drag }, }); ``` ##### `onDragMove` Called on every pointer move during dragging: ```jsx const { draggable } = useDraggable({ kind: "SQUARE", onDragMove: ({ event, dragStartEvent, element, data, dropTargets, top, left }) => { // Current drop targets under the pointer if (dropTargets.length) { console.log("Over drop zone:", dropTargets[0].data.name); } // Current position of the draggable console.log("Position:", top, left); }, }); ``` The `dropTargets` array contains information about all current drop targets under the cursor. Each entry has `data` (from the droppable's configuration) and `element` (the DOM element). ##### `onDragEnd` Called when dragging ends: ```jsx const { draggable } = useDraggable({ kind: "SQUARE", onDragEnd: ({ event, dragStartEvent, element, data, dropTargets }) => { if (dropTargets.length) { console.log("Dropped on:", dropTargets[0].data.name); } else { console.log("Dropped outside any drop target"); // Handle "cancel" case } }, }); ``` ### `useDroppable` Configuration The `useDroppable` hook accepts a configuration object with these options: | Option | Type | Description | | ---------- | ------------------------------------------ | -------------------------------------------- | | `accepts` | `string`, `symbol`, `array`, or `function` | **Required.** What draggable kinds to accept | | `data` | `object` | Data to share with draggables | | `disabled` | `boolean` | When `true`, disables dropping | **Event Callbacks:** | Callback | Description | | ------------ | ---------------------------------------------------- | | `onDragIn` | Called when a draggable enters this droppable | | `onDragOut` | Called when a draggable leaves this droppable | | `onDragMove` | Called when a draggable moves within this droppable | | `onDrop` | Called when a draggable is dropped on this droppable | #### Detailed Configuration Description ##### `accepts` (Required) Defines what kinds of draggables this drop target can accept: ```jsx // Accept a single kind const { droppable } = useDroppable({ accepts: "SQUARE", }); // Accept multiple kinds const { droppable } = useDroppable({ accepts: ["SQUARE", "CIRCLE"], }); // Use a function for more complex logic const { droppable } = useDroppable({ accepts: ({ kind, data }) => { // Check both kind and data to determine acceptance return kind === "SQUARE" && data.color === "red"; }, }); ``` ##### `data` Data associated with the droppable area: ```jsx const { droppable } = useDroppable({ accepts: "SQUARE", data: { zoneId: "dropzone-1", capacity: 5, color: "blue", }, }); ``` This data is accessible to draggables through the `dropTargets` array in their callbacks. ##### `disabled` When `true`, temporarily disables dropping: ```jsx const { droppable } = useDroppable({ accepts: "SQUARE", disabled: isFull, // Disable based on some condition }); ``` #### Callback Details ##### `onDragIn` Called when a draggable of an accepted kind first enters this drop target: ```jsx const { droppable } = useDroppable({ accepts: "SQUARE", onDragIn: ({ kind, data, event, element, dropElement, dropTargets }) => { console.log(`${kind} entered with data:`, data); // Change appearance, play sound, etc. }, }); ``` Arguments: - `kind` - The kind of the draggable - `data` - The data from the draggable - `event` - The current pointer event - `element` - The draggable element - `dropElement` - The droppable element - `dropTargets` - Array of all current drop targets under the pointer ##### `onDragOut` Called when a draggable leaves this drop target: ```jsx const { droppable } = useDroppable({ accepts: "SQUARE", onDragOut: ({ kind, data, event, element, dropElement, dropTargets }) => { console.log(`${kind} left the drop zone`); // Revert appearance changes, etc. }, }); ``` Arguments are the same as `onDragIn`. ##### `onDragMove` Called when a draggable moves within this drop target: ```jsx const { droppable } = useDroppable({ accepts: "SQUARE", onDragMove: ({ kind, data, event, element, dropElement, dropTargets }) => { // Calculate position within drop zone const rect = dropElement.getBoundingClientRect(); const relativeX = event.clientX - rect.left; const relativeY = event.clientY - rect.top; console.log(`Position in zone: ${relativeX}px, ${relativeY}px`); }, }); ``` Arguments are the same as `onDragIn`. ##### `onDrop` Called when a draggable is dropped on this target: ```jsx const { droppable } = useDroppable({ accepts: "SQUARE", onDrop: ({ kind, data, event, element, dropElement, dropTargets }) => { console.log(`${kind} was dropped with data:`, data); // Handle the dropped item (update state, etc.) }, }); ``` Arguments are the same as the other callbacks. ## Plugins Snapdrag offers a plugin system to extend its core functionality. Plugins can hook into the draggable lifecycle events (`onDragStart`, `onDragMove`, `onDragEnd`) to add custom behaviors. ### Scroller Plugin The `scroller` plugin automatically scrolls a container element when a dragged item approaches its edges. This is useful for large scrollable areas where users might need to drag items beyond the visible viewport. **Initialization** To use the scroller plugin, first create an instance of it by calling `createScroller(config)`. ```typescript import { createScroller } from "snapdrag/plugins"; const scroller = createScroller({ x: true, // Enable horizontal scrolling with default settings y: { threshold: 150, speed: 1000, distancePower: 2 }, // Enable vertical scrolling with custom settings }); ``` **Configuration Options (`ScrollerConfig`)** - `x`: (Optional) Enables or configures horizontal scrolling. - `boolean`: If `true`, uses default settings. If `false` or omitted, horizontal scrolling is disabled. - `object (AxisConfig)`: Allows fine-tuning of horizontal scrolling behavior: - `threshold` (number, default: `100`): The distance in pixels from the container's edge at which scrolling should begin. - `speed` (number, default: `2000`): The maximum scroll speed in pixels per second when the pointer is at the very edge of the container. - `distancePower` (number, default: `1.5`): Controls the acceleration of scrolling as the pointer gets closer to the edge. A higher value means faster acceleration. - `y`: (Optional) Enables or configures vertical scrolling. Accepts the same `boolean` or `object (AxisConfig)` values as `x`. **Usage with `useDraggable`** Once created, the scroller instance needs to be passed to the `plugins` array in the `useDraggable` hook's configuration. The scroller function itself takes the scrollable container element as an argument. ```jsx import { useDraggable } from "snapdrag"; import { createScroller } from "snapdrag/plugins"; import { useRef, useEffect, useState } from "react"; // Initialize the scroller plugin const scrollerPlugin = createScroller({ x: true, y: true }); const DraggableComponent = () => { // State to hold the container element once it's mounted const [scrollContainer, setScrollContainer] = useState(null); const { draggable } = useDraggable({ kind: "ITEM", data: { id: "my-item" }, plugins: [scrollerPlugin(scrollContainer)], }); return ( <div ref={setScrollContainer} style={{ overflow: "auto", height: "200px", width: "300px", border: "1px solid black" }} > <div style={{ height: "500px", width: "800px" }}> {/* Inner content larger than container */} {draggable( <div style={{ width: "100px", height: "50px", backgroundColor: "lightblue" }}> Drag me </div> )} {/* More draggable items or content here */} </div> </div> ); }; ``` **How it Works** 1. **Initialization**: `createScroller` returns a new scroller function configured with your desired settings. 2. **Plugin Attachment**: When you pass `scrollerPlugin(containerElement)` to `useDraggable`, Snapdrag calls the appropriate lifecycle methods of the plugin (`onDragStart`, `onDragMove`, `onDragEnd`). 3. **Drag Monitoring**: During a drag operation, `onDragMove` is continuously called. The scroller plugin checks the pointer's position relative to the specified `containerElement`. 4. **Edge Detection**: If the pointer moves within the `threshold` distance of an edge for an enabled axis (x or y), the plugin initiates scrolling. 5. **Scrolling Speed**: The scrolling speed increases polynomially (based on `distancePower`) as the pointer gets closer to the edge, up to the maximum `speed`. 6. **Animation Loop**: Scrolling is performed using `requestAnimationFrame` for smooth animation. 7. **Cleanup**: When the drag ends (`onDragEnd`) or the component unmounts, the plugin cleans up any active animation frames. **Important Considerations:** - The `containerElement` passed to the scroller function must be the actual scrollable DOM element. - Ensure the `containerElement` has `overflow: auto` or `overflow: scroll` CSS properties set for the respective axes you want to enable scrolling on. - If the scrollable container is not immediately available on component mount (e.g., if its ref is populated later), you might need to conditionally apply the plugin or update it, as shown in the example using `useState` and `useEffect` to pass the container element once it's available. - The plugin calculates distances based on the viewport. If your scroll container or draggable items are scaled using CSS transforms, you might need to adjust threshold and speed values accordingly or ensure pointer events are correctly mapped. The `scroller` plugin offers a straightforward way to add automatic scrolling to your drag-and-drop interfaces. It significantly enhances usability, especially when users need to drag items across large, scrollable containers or overflowing content areas. ## Browser Compatibility Snapdrag is compatible with all modern browsers that support Pointer Events. This includes: - Chrome 55+ - Firefox 59+ - Safari 13.1+ - Edge 18+ Mobile devices are also supported as long as they support Pointer Events. ## License MIT ## Author Eugene Daragan