今天是农历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月份左右在博客园有了自己的博客,时间匆匆而过,在博客园快有半年了!嗨!最后也祝福大家早点回家过年!路上一路顺风!时间也不早了,我也要休息!明天还要上班,感觉最后一个星期时间过得很慢很慢!嗨!