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 相关文章推荐
JS 自定义带默认值的函数
Jul 21 Javascript
jQuery中after()方法用法实例
Dec 25 Javascript
原生js配合cookie制作保存路径的拖拽
Dec 29 Javascript
jQuery动态添加
Apr 07 Javascript
jQuery给指定的table动态添加删除行的操作方法
Oct 12 Javascript
JS库之Highlight.js的用法详解
Sep 13 Javascript
jQuery.Sumoselect插件实现下拉复选框效果
Nov 09 jQuery
JavaScript实现构造json数组的方法分析
Aug 17 Javascript
vue-cli3.0+element-ui上传组件el-upload的使用
Dec 03 Javascript
VueCli3.0中集成MockApi的方法示例
Jul 05 Javascript
layui--select使用以及下拉框实现键盘选择的例子
Sep 24 Javascript
JS+HTML实现自定义上传图片按钮并显示图片功能的方法分析
Feb 12 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
php visitFile()遍历指定文件夹函数
2010/08/21 PHP
PHP程序级守护进程的实现与优化的使用概述
2013/05/02 PHP
Session的工作机制详解和安全性问题(PHP实例讲解)
2014/04/10 PHP
phpword插件导出word文件时中文乱码问题处理方案
2014/08/19 PHP
微信公众平台DEMO(PHP)
2016/05/04 PHP
关于javascript 回调函数中变量作用域的讨论
2009/09/11 Javascript
IE中的File域无法清空使用jQuery重设File域
2014/04/24 Javascript
javascript工厂方式定义对象
2014/12/26 Javascript
学JavaScript七大注意事项【必看】
2016/05/04 Javascript
初识简单却不失优雅的Vue.js
2016/09/12 Javascript
使用node.js实现微信小程序实时聊天功能
2018/08/13 Javascript
解决vue中修改export default中脚本报一大堆错的问题
2018/08/27 Javascript
微信小程序实现九宫格抽奖
2020/04/15 Javascript
详解Vue之计算属性
2020/06/20 Javascript
python将字符串转换成数组的方法
2015/04/29 Python
Python3实现的字典、列表和json对象互转功能示例
2018/05/22 Python
Python决策树之基于信息增益的特征选择示例
2018/06/25 Python
python将字符串以utf-8格式保存在txt文件中的方法
2018/10/30 Python
python 数据生成excel导出(xlwt,wlsxwrite)代码实例
2019/08/23 Python
python 遍历pd.Series的index和value
2019/11/26 Python
pandas的相关系数与协方差实例
2019/12/27 Python
python GUI编程(Tkinter) 创建子窗口及在窗口上用图片绘图实例
2020/03/04 Python
python爬虫请求头的使用
2020/12/01 Python
购买200个世界上最好的内衣品牌:Bare Necessities
2017/02/11 全球购物
新西兰床上用品和家居用品购物网站:Adairs
2018/04/27 全球购物
英国玛莎百货澳大利亚:Marks & Spencer Australia
2019/08/30 全球购物
当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
2014/09/09 面试题
《燕子专列》教学反思
2014/02/21 职场文书
班主任经验交流会主持词
2014/04/01 职场文书
全国爱眼日活动总结
2015/02/27 职场文书
六一文艺汇演主持词
2015/06/30 职场文书
运动会跳远广播稿
2015/08/19 职场文书
初三数学教学反思
2016/02/17 职场文书
vue中data改变后让视图同步更新的方法
2021/03/29 Vue.js
浅谈如何保证Mysql主从一致
2022/03/13 MySQL
mysql sql常用语句大全
2022/06/21 MySQL