主流瀏覽器默認為html元素提供的滾動條不美觀,而且前端開發人員想對其通過css進行統一樣式的美化也是不可實現的。比如ie可以通過樣式來實現簡單的美化、Webkit內核瀏覽器可以控制滾動條的顯示效果,firefox則不允許用戶為滾動條定義樣式。但是對於追求友好的用戶體驗的前端開發人員,是不會被這些瀏覽器的不一致行為所阻止的。我們可以自己通過標准的html元素模擬來實現自定義的滾動條。
這里是自己在工作不太忙的時候寫出來了一個用戶可以自定義的滾動條jscroll,以下簡稱jscroll。jscroll默認只提供一種滾動條樣式,部分樣式來自google webstore ,其中有部分css3樣式主要用於實現圓角,陰影效果。為實現跨瀏覽器情況下滾動條顯示效果的一致,對於ie6, 7, 8不支持css3的瀏覽器引入了 PIE.htc 文件。下面就實現的功能以及兼容性、使用方法、具體代碼實現分別做一下講解。
實現功能以及兼容性
jscroll實現了系統默認滾動條的幾乎所有功能,比如:拖動滾動條查看內容、滾動鼠標滾輪查看內容、點擊滾動條可到達區域的上方或者下方來觸發滾動條的滾動、鍵盤上下鍵來觸發滾動條的滾動。firefox、chrome,、ie9+ 等最新瀏覽器支持css3以及js的最新API,所以沒有任何兼容性問題。ie6, 7, 8 不支持css3通過引入PIE.htc 的hack文件來做兼容處理。js方面對於不支持的API通過舊的API來做了兼容。有最大兼容性問題的瀏覽器是ie6,不支持點擊滾動條可到達區域來觸發滾動條滾動,也不支持鍵盤上下鍵來觸發滾動條的滾動。導致這個問題的原因主要是因為引入了支持css3的PIE.htc文件,如果不引入該hack文件,所有操作都能支持,沒法辦為了顯示效果的一致,只好選擇了不支持部分功能。
使用方法
使用自定義滾動條最多的情況應該是頁面彈出層,或者是頁面上某一個區域,千萬不要對整個頁面的滾動條進行自定義操作。對於需要使用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'
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文件中修改。最后附上源碼,歡迎感興趣者下載試用。