昨天呢,介紹了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;
}