❓ 背景
最近实验室项目有个小需求,类似掘金代码编辑页的多栏布局,宽度可调节且有最小限制,当一个栏调至最小后接着往左或者往右调节。
查看相关的一些资料,大多是以监听mouse相关事件来实现的,还有一种css的实现方式实现分栏布局的调节。
也有一些拖拽resize的第三方库元素,功能有些冗余,还需要修改样式,并且不是一条线的resize。需要修改的太多,于是打算自己实现一下。
💡 实现思路
- 多栏布局通过绝对定位(position:absolute)控制。
- 多栏布局是变化的,所以通过数组来存储布局信息,包括每一栏的左侧(left)和宽度(width)信息。
- 多栏布局的外层父级容器监听mousedown事件,筛选点击dom并判断当前激活的resizer,然后进入mousemove逻辑,分左右移动滑动进行处理,最后在mouseup事件取消监听mousemove和mouseup。
📖 实现代码
⭐ 关键点分析
根据layout内panel数量进行初始化
本文简单通过ResizeObserver以平均划分的方式更新,后续可以外层容器resize时layout的比例进行等比更新。
const initPanelSize = (parentWidth) => {
const width = parentWidth / layout.value.length;
layout.value.forEach((item, index) => {
item.left = index * width;
item.width = width;
});
};
onMounted(() => {
// 初始化layout参数
if (mainRef.value) {
// 监听外层容器domresize,更新layout中每个panel的width和left
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = entry.contentRect.width;
initPanelSize(width);
}
});
resizeObserver.observe(mainRef.value);
}
});
滑动过程中出现问题卡顿,滑动太快会失效,或者松手依旧触发布局调节
主要就是在mousedown时,禁止元素拖拽和文字选中(参考文章[1]),再在mouseup时恢复。
这里我测试了一下,两个禁用事件只禁用其一貌似也可以解决问题。因为这个问题的出现是由于拖拽过程中选中了一部分文字,然后又拖拽了这段文字导致的。
// mousedown
// 禁止文字选择
document.onselectstart = () => false;
// 禁止元素拖拽
document.ondragstart = () => false;
// mouseup
// 允许文字选择
document.onselectstart = null;
// 允许元素拖拽
document.ondragstart = null;
mousemove处理逻辑
计算位移距离offsetX。
let offsetX = e.clientX - resizeConfig.last.pos;
找到最左侧和最右侧可以移动的panel下标,根据offsetX判断左移还是右移,当左(右)侧全部最小化且左(右)移时,停止resize。
// 找到左侧可以调整宽度的panel
let leftIndex = index;
while (leftIndex >= 0 && layout.value[leftIndex].width <= MIN_WIDTH) {
leftIndex--;
}
let rightIndex = index + 1;
while (rightIndex < layout.value.length && layout.value[rightIndex].width <= MIN_WIDTH) {
rightIndex++;
}
// 当左侧panel全部已经最小化则暂停
if (leftIndex < 0 && offsetX < 0) {
return;
}
// 当右侧panel全部已经最小化则暂停
if (rightIndex >= layout.value.length && offsetX > 0) {
return;
}
- 左移时,修改最左侧panel宽度,达到最小值时,修改offsetX。
- 然后,遍历最左侧panel到resizer辅助线之间被折叠至最小宽度的panel的位置。
- 最后修改resizer辅助线右侧第一个panel的宽度和位置。
// resizer左侧panel宽度
const newLeftWidth = layout.value[leftIndex].width + offsetX;
if (newLeftWidth <= MIN_WIDTH) {
offsetX = MIN_WIDTH - layout.value[leftIndex].width;
layout.value[leftIndex].width = MIN_WIDTH;
} else {
layout.value[leftIndex].width = newLeftWidth;
}
// 修改被折叠的中间panel(最小宽度)的位置,要放在修改offset的语句后面,否则会引起left偏移
for (let i = index; i > leftIndex; i--) {
layout.value[i].left += offsetX;
}
layout.value[index + 1].width -= offsetX;
layout.value[index + 1].left += offsetX;
resizeConfig.last.pos = e.clientX;
- 右移时,修改最右侧panel宽度,达到最小值时,修改offsetX。
- 然后,遍历resizer辅助线到最右侧panel之间(包括最右侧panel)被折叠至最小宽度的panel的位置。
- 最后修改resizer辅助线左侧第一个panel的宽度。
// resizer右侧panel宽度
const newRightWidth = layout.value[rightIndex].width - offsetX;
if (newRightWidth <= MIN_WIDTH) {
// 要和左侧反过来,因为这边的offset要是>0的,否则会引起left偏移,导致白边
offsetX = layout.value[rightIndex].width - MIN_WIDTH;
layout.value[rightIndex].width = MIN_WIDTH;
} else {
layout.value[rightIndex].width = newRightWidth;
}
// 修改被折叠的中间panel(最小宽度)的位置
for (let i = index + 1; i <= rightIndex; i++) {
layout.value[i].left += offsetX;
}
layout.value[index].width += offsetX;
resizeConfig.last.pos = e.clientX;
这里放两个,拖拽调节过程中分栏之间出现left偏移引起的白边问题(上文注释有标记)
- 代码顺序:遍历resizer到最左(右)侧之间的最小宽度panel的位置,需要在修改offset的代码之后
- offsetX的取值:左移右移offsetX取值不同,在达到最小值时需要修改offsetX时需要考虑offsetX取值正负的一致性
改善拖拽体验resizer辅助线加宽(仿照掘金代码编辑)
.resizer {
position: absolute;
background-color: #d3d3d3;
height: 100%;
width: 1px;
z-index: 1;
cursor: ew-resize;
}
.resizer::after {
content: "";
position: absolute;
left: 50%;
top: 0;
height: 100%;
width: 100%;
transform: translateX(-50%);
min-width: 12px !important;
}
💖 写在最后
感谢读到最后,如果能有所帮助最好啦!
即是个人总结分享,也是获得帮助的机会,有问题希望各位指出,再次感谢!