今天是農曆23 也是小年,在這祝福大家新年快樂!今天給大家分享的是:JS列表的下拉選單元件,因為目前專案正好要用到這個,所以提前研究了下,看到KISSY也有這麼一個元件,所以自己也封裝了一個,KISSY demo連結
KISSY元件名字叫 "一個解決大資料列表渲染效率的下拉選單元件。", 他對這個元件做了一次小優化。(假如伺服器返回10000條資料或者更多的話,那麼我們前端一次性操作10000條資料的話很會影響效能,他們做的優化是:將陣列拆分,根據瀏覽器本身的指令碼執行能力進行分批渲染。),但是目前kissy demo上有載入2000條資料的demo,在火狐下還是會有卡住的現象,如果稍不好的話 有可能會導致瀏覽器重啟的可能。而我今天做的demo和他們的功能類似,但是唯一不同點就是:假如返回10000條資料的話 我沒有對陣列分批渲染,而是迴圈10000次 把資料儲存到一個變數裡 然後一次性動態載入進來,或許這麼做和他們那種操作效率可能會低那麼點(具體的我沒有測試過)。所以我今天的標題沒有和他們那樣一起叫。所以今天的標題上:"JS列表的下拉選單元件". 首先要說明的是:一般的需求肯定是滿足的,一個下拉框也不可能有那麼多資料(一般情況下!)。
下面是我做的demo(JS列表的下拉選單元件)。JSFiddle地址如下:
基本原理:
滿足的基本功能是:一個基本下拉框,但是他與下拉框不同的是:他既可以輸入精確匹配到某一項,也可以點選下拉,也支援鍵盤上下移操作。但同時當我在輸入框輸入時候沒有匹配到某一項時候,點選文件document 那麼下拉框隱藏掉,input值為空。同時且支援靜態資料渲染 又支援post請求渲染資料。
基本的配置項如下:
如上面配置: 其中dataSource如果初始化為空陣列的話,那麼直接在內部發post請求渲染資料,否則的話 也可以渲染靜態資料:如下
dataSource: [
{text: "列表項1", value: 1},
{text: "列表項2", value: 2},
{text: "列表項3", value: 3},
{text: "列表項4", value: 4},
{text: "列表項5", value: 5},
{text: "列表項6", value: 6},
{text: "列表項7", value: 7},
{text: "列表項8", value: 8},
{text: "列表項9", value: 9},
{text: "列表項10", value: 10},
{text: "列表項11", value: 11}
]
如果dataSource 的長度大於0 的話 那麼他會按照靜態資料渲染,不會發post請求 否則的話 (如果陣列為空,支援發post請求) 去渲染資料。
對外提供的方法有:
setValue()
在外部例項話後 可以呼叫此方法 設定初始化值。比如demo頁面設定的格式如下:
// 設定初始化選擇項。
selectedItem: {
value: "4",
text: "列表項4"
}
getValue(); 獲取輸入框的值。
程式碼簡單的分析下:
首先初始化init方法:程式碼如下:
init: function(options) { this.config = $.extend(this.config, options || {}); var self = this, _config = self.config, _cache = self.cache; $('.drop-trigger').css({"left":_config.inputWidth - 20 + 'px'}); /* * 滑鼠點選輸入框時 渲染資料 */ $(_config.inputElemCls).each(function(index,item){ // 對input定義寬度 其父節點div也是根據input寬度定義的。 $(item).css({'width':_config.inputWidth}); var tagParent = $(item).closest(_config.parentCls); $(tagParent).css({'width':_config.inputWidth}); $(item).bind('keyup',function(e){ e.preventDefault(); var targetVal = $.trim($(this).val()), keyCode = e.keyCode, elemHeight = $(this).outerHeight(); var targetParent = $(this).closest(_config.parentCls); $(targetParent).css({'position':'relative'}); // 刪除標識 self._removeState(targetParent); var curIndex = self._keyCode(keyCode); if(curIndex > -1) { // 除了列舉那些鍵碼不發請求 self._keyUpAndDown(targetVal,e,targetParent); }else { // 渲染資料 self._renderHTML(targetVal,targetParent,elemHeight); // 如果值為空的話 那麼下拉選單隱藏掉 if(targetVal == '') { self._hide(targetParent); _cache.currentIndex = -1; _cache.oldIndex = -1; }else { self._show(targetParent); } } }); var targetParent = $(item).closest(_config.parentCls); $(_config.selectCls,targetParent).unbind('click'); $(_config.selectCls,targetParent).bind('click',function(){ var targetVal = $.trim($(item,targetParent).val()), elemHeight = $(item,targetParent).outerHeight(); // 渲染資料 self._renderHTML(targetVal,targetParent,elemHeight); }); }); /* * 點選document 不包括input輸入框時候 隱藏下拉框 */ $(document).unbind('click'); $(document).bind('click',function(e){ e.stopPropagation(); var target = e.target, targetParent = $(target).closest(_config.parentCls); var reg = _config.inputElemCls.replace(/^\./,''), selectCls = _config.selectCls.replace(/^\./,''); if($(target,targetParent).hasClass(reg) || $(target,targetParent).hasClass(selectCls)) { return; }else { self._hide(targetParent); } $(_config.inputElemCls).each(function(index,item){ if(!$(item).hasClass('state')) { $(item).val(''); } }); }); },
其中上面的 // 對input定義寬度 其父節點div也是根據input寬度定義的。 $(item).css({'width':_config.inputWidth}); var tagParent = $(item).closest(_config.parentCls); $(tagParent).css({'width':_config.inputWidth});
這幾句程式碼的意思是:
1 初始化時候 動態的設定input框的寬度 其中父元素的寬度也是根據input寬度來設定的,且下面的程式碼 下拉框的寬度也是根據input寬度渲染的。
2. 分別對input繫結keyup事件及下拉框小箭頭繫結點選click事件做相應的操作。首先keyup操作時,呼叫這個方法 self._removeState(targetParent);刪除相應的class (state),因為下面有當我用鍵盤下拉移到某一項時或者滑鼠點選下拉框某一項時候 會增加class(state),這樣做的目的是當我點選document時候會判斷input輸入框是否有這個class(state),如果沒有的話 清空input輸入框的值。否則的話,反之!接著判斷鍵碼 var curIndex = self._keyCode(keyCode); 這個方法.目的是為了當用上面那些鍵盤在輸入框操作時候 不發post請求(也就是說除了那些常見的鍵碼外發post請求)。如果鍵碼等於40的話 那麼執行下移操作,如果等於38的話 那麼是上移操作。否則的話 呼叫_renderHTML方法 渲染資料。(同樣當點選下拉小箭頭時候也呼叫此方法渲染資料。),下面的程式碼是點選文件document時候 首先判斷是否是輸入框或者是小箭頭的話,下拉框不做任何處理,否則的話 隱藏掉。點選document時候 做了另外一件事,就是說 如果此input沒有state類名時候清空輸入框資料。
_renderHTML方法程式碼如下:
_renderHTML: function(targetVal,targetParent,elemHeight) { var self = this, _config = self.config, _cache = self.cache; // 如果已經渲染了 先清空資料 if($('ul',targetParent).length > 0) { self._show(targetParent); $('ul',targetParent).html(''); } if(_cache.onlyCreate) { $(targetParent).append($('<ul></ul>')); _cache.onlyCreate = false; } var html = ''; /* * 如果設定了靜態資料的話 那麼直接使用靜態資料 否則的話 發post請求 * 由於程式碼沒有用 模板 所以直接for迴圈 */ if(_config.dataSource.length > 0) { for(var i = 0, ilen = _config.dataSource.length; i < ilen; i+=1) { if(_config.dataSource[i].text.indexOf(targetVal) >= 0) { html+= '<li class="dropmenu-item p-index'+i+'" data-value="'+_config.dataSource[i].value+'" data-title="'+_config.dataSource[i].text+'">'+_config.dataSource[i].text+'</li>'; }else { $('ul',targetParent).css({'border':'none'}); } } $('ul',targetParent).append(html); }else { // 發post請求 /**$.ajax({ type: 'post' });**/ // 假如返回的資料 如上所示的格式 var result = [ {text: "列表項1", value: 1}, {text: "列表項2", value: 2}, {text: "列表項3", value: 3}, {text: "列表項4", value: 4}, {text: "列表項5", value: 5}, {text: "列表項6", value: 6}, {text: "列表項7", value: 7}, {text: "列表項8", value: 8}, {text: "列表項9", value: 9}, {text: "列表項10", value: 10}, {text: "列表項11", value: 11} ]; for(var i = 0, ilen = result.length; i < ilen; i+=1) { if(result[i].text.indexOf(targetVal) >=0) { html+= '<li class="dropmenu-item p-index'+i+'" data-value="'+result[i].value+'" data-title="'+result[i].text+'">'+result[i].text+'</li>'; }else { $('ul',targetParent).css({'border':'none'}); } } $('ul',targetParent).append(html); } $('ul',targetParent).css({ "width":_config.inputWidth, 'overflow':'hidden','border':'1px solid #ccc','border-top':'none'}); $('ul,li',targetParent).css({'cursor':'pointer'}); var len = $('li',targetParent).length; if(len >= 10) { $('ul',targetParent).css({'height':'220px','overflow':'scroll'}); }else { $('ul',targetParent).css({'height':'auto','overflow':'hidden'}); } // hover事件 self._hover(targetParent); // 渲染後回撥函式 _config.renderHTMLCallback && $.isFunction(_config.renderHTMLCallback) && _config.renderHTMLCallback(); // 點選下來框某一項 self._clickItem(targetParent); },
程式碼做了如下事情:
1. 如果ul已經建立了(只建立一次) 則顯示且清空之前的資料。
2.如果設定了靜態資料的話(dataSource.length > 0) 那麼直接使用靜態資料 否則的話 發post請求.
3. 如果下拉框資料渲染時候 長度大於10的話 新增滾動條,否則的話 不新增。
接著就呼叫如下方法:
// hover事件 self._hover(targetParent); // 渲染後回撥函式 _config.renderHTMLCallback && $.isFunction(_config.renderHTMLCallback) && _config.renderHTMLCallback(); // 點選下來框某一項 self._clickItem(targetParent);
下面是所有的程式碼如下:
HTML依賴的結構如下:
<div class="parentCls"> <div class="drop-trigger"><i class="caret"></i></div> <input type="text" class="inputElem" autocomplete="off"/> </div>
其中父級元素class預設為 parentCls,可以根據自己自定義 如有需要 可以根據具體的值進行傳,input的類名class 預設為inputElem 也可以自定義。
CSS程式碼我就不貼了。可以根據自己的需要自己寫。如有需要或者可以看看JSfiddle原始碼 看看css程式碼。
下面是所有JS程式碼如下:
/** * 一個解決大資料列表渲染效率的下拉選單元件。 * @author tugenhua * @time 2014-01-21 */ function DropList(options) { this.config = { parentCls : '.parentCls', // 父元素class inputElemCls : '.inputElem', // 當前input標籤input的class inputWidth : 100, // 目標元素的寬度 selectCls : '.caret', // 下來小箭頭class hoverBg : 'hoverBg', // 滑鼠移上去的背景 isSelectHide : true, // 點選下拉框 是否隱藏 timeId : 100, // 預設多少毫秒消失下拉框 // 資料來源返回的格式如下:靜態資料 否則的話(如果陣列為空的話) 在內部發post請求 dataSource: [ {text: "列表項1", value: 1}, {text: "列表項2", value: 2}, {text: "列表項3", value: 3}, {text: "列表項4", value: 4}, {text: "列表項5", value: 5}, {text: "列表項6", value: 6}, {text: "列表項7", value: 7}, {text: "列表項8", value: 8}, {text: "列表項9", value: 9}, {text: "列表項10", value: 10}, {text: "列表項11", value: 11} ], renderHTMLCallback : null, // keyup時 渲染資料後的回撥函式 callback : null // 點選某一項 提供回撥 }; this.cache = { onlyCreate : true, // 只渲染一次程式碼 currentIndex : -1, oldIndex : -1, timeId : null // setTimeout定時器 }; this.init(options); } DropList.prototype = { constructor: DropList, init: function(options) { this.config = $.extend(this.config, options || {}); var self = this, _config = self.config, _cache = self.cache; $('.drop-trigger').css({"left":_config.inputWidth - 20 + 'px'}); /* * 滑鼠點選輸入框時 渲染資料 */ $(_config.inputElemCls).each(function(index,item){ // 對input定義寬度 其父節點div也是根據input寬度定義的。 $(item).css({'width':_config.inputWidth}); var tagParent = $(item).closest(_config.parentCls); $(tagParent).css({'width':_config.inputWidth}); $(item).bind('keyup',function(e){ e.preventDefault(); var targetVal = $.trim($(this).val()), keyCode = e.keyCode, elemHeight = $(this).outerHeight(); var targetParent = $(this).closest(_config.parentCls); $(targetParent).css({'position':'relative'}); // 刪除標識 self._removeState(targetParent); var curIndex = self._keyCode(keyCode); if(curIndex > -1) { // 除了列舉那些鍵碼不發請求 self._keyUpAndDown(targetVal,e,targetParent); }else { // 渲染資料 self._renderHTML(targetVal,targetParent,elemHeight); // 如果值為空的話 那麼下拉選單隱藏掉 if(targetVal == '') { self._hide(targetParent); _cache.currentIndex = -1; _cache.oldIndex = -1; }else { self._show(targetParent); } } }); var targetParent = $(item).closest(_config.parentCls); $(_config.selectCls,targetParent).unbind('click'); $(_config.selectCls,targetParent).bind('click',function(){ var targetVal = $.trim($(item,targetParent).val()), elemHeight = $(item,targetParent).outerHeight(); // 渲染資料 self._renderHTML(targetVal,targetParent,elemHeight); }); }); /* * 點選document 不包括input輸入框時候 隱藏下拉框 */ $(document).unbind('click'); $(document).bind('click',function(e){ e.stopPropagation(); var target = e.target, targetParent = $(target).closest(_config.parentCls); var reg = _config.inputElemCls.replace(/^\./,''), selectCls = _config.selectCls.replace(/^\./,''); if($(target,targetParent).hasClass(reg) || $(target,targetParent).hasClass(selectCls)) { return; }else { self._hide(targetParent); } $(_config.inputElemCls).each(function(index,item){ if(!$(item).hasClass('state')) { $(item).val(''); } }); }); }, // 鍵碼判斷 _keyCode: function(code) { var arrs = ['17','18','38','40','37','39','33','34','35','46','36','13','45','44','145','19','20','9']; for(var i = 0, ilen = arrs.length; i < ilen; i++) { if(code == arrs[i]) { return i; } } return -1; }, _renderHTML: function(targetVal,targetParent,elemHeight) { var self = this, _config = self.config, _cache = self.cache; // 如果已經渲染了 先清空資料 if($('ul',targetParent).length > 0) { self._show(targetParent); $('ul',targetParent).html(''); } if(_cache.onlyCreate) { $(targetParent).append($('<ul></ul>')); _cache.onlyCreate = false; } var html = ''; /* * 如果設定了靜態資料的話 那麼直接使用靜態資料 否則的話 發post請求 * 由於程式碼沒有用 模板 所以直接for迴圈 */ if(_config.dataSource.length > 0) { for(var i = 0, ilen = _config.dataSource.length; i < ilen; i+=1) { if(_config.dataSource[i].text.indexOf(targetVal) >= 0) { html+= '<li class="dropmenu-item p-index'+i+'" data-value="'+_config.dataSource[i].value+'" data-title="'+_config.dataSource[i].text+'">'+_config.dataSource[i].text+'</li>'; }else { $('ul',targetParent).css({'border':'none'}); } } $('ul',targetParent).append(html); }else { // 發post請求 /**$.ajax({ type: 'post' });**/ // 假如返回的資料 如上所示的格式 var result = [ {text: "列表項1", value: 1}, {text: "列表項2", value: 2}, {text: "列表項3", value: 3}, {text: "列表項4", value: 4}, {text: "列表項5", value: 5}, {text: "列表項6", value: 6}, {text: "列表項7", value: 7}, {text: "列表項8", value: 8}, {text: "列表項9", value: 9}, {text: "列表項10", value: 10}, {text: "列表項11", value: 11} ]; for(var i = 0, ilen = result.length; i < ilen; i+=1) { if(result[i].text.indexOf(targetVal) >=0) { html+= '<li class="dropmenu-item p-index'+i+'" data-value="'+result[i].value+'" data-title="'+result[i].text+'">'+result[i].text+'</li>'; }else { $('ul',targetParent).css({'border':'none'}); } } $('ul',targetParent).append(html); } $('ul',targetParent).css({ "width":_config.inputWidth, 'overflow':'hidden','border':'1px solid #ccc','border-top':'none'}); $('ul,li',targetParent).css({'cursor':'pointer'}); var len = $('li',targetParent).length; if(len >= 10) { $('ul',targetParent).css({'height':'220px','overflow':'scroll'}); }else { $('ul',targetParent).css({'height':'auto','overflow':'hidden'}); } // hover事件 self._hover(targetParent); // 渲染後回撥函式 _config.renderHTMLCallback && $.isFunction(_config.renderHTMLCallback) && _config.renderHTMLCallback(); // 點選下來框某一項 self._clickItem(targetParent); }, /* * 鍵盤上下移操作 * @method _keyUpAndDown * @param targetVal,e,targetParent */ _keyUpAndDown: function(targetVal,e,targetParent){ var self = this, _config = self.config, _cache = self.cache; // 如果請求成功後 返回了資料(根據元素的長度來判斷) 執行以下操作 if($('li',targetParent) && $('li',targetParent).length > 0) { var plen = $('li',targetParent).length, keyCode = e.keyCode; _cache.oldIndex = _cache.currentIndex; // 上移操作 if(keyCode == 38) { if(_cache.currentIndex == -1) { _cache.currentIndex = plen - 1; }else { _cache.currentIndex = _cache.currentIndex - 1; if(_cache.currentIndex < 0) { _cache.currentIndex = plen - 1; } } if(_cache.currentIndex !== -1) { !$($('li',targetParent)[_cache.currentIndex]).hasClass(_config.hoverBg) && $($('li',targetParent)[_cache.currentIndex]).addClass(_config.hoverBg).siblings().removeClass(_config.hoverBg); var curAttr = $($('li',targetParent)[_cache.currentIndex]).attr('data-title'); $(_config.inputElemCls,targetParent).val(curAttr); // 給當前的input元素增加一個標識 self._state(targetParent); } }else if(keyCode == 40) { //下移操作 if(_cache.currentIndex == plen - 1) { _cache.currentIndex = 0; }else { _cache.currentIndex++; if(_cache.currentIndex > plen - 1) { _cache.currentIndex = 0; } } if(_cache.currentIndex !== -1) { !$($('li',targetParent)[_cache.currentIndex]).hasClass(_config.hoverBg) && $($('li',targetParent)[_cache.currentIndex]).addClass(_config.hoverBg).siblings().removeClass(_config.hoverBg); var curAttr = $($('li',targetParent)[_cache.currentIndex]).attr('data-title'); $(_config.inputElemCls,targetParent).val(curAttr); // 給當前的input元素增加一個標識 self._state(targetParent); } }else if(keyCode == 13) { //回車操作 var curVal = $($('li',targetParent)[_cache.currentIndex]).attr('data-title'); $(_config.inputElemCls,targetParent).val(curVal); // 給當前的input元素增加一個標識 self._state(targetParent); // 點選下拉框某一項是否隱藏 下拉框 預設為true if(_config.isSelectHide) { self._hide(targetParent); } _cache.currentIndex = -1; _cache.oldIndex = -1; // 點選某一項後回撥 _config.callback && $.isFunction(_config.callback) && _config.callback(); // 按enter鍵 阻止form表單預設提交 return false; } } }, // 給當前的input元素增加一個標識 目的是判斷輸入值是否合法 _state: function(targetParent){ var self = this, _config = self.config; !$(_config.inputElemCls,targetParent).hasClass('state') && $(_config.inputElemCls,targetParent).addClass('state'); }, // 刪除input標識 _removeState: function(targetParent) { var self = this, _config = self.config; $(_config.inputElemCls,targetParent).hasClass('state') && $(_config.inputElemCls,targetParent).removeClass('state'); }, /* * hover 下拉框 */ _hover: function(targetParent){ var self = this, _config = self.config; $('.dropmenu-item',targetParent).each(function(index,item){ $(item).hover(function(){ !$(item).hasClass(_config.hoverBg) && $(item).addClass(_config.hoverBg); },function(){ $(item).hasClass(_config.hoverBg) && $(item).removeClass(_config.hoverBg); }); }) }, /* * 點選下拉框某一項 * @method _clickItem */ _clickItem: function(targetParent) { var self = this, _config = self.config; $('.dropmenu-item',targetParent).each(function(index,item){ $(item).unbind('click'); $(item).bind('click',function(e){ var target = e.target, title = $(target).attr('data-title'); $(_config.inputElemCls,targetParent).val(title); // 給當前的input元素增加一個標識 目的是判斷輸入值是否合法 !$(_config.inputElemCls,targetParent).hasClass('state') && $(_config.inputElemCls,targetParent).addClass('state'); // 點選某一項後回撥 _config.callback && $.isFunction(_config.callback) && _config.callback(); // 點選下拉框某一項是否隱藏 下拉框 預設為true if(_config.isSelectHide) { self._hide(targetParent); } }); }); }, /* * 顯示方法 * @mrthod _show {private} */ _show: function(targetParent) { var self = this, _config = self.config, _cache = self.cache; _cache.timeId && clearTimeout(_cache.timeId); if($('ul',targetParent).hasClass('hidden')) { $('ul',targetParent).removeClass('hidden'); } }, /* * 隱藏方法 * @method _hide {private} */ _hide: function(targetParent) { var self = this, _config = self.config, _cache = self.cache; _cache.timeId = setTimeout(function(){ if($(targetParent).length > 0) { !$('ul',targetParent).hasClass('hidden') && $('ul',targetParent).addClass('hidden'); }else { !$('ul').hasClass('hidden') && $('ul').addClass('hidden'); } },_config.timeId); }, /* * 給輸入框設定預設值 * @param {Object} * @method setValue {public} */ setValue: function(obj){ /** 物件格式如下 // 設定初始化選擇項。 selectedItem: { value: "4", text: "列表項4" }**/ var self = this, _config = self.config; $(_config.inputElemCls).val(obj.text); }, /* * 獲取輸入框的值 * @return value */ getValue: function() { var self = this, _config = self.config; return $(_config.inputElemCls).val(); } }; // 初始化 $(function(){ var a = new DropList({ dataSource: [] }); var selectedItem = { value: "4", text: "列表項4" }; a.setValue(selectedItem); });
外掛不足之處:
1. 在火狐或者google下 當下拉框下拉時候 按上移鍵 游標會先跳到最前面然後移到最後面,也就是說游標會移動,使用者體驗稍微有點不好,一般情況下,使用者也不會用上下移鍵,一般用滑鼠操作,但是這也是一個小bug,目前沒有找到具體的原因。我想可以用HTML5中的Range物件和 selection物件應該有辦法解決!後續有時間的話 稍微解決這麼一個bug。
2. 第二個不足之處,就是當資料量大的時候(比如資料下拉框有2000條資料甚至更多時候),前端效能肯定會有影響。淘寶kissy他是用的是對返回的陣列分批渲染,但是還是有影響的,目前沒有發現有什麼的更好的方法來解決這麼一個大資料的情況。
針對下拉框大資料的時候的個人想法:
首先我們明白 在視窗中頁面上假如有10000張圖片,我們根據 圖片離瀏覽器頂部的距離是否小於或者等於 可視區離瀏覽器頂部的距離 進行延遲載入渲染圖片,可以有效的提高效能,只載入第一螢幕的資料。那麼這個下拉框我們是否也可以根據這個原理也對它做這樣的處理:比如頁面一開始渲染的時候 我只載入10條資料且有滾動條,那麼當我下拉滾動條時候再進行分批渲染相應的資料,不管後臺返回我的是10000條資料也好或者更多,我們只關注且頁面一開始只渲染前面10條資料,後面的資料根據使用者操作下拉滾動條時候進行分別渲染出來,雖然目前我們前端是沒有辦法監聽這個事件的。目前也沒有辦法做到的,但是我今天站在使用者角度來考慮這麼一個問題的。或許隨著時間越長,未來的技術可以解決這麼一個問題的。期待中.......
總結:
2014年春節前,這篇部落格有可能是最後一篇了,如有不足之處,請大家多多指教!已經買了28號凌晨2點的火車回家,嘿嘿!明年繼續研究程式碼,研究前端技術,分享HTML5+CSS3的一些東西出來,及正要學習 資料結構與演算法。2013年9月份左右在部落格園有了自己的部落格,時間匆匆而過,在部落格園快有半年了!嗨!最後也祝福大家早點回家過年!路上一路順風!時間也不早了,我也要休息!明天還要上班,感覺最後一個星期時間過得很慢很慢!嗨!