【JavaScript】面向对象的Tab栏分页切换案例

之前我们用原生js实现过Tab栏的分页切换效果,这是一种面向过程的方式,详见【JavaScript】自定义属性的应用案例——Tab栏分页切换,而今天我们来通过一种面向对象的方式来重新实现一下Tab栏分页切换。

两大编程思想:

  1. 面向过程 POP(Process-oriented programming):分析出解决问题所需要的步骤,然后用函数把步骤按顺序实现。
  2. 面向对象 OOP(Object-oriented programming):把事务分解成一个个对象,然后由对象之间分工合作。以对象功能来划分问题,而不是步骤。

先来看一下Tab栏分页切换要实现什么效果:

  1. 点击某个Tab栏,可以实现切换效果;
  2. 点击"+"按钮,可以添加一个新的Tab栏;
  3. 点击"x"按钮,可以删除当前Tab栏;
  4. 双击某个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指向具体的实例化对象

一、准备工作

  1. 在这之前,首先需要获取各种元素对象,我们把它写在构造函数里面。构造函数页面一加载就直接执行。
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');
  1. 然后要创建一个初始化函数,给元素绑定事件,想要页面一加载,事件已经绑定好了,那么就需要在构造函数里面执行这个初始化函数。
    根据功能的分析,给他们绑定对应的事件。
    另外,我们需要给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');
  1. 定义一个全局变量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();
    }
    ......//省略一下
}

二、实现切换功能

  1. 切换功能很简单,就是样式的改变,点击某个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';
}
  1. 关于排他性中清除所有元素样式的代码,我们经常会用到,于是这里封装一个新的函数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函数更新元素。
  • 因为lisection,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:

  1. 当删除的不是当前被选中的tab栏时,span的点击事件会冒泡到父元素li上。点击删除按钮时,选中该元素,删除该元素。原来处于选中状态的tab栏就不再被选中,删除之后没有被选中的tab栏了。为了防止触发父元素li的点击事件,要阻止冒泡。理想状态应该是正常删除元素,原被选中的tab栏继续保持被选中状态。
  2. 当删除的是当前被选中的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();
            }
        }
    }

实现效果如下:
在这里插入图片描述

五、实现编辑功能

  1. 当双击之后,插入一个文本框,允许编辑,相当于将原来的innerHTML改成一个文本框。这里的文本框已提前在css中写好样式。
//编辑功能
editTab(){
    
    
    // 双击禁止选中文字
    window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
    this.innerHTML='<input type="text" />';
}
  1. 然后让文本框中的值等于原来的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();
}
  1. 失去焦点后,把文本框里面的值给原来的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;
    })
}
  1. 按下回车键也可以把文本框里面的值给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的指向问题

猜你喜欢

转载自blog.csdn.net/weixin_43790653/article/details/123513375