React框架下Drag && Drop 实践踩坑记录(1)

2,197 阅读4分钟

对于HTML5 Drag && Drop API 不熟悉的同学,可以先看下这篇文章,HTML5原生拖拽/拖放 Drag & Drop 详解。本文记录我在使用React框架在实现Drag && Drop 效果时所踩的坑。

在开发思维导图组件的时候,需要实现拖拽思维导图中的一个节点,拖动到图中另外一个节点上时,将源节点设为目标节点的子节点。为了开发这一功能,采用了HTML5 Drag && Drop API。在开发过程中有一些细节挺有意思的,在此分享出来。

我特地写了一个简单的Demo来一步步演示这个过程。这个demo很简单,仅仅是拖动一个正方形色块到另外一个正方形色块的上面。当鼠标在目标色块的区域内时,目标色块的背景色变为红色。

先写一个最简单的例子,这样简单的例子只是为了引出后面的问题。 运行效果如图:

import * as React from "react";
import * as cx from "classnames";
import "./demo.scss";

interface DemoProps {}

interface DemoState {
  // 是否Drag进入Dst 区域
  dragEnterDst: boolean;
}

export default class Demo1 extends React.Component<DemoProps, DemoState> {
  constructor(props) {
    super(props);

    this.state = {
      dragEnterDst: false
    };
  }

  onDragEnter = e => {
    this.setState({
      dragEnterDst: true
    });
  };

  onDragLeave = e => {
    this.setState({
      dragEnterDst: false
    });
  };

  render() {
    return (
      <div>
        <div className="src" draggable></div>

        <div
          className={cx("dst", {
            "drag-enter": this.state.dragEnterDst
          })}
          onDragEnter={this.onDragEnter}
          onDragLeave={this.onDragLeave}
        ></div>
      </div>
    );
  }
}

在这个简单的例子上继续加点料,假设目标色块包含一个子节点。接下来会发现,当鼠标移进目标色块区域时,触发了目标节点的onDragEnter事件,当鼠标移进目标色块的子节点色块时,触发了目标节点的onDragLeave事件。

代码如下:

export default class Demo extends React.Component<DemoProps, DemoState> {
  constructor(props) {
    super(props);

    this.state = {
      dragEnterDst: false
    };
  }

  onDragEnter = e => {
    console.log('onDragEnter');
    this.setState({
      dragEnterDst: true
    });
  };

  onDragLeave = e => {
    console.log('onDragLeave')
    this.setState({
      dragEnterDst: false
    });
  };

  render() {
    return (
      <div>
        <div className="src" draggable></div>

        <div
          className={cx("dst", {
            "drag-enter": this.state.dragEnterDst
          })}
          onDragEnter={this.onDragEnter}
          onDragLeave={this.onDragLeave}
        >
          <div className='dst-sub'/>
        </div>
      </div>
    );
  }
}

而对于目标节点即使有子节点,我想要的效果是移动到目标节点的子节点上时,目标节点的背景色依然是红色,只有当鼠标移出目标节点时,目标节点的红色背景色才消失。

怎么解决这个问题呢

方法一:

在 dst-sub 的css 里面设置 pointer-events: none; 这样当鼠标移动进入dst-sub时,dst上的onDragLeave事件不会被触发。

方法二: 在鼠标进入dst-sub时,出发了dst上的onDragLeave事件,在事件里面通过relatedTarget进行判断,如果relatedTarget是dst的自身或者其子元素,那么直接return

import * as React from "react";
import * as cx from "classnames";
import "./demo.scss";

interface DemoProps {}

interface DemoState {
  // 是否Drag进入Dst 区域
  dragEnterDst: boolean;
}

export default class Demo extends React.Component<DemoProps, DemoState> {
  constructor(props) {
    super(props);

    this.state = {
      dragEnterDst: false
    };
  }

  onDragEnter = e => {
    console.log('onDragEnter');
    console.log(e.nativeEvent.target);
    this.setState({
      dragEnterDst: true
    });
  };

  onDragLeave = e => {
    console.log('onDragLeave');
    let target = e.nativeEvent.target;
    let relatedTarget = e.nativeEvent.relatedTarget;
    // console.log(this.dst);
    console.log(target);
    console.log(relatedTarget);
    if(this.dst==relatedTarget || this.dst.contains(relatedTarget)) {
      return;
    }
    console.log('onDragLeave: set state false');
    this.setState({
      dragEnterDst: false
    });
  };

  dst;
  dstRef = (ref)=> {
    this.dst = ref;
  };

  render() {
    return (
      <div>
        <div className="src" draggable></div>

        <div
          className={cx("dst", {
            "drag-enter": this.state.dragEnterDst
          })}
          onDragEnter={this.onDragEnter}
          onDragLeave={this.onDragLeave}
          ref={this.dstRef}
        >
          <div className='dst-sub'>
            <div className='dst-sub-sub'/>
          </div>
        </div>
      </div>
    );
  }
}

方法三:

可以利用onDragOver事件,DragOver事件只有鼠标在目标区域里面时就会一直触发。用DragOver事件之后我把代码改造成这样

export default class Demo extends React.Component<DemoProps, DemoState> {
  constructor(props) {
    super(props);

    this.state = {
      dragEnterDst: false
    };
  }

  onDragOver = () => {
    if(!this.state.dragEnterDst) {
      this.setState({
        dragEnterDst: true
      })
    }
  }

  onDragLeave = (e) => {
    console.log('onDragLeave');
    e.preventDefault();
    this.setState({
      dragEnterDst: false
    });
  };

  render() {
    return (
      <div>
        <div className="src" draggable/>
        <div
          className={cx("dst", {
            "drag-enter": this.state.dragEnterDst
          })}
          onDragOver={this.onDragOver}
          onDragLeave={this.onDragLeave}
        >
          <div className='dst-sub'></div>
        </div>
      </div>
    );
  }
}

因为onDragOver事件一直被触发,

onDragOver = () => {
    if(!this.state.dragEnterDst) {
      this.setState({
        dragEnterDst: true
      })
    }
  }

onDragOver函数写成这样,只有当鼠标移进dst-sub区域时,onDragLeave函数将dragEnterDst设置成false,这个时候onDragOver函数会再度将dragEnterDst设置成true。

在Mac OSX 系统的Chrome浏览器运行这个demo,确实达到了我想要的效果

但是在windows 10 系统的Chrome浏览器运行这个demo, onDragLeave函数将dragEnterDst设置成false,这个时候onDragOver函数会立即再度将dragEnterDst设置成true,我看到源色块的背景色发生了一下抖动,由红变橙再变红,虽然这一过程极为短暂,但还是肉眼能够看到。现在我还暂时搞不明白为什么在不同操作系统的Chrome浏览器上运行同样的demo代码视觉表现效果会不一样,如果你们知道这是什么原因造成的,欢迎留言告诉我原因。