bokuweb / react-rnd

🖱 A resizable and draggable component for React.
https://bokuweb.github.io/react-rnd/stories
MIT License
3.93k stars 324 forks source link

enhancement: don't allow overlap #86

Open dexluce opened 7 years ago

dexluce commented 7 years ago

Hello,

I see a few way to restrict the overlaping of two rnd elements, but it would be nice to just set a boolean for that.

As I need this, could you tell me how you would do that? (Do I use onDrag callback to verify if overlap is happening and prevent it?)

Thank you very much for the nice work, regards

alexdemartos commented 7 years ago

That would be really nice, actually :)

dexluce commented 7 years ago

Well, I wasn't able to do it by myself. onDrag and onResize doesn't allow the use of any function on the rnd element. For exemple, this.rnd.updatePosition(... ) won't work in onDrag or onResize. Changing "bounds" doesn't works neither.

So... I change my approach. I have implemented a 'magnet' onResizeStop and onDragStop. I juste write quickly the psedo-code. Magnet is only apply on X axe. I don't have the time know but let my know and I'll write a more usable code then this:

var allDivBorderYouWantToMagnetize = {}
var magneticPos;

onDragStart(() => {
  //here, you want to keep all x magnetic value as key in an object.
  forEach(rnd in rndElements) {
    this.allDivBorderYouWantToMagnetize[rnd.xPosition].push(rnd)
  }
})
onDrag((rnd) => {
  Object.keys(this.allDivBorderYouWantToMagnetize).forEach((key) => {
    //magnet will be activated if you are 5px around another rnd element
    if (rnd.x - key > -5 && rnd.x - key < 5)
      this.magneticPos = key;
    else
      this.magneticPos = event.xPos;
  })
})
onDragStop(() => {
  this.rnd.setPosition({x: this.magneticPos})
})
alexdemartos commented 7 years ago

With that approach, the object only gets moved into its place after you've finished dragging. I'm trying to accomplish the same thing, but also while you're dragging the object (no luck yet).

I would need something like "return false" on "onDrag" to cancel the movement, or a way to access the React object inside "onDrag" to call setPosition if the overlap condition is met.

subeax commented 7 years ago

Hi guys, any updates on this?

pingbaohua commented 6 years ago

您好,我想问一下防止rnd重叠现在有实现吗

arthurepc commented 5 years ago

Hello! Did anyone find a way to solve this problem? I'm in need of the exact same thing

davvit commented 3 years ago

Hi, here is my proposal; for 2 Rnds for now - some issue with more than 2 - i suspect issue with the loop. https://codesandbox.io/s/react-rnd-test-lcp7b

Call this method whenever you need to check for collision. In my demo im running it when onDragStop( ) and onResizeStop( ) are triggered.

 function overlaps() {
    //get all draggables in the doc - should be done using refs etc in react  
    var draggables = document.getElementsByClassName("react-draggable");

   //iterate through all of them checking overlap.
    Array.from(draggables).map((item, e) => {
      Array.from(draggables).map((oItem, e) => {

        if (item !== oItem) {
          const rect1 = item.getBoundingClientRect();
          const rect2 = oItem.getBoundingClientRect();

          const isInHoriztonalBounds = rect1.x < rect2.x + rect2.width && rect1.x + rect1.width > rect2.x;
          const isInVerticalBounds = rect1.y < rect2.y + rect2.height && rect1.y + rect1.height > rect2.y;

          var isOverlapping = isInHoriztonalBounds && isInVerticalBounds;

          //right here you know the two components that are overlapping
          //resolve overlap the way best fits your project.

          //setting state.
          setCollision(isOverlapping);
        }
      });
    });
  }
Rados51 commented 2 years ago

This code handles collision detection and restoration to the last known "safe point" or default position when drag ends (if there is a collison of the elements).

/* This will handle our overlap calculations
In my code, I have this above component that handles single node */
function haveIntersection(other, main) {
    return !(
        main.x > other.x + other.width ||
        main.x + main.width < other.x ||
        main.y > other.y + other.height ||
        main.y + main.height < other.y
    );
}
/*
* mn872 is a class of each nested div, that holds text / icons
* Structure looks like this map > div.wrap > Rnd > div.wrap2 > div.mn872 > .... (simplified)
* We have to use .some() as it is not possible to stop forEach loop
* We need to stop loop, because next element would override the state to false
* You can also use .every(), but in that case you will flip return values
* setIsCollision() is just regular useState(bool)
*/
const [isCollision, setIsCollision] = useState(false);
function handleOverlap(node, xy) {
    const main = node?.querySelector(".mn872"); // current dragged or resized node
    const targetRect = main?.getBoundingClientRect();
    [...document.querySelectorAll(".mn872")].some((group) => {
        if (group === main) return; // continue with a loop if the current element is inside the group
        if (haveIntersection(group.getBoundingClientRect(), targetRect)) {
            setIsCollision(true);
            return true; // current element is overlapping - stop loop
        }
               setIsCollision(false);
               setSafePoint(xy); // remove this line if you want to snap to initial position
               return;  // continue with a loop - current element is NOT overlapping
    });
}
const [safePoint, setSafePoint] = useState({ x: 0, y: 0 });
function handleDragStart(e, { x, y }) {
    setSafePoint({ x, y });
}
/* We use 1ms timeout as browser needs a tiny bit of time to have everything in sync.
On the other hand, we need to have correct values. Try it without timeout and you will see.
I call this function from Rnd component during "onDrag" event. It should work for other events too. */
function handleDrag(e, { node, x, y }) {
    setTimeout(() => handleOverlap(node, { x, y }), 1);
}
function handleDragStop(e, { node: { clientWidth } }) {
    if (isCollision) {
        nodeRef.current.updatePosition(safePoint);
        setIsCollision(false);
        return;
    }

    //const width = clientWidth;
    //nodeRef.current.updateSize({ width, height: TL_DEFUALT_HEIGHT });
        const { x, y } = nodeRef.current.draggable.state;
        nodeRef.current.updatePosition({ x, y });
}

Some parts token from https://konvajs.org/docs/sandbox/Collision_Detection.html

sandeepzgk commented 1 year ago

@Rados51 can you help with an example on codesandbox or anything else that would help out a newbie on react and react-rnd

Rados51 commented 1 year ago

@sandeepzgk

This is the most basic example (check console.log on collision) https://codesandbox.io/s/react-rnd-collision-example-4impqy?file=/src/index.js

If you need material to start working with React, I would recommend Scrimba

nicollegah commented 1 year ago

I would also really love this to be implemented.

carterjfulcher commented 1 year ago

I second that it'd be great to extend the bounds prop to disallow overlapping

AdityaT-19 commented 4 months ago

@Rados51 how do I get the nodeRef ??

Rados51 commented 4 months ago

@AdityaT-19 With useRef from React.

But you can also have an external position and size state instead of directly updating its postion. If needed, you can also get x/y state (originally from nodeRef.current.draggable.state) via second argument (e: DraggableEvent, data: DraggableData).