之前我们用原生js实现过Tab栏的分页切换效果,这是一种面向过程的方式,详见【JavaScript】自定义属性的应用案例——Tab栏分页切换,而今天我们来通过一种面向对象的方式来重新实现一下Tab栏分页切换。
两大编程思想:
- 面向过程 POP(Process-oriented programming):分析出解决问题所需要的步骤,然后用函数把步骤按顺序实现。
- 面向对象 OOP(Object-oriented programming):把事务分解成一个个对象,然后由对象之间分工合作。以对象功能来划分问题,而不是步骤。
先来看一下Tab栏分页切换要实现什么效果:
- 点击某个Tab栏,可以实现切换效果;
- 点击"+"按钮,可以添加一个新的Tab栏;
- 点击"x"按钮,可以删除当前Tab栏;
- 双击某个Tab栏或Tab栏内容,可以对其进行编辑。
我们可以抽取出来一个大的对象,就是tab对象。它具有的功能模块有切换功能、添加功能、删除功能、编辑功能。需要传入的参数就是要做Tab栏分页切换效果的那个元素,我们传入它的id值。
因此我们可以抽取出来这样一个类:
class Tab {
constructor(id) {
//获取元素
this.main = document.getElementById(id);
}
//切换功能
toggleTab(){}
//添加功能
addTab(){}
//删除功能
removeTab(){}
//编辑功能
editTab(){}
}
先搭建一下页面结构:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 引入样式文件 -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="tabsBox" id="tab">
<div class="tab_list">
<ul>
<li class="current"><span>Tab1</span><span class="guanbi-btn">x</span></li>
<li><span>Tab2</span><span class="guanbi-btn">x</span></li>
<li><span>Tab3</span><span class="guanbi-btn">x</span></li>
</ul>
<div class="tabAdd">
<span>+</span>
</div>
</div>
<div class="tab_con">
<section class="item">Tab1内容</section>
<section>Tab2内容</section>
<section>Tab3内容</section>
</div>
</div>
<!-- 引入js文件 -->
<script src="tab.js"></script>
</body>
</html>
* {
margin: 0;
padding: 0;
}
li {
list-style: none;
}
.tabsBox {
width: 600px;
margin: 50px auto;
}
.tab_list {
height: 30px;
border: 1px solid rgb(182, 50, 50);
}
.tab_list li {
float: left;
/* width: 100px; */
height: 30px;
padding: 0 20px;
line-height: 30px;
text-align: center;
cursor: pointer;
position: relative;
border-right: 1px solid rgb(182, 50, 50);
}
.guanbi-btn {
position: absolute;
right: 0;
height: 12px;
width: 12px;
line-height: 6px;
border-bottom-left-radius: 12px;
font-size: 4px;
background-color: #333;
color: #fff;
}
.tabAdd span {
float: left;
height: 15px;
width: 15px;
line-height: 15px;
text-align: center;
border: 1px solid rgb(182, 50, 50);
margin: 6px 0 0 6px;
cursor: pointer;
}
.tab_list .current {
border-bottom: 1px solid #fff;
}
.tab_con {
border: 1px solid rgb(182, 50, 50);
border-top: 0;
height: 300px;
padding: 10px;
box-sizing: border-box;
}
.tab_con section {
display: none;
width: 100%;
height: 100%;
}
.tab_con .item {
display: block;
}
.tab_list input {
width: 80px;
height: 20px;
}
.tab_con input {
width: 70%;
height: 60%;
}
此时,我们只需要一句代码,就可以创建一个tab对象,传入要做Tab栏切换效果的元素的id,如下:
var mytab = new Tab('tab');
然后逐个实现功能模块,在实现过程中,特别需要注意this的指向问题。这个过程对this的指向问题要求了解非常清楚,遵循谁调用指向谁的原则,类中的this指向具体的实例化对象。
一、准备工作
- 在这之前,首先需要获取各种元素对象,我们把它写在构造函数里面。构造函数页面一加载就直接执行。
class Tab {
constructor(id) {
//获取元素
this.main = document.getElementById(id);
this.ul=this.main.querySelector('.tab_list ul:first-child');
this.lis = this.ul.querySelectorAll('li');
this.spans=this.ul.querySelectorAll('li span:first-child');
this.con=this.main.querySelector('.tab_con');
this.sections=this.con.querySelectorAll('section');
this.guanbiBtns=this.main.querySelectorAll('.tab_list .guanbi-btn');
this.add=this.main.querySelector('.tabAdd');
}
//切换功能
toggleTab(){}
//添加功能
addTab(){}
//删除功能
removeTab(){}
//编辑功能
editTab(){}
}
var mytab = new Tab('tab');
- 然后要创建一个初始化函数,给元素绑定事件,想要页面一加载,事件已经绑定好了,那么就需要在构造函数里面执行这个初始化函数。
根据功能的分析,给他们绑定对应的事件。
另外,我们需要给li添加一个index属性,方便section显示对应内容,我们在用原生js实现的时候讲到过,可以去查看一下。【JavaScript】自定义属性的应用案例——Tab栏分页切换
class Tab {
constructor(id) {
//获取元素
this.main = document.getElementById(id);
this.ul=this.main.querySelector('.tab_list ul:first-child');
this.lis = this.ul.querySelectorAll('li');
this.spans=this.ul.querySelectorAll('li span:first-child');
this.con=this.main.querySelector('.tab_con');
this.sections=this.con.querySelectorAll('section');
this.guanbiBtns=this.main.querySelectorAll('.tab_list .guanbi-btn');
this.add=this.main.querySelector('.tabAdd');
this.init();
}
//初始化函数
init(){
//重新绑定事件
for(var i = 0;i<this.lis.length; i++){
this.lis[i].dataIndex=i;
this.lis[i].addEventListener('click', this.toggleTab);
this.spans[i].addEventListener('dblclick',this.editTab);
this.sections[i].addEventListener('dblclick',this.editTab);
this.guanbiBtns[i].addEventListener('click',this.removeTab);
}
this.add.addEventListener('click',this.addTab);
}
//切换功能
toggleTab(){}
//添加功能
addTab(){}
//删除功能
removeTab(){}
//编辑功能
editTab(){}
}
var mytab = new Tab('tab');
- 定义一个全局变量that,来存放最大的实例化对象。因为在实现过程中this的指向随着调用者发生变化,而我们有时需要用到实例化对象本身。
var that;
class Tab {
constructor(id) {
that = this;
//获取元素
this.main = document.getElementById(id);
this.ul=this.main.querySelector('.tab_list ul:first-child');
this.lis = this.ul.querySelectorAll('li');
this.spans=this.ul.querySelectorAll('li span:first-child');
this.con=this.main.querySelector('.tab_con');
this.sections=this.con.querySelectorAll('section');
this.guanbiBtns=this.main.querySelectorAll('.tab_list .guanbi-btn');
this.add=this.main.querySelector('.tabAdd');
this.init();
}
......//省略一下
}
二、实现切换功能
- 切换功能很简单,就是样式的改变,点击某个tab栏,他的className改为‘current’,注意排他性即可。
注意这个函数里面的所有的this指向的应该是this.lis[i],因为this.lis[i]是toggleTab函数的调用者。this.lis[i].addEventListener(‘click’, this.toggleTab); 要想使用对象的lis或者sections等,必须是that.xxx。
//切换功能
toggleTab(){
for(var i=0;i<that.lis.length;i++){
that.lis[i].className='';
that.sections[i].className='';
}
this.className='current';
that.sections[this.dataIndex].className='item';
}
- 关于排他性中清除所有元素样式的代码,我们经常会用到,于是这里封装一个新的函数clearStyle(),而且封装的函数可以把原来的that改成this了,that.clearStyle(); 这样调用的话this就是指向的tab对象(时刻注意this的指向问题!!!),只需要在需要用到的地方调用一下:
//切换功能
toggleTab(){
that.clearStyle();
this.className='current';
that.sections[this.dataIndex].className='item';
}
//清除样式 排他思想
clearStyle(){
for(var i=0;i<this.lis.length;i++){
this.lis[i].className='';
this.sections[i].className='';
}
}
实现效果如下:
三、实现添加功能
创建新元素,追加到父元素上即可。
//添加功能
addTab(){
//直接用字符串形式创建li和section,创建新的Tab页时,tab页停留在创建的新元素上,之前所停留的页面要清除样式。
that.clearStyle();
var li = '<li class="current"><span>新建Tab页</span><span class="guanbi-btn">x</span></li>';
var section = '<section class="item">新建Tab页内容</section>';
//把这两个元素追加到对应的父元素里面
that.ul.insertAdjacentHTML('beforeend',li);
that.con.insertAdjacentHTML('beforeend',section);
}
实现效果如下:
- 可以看到有一个小bug,新添加的元素没有切换效果。这是因为在执行这个函数之前,已经获取完所有的li和section,并为他们绑定了切换事件,而新添加的元素并没有添加上切换事件。此时我们就需要重新获取所有的li和section,这样才能获取到新添加的li和section,然后再重新为他们绑定事件,我们把这些操作封装到一个update函数中。在添加完元素之后,调用一次update函数更新元素。
- 因为li和section,guanbiBtns,spans这些都是对应的,所以需要一并更新。
- 此时可以优化一下代码,把li,section,guanbiBtns,spans的获取和绑定从原来的地方拿出来放到update函数里,再在初始化函数中调用一下update函数作为初始获取元素。
var that;
class Tab {
//传入要做Tab栏的元素的id
constructor(id){
that=this;
//获取元素
this.main = document.getElementById(id);
this.ul=this.main.querySelector('.tab_list ul:first-child');
this.con=this.main.querySelector('.tab_con');
this.add=this.main.querySelector('.tabAdd');
this.init();
}
//初始化函数
init(){
this.update();
//绑定事件
this.add.addEventListener('click',this.addTab);
}
//切换功能
toggleTab(){
that.clearStyle();
this.className='current';
that.sections[this.dataIndex].className='item';
}
//添加功能
addTab(){
//直接用字符串形式创建li和section,创建新的Tab页时,tab页停留在创建的新元素上,之前所停留的页面要清除样式。
that.clearStyle();
var li = '<li class="current"><span>新建Tab页</span><span class="guanbi-btn">x</span></li>';
var section = '<section class="item">新建Tab页内容</section>';
//把这两个元素追加到对应的父元素里面
that.ul.insertAdjacentHTML('beforeend',li);
that.con.insertAdjacentHTML('beforeend',section);
that.update();
}
//删除功能
removeTab(){}
//编辑功能
editTab(){}
//清除样式 排他思想
clearStyle(){
for(var i=0;i<this.lis.length;i++){
this.lis[i].className='';
this.sections[i].className='';
}
}
//更新li和section
update(){
//重新获取lis和sections和guanbiBtns
this.lis = this.ul.querySelectorAll('li');
this.sections=this.con.querySelectorAll('section');
this.guanbiBtns=this.main.querySelectorAll('.tab_list .guanbi-btn');
this.spans=this.ul.querySelectorAll('li span:first-child');
//重新绑定事件
for(var i = 0;i<this.lis.length; i++){
this.lis[i].dataIndex=i;
this.lis[i].addEventListener('click', this.toggleTab);
this.spans[i].addEventListener('dblclick',this.editTab);
this.sections[i].addEventListener('dblclick',this.editTab);
this.guanbiBtns[i].addEventListener('click',this.removeTab);
}
}
}
实现效果如下:
四、实现删除功能
记录当前点击的索引值,分别删除对应li和section,删除之后更新元素。
//删除功能
removeTab(){
var index=this.parentNode.dataIndex;
//移除对应的li元素
this.parentNode.remove();
//移除对应的section元素
that.sections[index].remove();
that.update();
}
实现效果如下:
可以看到有两个bug:
- 当删除的不是当前被选中的tab栏时,span的点击事件会冒泡到父元素li上。点击删除按钮时,选中该元素,删除该元素。原来处于选中状态的tab栏就不再被选中,删除之后没有被选中的tab栏了。为了防止触发父元素li的点击事件,要阻止冒泡。理想状态应该是正常删除元素,原被选中的tab栏继续保持被选中状态。
- 当删除的是当前被选中的tab栏时,删除之后没有被选中的tab栏了。理想状态应该是他的前一个被选中,但是要注意判断临界值。
//删除功能
removeTab(e){
//阻止冒泡
e.stopPropagation();
var index=this.parentNode.dataIndex;
//移除对应的li元素
this.parentNode.remove();
//移除对应的section元素
that.sections[index].remove();
that.update();
//如果删除的是当前没有选定状态的li,那么删除他自己后,保持原来的li仍处于选定状态
if(document.querySelector('.current')) return;
//如果删除的是当前处于选定状态的li且处于选定状态的li不在第一个,那么删除后让他的前一个li处于选定状态
if(index > 0){
that.lis[index - 1].click();
} else {
//如果删除的是当前处于选定状态的li且处于选定状态的li在第一个,那么删除后让他的后一个li处于选定状态
if(that.lis.length == 0){
return;
}else{
that.lis[index].click();
}
}
}
实现效果如下:
五、实现编辑功能
- 当双击之后,插入一个文本框,允许编辑,相当于将原来的innerHTML改成一个文本框。这里的文本框已提前在css中写好样式。
//编辑功能
editTab(){
// 双击禁止选中文字
window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
this.innerHTML='<input type="text" />';
}
- 然后让文本框中的值等于原来的tab栏标题,且处于选定状态,用户要编辑的话直接输入即可覆盖。
//编辑功能
editTab(){
// 双击禁止选定文字
window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
//原来的tab栏标题
var str=this.innerHTML;
this.innerHTML='<input type="text" />';
var input = this.children[0];
input.value = str;
//里面的文字处于被选中状态,同时也自动获得了焦点
input.select();
}
- 失去焦点后,把文本框里面的值给原来的span/section元素,只需要把innerHTML改成只有值,巧妙删除掉了文本框。
//编辑功能
editTab(){
// 双击禁止选定文字
window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
var str=this.innerHTML;
this.innerHTML='<input type="text" />';
var input = this.children[0];
input.value = str;
//里面的文字处于被选中状态,同时也自动获得了焦点
input.select();
//失去焦点后,把文本框里面的值给span
input.addEventListener('blur', function () {
this.parentNode.innerHTML = this.value;
})
}
- 按下回车键也可以把文本框里面的值给span/section元素,相当于调用了一次blur函数。
//编辑功能
editTab(){
// 双击禁止选定文字
window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
var str=this.innerHTML;
this.innerHTML='<input type="text" />';
var input = this.children[0];
input.value = str;
//里面的文字处于被选中状态,同时也自动获得了焦点
input.select();
//失去焦点后,把文本框里面的值给span
input.addEventListener('blur', function () {
this.parentNode.innerHTML = this.value;
})
//按下回车键也可以把文本框里面的值给span
input.addEventListener('keyup', function (e) {
if (e.keyCode == 13) {
this.blur();
}
})
}
实现效果如下:
总结
至此,我们就实现了所有的效果。通过这个案例可以深入了解面向对象的编程思想,同时切实体会了this的指向问题。