昨天呢,介绍了js中类的概念,以及使用类创建对象的过程。今天就用js中的类实现一个小的功能,动态添加、删除标签页。emmmmm,也有点像tab栏切换,不过比tab栏切换多了添加和删除的功能。
案例说明
js动态实现标签页的创建和删除。这个案例呢,要用类来实现,Tab就是本次案例的类。但在写之前需要来分析并抽象类的特征和行为,也就是对应的类的属性和方法。
这里呢,我把元素的ID作为属性,增删改查的四个功能作为类的共有方法。因为一个网页中很可能出现多次这样的需求,我们只需设置好对象的ID就能创建一个Tab类的实例。
Tab类以下的四个功能
- 点击标签栏的选项可以实现切换的效果
- 点击 + 可以创建标签栏的选项以及对应的标签页
- 点击 x 可以删除标签栏的选项以及对应的标签页
- 双击标签栏选项或者标签页中的内容,可以修改它们的内容
根据Tab类的四个大功能,我们可以先搭建出类的基本结构
class Tab{
constructor(id){
this.main = document.querySelector("#tab");
}
// 初始化操作,用来让相关元素绑定事件的
init(){}
toggleTab(){}
addTab(){}
removeTab(){}
exidTabContent(){}
}
var tab = new Tab("#tab");
这里说一下init() 函数的作用,它就是用来给元素绑定事件的,一般是调用类的方法。
很多时候,我们的需求绝大多数都是数据的增删改查,从而在考虑问题的时候也会往这方面考虑的。ok,介绍完了,下面可以先下实现的效果。
切换效果
这个切换效果我之前写过了不止一遍,核心思想就是利用排他思想,就是添加和移除类的操作。
代码
// 切换功能
toggleTab() {
// 方法中this指向方法调用者 这里是 —— li标签
// this.index 当前li的索引号
// (1) 切换类 排他思想:干掉所有人留下自己
// this指向li,li中没有li标签的集合 只有实例对象也就是类中的构造函数的this才有li的集合
// 其他li标签移除类
that.removeClass();
this.className = 'liactive';
// (2) 切换标签对应的标签页
// 得到当前li对应的索引号 找到对应的section
that.tabSections[this.index].className = 'conactive';
}
// 移除类
removeClass() {
for (var i = 0; i < this.tabLis.length; i++) {
this.tabLis[i].className = '';
this.tabSections[i].className = '';
}
}
添加功能
点击 + 按钮,可以添加标签栏选项和对应的标签页。添加的元素实现的是默认被选中的状态。
但是这里需要注意的一点是,当我们创建一个新的标签选项时,新标签是没有切换效果的。
在最开始我们写程序时,获取元素是不出意外的话第一反应会在Tab类中的构造函数中获取,这就为创建的新元素没有效果埋下了隐患。
因为,在构造器中获取元素获取的是我们最开始已经有的,但是当我再去创建新元素时,此时的新元素并没有被构造器获取,所以新元素会缺失相应的事件,这里缺失的是元素的切换效果。
解决方法:用一个updateNode的方法用来获取需要动态创建的元素。
还有一个新知识点,insertAdjacentHTML(position,text)方法的使用。之前我们添加元素的思路是先创建元素然后添加元素,因为我们现在创建的元素里面包含的内容元素太多了。之前的方法使用起来比较麻烦,所以这里介绍一种新的方法 insertAdjacentHTML(positon,text)
position 是新创建元素添加的位置 字符串型参数
a. beforebegin 添加在元素自身的前面
b. afterbegin 添加在元素内容子元素的最前面
c. beforeend 添加在元素内容子元素的最后面
d. afterbend 添加在元素自身的后面
text 是字符串类型的代码
addTab() {
// this指向 + 号
that.removeClass();
var random = Math.floor(Math.random() * 100000);
// 创建li和section元素
var li = '<li class="liactive"><span>新标签页</span><span class="iconfont icon-guanbi"></span></li>';
var section = '<section class="conactive">新标签页内容' + random + '</section>';
// 添加元素
that.liParentUl.insertAdjacentHTML("beforeend", li);
that.sectionParentDiv.insertAdjacentHTML("beforeend", section);
// 到这里有两个bug
// 点击添加后,新添加的标签没有切换效果
// 是因为,li和section元素是先获取的,在添加后获取的元素中并没有新增的元素,所以没有效果
// 解决方法就是,要更新元素的获取,绑定事件 —— 写一个方法更新元素的方法
that.init(); // 重新初始化一下
}
上面的代码,第一行就调用了一下移除类的方法,这样的目的是为了当添加元素时有默认选中的选项显示的内容区与新建的内容区同时显示的效果。如下图。
这里的核心点就是 —— 写一个方法提取需要动态创建的元素
// 更新元素
updateNode() {
this.tabLis = this.main.querySelectorAll(".fisrstnav ul li");
this.tabSections = this.main.querySelectorAll(".tabscon section");
// x号
this.delBtn = this.main.querySelectorAll(".fisrstnav .icon-guanbi");
this.tabLisSpans = this.main.querySelectorAll(".fisrstnav li span:first-child");
}
删除功能
点击 x 删除与之对应的标签栏选项和标签页。
点击的 x 是标签选项的子元素,但是删除的确实点击元素的父元素。通过点击的元素找到点击元素的父元素,再去确定此时父元素的索引号,这样就能找到对应的标签栏选项和标签页。
代码中有用事件对象阻止了事件冒泡行为,因为在点击 x 时不需要有切换效果。如果不阻止事件冒泡,点击 x 时会有切换效果。
// 删除节点功能
removeTab(e) {
// this指向x号
// 首先去掉切换功能 点击时只需要删除标签和标签页不需要切换效果
e.stopPropagation(); // 阻止事件冒泡 防止span的父元素li点击事件的发生
// 获得当前x号父元素li的索引 删除对应的li和section
var index = this.parentNode.index;
console.log(index);
that.tabLis[index].remove();
that.tabSections[index].remove();
that.init(); // 因为实现了删除操作,我们需要重新初始化 绑定事件
}
做到这里,还是有bug的存在。当我们点击删除那个被选中的元素后,页面中就没有被选中的选项了,这样的效果给用户的体验非常不好。解决方案就是 —— 让被选中的选项的前一项被选中。当我们点击新建按钮创建元素后,因为新建的元素是被选中的状态,当我们点击没有选中的选项时,被选中的选项是保持不变的。当选中的的选项是第一项时,点击删除后,依然会出现没有选项选中的情况,所以要做的是若第一项被选中再点击删除时,让第一项选中。
总结三个bug:
第一:当我们删除选中的标签时,就没有选中的标签
第二:选中的标签是第一项,并且同时要删除第一项 此时没有选中的标签
第三:删除没有选中的标签时,被选中的标签保持不变
解决方案:
// 解三:被选中时,保持不变 返回空
if (document.querySelector(".liactive")) return;
// 解二:第一项的后面一项被选中
if (index == 0) {
that.tabLis[0].click();
}
// 解一:这里我们要的效果是,当删除选中的li时,让被删除li的前一项被选中
index--;
// 逻辑短路的应用,当上一个标签存在时执行click()事件 手动调用 li点击事件
that.tabLis[index] && that.tabLis[index].click();
再来看看实现的效果
修改功能
双击标签栏选项的文字部分或者标签页的内容部分,能够修改内容。
双击时,li中的span会创建一个文本框,并把li中的内容赋值给文本框。当文本框失去焦点或者按下回车键时,文本框修改后的值再赋值给li即可。
// 修改功能
exidTabContent() {
// this指向li中第一个span标签
var str = this.innerHTML;
// console.log(str);
this.innerHTML = '<input type="text">';
var input = this.children[0];
input.value = str;
input.select(); // 表单文字别选中
// 失去焦点时,更改内容 表单消失
input.addEventListener("blur", function () {
this.parentNode.innerHTML = this.value;
})
// 按enter键内容更改
input.addEventListener("keyup", function (e) {
if (e.keyCode === 13) {
// this.parentNode.innerHTML = this.value;
this.blur();
}
})
}
这里的注意点就是使用事件对象获取用户按下的是否为回车键。keyup事件和keyCode方法的使用。看下修改内容的效果。
初始化函数和构造函数的内容
构造器只传了一个参数,就是元素的ID。初始化函数存放的都是事件的绑定,同时还要获取更新的元素。
constructor(id) {
that = this;
// 获取元素
this.main = document.querySelector(id);
// li的父元素 ul
this.liParentUl = this.main.querySelector(".fisrstnav ul");
// section的父元素 div
this.sectionParentDiv = this.main.querySelector(".tabscon");
// +
this.addBtn = this.main.querySelector(".fisrstnav .tabadd span");
this.init();
}
// 初始化操作:用来让相关的元素绑定事件的
init() {
this.updateNode();
this.addBtn.addEventListener("click", this.addTab);
for (var i = 0; i < this.tabLis.length; i++) {
// 获得当前li的索引号
this.tabLis[i].index = i;
this.tabLis[i].addEventListener("click", this.toggleTab);
this.delBtn[i].addEventListener("click", this.removeTab);
this.tabLisSpans[i].addEventListener("dblclick", this.exidTabContent);
this.tabSections[i].addEventListener("dblclick", this.exidTabContent)
}
}
全部代码
案例结束了。
补上全部的JavaScript代码。
悄悄告诉你,里面有很详细的注释喔。
// 面向对象编程
// 抽取tab栏切换的功能
// 需求:
// 点击标签可以切换效果 切换就是标签对应标签页
// 点击 + 号 可以添加新的标签和标签页
// 点击 x 号 可以删除标签及对应的标签页
// 双击 可以修改标签和标签页的内容 生成一个文本框 按回车或者失去焦点时内容更改成功
// 简单来说就是 增删改查
window.onload = function () {
var that;
// 创建tab类
class Tab {
constructor(id) {
that = this;
// 获取元素
this.main = document.querySelector(id);
// li的父元素 ul
this.liParentUl = this.main.querySelector(".fisrstnav ul");
// section的父元素 div
this.sectionParentDiv = this.main.querySelector(".tabscon");
// +
this.addBtn = this.main.querySelector(".fisrstnav .tabadd span");
this.init();
}
// 初始化操作:用来让相关的元素绑定事件的
init() {
this.updateNode();
this.addBtn.addEventListener("click", this.addTab);
for (var i = 0; i < this.tabLis.length; i++) {
// 获得当前li的索引号
this.tabLis[i].index = i;
this.tabLis[i].addEventListener("click", this.toggleTab);
this.delBtn[i].addEventListener("click", this.removeTab);
this.tabLisSpans[i].addEventListener("dblclick", this.exidTabContent);
this.tabSections[i].addEventListener("dblclick", this.exidTabContent)
}
}
// 更新元素
updateNode() {
this.tabLis = this.main.querySelectorAll(".fisrstnav ul li");
this.tabSections = this.main.querySelectorAll(".tabscon section");
// x号
this.delBtn = this.main.querySelectorAll(".fisrstnav .icon-guanbi");
this.tabLisSpans = this.main.querySelectorAll(".fisrstnav li span:first-child");
}
// 切换功能
toggleTab() {
// 方法中this指向方法调用者 这里是 —— li标签
// this.index 当前li的索引号
// (1) 切换类 排他思想:干掉所有人留下自己
// this指向li,li中没有li标签的集合 只有实例对象也就是类中的构造函数的this才有li的集合
// 其他li标签移除类
that.removeClass();
this.className = 'liactive';
// (2) 切换标签对应的标签页
// 得到当前li对应的索引号 找到对应的section
that.tabSections[this.index].className = 'conactive';
}
// 移除类
removeClass() {
for (var i = 0; i < this.tabLis.length; i++) {
this.tabLis[i].className = '';
this.tabSections[i].className = '';
}
}
// 增加 +
// 之前我们添加元素的思路是先创建元素然后添加元素,因为我们现在创建的元素里面包含的内容太多了
// 之前的方法使用起来比较麻烦,所以这里介绍一种新的方法 insertAdjacentHTML(positon,text)
// position 是新创建元素添加的位置 字符串型参数
// beforebegin 添加在元素自身的前面
// afterbegin 添加在元素内容子元素的最前面
// beforeend 添加在元素内容子元素的最后面
// afterbend 添加在元素自身的后面
// text 是字符串类型的代码
// 增加功能
addTab() {
// this指向 + 号
that.removeClass();
var random = Math.floor(Math.random() * 100000);
// 创建li和section元素
var li = '<li class="liactive"><span>新标签页</span><span class="iconfont icon-guanbi"></span></li>';
var section = '<section class="conactive">新标签页内容' + random + '</section>';
// 添加元素
that.liParentUl.insertAdjacentHTML("beforeend", li);
that.sectionParentDiv.insertAdjacentHTML("beforeend", section);
// 到这里有两个bug
// 点击添加后,新添加的标签没有切换效果
// 是因为,li和section元素是先获取的,在添加后获取的元素中并没有新增的元素,所以没有效果
// 解决方法就是,要更新元素的获取,绑定事件 —— 写一个方法更新元素的方法
that.init();
}
// 删除节点功能
removeTab(e) {
// this指向x号
// 首先去掉切换功能 点击时只需要删除标签和标签页不需要切换效果
e.stopPropagation(); // 阻止事件冒泡 防止span的父元素li点击事件的发生
// 获得当前x号父元素li的索引 删除对应的li和section
var index = this.parentNode.index;
console.log(index);
that.tabLis[index].remove();
that.tabSections[index].remove();
that.init(); // 因为实现了删除操作,我们需要重新初始化 绑定事件
// 这里有bug,
// 第一:当我们删除选中的标签时,就没有选中的标签
// 第二:选中的标签是第一项,并且同时要删除第一项 此时没有选中的标签
// 第三:删除没有选中的标签时,被选中的标签保持不变
// 解三:被选中时,保持不变 返回空
if (document.querySelector(".liactive")) return;
// 解二:第一项的后面一项被选中
if (index == 0) {
that.tabLis[0].click();
}
// 解一:这里我们要的效果是,当删除选中的li时,让被删除li的前一项被选中
index--;
// 逻辑短路的应用,当上一个标签存在时执行click()事件 手动调用 li点击事件
that.tabLis[index] && that.tabLis[index].click();
}
// 修改功能
exidTabContent() {
// this指向li中第一个span标签
var str = this.innerHTML;
// console.log(str);
this.innerHTML = '<input type="text">';
var input = this.children[0];
input.value = str;
input.select(); // 表单文字别选中
// 失去焦点时,更改内容 表单消失
input.addEventListener("blur", function () {
this.parentNode.innerHTML = this.value;
})
// 按enter键内容更改
input.addEventListener("keyup", function (e) {
if (e.keyCode === 13) {
// this.parentNode.innerHTML = this.value;
this.blur();
}
})
}
}
var tab = new Tab("#tab");
}
结构
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>面向对象 Tab标签页</title>
<link rel="stylesheet" href="./css/tab.css">
<link rel="stylesheet" href="./css/style.css">
</head>
<body>
<main>
<h4>
Js 面向对象 动态添加标签页
</h4>
<div class="tabsbox" id="tab">
<!-- tab 标签 -->
<nav class="fisrstnav">
<ul>
<li class="liactive"><span>手机部分</span><span class="iconfont icon-guanbi"></span></li>
<li><span>电脑部分</span><span class="iconfont icon-guanbi"></span></li>
<li><span>相机部分</span><span class="iconfont icon-guanbi"></span></li>
</ul>
<div class="tabadd">
<span>+</span>
</div>
</nav>
<!-- tab 内容 -->
<div class="tabscon">
<section class="conactive">手机。。。。。。。</section>
<section>电脑。。。。。。</section>
<section>相机。。。。。。</section>
</div>
</div>
</main>
<script src="js/tab.js"></script>
</body>
</html>
css
* {
margin: 0;
padding: 0;
}
@font-face {
font-family: "iconfont";
src: url('./iconfont/iconfont.eot?t=1553960438096');
/* IE9 */
src: url('./iconfont/iconfont.eot?t=1553960438096#iefix') format('embedded-opentype'),
/* IE6-IE8 */
url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAK4AAsAAAAABmwAAAJrAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCCcAp4fwE2AiQDCAsGAAQgBYRtBzAbpQXIrrApw71oi3CCOyzEy8RvE4yIN8TD036/zp03qCYRjaJZNBFFS/gREoRGipQKofjuNrb+9XbTqrmXcqWzfTRDqFqWkhAJzYToaE6LQ7Q30CirRqSKMnj58DdIdrNAdhoTQJa5VGfLrtiAy+lPoAcZdUC57UljTR4TMAo4oL0xiqwYG8YueIHPCdTqYajty/t+bUpmrwvEnUK42lQhLMssVy1UNhzN4kmF6vSQVvMY/T5+HEU1SUXBbti7uBBrx++cgqJULp0GhAgBna5AgSkgE0eN6R1NwTitNt0yAI5VG7wr/8AljmoX7K+zq+tBF1Q8k9JTPWp1AjnJDgCzmM3bU0V31dsvV3M2eC6fHjaGfX/qS7U5Gr58vj6uD0bgxudyrV/OtHHyP+NZnpO1txbktjdY+3FB61+7nxeOzq8niGYnRwT3v3aZxeXf6rrNxl5//49WlEtZUUL1Pj3Bv1EO7MuG2namrCkbvcnApLUJtWpRhv2tzlRLx43kQ7WO2/FW6c5QqDZEZnYKFeosoVK1NdSa5E/XaVM1Ra7BhAEQmk0kjV5QaLbIzG5U6HRRqTkK1DqJtivrjMT1zJaNnIsihAiyQE3JdbszcW0Xiadzdl4d8UO0HSUGNDNXzl2hifYSO5pPjrorgdjUAAavoa5TKDZVUXD3kuuOOzh70fShvUiN2owtNsRxIREIIiATUCYpGO2aqXy/CxEeHcfuaKrLDiGbQ5kcEMsNIK8M5qCmR3mn8RFHOpcECBtlAAwWIZ2OAqV5kQoJXHvShORYBzrDZKhhb3uT8QPlrA3bmsKZV6i89DiTV2o1AAAA') format('woff2'),
url('./iconfont/iconfont.woff?t=1553960438096') format('woff'),
url('./iconfont/iconfont.ttf?t=1553960438096') format('truetype'),
/* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
url('./iconfont/iconfont.svg?t=1553960438096#iconfont') format('svg');
/* iOS 4.1- */
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-guanbi:before {
content: "\e676";
}
ul li {
list-style: none;
}
main {
width: 960px;
height: 500px;
border-radius: 10px;
margin: 50px auto;
}
main h4 {
height: 100px;
line-height: 100px;
text-align: center;
}
.tabsbox {
width: 900px;
margin: 0 auto;
height: 400px;
border: 1px solid lightsalmon;
position: relative;
}
nav ul {
overflow: hidden;
}
nav ul li {
float: left;
width: 100px;
height: 50px;
line-height: 50px;
text-align: center;
border-right: 1px solid #ccc;
position: relative;
}
nav ul li.liactive {
border-bottom: 2px solid #fff;
z-index: 9;
}
#tab input {
width: 80%;
height: 60%;
}
nav ul li span:last-child {
position: absolute;
user-select: none;
font-size: 12px;
top: -18px;
right: 0;
display: inline-block;
height: 20px;
}
.tabadd {
position: absolute;
/* width: 100px; */
top: 0;
right: 0;
}
.tabadd span {
display: block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
border: 1px solid #ccc;
float: right;
margin: 10px;
user-select: none;
}
.tabscon {
width: 100%;
height: 300px;
position: absolute;
padding: 30px;
top: 50px;
left: 0px;
box-sizing: border-box;
border-top: 1px solid #ccc;
}
.tabscon section,
.tabscon section.conactive {
display: none;
width: 100%;
height: 100%;
}
.tabscon section.conactive {
display: block;
}