css 中的 滚动条

1,702 阅读4分钟

滚动条问题

浏览器默认滚动条各不相同,感觉都是不好看的样式。而且同一种浏览器 window 和 mac 也有着比较大的区别。

于是打算从滚动条的css基础属性入手,然后制定两种解决方案,并阐述。

我想达到的效果是滚动条悬浮于盒子上,并不占用盒子宽度。类似 fixed 的效果

滚动条的组成

滚动条可以帮助盒子在固定区域内展示更多的内容,对比下面两种操作系统对于滚动条的处理,mac是比较理想的。

window 下的 chrome (92)

企业微信截图_16304688398073.png

mac 下的 chrome (92)

image.png

滚动条基础属性

  1. ::-webkit-scrollbar 整个滚动条
  2. ::-webkit-scrollbar-button 滚动条上的按钮,按钮有上下箭头,点击可以控制滚动条的移动
  3. ::-webkit-scrollbar-thumb 滚动条上的滚动滑块,点击可以拖拽
  4. ::-webkit-scrollbar-track 滚动条轨道
  5. ::-webkit-scrollbar-track-piece 滚动条没有滑块的轨道部分
  6. ::-webkit-scrollbar-corner 当同时拥有两个方向的滚动条时的交汇部分
  7. ::-webkit-resizer 某些元素的 corner 部分的部分样式. (例 textarea 的可拖动按钮)

隐藏滚动条,制作虚拟滚动条

主要是以下3个目标的实现 💡

  1. 监听内容页面的滚动事件 =》页面随之滚动
  2. 监听滚动条的拖拽 =》滚动条滚动 & 页面内容滚动
  3. 监听滚动轨道空白的点击 =》滚动条跳转 & 页面内容滚动

准备工作

html部分

<div class="box" id="box">
    <div class="content" id="content">
        <div class="blue">
            蓝色背景行
        </div>
    </div>
    <div class="scrollbar" id="scrollbar">
        <div class="thumb" id="thumb"></div>
    </div>
</div>

css部分

/* 利用css 实现 */

.box {
  width: 400px;
  height: 400px;
  background-color: orange;
  overflow: hidden;
  position: relative;
}

.content {
  width: 400px;
  height: 400px;
  overflow-y: scroll;
  overflow-x: hidden;
}

/* 当box被hover时,显示scroll */

.box:hover .scrollbar {
  opacity: 1 !important;
}

/* 隐藏原生滚动条 */

.box ::-webkit-scrollbar {
  display: none;
}

/* 滚动条容器,即滚动条轨道 */

.scrollbar {
  height: 100%;
  width: 6px;
  position: absolute;
  top: 0px;
  right: 0;
  opacity: 0;
  transition: 0.1s ease-out;
}

/* thumb 滚动条 */

.thumb {
  width: 6px;
  height: 100%;
  background-color: red;
  border-radius: 8px;
  position: absolute;
  cursor: pointer;
  top: 0px;
}

js中获取DOM

const scrollbar = document.getElementById('scrollbar') // 整个滚动条
const thumb = document.getElementById('thumb') // thumb 滚动条
const content = document.getElementById('content') // 内容容器,和滚动条容器并行

js中设置内容容器内填充 & 设置滚动条高度

// 渲染内容,产生滚动
content.innerHTML = content.innerHTML + new Array(400).fill('123').join('-')


// 设置滚动条thumb height,随着内容的变化变化
thumb.style.height = scrollHeight * 100 + '%'

监听内容容器的滚动

const clientHeight = content.clientHeight // 容器的高度 px
const canScrollHeight = content.scrollHeight - clientHeight // 滚动区域的高度 = 内容滚动高度 - 内容容器高度
const scrollHeight = clientHeight / content.scrollHeight // 滚动条的高度(长度,height) % = 内容容器高度 / 内容滚动高度


content.addEventListener('scroll', function (e) {
  const scrollTop = content.scrollTop

  // 设置滚动条 thumb 的 top。满足:(当前滚动 / 滚动区域) = (top / top最大值)
  thumb.style.top = (clientHeight - clientHeight * scrollHeight) * (scrollTop / canScrollHeight) + 'px'

})

监听滚动条的拖拽

thumb.addEventListener('mousedown', downHandler)

/**
 * 开始滚动
 */
let cursorDown = false // 拖拽标识
let y1 = 0 // 点击滚动条的位置 距离滚动条顶端的距离
let y2 = 0 // 点击滚动条的位置 距离滚动条底部的距离

// 鼠标开始点击
function downHandler(e) {
  y1 = e.layerY // 滚动条的 layerY 属性为点击位置到元素顶部的距离
  y2 = scrollHeight * clientHeight - y1 // 滚动条长度 - y1

  // 开始拖拽
  cursorDown = true

  // 如果一个元素上被添加多个事件,当事件触发的时候,会按照添加顺序执行。如果添加 stopImmediatePropagation(),那么这个元素上的其他事件将不执行
  e.stopImmediatePropagation();

  // 显示滚动条容器
  scrollbar.style.opacity = 1

  // 监听全屏拖动
  document.addEventListener('mousemove', moveHandler)

  // 监听全屏拖动结束
  document.addEventListener('mouseup', upHandler)

  // 拖动过程中去除选中事件
  document.onselectstart = () => {
    return false
  }
}

// mousemove 移动操作函数
function moveHandler(e) {
  console.log('moveHandler')
  const contentY = e.pageY
  // 如果超出了允许滚动的范围,不做处理
  if (contentY <= y1 || contentY >= (clientHeight - y2)) {
    return
  }

  // 设置滚动条 thumb top属性
  thumb.style.top = e.pageY - y1 + 'px'

  // 控制内容滚动 x,y
  content.scrollTo(0, thumb.offsetTop / (clientHeight - scrollHeight * clientHeight) * canScrollHeight)

}

// mouseup 结束拖动
function upHandler() {
  console.log('拖动结束,注销监听事件')
  // 结束拖拽
  cursorDown = false

  // 还原选中事件
  document.onselectstart = null

  // 移除监听全屏的两个事件
  document.removeEventListener('mousemove', moveHandler)
  document.removeEventListener('mouseup', upHandler)

  scrollbar.style.opacity = 0
}

监听点击滚动轨道

scrollbar.addEventListener('mousedown', clickScrollbar)

// 点击空白轨道
function clickScrollbar(e) {
  const pageY = e.layerY
  const height = scrollHeight * clientHeight
  let top = (pageY - height / 2)
  if (top < 0) {
    top = 0
  }
  if (top > (clientHeight - clientHeight * scrollHeight)) {
    top = (clientHeight - clientHeight * scrollHeight)
  }
  thumb.style.top = top + 'px'
  content.scrollTo(0, thumb.offsetTop / (clientHeight - scrollHeight * clientHeight) * canScrollHeight)
}

总结

  1. 实现自定义滚动条的每个步骤都是清晰明了的,需要我们耐心的讲它们拼接在一起

  2. 实现的原理和 el-scrollbar 基本一样,但是项目的公共组件在成本允许的前提下尽量我们自己实现。一方面可以借助这个组件整体的学习或复习遇到的知识点,很多优秀的用法可以积累下来,例如对 DOM 和 body 两者之间的交互的处理。另一方面可以定制团队的想法,虽然这个滚动条没啥太多可以定制的东西

  3. 监听 mousemove 事件的时候,刚开始我监听的是 document.body,会出现鼠标移出浏览器范围就失去监听,然后松开鼠标回来也能滚动。于是乎需要监听 document 上的 mousemove

  4. 对于鼠标事件的 event 和 直接获取 DOM 两者的位置属性对应的意思需要单独拎出来理解然后运用