拖拽组件是在前端开发中十分常见的一个功能,现在无论你是使用React还是Vue,都有很多现成的拖拽组件可以使用。不过,有些时候你可能还是需要自己去实现,那么就必须需要理解其实现原理。接下来这篇文章,我将详细介绍如何使用React框架来实现一个拖拽组件。
现如今,大部分的前端拖拽组件都依托于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
接口,可专门用来保存事件的数据内容。对应的接口有:
了解完这些基本接口后,我们就可以着手使用React来编写自己的拖放组件了:
我们第一个要实现是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>); }
接着我们就要来实现目标组件了,需要定义一个对外暴露的接口用来接收拖拽完成后的事件:
<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> ); };
最后,需要注意的是,如果需要处理移动端的兼容性,那么可以使用如下库: