摘要:是一個組件庫目前擁有的組件語法編寫,無依賴原生模塊化,以上支持,請開啟靜態服務器預覽效果,靜態服務器傳送門采用變量配置樣式辛苦造輪子,歡迎來倉庫四月份找工作,求內推,坐標深圳寫在前面去年年底項目中嘗試著寫過一個分頁的組件,然后就有了寫的想法
QingUI是一個UI組件庫寫在前面
目前擁有的組件:DatePicker, TimePicker, Paginator, Tree, Cascader, Checkbox, Radio, Switch, InputNumber, Input
ES6語法編寫,無依賴
原生模塊化,Chrome63以上支持,請開啟靜態服務器預覽效果,靜態服務器傳送門
采用CSS變量配置樣式
辛苦造輪子,歡迎來github倉庫star:QingUI
四月份找工作,求內推,坐標深圳
去年年底項目中嘗試著寫過一個分頁的Angular組件,然后就有了寫QingUI的想法
過程還是非常有意思的
接下來我會用幾篇文章分別介紹每個組件的大概思路,請大家耐心等待
這一篇介紹Tree樹結構
最重要的,求star,求內推
repo: QingUI少廢話,先上圖 渲染
作為樹組件,想都不用想,肯定用遞歸
但是QingUI的組件統一用一個div.qing qing-component包裹,所以用div把遞歸函數包起來
const tpl = `${this.renderTrunk(this.data)}`;
然后我們再來看renderTrunk函數
首先簡要介紹一下標簽結構
trunk可以理解為樹的一項,是樹干
fruit是包裹信息用的,里面有三角箭頭,checkbox和label
如果它有子項,則fruit后面再加一個sub,sub里面當然又是一個或多個trunk
indentTree配置項里有一個indent,指的是所有的子項相對于父項縮進的距離
頂層項沒有父項,所以不需要縮進
于是我們就需要在遞歸的時候判斷,現在是頂層項還是子項
這個簡單
// data是遞歸函數傳進來的 const inner = data !== this.data;
于是我們的縮進也解決了
const marginLeft = `${inner ? `style="margin-left: ${this.indent}px;"` : ""}`;expand
Tree配置項里還有一個expand,它有三個選項:none、all和first
它決定的是初始加載的時候子項是全部閉合、全部展開還是只有第一個頂層項展開
子項是否展開是如何控制的呢?
當然是通過高度,height: 0; overflow: hidden;的時候閉合,height: auto; overflow: hidden;的時候展開
于是就成了下面這樣
expand等于first時,意思是只有頂層項的第一項才會展開
這里有一個小技巧
expand我們給了三個可選項,但是萬一用戶偏偏傳個hello進來呢?
反正none是默認項,條件判斷的時候,我只認all和first,除此之外都是默認配置
這屬于對錯誤參數的靜默處理,我也不告訴你傳錯了,但是你也別想要任何效果
這樣就不需要對參數多做一個校驗了
for (let i = 0; i < data.length; i++) { let arrowTpl, subTpl; if (this.expand === "all") { arrowTpl = ""; subTpl = ""; } else if (this.expand === "first") { const boo = !inner && i === 0; arrowTpl = boo ? "" : ""; subTpl = ``; } else { arrowTpl = ""; subTpl = ""; } } fruitfruit這里,如果沒有子項,是不需要三角箭頭的
另外不需要checkbox的話就不顯示checkbox,只作為樹結構展示用
tpl += `${item.sub ? arrowTpl : ""} ${this.checkable ? "" : ""} ${item.label}`;最后,如果有子項,別忘了遞歸
if (item.sub) { tpl += subTpl; tpl += this.renderTrunk(item.sub); }整個模板部分大概就是這樣子的
我們看一下全貌:
render() { const tpl = `${this.renderTrunk(this.data)}`; this.$mount.innerHTML = tpl; } renderTrunk(data) { const inner = data !== this.data; const marginLeft = `${inner ? `style="margin-left: ${this.indent}px;"` : ""}`; let tpl = ""; for (let i = 0; i < data.length; i++) { const item = data[i]; tpl += ``; let arrowTpl, subTpl; if (this.expand === "all") { arrowTpl = ""; subTpl = ""; } else if (this.expand === "first") { const boo = !inner && i === 0; arrowTpl = boo ? "" : ""; subTpl = ``; } else { arrowTpl = ""; subTpl = ""; } return tpl; } 映射"; } tpl += `"; } tpl += "${item.sub ? arrowTpl : ""} ${this.checkable ? "" : ""} ${item.label}`; if (item.sub) { tpl += subTpl; tpl += this.renderTrunk(item.sub); tpl += "當我需要點擊checkbox導致data的某個對象的checked屬性變更時,我的做法是維護一套DOM結構的映射
映射跟DOM結構是一一對應的,跟data也是一一對應的
它可以很好的作為橋梁同步數據
為什么不直接操作data呢?
data是用戶傳進來的結構化數據,后面還需要通過回調傳回去的,我們不應該修改用戶的數據結構
映射需要保存些什么?
保存對應的DOM節點
保存checked屬性的值
保存當前節點在樹結構中的位置
queue一個小問題,如何保存當前節點在樹結構中的位置呢?
當我們說在樹結構中的位置的時候,我們想知道的是當前節點在第幾級,以及當前節點在該級的第幾個
我用queue關鍵字來保存位置信息,可能不是非常語義化,你來打我呀!
舉個栗子
假如我要知道宋佳的位置,queue的值為"111",表示它是一個三級子項,它的爺爺第一級是第二項,它的爹爹第二級也是第二項,它自己也是第二項
再舉個栗子,倪妮的queue就是"10"
有兩個例子,應該不會有理解上的偏差了吧
data = [ { label: "霍思燕", }, { label: "江疏影", sub: [ { label: "倪妮", }, { label: "高圓圓", sub: [ { label: "張雨綺", }, { label: "宋佳", }, ], }, ], }, ];ES6有一個新特性,如果對象的鍵和值變量是一樣的,那么不需要寫冒號,又給你省了不少時間可以用來浪費,開不開心!
buildCbTree(fatherCb, item) { const childCbs = fatherCb.children; for (let i = 0; i < childCbs.length; i++) { const fruit = childCbs[i].firstElementChild; const sub = fruit.nextElementSibling; const cb = fruit.firstElementChild.nextElementSibling; const checked = cb.classList.contains("checked"); const queue = item.queue ? item.queue + String(i) : String(i); let obj = {cb, checked, queue}; if (sub) { obj.sub = []; this.buildCbTree(sub, obj); } item.sub.push(obj); } }它的初始參數是什么呢?
this.$mount.firstElementChild就是開始加的包裹元素,上面有標識QingUI的class,所以它就是根元素
this.buildCbTree(this.$mount.firstElementChild, {sub: this.cbTree});checkbox事件我們先來捋一捋,當我們點擊某一項的checkbox時
它自己只有兩種狀態,要么checked,要么去除checked
如果它有子項,則所有子項以及孫項以及所有的后代項跟隨它的腳步,要么全部checked,要么全部去除checked
但是如果它有父項(它是有可能沒有父項的,如果自己是頂層項的話),需要根據自己的兄弟來決定父項的checked屬性,依照這個邏輯往上遞歸
如果自己和兄弟都去除了checked,則父項去除checked
如果自己和兄弟都checked,則父項checked
如果自己或者兄弟至少有一個checked,則父項somechecked
所以每一次點擊都要做三條線的處理
$cb.addEventListener("click", function(event) { event.stopPropagation(); const checked = !this.classList.contains("checked"); // cb事件 checked ? self.$cbEvent(item, "all") : self.$cbEvent(item, "none"); // cb子代事件 checked ? self.childCbsEvent($sub, "all") : self.childCbsEvent($sub, "none"); // cb父代事件 self.fatherCbsEvent(queue); });checkbox自代事件自己雖然只有兩種狀態,但我們可以把它作為抽象函數,因為無論是父項、子項還是自己,都是checkbox而已,只不過是一個還是多個
所以我們給它三種狀態,作為action參數傳進來
$cbEvent(item, action) { const $cb = item.cb; const CL = $cb.classList; switch (action) { case "all": item.checked = true; CL.contains("somechecked") ? CL.remove("somechecked") : ""; CL.add("checked"); break; case "some": item.checked = false; CL.contains("checked") ? CL.remove("checked") : ""; CL.add("somechecked"); break; case "none": item.checked = false; CL.contains("somechecked") ? CL.remove("somechecked") : ""; CL.contains("checked") ? CL.remove("checked") : ""; break; } }checkbox子代事件子代事件也只有兩種狀態,跟自身同步
只需要遞歸就好了
需要提醒的是,這里所有的操作都是針對映射cbTree進行的
childCbsEvent($sub, action) { if (!$sub) { return; } const self = this; function recursive($sub) { for (const $item of $sub) { self.$cbEvent($item, action); if ($item.sub) { recursive($item.sub); } } } recursive($sub); }checkbox父代事件要決定父項的狀態,先要找到自己的兄弟,家族財產怎么分割,還得兄弟一起商量
兄弟怎么找呢?
先要找到包裹自己和兄弟的那個實體,你才能遍歷呀,這時候queue就派上用場了
現在我知道實體的位置,只需要this.cbTree[a][b][c]這么找下去不就完了!
findFatherItem(queue) { const n = queue.length - 1; // 頂級item沒有父item if (n === 0) { return; } let fatherItem = this.cbTree; for (let i = 0; i < n; i++) { const char = queue.charAt(i); if (i < n - 1) { fatherItem = fatherItem[char].sub; } else { fatherItem = fatherItem[char]; } } return fatherItem; }找到了實體,接下來遍歷就行了
當然,這里面的兄弟實際上是包括自己的
因為DOM操作比較慢,我原以為這里做計算的時候,有可能自身的class變更還沒生效呢
因為在我的印象里,DOM操作好像是異步的,其實不是的,所以把自己納入進來是沒問題的
findSiblingCbs($sub) { let $siblingCbs = []; for (const $item of $sub) { $siblingCbs.push($item.cb); } return $siblingCbs; }最后就是根據最初的邏輯把結果丟進那個抽象函數里
當然,記住樹結構里一切都是遞歸的
fatherCbsEvent(queue) { const fatherItem = this.findFatherItem(queue); if (!fatherItem) { return; } const $siblingCbs = this.findSiblingCbs(fatherItem.sub); let allFlag = true; let noneFlag = true; for (const $item of $siblingCbs) { const cl = $item.classList; if (!cl.contains("checked")) { allFlag = false; } if (cl.contains("checked") || cl.contains("somechecked")) { noneFlag = false; } // flag全都已經變化,退出循環 if (!allFlag && !noneFlag) { break; } } if (allFlag) { this.$cbEvent(fatherItem, "all"); } else if (noneFlag) { this.$cbEvent(fatherItem, "none"); } else { this.$cbEvent(fatherItem, "some"); } this.fatherCbsEvent(fatherItem.queue); }更新data不知道你們意識到沒有,上面做的所有事,僅僅是改變了DOM樣式和映射cbTree
用戶想在回調里拿到的是一開始傳進來的data呀
不過這好辦,因為cbTree和data的結構是一毛一樣的
只需要一個遞歸就解決問題
最終我們得到的data就是checked屬性已經產生變化的data
updateData(tree, data) { for (let i = 0; i < tree.length; i++) { const ti = tree[i]; const di = data[i]; if (ti.checked) { di.checked = true; } else { di.checked = false; } if (ti.sub) { this.updateData(ti.sub, di.sub); } } }高度動畫前面講expand配置項的時候,我們提到過高度為0或是auto的問題
初始是什么沒關系
我們想讓展開或收起的過程更加平滑一點
然而我們知道height: auto;是無法產生CSS動畫的,CSS動畫必須得有一個確定的數值
那么怎么辦呢?
有人說可以用max-height屬性,給max-height一個很大的固定的值,就可以產生動畫了
我覺得這樣,動畫效果不會好,而且很不優雅
試想一下,比如說現在高度是0,我能不能把高度改成auto,再通過JS計算出真實的高度,再把高度改成0
現在我們手里有它本來的確定的高度值,就可實現動畫了
最后又把它的高度改成auto,因為它有可能有子項,固定高度會出問題的
有點繞,看代碼
subHeightToggle($sub) { let h = $sub.getBoundingClientRect().height; if (h > 0) { // 從auto變成具體的值 $sub.style.height = `${h}px`; setTimeout(() => { $sub.style.height = "0px"; }, 0); } else { $sub.style.height = "auto"; h = $sub.getBoundingClientRect().height; $sub.style.height = "0px"; setTimeout(() => { $sub.style.height = `${h}px`; }, 0); // 動畫完成變成auto setTimeout(() => { $sub.style.height = "auto"; }, 200); } }我們老是說盡量不要操作DOM,其實這是要看付出回報比的,如果能夠實現平滑效果,實現方式又足夠優雅
咱們的計算機沒你說的那么脆弱
寫在后面Tree比較核心的邏輯就在這里了
樹結構算是QingUI里比較難的組件了,其中的實現方式我也試過好幾版
越是難,其實越有成就感
下一篇文章介紹Cascader,敬請期待
最后,求star,求內推
repo: QingUI文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93736.html
相關文章
徒手擼UI之DatePicker
摘要:是一個組件庫目前擁有的組件語法編寫,無依賴原生模塊化,以上支持,請開啟靜態服務器預覽效果,靜態服務器傳送門采用變量配置樣式辛苦造輪子,歡迎來倉庫四月份找工作,求內推,坐標深圳寫在前面去年年底項目中嘗試著寫過一個分頁的組件,然后就有了寫的想法 QingUI是一個UI組件庫目前擁有的組件:DatePicker, TimePicker, Paginator, Tree, Cascader, ...
徒手擼UI之Paginator
摘要:是一個組件庫目前擁有的組件語法編寫,無依賴原生模塊化,以上支持,請開啟靜態服務器預覽效果,靜態服務器傳送門采用變量配置樣式辛苦造輪子,歡迎來倉庫四月份找工作,求內推,坐標深圳寫在前面去年年底項目中嘗試著寫過一個分頁的組件,然后就有了寫的想法 QingUI是一個UI組件庫目前擁有的組件:DatePicker, TimePicker, Paginator, Tree, Cascader, ...
徒手擼UI之TimePicker
摘要:是一個組件庫目前擁有的組件語法編寫,無依賴原生模塊化,以上支持,請開啟靜態服務器預覽效果,靜態服務器傳送門采用變量配置樣式辛苦造輪子,歡迎來倉庫四月份找工作,求內推,坐標深圳寫在前面去年年底項目中嘗試著寫過一個分頁的組件,然后就有了寫的想法 QingUI是一個UI組件庫目前擁有的組件:DatePicker, TimePicker, Paginator, Tree, Cascader, ...
徒手擼UI之Cascader
摘要:但是如果一剎那我不想選江疏影了,我想選張雨綺因為胸大,首先我要從霍思燕換到高圓圓,然后轉到張雨綺,選中展示出來,這時候就要先刪除霍思燕,然后把高圓圓和張雨綺進來。 QingUI是一個UI組件庫目前擁有的組件:DatePicker, TimePicker, Paginator, Tree, Cascader, Checkbox, Radio, Switch, InputNumber, I...
徒手擼框架--高并發環境下的請求合并
摘要:我們就可以將這些請求合并,達到一定數量我們統一提交。總結一個比較生動的例子給大家講解了一些多線程的具體運用。學習多線程應該多思考多動手,才會有比較好的效果。地址徒手擼框架系列文章地址徒手擼框架實現徒手擼框架實現 原文地址:https://www.xilidou.com/2018/01/22/merge-request/ 在高并發系統中,我們經常遇到這樣的需求:系統產生大量的請求,但是這...
發表評論
0條評論
2i18ns
男|高級講師
TA的文章
閱讀更多
Go 1.18 將支持泛型,Go 團隊技術 leader 有話說
閱讀 3551·2021-11-08 13:15
CSS3 選擇器
閱讀 2107·2019-08-30 14:20
nec的reset文件摘要
閱讀 1386·2019-08-28 18:08
CSS基礎教程筆記
閱讀 977·2019-08-28 17:51
前端—初級階段5(16-20)
閱讀 1484·2019-08-26 18:26
Canvas 文字碰撞檢測并抽稀
閱讀 2989·2019-08-26 13:56
WebSocket 協議
閱讀 1484·2019-08-26 11:46
關于git常用命令
閱讀 2586·2019-08-23 14:22
閱讀需要支付1元查看