本文详细介绍了一款名为 jscroll 的 JavaScript 自定义滚动条插件。该插件旨在解决主流浏览器原生滚动条样式不统一且难以通过 CSS 美化的问题。文章深入解析了 jscroll 的核心功能,包括拖动、滚轮、键盘及点击区域滚动,并重点阐述了针对 IE6-8 等老旧浏览器通过引入 PIE.htc 文件实现 CSS3 兼容的技术方案。同时,提供了详细的使用方法、DOM 结构创建逻辑及事件绑定代码分析,帮助开发者实现跨浏览器一致的滚动体验。
jscroll 自定义滚动条插件概述
主流浏览器默认提供的 HTML 元素滚动条外观往往不尽如人意,且前端开发人员难以通过 CSS 对其进行统一的美化。例如,IE 浏览器可以通过样式实现简单的美化,Webkit 内核浏览器可以控制滚动条的显示效果,而 Firefox 则不允许用户为滚动条定义样式。然而,对于追求友好用户体验的前端开发人员来说,这些浏览器间的不一致行为并不会成为阻碍。我们可以通过标准的 HTML 元素来模拟实现自定义滚动条。
这是本人在工作闲暇时编写的一个用户可自定义滚动条组件,简称 jscroll。jscroll 默认仅提供一种滚动条样式,部分样式灵感来自 Google Web Store,其中包含部分 CSS3 样式,主要用于实现圆角和阴影效果。为了确保跨浏览器环境下滚动条显示效果的一致性,针对不支持 CSS3 的 IE6、IE7 和 IE8 浏览器,引入了 PIE.htc 文件。接下来,将分别讲解其实现功能、兼容性、使用方法以及具体代码实现。
实现功能以及兼容性
功能实现与浏览器兼容性分析
jscroll 实现了系统默认滚动条的几乎所有功能,例如:拖动滚动条查看内容、滚动鼠标滚轮查看内容、点击滚动条可到达区域的上方或下方来触发滚动条的滚动、通过键盘上下键来触发滚动条的滚动。Firefox、Chrome、IE9+ 等最新浏览器支持 CSS3 以及 JS 的最新 API,因此不存在任何兼容性问题。对于 IE6、IE7、IE8 不支持 CSS3 的情况,通过引入 PIE.htc 的 hack 文件来做兼容处理。在 JS 方面,对于不支持的 API,通过旧版 API 做了兼容。兼容性问题的最大挑战在于 IE6,它不支持点击滚动条可到达区域来触发滚动条滚动,也不支持键盘上下键来触发滚动条的滚动。导致这个问题的原因主要是因为引入了支持 CSS3 的 PIE.htc 文件,如果不引入该 hack 文件,所有操作都能支持。但为了显示效果的一致,只好选择了不支持部分功能。
使用方法
jscroll 使用方法与配置
使用自定义滚动条最多的场景应该是页面弹出层,或者是页面上某一个区域,切勿对整个页面的滚动条进行自定义操作。对于需要使用 jscroll 的元素,需要添加自定义属性 data-scroll="true" 来告诉程序需要使用 jscroll 来替换系统默认的滚动条。同时,还需要通过添加自定义属性 data-width=""、data-height="" 来指定元素要显示的宽度和高度。jscroll 会根据用户定义的宽度和高度计算内容的显示宽度以及滚动条显示的高度,并添加交互的事件。
具体代码实现
核心代码实现与逻辑解析
jscroll 的实现逻辑并不复杂,实现具体功能的 JS 代码不到 400 行,但这里依赖了一些基础的方法,所以需要引入 squid.js 作为基础方法的支持。对滚动条样式的控制的 CSS 在一个单独的 jscroll-1.0.css 文件里面,用户可以自己修改扩展以满足自己的需求。下面是对实现具体功能的每个方法做一个简单的分析:
代码如下:
init: function(selector, context) {
selecotr = selector || 'data-scroll'
context = context || document
var elems = squid.getElementsByAttribute(selector, context)
this.initView(elems)
}init() 是初始化函数,根据指定 selector 和 context 获取需要使用自定义滚动条的元素,selector 默认是 data-scroll,上下文默认是当前 document。这里无论元素自定义属性 data-scroll="true" 或者 data-scroll="false" 都会使用自定滚动条覆盖系统默认滚动条,squid 的 getElementsByAttribute() 方法只是提供通过元素是否有指定属性来查找元素而忽略属性值,这个方法没有 jquery 选择器或者高级浏览器提供的 querySelectorAll() 方法强大,因为这里 squid 只是做最基本的支持。找到需要自定义滚动条的元素之后调用 initView 方法来初始化滚动条整体结构和显示。
代码如下:
initView: function(elems) {
var i = 0,
length = elems.length,
elem;
for(; i < length; i++) {
elem = elems[i]
if(this.hasScroll(elem)) {
this.create(elem)
}
}
this.initEvent()
}initView() 方法会首先对页面上获取的带有自定义属性 data-scroll 的元素遍历,判断每一个元素是否会出现滚动条,通过 hasScroll() 方法判断。如果元素会出现滚动条则调用 create() 方法分别创建自定义的滚动条。initView() 方法结束会调用 initEvent() 方法。
代码如下:
//是否有滚动条
hasScroll: function(elem) {
return elem.offsetHeight < elem.scrollHeight
}hasScroll() 方法用于判断元素是否会出现滚动条,返回 true 或者 false。这里忽略元素的 margin 和 padding,通过 jscroll 创建的滚动条默认 margin 和 padding 都是 0。
代码如下:
//创建滚动条元素整体结构
create: function(elem) {
var wrapper,
list,
//滚动条元素
s,
//带滚动条元素显示的高度
height = elem['data-height'] || elem.getAttribute('data-height'),
//带滚动条元素显示的宽度
width = elem['data-width'] || elem.getAttribute('data-width'),
//滚动条显示高度
value;
//wrapper
wrapper = document.createElement('div')
wrapper.className = 'jscroll-wrapper'
//forbid select text, for ie9
/*
* wrapper.onselectstart = function() {
* return false
* }
*/
squid.css(wrapper, {
height: height + 'px',
width: width + 'px'
})
squid.addClass(elem, 'jscroll-body')
//overwrite the user define style
squid.css(elem, {
overflow: 'visible',
position: 'absolute',
height: 'auto',
width: (width - 40) + 'px',
padding: '0 20px 0 23px'
})
//list
list = document.createElement('div')
list.className = 'jscroll-list unselectable'<BR> list.unselectable = 'on'
squid.css(list, {
height: (height - 5) + 'px'
})
//滚动条
s = document.createElement('div')
s.className = 'jscroll-drag unselectable'
s.unselectable = 'on'
s.setAttribute('tabindex', '1')
s.setAttribute('hidefocus', true)
list.appendChild(s)
wrapper.appendChild(list)
//把需要出现滚动条的元素包裹起来
elem.parentNode.replaceChild(wrapper, elem)
wrapper.insertBefore(elem, list)
//滚动条高度
value = this.scrollbarHeight(elem, height)
squid.css(s, {
height: value + 'px'
})
//add event
this.regEvent(wrapper)
}create() 方法用户调整创建带有自定义滚动条的元素整体结构,首先通过创建了 wrapper 元素,用于包装会出现滚动条的元素 elem 和滚动条可滚动的区域元素 list 以及滚动条元素 s。通过从出现滚动条元素设置的自定义属性 data-width、data-height 分别设置 wrapper 元素的宽度和高度。通过 scrollbarHeight() 方法计算得到了滚动条元素显示的高度,整体结构不算复杂。创建自定义滚动条整体结构之后是为滚动条元素 s 和滚动条可到达区域元素 list 添加事件处理,通过 regEvent() 方法实现。
代码如下:
//计算滚动条的高度
scrollbarHeight: function(elem, height) {
var value = elem.scrollHeight;
return (height / value) * height
}scrollbarHeight() 方法通过简单的数学计算返回滚动条元素应该显示的高度。
代码如下:
initEvent: function() {
var that = this,
_default,
elem,
top,
min,
max,
prev,
parent,
sbody,
unit;
//滚动条滚动
squid.on(document, 'mousemove', function(event) {
elem = that.scrolling.elem
if(elem !== null) {
squid.addClass(elem, 'scrolling')
top = event.clientY - that.scrolling.diffy
parent = that.ie6 ? elem.parentNode.parentNode : elem.parentNode
min = that.limits[elem].min
max = that.limits[elem].max
prev = parent.previousSibling
sbody = prev.tagName.toLowerCase() === 'div' ? prev : prev.previousSibling
_default = parseInt(sbody['data-height'] || sbody.getAttribute('data-height'), 10)
unit = (sbody.scrollHeight - _default) / max
squid.addClass(sbody.parentNode, 'unselectable')
if(top < min) {
top = min
}else if(top > max) {
top = max
}
elem.style.top = top + 'px'
that.doScroll(sbody, top * unit)
}
})
//滚动结束
squid.on(document, 'mouseup', function(event) {
elem = that.scrolling.elem
if(elem) {
prev = that.ie6 ? elem.parentNode.parentNode.previousSibling : elem.parentNode.previousSibling
sbody = prev.tagName.toLowerCase() === 'div' ? prev : prev.previousSibling
squid.removeClass(sbody.parentNode, 'unselectable')
}
that.scrolling.elem = null
that.scrolling.diffy = 0
})
}initEvent() 方法实现了为 document 元素添加 mousemove 和 mouseup 事件,mousemove 实现了在拖动滚动条元素滚动时查看的内容跟随变化。代码首先判断当前是否有拖动滚动条查看内容的操作,如果有就计算滚动条被拖动到的位置,然后计算查看内容应该到的地方。代码里对 ie6 的判断,是因为引入的 PIE.htc 文件破坏了原有的结构(为了实现跨浏览器下显示效果的一致,付出太大了!!!)。mouseup 事件处理程序实现了清除上次操作的滚动条元素。
代码如下:
//添加滚动条事件
regEvent: function(elem) {
var that = this,
sbody = elem.firstChild,
list = sbody.nextSibling,
//滚动条元素
s = list.firstChild,
//滚动条滚动最小值
min = 0,
//滚动条滚动最大值
max = list.offsetHeight - s.offsetHeight,
_default = parseInt(sbody['data-height'] || sbody.getAttribute('data-height'), 10),
unit = (sbody.scrollHeight - _default) / max,
//firefox浏览器
firefox = 'MozBinding' in document.documentElement.style,
//鼠标滚轮事件
mousewheel = firefox ? 'DOMMouseScroll' : 'mousewheel',
//opera浏览器
opera = window.oprea && navigator.userAgent.indexOf('MSIE') === -1,
//is firing mousedown event
firing = false,
//鼠标点击,定时器执行时间
interval,
//滚动条距离容器高度
top,
//滚动条当前top值
cur,
//每次滚动多少像素
speed = 18;
//变量缓存min, max
this.limits[s] = {
min: 0,
max: max
}
//scroll事件 鼠标滑动滚轮移动滚动条
squid.on(elem, mousewheel, function(event) {
var delta;
if(event.wheelDelta) {
delta = opera ? -event.wheelDelta / 120 : event.wheelDelta / 120
}else{
delta = -event.detail / 3
}
cur = parseInt(s.style.top || 0, 10)
//向上滚动
if(delta > 0) {
top = cur - speed
if(top < min) {
top = min
}
}else{//向下滚动
top = cur + speed
if(top > max) {
top = max
}
}
s.style.top = top + 'px'
that.doScroll(sbody, top * unit)
//阻止body元素滚动条滚动
event.preventDefault()
})
//ie6, 7, 8下,如果鼠标连续点击两次且时间间隔太短,则第二次事件不会触发
//拖动滚动条,点击滚动条可到达区域
squid.on(list, 'mousedown', function(event) {
var target = event.target,
y = event.clientY;
target = event.target
if(target.tagName.toLowerCase() === 'shape')
target = s
//鼠标点击元素是滚动条
if(target === s) {
//invoke elem setCapture
s.setCapture && s.setCapture()
that.scrolling.diffy = y - s.offsetTop
//鼠标移动过程中判断是否正在拖动滚动条
that.scrolling.elem = s
}else if(target.className.match('jscroll-list')){
firing = true
interval = setInterval(function() {
if(firing) {
that.mouseHandle(list, y, unit)
}
}, 80)
}
})
//鼠标松开滚动条停止滚动
squid.on(list, 'mouseup', function() {
//invoke elem releaseCapture
s.releaseCapture && s.releaseCapture()
firing = false
clearInterval(interval)
})
//滚动条元素获取焦点,可以触发keyup事件
squid.on(s, 'click', function() {
this.focus()
})
//滚动条获取焦点后,触发键盘上下键,滚动条滚动
squid.on(s, 'keydown', function(event) {
var keyCode = event.keyCode,
state = false;
cur = parseInt(s.style.top || 0, 10)
switch(keyCode) {
case 38:
top = cur - speed
if(top < min) {
top = min
}
state = true
break
case 40:
top = cur + speed
if(top > max) {
top = max
}
state = true
break
default:
break
}
if(state) {
s.style.top = top + 'px'
that.doScroll(sbody, top * unit)
}
event.preventDefault()
})
}regEvent() 方法实现了以下功能,应该是 jscroll 组件的核心方法了:
1. 鼠标在包含滚动条的元素区域,上下滚动鼠标滚轮,查看的内容跟随滚轮上下翻的功能
2. 点击滚动条可到达区域,即滚动条上方或者下方,滚动条和查看的内容向上或者向下滚动。鼠标点击滚动条可到达区域不松开,可连续滚动滚动条和查看的内容,通过调用 mouseHandle() 方法来具体实现该功能。
3. 点击滚动条元素后,可以通过键盘上下键来触发滚动条和查看内容的滚动
代码如下:
//鼠标点击滚动条可到达区域上面或者下面时,滚动条滚动
mouseHandle: function(elem, place, unit) {
var prev = elem.previousSibling,
//包含滚动条显示内容元素
a = prev.tagName.toLowerCase() === 'div' ? prev : prev.previousSibling,
//
n = elem.firstChild,
//滚动条元素
s = this.ie6 ? n.lastChild : n.tagName.toLowerCase() === 'div' ? n : n.nextSibling,
//滚动条高度
height,
//list元素距body的top值
value,
//滚动条距离容器高度
top,
//滚动条距body的top值
sTop,
//滚动条滚动最小值
min,
//滚动条滚动最大值
max,
//每点击滚动条可到达区域,滚动条向下或向上移动10px
step = 10,
//鼠标点击滚动条可到达区域距离最顶端或者最底端小于distance时,滚动条能够自动移动到最顶端或者最低端
distance = 20;
min = this.limits[s].min
max = this.limits[s].max
height = s.offsetHeight
top = parseInt(s.style.top || 0, 10)
value = squid.getOffset(elem).top
sTop = squid.getOffset(s).top
//鼠标点击滚动条下方区域,滚动条向下滚动
if(place > sTop) {
if(value + elem.offsetHeight - place < distance && (elem.offsetHeight - height - top) < distance) {
top = max
}else{
if((sTop + height + step) <= place) {
top += step
}else{
top = place - value - height
}
}
}else{
//鼠标点击区域距滚动条顶端小于滚动条长度时,滚动条自动滚动到最顶端
if(place - value < distance && top < distance) {
top = min
}else{
//滚动条距页面顶部高度减去鼠标clientY值大于step
if(sTop - place >= step) {
top -= step
}else{
top = place - value
}
}
}
if(top < min) {
top = min
}else if(top > max) {
top = max
}
s.style.top = top + 'px'
this.doScroll(a, top * unit)
}mouseHandle() 方法通过判断鼠标点击位置在页面中的位置坐标,和滚动条元素在页面中的位置来判断是点击了滚动条的上方区域还是下方区域。如果点击了下方区域则滚动条向下滚动,否则向上滚动,对于点击的位置在上方区域或者下方区域小于 distance 值时,滚动条自动滚动到最小值或者最大值。
显示效果与注意事项
显示效果
该控件的 demo 使用了淘宝网用户注册协议内容,因为 Firefox、Chrome 等高级浏览器都能保证很好的兼容性和显示效果,所以这里只展示 ie 低版本浏览器显示效果, ie 浏览器显示截图如下:
ie6下

初始化之后

滚动过程中

滚动到底部
ie7

滚动条初始化之后

滚动过程中

滚动到最底部
ie9

开始滚动前

滚动过程中

滚动到最底部
总结:基本的功能实现代码就这么多了,可能分析的不够细致,里面涉及最多的也许就是位置的计算,事件的绑定处理。如果有什么问题,欢迎一起沟通、学习、交流。
注意:PIE.htc 文件路径要放正确,引用时写成绝对路径,否则在 ie6, 7, 8 下没有 css3 的效果(如果那样我代码里所做的兼容处理就没啥意义了!),需要改变引用路径的话可以在 jscroll-1.0.css 文件中修改。最后附上源码,欢迎感兴趣者下载试用。