HTML5实战与剖析之原生拖拽---React中拖拽组件实现

前言: 拖拽组件是在前端开发中十分常见的一个功能,现在无论你是使用React还是Vue,都有很多现成的拖拽组件可以使用。不过,有些时候你可能还是需要自己去实现,那么就必须需要理解其实现原理。

背景: 这周接了个任务, 将antD的穿梭框做成可拖拽的(左右都可拖拽, 且支持多选拖拽)。看了下antD的api, 发现没有配置, 就决定自己写个原生实现了, 借鉴了大佬的demo, 看了一下拖拽是如何实现的, 再应用在项目中了。

了解HTML的拖拽

现如今,大部分的前端拖拽组件都依托于HTML5原生提供的拖放接口。那么在开始用具体框架来封装组件的之前,就需要搞清楚这些原生的接口功能。

HTML 5的DOM鼠标事件中添加了drag这个事件。对于一个设置了draggable属性的页面元素来说,只要将其拖动到一个同样带有droppable属性的元素上,就算完成了一次完整的拖放功能。在这一过程中,会分别触发一些如下事件类型: 

提示: 链接和图片默认是可拖动的,不需要 draggable 属性。

draggable有三个值,如下所示:

draggable = true(元素可以被拖动)

draggable = false(元素不能被拖动)

draggable = auto(浏览器可以自主决定某个元素是否可以被拖动)

被拖拽元素触发的事件 (源元素):

* ondragstart - 当一个元素开始被拖拽的时候触发 (按下鼠标键并开始移动鼠标的时候触发dragstart事件)
* ondrag - 元素正在拖动时触发 (在元素被拖动期间会持续触发drag事件, 与mousemove和touchmove事件类似)
* ondragend - 用户完成元素拖动后触发, 当拖动停止时 (无论把元素放到了有效的放置目标,还是放到了无效的放置目标上),都会发生dragend事件。

说明: 默认情况下,浏览器不会再拖动期间改变被拖动元素的外观。但是可以自行修改。不过,大多数浏览器会为正被拖动的元素创建一个半透明的副本,这个副本始终跟随光标移动。当某个元素被拖动到一个有效的放置目标的时候, 会触发下列事件:

释放拖拽元素时触发的事件(目标元素)

* ondragenter - 当被鼠标拖动的对象进入其容器范围内时触发此事件 (类似于mouseover事件)
* ondragover - 当被鼠标拖动的对象移动经过一个元素时触发(会连续触发dragover事件, 每100毫秒触发一次, 类似于mousemove事件)
* ondragleave - 当被鼠标拖动的对象离开其容器范围内时触发 (类似于mouseout事件)
* ondrop - 在拖拽操作结束释放时于释放元素上触发

需要注意:dragenter,dragover(dragend)事件下我们需要阻止浏览器的默认行为,让我们拖拽的元素成为可释放的元素。

熟悉这些基本事件类型后,实现上就是在源对象和目标对象上分别绑定对应的事件处理函数,并监听处理即可。

除了这些拖放的事件接口外,我们通常还需要处理数据的传递。HTML5中同样提供了简便的接口,在对应的监听函数内,我们可以拿到event对象,在这个对象内部有个DataTransfer接口,可专门用来保存事件的数据内容。DataTransfer对应的方法有:

拖拽携带的数据处理

* event.dataTransfer.setData(format, data)      添加拖拽数据,这个方法接收两个参数,第一个参数是数据类型(可自定义, 只能填入类似“text/plain”或“textml”的表示 MIME类型的文字),第二个参数是要携带的数据;

* event.dataTransfer.getData(format)      反向操作,获取数据,只接收一个参数,即数据类型;

* event.dataTransfer.clearData(format)     清除数据;从dataTransfer对象中删除指定格式的数据,参数可选,若不给参数,将删除对象中所有的数据。

* event.dataTransfer.setDragImage(el, x, y)     可自定义拖放过程中鼠标旁边的图像; 设置拖放操作的图标,其中el代表自定义图标,x代表图标与鼠标在水平方向上的距离,y代表图标与鼠标在垂直方向上的距离

下面就是一个简单的demo了~

import React, { useState, useRef } from 'react';

const list = [
  {
    uid: '1',
    text: '序列1',
  },
  {
    uid: '2',
    text: '序列2',
  },
  {
    uid: '3',
    text: '序列3',
  },
  {
    uid: '4',
    text: '序列4',
  },
  {
    uid: '5',
    text: '序列5',
  },
];

const Drag: React.FC<{}> = () => {
  // console.log('list', list)
  const [rightList, setRightList] = useState(list);
  const [leftList, setLeftList] = useState([]);

  //鼠标华划过接受拖拽元素的事件
  const handleDrop = (callBack, e, arrow) => {
    e.preventDefault();  //阻止默认事件:防止打开拖拽元素的url(Firefox)
    console.log('handleDrop', callBack, e, arrow);
    const {
      dataset: { id },
    } = e.target;
    console.log('id', id);

    const curData = JSON.parse(e.dataTransfer.getData('itemData'));
    console.log('curData', curData);
    callBack((prevData) => {
      console.log('prevData', prevData);
      const diffData = prevData.filter((item) => item.uid !== curData.uid);
      // id 不存在是在不同盒子内拖拽  存在则是在本身盒子内拖拽
      // 项目中发现, 只要鼠标经过元素, 且元素被自定义过data-id属性, id都可以都能拿到!
      if (!id) return [...diffData, curData];
      // 找到鼠标划过的目标元素的其盒子内的位置
      const index = diffData.findIndex((item) => item.uid === id);
      //把拖拽元素放置在鼠标划过元素的上方
      diffData.splice(index, 0, curData);
      return diffData;
    });
    //朝左拖拽
    if (arrow === 'left') {
      setRightList((prvData) => prvData.filter((item) => item.uid !== curData.uid));
    }
    // 朝右拖拽
    else {
      setLeftList((prvData) => prvData.filter((item) => item.uid !== curData.uid));
    }
  };
  // 拖拽元素进入目标元素时触发事件-为目标元素添加拖拽元素进入时的样式效果
  const handleDragEnter = (e) => e.target.classList.add('over');

  // 拖拽元素离开目标元素时触发事件-移除目标元素的样式效果
  const handleDragLeave = (e) => e.target.classList.remove('over');

  return (
    <div>
      <div
        style={
   
   { width: '300px', height: '300px' }}
        onDragOver={(e) => {
          e.preventDefault();
        }}
        onDrop={(e) => handleDrop(setLeftList, e, 'left')}
      >
        {leftList.map((item) => (
          <div
            className="item"
            draggable
            key={item.uid}
            data-id={item.uid}
            onDragStart={(e) => {
              e.dataTransfer.setData('itemData', JSON.stringify(item));
            }}
          >
            {item.text}
          </div>
        ))}
      </div>
      <div
        style={
   
   { width: '300px', height: '300px' }}
        onDragOver={(e) => {
          e.preventDefault();
        }}
        onDrop={(e) => handleDrop(setRightList, e, 'right')}
        onDragEnter={handleDragEnter}
        onDragLeave={handleDragLeave}
      >
        {rightList.map((item) => (
          <div
            className="item"
            draggable
            key={item.uid}
            data-id={item.uid}
            onDragStart={(e) => {
              e.dataTransfer.setData('itemData', JSON.stringify(item));
            }}
          >
            {item.text}
          </div>
        ))}
      </div>
    </div>
  );
};

export default Drag;

额外的样式处理 (如需要自定义样式处理时再看)

添加拖放效果

要实现拖放的视觉效果,需要effectAllowed和dropEffect两个属性结合起来使用。

dropEffect:设置拖放目标允许发生的拖放行为,如果此处设置的拖放行为不在effectAllowed属性设置的可拖放行为内,拖放操作将会失败。该属性值只允许为"null"、"copy"、"link"或"move";

effectAllowed:设置拖动元素允许发生的拖动行为,该属性值可为"none"、"copy"、"copyLink"、"copyMove"、"link"、"linkMove"、"move"、"all"或"uninitialized";

// 先设置一些效果常量:(内置属性) 
// 可以通过props传递

export const All = "all";

export const Move = "move";

export const Copy = "copy";

export const Link = "link";

export const CopyOrMove = "copyMove";

export const CopyOrLink = "copyLink";

export const LinkOrMove = "linkMove";

export const None = "none";


// 被拖拽组件
const Drag = (props) => {

    // 如果想自定义拖拽图像(非必须)
    const [isDragging, setIsDragging] = React.useState(false);
    const image = React.useRef(null);
    React.useEffect(() => {
        image.current = null;
        if (props.dragImage) {
            image.current = new Image();
            image.current.src = props.dragImage;
        }

    }, [props.dragImage]);

    // 拖拽开始
    const startDrag = e => {
        setIsDragging(true);
        // 设置数据(必须)
        e.dataTransfer.setData("drag-item", props.dataItem);
        // 设置effectAllowed属性添加效果(非必须)
        e.dataTransfer.effectAllowed = props.dropEffect;
        // 设置图片(非必须)
        if (image.current) {
            e.dataTransfer.setDragImage(image.current, 0, 0);
        }
    };

    // 拖拽结束
    const dragEnd = () => setIsDragging(false);  

    return (
        {props.children}
    );
}


// 目标组件
const DropTarget = (props) => {

    // 滑过时
    const dragOver = e => {
        // 阻止默认行为
        e.preventDefault();
        // 添加效果(非必须)
        e.dataTransfer.dropEffect = props.dropEffect;
    }

    // 进入时(非必须)
    const dragEnter = e => {
        e.dataTransfer.dropEffect = props.dropEffect;
    }

    // 释放时
    const drop = e => {
        // 获取数据(必须)
        const droppedItem = e.dataTransfer.getData("drag-item");
        // 触发回调函数(必须)
        if (droppedItem) {
            props.onItemDropped(droppedItem);
        }
    }

    return (
        {props.children}
    )

}

// 样式(可以根据isDragging来进行判断)
const draggingStyle = {
    opacity: 0.25,
};

【浏览器支持】

目前只有Internet Explorer 9、Firefox、Opera 12、Chrome 以及 Safari5支持拖放,在 Safari5.1.2 中不支持拖放。

关于自定义放置目标以及浏览器兼容性的一点说明

  在拖动元素经过某些无效放置目标的时候,可以看到一种特殊鼠标手势(圆环中一条反斜线),表示不能放置。虽然所有元素都支持放置目标事件,但是这些元素默认是不允许放置的。如果拖动元素经过不允许放置的元素,无论用户如何操作,都不会发生drop事件。不过,你可以把任何元素变成有效的放置目标,方法是重写dragenter和dragover事件的默认行为(event.preventDefault)。

  重写了默认行为之后,就会发现当拖动着元素移动到放置目标上的时候,光标变成允许放置的符号。在Firefox 3.5+中,放置事件的默认行为是打开被放到放置目标上的url。如果是把图像拖到放置目标上,页面就会转向图像文件。如果是把文本拖放到放置目标上,则会导致无效url错误。所以为了让Firefox支持政正常的拖放,还要取消drop事件的默认行为,阻止打开拖拽元素的URL。

收获:

1.原生H5拖拽事件的学习, 还有其他原生事件的熟悉

2. react Hooks+ TS的熟悉

3. 拖拽与数据的禁用处理逻辑

不足:

1. 多选拖拽的样式还是没有实现

2.对原生事件还是不够熟悉

参考: https://blog.csdn.net/weixin_45750771/article/details/125546827

https://blog.csdn.net/weixin_35521120/article/details/113522235

猜你喜欢

转载自blog.csdn.net/BUG_CONQUEROR_LI/article/details/126758117