konvajs / react-konva

React + Canvas = Love. JavaScript library for drawing complex canvas graphics using React.
https://konvajs.github.io/docs/react/
MIT License
5.8k stars 260 forks source link

Group Item when on top of another item #816

Closed harrysayers closed 1 week ago

harrysayers commented 3 weeks ago

Hello, I'm posting an issue as I've looked on StackOverflow, Git and the Docs and can't seem to find anyway of doing the following.

I have a stage with lots components (Shapes & HTML elements) when I drag one of these elements on to another I want to group those components together. I can't think of a way of doing this so any help would be appreciated.

This is the how my app looks

<App>
  <Stage>
      {
          arrayofelements.map(()=>{
            <comp1/>
            <comp2/>
              ....
        }
    }
  </Stage>
</App>
lavrton commented 3 weeks ago

How do you want to represent it in state? Additional state with groups? Or inside the same arrayofelements with some nested structure?

harrysayers commented 3 weeks ago

That is one of the things that i'm struggling with... Currently I have one array of objects that stores all components. I want one component (let's call it 'container') that when another object is dragged over the top it's grouped with the 'container'... I was thinking that there would be a property in the array called "grouped: bool" & "grouped_by: Id of the container" and you could use those to change how the components are rendered ? Again, struggling with this so any help is much appreciated.

harrysayers commented 2 weeks ago

@lavrton have you got any thoughts on this... have been trying to work this out for a few days to no avail.

Any help is much appreciated!

lavrton commented 2 weeks ago

I would suggest having different types of elements in the state. So you may have a "group" or "container" that may have children in it. On drag end, you can check overlaps and move an element into such container.

import { createRoot } from "react-dom/client";
import React, { useState } from "react";
import { Stage, Layer, Rect, Group } from "react-konva";

const App = () => {
  const [elements, setElements] = useState([
    {
      id: 1,
      type: "container",
      x: 50,
      y: 50,
      width: 100,
      height: 100,
      children: [],
    },
    { id: 2, type: "element", x: 200, y: 200, width: 50, height: 50 },
    { id: 3, type: "element", x: 300, y: 300, width: 50, height: 50 },
    // Add more elements as needed
  ]);

  const handleDragEnd = (id, newPos) => {
    setElements((prevElements) => {
      const draggedElement = prevElements.find((el) => el.id === id);
      const container = prevElements.find(
        (el) =>
          el.id !== id &&
          el.type === "container" &&
          isInsideContainer(newPos, el) // Check if the dragged item is inside this container
      );

      if (container) {
        // Move dragged element to the container’s children
        return prevElements
          .map((el) => {
            if (el.id === container.id) {
              return {
                ...el,
                children: [
                  ...el.children,
                  { ...draggedElement, x: newPos.x - el.x, y: newPos.y - el.y },
                ],
              };
            }
            return el.id === id ? null : el;
          })
          .filter(Boolean); // Filter out the dragged element from the main array
      }
      // If no container, update position as usual
      return prevElements.map((el) =>
        el.id === id ? { ...el, x: newPos.x, y: newPos.y } : el
      );
    });
  };

  const isInsideContainer = (pos, container) => {
    return (
      pos.x > container.x &&
      pos.x < container.x + container.width &&
      pos.y > container.y &&
      pos.y < container.y + container.height
    );
  };

  return (
    <Stage width={window.innerWidth} height={window.innerHeight}>
      <Layer>
        {elements.map((el) =>
          el.type === "container" ? (
            <Group
              key={el.id}
              x={el.x}
              y={el.y}
              draggable
              onDragEnd={(e) => handleDragEnd(el.id, e.target.position())}
            >
              <Rect width={el.width} height={el.height} fill="lightblue" />
              {el.children.map((child) => (
                <Rect
                  key={child.id}
                  x={child.x}
                  y={child.y}
                  width={child.width}
                  height={child.height}
                  fill="blue"
                />
              ))}
            </Group>
          ) : (
            <Rect
              key={el.id}
              x={el.x}
              y={el.y}
              width={el.width}
              height={el.height}
              fill="red"
              draggable
              onDragEnd={(e) => handleDragEnd(el.id, e.target.position())}
            />
          )
        )}
      </Layer>
    </Stage>
  );
};

export default App;

const container = document.getElementById("root");
const root = createRoot(container);
root.render(<App />);

https://codesandbox.io/p/sandbox/react-konva-drop-into-container-2q2d8k

harrysayers commented 2 weeks ago

Thanks you for this. I've nearly implemented this, however, there is a really strange bug/side effect happening with this approach...

Inside my container i'm iterating over the container children (like you have above) which looks like this.

  <Group
       key={el.id}
       x={el.x}
       y={el.y}
       draggable
       onDragEnd={(e) => handleDrag()}
  >
     <Rect width={el.width} height={el.height} fill="lightblue" />
     {el.children.map((child) => (
           if(child.type == 'a'){
               <compA item={child}/>
           }else if(child.type == 'b'){
               <compB item={child}/>
          }
      ))}
</Group>

the issue is that when I'm moving the child components (who have OnDrag functions as well) it's triggering the onDragEnd function of the container without me knowing as it's not updating on the canvas...

do you know why this is happening?

lavrton commented 2 weeks ago

This may help: https://konvajs.org/docs/events/Cancel_Propagation.html

harrysayers commented 1 week ago

I'm calling e.evt.cancelBubble = true; in my child mouse event however when I console.log the event cancelBubble is still set as false and not preventing the parent triggering.

harrysayers commented 1 week ago

Actually, I did a restart of everything and now it is working... Thank you very much for all your help @lavrton.

I will close this now and hopefully it will help someone else in the future.