如何通过jQuery/CSS重画select元素样式

此文在21日有修改。

话说某个晚上,在我发完如何重绘checkbox之后秋水菊苣问我如何重画一个select元素,我当时还觉得大概会是个很简单的事情,就接了这个科研任务……可是真正上手写的时候才发现(;´ ༎ຶ Д ༎ຶ)σ 太他娘的坑了(;´ ༎ຶ Д ༎ຶ)σ 妈妈我再也不造轮子了(;´ ༎ຶ Д ༎ຶ)σ 造轮大法一点也不好(;´ ༎ຶ Д ༎ຶ)σ 我的信仰崩溃了。

言归正传,select元素其实一直都挺恶心的,最近接商单的时候也是因为select不好看煞风景,客户让我解决一下。我刚开始用了一个消除掉mouse-eventspan给盖上了,不过IE9下根本不兼容ヽ( ° ▽°)ノ,于是我们就只能开始自己徒手造了。

由于这次的代码特别长,所以先来看这个轨道图:

HTML这部分非常非常简单,扔个select进去:

1
2
3
4
5
6
7
8
9
<select>
<option value="a">AAA</option>
<option value="b">CCC</option>
<option value="c">AAA</option>
<option value="d">CCC</option>
<option value="e">AAA</option>
<option value="f">BBB</option>
<option value="g">AAA</option>
</select>

一般情况下是先HTML再CSS最后JS这样的书写步骤,不过今天的比较特殊,让我们先来写JS,因为CSS需要根据JS来调整。

下面的内容对应轨道图的图1和图2:

看着轨道图,先给select上个套($(document).ready略):

1
$('select').wrap('<span class="s_select"></span>');

然后在容器里面追加内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
$('.s_select').append('<button class="s_choosen"></button><ul class="s_select_body"></ul>');
$('select').each(function() {
var options = [], values = [], x;
$(this).children('option').each(function() {
options.push($(this).html());
values.push($(this).attr('value'));
});
$(this).next('.s_choosen').html(options[0]);
var selectBody = $(this).nextAll('.s_select_body');
for (x in options) {
selectBody.append('<li val="' + values[x] + '">' + options[x] + '</li>');
}
});

没啥复杂的。

不过有一个地方需要强调,也是我最近领悟出来的东西:无论啥情况都不要让JS去自动初始化变量,推荐全手动初始化,不然会闹很多作用域的毛病。

var定义出来的变量是仅限当前作用域不对父类产生任何干涉的,具体有没有干涉到自己去看看netbeans的代码高亮就好了( • ω•́ )。

下面的内容对应轨道图的图3:

要触发一系列selection操作都需要按.s_choosen,所以$('.s_choosen').click(function(){…});下面所有的代码都包裹在这里面。

1
2
3
var that = this;
var selectBody = $(this).next('.s_select_body');
var selectList = selectBody.children('li');

that变量存储这个button,用selectList存储遍历出的li的结果。这里有一点需要强调,如果一个遍历结果需要被多次复用,那么一定要将这个遍历结果存储到一个单独的对象中,再进行进一步操作,否则将造成不必要的资源损耗。

1
if (!selectBody.hasClass("selected")) {

当selectBody这个选择器有selected这个属性的时候(这个属性表示option容器已经展开,后面会提到。)

1
selectBody.addClass("selected");     //【1】

这就是刚刚说的给option容器添加标识,我在这里打了个标记,一会在CSS中找这个标记,他俩是对应的哦(๑•̀ㅂ•́)و✧。

1
2
3
4
5
6
7
8
setTimeout(function() {
$("body").one("click.s_select_body", function() {
$('.s_select_body').each(function() {
$(this).removeClass('selected');
$(document).off('.s_select_keydown');
});
});
}, 1);

当在文档任何一处点击时,让所有的option列表都缩回去。这里用了一个timeout延迟出现,因为你去点击.s_choosen时也算在文档上点击了一下,不设置延迟的话这个事件会被一并触发,所以我设定了当.s_choosen被点击后一百毫秒才监听文档点击事件。
jQuery的one方法是只绑定一次,触发完毕后自动自杀。
下面定义键盘事件要用到的变量:

1
2
3
4
5
var _S_ = {
select: {
itemId: -1,
totalNum: selectList.length - 1
}};

这段代码是拿来存储键盘事件的指针的,新建了一个对象,对象里存储的是当前鼠标指向的option序号和总共有多少个optionselectList.length返回的是selectList这个选择器里有多少个元素,这里是对jQuery选择器的一个活用。我之前的文章有讲过jQuery选择器和JS原生选择器互换的用法,在这里就不说了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$(document).on('keydown.s_select', function(event) { //当键盘按下的时候触发这个监听器
selectList.each(function() {
$(this).addClass('keyboard'); //【2】对应轨道图上的“屏蔽鼠标样式、唤起键盘样式”,这个在CSS里会特别说明
});

$(selectList).one('mousemove.s_select_key_mouse', function() { //对应轨道图上的“鼠标在指定区域内发生移动”
_S_.select.itemId = -1;
selectList.each(function() {
$(this).removeClass('selected keyboard'); //对应轨道图上的“取消键盘样式”
});
});


//因为需要被复用,所以这里定义了一个键盘切换option指针的函数
function changeItem() { //这部分是控制option菜单无尽滚动的,当滚动到最后一个时,再按下下键就滚到第一个,反之亦然
if (_S_.select.itemId < 0)
_S_.select.itemId = _S_.select.totalNum;
if (_S_.select.itemId > _S_.select.totalNum)
_S_.select.itemId = 0;

selectList.each(function() {
$(this).removeClass("selected"); //先移除所有option的指针标记
});

$(selectList[_S_.select.itemId]).addClass("selected"); //【3】再单独给被选中的项目添加指针标记
}

if (event.which === 38) {
_S_.select.itemId -= 1; //键盘方向键上对应的ASICII码是38,当按下的是上时指针上滚
changeItem(); //调用切换指针的复用类
}
if (event.which === 40) {
_S_.select.itemId += 1; //ヾ(:3ノシヾ)ノシ
changeItem();
}

if (event.which === 13) {
$(that).blur();
//在你进行键盘操作的时候其实button一直都是保持焦点的,如果当按下回车时,不先让button失焦,
//button的Click事件就会被触发,导致option列表被重新显示出来,这个bug我调了一个小时( ˘•ω•˘ )
$(selectList[_S_.select.itemId]).click(); //对应轨道图的“模拟点击列表”
}

});

解释下为啥在listener内定义function,其实这很奇葩,但是有的时候选择器异常繁杂,在listener里定义方法就能规避很多问题,比如很方便的使用this之类的。

然后说一下这行:$(document).off(‘.s_select_key_mouse’);

我在设置监听的时候为这个监听单独命了一个名:$(document).on(‘keydown.s_select’, function(event) {…});

这样做是为了避免在解绑监听的时候对其他监听造成干扰,最小影响和独立命名空间的原则需要时刻注意。

然后,mousemove那个Listener必须使用one或者去用off自杀,这不只是代码规范的问题,引一段W3C的话:

注意:用户把鼠标移动一个像素,就会发生一次 mousemove 事件。处理所有 mousemove 事件会耗费系统资源。请谨慎使用该事件。

你的页面会卡飞。

1
2
3
4
5
6
7
8
$('.s_select_body>li').click(function() {
var selectObject = $(this).parents('.s_select').children('select');
selectObject.val($(this).attr('val'));
selectObject.next('.s_choosen').html($(this).html());
$(document).off(".s_select");

selectObject.nextAll('.s_select_body').removeClass('selected');
});

option被按下的时候开始给select进行赋值操作,并改变button的文字,改成什么呢~改成被选中的option呗(≖ᴗ≖๑)

最后一行是移除所有键盘选中的标识,没啥说的。

1
2
3
4
5
6
7
8
9
setTimeout(function() {
$("body").on("click.s_select_body", function() {
$('.s_select_body').each(function() {
$(this).removeClass('selected');
});

$("body").off(".s_select_body");
});
}, 100);

这样当你点击了option或者是文档其他位置都能把ul缩回去,点击option的时候有单独的事件监听给select对象赋值,这边再隐藏掉option容器,这样就完成了一次操作。

为了防止你有啥奇葩JS或者奇葩代码,最后我们加上一个保险的东西:

1
event.preventDefault();

禁掉默认行为。

最后:

1
2
3
} else {
$("body").click();
}

容器已经显示出来的话就点一下body把所有容器都归位(此处对应轨道图的“是”这一部分)。

JS部分就完成啦。

接下来写CSS(实际上我在写这个东西的时候也是差不多按照这么个顺序写的。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
.s_select{
position:relative;
}

.s_select select{
display:none;
}

.s_select .s_choosen{
width:126px;
height:26px;
padding:0 26px 0 0;
background:white;
border:1px solid #CCC;
border-radius:13px;
outline:none;
position:relative;
}

.s_select .s_choosen::after{
content:'>';
top:-1px;
right:-1px;
width:26px;
height:26px;
color:white;
background:#00a2ff;
border-radius:50%;
line-height:26px;
text-align:center;
display:block;
position:absolute;
-webkit-transform: rotate(90deg);
}

.s_select .s_choosen:hover{
border:1px solid #DDD;
}

.s_select .s_choosen:hover::after{
background:#0096ff;
}

.s_select .s_choosen:active{
border:1px solid #BBB;
}

.s_select .s_choosen:active::after{
-webkit-transform: rotate(-90deg);
}


.s_select .s_select_body{
top:19px;
left:15px;
height:0;
width:90px;
border:1px solid transparent;
box-sizing:border-box;
overflow:hidden;
position:absolute;
}

.s_select .s_select_body.selected{ //【1】
border:1px solid #ddd;
height:auto;
}

.s_select .s_select_body li{
padding:5px 10px;
}

.s_select .s_select_body li:hover{
background:#deedf1;
}

.s_select .s_select_body li.keyboard:hover{ //【2】
background:transparent;
}

.s_select .s_select_body li.selected{ //【3】
background:#deedf1 !important;
}

说说打标注的地方,【2】是当触发键盘事件时,鼠标指向的option不高亮,否则页面中会出现两个高亮,很奇葩。

如果把3写在2前面还不加important,keyboard:hover不高亮了你正常键盘操作的时候鼠标指向的那个option会一直不高亮,所以这里这么个顺序写它。

最最后,经验:在调EventListener的事件时如果出现了莫名其妙的bug优先考虑有没有冲突事件被触发了。

之后,没了(∫・ω・)∫

很简单吧(∫・ω・)∫

最后,DEMO在这里,原谅我桀骜不羁的文件名ヾ(:3ノシヾ)ノシ