在ReactJs中拖动Cursor位置的元素

问题描述 投票:0回答:1

我正在尝试用 React 构建一个游戏,它需要拖放功能。该游戏有两个主要组件:SideBar 和 GameBoard。 SideBar 包含要拖动到 GameBoard 上的元素。拖放有效,但元素被放置在错误的位置(与光标位置的偏移),我在网上查找了有关如何放置在光标位置的资源,但大多数答案仅使用第三个派对套餐和非派对套餐并没有真正的答案

您可以在此处查看以下代码的输出:https://gist.github.com/assets/31410839/48cbdaae-2323-461e-a660-286d0fc971bd

这是完整的代码

App.tsx 保存具有 DraggedElement 状态的 SideBar 和 GameBoard

// App.tsx
import React, { useState } from "react";
import GameBoard from "../src/components/GameBoard/GameBoard";
import SideBar from "../src/components/SideBar/SideBar";
import { ElementData } from "./Types";
function App() {
  const [draggedElement, setDraggedElement] = useState<ElementData | null>(null);
  return (
    <div className="min-h-screen flex flex-col md:flex-row bg-gray-100">
      <div className="md:w-1/5 bg-fuchsia-50 p-4">
        <h1 className="text-xl font-bold mb-4">Infinite Things ♾️</h1>
        <SideBar setDraggedElement={setDraggedElement} />
      </div>
      <div className="md:w-4/5 bg-amber-50 flex-grow">
        <GameBoard draggedElement={draggedElement} />
      </div>
    </div>
  );
}

export default App;

GameBoard.tsx

import React, { useState } from "react";
import Element from "../Element";
import { ElementData } from "../../Types";

interface GameBoardProps {
  draggedElement: ElementData | null;
}

const GameBoard: React.FC<GameBoardProps> = ({ draggedElement }) => {
  const [elements, setElements] = useState<ElementData[]>([
    { id: "Water", dx: 0, dy: 0, color: "#3498db" },
    { id: "Fire", dx: 0, dy: 0, color: "#e74c3c" },
    { id: "Wind", dx: 0, dy: 0, color: "#2ecc71" },
  ]);

  const [draggingElement, setDraggingElement] = useState<ElementData | null>(null);

  const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
    e.preventDefault();
    e.currentTarget.style.border = "none";

    if (draggedElement) {
      // Check if the dropped element is from the sidebar or internal drag
      const isExternalDrop = !draggingElement;  // Corrected this line

      if (isExternalDrop) {
        // External drop: add the dragged element from the sidebar
        const rect = e.currentTarget.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;

        const newElement = { ...draggedElement, dx: x, dy: y };
        setElements((prevElements) => [...prevElements, newElement]);
      } else {
        // Internal drop: update the position of the dragged element
        const rect = e.currentTarget.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;

        const updatedElement = { ...draggingElement, dx: x, dy: y };

        setElements((prevElements) =>
          prevElements.map((el) => (el.id === draggingElement.id ? updatedElement : el))
        );
      }
    }

    // Reset the dragging element in the state
    setDraggingElement(null);
  };


  const handleDragStart = (
    e: React.DragEvent<HTMLDivElement>,
    element: ElementData
  ) => {
    console.log("drag start", element.id);
    // Set the dragged element in the state
    setDraggingElement(element);

    // Set data to be transferred during the drag
    e.dataTransfer.setData("elementId", element.id.toString());
  };

  const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
    e.preventDefault();

    // Check if there's a dragging element
    if (draggingElement) {
      // Calculate new position based on cursor location
      const rect = e.currentTarget.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;

      // Update dx and dy in draggingElement (or state)
      setDraggingElement((prevElement) => {
        if (prevElement) {
          return { ...prevElement, dx: x, dy: y };
        }
        return prevElement;
      });
    }
  };


  const handleDragEnd = () => {
    // Reset the dragging element in the state
    setDraggingElement(null);
  };

  const handleDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
    e.preventDefault();
    e.currentTarget.style.border = "2px dashed #333";

    // Get the elementId from dataTransfer only if draggingElement is null
    if (!draggingElement && draggedElement) {
      console.log("drag enter", draggedElement?.id);
      setDraggingElement(draggedElement);
    }
  };

  const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
    e.currentTarget.style.border = "none";
  };

  return (
    <div
      className="game-board bg-gray-200 relative h-full p-4"
      onDrop={handleDrop}
      onDragOver={handleDragOver}
      onDragEnter={handleDragEnter}
      onDragLeave={handleDragLeave}
      onDragEnd={handleDragEnd}
    >
      <h2 className="text-xl font-bold mb-4">Game Board</h2>
      <div className="min-w-0 absolute">
        {elements.map((element) => (
          <Element
            key={element.id}
            element={element}
            onDragStart={(e) => {
              e.dataTransfer.setData("elementId", element.id.toString());
              handleDragStart(e, element);
            }}
          />
        ))}
      </div>
    </div>
  );
};

export default GameBoard;

容纳元素的侧边栏

// SideBar.tsx
import React from "react";
import Element from "../Element";
import { ElementData } from "../../Types";

interface SideBarProps {
  setDraggedElement: React.Dispatch<React.SetStateAction<ElementData | null>>;
}

const SideBar: React.FC<SideBarProps> = ({ setDraggedElement }) => {
  const elements: ElementData[] = [
    { id: "Water", dx: 0, dy: 0, color: "#3498db" },
    { id: "Fire", dx: 0, dy: 0, color: "#e74c3c" },
    { id: "Wind", dx: 0, dy: 0, color: "#2ecc71" },
  ];

  const handleDragStart = (
    e: React.DragEvent<HTMLDivElement>,
    element: ElementData
  ) => {
    console.log("drag start in Sidebar for", element.id);
    setDraggedElement(element);
  };

  return (
    <div className="sidebar">
      <h2 className="text-xl font-bold mb-4">Side Bar</h2>
      <div className="grid grid-cols-2 gap-4">
        {elements.map((element) => (
          <Element
            key={element.id}
            element={element}
            onDragStart={(e) => handleDragStart(e, element)}
          />
        ))}
      </div>
    </div>
  );
};

export default SideBar;

将被拖动的Element组件

// Element.tsx
import React from "react";
import { ElementData } from "../Types";
interface ElementProps {
  element: ElementData;
  onDragStart: (e: React.DragEvent<HTMLDivElement>, element: ElementData) => void;
}

function Element({ onDragStart, element }: ElementProps) {
  return (
    <div
      className={`element p-4 min-w-4 text-white text-center font-bold rounded-2xl shadow-lg cursor-pointer`}
      onDragStart={(e) => {
        onDragStart(e, element);
      }}
      style={{
        background: `${element.color}`,
        transform: `translate(${element.dx}px, ${element.dy}px)`,
      }}
      draggable
    >
      {element.id}
    </div>
  );
}

export default Element;

如果代码看起来很混乱,我很抱歉,我是 React 的初学者。

reactjs drag-and-drop draggable
1个回答
0
投票

handleDrop()
中的
GameBoard
中,您需要充分考虑正在重新定位的元素的起始位置。这是因为
translate()
相对于其“起始”位置移动元素。

// Get the cursor position, relative to the top-left corner of the `.game-board` element.
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;

// Get reference to the element of the inner `<div class="min-w-0 absolute">`
// the contains the draggables.
const inner = e.currentTarget.children[1];

// Find the index of the draggable we dropped.
const i = elements.findIndex(el => el.id === draggingElement.id);
// Get the DOM element of the draggable inside the game board.
const domElement = inner.children[i];
        
const updatedElement = {
  ...draggingElement,
  // Offset `dx` and `dy` values so they are relative to the "starting" position of the draggable.
  dx: x - inner.offsetLeft - domElement.offsetLeft,
  dy: y - inner.offsetTop - domElement.offsetTop,
};

const { useState, useRef } = React;

function Element({ onDragStart, element }) {
  return (
    <div
      className={`element p-4 min-w-4 text-white text-center font-bold rounded-2xl shadow-lg cursor-pointer`}
      onDragStart={(e) => {
        onDragStart(e, element);
      }}
      style={{
        background: `${element.color}`,
        transform: `translate(${element.dx}px, ${element.dy}px)`,
      }}
      draggable
    >
      {element.id}
    </div>
  );
}

const GameBoard = ({ draggedElement }) => {
  const [elements, setElements] = useState([
    { id: "Water", dx: 0, dy: 0, color: "#3498db" },
    { id: "Fire", dx: 0, dy: 0, color: "#e74c3c" },
    { id: "Wind", dx: 0, dy: 0, color: "#2ecc71" },
  ]);

  const [draggingElement, setDraggingElement] = useState(null);

  const handleDrop = (e) => {
    e.preventDefault();
    e.currentTarget.style.border = "none";

    if (draggedElement) {
      // Check if the dropped element is from the sidebar or internal drag
      const isExternalDrop = !draggingElement;  // Corrected this line

      if (isExternalDrop) {
        // External drop: add the dragged element from the sidebar
        const rect = e.currentTarget.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;

        const newElement = { ...draggedElement, dx: x, dy: y };
        setElements((prevElements) => [...prevElements, newElement]);
      } else {
        const rect = e.currentTarget.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;
        
        const inner = e.currentTarget.children[1];
        
        const i = elements.findIndex(el => el.id === draggingElement.id);
        const domElement = inner.children[i];
        
        const updatedElement = {
          ...draggingElement,
          dx: x - inner.offsetLeft - domElement.offsetLeft,
          dy: y - inner.offsetTop - domElement.offsetTop,
        };
        setElements((prevElements) => {
          return prevElements.map((el) => (el.id === draggingElement.id ? updatedElement : el))
        });
      }
    }

    // Reset the dragging element in the state
    setDraggingElement(null);
  };


  const handleDragStart = (
    e,
    element
  ) => {
    console.log("drag start", element.id);
    // Set the dragged element in the state
    setDraggingElement(element);

    // Set data to be transferred during the drag
    e.dataTransfer.setData("elementId", element.id.toString());
  };

  const handleDragOver = (e) => {
    e.preventDefault();

    // Check if there's a dragging element
    if (draggingElement) {
      // Calculate new position based on cursor location
      const rect = e.currentTarget.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;

      // Update dx and dy in draggingElement (or state)
      setDraggingElement((prevElement) => {
        if (prevElement) {
          return { ...prevElement, dx: x, dy: y };
        }
        return prevElement;
      });
    }
  };

  const handleDragEnd = () => {
    // Reset the dragging element in the state
    setDraggingElement(null);
  };

  const handleDragEnter = (e) => {
    e.preventDefault();
    e.currentTarget.style.border = "2px dashed #333";

    // Get the elementId from dataTransfer only if draggingElement is null
    if (!draggingElement && draggedElement) {
      console.log("drag enter", draggedElement.id);
      setDraggingElement(draggedElement);
    }
  };

  const handleDragLeave = (e) => {
    e.currentTarget.style.border = "none";
  };

  return (
    <div
      className="game-board bg-gray-200 relative h-full p-4"
      onDrop={handleDrop}
      onDragOver={handleDragOver}
      onDragEnter={handleDragEnter}
      onDragLeave={handleDragLeave}
      onDragEnd={handleDragEnd}
    >
      <h2 className="text-xl font-bold mb-4">Game Board</h2>
      <div className="min-w-0 absolute">
        {elements.map((element) => (
          <Element
            key={element.id}
            element={element}
            onDragStart={(e) => {
              e.dataTransfer.setData("elementId", element.id.toString());
              handleDragStart(e, element);
            }}
          />
        ))}
      </div>
    </div>
  );
};

const SideBar = ({ setDraggedElement }) => {
  const elements = [
    { id: "Water", dx: 0, dy: 0, color: "#3498db" },
    { id: "Fire", dx: 0, dy: 0, color: "#e74c3c" },
    { id: "Wind", dx: 0, dy: 0, color: "#2ecc71" },
  ];

  const handleDragStart = (
    e,
    element
  ) => {
    console.log("drag start in Sidebar for", element.id);
    setDraggedElement(element);
  };

  return (
    <div className="sidebar">
      <h2 className="text-xl font-bold mb-4">Side Bar</h2>
      <div className="grid grid-cols-2 gap-4">
        {elements.map((element) => (
          <Element
            key={element.id}
            element={element}
            onDragStart={(e) => handleDragStart(e, element)}
          />
        ))}
      </div>
    </div>
  );
};

function App() {
  const [draggedElement, setDraggedElement] = useState(null);
  return (
    <div className="min-h-screen flex flex-col md:flex-row bg-gray-100">
      <div className="md:w-1/5 bg-fuchsia-50 p-4">
        <h1 className="text-xl font-bold mb-4">Infinite Things ♾️</h1>
        <SideBar setDraggedElement={setDraggedElement} />
      </div>
      <div className="md:w-4/5 bg-amber-50 flex-grow">
        <GameBoard draggedElement={draggedElement} />
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('app')).render(<App/>);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js" integrity="sha512-8Q6Y9XnTbOE+JNvjBQwJ2H8S+UV4uA6hiRykhdtIyDYZ2TprdNmWOUaKdGzOhyr4dCyk287OejbPvwl7lrfqrQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js" integrity="sha512-MOCpqoRoisCTwJ8vQQiciZv0qcpROCidek3GTFS6KTk2+y7munJIlKCVkFCYY+p3ErYFXCjmFjnfTTRSC1OHWQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.tailwindcss.com/3.4.1"></script>

<div id="app"></div>

© www.soinside.com 2019 - 2024. All rights reserved.