纯css实现多栏拖动大小

5/29/2022 css

本文的代码和实现思路参考 公众号 - iCSS 前端趣闻:CSS 实现可拉伸调整尺寸的分栏布局 (opens new window) 下文为记录和整理学习的过程中自身的思考。如有侵权请联系我删除

# 纯 css 实现多栏拖动大小

查看效果演示

# 实现原理

核心是使用 css 的 resize (opens new window) 属性。当节点拖动的时候,resize 属性会自动帮我们修改 dom 节点的行内样式(width/height)。免去了我们自己用 js 实现拖动

配合 flex 布局,实现一个容器设置宽/高,剩余的容器占 flex:1 实现自适应的布局

/* 横向拖动的时候关键的css */
.resize {
  width: 100%;
  height: 16px;
  transform: scaleY(100);
  transform-origin: left;
  overflow: scroll;
  resize: horizontal;
  opacity: 0;
}

/* 垂直拖动的时候关键的css */
.resize {
  width: 16px;
  height: 16px;
  transform: scaleX(100);
  transform-origin: left;
  overflow: scroll;
  resize: vertical;
  opacity: 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

第二个核心原理:宽高 16pxscaleXscaleY

  • 为什么是 16px?

因为把一个节点设置为 resize 后,他的可以拖动变化宽高的边框是 16px~17px。chrome 是 17,火狐只有缩小到 16px 的时候上下箭头才会被隐藏,所以就把火狐预估为 16px。所以还是稳妥起见取小不取大,以免多余元素没被隐藏到


  • scaleXscaleY 的作用

就算设置了 resize 属性,也并不是整个 div 的边框都能拖动,能用于拖动的只有下图中红色框圈中的区域,其余的边框是不能触发 resize 效果的。

而根据上面的可以得知,红色框框的大小可以理解为 16*16px。如果我们想让整个容器的边框都可以拖动,只需要把这个 16* 16 往对应的方向拉大即可。如下图:

往垂直方向,放大 10 倍,红框 1 中整条范围都可以触发 resize 了

换为横向同理:

所以 scaleXscaleY 是为了对应横向/竖向的拖动的方块放大足够大的距离。而这个放大的倍数目前给的是 100 (尽可能大的倍数,能覆盖父 div 对应的边框长度即可)

比如一个需要缩放的 div 的大小为 160*160px。 那么 .resize 其实只需要 scaleX(10),那么 resize 用于拖动的边框长度也是 160,就可以完全覆盖这个 div。为什么不能采用 scaleX(10) 呢?

假设我们现在设置缩放的位置是根据 transform-origin: left top; 来设置的;就会看到如下的效果:

如果改为 transform-origin: left; 或者直接去掉 transform-origin: left; 属性,效果如下图:

而放大到 100(甚至更大的时候),情况如下图:

# 实现原理的最小实现 demo

拖动右边边框实现缩放

<style>
  .box {
    background-color: #99cccc;
    display: inline-block;
    height: 160px;
    overflow: hidden;
  }

  .resize {
    width: 160px;
    height: 16px;
    /* 可以尝试修改这个缩放数值查看效果 */
    transform: scaleY(100);
    transform-origin: left;
    overflow: scroll;
    resize: horizontal;
    opacity: 0.1;
  }
</style>

<div class="box">
  <div class="resize"></div>
  <div>resize</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 关于左右拖动方向的问题

如果你有打开我的 demo,可以看到 demo-2 左右布局升级版 的示例,找到右边节点的 css 把 direction 这个属性关掉,然后在试下拖动右边的容器

.right.aside .resize {
  direction: rtl;
}
1
2
3

这时候你会发现红色边框之前光标是拖动的箭头,现在变成了普通的箭头了。真正可以拖动的跑到了最右边。

可是这时候往右拖动,div 往左扩充。往左拖动,div 反而开始收缩。这明显很反人类。

所以就用到 css 的 direction (opens new window) 属性。这个属性也就 2 个值,ltrrtl。具体的演示可以看 MDN 的文档的效果。

可以这么理解: 正常我们阅读顺序是 从左往右,这时候 .resize 是在右下角,那如果我们把这个 右边的 .resize 的阅读方向改为 从右往左。这时候 .resize 就能被换到左下角去,这时候拖动就符合我们人类直觉了。

而且我们显示的内容并不在 .resize 容器中,.resize 只是为了设置宽度,撑开父容器的宽度,所以.resize 的阅读方向并不影响网页上的显示

# 关于上下拖动方向的问题

可以看到我的 demo-3 上下左右拖动布局

这里我的方法和大佬的并不一样~

贴上大佬的 demo 地址 demo (opens new window)

我选择的方法是让 .resize 贴近我们要用于拖动的那条边。

可以看下面的示例图,我利用的是 div 的顺序,让 .resize 贴近边(.resize 不能设置定位,否则就不能撑开父节点了)

大佬的实现方式是用到了

transform: scale(100, -1);
1

第二个参数 -1 其实就是说在Y轴使用反方向,是的 .resize 的定点换到左上角去(我的demo没这么用,可以自行探索一下)

# 关于极限拖动不松开的问题

如果按标题所说,纯 css 实现拖动大小,大体木有问题!可是细节会出问题

比如我的 demo-1 里面就提出的:当左右拉动到极限时松开鼠标 ,就无法重新拉回去。就是拖动 div 到 div 都不能在扩张,鼠标已经超出了拖动的线的时候

比如下图:一直拖动,已经无法在扩张了,然后鼠标在红色箭头的位置松开

为什么会出现这样的情况?看下图的解释:

因为 .resize 在拖动过程中,浏览器一直在拿鼠标计算偏移距离,鼠标没松开的时候偏移都一直还在计算。可以看到 body 的宽度才 1457px。而 .resize 的宽度已经达到了 1517px。那就更加超过 .slide 的容器了。

这时候拖动的边界其实是在 1517px 的最右边,显然 .slide 容器已经看不到他的最右边的边界了

这种情况也很好解决,其实无非就是当前 .resize 的宽度已经超出了父容器的宽度了,父容器已经找不到 .resize 的边界了。

那么我们检测到这种情况的时候,把 .resize 的宽度重置为父容器的宽度

除了宽度问题,高度的拖动也会存在同样的问题,所以 demo-4 借助了 js 的能力。搞来如下的代码:

var observe = new MutationObserver(function (mutationsList, observer) {
  var target = mutationsList[0] ? mutationsList[0].target : null
  if (!target) {
    return false
  }

  var parent = target.parentNode
  var classList = target.classList
  var isHorizontal = classList.value.indexOf('horizontal') !== -1

  if (isHorizontal) {
    var parentWidth = parent.clientWidth
    var diffWidth = target.clientWidth - parentWidth
    if (diffWidth > -18 || parentWidth * -1 === diffWidth) {
      target.style.width = parentWidth + 'px'
    }
  } else {
    var offsetTop = target.offsetTop + 16
    var parentHeight = parent.clientHeight
    var maxHeight = parentHeight - offsetTop
    var diffHeight = target.clientHeight - maxHeight
    if (diffHeight > 2) {
      target.style.height = maxHeight + 'px'
    }
  }
})

var resizeDom = document.querySelectorAll('.js-demo .resize')
resizeDom.forEach(item => {
  observe.observe(item, { attributes: true })
})
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

简单说一下代码的意思

  1. 找到所有 .resize 的节点(document.querySelectorAll('.js-demo .resize'))
  2. 利用 MutationObserver (opens new window) 能力,监听 div 的变化 new MutationObserver()
  3. 每次 observe 只能监听一个节点,所以来了个循环
  4. 监听到该 div 变化的时候,根据 class 类名判断一下是往哪个方向移动的(左右的话就设置宽度,上下设置高度)
  5. 根据一些简单的数学公式,得出 div 什么时候被拖动到边界后就重置一下宽度或者高度
  6. 优化的点在于这个拖动的时候触发监听太频繁了,可以加个防抖(懒,就没加了)

具体的效果看 demo-4 吧! 不然我就白写了啊

# 最后

以上就是跟着大佬学习容器拖动修改大小的全部学习笔记

加入一些简单的 js 实现一个通用的拖动,毕竟纯靠 css 能实现固然最好,可是 css 做不了那么复杂的计算

包括 resize 属性虽然是 css,可是也是浏览器底层帮我们改变了节点的样式(可以理解为把这部分的逻辑给了浏览器帮我们做了)

Last Updated: 1/7/2024, 5:51:59 PM