由於主流瀏覽器對select元素渲染不同,所以在每種瀏覽器下顯示也不一樣,最主要的是預設情況下UI太粗糙,即使透過css加以美化也不能達到很美觀的效果。這對於我們這些專注於UX的前端開發人員是無法容忍的。於是在專案不太忙的時候,就計畫寫一個模擬的select控制項出來。接下來就把實作的細節、遇到的問題、如何使用跟大家分享一下。
1. 實作細節
init: function(context) {
//取得指定上下文所有select元素
var elems = squid.getElementsByTagName('select', context)
this.globalEvent()
this.initView(elems)
}
在一個使用者註冊的應用程式場景,有多個select元素。模擬的select控制項(以下簡稱jselect)初始化方法會取得頁面上所有select元素,然後綁定全域事件globalEvent,初始化頁面顯示initView。 globalEvent方法如下:
globalas: function() >//document 新增click事件,使用者處理每個jselect元素展開關閉
var target,
className,
elem,
wrapper,
status,
that = this;
squid.on(document, 'click', function(event) {
target = event.target,
className = target.className;
switch(className) { case 'select-icon':
case 'select-default unselectable':
elem = target.tagName.toLowerCase() === 'div' ? target : target.previousSibling wrapper = elem .nextSibling.nextSibling
//firefox 滑鼠右鍵會觸發click事件
//滑鼠左鍵點選執行
if(event.button === 0) {
//初始化選取元素
that.initSelected(elem)
if(squid.isHidden(wrapper)) {
status = 'block'
//關閉所有展開jselect
that.closeSelect()
}else{
status = 'none'
}
wrapper.style.display = status
elem.focus()
}else if(event.button === 2){
wrapper.style.display = 'none'
}
that.zIndex(wrapper)
break
case 'select-option':
case 'select-option selected':
if(event.button === 0) {
that.fireSelected(target, target.parentNode.parentNode.previousSibling.previousSibling)
wrapper.style.display = 'none'
}
break
default:
while(target && target.nodeType !== 9) {
if(target.nodeType === 1) {
if(target.className === ' select-wrapper') {
return
}
}
target = target.parentNode
}
that.closeSelect()
break
}
that.closeSelect()
break
}
}
} )
}
globalEvent實現了在document綁定click事件,然後在頁面上觸發點擊事件的時候通過事件代理來判斷當前點擊元素是否是需要進行處理的目標元素,判斷條件是透過元素的class,程式碼中語句的分支分別是:展開目前點選的jselect元素下拉、選取點擊清單項目、判斷是否需要關閉jselect。
程式碼如下:
var i = 0,
elem,
length = elems.length,
enabled;
for(; i elem = elems[i]
enabled = elem.getAttribute('data-enabled')
//使用系統select
if(!enabled || enabled === 'true')
continue
if(squid.isVisible(elem))
elem.style.display = 'none'
this.create(elem)
}
}
initView實作了將需要使用jselect替換的select元素先隱藏然後呼叫create方法,產生單一jselect的整體結構並插入到頁面並替代預設select位置。
create方法如下:
create: function(elem) {
var data = [],
i = 0,
長度,
選項,
選項,
值,
文本,
obj,
lis,
ul,
_default,
圖標,
selectedText,
selectedValue,
div,
包裝,
selectedValue,
div,
包裝,
selectedValue,
div,
包裝,
selectedValue,
div,
包裝,
selectedValue,
div,
包裝,
位置,
左,
頂部,
cssText;
options = elem.getElementsByTagName('option')
length = options.length
for;
option = options[i]
value = option.value
text = option.innerText || option.textContent
obj = {
value:值,
text: 文字
}
if(option.selected) {
selectedValue = 值
selectedText = text
obj['selected'] = true
}
obj['selected'] = true
}
data.push(obj)
}
lis = this.render(this.tmpl, data)
ul = ''
//
div = document.createElement('div')
div.style.display = 'none'
div.className = 'select-wrapper'
//已選取元素
_default = document.createElement('div')
_default.className = 'select-default unselectable'
_default.unselectable = 'on'
//讓div元素能夠取得焦點
_default.setAttribute('tabindex', '1')
_default.setAttribute('data-value', selectedValue)
_default.setAttribute('hidefocus', true)
_default.setAttribute('hidefocus', true) div.appendChild(_default)
//選擇icon
icon = document.createElement('span')
icon.className = 'select-icon'
div.append. (icon)
//下拉清單
wrapper = document.createElement('div')
wrapper.className = 'select-list hide'
wrapper.innerHTML = ul
//生成新的元素
div.appendChild(wrapper)
//插入到select元素後面
elem.parentNode.insertBefore(div, null)
//取得select元素left top值
/ /先設定select顯示,取完left,top值後重新隱藏
elem.style.display = 'block'
//事件綁定
}
create方法關係實作了將系統select資料複製到jselect下拉列表,jselect的體係是最外層有一個類別為select -wrapper的元素包裹,裡面有class為select-default的元素用於買家已選的元素,class為select-icon的元素用戶告訴用戶這是一個下拉列表,class為select-list的div元素裡麵包含了裡面的一個ul元素是從系統select拷貝的選項的文字和值分別存放在li元素的文字和data-value屬性。 sysEvent方法是為jselect新增鍵盤點擊展開關閉下拉清單事件以及上下選擇下拉元素回車選取下拉元素事件。 squid.position方法用來取得系統select元素相對於其offsetParent的位置,這裡和取得系統select元素的offset是有區別的。其實就是取得自己的offset得到top,left值分別對應offsetParent取得的然後offset的top,left值。最後是將jselect插入到系統select元素後面,顯示到頁面。
jselect建立的基本流程就是上面描述的這樣,剩下的就是細節地方的實現,也就是說:點擊展開下拉顯示上次已選擇的元素,具體實現該功能是initSelected方法如下
複製程式碼
程式碼如下:
initSelected: function(elem) {
initSelected: function(elem) { elem.innerText || elem.textContent,
curValue = elem.getAttribute('data-value'),
wrapper = elem.nextSibling.nextSibling,
n =wrapper.firstChild.first.文字,
值,
目錄,
最小值= 0,
最大值,
隱藏= false;
for(; n; n = n.nextSibling) {
text = n.innerText || n.textContent
value = n.getAttribute('data-value')
if(curText === text && curValue === value) {
//顯示已選取的元素
if( squid.isHidden(wrapper)) {
wrapper.style.display = 'block'
hidden = true
}
max =wrapper.scrollHeight
if(n.offsetTop > (max / 2)) {
if(wrapper.clientHeightwrapper.scrollTop === max)
dir = '向上'
else
dir = '向下'
}else{
if(wrapper.scrollTop === min) dir = '向下' else dir = '向上' } this .inView(n, 包裝器, dir) if(hidden) wrapper.style.display = 'none' this.activate(n) break } } }
此方法接收class為select-default的div元素即用於存放使用者已選取內容的元素,具體實作方式是先遍歷所有選項取得class有selected的li元素,透過activate方法標示為目前已選取的元素。這裡有一個需要計算的地方,就是每次展開下拉清單都要將已選取的元素捲動到頁面視覺區。因為有可能下來列表內容很多,但是下拉列表的外層select-list會有一個最大的高度,超過最大高度會出現滾動條,默認不做計算的話有可能已選中的元素會在滾動條下面或者是捲軸上面,所以需要透過計算來重置容器捲軸的位置。具體是已選取內容顯示到捲軸的上面還是下面需要根據已選取元素的offsetTop值是否大於外層容器select-list的實際高度一半,把已選取元素顯示到視覺區的方式是inView方法。 inView方法如下
inView: function(el del), wrapper ) {
var scrollTop = wrapper.scrollTop,
//已選取元素offsetTop
offsetTop = elem.offsetTop,
top;
if(dir === 'up' ) {
if(offsetTop === 0) {
//捲軸置頂
wrapper.scrollTop = offsetTop;
}else if(offsetTop top = offsetTop - scrollTop
//捲軸捲動到top值
this.scrollInView(wrapper, top)
}
}else{
var clientHeight = wrapper.clientHeight;
ifentHeight = wrapper.clientHeight;
ifentHeight = wrapper.clientHeight; (offsetTop elem.offsetHeight === wrapper.scrollHeight) {
wrapper.scrollTop = wrapper.scrollHeight - wrapper.clientHeight
}else if(offsetTop elem.offsetHeight > cliclientHeight rollToptop) elem.offsetHeight) - (scrollTop clientHeight)
this.scrollInView(wrapper, top)
}
}
}
scrollInView: function(elem, top)
setTimeout(function() {
elem.scrollTop = top
}, 10)
}
這個方法實作放到了setTimeout裡面做了一個延遲加入到javascript執行佇列裡面,主要解決的是IE8下展開下拉列表滾動條會最終滾動到頂部,忽略代碼設定的scrollTop(從表現上來看好像對scrollTop的設定也能生效,但是最後會重置滾動條到頂部,不知道IE8為什麼會有這個問題。
整個的實作細節大致就這麼多,鍵盤上下鍵回車鍵,關閉下拉清單邏輯都很簡單。
遇到的問題
如何讓div獲取焦點來回應鍵盤keydown, keyup, keypress事件,到谷歌(非常時期谷歌都不好用了,沒辦法誰讓這是咱的特色呢)找一些資料最後發現需要為div元素設定tabindex屬性,這樣就可以讓div元素取得焦點,來回應使用者的操作。因為瀏覽器在預設情況下雙擊或者是點擊太頻繁的話會選中當前區域,為了取消這個預設操作給用戶一個好的體驗需要為div元素添加一個屬性unselectable,不過這個屬性只能適用於IE瀏覽器,其他瀏覽器下可以透過加上一個class名字是unselectable來避免這個問題。其他的問題都是邏輯上的控制,還有一些位置的計算了,這裡就不再說了。
使用方法 首先是在頁面模板把希望透過jselect替換的元素隱藏或不做任何處理,預設情況下jselect會取得頁面所有select依序替換,如果不希望jselect替換的select元素
需要加入自訂屬性data-enabled="true"。當然加入data-enabled="false"和沒有這個自訂屬性一樣都會被jselect取代。在使用的過程中可能對於佈局結構比較複雜的頁面還會有其他的問題,因為我測試的頁面結構很簡單,所以可能沒有測試出來。
使用jselect需要先引入squid.js,然後引入jselect-1.0.js, jselect-1.0.css文件,在需要呼叫jselect的地方透過以下的呼叫方式來初始化jselect:squid.swing.jselect();
註:jselect原始碼以及demo可以透過
這裡下載。