📔 分栏布局的拖拽调节宽度实现方案

2,243 阅读4分钟

❓ 背景

最近实验室项目有个小需求,类似掘金代码编辑页的多栏布局,宽度可调节且有最小限制,当一个栏调至最小后接着往左或者往右调节。

查看相关的一些资料,大多是以监听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;
}

💖 写在最后

感谢读到最后,如果能有所帮助最好啦!

即是个人总结分享,也是获得帮助的机会,有问题希望各位指出,再次感谢!

🔗 参考文章

[1] 禁用文字选择、元素拖动事件

[2] css实现方式