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 相关文章推荐
兼容ie、firefox的图片自动缩放的css跟js代码分享
Jan 21 Javascript
使用js声明数组,对象在jsp页面中(获得ajax得到json数据)
Nov 05 Javascript
分享纯手写漂亮的表单验证
Nov 19 Javascript
javascript作用域链(Scope Chain)用法实例解析
Nov 30 Javascript
js实现一个简单的数字时钟效果
Mar 29 Javascript
JavaScript切换搜索引擎的导航网页搜索框实例代码
Jun 11 Javascript
JavaScript数据类型和变量_动力节点Java学院整理
Jun 26 Javascript
zTree树形菜单交互选项卡效果的实现方法
Dec 25 Javascript
基于Vue组件化的日期联动选择器功能的实现代码
Nov 30 Javascript
详解单页面路由工程使用微信分享及二次分享解决方案
Feb 22 Javascript
vue+animation实现翻页动画
Jun 29 Javascript
vue mvvm数据响应实现
Nov 11 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编码规范的深入探讨
2013/06/06 PHP
php 模拟GMAIL,HOTMAIL(MSN),YAHOO,163,126邮箱登录的详细介绍
2013/06/18 PHP
使用php的HTTP请求的库Requests实现美女图片墙
2015/02/22 PHP
如何用PHP来实现一个动态Web服务器
2015/07/29 PHP
PHP实现的同步推荐操作API接口案例分析
2016/11/30 PHP
一份老外写的XMLHttpRequest代码多浏览器支持兼容性
2007/01/11 Javascript
Javascript 实现TreeView CheckBox全选效果
2010/01/11 Javascript
深入理解JavaScript作用域和作用域链
2011/10/21 Javascript
javascript-表格排序(降序/反序)实现介绍(附图)
2013/05/30 Javascript
JS验证身份证有效性示例
2013/10/11 Javascript
教你如何使用node.js制作代理服务器
2014/11/26 Javascript
javascript中Object使用详解
2015/01/26 Javascript
微信企业号开发之微信考勤Cookies的使用
2015/09/11 Javascript
JavaScript中Function函数与Object对象的关系
2015/12/17 Javascript
一篇文章掌握RequireJS常用知识
2016/01/26 Javascript
Bootstrap对话框使用实例讲解
2016/09/24 Javascript
利用Js的console对象,在控制台打印调式信息测试Js的实现
2016/11/26 Javascript
一步步教你利用webpack如何搭一个vue脚手架(超详细讲解和注释)
2018/01/08 Javascript
JavaScript多态与封装实例分析
2018/07/27 Javascript
vue-cli 默认路由再子路由选中下的选中状态问题及解决代码
2018/09/06 Javascript
vue + typescript + 极验登录验证的实现方法
2019/06/27 Javascript
vue通过数据过滤实现表格合并
2020/11/30 Javascript
layui实现form表单同时提交数据和文件的代码
2019/10/25 Javascript
JavaScript原型式继承实现方法
2019/11/06 Javascript
开发Node CLI构建微信小程序脚手架的示例
2020/03/27 Javascript
js实现磁性吸附的示例
2020/10/26 Javascript
JS相册图片抖动放大展示效果的示例代码
2021/01/29 Javascript
简单介绍Python中的filter和lambda函数的使用
2015/04/07 Python
python实现中文分词FMM算法实例
2015/07/10 Python
python: 自动安装缺失库文件的方法
2018/10/22 Python
给我一面国旗 python帮你实现
2019/09/30 Python
Python使用docx模块实现刷题功能代码
2020/02/13 Python
商务会议邀请函
2014/01/09 职场文书
银行转正自我鉴定
2014/09/29 职场文书
2016年春节慰问信息
2015/03/25 职场文书
springboot入门 之profile设置方式
2022/04/04 Java/Android