您如何使用状态变量在 React / 看板中进行嵌套拖放,但在 React 中不起作用

How do you use state variables to make a nested drag and drop in React / Kanban board not working in React

上下文:我试图解决的主要问题是停止 onDragStart 事件处理程序(通过 e.stopPropagation())的传播会一起禁用拖放功能。但是,当我尝试在状态中设置标志时,停止在父元素上触发拖放事件;标志不起作用/状态未及时设置或其他原因。

设置:看板样式组件,具有包含可拖动卡片的可拖动列。

当您拖动列并对其重新排序时,这些列能够正确呈现。这是通过在 stateHook 中有一个列数组来完成的,当列的“onDragStart”事件被触发时,在状态中设置 draggingIndex。然后,当另一列触发“onDragOver”事件时,列数组被拼接,以将拖动列从其原始位置移除并将其插入到新顺序中。这工作正常。

当我尝试拖动卡片而不是列时出现问题。当“onDragStart”事件在卡片上触发时,我在状态挂钩中设置了一个标志。 setDragCategory(“卡片”)。列元素上的事件侦听器应该检查“dragCategory === 'card'”是否存在。如果确实如此,他们应该退出函数而不是 运行 任何代码。

目标是当您开始在列上拖动时,它的所有事件侦听器都会正常触发。但是,如果您开始在卡片上拖动,则列的事件侦听器实际上会在它们执行任何操作之前通过退出来停用。

即使卡片上的“onDragStart”处理程序首先 运行ning(状态设置为 dragCategory ===“card”),它也不会阻止列的事件处理程序 运行ning。列的事件处理程序然后设置 dragCategory ===“列。”因此,我尝试拖动卡片,但列正在重新排序。

我不明白为什么专栏的事件侦听器在发生这种情况之前没有退出它们的处理程序。

谢谢指点!

如果您将此代码直接粘贴到 create-react-app 项目的 App.js 文件中,该代码应该可以工作。

App.js:

import React, { useState } from "react";
import { v4 as uuid } from "uuid";

import "./App.css";

const data = {};
data.columns = [
  { name: "zero", cards: [{ text: "card one" }, { text: "card two" }] },
  { name: "one", cards: [{ text: "card three" }, { text: "card four" }] },
  { name: "two", cards: [{ text: "card five" }, { text: "card six" }] },
  { name: "three", cards: [{ text: "card seven" }, { text: "card eight" }] },
  { name: "four", cards: [{ text: "card nine" }, { text: "card ten" }] },
  { name: "five", cards: [{ text: "card eleven" }, { text: "card twelve" }] },
];

function App() {
  // when a card starts to drag, dragCategory is set to "card."  This is supposed to be a flag that will stop the columns' event listeners before they cause any changes in the state.  
  let [dragCategory, setDragCategory] = useState(null);

  // all of this is for reordering the columns. I have not gotten it to work well enough yet, to be able to write the state for reordering the cards:
  let [columns, setColumns] = useState(data.columns);
  let [draggingIndex, setDraggingIndex] = useState(null);
  let [targetIndex, setTargetIndex] = useState(null);

  return (
    <div className="App">
      <header>drag drop</header>
      <div className="kanban">
        {columns.map((column, i) => {
          return (
            <div
              data-index={i}
              onDragStart={(e) => {
                console.log("column drag start");
                // ERROR HERE: this function is supposed to exit here, if the drag event originated in a "card" component, but it is not exiting.
                if (dragCategory === "card") {
                  e.preventDefault();
                  return null;
                }
                setDragCategory("column");
                setDraggingIndex(i);
              }}
             // using onDragOver instead of onDragEnter because the onDragEnter handler causes the drop animation to return to the original place in the DOM instead of the current position that it should drop to.
              onDragOver={(e) => {
                if (dragCategory === "card") return null;
                // allows the drop event
                e.preventDefault();
                setTargetIndex(i);
                if (
                  dragCategory === "column" &&
                  targetIndex != null &&
                  targetIndex != draggingIndex
                ) {
                  let nextColumns = [...columns];
                  let currentItem = nextColumns[draggingIndex];
                  // remove current item
                  nextColumns.splice(draggingIndex, 1);
                  // insert item
                  nextColumns.splice(targetIndex, 0, currentItem);
                  setColumns(nextColumns);
                  setTargetIndex(i);
                  setDraggingIndex(i);
                }
              }}
              onDragEnter={(e) => {}}
              onDragEnd={(e) => {
                setDragCategory(null);
              }}
              onDrop={(e) => {}}
              className="column"
              key={uuid()}
              draggable={true}
            >
              {column.name}
              {column.cards.map((card) => {
                return (
                  <div
                    onDragStart={(e) => {
                      
                      setDragCategory("card");
                    }}
                    key={uuid()}
                    className="card"
                    draggable={true}
                  >
                    {card.text}
                  </div>
                );
              })}
            </div>
          );
        })}
      </div>
    </div>
  );
}

export default App;

并将此启动程序 css 粘贴到 App.css 文件中。

App.css

.kanban {
  display: flex;
  height: 90vh;
}

.column {
  border: solid orange 0.2rem;
  flex: 1;
}

.card {
  height: 5rem;
  width: 90%;
  margin: auto;
  margin-top: 2rem;
  border: solid gray 0.2rem;
}

您遇到这个问题是因为状态更新是异步的。

https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous

在您的 onDragStart 事件侦听器正在检查是否 dragCategory === "card" 状态更改尚未发生的列中。这就是不满足条件的原因。

要解决您的问题,您需要在卡片元素的 onDragStart 中添加 event.stopPropagation()。 这样,当您拖动卡片时,您的 onDragStart 列根本不会触发。

像这样:

onDragStart={(e) => {
    e.stopPropagation();
    setDragCategory('card');
}}

此外,如果你有多个状态相互依赖,reducers 更合适。

https://reactjs.org/docs/hooks-reference.html#usereducer

我有一点时间,所以我使用 reducer 而不是 state 创建了一个 codesandox 来解决这个问题。

可以进行改进,但我没有更多时间,我认为它可以让您走上正轨。

有时 onDragEnd 不会为卡片触发。我不知道为什么。

这就是为什么有时卡片会保持带有虚线的拖动样式。发生这种情况时,它会停止正常工作:/

https://codesandbox.io/s/zealous-kate-ezmqj

编辑: 这是一个可能对您有帮助的图书馆:

https://github.com/atlassian/react-beautiful-dnd

下面是实现的库示例:

https://react-beautiful-dnd.netlify.app/iframe.html?id=board--simple