滚动条问题
浏览器默认滚动条各不相同,感觉都是不好看的样式。而且同一种浏览器 window 和 mac 也有着比较大的区别。
于是打算从滚动条的css基础属性入手,然后制定两种解决方案,并阐述。
我想达到的效果是滚动条悬浮于盒子上,并不占用盒子宽度。类似 fixed 的效果
滚动条的组成
滚动条可以帮助盒子在固定区域内展示更多的内容,对比下面两种操作系统对于滚动条的处理,mac是比较理想的。
window 下的 chrome (92)
mac 下的 chrome (92)
滚动条基础属性
- ::-webkit-scrollbar 整个滚动条
- ::-webkit-scrollbar-button 滚动条上的按钮,按钮有上下箭头,点击可以控制滚动条的移动
- ::-webkit-scrollbar-thumb 滚动条上的滚动滑块,点击可以拖拽
- ::-webkit-scrollbar-track 滚动条轨道
- ::-webkit-scrollbar-track-piece 滚动条没有滑块的轨道部分
- ::-webkit-scrollbar-corner 当同时拥有两个方向的滚动条时的交汇部分
- ::-webkit-resizer 某些元素的 corner 部分的部分样式. (例 textarea 的可拖动按钮)
隐藏滚动条,制作虚拟滚动条
主要是以下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)
}
总结
-
实现自定义滚动条的每个步骤都是清晰明了的,需要我们耐心的讲它们拼接在一起
-
实现的原理和 el-scrollbar 基本一样,但是项目的公共组件在成本允许的前提下尽量我们自己实现。一方面可以借助这个组件整体的学习或复习遇到的知识点,很多优秀的用法可以积累下来,例如对 DOM 和 body 两者之间的交互的处理。另一方面可以定制团队的想法,虽然这个滚动条没啥太多可以定制的东西
-
监听 mousemove 事件的时候,刚开始我监听的是 document.body,会出现鼠标移出浏览器范围就失去监听,然后松开鼠标回来也能滚动。于是乎需要监听 document 上的 mousemove
-
对于鼠标事件的 event 和 直接获取 DOM 两者的位置属性对应的意思需要单独拎出来理解然后运用