DEV Community

sheep
sheep

Posted on

Closing the Gap: Adding Drag-and-Drop for Bookmarks

It's been encouraging to see my browser extension, Bookmark Dashboard, has now reached 700+ users (500+ on Chrome, 200+ on Edge). Recently I have made a bunch of optimizations, and today I want to share how I implemented the drag-and-drop feature for bookmarks.
drag a bookmark to folder

Usually the browser's native bookmark manager already supports dragging and dropping bookmarks or folders to move them around. However, this convenient feature was missing in Bookmark Dashboard until now. Previously, moving bookmarks or folders mainly involved opening a modal, selecting the target location, and finally clicking a confirmation button. If I just want to drop a bookmark into a folder right next to it, there’s no denying that drag-and-drop is the more intuitive and convenient way.

To make Bookmark Dashboard embody all the strengths of the native bookmark manager, I decided to tackle this.

Drag and Drop API

HTML already provides the Drag and Drop API, which can be used to enable drag-and-drop functionality. React also offers properties like onDragStart, onDragEnd, and onDrop to handle drag-and-drop events.

Now let’s take a look at the implementation.

Making Bookmarks Draggable

Define onDragStart for the bookmark component, making it a draggable element. Then dragging bookmarks will trigger the handler immediately. Inside the handler, the bookmark’s ID can be passed via dataTransfer.

function BookmarkItem(props) {
  const { bookmarkData } = props;
  // ...
  return (
      <a
        href={bookmarkData.url}
        target='_blank'
        rel='noreferrer'
        onDragStart={(evt) => {
          evt.dataTransfer.setData('text/plain', bookmarkData.id);
        }}
      >
        /* display bookmark */
      </a>
  )
}
Enter fullscreen mode Exit fullscreen mode

Making Folders Droppable

Define onDrop for the folder component, making it a droppable element. Then drag a bookmark onto a folder and drop it will just trigger the handler. In the onDrop handler, first retrieve the bookmark ID from dataTransfer, then use the browser’s bookmarks API, passing the current folder’s ID as the parentId, to complete the move.

function Folder(props) {
  const { folderData } = props;
  // ...
  return (
      <div
          onDrop={(evt) => {
            const targetId = evt.dataTransfer.getData('text/plain');
            // just for chrome
            chrome?.bookmarks?.move(targetId, { parentId: folderData.id }, () => {});
          }}
      >
        /* display folder */
      </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Just like this, the basic drag-and-drop functionality is working!

Improving the UX with Visual Feedback

By now the user experience isn’t quite polished enough. I think a visual hint is needed when dragging a bookmark over a folder, like showing a border around the folder to indicate that dropping the bookmark will move it there.

The Drag and Drop API’s onDragEnter and onDragLeave are perfect for this. Add onDragEnter and onDragLeave to the folder component, then if a bookmark is dragged over the folder, set the border color to gray from onDragEnter. When the bookmark is dragged away, remove the border color from onDragLeave.

function Folder(props) {
  const { folderData } = props;
  // ...
  return (
      <div
          className='border border-transparent'
          onDragEnter={(evt) => {
            evt.currentTarget.style.borderColor = 'gray';
          }}
          onDragLeave={(evt) => {
            evt.currentTarget.style.borderColor = '';
          }}
          onDrop={(evt) => {
            evt.target.style.borderColor = '';
            const targetId = evt.dataTransfer.getData('text/plain');
            // just for chrome
            chrome?.bookmarks?.move(targetId, { parentId: folderData.id }, () => {});
          }}
      >
        /* display folder */
      </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

This makes it clear which folder the bookmark will be dropped into and helps prevent mistakes.

Solving the onDragLeave Trigger Issue

During testing, I noticed a problem: onDragEnter and onDragLeave weren’t just triggered on the folder component. They were also triggered on its child elements. This meant that if I dragged a bookmark over a folder, onDragEnter would fire, if I then moved the cursor over the folder’s title or action buttons (still inside the folder), onDragLeave would fire unexpectedly. This caused the folder border to flicker even though the bookmark was still technically over the folder.

This was an unexpected challenge. After looking into how other developers handled it, here’s the solution I went with:

const [isDragging, setIsDragging] = useState(false);

function BookmarkItem(props) {
  const { bookmarkData } = props;
  // ...
  return (
      <a
        href={bookmarkData.url}
        target='_blank'
        rel='noreferrer'
        onDragStart={(evt) => {
          evt.dataTransfer.setData('text/plain', bookmarkData.id);
          setIsDragging(true);
        }}
        onDragEnd={() => setIsDragging(false)}
      >
        /* display bookmark */
      </a>
  )
}

function Folder(props) {
  const { folderData } = props;
  // ...
  return (
      <div
          className={`border border-transparent ${isDragging ? 'dragging' : ''}`}
          onDragEnter={(evt) => {
            evt.currentTarget.style.borderColor = 'gray';
          }}
          onDragLeave={(evt) => {
            evt.currentTarget.style.borderColor = '';
          }}
          onDrop={(evt) => {
            evt.target.style.borderColor = '';
            const targetId = evt.dataTransfer.getData('text/plain');
            // just for chrome
            chrome?.bookmarks?.move(targetId, { parentId: folderData.id }, () => {});
          }}
      >
        /* display folder */
      </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

In the bookmark component’s onDragStart handler, I set an isDragging flag to indicate that a drag is in progress. I also defined onDragEnd for the bookmark component to reset isDragging to false when the drag ends. The isDragging flag is used to conditionally add a dragging class to the folder component. Then define the following CSS for the dragging class:

.dragging * {
  pointer-events: none;
}
Enter fullscreen mode Exit fullscreen mode

This way, while a bookmark is being dragged, the dragging class is applied to the folder, and all its child elements ignore mouse events. This prevents onDragEnter and onDragLeave from being triggered by the children. Once the drag ends, isDragging is reset to false, the dragging class is removed, and normal interaction resumes.

Now With these all, the drag-and-drop feature is ready for use!

Wrapping Up

Bookmark Dashboard has been updated to v0.4.1 for Chrome and Edge and what makes me even happier is that the Firefox version was also published just a few days ago.

Besides drag-and-drop, the latest release includes many other smaller features and optimizations. Feel free to give it a try and

Thanks for reading!

Top comments (0)