如何使用React来开发拖拽组件

拖拽组件是在前端开发中十分常见的一个功能,现在无论你是使用React还是Vue,都有很多现成的拖拽组件可以使用。不过,有些时候你可能还是需要自己去实现,那么就必须需要理解其实现原理。接下来这篇文章,我将详细介绍如何使用React框架来实现一个拖拽组件。

理解HTML5 拖放API

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

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

事件类型 事件处理函数 含义
drag ondrag 拖放进行中
dragend/dragstart ondragend/ondragstart 开始拖放和结束拖放
dragover ondragover 当元素或选中的文本被拖到一个目标目标上(每100毫秒触发一次)。
dragenter/dragleave ondragenter/ondragleave 源对象开始进入/离开目标对象范围内
drop ondrop 源对象被拖放到目标对象上

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

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

  • event.dataTransfer.setData: 添加拖拽数据,这个方法接收两个参数,第一个参数是数据类型(可自定义),第二个参数是对应的数据
  • event.dataTransfer.getData:反向操作,获取数据,只接收一个参数,即数据类型
  • event.dataTransfer.clearData: 清除数据
  • event.dataTransfer.setDragImage: 可自定义拖放过程中鼠标旁边的图像
  • event.dataTransfer.effectAllowed: 指定拖放操作所允许的一个效果,有多个属性值,如link, move等,具体可参考https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer/effectAllowed

了解完这些基本接口后,我们就可以着手使用React来编写自己的拖放组件了:

实现Drag组件

我们第一个要实现的是Drag组件,它会作为我们的源对象,其子组件都可以进行拖动。就像这样:

<Drag dataItem="item">
    <div>这个组件可以拖动</div>
</Drag>

我们先来实现最基础的功能,通过setData接口来传递数据:

const Drag = (props) => {
    
    const startDrag = ev => {
        // 传输数据
        ev.dataTransfer.setData("drag-item", props.dataItem);
    };
    
    return (
      <div draggable onDragStart={startDrag}>
        {props.children}
      </div>);
}

实现Drop组件

接着我们就要来实现目标组件了,需要定义一个对外暴露的接口用来接收拖拽完成后的事件:

<DropTarget onItemDropped={itemDropped}>
    <div>
        请将组件拖放到这里
    </div>
</DropTarget>

从实现上来说,监听onDragOveronDrop这两个事件就可以了:

const DropTarget = (props) => {
    const dragOver = ev => {
        ev.preventDefault();
    }

    const drop = ev => {
        // 获取数据
        const droppedItem = ev.dataTransfer.getData("drag-item");
        if (droppedItem) {
            // 触发回调函数
            props.onItemDropped(droppedItem);
        }
    }
    
    return (
        <div onDragOver={dragOver} onDrop={drop}>
            {props.children}
        </div>
    )
}

添加拖放效果

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

先在Drag组件上设置effectAllowed属性:

const Drag = (props) => {
    
    const startDrag = ev => {
        ev.dataTransfer.setData("drag-item", props.dataItem);
        // 添加效果
        ev.dataTransfer.effectAllowed = props.dropEffect;
    };
    
    return (
      <div draggable onDragStart={startDrag}>
        {props.children}
      </div>);
}

接着我们设置一些效果常量:

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";

然后在目标组件上,我们通过给dropEffect属性赋值来引用这些效果常量,修改代码如下:

const DropTarget = (props) => {
    const dragOver = ev => {
        ev.preventDefault();
        // 添加效果
        ev.dataTransfer.dropEffect = props.dropEffect;
    }
    
    const dragEnter = ev => {
        ev.dataTransfer.dropEffect = props.dropEffect;
    }

    const drop = ev => {
        const droppedItem = ev.dataTransfer.getData("drag-item");
        if (droppedItem) {
            props.onItemDropped(droppedItem);
        }
    }
    
    return (
        <div onDragOver={dragOver} onDrop={drop} onDragEnter={dragEnter}>
            {props.children}
        </div>
    )
}

Drag.defaultProps = {
    dropEffect: dropEffects.All, // 设置默认的效果
};

进一步完善

到这一步,大体的功能我们都完成的七七八八了,最后还剩下一些收尾的工作。首先我们可以添加接口用来让用户可以自定义拖拽图像:

const Drag = (props) => {
    
    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 = ev => {
        ev.dataTransfer.setData("drag-item", props.dataItem);
        ev.dataTransfer.effectAllowed = props.dropEffect;
        // 设置图片
        if (image.current) {
            ev.dataTransfer.setDragImage(image.current, 0, 0);
        }
    };
    
    return (
      <div draggable onDragStart={startDrag}>
        {props.children}
      </div>);
}

接着,我们再来添加样式:

// 样式
const draggingStyle = {
    opacity: 0.25,
};

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 = ev => {
        setIsDragging(true);
        ev.dataTransfer.setData("drag-item", props.dataItem);
        ev.dataTransfer.effectAllowed = props.dropEffect;
        if (image.current) {
            ev.dataTransfer.setDragImage(image.current, 0, 0);
        }
    };

    // 拖拽结束时,添加样式
    const dragEnd = () => setIsDragging(false);

    return (
        <div style={isDragging ? draggingStyle : {}} draggable onDragStart={startDrag} onDragEnd={dragEnd}>
            {props.children}
        </div>
    );
};

最后,需要注意的是,如果需要处理移动端的兼容性,那么可以使用如下库:

https://github.com/timruffles/mobile-drag-drop

参考资料

https://app.pluralsight.com/guides/drag-and-drop-react-components(本文代码例子主要来源于此)

https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352