拖拽表格

哎,最近公司PM又搞事了,非要做什么拖拽表格,拖拽表头调整位置不行,还要能调整宽度…
市面上现成的轮子,要么是拖拽位置,要么是拖拽宽度,简单找了找,还是自己造轮子吧,自己造的想怎么捏怎么捏。

需求刚刚开始的时候,根据开源大佬提示,写了这样的拖拽样式,开开心心的交给pm看,结果被残忍的驳回,“我要拿得起放得下的表头”:Pm如是说。
好吧,好吧,我写,我写。

现在需求写好了,我来整理一下思路吧,看着很难,写起来很难真正做完又不觉得难的一个需求,还是被坑了好些下的。
首先要明确需求,表格形式,要能拿起当前点击的表头,然后拖动,当他经过左右两侧任意一个表头一半宽度的时候,在该表头,左&右增加一个2像素的边,松开鼠标,将拿起的表头放到竖线处,这是表头的拖动。
其次,表头的宽度可以任意拖动

重点来了,两端需要有固定的列

好了,需求明确了,那么我们现在开始整理思路造作吧,
饿了么的表格有现成的拖动宽,固定列,那么就用饿了么的表格组件吧,
emmm,组长说,用div自己布局吧,展开元素内容过多,用饿了么的组件会有限制。
行行行,我都自己写!

先确定表头的数据格式,此处还是要借鉴饿了么,白嫖一时爽,一直白嫖一直爽~

1
2
3
4
5
6
7
tableHeader [
{
label: '表头名称',
prop: 'name', // 当前列数据对应字段,很重要
width: '' // 当前宽度
}
]

通过创建一个数组来存储表头(行),通过调整数组顺序来调整列表排序,通过数组内对象属性width来存储当前列宽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div class="d-table" ref="dTable">
<el-scrollbar ref="el_scroll_dom" class="default-scrollbar" wrap-class="default-scrollbar__wrap" view-class="default-scrollbar__view">
<div class="d-table_header thead flex" :style="[{width:allwidth}]">
<div
v-for="(col,index) in tableHeader"
:key="col.label"
:class="['d-table_th', 'column_' + index]"
:style="{ width: parseInt(col.width)+'px'}"
@mousedown="handleMouseDown($event, index)"
@mousemove="handleMouseMove($event, index)"
>
{{col.label}}
</div>
<div class="draggingDom" ref="draggingDom" v-show="draggingDomVisible">{{draggingText}}</div>
<div class="resize-proxy" ref="resizeProxy" v-show="resizeProxyVisible"></div>
</div>
</el-scrollbar>
</div>

这样,一个表头我们就渲染出来了,至于方法我们下面说。

className column_ 用来区分每一个表头,毕竟操作表头的宽度一定会操作dom,mousedown 方法为当前鼠标点下事件,即用户操作宽度或调整位置的触发点,mousemove 用来记录鼠标移动,这里的mousemove主要用来拖动表头宽度时,鼠标样式的效果。

接下来是拖动表头宽度

之所以先写这个,原因是整个拖拽的思路都是白嫖了饿了么表格宽度调整
首先我们需要整理一下,关于拖动都需要哪些元素,这里应用了getBoundingClientRect函数

1
2
3
4
5
6
7
8
9
10
11
12
const tableEl = this.$refs.dTable  // 获取外层父级实例
const tableLeft = tableEl.getBoundingClientRect().left // 父级距离屏幕左边距离
const columnEl = this.$el.querySelector('.column_'+i) // 获取当前点击元素dom
const columnRect = columnEl.getBoundingClientRect()
const minLeft = columnRect.left - tableLeft + 30 // 为什么加30?

this.dragState = {
startMouseLeft: event.clientX, // 鼠标当前x坐标,设置为起始
startLeft: columnRect.right - tableLeft, // 距离左边初始值
startColumnLeft: columnRect.left - tableLeft, // 表头左边距离
tableLeft
}

然后我们设置鼠标经过时样式变化,在鼠标经过的元素右边距离8像素的位置,将鼠标指针更改为col-resize,并且将当前点拖拽锁设定为拖拽宽度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 鼠标移动事件,增加鼠标样式
handleMouseMove(event, i) {
let target = event.target;
if (!this.dragging) {
let rect = target.getBoundingClientRect();
const bodyStyle = document.body.style;
if (rect.width > 12 && rect.right - event.pageX < 8) {
bodyStyle.cursor = 'col-resize';
target.style.cursor = 'col-resize';
this.draggingColumn = true
} else if (!this.dragging) {
bodyStyle.cursor = '';
target.style.cursor = 'move';
this.draggingColumn = null;
}
}

},

接下来开始写鼠标拖拽表头宽度,我们先将必要元素准备好

1
2
3
4
5
6
7
8
9
this.dragging = true; // 开启拖动
this.resizeProxyVisible = true; // 显示拖拽线实例
const resizeProxy = this.$refs.resizeProxy; // 获取实例
resizeProxy.style.left = this.dragState.startLeft + 'px'; 修改左边距
document.onselectstart = function() { return false; }; // 禁止选中文字
document.ondragstart = function() { return false; }; // 禁止拖拽
// 添加监听事件,以监听鼠标移动,抬起事件
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);

然后我们设置鼠标移动效果

1
2
3
4
5
const handleMouseMove = (event) => {
const deltaLeft = event.clientX - this.dragState.startMouseLeft; // 获取鼠标实际滚动距离
const proxyLeft = this.dragState.startLeft + deltaLeft; // 当前所处位置
resizeProxy.style.left = Math.max(minLeft, proxyLeft) + 'px'; // 设置竖线位置
};

现在我们再设置鼠标抬起事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const handleMouseUp = () => {
if (this.dragging) {
const {
startColumnLeft,
startLeft
} = this.dragState;
const finalLeft = parseInt(resizeProxy.style.left, 10);
let columnWidth = finalLeft - startColumnLeft;
if(columnWidth < 68) columnWidth = 68 // 最小拖动宽度,可以按需求自己定义
this.tableHeader[i].width = columnWidth
const params = JSON.stringify(this.tableHeader)
// 这里执行请求事件,将表头以json格式发送给后端,保存当前设置的宽度,完成后设置当前元素宽
columnEl.style.width = columnWidth + 'px'
document.body.style.cursor = '';
this.dragging = false;
this.draggingColumn = null;
this.dragState = {};
this.resizeProxyVisible = false;
}
// 移除监听事件
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.onselectstart = null;
document.ondragstart = null;
};

这样,拖动宽度的方法就完成了,主要参照的饿了么表格宽度调整。

拖拽表头

下面我们开始写拖拽表头元素,思路与拖拽宽度一样,我们创建一个dom(draggingDom)来作为‘拿起’的元素,然后当我们挪动超过左右元素的一半宽度时,我们将前(后)元素设置边框。起初写到这里时,陷入思维误区,卡了很久,感谢好基友yogwang提供的中线思路,不然我还在傻傻的动态计算元素宽度。
我们将整个表头元素模拟成坐标轴,以当前鼠标点击的节点为(0 ,0),向左边为负(dragging_left),向右为正(dragging_right)
同样,我们先将需要的元素准备好,先计算每个表头结构的中线位置,时间紧迫,这里写了两个方法来计算,后面有时间复盘再来修改吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 左边中线数组
getPreLine(i) {
let arr = []
let _this = this
const setPreWith = (n)=> {
if(!n) return false
let len = this.getDomsWidth(n,i) + _this.tableHeader[n-1].width /2
arr.push(-len)
setPreWith(n - 1)
}
setPreWith(i)
const newArr = arr.reverse()
return newArr
},
// 获取临近宽度集合
getDomsWidth(k,j) {
let _this = this
let len = 0
for(let i = k; i < j; i++) {
len = len + this.tableHeader[i].width
}
return len
},
const lineArr = this.getPreLine(i).concat([0]).concat(this.getNextLine(i)) // 中线数组集合

同样的,我们使用一个dom来承载拖动的内容,点击时定义其宽高以及初始位置

1
2
3
4
5
6
7
8
9
10
11
12
// 拖动表头位置
this.draggingcell = true
this.draggingDomVisible = true
this.$refs.draggingDom.style.width = columnRect.width + 'px'
this.$refs.draggingDom.style.left = columnRect.left - 20 + 'px'
this.draggingText = this.tableHeader[i].label
// 获取中线
let end = i;
this.activedIndex = i
// 阻止默认事件
document.onselectstart = function() { return false; };
document.ondragstart = function() { return false; };

下面是拖动表头的鼠标移动事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const handleMouseMove = (event) => {
//进行移动
const deltaLeft = event.clientX - this.dragState.startMouseLeft; // 实际位移距离
const proxyLeft = deltaLeft + columnRect.left - 20 // 位移后距离左边距离
this.$refs.draggingDom.style.left = proxyLeft + 'px'; // 生成初始位置
// 点击处为原点,向左为负,向右为正,我们只需要对比前后两个dom的中线即可
if(deltaLeft < 0) {
this.dragState.direction = 'left'
// 如果移动距离小于前一个dom中线时
if(deltaLeft < lineArr[end-1]) {
removeClass(this.$el.querySelector('.column_'+ end),'dragging_left')
removeClass(this.$el.querySelector('.column_'+ end),'dragging_right')
--end;
addClass(this.$el.querySelector('.column_'+ end),'dragging_'+this.dragState.direction)
} else if (deltaLeft >= lineArr[end]) {
removeClass(this.$el.querySelector('.column_'+ end),'dragging_left')
removeClass(this.$el.querySelector('.column_'+ end),'dragging_right')
++end
addClass(this.$el.querySelector('.column_'+ end),'dragging_'+this.dragState.direction)
}
} else if((deltaLeft > 0)){
this.dragState.direction = 'right'
if(deltaLeft > lineArr[end+1]) {
removeClass(this.$el.querySelector('.column_'+ end),'dragging_left')
removeClass(this.$el.querySelector('.column_'+ end),'dragging_right')
++end
addClass(this.$el.querySelector('.column_'+ end),'dragging_'+this.dragState.direction)
} else if(deltaLeft <= lineArr[end]) {
removeClass(this.$el.querySelector('.column_'+ end),'dragging_left')
removeClass(this.$el.querySelector('.column_'+ end),'dragging_right')
--end
addClass(this.$el.querySelector('.column_'+ end),'dragging_'+this.dragState.direction)
}
}
};

最后我们来看一下鼠标抬起时事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const handleMouseUp = (event) => {
// 鼠标抬起时,我们先移除当前的边线
removeClass(this.$el.querySelector('.column_'+ end),'dragging_left')
removeClass(this.$el.querySelector('.column_'+ end),'dragging_right')
end !== this.activedIndex && this.setThDom(end)
// 此处可以将修改值传给后端
this.draggingDomVisible = false
this.draggingcell = false
this.dragState = {};
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.onselectstart = null;
document.ondragstart = null;
}

就像拖拽宽度一样,我们同样需要监听鼠标事件

1
2
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);

好了,这样拖拽的表格以及拖拽宽度就完成了,完整的demo我会整理好发到git仓库上。