dnd-kit icon indicating copy to clipboard operation
dnd-kit copied to clipboard

Get Relative Top and Left of Draggable When Dragged Into Droppable

Open criticalcognition opened this issue 2 years ago • 10 comments
trafficstars

Examples

I have looked through the examples and documentation and could not determine how to do this. I found an example of dragging a Draggable that uses delta.x and delta.y to establish top and left coordinates but no Droppable:

https://master--5fc05e08a4a65d0021ae0bf2.chromatic.com/?path=/story/core-draggable-hooks-usedraggable--basic-setup

I found an example of dragging a Draggable from outside a Droppable into the Droppable, but it just makes the target Droppable the parent without explicitly determining where the Draggable was dropped inside the Droppable:

https://master--5fc05e08a4a65d0021ae0bf2.chromatic.com/?path=/story/core-droppable-usedroppable--basic-setup

Goal

I am trying to figure out how to get the relative top and left of a Draggable when it is dropped into a Droppable. This will allow the rendering of the item exactly where it was dropped. Think about how a WYSIWYG HTML designer would work. Here is some code to reproduce the issue. Note in the handleDragEnd event I am attempting to get the X and Y coordinates. I have looked through all of the properties of the event object passed into the handleDragEnd event but could not find this.

import React, { useState } from 'react';
import { DndContext, useDraggable, useDroppable } from '@dnd-kit/core';

  interface DraggableProps {
    id: string;
    children: React.ReactNode;
  }
  
  export function Draggable(props: DraggableProps) {
    const { id, children } = props;
    const { attributes, listeners, setNodeRef, transform } = useDraggable({
      id,
    });
  
    const style = {
        width: 100,
        height: 30,
        border: '1px solid',
        borderColor: 'black',
        transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : undefined,
    };
  
    return (
      <div ref={setNodeRef} style={style} {...listeners} {...attributes}>
        {children}
      </div>
    );
  }

  interface DroppableProps {
    id: string;
    children: React.ReactNode;
  }
  
  export function Droppable(props: DroppableProps) {
    const { id, children } = props;
    const { isOver, setNodeRef } = useDroppable({
      id,
    });
  
    return (
      <div ref={setNodeRef} id={id} style=
      {{
        width: 500,
        height: 500,
        border: '1px solid',
        borderColor: isOver ? '#4c9ffe' : '#EEE',
      }}
      >
        {children}
      </div>
    );
  }

  const FormEditor: React.FC = () => {

    // The state for the JSON data of the controls
    const [jsonData, setJsonData] = useState<any>({});

  const handleDragEnd = (event: any) => {
    const { over } = event;
    if (over) {
      // Add a new control to the JSON data with a unique id
      const id = `control-${Date.now()}`;
       setJsonData((data: any) => ({
        ...data,
        [id]: {
          type: event.active.id,
          x:   HOW DO I GET THIS?
          y:   HOW DO I GET THIS?
          width: 100,
          height: 30,
          text: event.activatorEvent.srcElement.textContent,
        },
      }));
    }
  };

  return (
    <div className="App" style={{display: 'flex', gap: 30}}>
      <DndContext onDragEnd={handleDragEnd}>
        <div id="htmlControls">
          <h1>HTML Controls</h1>
            <Draggable id="Button">Button</Draggable>
            <Draggable id="Input">Input</Draggable>
            <Draggable id="Label">Label</Draggable>
        </div>
          <div>
            <h1>Canvas</h1> 
            <Droppable id="droppableCanvas">
              {/* Render the controls from the JSON data */}
              {Object.keys(jsonData).map((id) => {
                const control = jsonData[id];
                return (
                  <div
                    key={id}
                    id={id}
                    className={`control ${control.type}`}
                    style={{
                      position: 'relative',
                      left: control.x,
                      top: control.y,
                      width: control.width,
                      height: control.height,
                      border: '1px solid',
                      borderColor: 'black',
                    }}
                  >
                    {control.text}
                  </div>
                );
              })}
            </Droppable>
          </div>
          </DndContext>
    </div>
  );

  };
  
  export default FormEditor;

criticalcognition avatar Oct 31 '23 15:10 criticalcognition

Currently trying to figure out the same thing, @criticalcognition were you able to find a solution?

abdullahfawad avatar Dec 06 '23 21:12 abdullahfawad

Here's how I ended up doing this (inside the drag end listener):

    const board = {droppable}.getBoundingClientRect();
    if (event.active.rect.current.translated) {
      {draggable}.position.x = event.active.rect.current.translated?.left - board?.x
      {draggable}.position.y = event.active.rect.current.translated?.top - board?.y
    }

Gets the draggable item's location and subtracts from it the droppable container's location (relative to the viewport). I do imagine there might be a native alternative to getBoundingClientRect() in the library itself, but for now this works perfectly fine for me.

Code Sandbox

abdullahfawad avatar Jan 01 '24 13:01 abdullahfawad

@abdullahfawad can you provide a more thorough example?

What is and how are you getting {droppable} and {draggable}?

atwright147 avatar Jan 02 '24 16:01 atwright147

@atwright147 By {draggable} and {droppable} I mean the drag item and the droppable container, the pair we need the relative top and left for. And yes, I just quickly made a code sandbox, I've attached the link to it in my original comment. Hope that helps!

abdullahfawad avatar Jan 02 '24 19:01 abdullahfawad

@abdullahfawad Thanks for the code sample. Unfortunately this does not address the original issue. In your example, your Draggable starts INSIDE the Droppable. What we are trying to do is figure out how to get the X and Y when the Draggable starts OUTSIDE the Droppable and then dragged INSIDE.

criticalcognition avatar Jan 02 '24 19:01 criticalcognition

@abdullahfawad sweeeeeet! That is what I thought it would be, cool. Thanks

atwright147 avatar Jan 02 '24 22:01 atwright147

Hi @criticalcognition, thank you for creating this issue. Were you able to solve this problem? If yes, it will be great if you can share your approach.

I have a similar situation where I drop an item from outside, into a droppable area, and want to create it where the user drops it.

I initially thought of using the mouse position at the instant the element is dropped, combined with snapCursorToCenter modifier for the DragOverlay and do some geometric calculations on the droppable object's size and position, from over as well as the size of the component to be created (after drop). But I don't see a way to get mouse coordinates in my DragEndEventHandler without adding a custom event handler. And I am not sure how good of a solution this is.

Any input will be appreciated!

akshat157 avatar Mar 30 '24 11:03 akshat157

@akshat157 - I'm sure that there is a solution but I have not found one yet. I'm thinking of eventually either using a different library or diving deep into the dnd-kit code to determine how the x and y mouse coordinates are calculated. Maybe it can't be accurately calculated and that's why @clauderic hasn't added it to the DragEndEventHandler.

criticalcognition avatar Apr 02 '24 15:04 criticalcognition

Thanks for getting back @criticalcognition! I thought of a potential solution. It is a little messy and naive, but could still be helpful, so I am posting it here. Note that I have not tested the complete solution so please take it with a grain of salt.

I am able to get mouse's global coordinates (w.r.t document.body), from which, subtracting the (left, top) of a droppable (over.rect.left, over.rect.top), will give you the relative coordinates of the mouse.

Here is a code snippet from my project.

  const myFunction = (event: MouseEvent) => {
    console.log(event.clientX, event.clientY)         // <-- Global mouse coordinates at the instant of drop.
  }
  const handleDragStart = (event: DragStartEvent) => {
    document.body.addEventListener('mouseup', myFunction)
  }
  const handleDragEnd = (event: DragEndEvent) => {
    document.body.removeEventListener('mouseup', myFunction)
    event.over && console.log(event.over.rect.left, event.over.rect.top)    // <-- subtract these from the values above to get the relative mouse coordinates
    event.active &&
      console.log(
        event.active.rect.current.translated?.width,      // <-- subtract the half of values from the mouse coordinates
        event.active.rect.current.translated?.height,     // to get the final relative coordinates of a draggable.
      )
  }

If we use the snapCursorToCenter modifier, to get the final relative left, top of a draggable, we can subtract width/2 and height/2 of the draggable's bounding rect from the mouse coordinates we got.

On a side note though, it seems like as of now, dnd-kit is extremely suitable for use cases like a kanban board or sortable lists, but is still a WIP in terms of a figma like - designer like use cases where the compnents can be drag-n-dropped freely. I too am thinking about moving to another library, since I need resizing and selection capabilities along with drag and drop too. https://github.com/daybrush/moveable is an appealing candidate.

akshat157 avatar Apr 02 '24 17:04 akshat157