拖拽组件是在前端开发中十分常见的一个功能,现在无论你是使用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>
从实现上来说,监听onDragOver
和onDrop
这两个事件就可以了:
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>
);
};
最后,需要注意的是,如果需要处理移动端的兼容性,那么可以使用如下库:
参考资料
共同学习,写下你的评论
评论加载中...
作者其他优质文章