javascript模拟select,jselect的方法实现


Posted in Javascript onNovember 08, 2012

由于主流浏览器对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方法如下:

globalEvent: 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 
} 
}) 
}

globalEvent实现了在document绑定click事件,然后在页面上触发点击事件的时候通过事件代理来判断当前点击元素是否是需要进行处理的目标元素,判断条件是通过元素的class,代码中语句的分支分别是:展开当前点击的jselect元素下拉、选中点击列表项、判断是否需要关闭jselect。

initView方法如下:

initView: function(elems) { 
var i = 0, 
elem, 
length = elems.length, 
enabled; for(; i < length; 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, 
length, 
option, 
options, 
value, 
text, 
obj, 
lis, 
ul, 
_default, 
icon, 
selectedText, 
selectedValue, 
div, 
wrapper, 
position, 
left, 
top, 
cssText; options = elem.getElementsByTagName('option') 
length = options.length 
for(; i < length; i++) { 
option = options[i] 
value = option.value 
text = option.innerText || option.textContent 
obj = { 
value: value, 
text: text 
} 
if(option.selected) { 
selectedValue = value 
selectedText = text 
obj['selected'] = true 
} 
data.push(obj) 
} 
lis = this.render(this.tmpl, data) 
ul = '<ul class="select-item">' + lis + '</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.innerHTML = selectedText 
div.appendChild(_default) 
//选择icon 
icon = document.createElement('span') 
icon.className = 'select-icon' 
div.appendChild(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' 
//事件绑定 
this.sysEvent(div) 
position = squid.position(elem) 
elem.style.display = 'none' 
left = position.left 
top = position.top 
cssText = 'left: ' + left + 'px; top: ' + top + 'px; display: block;' 
div.style.cssText = cssText 
}

create方法实现了将系统select数据拷贝到jselect下拉列表,jselect的层级关系是最外层有一个class为select-wrapper的元素包裹,里面有class为select-default的元素用于存放已选的元素,class为select-icon的元素用户告诉用户这是一个下拉列表,class为select-list的div元素里面包含了一个ul元素里面是从系统select拷贝的option的文本和值分别存放在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) { 
var curText = elem.innerText || elem.textContent, 
curValue = elem.getAttribute('data-value'), 
wrapper = elem.nextSibling.nextSibling, 
n = wrapper.firstChild.firstChild, 
text, 
value, 
dir, 
min = 0, 
max, 
hidden = 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.clientHeight + wrapper.scrollTop === max) 
dir = 'up' 
else 
dir = 'down' 
}else{ 
if(wrapper.scrollTop === min) 
dir = 'down' 
else 
dir = 'up' 
} 
this.inView(n, wrapper, 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(elem, wrapper, dir) { 
var scrollTop = wrapper.scrollTop, 
//已选中元素offsetTop 
offsetTop = elem.offsetTop, 
top; if(dir === 'up') { 
if(offsetTop === 0) { 
//滚动条置顶 
wrapper.scrollTop = offsetTop; 
}else if(offsetTop < scrollTop) { 
top = offsetTop - scrollTop 
//滚动条滚动到top值 
this.scrollInView(wrapper, top) 
} 
}else{ 
var clientHeight = wrapper.clientHeight; 
if(offsetTop + elem.offsetHeight === wrapper.scrollHeight) { 
wrapper.scrollTop = wrapper.scrollHeight - wrapper.clientHeight 
}else if(offsetTop + elem.offsetHeight > clientHeight + scrollTop) { 
top = (offsetTop + elem.offsetHeight) - (scrollTop + clientHeight) 
this.scrollInView(wrapper, top) 
} 
} 
}

inView方法需要判断是向上滚动还是向下滚动,scrollInView方法代码很简单就是把下拉列表外层容器的scrollTop设置为指定的值。方法实现如下
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可以通过这里下载。
Javascript 相关文章推荐
jQuery 1.8 Release版本发布了
Aug 14 Javascript
js中confirm实现执行操作前弹出确认框的方法
Nov 01 Javascript
ajax读取数据后使用jqchart显示图表的方法
Jun 10 Javascript
基于jquery实现鼠标滚轮驱动的图片切换效果
Oct 26 Javascript
jquery实现垂直和水平菜单导航栏
Aug 27 Javascript
基于iscroll.js实现下拉刷新和上拉加载效果
Nov 28 Javascript
vue项目总结之文件夹结构配置详解
Dec 13 Javascript
node实现登录图片验证码的示例代码
Apr 20 Javascript
JS异步错误捕获的一些事小结
Apr 26 Javascript
js判断复选框是否选中的方法示例【基于jQuery】
Oct 10 jQuery
js实现掷骰子小游戏
Oct 24 Javascript
vue实现顶部菜单栏
Nov 08 Javascript
js实现图片放大缩小功能后进行复杂排序的方法
Nov 08 #Javascript
jquery的ajax()函数传值中文乱码解决方法介绍
Nov 08 #Javascript
表头固定(利用jquery实现原理介绍)
Nov 08 #Javascript
Javascript继承(上)——对象构建介绍
Nov 08 #Javascript
异步javascript的原理和实现技巧介绍
Nov 08 #Javascript
找出字符串中出现次数最多的字母和出现次数精简版
Nov 07 #Javascript
jquery 如何动态添加、删除class样式方法介绍
Nov 07 #Javascript
You might like
星际争霸兵种名称对照表
2020/03/04 星际争霸
关于初学PHP时的知识积累总结
2013/06/07 PHP
mac系统下为 php 添加 pcntl 扩展
2016/08/28 PHP
PHP设计模式之适配器模式原理与用法分析
2018/04/25 PHP
ThinkPHP5 验证器的具体使用
2018/05/31 PHP
关于laravel-admin ueditor 集成并解决刷新的问题
2019/10/21 PHP
laravel框架中控制器的创建和使用方法分析
2019/11/23 PHP
Aster vs KG BO3 第二场2.18
2021/03/10 DOTA
javascript css在IE和Firefox中区别分析
2009/02/18 Javascript
jQuery 通过事件委派一次绑定多种事件,以减少事件冗余
2010/06/30 Javascript
HTML颜色选择器实现代码
2010/11/23 Javascript
JQuery1.8 判断元素是否绑定事件的方法
2014/07/10 Javascript
使用Node.js实现HTTP 206内容分片的教程
2015/06/23 Javascript
javascript+HTML5的canvas实现七夕情人节3D玫瑰花效果代码
2015/08/04 Javascript
JavaScript中利用jQuery绑定事件的几种方式小结
2016/03/06 Javascript
Javascript对象字面量的理解
2016/06/22 Javascript
折叠菜单及选择器的运用
2017/02/03 Javascript
jQuery实现动态控制页面元素的方法分析
2017/12/20 jQuery
python连接MySQL、MongoDB、Redis、memcache等数据库的方法
2013/11/15 Python
Flask框架的学习指南之开发环境搭建
2016/11/20 Python
详解python脚本自动生成需要文件实例代码
2017/02/04 Python
使用Python和xlwt向Excel文件中写入中文的实例
2018/04/21 Python
django 实现电子支付功能的示例代码
2018/07/25 Python
Pycharm无法使用已经安装Selenium的解决方法
2018/10/13 Python
Python实现FTP文件传输的实例
2019/07/07 Python
解决django xadmin主题不显示和只显示bootstrap2的问题
2020/03/30 Python
Django 后台带有字典的列表数据与页面js交互实例
2020/04/03 Python
如何将tensorflow训练好的模型移植到Android (MNIST手写数字识别)
2020/04/22 Python
澳大利亚快时尚鞋类市场:Billini
2018/05/20 全球购物
都柏林通行卡/城市通票:The Dublin Pass
2020/02/16 全球购物
如果有两个类A,B,怎么样才能使A在发生一个事件的时候通知B
2016/03/12 面试题
家长通知书家长意见
2015/06/03 职场文书
2016年度师德标兵先进事迹材料
2016/02/26 职场文书
Goland使用Go Modules创建/管理项目的操作
2021/05/06 Golang
Redis高级数据类型Hyperloglog、Bitmap的使用
2021/05/24 Redis
win10滚动条自动往上跑怎么办?win10滚动条自动往上跑的解决方法
2022/08/05 数码科技