最终效果展示:
案例分析
这个案例主要分成上下两大部分,上面部分主要是列表显示,下面部分主要放置按钮。当点击某一列表项的时候,当前列表项则会显示被选中状态,重复点击则显示未选中状态。当所有的选项被选中的时候,全选按钮则显示红色,否则,显示灰色;当所有选项未被选中的时候,全不选按钮则显示红色,否则,显示灰色;当选中某一部分选项的时候,切换反选按钮,则未被选中的对象会显示被选中状态;按钮切换过程中,只能是一个按钮显示红色。
案例难点分析
- 如何切换选中与未选中状态
- 当全部选项被选中的时候,如何改变选中按钮的状态
- 当全部选项变成未被选中状态的时候,如何改变未选中按钮状态。
- 反选效果如何实现
结构分析
当我们拿到一张设计图的时候,我们第一步要做的不是直接撸代码,而是对设计图进行区域划分,也就是所谓的结构分析。
首先,如上图,在所有的东西的外面应该会有一个大盒子,将所有的东西包裹起来,因为这样子设置会方便往后的操作,比如将盒子里面的东西全部搬到页面的水平中间的位置。
(如上图)我们可以用 div 元素来描述最外面的盒子,然后给这个盒子设置 class 属性值 contain 进行描述,而在这个 contain 盒子里面,主要分成上下两个部分,上面这个部分,我们可以使 ul 元素去描述,我们可以给 ul 描述 class 属性值 header 进行描述,这个盒子主要是放置我们的列表内容;而同样道理,我们可以使用 div 元素描述最下面的盒子,我们可以给最下面的盒子 class 属性值 footer 进行描述,这个盒子主要放置我们的按钮组。因此,我们可以用以下代码进行描述。
<div class="contain">
<ul class="header">
</ul>
<div class="footer">
</div>
</div>
对于列表的每一项内容,我们可以使用 li 元素去描述,在 li 元素里面,我们可以放置任意的元素。
接下来,我们要分析,究竟使用什么元素是描述我们的复选框,一种方法是是使用复选框元素 check 去描述,另一种方法是就是使用图片切换效果去描述,这里,我使用的是后者,使用切换不同的背景图片,如下图,正常情况显示的是没有选中的图片,如果被点击后,就会显示有 √ 的图片(可参考下图)。
那么,问题来了,我们应该使用什么元素标签去存放我们的图片呢?这里,我使用了 span 元素标签,span 元素标签是行内标签,它可以跟 li 其他的元素横排显示,图片是要作为该元素的背景图片方式存在的。而每一项内的文字,我们可以直接在 span 元素后填写上去,如下代码:
<ul class="header">
<li><span></span>1</li>
<li><span></span>2</li>
<li><span></span>3</li>
<li><span></span>4</li>
<li><span></span>5</li>
<li><span></span>6</li>
</ul>
footer 盒子内有三个按钮,这三个按钮,我们也可以使用 span 元素去描述,span 元素用途还是蛮广的,可以作为单纯文本显示,也可以作为具有特殊样式效果存在,也可以用作为显示图标。最后,我们给每一个小按钮都添加相同的 class 属性值,以便后期调用。
<div class="footer">
<span class="btn">全选</span>
<span class="btn">全不选</span>
<span class="btn">反选</span>
</div>
样式分析
接下来,我们要控制各个元素在网页上表现形式。
设置初始样式
网页上很多元素都具有内置样式的,什么是内置样式呢?可以理解为,样式是一出生就有的,比如说,ul 列表元素,列表的每一项前面都会有小黑圆点,这个小黑圆点就是这个元素一出生就有的,而且,这个小黑圆点看起来不好看,我们要把它们去掉,有比如说,如下图,body 元素默认情况下是距离页面左上右下都具有一定的距离的,在实际开发中,我们也要把他们给去掉。
因此,我们可以在 CSS 文件中输入如下代码:
//取消所有元素默认的margin值与padding值
*{
margin:0;
padding:0;
}
//去掉ul元素默认的列表样式
ul{
list-style:none
}
效果图如下:
控制 contain 在页面居中显示
首先控制最外面的盒子在网页中水平方向居中。我们可以给 contain 这个盒子设置 margin 属性,只要给 margin 左右值设置为自动,那么,盒子自然就会在左右方向进行水平方向居中了。但是,在设置左右 marign 值的时候,我们要满足以下的条件:
- 盒子必须是块级元素,只要块级元素才能设置宽度值。
- 这个盒子要给与具体的宽度。
.contain{
//设置盒子的宽度为800px
width:800px
//控制盒子在距离上下距离100px,左右自动居中
margin:100px auto;
}
效果图:
控制 header 列表盒子在页面显示样式
首先,我们要给列表盒子添加 1 像素的边框线,接着,我们要控制列表盒子里面的内容跟边框线的上下左右都有一定的距离,这里,我们可以使用 padding 去描述,而我们还要拉大我们的列表盒子与下方的按钮盒子的距离,这里,我们可以使用 marign-bottom 去描述。因此,我们可以使用以下代码进行描述。
ul{
/* 去掉列表默认的编号样式 */
list-style: none;
/* 设置列表盒子的边框线样式 */
border: 1px solid rgba(0,0,0,0.1);
/* 设置列表盒子距离边框线上下左右四个方向的距离 */
padding: 20px 20px;
/* 增大列表盒子与下方按钮盒子的距离 */
margin: 20px;
}
效果图如下
控制列表项内容垂直居中显示
从效果图中,我们可以看出,每一项都仿佛是挤在一起,因此,我们要设置每一列表项的高度以及距离,除此之外,我们还要设置每一列表项内的文字在垂直方向中居中对齐,我们可以通过以下代码实现。
li{
/* 设置每一列表项的高度为50px */
height: 50px;
/* 设置每一列表项的行高与高度相同,当行高与高度相等的时候,
内容自动在垂直方向居中。 */
line-height: 50px;
}
效果图:
实现列表间隔色效果
我们可以使用子元素选择器 :nth-child(odd),控制列表中所有的奇数项,:nth-child(n) 可以控制某一个子元素,n 可以是阿拉伯数字 1、2、3。也可以是描述奇数的单词,odd, 代表所有奇数项,如 1、3、5 项。也可以是偶数 even,代表所有偶数项。如 2、4、6。这里,我们将奇数项的背景色改变,如以下代码:
li:nth-child(odd){
background-color: rgba(255,255,0,0.05);
}
效果图:
背景图片切换效果
因为 span 元素是行内元素,行内元素默认是无法设置宽度以及高度,因此,我们首先要将 span 元素转换为具有块级元素方法的元素。这里,我把 span 元素转换为中间元素,既具备了行内元素的特点,也具备了块级元素的特点。
li >span{
转换为行内-块级元素
display: inline-block;
设置宽度
width: 30px;
设置高度
height: 40px;
}
接下来,我们给当前 span 元素设置背景图片,background 属性是一个复合属性,如下图,在 background 属性内有很多的默认属性值。
有一些属性我们采用默认值就可以了,在这里,我们要更改其中一些值:
- background-image:图片来源地址
- background-position:图片位置
- background-size:图片大小
- background-repeat:是够重复
接下来,我们把相应的值覆盖上去即可,属性部分属性是可以不按照顺序排列的,如下代码:
/* 图片来源,默认不选中图片 不重复 图片在区域内居中位置 图片在区域内完全显示 */
background: url(img/un_check.jpg) no-repeat center/contain;
效果图:
增大按钮盒子距离左边距离
从下图我们可以看到,此时,下方按钮盒子与上方列表盒子没有对齐。
因此,我们可以考虑在下方盒子左边的位置上增加一些泡沫,可以使用 padding,也可以使用 margin。这里,我们使用 margin。
.footer{
margin-left: 20px;
}
效果图:
设置按钮样式
上面的列表盒子的样式我们已经完成了,接下来,我们完成下面的按钮盒子。从效果图中可以看出来:
按钮盒子里面的按钮是横排显示的,按钮之间是存在一定的距离的,按钮里面的文字跟按钮盒子边界是有一定的距离的。按钮盒子里面的文字是白色。按钮盒子背景色是红色。按钮有圆角效果。于是,我们可以使用如下代码进行描述:
.btn{
/* 设置按钮盒子里面的内容距离按钮盒子边界的距离 */
padding: 3px 5px;
/* 设置圆角效果 */
border-radius: 5px;
/* 控制按钮盒子里面文字颜色是白色 */
color: white;
/* 设置按钮盒子背景色是透明色灰色 */
background-color: #ccc;
/* 增大按钮之间右边的距离 */
margin-right: 10px;
}
设置伪状态
到此,案例的样式我们已经完成了 90% 了,接下来,我们优化一下用户体验效果。比如,当我们把鼠标移到每一项上面的时候,列表项的背景颜色会发生改变,这时,我们就可以使用到伪元素 :hover。首先,我们要确定触发器,也就是说,当我们触发什么状态的时候,效果才会产生,是当我们把鼠标移到列表每一项上面的时候,状态才产生,因此,触发器是列表项,列表项的元素标签是 li,因此,我们可以使用如下代码:
li:hover{
改变列表项的背景色
background-color: rgb(255, 254, 173);
改变鼠标图标,变成抓手图标
cursor: pointer;
}
同样道理,当我们把鼠标移到按钮上面的时候,鼠标图标也会变成抓手图标。因此,代码可以这样写:
.btn:hover{
cursor: pointer;
}
设置交互初始样式
在文档渲染的时候,全部按钮是没有选中的,因此,全不选按钮的背景色应该是红色才对,对于初始的样式,我们可以定义一个初始样式,如下代码:
.btn-active{
background-color: red;
}
接着,把设置好的样式添加到全不选按钮上:
<span class="btn btn-active">全不选</span>
效果图:
添加交互功能
引入 JS
在 body 结束标签前面,我们插入如下代码:
<script></script>
告诉浏览器,我们要在这个地方使用 JS。
获取相关元素
这个案例中,我们要使用 JS 控制的对象有每一个列表项以及我们的每一个按钮。这里,我们可以使用 document.querySelectorAll ('选择器') 这个方法获取有关的元素集合,比如说,我们的列表项是由很多项组成的,它就是一个集合。对于选择器,我们在这里可以采用标签选择器,因此,我们可以写如下代码:
let olis=document.querySelectorAll('ul > li');
这里要注意了,document.querySelectorAll ('选择器') 这个方法返回的是一个集合,由很多列表项组成,我们可以使用 console.dir(olis) 这个方法对获取到的对象进行打印。
在控制台上我们就可以看到我们我们想要控制的所有列表项了,呆会我们会使用循环控制每一个列表项。
同样道理,我们也可以使用 document.querySelectorAll ('选择器') 方法获取我们 footer 盒子下面的 3 个按钮组成的集合了。
let obtns=document.querySelectorAll('.btn');
接着,我们可以使用 consloe.dir(obtns) 方法,对返回的集合进行打印。
因此,我们可以使用数组下表来获取到每一个按钮对象,比如说,obtns[0] 对应的就是全选,obtns[1] 对应的就是全不选按钮,obtns[2] 对应的就是反选按钮。
添加自定义属性与事件
接下来,我们要给每一个列表添加自定义属性“bool”,用此来存储当前的是否选中状态。默认情况下,bool 的值是 false 的,当当前列表项被点击的时候,bool 的值才会变成 ture。那么,问题来了,我们如何控制到列表的每一项呢?这时,我们就可以使用循环的方法来获取数组的每一项了。
for(let i=0;i<olis.length;i++){
//默认每一个li都是没有选中的状态
olis[i].bool=false;
}
接着,我们要为每一个列表项添加点击事件。
olis[i].onclick=function(){
}
巧用“开关”切换背景图片
当列表项被点击后,列表里面的 span 元素的背景图片会被切换。因此,现在,我们要获取到 li 元素里面的 span 元素才可以了,我们可以通过 console.dir() 方法打印 li 元素,然后根据路径找到 span 元素。
我们点开 li,找到 childNodes,接着点开 childNodes,才能找到 span 元素,那么,如何去描述当前的路径呢?首先,我们将它们分级,找到谁是父亲,谁是儿子。举个例子,li 元素包含 childNdodes,因此,后者是前者的儿子,后者比前者低一级,同样道理,span 是 childNodes 的儿子,span 比前者低一级。
分清层级关系后,我们就可以使用 . 来描述关系了,点可以理解为“里面的”意思,如父亲。儿子,我们可以获取到父亲里面的儿子。又比如说,li.childNdodes 返回的是 li 里面的 childNdodes 这个儿子。那么,如何获取到 span 呢,因为 span 前面会有一个下标 0,如果看到下标 0、1、2 的话,那么久说明 span 是处在一个集合里面,是集合的话,我们就可以使用数组下标的方法获取它。因此,我们可以使用如下代码获取到 span 元素。
for(let i=0;i<olis.length;i++){
//默认每一个li都是没有选中的状态
olis[i].bool=false;
olis[i].onclick=function(){
olis[i].childNodes[0]
}
}
获取到 span 元素后,接下来我们要修改它的背景图片,我们可以使用添加新样式 classList.toggle('样式名称') 这种方法。classList.toggle('样式名称') 它会先判断当前对象是否含有此样式,如果有就不添加,如果没有就添加。
因此,我们现在 CSS 里面定义新的样式,切换背景图片:
.active{
background: url(img/check.jpg) no-repeat center/contain;
}
我们给当前被点击的列表项添加新的样式,注意,不会删掉原有样式。
olis[i].childNodes[0].classList.toggle('active');
olis[i].onclick=function(){
clear();
olis[i].bool=!olis[i].bool;
olis[i].bool?count++:count--;
olis[i].childNodes[0].classList.toggle('active');
isAll();
}
全选效果实现
我们现在已经实现了点击某一列表项的时候,当前列表显示选中状态,重复点击的话,显示没有选中状态。现在当我们把所有选项选中的时候,却出现 Bug 了。
因此,接下来,我们要实现以下效果。
- 全选按钮背景色变红
- 全不选按钮背景色变为 #ccc
要判断当前是否全部选中,我们还需要一个变量存放当前列表项的选择个数。
首先,当某个选项被点击的时候,bool 值改变当前值,下一步,我们要统计 bool=true 的个数。接着判断要不要变色。代码如下。
声明全局变量:
let count=0;
olis[i].onclick=function(){
//切换列表项的背景图片
olis[i].childNodes[0].classList.toggle('active');
//修改bool的值,每一次点击,值取反
olis[i].bool=!olis[i].bool;
//每一次点击都要修改统计后的值,如果点击后bool的值为ture
// 则count增加1,否则,减去1
olis[i].bool?count++:count--;
// 判断count是否等于数组的长度,如果相等,则给修改全选按钮的背景色为红色,否则,去掉红色样式。
count==len?obtns[0].classList.add('btn-active'):obtns[0].classList.remove('btn-active');
}
接着要取消全不选样式:
// 判断count是否等于0,如果相等,则修改全选按钮的背景色为红色,否则,去掉红色样式。
count==0?obtns[1].classList.add('btn-active'):obtns[1].classList.remove('btn-active');
效果图:
反向绑定
接下来,我们要给全选按钮添加点击事件,当点击全选按钮的时候,按钮背景色变成红色,还有就是,全部未选择选项也要全部选中。
obtns[0].onclick=function(){
// clear();
// 获取所有的列表项
for(let m=0;m<len;m++){
// 修改所有列表项的值为true
olis[m].bool=true;
// 设置每一列表项的span背景图片为选中图片
olis[m].childNodes[0].classList.add('active');
// 修改当前count为数组长度
count=len;
// 判断修改按钮是否要显示红色
count==len?obtns[0].classList.add('btn-active'):obtns[0].classList.remove('btn-active');
count==0?obtns[1].classList.add('btn-active'):obtns[1].classList.remove('btn-active')
}
}
提取函数
全选效果以及反向绑定的代码中,有相同的代码,我们可以将他们提取出来,成为一个新函数,这样就可以方便我们在往后的地方使用:
function isAll(){
count==len?obtns[0].classList.add('btn-active'):obtns[0].classList.remove('btn-active');
count==0?obtns[1].classList.add('btn-active'):obtns[1].classList.remove('btn-active')
}
全不选效果实现
根据全选的原理,我们可以轻松地书写全不选逻辑:
obtns[1].onclick=function(){
for(let n=0;n<len;n++){
olis[n].bool=false; olis[n].childNodes[0].classList.remove('active');
obtns[0].classList.add('btn-active');
count=0;
isAll();
}
}
反选效果实现
//反选
obtns[2].onclick=function(){
// clear();
for(let n=0;n<len;n++){
// 把选择状态为ture的改变成false,false改变成true
olis[n].bool?olis[n].bool=false:olis[n].bool=true;
// 改变状态,判断每一列表项当前bool的值,如果是ture,显示选择状态,如果是false,则显示不选择状态
olis[n].bool?olis[n].childNodes[0].classList.add('active'):olis[n].childNodes[0].classList.remove('active');
//将count值恢复为0
count=0;
//重新统计选中的个数
olis.forEach(function(a,b){
a.bool?count++:count;
});
//修改当前按钮的背景色
obtns[2].classList.add('btn-active');
}
}
效果图:
从效果图上看,还存在一个 Bug,当我们点击反选后,再切换到其他按钮,会出现两个被激活的按钮。解决的方法是我们新建一个函数,将所有的按钮的背景色都不使用红色。接着在自己按钮被激活的时候再添加。
定义清除样式
//清楚按钮样式
function clear(){
for(let i=0;i<obtns.length;i++){
obtns[i].classList.remove('btn-active');
}
}
分别给所有的按钮逻辑代码中添加以下代码:
clear();
案例源码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<style type="text/css">
*{
margin: 0;
padding: 0;
}
ul{
/* 去掉列表默认的编号样式 */
list-style: none;
/* 设置列表盒子的边框线样式 */
border: 1px solid rgba(0,0,0,0.1);
/* 设置列表盒子距离边框线上下左右四个方向的距离 */
padding: 20px 20px;
/* 增大列表盒子与下方按钮盒子的距离 */
margin: 20px;
}
.contain{
width: 800px;
margin: 100px auto;
}
li{
/* 设置每一列表项的高度为50px */
height: 50px;
/* 设置每一列表项的行高与高度相同,当行高与高度相等的时候,
内容自动在垂直方向居中。 */
line-height: 50px;
}
li:nth-child(even){
background-color: rgba(255,255,0,0.05);
}
li >span{
vertical-align:middle;
margin-right: 5px;
/* 转换为行内-块级元素 */
display: inline-block;
/* 设置宽度 */
width: 30px;
/* 设置高度 */
height: 40px;
/* 图片来源,默认不选中图片 不重复 图片在区域内居中位置 图片在区域内完全显示 */
background: url(img/un_check.jpg) no-repeat center/contain;
}
.footer{
margin-left: 20px;
}
.btn{
/* 设置按钮盒子里面的内容距离按钮盒子边界的距离 */
padding: 3px 5px;
/* 设置圆角效果 */
border-radius: 5px;
/* 控制按钮盒子里面文字颜色是白色 */
color: white;
/* 设置按钮盒子背景色是透明色灰色 */
background-color: #ccc;
/* 增大按钮之间右边的距离 */
margin-right: 10px;
}
.btn:hover{
cursor: pointer;
}
li:hover{
/* 改变列表项的背景色 */
background-color: rgb(255, 254, 173);
/* 改变鼠标图标,变成抓手图标 */
cursor: pointer;
}
.active{
background: url(img/check.jpg) no-repeat center/contain;
}
.btn-active{
background-color: red;
}
</style>
</head>
<body>
<div class="contain">
<ul class="header">
<li><span></span>1</li>
<li><span></span>2</li>
<li><span></span>3</li>
<li><span></span>4</li>
<li><span></span>5</li>
<li><span></span>6</li>
</ul>
<div class="footer">
<span class="btn">全选</span>
<span class="btn btn-active">全不选</span>
<span class="btn">反选</span>
</div>
</div>
<script>
let olis=document.querySelectorAll('ul > li');
let obtns=document.querySelectorAll('.btn');
// console.dir(obtns);
let count=0;
let len=olis.length;
for(let i=0;i<olis.length;i++){
//默认每一个li都是没有选中的状态
olis[i].bool=false;
//正常点击
olis[i].onclick=function(){
//切换列表项的背景图片
olis[i].childNodes[0].classList.toggle('active');
// clear();
//修改bool的值,每一次点击,值取反
olis[i].bool=!olis[i].bool;
//每一次点击都要修改统计后的值,如果点击后bool的值为ture
// 则count增加1,否则,减去1
olis[i].bool?count++:count--;
// 判断count是否等于数组的长度
count==len?obtns[0].classList.add('btn-active'):obtns[0].classList.remove('btn-active');
count==0?obtns[1].classList.add('btn-active'):obtns[1].classList.remove('btn-active');
}
}
function isAll(){
count==len?obtns[0].classList.add('btn-active'):obtns[0].classList.remove('btn-active');
count==0?obtns[1].classList.add('btn-active'):obtns[1].classList.remove('btn-active')
}
//全选
obtns[0].onclick=function(){
clear();
// 获取所有的列表项
for(let m=0;m<len;m++){
// 修改所有列表项的值为true
olis[m].bool=true;
// 设置每一列表项的span背景图片为选中图片
olis[m].childNodes[0].classList.add('active');
// 修改当前count为数组长度
count=len;
// 判断修改按钮是否要显示红色
isAll();
}
}
// 全不选
obtns[1].onclick=function(){
clear();
for(let n=0;n<len;n++){
olis[n].bool=false;
olis[n].childNodes[0].classList.remove('active');
obtns[0].classList.add('btn-active');
count=0;
isAll();
}
}
//反选
obtns[2].onclick=function(){
clear();
for(let n=0;n<len;n++){
// 把选择状态为ture的改变成false,false改变成true
olis[n].bool?olis[n].bool=false:olis[n].bool=true;
// 改变状态,判断每一列表项当前bool的值,如果是ture,显示选择状态,如果是false,则显示不选择状态
olis[n].bool?olis[n].childNodes[0].classList.add('active'):olis[n].childNodes[0].classList.remove('active');
//将count值恢复为0
count=0;
//重新统计选中的个数
olis.forEach(function(a,b){
a.bool?count++:count;
});
//修改当前按钮的背景色
obtns[2].classList.add('btn-active');
}
}
//清楚按钮样式
function clear(){
for(let i=0;i<obtns.length;i++){
obtns[i].classList.remove('btn-active');
}
}
</script>
</body>
</html>
本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。