React结合Drag API实现拖拽示例详解
作者:samwangdd
认识拖拽
鼠标拖拽是一个常见的交互场景,在这个熟悉的过程将会发生哪些事件?
拖拽事件指用户通过鼠标(或其他指针设备)将元素移到一个新的位置上。拖拽过程涉及两个对象:被拖拽元素(上图中 A )和可释放目标(上图中 B )
被拖拽元素
默认情况下,图片、链接和文本是可拖动的。HTML5 在所有 HTML 元素上规定了一个 draggable
属性, 表示元素是否可以拖动。图片和链接的 draggable 属性自动被设置为 true,而其他所有元素此属性的默认值为 false。
某个元素被拖动时,会依次触发以下事件:
ondragstart
:拖动开始,当鼠标按下并且开始移动鼠标时,触发此事件;整个周期只触发一次;ondrag
:只要元素仍被拖拽,就会持续触发此事件;ondragend
:拖拽结束,当鼠标松开后,会触发此事件;整个周期只触发一次。
可释放目标
当把拖拽元素移动到一个有效的放置目标时,目标对象会触发以下事件:
ondragenter
:只要一把拖拽元素移动到目标时,就会触发此事件;ondragover
:拖拽元素在目标中拖动时,会持续触发此事件;ondragleave
或ondrop
:拖拽元素离开目标时(没有在目标上放下),会触发ondragleave
;当拖拽元素在目标放下(松开鼠标),则触发ondrop
事件。
🏝 目标元素默认是不能够被拖放的,即不会触发
ondrop
事件,可以通过在目标元素的ondragover
事件中取消默认事件来解决此问题。
生命周期
拖拽操作中的数据传输
除非数据受影响,否则简单的拖放并没有实际意义。为实现拖动操作中的数据传输,event
对象上暴露了 dataTransfer
对象,用于从被拖动元素向放置目标传递字符串数据。我们使用它来通知画布,当前需要渲染的组件是什么。
dataTransfer 对象主要有两个方法:getData() 和 setData(),分别用来获取和存储值。setData()的第一个参数以及 getData()的唯一参数是一个字符串,表示要设置的数据类型:"text"或"URL"
🏝 虽然这两种数据类型是 IE 最初引入的,但 HTML5 已经将其扩展为允许任何 MIME 类型。为向后 兼容,HTML5 还会继续支持"text"和"URL",但它们会分别被映射到"text/plain"和"text/uri-list”
需要注意的是:存储在 dataTransfer
对象中的数据只能在放置事件中读取。如果没有在 ondrop
事件中取得这些数据,dataTransfer
对象就会被销毁,数据也会丢失。
代码实现
我在项目中使用 React 来实现,并且考虑到跨组件通信,我使用了 dva 来管理数据流。
如何标记当前拖拽的元素?
HTML5 支持的 data-x
属性,我们可以将当前组件的类型 Rectangle 赋值给它,这样处理和画布组件通信方便一些
const Block = (props) => { const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => { // 向拖拽数据中添加项目 e.dataTransfer.setData('text', e.target.dataset.index); }; return ( <div onDragStart={handleDragStart}> <Button draggable data-index="Rectangle"> 二维码 </Button> </div> ); };
在上文中讲到,dataTransfer 的数据必须在 handleDrop 方法中获取。实际的用来保存画布中的所有组件的数据:
function DragEditor(props) { const { dvaStore, dispatch } = props; // 阻止浏览器默认事件,否则 ondrop 不会触发 const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => { e.preventDefault(); }; const handleDrop = (e: React.DragEvent<HTMLDivElement>) => { e.preventDefault(); // 获取拖拽元素的组件类型 const type = e.dataTransfer.getData('text'); // COMPONENT_LIST 定义了组件的数据格式,根据 type 匹配 const component = COMPONENT_LIST.filter( (i) => i.component === type, )[0]; // 将组件数据添加到 store,画布将会根据数据渲染出组件 if (component) { dispatch?.({ type: 'store/addComponent', payload: component, }); } }; return (...); }
在画布中拖动
拖动主要依赖组件的初始位置,鼠标开始位置、结束位置。根据后两组得到鼠标移动的距离,和初始位置相加后,得到最终位置。
function DragEditor(props: IEditorProps) { const { dvaStore, dispatch } = props; const [startAxis, setStartAxis] = React.useState({ x: 0, y: 0 }); // 鼠标开始拖动时的位置 const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => { setStartAxis({ x: e.clientX, y: e.clientY }); }; const handleDragEnd = (e: React.DragEvent<HTMLDivElement>, data: IComponentSchema) => { // 鼠标移动的距离 const displacementX = e.clientX - startAxis.x; const displacementY = e.clientY - startAxis.y; // 计算组件的终点位置:初始位置 + 鼠标移动的距离 const endX = Number(data.style.left) + displacementX; const endY = Number(data.style.top) + displacementY; // 限制坐标的最小值为 0 const top = Math.max(endY, 0); const left = Math.max(endX, 0); // 更新当前组件样式 dispatch?.({ type: 'store/setShapeStyle', payload: { top, left }, }); }; return ( {dvaStore.componentsData.map((i) => { return ( <RenderComponent type={i.component} componentData={i} key={i.generateId} onDragStart={handleDragStart} onDragEnd={(e) => handleDragEnd(e, i)} /> ); })} ); }
数据结构
最后,就是组件和数据结构的设计,RenderComponent 是一个自定义的组件,会根据传入的 type 属性渲染对应的组件。组件的数据结构设计如下:
export const COMPONENT_LIST = [ { component: 'Rectangle', // 组件名称 label: '矩形', // 左侧 Blocks 组件列表中显示的名字 propValue: '', // 组件所使用的值 icon: 'BorderOuterOutlined', // 左侧组件列表中显示的 icon 图标 animations: [], // 动画列表 events: {}, // 事件列表 style: { // 组件样式 width: 100, height: 100, top: 0, left: 0, }, }, { component: 'Text', label: '文字', propValue: '文字', icon: '', animations: [], events: {}, style: { width: 200, height: 33, fontSize: 14, fontWeight: 500, lineHeight: '', letterSpacing: 0, textAlign: '', color: '', }, }, ];
总结
拖拽是非常有趣的一种交互,特别是在低代码场景下非常重要。使用原生 API 能够让我们更加了解底层的一些细节,React 社区也有一些优秀的第三方框架,如:react-dragable, react-beautiful-dnd,大家有兴趣不妨再多了解下。
Links HTML 拖放 API
以上就是React结合Drag API实现拖拽示例详解的详细内容,更多关于React Drag API拖拽的资料请关注脚本之家其它相关文章!