我正在尝试用 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 的初学者。
在
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>