写在前面
Drawer 组件在日常开发中使用的频率还是挺高的,一般情况下,都可以满足我们的日常使用。
最近笔者看到这样的一个需求,就是把 Drawer 改造成可拖拽的效果,因为改成可拖拽的就可以对里面的内容进行自适应的宽度改变,比如 Echarts 图表类的比较适合,拖拽的宽一些可视性会更好。
那话不多少,我们来想一下怎么实现这个功能。
思路
首先先来看一下 Drawer 组件,如果我们想拖拽一定是沿着左边的线去拖拽,或者可以加个图标固定拖拽的位置都可以。
那思路也很简单:
要达到拖拽效果,肯定要满足的是我们的拖拽的距离等于 Drawer 的宽度,只要满足这个条件,那拖拽效果就成立了。
那现在的问题就转变为怎么求拖拽的距离了。
拖拽距离 = 终止位置 - 起始位置
当按下时,需要记录起始位置,移动过程中记录实时变化的X轴坐标即可。
有了大概思路,现在我们来实现吧。
加边拖动
首先我们需要确定按下的元素,已知我们需要按着左边的边进行移动,也可以用图标代替,现在我们先使用边,图标同理,因为需要给边添加相关事件,这里我们需要添加一个假的边进行实现。
这是我们的原始代码,现在先加边
<Drawer title="Draggable Drawer" placement="right" onClose={onClose} visible={visible} getContainer={false} > <p>Some contents...</p> <p>Some contents...</p> <p>Some contents...</p> </Drawer>
加边的思路是在 Drawer 组件内增加一个容器,将容器的边和 Drawer 组件的边重合即可,此时需要设置 Drawer 组件的 padding 为 0,要不然不会重叠。
<Drawer title="Draggable Drawer" placement="right" onClose={onClose} visible={visible} getContainer={false} bodyStyle={{ padding: 0 }} > <div className="drawerContent"> <p>Some contents...</p> <p>Some contents...</p> <p>Some contents...</p> </div> </Drawer>
红色边框就是我们的容器,可以看到已经重叠了
现在还不行,因为我们只想拖拽左边的线,不能对整个容器增加事件,也很简单,再加一条竖线放在左边进行重叠即可。
代码如下:
<Drawer title="Draggable Drawer" placement="right" onClose={onClose} visible={visible} getContainer={false} bodyStyle={{ padding: 0 }} width="auto" > <div className="drawerContent"> <div className="dragLine"></div> <p>Some contents...</p> <p>Some contents...</p> <p>Some contents...</p> </div> </Drawer>
为了展示,现在有些覆盖,后面效果调整宽度和背景色即可。
事件
现在边已经好了,我们接下来只需要对边增加相关事件即可。
鼠标按下
当鼠标按下时,记录起始位置,拿到 Drawer 组件内容器的开始宽度,同时增加移动和抬起事件,因为后面需要移动元素的。
这里解释一下为什么是拿到 Drawer 组件内容器的宽度,而不是直接设置 Drawer 的宽度,这里我使用的是将 Drawer 的宽度为 auto,通过容器的宽度去撑起 Drawer 的宽度,你也可以直接设置Drawer 的宽度,但是移动过程中会有一些问题,这里就不做演示了,感兴趣的可以试试。
const startX = useRef(0) const startWidth = useRef(0) const onMouseDown = (e: React.MouseEvent<HTMLDivElement>) => { startX.current = e.clientX startWidth.current = drawerRef.current?.getBoundingClientRect().width || 0 document.addEventListener('mousemove', onMouseMove) document.addEventListener('mouseup', onMouseUp) }
鼠标移动
鼠标按下记录起始位置,移动时就需要进行计算了,实时得到移动的距离。
解释一下 newWidth 就是新的宽度,正常情况下应该等于:容器的宽度 + 移动的距离
但也有移动到左侧停止,再往右侧移动的
因为是先往左侧移动,移动得到的差值是负数,所以直接减就行。
如果是移动到左侧停止,再往右侧移动的,那差值是正值,直接减就行了。
const onMouseMove = (e: MouseEvent) => { console.log('e.clientX : ', e.clientX) const newWidth = startWidth.current - (e.clientX - startX.current) if (drawerRef.current) { drawerRef.current.style.width = `${newWidth > 0 ? newWidth : 0}px` } }
鼠标抬起
当鼠标抬起时,只需要移除相关事件即可。
const onMouseUp = useCallback() => { document.removeEventListener('mousemove', onMouseMove) document.removeEventListener('mouseup', onMouseUp) }
初始效果
bug修复
可以看到效果基本实现了,但是有一些小bug
第一:拖拽过程有文字选择效果,不好
解决办法:只需要在鼠标按下事件里增加阻止默认事件,阻止捕获和冒泡阶段中当前事件的进一步传播。
const onMouseDown = (e: React.MouseEvent<HTMLDivElement>) => { e.preventDefault() e.stopPropagation() startX.current = e.clientX startWidth.current = drawerRef.current?.getBoundingClientRect().width || 0 document.addEventListener('mousemove', onMouseMove) document.addEventListener('mouseup', onMouseUp) }
第二:移动到最右侧时,里面的内容容器的宽度还在减小
解决办法:给容器设置宽度以及,最小宽度即可
.drawerContent { min-width: 200px; width: 200px; height: 100%; border: 1px solid red; position: relative; }
再来看看效果,现在上面的问题都没了,还不错
图标拖动
上面我们是通过加边实现的,但往往实际需求中,可能会有其他的方式要求,比如加一个固定的图标或者UI设计的进行拖动,这里我们就加个简单的图标,事件和上面一致。
<Drawer title="Draggable Drawer" placement="right" onClose={onClose} visible={visible} getContainer={false} width="auto" bodyStyle={{ padding: 0 }} > <div ref={drawerRef} className="drawerContent"> <DragOutlined className="dragLine" style={{ fontSize: '50px' }} onMouseDown={onMouseDown}/> <p>Some contents...</p> <p>Some contents...</p> <p>Some contents...</p> </div> </Drawer>
同时设置样式
.dragLine { cursor: ew-resize; width: 50px; height: 50px; position: absolute; top: 43%; left: -25px; }
但是发现只显示了一半,另一半被遮挡住了
只需要设置Drawer组件的overflow:visible即可
bodyStyle={{ padding: 0, overflow: 'visible' }}
效果一致,大家可以根据自己需求进行更改
Drawer在左侧时
上面说的是 Drawer 在右侧时,现在说一下在左侧时,道理一样,只需要更改按压元素的位置即可移动时计算的距离即可。
定位改到右侧
.dragLine { cursor: ew-resize; position: absolute; top: 50%; right: -25px; }
往右移动改为加即可
const newWidth = startWidth.current + (e.clientX - startX.current)
效果一致
总结
实现这个需求首先确定按下元素的位置,为按下元素添加相应事件,根据移动的距离动态赋值内容容器的宽度,再根据Drawer的width:auto,自动撑开Drawer组件即可。
当然实现过程中也有其他小 bug,比如选中文字,设置最小宽度等等,其实还有小问题,文章中并没有呈现,就是如果里面的内容时 iframe 引入的话,会导致移动事件失效,这个会单独写一篇文章,主要方便有相同问题的朋友进行搜索解决问题。
全部源码
import React, { useState, useRef, useCallback, useEffect } from 'react' import { Drawer, Button } from 'antd' import '../DraggableDrawer.css' // 为了额外的样式 import { DragOutlined } from '@ant-design/icons' export default function DrawDemo() { const [visible, setVisible] = useState(false) const drawerRef = useRef<HTMLDivElement>(null) const startX = useRef(0) const startWidth = useRef(0) const showDrawer = () => { setVisible(true) } const onClose = () => { setVisible(false) } const onMouseDown = (e: React.MouseEvent<HTMLDivElement>) => { e.preventDefault() e.stopPropagation() startX.current = e.clientX startWidth.current = drawerRef.current?.getBoundingClientRect().width || 0 console.log('startWidth.current: ', startWidth.current) document.addEventListener('mousemove', onMouseMove) document.addEventListener('mouseup', onMouseUp) } const onMouseMove = (e: MouseEvent) => { const newWidth = startWidth.current - (e.clientX - startX.current) if (drawerRef.current) { drawerRef.current.style.width = `${newWidth > 0 ? newWidth : 0}px` } } const onMouseUp = () => { document.removeEventListener('mousemove', onMouseMove) document.removeEventListener('mouseup', onMouseUp) } return ( <> <Button type="primary" onClick={showDrawer}> Open Draggable Drawer </Button> <Drawer title="Draggable Drawer" placement="right" onClose={onClose} visible={visible} getContainer={false} width="auto" bodyStyle={{ padding: 0, overflow: 'visible' }} > <div ref={drawerRef} className="drawerContent"> <DragOutlined className="dragLine" style={{ fontSize: '50px' }} onMouseDown={onMouseDown} /> {/* <iframe src="http://localhost:8080/management"></iframe> */} {/* <div className="dragLine" onMouseDown={onMouseDown}></div> */} </div> </Drawer> </> ) }