npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

snapdrag-beta

v0.7.16

Published

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

Downloads

29

Readme

What is it?

Snapdrag is a library for drag-and-drop with React in the first place. I was tired of the bulky APIs other libraries offer, so decided to experiment a bit on the ergonomics and simplicity, while maintaining flexibility and customization. It's built on top of snapdrag/core, the universal building block for any framework and vanilla JS.

Key Features

  • Dead simple - just two hooks and overlay component to go
  • Super ergonomic - no need for memoizing callbacks or config
  • Full customization - rich event system
  • Two-way data exchange for draggable and droppable
  • Multiple targets at the same point - do your logic for multilayer interactions
  • No HTML5 drag-and-drop used - for good and for bad

Installation

npm i --save snapdrag

yarn add snapdrag

Show me the code!

Here's the simplest example of two squares. The draggable square carries color in its data, the droppable square reacts to the drag interaction and sets its color according to the color. When dropped, the text of the droppable square is updated.

import { useDraggable } from "snapdrag";

export const DraggableSquare = ({ color }: { color: string }) => {
  const { draggable, isDragging } = useDraggable({
    kind: "SQUARE",
    data: { color },
    move: true,
  });

  const opacity = isDragging ? 0.5 : 1;

  return draggable(
    <div className="square" style={{ backgroundColor: color, opacity }}>
      {isDragging ? "Dragging" : "Drag me"}
    </div>
  );
};
import { useDroppable } from "snapdrag";

export const DroppableSquare = ({ color }: { color: string }) => {
  const [text, setText] = React.useState("Drop here");

  const { droppable, hovered } = useDroppable({
    accepts: "SQUARE",
    onDrop({ data }) {
      setText(`Dropped ${data.color}`);
    },
  });

  const backgroundColor = hovered ? hovered.data.color : color;

  return droppable(
    <div className="square" style={{ backgroundColor }}>
      {text}
    </div>
  );
};
import { Overlay } from "snapdrag";

export default function App() {
  return (
    <>
      {/* Render squares with absolute wrappers for positioning */}
      <div style={{ position: "relative" }}>
        <div style={{ position: "absolute", top: 100, left: 100 }}>
          <DraggableSquare color="red" />
        </div>
        <div style={{ position: "absolute", top: 100, left: 300 }}>
          <DroppableSquare color="green" />
        </div>
      </div>
      {/* Render overlay to show the dragged component */}
      <Overlay />
    </>
  );
}

How it works

So basically, Snapdrag has two hooks, useDraggable and useDroppable, and the Overlay component. The overlay must be rendered on top of the app to show the drag interactions, see the example and notes below.

useDraggable

useDraggable hook returns an object with draggable and isDragging properties. To make it work, just wrap your component with draggable, and then use isDragging to get the drag status. The only required field in the hook config is kind - it defines how to differentiate the draggable from others:

const DraggableSquare = () => {
  const { draggable, isDragging } = useDraggable({
    kind: "SQUARE",
    // other fields are optional
  });

  return draggable(<div>{isDragging ? "dragging" : "drag me"}</div>);
};

Important note: the wrapped component must take a ref to the DOM node to be draggable. If you specify another ref for the component explicitly, draggable will handle it correctly, like this:

const ref = useRef(null); // ref for your own logic

const { draggable, isDragging } = useDraggable({
  kind: "SQUARE",
});

// the ref will be populated as usual
return draggable(<div ref={ref} />);

Moreover, the return result of the draggable wrapper is just the same component (but with ref to internals). As usual, it can be wrapped in another wrapper, say, droppable. This allows your component to be draggable and droppable at the same time:

const { draggable, isDragging } = useDraggable({
  kind: "SQUARE", 
});

const { droppable, hovered } = useDroppable({
  accepts: "SQUARE",
});

const text = isDragging ? "Dragging" : hovered && "Hovered" : "Drag me";

// the order doesn't matter
return draggable(droppable(<div className="square">{text}</div>));

useDraggable config

useDraggable takes a config that carries the kind, data, and event handlers. You don't have to memoize the config and its handlers, it's fine to swap it anytime with new closures and data.

Here's a detailed description of each config field:

const { useDraggable, isDragging } = useDraggable({
  // "kind" is the type of the draggable. 
  // Drop targets specify the kind they accept in "accepts" field,
  // The kind can be a string or symbol
  kind: "SQUARE",

  // "data" is the data associated with the draggable.
  // It's visible to drop targets when drag interaction occurs
  data: { color: "red" },

  // It can also be a function that returns the data:
  data: ({ dragElement, dragStartEvent }) => ({ color: "red" }),

  // "shouldDrag" is an optional callback to define if the element
  // should react to drag interactions. It will keep executing on every move
  // until it returns true or the drag interaction ends
  shouldDrag: ({ event, dragStartEvent, element, data }) => {
    // event: MouseEvent from the pointermove handler
    // dragStartEvent: MouseEvent from the pointerdown handler
    // element: the element on which the drag interaction occurs
    // data: the data associated with the draggable
    // Must return `true` or `false`
    return true;
  },

  // "disabled" means no drag at all, you know :)
  disabled: false,

  // by default, drag interaction clones the component to the overlay layer
  // "move" means that the component is moved instead of being cloned, so null is rendered instead
  move: true,

  // "component" is a function to get a component that will be shown as draggable
  // "data" is the current data for the draggable
  component: ({ data }) => <Square color="blue" />,

  // "placeholder" is a function to get a component that will be shown in place of draggable component
  // When specified, the "move" option is ignored
  placeholder: ({ data }) => <Square color="gray" />,

  // "offset" determines where to show the dragging component relative the the cursor position
  // If not specified, it computes it in that way, so the component position matches 
  // rendered position before the drag
  offset: { top: 0, left: 0 },

  // alternatively, you can put complex computation of the offset in the function
  // It's called only once when drag starts
  offset: ({ element, event, data }) => {
    // element: native element of draggable
    // event: pointerdown event
    // data: associated data
    return { top: 0, left: 0 };
  },

  // "onDragStart" is optional callback. It's called when drag interaction starts
  onDragStart: ({ event, dragStartEvent, element, data }) => {
    // event: MouseEvent from the pointermove handler
    // dragStartEvent: MouseEvent from the pointerdown handler
    // element: the element on which the drag interaction occurred
    // data: current data of the draggable
  },

  // "onDragMove" is called on every mouse move during the interaction. Don't put expensive logic here
  onDragMove: ({ event, dragStartEvent, element, data, dropTargets, top, left }) => {
    // dropTargets: an array of drop targets where the draggable is currently over
    // top and left: coordinates of rendered draggable element (not mouse coordinates)
  },

  // "onDragEnd" is called when drag interaction ends.
  // "dropTargets" will be empty array if the draggable wasn't dropped
  onDragEnd: ({ event, dragStartEvent, element, data, dropTargets }) => {
    // arguments are all the same as is "onDragMove" except "top" and "left"
  },

  // `mouseConfig` is an option from snapdrag/core
  // See the core documentation for it
  mouseConfig: undefined,

  // `plugins` is also an option from snapdrag/core
  plugins: undefined,
});

The isDragging prop from the hook is just a sugar over manually changing a state from onDragStart and onDragEnd (not exactly, it has some internal usage).

useDroppable

Like useDraggable, useDroppable takes a config and returns an object with two fields: draggable and hovered. To make your component react to drop interactions, wrap it with droppable. To define what draggable it should accept, define the required accepts field. It can be a string or symbol, an array of them, or a function (see docs below):

export const DroppableSquare = ({ color }: { color: string }) => {
  const { droppable, hovered } = useDroppable({
    accepts: "SQUARE",
    // other config fields are optional
  });

  const backgroundColor = hovered ? hovered.data.color : color;

  return droppable(
    <div className="square" style={{ backgroundColor }}></div>
  );
};

When the droppable is hovered by the corresponding draggable, the hovered returns its data and kind. Elsewhere, it's null.

As said above, the component can be wrapped both in draggable and droppable, the order doesn't matter.

useDroppable config

const { droppable, hovered } = useDroppable({
  // "accepts" defines the kinds of draggable items this droppable area can accept.
  // It can be a single kind, an array of kinds,
  accepts: "TASK",

  // or be a function that gets "kind" and "data" from draggable and returns bool
  accepts: ({ kind, data,  }) => kind === "TASK" && data.task.project === task.project,

  // "data" is optional and can be used to store additional information related to the droppable area.
  data: { maxCapacity: 5 },

  // "onDragIn" is called when a draggable item of an accepted kind enters the droppable area.
  onDragIn: ({ kind, data, event, element, dropElement, dropTargets }) => {
    // kind: the kind of the draggable
    // data: the data associated with the draggable
    // event: the MouseEvent associated with the drag
    // element: the element being dragged
    // dropElement: the droppable element
    // dropTargets: an array of current drop targets
    console.log(`Draggable ${kind} entered with data`, data);
  },

  // "onDragOut" is called when a draggable item leaves the droppable area.
  onDragOut: ({ kind, data, event, element, dropElement, dropTargets }) => {
    console.log(`Draggable ${kind} left with data`, data);
  },

  // "onDragMove" is called when a draggable item moves within the droppable area.
  onDragMove: ({ kind, data, event, element, dropElement, dropTargets }) => {
    console.log(`Draggable ${kind} moved with data`, data);
  },

  // "onDrop" is called when a draggable item is dropped within the droppable area.
  onDrop: ({ kind, data, event, element, dropElement, dropTargets }) => {
    console.log(`Draggable ${kind} dropped with data`, data);
  },
});

Author

Eugene Daragan

License

MIT